Golang 函数传参使用切片而不使用数组为什么?
Posted 知其黑、受其白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang 函数传参使用切片而不使用数组为什么?相关的知识,希望对你有一定的参考价值。
阅读目录
数组与切片的区别
Go里面的数组是值类型,切片是引用类型。
值类型的对象在做为实参传给函数时,形参是实参的另外拷贝的一份数据,对形参的修改不会影响函数外实参的值。
因此在如下例子中两次打印的指针地址是不一样的。
数组是值类型
package main
import "fmt"
func arrayTest(x [2]int)
fmt.Printf("%p \\n", &x)
// 0xc00000e0d0
func main()
arrayA := [2]int1, 2
fmt.Printf("%p \\n", &arrayA)
// 0xc00000e100
arrayTest(arrayA)
假想每次传参都用数组,那么每次数组都要被复制一遍。如果数组大小有 100万,在64位机器上就需要花费大约 800W 字节,即 8MB 内存。这样会消耗掉大量的内存。
切片是引用类型
引用类型,则没有这个拷贝的过程,实参与形参指向的是同一块内存地址。
package main
import "fmt"
func sliceTest(x []int)
fmt.Printf("%p \\n", x)
// 0x3c9580
func main()
sliceA := make([]int, 0)
fmt.Printf("%p \\n", sliceA)
// 0x3c9580
sliceTest(sliceA)
由此我们可以得出结论:
把第一个大数组传递给函数会消耗很多内存,采用切片的方式传参可以避免上述问题。切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。
那么你肯定要问了,数组指针也是引用类型啊,也不一定要用切片吧?
确实,传递数组指针是可以避免对值进行拷贝的内存浪费。
切片函数传递是引用传值示例
package main
import "fmt"
func modify(arr []int)
arr[0] = 999
func main()
arr := []int1, 2, 3, 4, 5
modify(arr)
fmt.Println(arr)
PS E:\\TEXT\\test_go\\test> go run .\\main.go
[999 2 3 4 5]
PS E:\\TEXT\\test_go\\test>
package main
import "fmt"
func sliceTest(x []int)
x[1] = 888
func main()
sliceA := make([]int, 0)
sliceA = append(sliceA, 1, 2, 3)
sliceTest(sliceA)
fmt.Println(sliceA)
PS E:\\TEXT\\test_go\\test> go run .\\main.go
[1 888 3]
PS E:\\TEXT\\test_go\\test>
数组对比切片有哪些优势?
1 编译检查越界
由于数组在声明后,长度就是固定的,因此在编译的时候编译器可以检查在索引取值的时候,是否有越界。
func main()
array := [2]int
array[2] = 2
//invalid array index 2 (out of bounds for 2-element array)
而切片的长度只有运行时才能知晓,编译器无法检查。
2 长度是类型的一部分
在声明一个数组的类型时,需要指明两点:
- 元素的类型
- 元素的个数
var array [2]int
因此长度是数组类型的一部分,两个元素类型相同,但可包含的元素个数不同的数组,属于两个类型。
func main()
var array1 [2]int
var array2 [2]int
var array3 [3]int
fmt.Println(reflect.TypeOf(array1) == reflect.TypeOf(array2)) // true
fmt.Println(reflect.TypeOf(array1) == reflect.TypeOf(array3)) // false
基于这个特点,可以用它来达到一些合法性校验的目的,例如 IPv4 的地址可以声明为 [4]byte,符合该类型的数组就是合法的 ip,反之则不合法。
3 数组可以比较
类型相同的两个数组可以进行比较。
func main()
array1 := [2]int1,2
array2 := [2]int1,2
array3 := [2]int2,1
fmt.Println(array1 == array2) // true
fmt.Println(array1 == array3) // false
类型不同(长度不同)的数组 和 切片均不行。
可比较这一特性,决定了数组也可以用来当 map 的 key 使用。
func main()
array1 := [2]int1,2
dict := make(map[[2]int]string)
dict[array1] = "hello"
fmt.Println(dict) // map[[1 2]:hello]
引用类型与指针,有什么不同?
切片是一个引用类型,将它作为参数传入函数后,你在函数里对数据作变更是会实时反映到实参切片的。
func foo(s []int)
s[0] = 666
func main()
slice := []int1,2
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [666 2]
此时切片这一引用类型,是不是有点像指针的效果?是的。
但它又和指针不一样,这一点主要体现在:
在形参中所作的操作并不一定都会反映在实参上。
还是以切片为例,我在形参上对切片进行扩容,发现形参扩容后,实参并没有发生改变。
func foo(s []int)
s = append(s, 666)
func main()
slice := []int1,2
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [1 2]
这是为什么呢?
这是因为当你对一个切片 append 的时候,它会做这些事情:
- 新建一个新的切片 slice2,其实长度与 slice1 一样,但容量是 slice1 的两倍,此时 slice2 底层指向的匿名数组和 slice1 不是同一个。
- 将 slice1 底层的数组的元素,一个一个的拷贝给 slice2 底层的数组。
- 并把扩容的元素也拷贝到 slice2中。
- 最后把新的 slice2 返回回来,这就是为什么指针不用返回,而 slice.append 也要返回的原因。
append 实现引用传递
package main
import "fmt"
func foo(s *[]int)
*s = append(*s, 666)
func main()
slice := []int1, 2
fmt.Println(slice) // [1 2]
foo(&slice)
fmt.Println(slice) // [1 2 666]
Golang中函数的参数为切片时是传引用还是传值?
对于这个问题,可能会有很多认为是传引用,就比如下面这段代码:
func foo(s []int)
s[0] = 666
func main()
slice := []int1,2
fmt.Println(slice) // [1 2]
foo(slice)
fmt.Println(slice) // [666 2]
如果你不了解 Go 中切片的底层结构,你很可能会误信上面的观点。
但其实不是,Go语言中都是值传递,而不是引用传递,也不是指针传递。
Go 中切片的底层结构是这样的:
type slice struct
array unsafe.Pointer
len int
cap int
当你将切片作为实参传给函数时,函数是会拷贝一份实参的结构和数据,生成另一个切片,实参切片和形参切片,不仅是长度、容量相等,连指向底层数组的指针都是一样的。
通过分别打印实参切片和形参切片的指针地址,就能验证这一观点:
func foo(s []int)
fmt.Printf("%p \\n", &s) // 0xc00000c080
s = append(s, 666)
func main()
slice := []int1,2
fmt.Printf("%p \\n", &slice) // 0xc00000c060
foo(slice)
fmt.Printf("%p \\n", &slice) // 0xc00000c060
package main
import "fmt"
func foo(s *[]int)
fmt.Printf("%p \\n", &s) // 0xc00012c020
*s = append(*s, 666)
func main()
slice := []int1, 2
fmt.Printf("%p \\n", &slice) // 0xc000100048
foo(&slice)
fmt.Printf("%p \\n", &slice) // 0xc000100048
以上是关于Golang 函数传参使用切片而不使用数组为什么?的主要内容,如果未能解决你的问题,请参考以下文章