go语言函数传递slice类型变量

这篇文章介绍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值,还是之前的值。

在上述例子中

  1. 第一个是s1和第二个s1输出的值一模一样,这是在调用useSlice(…)前后打出来的,可见尽管在useSlice里面修改的slice的值,但是main函数并不知道。
  2. 所有输出的v1值都是相同的,即他们指向的数据存储地址是同一块地址。
  3. s2的两次输出,除了v2值加一以为,其他都是一样的,说明此时append函数的返回值,就是append传入参数的值。
  4. 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对象。

    原文作者:CodingCode
    原文地址: https://www.jianshu.com/p/b4b7dda00b03
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞