Go切片在函数中的传递

前言

最近在优化之前实习生的代码,在做control层和db层之间参数传递的时候,老师提出了切片就是引用类型,那么在使用数据库Find功能时只需要传model切片即可,就不需要传切片指针了,但是自己在实验的时候发现直接传切片时无法获取值的,只有传切片指针才行

1
2
3
4
5
6
7
8
//传切片
func GetArticles(articles []model.Article) error {
return db.GetMysqlDB().Order("id desc").Find(&articles).Error
}
//传切片指针
func GetArticles(articles *[]model.Article) error {
return db.GetMysqlDB().Order("id desc").Find(&articles).Error
}

后对此情况对网上的参考资料引用了大部分,做了少许修改的标注

引用参考相关文章

slice的参数

切片和数组是不一样的,数组是值类型,切片时引用类型,则我们相信在函数中传递地址切片,函数中改变,会直接影响到函数外与之对应的切片,现在做一个实验验证下想法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s := []int{1, 2, 3}
SliceTest1(s)
fmt.Println(s)
}

func SliceTest1(num []int) {
if num != nil && len(num) >= 1 {
num[0]=666
return
}
return
}
//[666 2 3]

上面的代码结果验证了我们的想法,但是在函数中打印切片的地址,则会发现和函数外对应的切片地址不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
num := []int{1, 2, 3}
fmt.Printf("main addr is %p\n",&num)
SliceTest1(num)
}

func SliceTest1(num []int) {
fmt.Printf("function slice addr is %p\n",&num)
if num != nil && len(num) >= 1 {
num[0]=666
return
}
return
}
//main addr is 0xc0000044a0
//function slice addr is 0xc0000044e0

此时我们需要先了解下slice的实现

Slice的实现

切片不等同于数组,但他依赖数组实现的,切片是一种复合结构,它由三部分组成的,第一部分是底层数组的指针ptr,第二部分是切片大小len,最后是切片容量cap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func SliceTest2() {
num := [5]int{1, 2, 3, 4, 5}
slice := num[1:4]
slice2 := num[2:5]
fmt.Printf("slice: %v,slice addr %p\n", slice, &slice)
fmt.Printf("slice: %v,slice addr %p\n", slice2, &slice2)
num[2] = 11
fmt.Printf("slice: %v,slice addr %p\n", slice, &slice)
fmt.Printf("slice: %v,slice addr %p", slice2, &slice2)
}
//slice: [2 3 4],slice addr 0xc0000044a0
//slice: [3 4 5],slice addr 0xc0000044c0
//slice: [2 11 4],slice addr 0xc0000044a0
//slice: [11 4 5],slice addr 0xc0000044c0
  • 我们看这个例子,有一个5个元素的数组,slice,slice2分别截取了数组的一部分,并且有共同的一部分,我们现在能明确的就是,两个切片共用一个数组,所以一个改变都改变,还有就是两个数组是两个不同的对象,很明显内存地址不同
  • 从数组中切一块下来形成切片很好理解,有时候我们用make函数创建切片,实际上golang会在底层创建一个匿名的数组。如果从新的slice再切,那么新创建的两个切片都共享这个底层的匿名数组

作为函数切片的参数

回到最开始的问题,当函数的参数是切片的时候,到底是传值还是传引用?从changeSlice函数中打出的参数s的地址,可以看出肯定不是传引用,毕竟引用都是一个地址才对。然而changeSlice函数内改变了s的值,也改变了原始变量slice的值,这个看起来像引用的现象,实际上正是我们前面讨论的切片共享底层数组的实现

即切片传递的时候,传的是数组的值,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
num := []int{0, 1}
SliceTest3(num)
fmt.Println(num)
}

func SliceTest3(num []int) {
num = append(num, 2)
fmt.Println(num)
}
//[0 1 2]
//[0 1]

Slice或者array作为函数参数传递的时候,本质是传值而不是传引用。传值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。(个人感觉称之为传切片可能比传值的表述更准确)。函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础。也就是最外面的原始切片是否改变,取决于函数内的操作和切片本身容量

如果想要传引用的时候怎么办的,则传地址即可

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
num := []int{1, 2}
SliceTest4(&num)
fmt.Println(num)
}

func SliceTest4(num *[]int) {
*num = append(*num, 2)
fmt.Println(*num)
}
//[0 1 2]
//[0 1 2]

总结

golang提供了array和slice两种序列结构。其中array是值类型。slice则是复合类型。slice是基于array实现的。slice的第一个内容为指向数组的指针,然后是其长度和容量。通过array的切片可以切出slice,也可以使用make创建slice,此时golang会生成一个匿名的数组

因为slice依赖其底层的array,修改slice本质是修改array,而array又是有大小限制,当超过slice的容量,即数组越界的时候,需要通过动态规划的方式创建一个新的数组块。把原有的数据复制到新数组,这个新的array则为slice新的底层依赖

数组还是切片,在函数中传递的不是引用,是另外一种值类型,即通过原始变量进行切片传入。函数内的操作即对切片的修改操作了。当然,如果为了修改原始变量,可以指定参数的类型为指针类型。传递的就是slice的内存地址。函数内的操作都是根据内存地址找到变量本身

则在最开始db层操作中,因为传入的肯定是空的model切片,在查找时必定会发生扩容,则此时就需要传入切片指针才行


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!