Scoping in Go is built around the notion of code blocks. You can find several good explanations of how variable scoping work in Go on Google. I’d like to highlight one slightly unintuitive consequence of Go’s block scoping if you’re used to a language like Python, keeping in mind, this example does not break with Go’s notion of block scoping:
Let’s start with a common pattern in Python:
class Data(object):
def __init__(self, val):
self.val = val
def __repr__(self):
return('Data({})'.format(self.val))
li = [Data(2), Data(3), Data(5)]
print(li)
for d in li:
d.val += 1
print(li)
Output:
[Data(2), Data(3), Data(5)]
[Data(3), Data(4), Data(6)]
Here, we mutate val
on each of the Data
classes in the list and show our result at the end of the loop, confirming that each val
was incremented. However, in Go, a similiar looking construction actually produces a different result:
package main
import "fmt"
func main() {
type Data struct {
val int
}
l := []Data{
{val: 2},
{val: 3},
{val: 5},
}
fmt.Printf("%+v\n", l)
for _, d := range l {
d.val += 1
}
fmt.Printf("%+v\n", l)
}
https://play.golang.org/p/O6vT7s8Qiym
Output:
[{val:2} {val:3} {val:5}]
[{val:2} {val:3} {val:5}]
So why does this happen? Block scoping.
Let’s look at simple example of block scoping:
package main
import "fmt"
func main() {
x := 1
fmt.Println(x)
if true {
x := 2
fmt.Println(x)
}
fmt.Println(x)
}
https://play.golang.org/p/1ebX4Oy92d0
Output:
1
2
1
The value of x
changes after being assigned in the if
statement code block, but is discarded after exiting the block.
We see the same behavior inside a for
loop:
package main
import "fmt"
func main() {
x := "main"
fmt.Println(x)
for i := 0; i < 1; i++ {
x := "for"
fmt.Println(x)
}
fmt.Println(x)
}
https://play.golang.org/p/bOH2ObC5vS1
Output:
main
for
main
You can even create a code block with bare curly braces:
package main
import "fmt"
func main() {
x := "main"
fmt.Println(x)
{
x := "for"
fmt.Println(x)
}
fmt.Println(x)
}
https://play.golang.org/p/N1TWR29F12n
Output:
main
for
main
Now let’s return to our original example:
package main
import "fmt"
func main() {
type Data struct {
val int
}
l := []Data{
{val: 2},
{val: 3},
{val: 5},
}
fmt.Printf("%+v\n", l)
for _, d := range l {
d.val += 1
}
fmt.Printf("%+v\n", l)
}
https://play.golang.org/p/O6vT7s8Qiym
The loop variable d
is in the block scope of the for
loop. As we saw above, anything you do to a variable scoped to a block ceases to exist outside the block. So how can we actually change the value of the val
field on the structs in the slice? We can’t use the loop variable, since it’s a value scoped to inside our loop.
Let’s try and get access the structs directly inside our loop. We’re actually not too far off:
package main
import "fmt"
func main() {
type Data struct {
val int
}
l := []Data{
{val: 2},
{val: 3},
{val: 5},
}
fmt.Printf("%+v\n", l)
for i, _ := range l {
l[i].val += 1
}
fmt.Printf("%+v\n", l)
}
https://play.golang.org/p/uo4ntARc0HG
Output:
[{val:2} {val:3} {val:5}]
[{val:3} {val:4} {val:6}]
Cool. It looks like we got the result we were looking for. But why does our first example work in Python? Python actually is hiding the use of values and pointers/references whereas Go requires us to handle them explictly. Using pointers in Go, we can construct a loop that behaves similarly to our Python example:
package main
import "fmt"
type Data struct {
val int
}
func main() {
l := []*Data{
{val: 2},
{val: 3},
{val: 5},
}
prettyPrint(l)
for _, d := range l {
d.val += 1
}
prettyPrint(l)
}
func prettyPrint(l []*Data) {
out := "["
for i, d := range l {
out += fmt.Sprintf("%+v", d)
if i != len(l)-1 {
out += " "
}
}
out += "]"
fmt.Println(out)
}
https://play.golang.org/p/bX6zbswFc3w
Output:
[&{val:2} &{val:3} &{val:5}]
[&{val:3} &{val:4} &{val:6}]
Note: we are now using a slice of *Data
rather than a slice of Data
. This example works similarly to the one in Python. Even though d
is a variable scoped to the for
loop, its value is a pointer that points to the same memory location as the pointers in l
respectively. We can validate that in the following manner:
package main
import "fmt"
type Data struct {
val int
}
func main() {
l := []*Data{
{val: 2},
{val: 3},
{val: 5},
}
for i, d := range l {
fmt.Println("variable addresses")
println(&d)
println(&l[i])
fmt.Println("pointer addresses")
println(d)
println(l[i])
break
}
}
https://play.golang.org/p/q6l8cdllQy1
Output:
variable addresses
0x1042ff84
0x1042ff9c
pointer addresses
0x1042ff80
0x1042ff80
We see that the memory locations of the variables d
and l[i]
are different, but that they both point to the same memory location – the same struct in l
.
You may wonder how in our earlier example we managed to mutate values in the slice without explicitly using pointers or references. We sidestepped these issues by using the slice l
rather than a variable scoped to the for
loop. The value of l[i]
(where i
is some index) is the same inside and outside the loop and references the same struct in memory. Here’s proof:
package main
func main() {
type Data struct {
val int
}
l := []Data{
{val: 2},
{val: 3},
{val: 5},
}
println(&l[0])
for i, _ := range l {
println(&l[i])
l[i].val += 1
break
}
}
https://play.golang.org/p/cIgaloLhcvQ
Output:
0x1042ff9c
0x1042ff9c
Note: To see a variable’s memory address println(&...)
can be substituted with fmt.Println(unsafe.Pointer(&...))
Hope you enjoyed!