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!