After the last article, continue to discuss the following questions:

  1. What is the difference between value passing, pointer passing and reference passing?
  2. Why are slice, map, and channel reference types?
  3. Is slice passing a function by reference in Go? If not, why can I change its value inside the function?

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The document address: https://golang.org/ref/spec#Calls

The official document has clearly stated that there is only one way of value transfer for function parameters in Go. In order to enhance my understanding, I will sort out each way of parameter transfer once more.

Value passed

Value passing is when a function is called and a copy of the actual parameter is passed to the function so that the actual parameter is not affected if it is modified.

Concepts always feel like textbooks. Write some code to verify them.

func main(a) {
	a := 10
	fmt.Printf("%#v\n", &a) // (*int)(0xc420018080)
	vFoo(a)
}

func vFoo(b int) {
	fmt.Printf("%#v\n", &b) // (*int)(0xc420018090)
}
Copy the code

The comment content is the output of my machine, you will get different output if you run it

After the argument a is passed to parameter B of vFoo, inside vFoo, b is allocated space on the stack as a local variable, and the value of A is copied exactly.

After executing the code, we see that a and B have completely different memory addresses, which means that they have the same value (b copies A, of course), but are in different parts of memory, and therefore inside vFoo, a is unaffected by changing the value of B.

The left side of the figure shows the allocation of memory before the call, and the right side shows the variables allocated after the call. Note that even if vFoo’s parameter name is a, the actual participating parameter has its own memory space, because parameter names are for programmers only, as we made clear in the previous article.

Pointer passed

A parameter is a pointer to the address of the argument. A pointer to a parameter is an operation on the argument itself.

Is it in the clouds? Again, code is used to analyze so-called pointer passing.

func main(a) {
	a := 10
	pa := &a
	fmt.Printf("value: %#v\n", pa) // value: (*int)(0xc420080008)
	fmt.Printf("addr: %#v\n", &pa) // addr: (**int)(0xc420088018)
	pFoo(pa)
}

func pFoo(p * int) {
	fmt.Printf("value: %#v\n", p) // value: (*int)(0xc420080008)
	fmt.Printf("addr: %#v\n", &p) // addr: (**int)(0xc420088028)
}
Copy the code

A variable is defined and the address is stored in the pointer variable pa. According to our conclusion, only value is passed in Go, so after the pointer variable PA is passed to the parameter P of the function, the parameter will be a copy of it on the stack, and they will have different addresses respectively, but the value of the two is the same (both are the address of variable A). The above comment is the result of my program running. The addresses of PA and P are unrelated to each other, indicating that value copy occurred in parameter transmission.

In function pFoo, the address of the parameter p is not the same as the address of the argument pa, but their value in memory is the address of variable A, so the value of a can be changed by pointer-related operations.

In the figure, & A represents the address of A, and the value is 0xC420080008

reference

The so-called reference passing refers to passing the address of the actual parameter to the function when calling the function, so that the modification of the parameter in the function will affect the actual parameter.

Since reference passing does not exist in Go, it is often said that reference passing in Go also applies to the types of Slice, Map, and Channel (this is a mistake), so to explain reference passing, please take a look at the C++ code (very simple, of course).

void rFoo(int & ref) {
    printf("%p\n", &ref);// 0x7ffee5aef768
}

int main(a) {
    int a = 10;
	  printf("%p\n", &a);// 0x7ffee7307768
    int & b = a;
    printf("%p\n", &b);// 0x7ffee5aef768
    rFoo(b);
    return 0;
}
Copy the code

Here we simply define a reference in main and pass it to the function rFoo. So what does a proper reference pass look like?

Here b is an alias for A, so a and B must have the same address. If b is passed to ref, ref will be the alias of b, and they will have the same address. By printing information in the rFoo function, you can see that all three have exactly the same address, which is called reference-passing.

There is no reference passing in Go

Function calls in Go only pass values, but type references have reference types: slice, map, channel. Here’s the official line:

There’s a lot of history on that topic. Early on, maps and channels were syntactically pointers and it was impossible to declare or use a non-pointer instance. Also, we struggled with how arrays should work. Eventually we decided that the strict separation of pointers and values made the language harder to use. Changing these types to act as references to the associated, shared data structures resolved these issues. This change added some regrettable complexity to the language but had a large effect on usability: Go became a more productive, comfortable language when it was introduced.

We started with pointer syntax, but for some reason we changed it to a reference, but this reference is different from a C++ reference, which is a structure that shares associated data. A further discussion of this issue will be covered in the Slice article, but now back to today’s topic.

So where does the reference to Go come from? What I think is confusing is the fact that reference types like Map, slice, and channel are passed inside functions and can be modified inside functions.

Slice is used to verify that the three types are passed by value.

func main(a) {
	arr := [5]int{1.3.5.6.7}
	fmt.Printf("addr:%p\n", &arr)// addr:0xc42001a1e0
	s1 := arr[:]
	fmt.Printf("addr:%p\n", &s1)// addr:0xc42000a060

	changeSlice(s1)
}

func changeSlice(s []int) {
	fmt.Printf("addr:%p\n", &s)// addr:0xc42000a080
	fmt.Printf("addr:%p\n", &s[0])// addr:0xc42001a1e0
}
Copy the code

The code defines an array ARR, which is then used to generate a slice. If there is reference passing in go, the address of parameter s should be the same as that of parameter s1 (as shown above in c++). In the actual case, we can see that they have completely different addresses.

This is why we can modify slice inside the function, because when it is passed as an argument, slice itself is a value copy, but it internally references the structure of the corresponding array. So s[0] is a reference to arr[0], which is why it can be modified.

summary

  • In Go, there is only one way to pass function parameters;
  • Slice, map, and channel are all reference types, but they are different from c++;
  • Slice can modify the corresponding array value after passing parameters through a function because slice holds Pointers to the array, not because of reference passing.

The following article attempts to explain why slice must be initialized with make and what it does. What does it do each time it dynamically expands capacity?