zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Golang 函数传参使用切片而不使用数组为什么?

数组Golang 函数 为什么 切片 传参 使用
2023-09-11 14:14:56 时间

数组与切片的区别

Go里面的数组是值类型,切片是引用类型。

值类型的对象在做为实参传给函数时,形参是实参的另外拷贝的一份数据,对形参的修改不会影响函数外实参的值。

因此在如下例子中两次打印的指针地址是不一样的。

数组是值类型

package main

import "fmt"

func arrayTest(x [2]int) {
	fmt.Printf("%p \n", &x)
	// 0xc00000e0d0
}

func main() {
	arrayA := [2]int{1, 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 := []int{1, 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]int{1,2}
    array2 := [2]int{1,2}
    array3 := [2]int{2,1}
    fmt.Println(array1 == array2) // true
    fmt.Println(array1 == array3) // false
}

类型不同(长度不同)的数组 和 切片均不行。

可比较这一特性,决定了数组也可以用来当 map 的 key 使用。

func main() {
    array1 := [2]int{1,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 := []int{1,2}
    fmt.Println(slice) // [1 2]
    foo(slice)
    fmt.Println(slice) // [666 2]
}

此时切片这一引用类型,是不是有点像指针的效果?是的。

但它又和指针不一样,这一点主要体现在:
在形参中所作的操作并不一定都会反映在实参上。

还是以切片为例,我在形参上对切片进行扩容,发现形参扩容后,实参并没有发生改变。

func foo(s []int)  {
    s = append(s, 666)
}

func main() {
    slice := []int{1,2}
    fmt.Println(slice) // [1 2]
    foo(slice)
    fmt.Println(slice) // [1 2]
}

这是为什么呢?

这是因为当你对一个切片 append 的时候,它会做这些事情:

  1. 新建一个新的切片 slice2,其实长度与 slice1 一样,但容量是 slice1 的两倍,此时 slice2 底层指向的匿名数组和 slice1 不是同一个。
  2. 将 slice1 底层的数组的元素,一个一个的拷贝给 slice2 底层的数组。
  3. 并把扩容的元素也拷贝到 slice2中。
  4. 最后把新的 slice2 返回回来,这就是为什么指针不用返回,而 slice.append 也要返回的原因。

append 实现引用传递

package main

import "fmt"

func foo(s *[]int) {
	*s = append(*s, 666)
}
func main() {
	slice := []int{1, 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 := []int{1,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 := []int{1,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 := []int{1, 2}
	fmt.Printf("%p \n", &slice) // 0xc000100048
	foo(&slice)
	fmt.Printf("%p \n", &slice) // 0xc000100048
}