Interview question: does Go have reference variables and reference passing?

Posted by ohaus on Thu, 18 Nov 2021 06:40:27 +0100

preface

If you have used map and channel in Go, you will find that you can change the values of external map and channel in the function body without adding pointer marks * to map and channel in the function parameters.

This will give people an illusion: are map and channel reference variables similar to C + +, and function parameters are passed by reference?

For example, the following example:

// 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)
}

The result of the program running is:

begin: map[a:1 b:2]
after: map[a:1 b:2 c:3]

In the above example, the function changeMap changes the value of the external map type counter.

Is the map parameter passed by reference? With this in mind, let's first review what reference variables and reference passing are.

What are reference variable and pass by reference

Let's first review the reference variables and reference passing in C + +. Take the following example:

// example2.cpp
#include <iostream>

using namespace std;

/*The function changeValue is passed by reference*/
void changeValue(int &n) {
    n = 2;
}

int main() {
    int a = 1;
    /*
    b Is a reference variable, which refers to variable a
    */
    int &b = a;
    cout << "a=" << a << " address:" << &a << endl;
    cout << "b=" << b << " address:" << &b << endl;
    /*
    Calling changeValue changes the value of the external argument a
    */
    changeValue(a);
    cout << "a=" << a << " address:" << &a << endl;
    cout << "b=" << b << " address:" << &b << endl;
}

The running result of the program is:

a=1 address:0x7ffee7aa776c
b=1 address:0x7ffee7aa776c
a=2 address:0x7ffee7aa776c
b=2 address:0x7ffee7aa776c

In this example, variable b is a reference variable and variable a is referenced. A reference variable is like an alias of an original variable. The characteristics of reference variable and reference transfer are as follows:

  • The memory address of the reference variable is the same as that of the original variable. As in the above example, the memory addresses of the reference variable b and the original variable a are the same.
  • Functions are passed by reference, and the value of external arguments can be changed. As in the above example, the changeValue function uses reference passing to change the value of the external argument a.
  • Changes to the value of the original variable will also change the value of the reference variable. As in the above example, the modification of a by the changeValue function also changes the value of the reference variable b.

Does Go have reference variable and pass by reference?

The first conclusion is that there are no reference variables and reference passing in Go language.

In Go language, it is impossible for two variables to have the same memory address, so there is no reference variable.

Note: it is impossible for two variables to have the same memory address, but it is possible for two variables to point to the same memory address. These two variables are different. Refer to the following example:

// 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)
}

The running result of the program is:

p1 value: 0xc0000ac008  address: 0xc0000ae018
p2 value: 0xc0000ac008  address: 0xc0000ae020

It can be seen that the values of variables p1 and p2 are the same, both pointing to the memory address of variable a. However, the memory addresses of variables p1 and p2 are different. The memory addresses of reference variables and original variables in C + + are the same.

Therefore, there is no reference variable in Go language, so there is no reference transfer.

Is there a counterexample of map passing by reference

Take the following example:

// 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)
}

You can think about it for a while and think about the result of the program.

The actual running results of the program are as follows:

before init, data == nil: true
in function initMap, data == nil: false
after init, data == nil: true

It can be seen that the function initMap does not change the value of the external argument data, so it is also proved that the map is not a reference variable.

The question is, why is map not passed by reference as a function parameter, but in the example given at the beginning of this article, the value of external arguments can be changed?

What is map?

The conclusion is that the map variable is a pointer to runtime.hmap

When we initialize the map with the following code

data := make(map[string]int)

The Go compiler turns make calls into pairs runtime.makemap Let's take a look at the source code implementation of 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  }

As can be seen from the above source code, runtime.makemap returns a pointer to the runtime.hmap structure.

We can also verify whether the map variable is a pointer through the following example.

// 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))
}

The running result of the program is:

data size: 8
pointer size: 8

The size of the map is the same as that of the pointer, which is 8 bytes.

Readers who think more deeply may have another question:

Since map is a pointer, why is there such a sentence in the description of make() function as 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:

If map is a pointer, shouldn't make return * map[string]int? Why does the official document say not a pointer to it

In fact, there is also an evolution process in the history of Go language. Take a look at the statement of Ian Taylor, one of the authors of Go:

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.

Therefore, in the early days of Go language, pointer form was indeed used for map, but finally Go designers found that almost no one used map without pointer, so they directly removed the formal pointer symbol *.

summary

map and channel are essentially pointers pointing to the Go runtime structure. With this in mind, let's review the examples mentioned earlier:

// 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)
}

Since map is a pointer, in the function initMap,

data = make(map[string]int)

This sentence is equivalent to re assigning the data pointer. The data pointer inside the function no longer points to the memory address of the runtime.hmap structure corresponding to the external argument data.

Therefore, the modification of data in the function body does not affect the values of the external argument data and the corresponding runtime.hmap structure of data.

The actual running results of the program are as follows:

before init, data == nil: true
in function initMap, data == nil: false
after init, data == nil: true

code

Relevant codes and descriptions are open source in GitHub: Does Go have reference variables and reference passing?

You can also search the official account: coding step up to see more Go knowledge.

References

Topics: Go Back-end