Variables and functions in Go, oh my!

Pop quiz! What do you expect this code to do?

func main() {
  x := 1
  f := func() {
    fmt.Printf("x is %d\n", x)
  }
  
  x = 2
  f()
}

Try it at the Go Playground

This is a super-simplified example of something that recently came up reviewing some code at Aqua. Some function and variable names have been changed to protect the innocent.

You can refer to x inside the function f() because it’s in scope. The value for x inside f() is whatever value it has at the point f() is invoked.

But you can’t do this:

func main() {
  f := func() {
    fmt.Printf("x is %d\n", x)
  }
  
  x := 2
  f()
}

You’re not allowed to use variables before they are declared, because the compiler needs to know what type of variables it’s dealing with. That code is not OK, in the same way that this isn’t OK:

func main() {
  fmt.Printf(“x is %d\n”, x)
  x := 2
}

The important point is that the compiler needs to know the type of the variable in advance of your function, but it doesn’t need the value in advance. You just need to declare it, you don’t need to define it. This is fine:

func main() {
  var x int

  f := func() {
    fmt.Printf("x is %d\n", x)
  }
  
  x = 2
  f()
}

We had a bug that was a much more complicated version of this:

func main() {

  y := 1
  x := y

  f := func() {
    fmt.Printf("x is %d\n", x)
  }

  // Getting the right value for y
  y = 2

  f()
}

We were setting the variable x too early, before we had the correct value (though in our real-world case the variables were nested structures, not just ints, so it was harder to see the problem).

The proposed fix looked more like this:

func main() {

  y := 1

  f := func() {
    x := y
    fmt.Printf("x is %d\n", x)
  }

  // Getting the right value for y
  y = 2

  f()
}

This works fine, but it made the definition of f() quite long (remember we are dealing with variables much more complex than ints). This made it harder to read – by the time you got to the end you had forgotten that it was a function definition being assigned to a variable, and not being executed until later.

Following the principle of left-aligning the happy path, I’m a fan of keeping inline definitions short for readability. So the final version looks more like this:

func main() {

  var x int

  f := func() {
    fmt.Printf("x is %d\n", x)
  }

  // Getting the right value for y
  y := 2
  x = y

  f()
}