https://en.wikipedia.org/wiki/History_of_Lego

Blocks in Go

Declaration binds identifier to value like package, variable, type etc. After declaration it’s important to know where in the source code identifier refers to specified value (colloquially speaking where such name can be used).

Go is lexically scoped so identifier resolution depends on where in the source code name is declared. It’s completely different approach than in dynamically scoped languages where visibility isn’t related to the place of declaration. Consider Bash script below:

#!/bin/bash
f() {
local v=1
g
}
g() {
echo "g sees v as $v"
}
h() {
echo "h sees v as $v"
}
f
g
h

Variable v is defined in function f but because g is called by f it has access to it:

> ./scope.sh
g sees v as 1
g sees v as
h sees v as

When g is called independently or different function like h is used then variable v is not defined. Visibility in dynamically scoped languages isn’t a static thing (lexical scoping is also called static scoping) but depends on the control flow.

Attempt to compile similar code in Go throws a compilation error:

package main
import “fmt”
func f() {
v := 1
g()
}
func g() {
fmt.Println(v) // "undefined: v"
}
func main() {
f()
}

Lexical scoping in Go uses blocks so it’s important to understand first what block is before trying to understand visibility rules.


Block is a sequence of statements (empty sequence is also valid). Blocks can be nested and are denoted by curly brackets:

package main
import “fmt”
func main() {
{ // start outer block
a := 1
fmt.Println(a)
{ // start inner block
b := 2
fmt.Println(b)
} // end inner block
} // end outer block
}

Besides blocks which are explicitly marked there a few implicit ones:

  • universe block contains all source code,
  • package block contains all package’s source code (package can be spread over several files in a single directory),
  • file block contains file’s source code,
  • for statement is in its own implicit block:
for i := 0; i < 5; i++ {
fmt.Println(i)
}

so variable i declared in init statement can be accessed in condition, post statement and nested block with loop body. Attempt to use i after for statement cause “undefined: i” compilation error.

  • if statement is in its own implicit block:
if i := 0; i >= 0 {
fmt.Println(i)
}

allowing to declare variable which can be used in expression, nested block evaluated when expression is true or block for else clause.

  • switch statement is in its own implicit block:
switch i := 2; i * 4 {
case 8:
fmt.Println(i)
default:
fmt.Println(“default”)
}

In the same way as with if statement it’s possible to use f.ex. short variable declaration to introduce binding available in case clauses.

  • each clause in a switch statement acts like a implicit block
switch i := 2; i * 4 {
case 8:
j := 0
fmt.Println(i, j)
default:
// "j" is undefined here
fmt.Println(“default”)
}
// "j" is undefined here

If each case clause would be a block according to language grammar then it wouldn’t be this extra case. It would require though to use curly brackets for each clause, making code slightly less readable and concise.

  • each clause in select statement acts like a implicit block. It’s analogous case as with clauses in switch statements:
    tick := time.Tick(100 * time.Millisecond)
LOOP:
for {
select {
case <-tick:
i := 0
fmt.Println(“tick”, i)
break LOOP
default:
// "i" is undefined here
fmt.Println(“sleep”)
time.Sleep(30 * time.Millisecond)
}
}
// "i" is undefined here

Scoping (visibility) is described in “Scope in Go”. Blocks play crucial role in defining the whole machinery behind scopes.

Resources