To be clear, Go does not have reference variables, so Go does not have pass-by-reference function call semantics.
Dave Cheney
What is a reference variable?
In languages like C++ you can declare an alias, or an alternate name to an existing variable. This is called a reference variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| #include <stdio.h>
int main() {
// integer type
int a = 10;
// reference type; type annotation: "int &"
// means "reference to an integer variable" type
int &b = a;
int &c = b;
// 10 10 10
printf("%d %d %d\n", a, b, c);
// reference variables share the same memory address
// with the variable which they refer to;
// 0x7ffeea4654f8 0x7ffeea4654f8 0x7ffeea4654f8
printf("%p %p %p\n", &a, &b, &c); // here, & is "address of" operator
// pointer type; type annotation: "int *"
// means "pointer to an integer variable" type
int *p1 = &a;
int *p2 = &a;
int *p3 = &a;
// here, * is "deferencing"(or "content of") operator
// 10 10 10
printf("%d %d %d\n", *p1, *p2, *p3);
// pointer variables have their own memory address
// 0x7ffee82644e0 0x7ffee82644d8 0x7ffee82644d0
printf("%p %p %p\n", &p1, &p2, &p3);
return 0;
}
|
You can see that a, b, and c all refer to the same memory location. A write to a will alter the contents of b and c. This is useful when you want to declare reference variables in different scopes–namely function calls.
On the other hand, pointer variables hold memory addresses as their content. p1, p2, and p3 all hold the same memory address, but they are completely different variables and have their own memory addresses.
Go does not have reference variables
Unlike C++, Go does not have reference variables.
It is NOT possible to create a Go program where two variables share the same storage location in memory. It is possible to create two variables whose contents point to the same storage location, but that is not the same thing as two variables who share the same storage location.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package main
import "fmt"
func main() {
// integer type
var a int
// pointer type; type annotation: "*int"
var b, c *int = &a, &a
// b and c holds the same memory address as their contents
// 0xc000094018 0xc000094018
fmt.Println(b, c)
// but b and c are completely different variables with their own memory addresses
// 0xc0000a2018 0xc0000a2020
fmt.Println(&b, &c)
}
|
In this example, b and c hold the same value(the address of a), however, b and c themselves are stored in unique locations. Updating the contents of b would have no effect on c.
There is no “reference type” in Go
What about struct and map and channel, they are reference types, no?
NOOOOO!
By the way, I’m not mentioning slice here because slice is a special data type associated with its “underlying array” in Go.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package main
import "fmt"
type Person struct {
Age int
}
func modifyAge(p Person) {
p.Age = 100
}
func main() {
john := Person{
Age: 50,
}
modifyAge(john)
fmt.Println(john.Age == 100) // false
}
|
If the struct john
was a C++ style reference variable, we should see true
to be printed out. But it’s not.
So, we can conclude that Go does not have pass-by-reference semantics because Go does not have reference variables.
Maps in structs
Well, how would you explain this example?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package main
import "fmt"
type Person struct {
Skills map[string]int
}
func modifySkills(p Person) {
p.Skills["Writing"] = 6
}
func main() {
john := Person{
Skills: map[string]int{
"Making Mistakes": 10,
},
}
modifySkills(john)
fmt.Printf("%+v", john) // {Skills:map[Making Mistakes:10 Writing:6]}
}
|
A map value is a pointer to a runtime.hmap structure.
Dave Cheney
So, john.Skills
holds the pointer to the content of the map variable, and thus, modifySkills
function modifies the content of the Skills
map variable.
The same holds true for nested maps, structs, and channels.
What is a pointer in Go?
Off-topic, but not entirely.
This Gist explains it awesomely.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| package main
import "fmt"
func main() {
i, j := 42, 2701
fmt.Println(i, j)
fmt.Println(&i, &j)
// you can read "&i" as "address of i"
p := &i
// var p *int
// here, p's type is "pointer pointing to integers"
fmt.Printf("%T\n", p)
// *p
// here, * is an operator that returns what p is pointing to
// it is also called "dereferencing"
fmt.Println(*p)
// changing value of *p will change the value of i
*p = 21
fmt.Println(i)
p = &j
*p = *p / 37
fmt.Println(j)
// this function call mutates the value of i
squareVal(&i)
fmt.Println(i)
}
func squareVal(p *int) {
*p *= *p
// so, return a pointer or a value? up to ya!
// if you return a pointer, the Go garbage collector will have something more todo with the heap.
// just focus on readibility for now
// consider trade-offs when you optimize
fmt.Println(p, *p)
}
|
Alright, that’s it for now.
Happy coding, Gophers!