这篇文章介绍slice类型数据是如何在函数之间传递的。
package main
import (
"fmt"
"unsafe"
)
type myslice struct {
v1 uintptr
v2 uint64
v3 uint64
}
var p * myslice
func main() {
s1 := make([]int64, 2, 4)
s1[0] = 0x11
s1[1] = 0x22
// print s1
p = (* myslice)(unsafe.Pointer(&s1))
fmt.Printf("s1 p=%p,v1=%x,v2=%x,v=%x\n", p, p.v1, p.v2, p.v3)
s3 := useSlice(s1)
// print s1
p = (* myslice)(unsafe.Pointer(&s1))
fmt.Printf("s1 p=%p,v1=%x,v2=%x,v=%x\n", p, p.v1, p.v2, p.v3)
// print s3
p = (* myslice)(unsafe.Pointer(&s3))
fmt.Printf("s3 p=%p,v1=%x,v2=%x,v=%x\n", p, p.v1, p.v2, p.v3)
}
func useSlice(s2 []int64) []int64 {
// print s2
p = (* myslice)(unsafe.Pointer(&s2))
fmt.Printf("s2 p=%p,v1=%x,v2=%x,v=%x\n", p, p.v1, p.v2, p.v3)
s2 = append(s2, 0x33)
// print s2
p = (* myslice)(unsafe.Pointer(&s2))
fmt.Printf("s2 p=%p,v1=%x,v2=%x,v=%x\n", p, p.v1, p.v2, p.v3)
return s2
}
运行结果如下:
$ go build && ./main
s1 p=0xc42000a2a0,v1=c42000a2c0,v2=2,v=4
s2 p=0xc42000a320,v1=c42000a2c0,v2=2,v=4
s2 p=0xc42000a320,v1=c42000a2c0,v2=3,v=4
s1 p=0xc42000a2a0,v1=c42000a2c0,v2=2,v=4
s3 p=0xc42000a300,v1=c42000a2c0,v2=3,v=4
通过这个例子代码,我们非常清楚明确:go语言函数传参是传的值。
在我们slice的例子中,这个值是slice本身的值,即24个字节(包含指向数据的指针,以及slice的len和cap值),而不是slice所包含的数据的值。所以在callee函数内部可以访问到slice元素的值,进而在callee函数内部可以修改slice元素的值,并对caller可见;但是注意不能使用插入和删除,因为callee的metadata是caller的metadata的拷贝,而不是引用,当在callee里面插入和删除数据时,caller的metadata并没有发生变化,即caller中记录的len值,还是之前的值。
在上述例子中
- 第一个是s1和第二个s1输出的值一模一样,这是在调用useSlice(…)前后打出来的,可见尽管在useSlice里面修改的slice的值,但是main函数并不知道。
- 所有输出的v1值都是相同的,即他们指向的数据存储地址是同一块地址。
- s2的两次输出,除了v2值加一以为,其他都是一样的,说明此时append函数的返回值,就是append传入参数的值。
- s3的值是新分配的slice对象,它里面的值和第二个s2输出时一样的,即是useSlice函数的返回值。
有同学可能会疑问了,append既然输出参数就是出入参数,那不是多此一举吗,不用处理返回也行啊:
func useSlice(s2 []int64) []int64 {
append(s2, 0x33)
return s2
}
可是,编译器直接就报错
./main.go:<line>: append(s2, 51) evaluated but not used
我也不知道为什么go要这么设计,我难道丢弃放回值不行吗?
但是对于我们这个功能来说,必须要赋值的,因为append并没有修改原来的s2,它修改的是拷贝,append也是一个普通函数,对于slice也是传值进入的,传入24字节,append函数修改了作为参数复制的24字节,但是对于调用append的函数而言,那个slice已经和append内部使用的slice不是同一个24字节的内容,所以append需要返回一个slice对象,而对于调用者来说,最常见的用法是把这个传出参数,重新赋值给传入参数,即:
s2 = append(s2, …)
最后我们看一下汇编码,如何传递slice的
package main
func main() {
var ss []int64
useSlice(ss)
}
func useSlice(ss []int64) {
ss[0x11] = 0x21;
}
main函数的代码片段
var ss []int64
467f0d: 48 c7 44 24 18 00 00 movq $0x0,0x18(%rsp)
467f14: 00 00
467f16: 48 c7 44 24 20 00 00 movq $0x0,0x20(%rsp)
467f1d: 00 00
467f1f: 48 c7 44 24 28 00 00 movq $0x0,0x28(%rsp)
467f26: 00 00
useSlice(ss)
467f28: 48 c7 04 24 00 00 00 movq $0x0,(%rsp) # data pointer
467f2f: 00
467f30: 48 c7 44 24 08 00 00 movq $0x0,0x8(%rsp) # len value
467f37: 00 00
467f39: 48 c7 44 24 10 00 00 movq $0x0,0x10(%rsp) # cap value
467f40: 00 00
467f42: e8 19 00 00 00 callq 467f60 <main.useSlice>
useSlice的代码
func useSlice(ss []int64) {
467f60: 48 83 ec 08 sub $0x8,%rsp
467f64: 48 89 2c 24 mov %rbp,(%rsp)
467f68: 48 8d 2c 24 lea (%rsp),%rbp
ss[0x11] = 0x21;
467f6c: 48 8b 44 24 18 mov 0x18(%rsp),%rax # len value
467f71: 48 8b 4c 24 10 mov 0x10(%rsp),%rcx # data pointer
467f76: 48 83 f8 21 cmp $0x11,%rax # 比较下标0x11和slice的len域,是否越界
467f7a: 77 02 ja 467f7e <main.useSlice+0x1e>
467f7c: eb 14 jmp 467f92 <main.useSlice+0x32>
467f7e: 48 c7 81 08 01 00 00 movq $0x22,0x88(%rcx) # 把值0x22赋给slice[0x11]
467f85: 2c 00 00 00
467f89: 48 8b 2c 24 mov (%rsp),%rbp
467f8d: 48 83 c4 08 add $0x8,%rsp
467f91: c3 retq
我们可以看到main函数把slice的三个成员全部通过堆栈传递给了useSlice,然后在useSlice里面在定义slice对象。