关于golang:面试题Go有引用变量和引用传递么

7次阅读

共计 5259 个字符,预计需要花费 14 分钟才能阅读完成。

前言

在 Go 中如果应用过 map 和 channel,就会发现把 map 和 channel 作为函数参数传递,不须要在函数形参里对 map 和 channel 加指针标记 * 就能够在函数体内扭转内部 map 和 channel 的值。

这会给人一种错觉:map 和 channel 难道是相似 C ++ 的援用变量,函数传参的时候应用的是援用传递?

比方上面的例子:

// example1.go
package main

import "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:0x7ffee7aa776c
b=1 address:0x7ffee7aa776c
a=2 address:0x7ffee7aa776c
b=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.go
package main

import "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: 0xc0000ae018
p2 value: 0xc0000ac008  address: 0xc0000ae020

能够看出,变量 p1 和 p2 的值雷同,都指向变量 a 的内存地址。然而变量 p1 和 p2 本人自身的内存地址是不一样的。而 C ++ 里的援用变量和原变量的内存地址是雷同的。

因而,在 Go 语言里是不存在援用变量的,也就天然没有援用传递了。

有 map 不是应用援用传递的反例么

看上面的例子:

// example4.go
package main

import "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: true
in function initMap, data == nil: false
after 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 bucket
300  // 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 = 0
307      }
308  
309      // initialize Hmap
310      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 = B
322  
323      // allocate initial hash table
324      // 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 *bmap
328          h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
329          if nextOverflow != nil {330              h.extra = new(mapextra)
331              h.extra.nextOverflow = nextOverflow
332          }
333      }
334  
335      return h
336  }

从下面的源代码能够看出,runtime.makemap 返回的是一个指向 runtime.hmap 构造的指针。

咱们也能够通过上面的例子,来验证 map 变量到底是不是指针。

// example5.go
package main

import (
    "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: 8
pointer 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 wrote map 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.go
package main

import "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: true
in function initMap, data == nil: false
after init, data == nil: true

代码

相干代码和阐明开源在 GitHub:Go 有援用变量和援用传递么?

也能够搜寻公众号:coding 进阶,查看更多 Go 常识。

References

  • https://dave.cheney.net/2017/…
正文完
 0