Part1 切片类型的定义 Go 中的 Slice 可以用reflect.SliceHeader来表示。
1 2 3 4 5 6 7 8 9 10 11 type SliceHeader struct { Data uintptr Len int Cap int }
上述定义还不够清楚,我们再看下面这个定义。
1 2 3 4 5 type slice struct { array unsafe.Pointer len int cap int }
通过这个定义可以看出,切片实际开辟了一个连续地址空间用来存储数据(可以认为是数组),并定义了len字段来管理当前数据的长度,cap字段来管理当前分配的内存空间大小。
Part2 打印切片的地址 下面这段程序分别用三种不同的方式尝试去打印切片s。
1 2 3 4 5 6 7 8 9 10 11 12 13 import "fmt" func mSlicePrint () { s := []int {1 , 2 , 3 } fmt.Printf("s: %p\n" , s) fmt.Printf("&s: %p\n" , &s) fmt.Printf("&s[0]: %p\n" , &s[0 ]) }
可以看到,s 和 &s[0] 打印的地址是一样的,而 &s 打印的地址却不同,这是为什么呢?
根据 package fmt 的文档 https://pkg.go.dev/fmt ,%p的作用如下。
1 2 3 4 5 6 7 Slice %p address of 0th element in base 16 notation, with leading 0x Pointer %p base 16 notation, with leading 0x The %b, %d, %o, %x and %X verbs also work with pointers, formatting the value exactly as if it were an integer.
因此,s 和 &s[0] 打印的都是切片背后的[数组地址],并且是数组首个元素的地址。
而 &s 打印的则是Part 1中讨论的切片结构体的地址,即[切片地址]。
下面这段代码打印出了切片结构体的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import "fmt" type slice struct { array unsafe.Pointer len int cap int } func mSlicePrint () { s := []int {1 , 2 , 3 } sp := (*slice)(unsafe.Pointer(&s)) fmt.Printf("%v\n" , sp) fmt.Printf("%p\n" , &sp) }
Part3 切片的一个坑 我们知道 Go 语言无论是传递基本类型、结构体还是指针,都会对传递的参数进行复制,也就是传值。
传值时,函数调用会复制参数,被调用方和调用方持有不相关的两份数据。
那假如我们把切片作为参数传递,能否修改切片中的数据呢?来看下面这段代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import ( "fmt" "unsafe" ) type slice struct { array unsafe.Pointer len int cap int } func changeSlice (s []int ) { s[0 ] = 200 s = append (s, 100 ) sp := (*slice)(unsafe.Pointer(&s)) fmt.Printf("[changeSlice] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) return } func mSliceParam () { s := make ([]int , 1 , 10 ) sp := (*slice)(unsafe.Pointer(&s)) fmt.Printf("[mSliceParam] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) changeSlice(s) sp = (*slice)(unsafe.Pointer(&s)) fmt.Printf("[mSliceParam] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) return }
可以看到,由于是值传递,所以&s打印出的[切片地址]已经不一样了。
再来看切片数据,既然切片地址不同,那为何s[0]的数据仍然被修改了呢?
原来,切片包含的数组指针也被拷贝了一份,虽然是两个不同的指针,但指向的却是同一个地址空间,因此s打印出的[数组地址]是相同的。
此外,我们还可以注意到,虽然s[0]被修改了,但append操作却没有起作用,这是为什么呢?
从打印的切片结构中我们可以找到答案,虽然数组地址相同,但切片结构中的len是值传递的。
1 2 3 [mSliceParam] &{0xc00001c190 1 10} [changeSlice] &{0xc00001c190 2 10} [mSliceParam] &{0xc00001c190 1 10}
也就是说,changeSlice函数中的append操作只是修改了len的拷贝,而mSliceParam函数中切片的长度还是 1,因此新增的值自然就无法打印出来了。
Part4 如何解决1 既然len无法传递出来,那难道在外层函数就无法获取到新增的值吗?
在阅读io库的源码时,我看到了如下的接口。
1 2 3 type Reader interface { Read(p []byte ) (n int , err error ) }
Reader接口的Read方法,接受一个[]byte切片,并返回读取到的字节数。
同样的,只要我们把新的len返回,不就能获取到新增的值了吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import ( "fmt" "unsafe" ) func changeSliceFix (s []int ) (n int ) { s[0 ] = 200 s = append (s, 100 , 50 ) sp := (*slice)(unsafe.Pointer(&s)) fmt.Printf("[changeSlice] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) return len (s) } func mSliceParamFix () { s := make ([]int , 1 , 10 ) sp := (*slice)(unsafe.Pointer(&s)) fmt.Printf("[mSliceParam] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) n := changeSliceFix(s) sn := s[:n] sp = (*slice)(unsafe.Pointer(&s)) fmt.Printf("[mSliceParam s] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , s, s, &s, sp) spn := (*slice)(unsafe.Pointer(&sn)) fmt.Printf("[mSliceParam sn] [切片数据]%v, [数组地址]%p, [切片地址]%p, [切片结构]%v\n" , sn, sn, &sn, spn) }
但要注意的是,这里对s的修改不能超过其容量,否则会触发扩容,新增的值会放在新的内存地址,那在函数外是无论如何也读取不到了。
Part5 如何解决2 切片指针 1 2 3 4 func changeSlicePointer (s *[]int ) { *s = append (*s, 100 ) return }
相关链接