golang slice实践以及底层实现


Topic:

  1. slice是传值还是传地址
  2. slice作为函数入参的各种坑
  3. 切片的数据结构
  4. 切片的创建
  5. 切片的扩容
  6. 切片的拷贝
  7. base golang 1.13

slice是传值还是传地址

先看测试例子1:

func testModifyElem(s []int) {
   
	s[0] = 1
	fmt.Printf("s inter address is %p \n", s)
	fmt.Println("s=", s)
}

func main()  {
   
	fmt.Println("------------------------ slice0 -------------------------")
	sa := make([]int, 10)
	fmt.Printf("sa address is %p \n", sa)
	fmt.Println("sa=", sa)
	testModifyElem(sa)
	fmt.Printf("post sa address is %p \n", sa)
	fmt.Println("post sa=", sa)
}

输出结果是:

------------------------ slice0 -------------------------
sa address is 0xc00001e0a0 
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc00001e0a0 
s= [1 0 0 0 0 0 0 0 0 0]
post sa address is 0xc00001e0a0 
post sa= [1 0 0 0 0 0 0 0 0 0]

上面的testModifyElem函数传递的是 slice 对象,输出结果可以看出两点:

  1. 函数内部对slice某个元素的修改,会影响函数外面的slice。
  2. 函数内部的slice地址和函数外部的slice是一致的。

这种现象会给人一种感觉,golang slice的传递是引用传递(实际上是错误的)。我们再来看一个例子:

func testModifyElem2(s []int) {
   
	s[0] = 1
	fmt.Printf("s inter address is %p \n", s)
	fmt.Println("s=", s)
	s = append(s, 11)
	fmt.Printf("s inter address(after append) is %p \n", s)
	fmt.Println("s=(after append)", s)
}

func main()  {
   
	fmt.Println("------------------------ slice0 -------------------------")
	sa := make([]int, 10)
	fmt.Printf("sa address is %p \n", sa)
	fmt.Println("sa=", sa)
	testModifyElem2(sa)
	fmt.Printf("post sa address is %p \n", sa)
	fmt.Println("post sa=", sa)
}

输出结果是:

------------------------ slice0 -------------------------
sa address is 0xc00009e000 
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc00009e000 
s= [1 0 0 0 0 0 0 0 0 0]
s inter address(after append) is 0xc0000b0000 
s=(after append) [1 0 0 0 0 0 0 0 0 0 11]
post sa address is 0xc00009e000 
post sa= [1 0 0 0 0 0 0 0 0 0]

从这个输出结果我们可以得到:

  1. 函数内部对slice某个元素的修改,会影响函数外面的slice。
  2. 如果函数内部对slice的append操作,不会影响到函数外部slice。
  3. 我们的slice len和cap都是10,函数内部的append会导致扩容,扩容之后,地址发生了变化,但是函数外部的slice地址并没有变化。这说明了golang的slice传递slice不是引用传递,而是值传递。

还有一种场景,如果slice在函数内部append不会导致扩容呢?看下面的例子:

func testModifyElem2(s []int) {
   
	s[0] = 1
	fmt.Printf("s inter address is %p \n", s)
	fmt.Println("s=", s)
	s = append(s, 11)
	fmt.Printf("s inter address(after append) is %p \n", s)
	fmt.Println("s=(after append)", s)
}

func main()  {
   
	fmt.Println("------------------------ slice0 -------------------------")
	sa := make([]int, 10, 20)
	fmt.Printf("sa address is %p \n", sa)
	fmt.Println("sa=", sa)
	testModifyElem2(sa)
	fmt.Printf("post sa address is %p \n", sa)
	fmt.Println("post sa=", sa)
}

运行结果如下:

------------------------ slice0 -------------------------
sa address is 0xc0000ac000 
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc0000ac000 
s= [1 0 0 0 0 0 0 0 0 0]
s inter address(after append) is 0xc0000ac000 
s=(after append) [1 0 0 0 0 0 0 0 0 0 11]
post sa address is 0xc0000ac000 
post sa= [1 0 0 0 0 0 0 0 0 0]

我们可以看到两点:

  1. 函数内部的append不会影响到外部的slice;
  2. 如果函数内部的append不会导致扩容,那么slice的地址也不会变化。

这个结论进一步验证了我们的结论。

所以我们的结论就是:值传递。

  1. slice的函数传参或者赋值,其实都是值传递,也就是会发生拷贝。slice内部是持有三个变量,实际存储数据的数组地址、当前slice的len以及slice的容量。
  2. 传参时,实际上会拷贝len、cap、以及底层实际存储数据的数组的首地址。
  3. 如果函数内部发生了append的操作,函数外部是感知不到的。

当我们调用函数 testMethod(s) 的时候,实际上类似于 testMethod(*addr, len, cap)。

其实我们在调用append函数的时候,用法都是 s = append(s, data...) 。 append函数实际上返回的是一个新的slice对象,然后新的对象的属性;len、cap以及底层数组首地址会拷贝赋值给s。

slice作为函数入参的各种坑

前面一章其实已经列出了slice传参的一些坑。这里再列出一些典型的坑。

Case1: 函数入参是一个slice, 期待函数内部的的修改,比如新增、删除、更改会在外部生效。
这里的实现有两种方法:传一个slice的指针进去;或者return入参的slice并且赋值给外部的slice。

(1)传指针, 直接看code:

func testSlicePtr(sp *[]int) {
   
	fmt.Printf("sp address is %p \n", *sp)
	fmt.Println("sp=", *sp
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值