前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解Go切片

深入理解Go切片

作者头像
用户4766018
发布2022-08-19 10:05:56
1580
发布2022-08-19 10:05:56
举报
文章被收录于专栏:格物致知格物致知

一,引子

从最近遇到一个bug说起,示意代码如下:

代码语言:javascript
复制
func test(a interface{}) {
	s, _ := a.([]int)
	s = append(s, 1)
	fmt.Printf("%v,%p\n", s, &s)
	return
}

func main() {
	s := make([]int, 0)
	test(s)
	fmt.Printf("%v,%p\n", s, &s)
}

期望的是函数里面对切片的操作会返回出来,结果却没有。

运行结果如下:

代码语言:javascript
复制
[1],0xc000004090
[],0xc000004078

上述过程涉及两个问题,一是函数传接口或者切片类型的参数到底是传值还是传引用,一是切片转接口发生了什么。

二,go函数传参规则

官方文档说明如下,出处 https://go.dev/doc/faq#pass_by_value。

代码语言:javascript
复制
When are function parameters passed by value?
As in all languages in the C family, everything in Go is passed by value. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter. For instance, passing an int value to a function makes a copy of the int, and passing a pointer value makes a copy of the pointer, but not the data it points to. (See a later section for a discussion of how this affects method receivers.)

Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn't copy the data it points to. Copying an interface value makes a copy of the thing stored in the interface value. If the interface value holds a struct, copying the interface value makes a copy of the struct. If the interface value holds a pointer, copying the interface value makes a copy of the pointer, but again not the data it points to.

大致意思是go语言中,所有函数参数的传递都是用的值复制的方式传递参数,包括切片,接口,map等。那为什么map表现起来像在传引用呢,是因为map中的指针复制后,其指向的地址是相同的,所以对相同地址的操作看起来像是在传递引用。

三,传递切片发生了什么

切片从实现上看定义如下:

代码语言:javascript
复制
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

一个切片是由指针,长度,和容量组成。Data 是一片连续的内存空间,这片内存空间可以用于存储切片中的全部元素,数组中的元素只是逻辑上的概念,底层存储其实都是连续的,所以我们可以将切片理解成一片连续的内存空间加上长度与容量的标识。

当切片作为参数传递到函数中的时候, 切片的这三个字段都会被复制成为一个新的切片;新切片与旧切片指向相同的地址,但是长度和容量其实已经被复制了;这样的行为如果不了解就会导致一些莫名其妙的问题。比如函数内对切片进行修改元素操作,如果直接修改旧的元素,因为是修改的Data指向的地址,所以函数外也能看到修改后的元素;但是如果是添加或者删除元素,函数内部对Len或者Cap的修改并不会反应到函数外部。所以一个良好的建议是,切片参数用取地址的方式来进行传递。

如下代码可以更好的理解:

代码语言:javascript
复制
func test(a interface{}) {
	s, _ := a.([]int)
	s[0] = 10
	s = append(s, 2)
	fmt.Printf("%v,%p\n", s, &s)
	return
}

func main() {
	s := make([]int, 1)
	s[0] = 1
	test(s)
	fmt.Printf("%v,%p\n", s, &s)
}

输出结果:

代码语言:javascript
复制
[10 2],0xc000096078
[10],0xc000096060

可以看到0号位置的元素修改后可以反应到函数外部,而添加的元素则丢失了,原因就是切片的长度复制后在函数内的修改不会反应到函数外部,而指针指向的内容修改是外部可见的。可以进一步将切片内部打印出来:

代码语言:javascript
复制
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

func printSlice(a []int) {
	b := (*slice)(unsafe.Pointer(&a))
	fmt.Printf("%v,%+v\n", a, b)
}

func test(a interface{}) {
	s, _ := a.([]int)
	s = append(s, 2)
	s[0] = 10
	fmt.Printf("%v,%p\n", s, &s)
	printSlice(s)
	return
}

func main() {
	s := make([]int, 1)
	s[0] = 1
	printSlice(s)
	test(s)
	fmt.Printf("%v,%p\n", s, &s)
	printSlice(s)
}

执行结果:

代码语言:javascript
复制
[1],&{array:0xc0000a8060 len:1 cap:4}
[10 2],0xc0000960c0
[10 2],&{array:0xc0000a8060 len:2 cap:4}
[10],0xc000096060
[10],&{array:0xc0000a8060 len:1 cap:4}

可以看到切片的指针是一样的,但是len在函数返回后并没有将test函数内的修改返回。这里切片初始化的时候指定了容量是4,如果不指定容量,test函数中的append会导致切片重新分配内存,指针地址就会改变,而对新分配的地址的修改也会丢失。

当需要将切片的修改返回到函数外部的时候,正确的做法是取切片地址传参数。

四,切片转换为接口发生了什么

接口内部实现上分为两种:

1,使用 runtime.iface 结构体表示包含方法的接口

2,使用 runtime.eface 结构体表示不包含任何方法的 interface{} 类型

上述切片转接口就是转成第二种eface接口,定义如下:

代码语言:javascript
复制
type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}

直观的推测,当切片转为接口之后,_type字段表示切片类型,data指向原有切片的地址。代码验证:

代码语言:javascript
复制
type eface struct { // 16 字节
	_type unsafe.Pointer
	data  unsafe.Pointer
}

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

func printSlice(a *[]int) {
	b := (*slice)(unsafe.Pointer(a))
	fmt.Printf("%p, %v,%+v\n", a, a, b)
}

func test(a interface{}) {
	fmt.Printf("slice:%p, %v\n", &a, a)

	b := (*eface)(unsafe.Pointer(&a))
	fmt.Printf("[eface]%p, %v,%+v\n", &a, a, b)

	c := a.([]int)
	printSlice(&c)

	c[0] = 10
	c = append(c, 20)

	d := (*slice)(unsafe.Pointer(b.data))
	fmt.Printf("[eface.data]%v,%+v\n", d, d)

	return
}

func main() {
	s := make([]int, 1)
	s[0] = 1
	printSlice(&s)
	test(s)
	printSlice(&s)
}

输出:

代码语言:javascript
复制
0xc000004078, &[1],&{array:0xc00000a098 len:1 cap:1}
slice:0xc00004e230, [1]
[eface]0xc00004e230, [1],&{_type:0x3d47a0 data:0xc0000040c0}
0xc0000040d8, &[1],&{array:0xc00000a098 len:1 cap:1}
[eface.data]&{0xc00000a098 1 1},&{array:0xc00000a098 len:1 cap:1}
0xc000004078, &[10],&{array:0xc00000a098 len:1 cap:1}

仔细观察输出,可以发现接口的data字段指向一个复制后的切片,切片内部的array域都是指向同一个地址,因此切片以接口形式传入函数内部其实也发生了一次值赋值,函数内部对与切片的长度或者容量的修改,也不会返回到函数外部。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-05-30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一,引子
  • 二,go函数传参规则
  • 三,传递切片发生了什么
  • 四,切片转换为接口发生了什么
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档