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对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,219评论 0 4
  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,598评论 1 19
  • 86.复合 Cases 共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式...
    无沣阅读 1,354评论 1 5
  • 本章内容 使用对象 创建并操作数组 理解基本的 JavaScript 类型 使用基本类型和基本包装类型 引用类型的...
    闷油瓶小张阅读 677评论 0 0
  • 一阵凉风袭来,吹的人不禁一颤,也不知这秋日孤风怎就没来由的引来一阵近冬的气息。孤道上的行人皆都心里一凉,打了个寒战...
    水石阅读 639评论 1 2