前言
在Go中如果应用过map和channel,就会发现把map和channel作为函数参数传递,不须要在函数形参里对map和channel加指针标记*就能够在函数体内扭转内部map和channel的值。
这会给人一种错觉:map和channel难道是相似C++的援用变量,函数传参的时候应用的是援用传递?
比方上面的例子:
// example1.gopackage mainimport "fmt"func changeMap(data map[string]interface{}) { data["c"] = 3}func main() { counter := map[string]interface{}{"a": 1, "b": 2} fmt.Println("begin:", counter) changeMap(counter) fmt.Println("after:", counter)}
程序运行的后果是:
begin: map[a:1 b:2]after: map[a:1 b:2 c:3]
下面的例子里,函数changeMap扭转了内部的map类型counter的值。
那map传参是应用的援用传递么?带着这个问题,咱们先回顾下什么是援用变量和援用传递。
什么是援用变量(reference variable)和援用传递(pass-by-reference)
咱们先回顾下C++里的援用变量和援用传递。看上面的例子:
// example2.cpp#include <iostream>using namespace std;/*函数changeValue应用援用传递*/void changeValue(int &n) { n = 2;}int main() { int a = 1; /* b是援用变量,援用的是变量a */ int &b = a; cout << "a=" << a << " address:" << &a << endl; cout << "b=" << b << " address:" << &b << endl; /* 调用changeValue会扭转内部实参a的值 */ changeValue(a); cout << "a=" << a << " address:" << &a << endl; cout << "b=" << b << " address:" << &b << endl;}
程序的运行后果是:
a=1 address:0x7ffee7aa776cb=1 address:0x7ffee7aa776ca=2 address:0x7ffee7aa776cb=2 address:0x7ffee7aa776c
在这个例子里,变量b是援用变量,援用的是变量a。援用变量就好比是原变量的一个别名,援用变量和援用传递的特点如下:
- 援用变量和原变量的内存地址一样。就像下面的例子里援用变量b和原变量a的内存地址雷同。
- 函数应用援用传递,能够扭转内部实参的值。就像下面的例子里,changeValue函数应用了援用传递,扭转了内部实参a的值。
- 对原变量的值的批改也会扭转援用变量的值。就像下面的例子里,changeValue函数对a的批改,也扭转了援用变量b的值。
Go有援用变量(reference variable)和援用传递(pass-by-reference)么?
先给出论断:Go语言里没有援用变量和援用传递。
在Go语言里,不可能有2个变量有雷同的内存地址,也就不存在援用变量了。
留神:这里说的是不可能2个变量有雷同的内存地址,然而2个变量指向同一个内存地址是能够的,这2个是不一样的。参考上面的例子:
// example3.gopackage mainimport "fmt"func main() { a := 10 var p1 *int = &a var p2 *int = &a fmt.Println("p1 value:", p1, " address:", &p1) fmt.Println("p2 value:", p2, " address:", &p2)}
程序运行后果是:
p1 value: 0xc0000ac008 address: 0xc0000ae018p2 value: 0xc0000ac008 address: 0xc0000ae020
能够看出,变量p1和p2的值雷同,都指向变量a的内存地址。然而变量p1和p2本人自身的内存地址是不一样的。而C++里的援用变量和原变量的内存地址是雷同的。
因而,在Go语言里是不存在援用变量的,也就天然没有援用传递了。
有map不是应用援用传递的反例么
看上面的例子:
// example4.gopackage mainimport "fmt"func initMap(data map[string]int) { data = make(map[string]int) fmt.Println("in function initMap, data == nil:", data == nil)}func main() { var data map[string]int fmt.Println("before init, data == nil:", data == nil) initMap(data) fmt.Println("after init, data == nil:", data == nil)}
大家能够先思考一会,想想程序运行后果是什么。
程序理论运行后果如下:
before init, data == nil: truein function initMap, data == nil: falseafter init, data == nil: true
能够看出,函数initMap并没有扭转内部实参data的值,因而也证实了map并不是援用变量。
那问题来了,为啥map作为函数参数不是应用的援用传递,然而在本文最结尾举的例子里,却能够扭转内部实参的值呢?
map到底是什么?
论断是:map变量是指向runtime.hmap的指针
当咱们应用上面的代码初始化map的时候
data := make(map[string]int)
Go编译器会把make调用转成对runtime.makemap的调用,咱们来看看runtime.makemap的源代码实现。
298 // makemap implements Go map creation for make(map[k]v, hint).299 // If the compiler has determined that the map or the first bucket300 // can be created on the stack, h and/or bucket may be non-nil.301 // If h != nil, the map can be created directly in h.302 // If h.buckets != nil, bucket pointed to can be used as the first bucket.303 func makemap(t *maptype, hint int, h *hmap) *hmap {304 mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)305 if overflow || mem > maxAlloc {306 hint = 0307 }308 309 // initialize Hmap310 if h == nil {311 h = new(hmap)312 }313 h.hash0 = fastrand()314 315 // Find the size parameter B which will hold the requested # of elements.316 // For hint < 0 overLoadFactor returns false since hint < bucketCnt.317 B := uint8(0)318 for overLoadFactor(hint, B) {319 B++320 }321 h.B = B322 323 // allocate initial hash table324 // if B == 0, the buckets field is allocated lazily later (in mapassign)325 // If hint is large zeroing this memory could take a while.326 if h.B != 0 {327 var nextOverflow *bmap328 h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)329 if nextOverflow != nil {330 h.extra = new(mapextra)331 h.extra.nextOverflow = nextOverflow332 }333 }334 335 return h336 }
从下面的源代码能够看出,runtime.makemap返回的是一个指向runtime.hmap构造的指针。
咱们也能够通过上面的例子,来验证map变量到底是不是指针。
// example5.gopackage mainimport ( "fmt" "unsafe")func main() { data := make(map[string]int) var p uintptr fmt.Println("data size:", unsafe.Sizeof(data)) fmt.Println("pointer size:", unsafe.Sizeof(p))}
程序运行后果是:
data size: 8pointer size: 8
map的size和指针的size一样,都是8个字节。
思考更为深刻的读者,看到这里,可能还会有一个疑难:
既然map是指针,那为什么make()函数的阐明里,有这么一句Unlike new, make's return type is the same as the type of its argument, not a pointer to it.
The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:
如果map是指针,那make返回的不应该是*map[string]int么,为啥官网文档里说的是not a pointer to it.
这里其实也有Go语言历史上的一个演变过程,看看Go作者之一Ian Taylor的说法:
In the very early days what we call maps now
were written as pointers, so you wrote *map[int]int. We moved away
from that when we realized that no one ever wrotemap
without
writing*map
. That simplified many things but it left this issue
behind as a complication.
所以,在Go语言晚期,确实对于map是应用过指针模式的,然而最初Go设计者们发现,简直没有人应用map不加指针,因而就间接去掉了模式上的指针符号*。
总结
map和channel,实质上都是指针,指向Go runtime构造。带着这个思路,咱们再回顾下之前讲过的例子:
// example4.gopackage mainimport "fmt"func initMap(data map[string]int) { data = make(map[string]int) fmt.Println("in function initMap, data == nil:", data == nil)}func main() { var data map[string]int fmt.Println("before init, data == nil:", data == nil) initMap(data) fmt.Println("after init, data == nil:", data == nil)}
既然map是一个指针,因而在函数initMap里,
data = make(map[string]int)
这一句等于把data这个指针,进行了从新赋值,函数外部的data指针不再指向内部实参data对应的runtime.hmap构造体的内存地址。
因而在函数体内对data的批改,并没有影响内部实参data以及data对应的runtime.hmap构造体的值。
程序理论运行后果如下:
before init, data == nil: truein function initMap, data == nil: falseafter init, data == nil: true
代码
相干代码和阐明开源在GitHub:Go有援用变量和援用传递么?
也能够搜寻公众号:coding进阶,查看更多Go常识。
References
- https://dave.cheney.net/2017/...