缘起
不论应用什么语言,日常生活中能常在技术群中看到相似这样的问题(当然这个图
是我瞎编的,实在的讨论会比图中 peace 一些~):
自己在这个话题上被他人鄙视过,这次写一篇文章,好好钻研一下这个话题~ 这张图的问题是: T类型在函数调用中是援用传递还是值传递。想要弄清这个问题,须要明确什么是援用,什么是值,所以本文会先讨论一下 T类型的数据类型是值类型还是援用类型。另外,文章只针对Golang这门语言进行摸索。 那么,什么是值类型,援用传递又是怎么回事呢?上面就跟小编一起来理解一下吧(~:
验证
数据类型
对于值类型、援用类型,维基百科中这样定义:
In computer programming, data types can be divided into two categories: A value of value type is the actual value. A value of reference type is a reference) to another value。
定义中把数据类型分为值类型和援用类型两类,而后介绍 值类型的值是信息自身;援用类型来的值是援用,这个援用能够为 nil,也能够是一个援用值,用户能够依据援用值找到信息自身。
举个例子,当初有个变量要去存不同类型的值。对于一些占用空间比拟小的类型,比方 整数、浮点数和bool类型,变量存的是这些值自身;而对于一些占用空间较大的类型,变量存的是类型的指针,用户能够依据指针找到这个值,这样的益处之一是能够节俭内存。留神对于援用类型,如果两个变量都保留某个值的援用,一个变量通过援用把信息扭转后,用户能够通过另一个变量看到信息的变动。
为啥会有援用类型呢,如果须要在多个过程中针对某个数据进行计算,那就得用地址作为信息去传递。达到的成果是 两个变量都保留某个值的援用,一个变量通过援用把信息扭转后,用户通过另一个变量看到扭转后的信息。这样做还有个益处是能够节俭空间,因为你能够应用指针来代替一个占用空间很大的构造体的传递。
简略通过图片看一下这两种分类的区别:
值类型(Golang代码)
援用类型(C++代码)
从图片上不能直观看出数据类型地址散布,接着通过代码来察看一下,C++中有援用类型,通过&
符号即可申明,例子如下:
#include <stdio.h>int main() { int a = 10; int &b = a; // 定义了一个援用变量b去援用a的值, 下同 int &c = b; printf("%d %d %d\n", a, b, c); printf("%p %p %p\n", &a, &b, &c); a = 100; printf("%d %d %d\n", a, b, c); return 0;}
这段代码的运行后果为
~ g++ main.cpp -o fk1 && ./fk1
10 10 10
0x7ffee11148c8 0x7ffee11148c8 0x7ffee11148c8
100 100 100
Golang中没有&T
类型,依照内置类型做分类,Golang里有int、float、string、map、slice、channel、struct、interface、func等数据类型,首先用int写一个和上文C++代码相似的例子:
int
package mainimport "fmt"func main() { a := 10086 var b, c = &a, &a // b、c变量存的都是a的地址 fmt.Println(b, c) // b、c变量保留的地址雷同 fmt.Println(&b, &c) // b、c变量自身的值不雷同 d := 100 b = &d // b扭转,a c的值不变 fmt.Println(a, *b, *c)}
输入后果:
0xc00001a0b0 0xc00001a0b0
0xc00000e028 0xc00000e030
10086 100 10086
在这段代码中,b和c都保留了a的地址,然而b、c自身是独立的,扭转b的值不会对a、c产生影响。所以能够把Golang中的int类型归为值类型之内。
int这种数据类型比较简单,个别不会对其产生疑难,比拟有争议的map、slice、channel这些数据类型的分类,这些类型只靠打印地址不够的。俗话说,源码背后了无机密,尽管 Golang 号称在1.5版本就实现了自举,但源码中至今还有大量的平台相干的汇编代码。如果咱们当初想理解一下这个问题:make函数为啥能初始化map、slice、chan这三种不同的数据类型
。只看golang源码就答复不了这个问题。所以俗话又说了:如果源码解决不了问题,就用go tool compile
命令看一下plan9汇编。通过汇编,咱们能够察看到指令级别的代码行为。只有看懂了汇编码,任何花里胡哨的技术名词在你背后就如同嗷嗷待哺的小鸡仔一样不堪一击。所以让咱们间接通过汇编来看一下下面的例子具体做了啥:
package mainfunc main() { var a = 10086 b := &a print(b, ",", *b)}
咱们应用 go tool compile -S -N -l main.go
打印汇编信息,简略阐明一下: go tool compile
命令用于调用Golang的底层命令工具,-S
参数示意输入汇编格局,-N
参数示意禁用优化 ,-l
参数示意禁用内联,有的函数会用inline函数关键字润饰,这样编译器在编译过程中会间接开展函数的代码,升高函数调用开销。n个汇编指令示意一行语句的执行,这里次要关注第4行和第5行的指令即可:
➜ fk git:(master) ✗ go tool compile -S -N -l main.go"".main STEXT size=143 args=0x0 locals=0x30------------------------------------------------调度相干代码 头部 start ------------------------------------------------// 00000~00013次要作用: 查看是否函数栈帧够用,不够用跳到尾部进行扩容 0x0000 00000 (main.go:3) TEXT "".main(SB), ABIInternal, $48-0 // 申明main函数, $48-0中:$48代表函数栈空间大小是48字节 ,0代表函数没有参数和返回值 0x0000 00000 (main.go:3) MOVQ (TLS), CX // 把以后g的地址赋给CX寄存器 0x0009 00009 (main.go:3) CMPQ SP, 16(CX) // 16(CX)对应g.stackguard0, 与SP寄存器进行比拟 0x000d 00013 (main.go:3) JLS 133 // 如果SP寄存器小于stackguard0,跳转到133这个地位 //00013代表地位------------------------------------------------调度相干代码 头部 end ------------------------------------------------ 0x000f 00015 (main.go:3) SUBQ $48, SP // SP-48 使其指向栈顶地位,这行命令是为了设置stack frame空间, 让SP指向栈顶地位 0x0013 00019 (main.go:3) MOVQ BP, 40(SP) // *(SP+40) = BP 0x0018 00024 (main.go:3) LEAQ 40(SP), BP // 把*(SP+40) 的地址赋值给BP寄存器, 使BP寄存器指向以后函数栈帧的栈底地位 0x001d 00029 (main.go:3) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) // FUNCDATA 和 PCDATA均是gc应用,可疏忽,后以...代替 0x001d 00029 (main.go:3) FUNCDATA $1, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 0x001d 00029 (main.go:3) FUNCDATA $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB) 0x001d 00029 (main.go:4) PCDATA $0, $0 // FUNCDATA 和 PCDATA均是gc应用,疏忽 0x001d 00029 (main.go:4) PCDATA $1, $0 0x001d 00029 (main.go:4) MOVQ $10086, "".a+16(SP) // a = 10086 0x0026 00038 (main.go:5) PCDATA $0, $1 0x0026 00038 (main.go:5) LEAQ "".a+16(SP), AX // 把 a变量的地址 赋给AX寄存器 0x002b 00043 (main.go:5) PCDATA $1, $1 0x002b 00043 (main.go:5) MOVQ AX, "".b+32(SP) // 把 AX寄存器的值 赋给b变量 0x0030 00048 (main.go:7) PCDATA $0, $0 0x0030 00048 (main.go:7) TESTB AL, (AX) 0x0032 00050 (main.go:7) MOVQ "".a+16(SP), AX 0x0037 00055 (main.go:7) MOVQ AX, ""..autotmp_2+24(SP) 0x003c 00060 (main.go:7) CALL runtime.printlock(SB) 0x0041 00065 (main.go:7) PCDATA $0, $1 0x0041 00065 (main.go:7) PCDATA $1, $0 0x0041 00065 (main.go:7) MOVQ "".b+32(SP), AX 0x0046 00070 (main.go:7) PCDATA $0, $0 0x0046 00070 (main.go:7) MOVQ AX, (SP) 0x004a 00074 (main.go:7) CALL runtime.printpointer(SB) 0x004f 00079 (main.go:7) PCDATA $0, $1 0x004f 00079 (main.go:7) LEAQ go.string.","(SB), AX 0x0056 00086 (main.go:7) PCDATA $0, $0 0x0056 00086 (main.go:7) MOVQ AX, (SP) 0x005a 00090 (main.go:7) MOVQ $1, 8(SP) 0x0063 00099 (main.go:7) CALL runtime.printstring(SB) 0x0068 00104 (main.go:7) MOVQ ""..autotmp_2+24(SP), AX 0x006d 00109 (main.go:7) MOVQ AX, (SP) 0x0071 00113 (main.go:7) CALL runtime.printint(SB) 0x0076 00118 (main.go:7) CALL runtime.printunlock(SB) 0x007b 00123 (main.go:8) MOVQ 40(SP), BP 0x0080 00128 (main.go:8) ADDQ $48, SP 0x0084 00132 (main.go:8) RET 0x0085 00133 (main.go:8) NOP------------------------------------------------调度相干代码 尾部 start ------------------------------------------------// 00133 次要作用:1.栈扩容;2.被runtime治理调度 0x0085 00133 (main.go:3) PCDATA $1, $-1 // FUNCDATA 和 PCDATA均是gc应用,疏忽 0x0085 00133 (main.go:3) PCDATA $0, $-1 // FUNCDATA 和 PCDATA均是gc应用,疏忽 0x0085 00133 (main.go:3) CALL runtime.morestack_noctxt(SB) // morestack but not preserving ctxt. 执行栈空间扩容 0x008a 00138 (main.go:3) JMP 0------------------------------------------------调度相干代码 尾部 end ------------------------------------------------
通过汇编咱们能够看到b变量保留的是a变量的地址,这个过程是用AX寄存器实现的(附录局部会介绍Plan9指令,了解有问题的同学能够先看附录)。
string
让咱们接着看一下string这种数据结构底层做了啥:
package mainfunc main() { var a = "hello" b := &a c := "world" b = &c println(*b, b) // world 0xc000044730 println(a, &a) // hello 0xc000044740}
汇编剖析(只有剖析main.go:4和main.go:5):
➜ fk git:(master) ✗ go tool compile -S -N -l main.go | grep -v PCDATA 0x0021 00033 (main.go:4) LEAQ go.string."hello"(SB), AX // AX 取hello这个.rodata段数据的地址 0x0028 00040 (main.go:4) MOVQ AX, "".a+24(SP) // 把AX 赋给a变量 地位:SP+24byte 0x002d 00045 (main.go:4) MOVQ $5, "".a+32(SP) // 把5(字符串长度)赋给a变量 地位:SP+32byte 0x0036 00054 (main.go:5) LEAQ "".a+24(SP), AX // AX取 "".a+24(SP) 的地址 0x003b 00059 (main.go:5) MOVQ AX, "".b+16(SP) // 把AX的值赋给b变量
从汇编中能够看到b:=&a
语句实际上是拷贝a变量的地址。在汇编层面 string是一个指针和len长度,赋值时会取个复合构造的地址,这也合乎runtime.string.go
的定义,其中str这个指针会执行字节数组。
type stringStruct struct { str unsafe.Pointer len int}
把代码略微改一下:
package mainfunc main() { var a = "hello" b := a println(a, ",", b) // hello , hello println(&a, ",", &b) // 0xc000044740 , 0xc000044730}
汇编剖析
➜ fk git:(master) ✗ go tool compile -S -N -l main.go 0x0021 00033 (main.go:4) LEAQ go.string."hello"(SB), AX // AX 取hello这个.rodata段数据的地址 0x0028 00040 (main.go:4) MOVQ AX, "".a+48(SP) // AX 赋值给a 地位: sp+48byte 0x002d 00045 (main.go:4) MOVQ $5, "".a+56(SP) // 长度5赋值给a 地位: sp+56byte 0x0036 00054 (main.go:5) MOVQ AX, "".b+32(SP) // AX 赋值给b 地位: sp+32byte 0x003b 00059 (main.go:5) MOVQ $5, "".b+40(SP) // 长度5赋值给b 地位: sp+40byte
当b是string类型时,执行b := a
时,b的值是信息自身对b的批改都不会影响到a;
当b取string地址时,执行b = &c
只是让b保留另一份指针,也不会影响到a自身的值,阐明string是值类型。
slice
代码:
package mainimport "fmt"func main() { a := make([]int, 10) a[0] = 1 b := a fmt.Println(a, b)}
汇编:
0x002f 00047 (main.go:6) LEAQ type.int(SB), AX // 把type.int值的指针赋给AX 0x0036 00054 (main.go:6) MOVQ AX, (SP) // 把寄存器里的值赋给sp 0x003a 00058 (main.go:6) MOVQ $10, 8(SP) // 把len的值赋给sp+8的地位 0x0043 00067 (main.go:6) MOVQ $10, 16(SP) // 把cap的值赋给sp+16的地位 (以上这几行都是为了给makeslice筹备参数) 0x004c 00076 (main.go:6) CALL runtime.makeslice(SB) // 调用makeslice 0x0051 00081 (main.go:6) MOVQ 24(SP), AX // AX = *(sp+24) 把makeslice的后果赋给AX 0x0056 00086 (main.go:6) MOVQ AX, "".a+96(SP) // AX 赋给变量a 地位:sp + 96byte 0x005b 00091 (main.go:6) MOVQ $10, "".a+104(SP) // len 10 赋给变量a 地位:sp + 104byte 0x0064 00100 (main.go:6) MOVQ $10, "".a+112(SP) // cap 10 赋给变量a 地位:sp + 112byte 0x006d 00109 (main.go:7) JMP 111 // 这行感觉没啥卵用 0x006f 00111 (main.go:7) MOVQ $1, (AX) // a[0] = 1 0x0076 00118 (main.go:9) MOVQ "".a+104(SP), AX // 把len赋给AX 0x007b 00123 (main.go:9) MOVQ "".a+96(SP), CX // 把指针赋给CX 0x0080 00128 (main.go:9) MOVQ "".a+112(SP), DX // 把cap赋给DX 0x0085 00133 (main.go:9) MOVQ CX, "".b+72(SP) // CX赋给b 0x008a 00138 (main.go:9) MOVQ AX, "".b+80(SP) // AX赋给b 0x008f 00143 (main.go:9) MOVQ DX, "".b+88(SP) // DX赋给b
makeslice函数签名为func makeslice(et *_type, len, cap int) unsafe.Pointer
。通过汇编能够看到,初始化slice的步骤为: 1.筹备信息,2. 调用makeslice函数,3. 把函数的后果指针、len信息、cap信息赋给变量。在执行b := a
语句时,又持续把指针信息、长度、容量赋给另一个变量。其中slice的底层数据结构如下所示:
type slice struct { array unsafe.Pointer len int cap int }
这样的体现让slice这种数据类型仿佛属于援用类型这个品种,在Go语言的官网文档有段申明map的定义中能找到相似的形容:
Map types are reference types, like pointers or slices, and so the value ofm
above isnil
; it doesn't point to an initialized map.
遗憾的是,slice在某些场合的体现并不属于援用类型:
package mainfunc fk(a []int) { a = make([]int, 0) println(a == nil) // false}func main() { var a []int println(a == nil) // true fk(a) println(a == nil) // true}
实际上,早在13年,Go语言之父之一就在go spec中申明:
spec: Go has no 'reference types'
在形容slice时,也把之前的reference to
这种偏“清晰”的词汇改为了descriptor for
。并顺便删掉了Slices, maps and channels are reference types
。
map
代码:
package mainimport "fmt"func main() { a := make(map[string]int) b := a fmt.Println(a, b)}
汇编(非相干汇编代码已删去):
$ go tool compile -S -N -l func-param.go 0x002f 00047 (main.go:6) CALL runtime.makemap_small(SB) // 调用 makemap_small 函数 0x0034 00052 (main.go:6) MOVQ (SP), AX // AX = *(BP) 0x0038 00056 (main.go:6) MOVQ AX, "".a+56(SP) // 把 AX 赋给a变量 0x003d 00061 (main.go:7) MOVQ AX, "".b+48(SP) // 把 AX 赋给b变量
其中 makemap_small 的函数签名为func makemap_small() *hmap
,能够看到在不论是初始化a,还是执行b的赋值语句,底层都是在把指针赋给变量。map类型实质上是一个指向hmap
的指针。具备指针的性质。
这让它看起来像是援用类型,然而它同样有非援用类型的体现:
package mainfunc fk(m map[string]int) { m = make(map[string]int) println(m == nil) // false}func main() { var a map[string]int println(a == nil) // true fk(a) println(a == nil) // true}
channel
代码
package mainfunc main() { a := make(chan int) b := a println(a, b)}
汇编:
0x001d 00029 (main.go:4) LEAQ type.chan int(SB), AX // 把type.chan int值的指针赋给AX0x0024 00036 (main.go:4) MOVQ AX, (SP) // *(SP) = AX0x0028 00040 (main.go:4) MOVQ $0, 8(SP) // *(SP+8) = 00x0031 00049 (main.go:4) CALL runtime.makechan(SB) // 调用runtime.makechan0x0036 00054 (main.go:4) MOVQ 16(SP), AX // AX = *(SP+16) 即把makechan的后果赋给AX寄存器0x003b 00059 (main.go:4) MOVQ AX, "".a+32(SP) // a = AX0x0040 00064 (main.go:5) MOVQ AX, "".b+24(SP) // b = AX
chan和slice有相似,都是调用runtime外面的函数并把后果指针赋给变量,makechan的函数签名为: func makechan(t *chantype, size int) *hchan
。
struct
代码:
package mainimport "fmt"type F struct { A int}func main() { a := F{A: 1} b := a b.A = 2 fmt.Println(a, b) // {1} {2}}
汇编:
0x002f 00047 (main.go:10) MOVQ $0, "".a+56(SP) // 这行预计是为了初始化a 0x0038 00056 (main.go:10) MOVQ $1, "".a+56(SP) // 把 1 赋值给a 0x0041 00065 (main.go:11) MOVQ $1, "".b+48(SP) // 把 1 赋值给b 0x004a 00074 (main.go:13) MOVQ $2, "".b+48(SP) // b的值批改为2
构造体这种数据类型没什么争议,不论在什么层面上都更像值类型。
小结
通过上面对各种数据类型在运行时地址、源码以及汇编层面的体现,并联合Go官网文档,有的读者可能还是有点懵逼,我感觉这是失常的。即便Go语言之父之一的大佬13年举大旗明确阐明Go中没有援用类型,然而在18年的文档中还是反水说xx type is reference type 。这篇文档兴许是其他人写的,侧面阐明这个概念的确是confused~
函数调用
同样先来看看定义:
By definition, pass by value means you are making a copy in memory of the actual parameter's value that is passed in, a copy of the contents of the actual parameter. ... In pass by reference (also called pass by address), a copy of the address of the actual parameter is stored.
中文意思是:
值传递会在内存中拷贝一份实参的值,值是指实参的内容。援用传递会拷贝一份实参的地址。
通过图片看一下两种调用的区别:
值传递(Go代码):
援用传递(c++):
通过c++代码看一下援用传递的理论体现:
#include <stdio.h>void fk(int & count)// & 使其进行援用传递{ count=count+1; printf("fk: %p, %d\n",&count, count); // 把各种变量信息打印进去}int main(){ int count=0; // printf("before call fk: %p, %d\n",&count, count); //调用函数前看一下各个变量信息 fk(count); printf("after call fk: %p, %d\n",&count, count); //调用函数后看一下各个变量信息 return 0;}
输入后果:
$ g++ main.cpp -o fk1 && ./fk1
before call fk: 0x7ffee90b57f8, 0
fk: 0x7ffee90b57f8, 1
after call fk: 0x7ffee90b57f8, 1
Go语言中是没有援用传递的,官网文档中Q&A局部对函数调用中参数传递早有定义:
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 theint
, and passing a pointer value makes a copy of the pointer, but not the data it points to.
大略翻译一下: Golang中函数传递都是值传递,也就是说函数总是取得传入参数的正本,就如同一个赋值语句讲值调配给参数一样。举例来说:在函数里传入一个 int 类型时会拷贝一个 int 类型的正本,传入一个指针将会拷贝一份指针正本,但并不会拷贝指针指向的值。
通过后面的剖析,置信读者对一些根本数据类型曾经有肯定的想法。让咱们看一下答案中专门强调的指针类型在函数传参中的体现:
package mainimport "fmt"func fk(a *int) { fmt.Printf("func a'value: %p\n", a) // func a'value: 0xc00001a0a0 fmt.Printf("func a'address: %p\n", &a) // func a'address: 0xc00000e030 // 指针指向的值一样,然而会copy一个新的指针}func main() { a := 10086 fmt.Printf("main a'adreess: %p\n", &a) // main a'adreess: 0xc00001a0a0 fk(&a)}
指针类型作为函数参数在传递时会拷贝一份新的指针,只不过两份指针指向同一个值。从后果来看合乎值传递的概念。
总结
以一些词汇对事物做分类的目标是要升高用户的了解老本,然而 援用类型和值类型 对变量分类, 援用传递和值传递 对函数调用分类,不仅没有降低成本,反而让人更困惑了。所以集体认为对于数据类型、函数调用这部分常识了解底层原理即可,不要为几个概念来回撕逼了。
参考
spec: Go has no 'reference types'
About the terminology "reference type" in Go
pass_by_value
Value_type_and_reference_type
golang-has-no-reference-values
There is no pass-by-reference in Go
T 还是 *T, 这是一个问题
Golang汇编命令解读
对于援用(reference)这个术语
Go语言参数传递是传值还是传援用