http://www.kameleon.co.uk/top-5-business-cliches/

Scopes in Go

Michał Łowicki
golangspec

--

Understanding how scopes work requires prior knowledge about blocks which is covered in “Blocks in Go”.

Scope of identifier is a part of the source code (or even all) where identifier binds to certain value like variable, constant, package etc.

package mainimport “fmt”func main() {
{
v := 1
{
fmt.Println(v)
}
fmt.Println(v)
}
// “undefined: v” compilation error
// fmt.Println(v)
}

It’s easy to predict by seasoned engineer what will be printed by such program:

> ./bin/sandbox
1
1

Last call to fmt.Println is commented out as it causes compilation error. It’ll be clarified soon why is that. In short variable v is out of the scope after curly bracket closing block embracing its declaration.

It’s worth to mention that assigning new value to variable doesn’t affect scoping (aka visibility):

v := 1
{
v = 2 // assignment
fmt.Println(v)
}
fmt.Println(v)

outputs:

>./bin/sandbox
2
2

and it behaves differently than:

v := 1
{
v := 2 // short variable declaration
fmt.Println(v)
}
fmt.Println(v)

which prints:

>./bin/sandbox
2
1

Scoping is linked closely with declaration of identifier (being more precise with the place where identifier is declared).

Variable or constant declaration

The scope of variable identifier encompasses innermost containing block (either implicit one or denoted by curly brackets):

func main() {
{
v := 1
{
fmt.Println(v)
{
fmt.Println(v)
}
}
fmt.Println(v)
}
}

It’s 100% valid code which prints:

> ./bin/sandbox
1
1
1

Scope starts after declaration so code:

func main() {
fmt.Println(v)
v := 1
}

throws a compilation error “undefined: v”. Short variable declaration can be used to declare many variables at once:

a, b := 0, 1

but all identifiers from particular declaration are available right after the whole statement so it’s illegal to write:

a, b := 1, a  // undefined: a

The same scoping rules as with short variable declarations apply to:

  • variable declaration (denoted by “var” keyword)
  • constant declaration (denoted by “const” spec)

In factored variable or constant declaration identifier(s) is defined right after variable specification not the whole statement so:

var (
a = 1 // variable specification no. 1
b = a // variable specification no. 2
)fmt.Println(a, b)

is completely valid and gives:

> ./bin/sandbox
1 1

As with short variable declaration and many identifiers:

var (
a, b = 1, a
)

produces compilation error— “undefined: a”.

Type declaration

Type declaration is just like variable or constant declaration when it goes to variable scope — it encompasses innermost containing block. What is different though is where it starts and this is not at the end of declaration but at the identifier. This extra “space” enables to define recursive types:

type X struct {
name string
next *X
}
x := X{name: “foo”, next: &X{name: “bar”}}
fmt.Println(x.name)
fmt.Println(x.next.name)
fmt.Println(x.next.next)

outputs:

> ./bin/sandbox
foo
bar
<nil>

next field must be a pointer. It’s not possible to make declaration like:

type X struct {
name string
next X
}

since compiler will respond “invalid recursive type X” because while creating type X and in order to calculate its size compiler finds field next of type X for which it has to do the same —define its size so we’ve an infinite recursion. With pointer it’s doable as compiler knows the size of the pointer for desired architecture.

Predeclared identifiers

There is a bunch of built-in identifiers:

  • types: bool, int32, int64, float64, …
  • nil
  • functions: make, new, panic, …
  • constants like true or false

All of them have scope set to universal block so are available everywhere.

Imports

While importing packages their names have scope set to file block. This way can be used only in file having proper import statement and cannot be accessed f.ex. from the whole package:

// sandbox.go
package main
import “fmt”func main() {
fmt.Println(“main”)
f()
}
// utils.go
package main
func f() {
fmt.Println(“f”)
}

While trying to compile such package compiler will fail with message “undefined: fmt in fmt.Println”.

Identifier at the top level

Variables, constants, types, functions declared outside of any function are visible across the whole package (the scope is the package block):

// sandbox.go
package main
func main() {
f()
}
// utils.go
package main
import “fmt”func f() {
fmt.Println(“It works!”)
}

Above package compiles fine and prints:

> ./bin/sandbox
It works!

Functions and methods

Method receiver, function parameter or result variable are visible only in function body — this seems obvious enough to skip examples.

Shadowing

The same identifier cannot be declared twice in the same block. It’s possible though to redeclare it in an inner block (blocks can be nested in a similar way as onion or ogres are layered) . If such redeclared identifier is in scope it denotes innermost declaration:

v := "outer"
fmt.Println(v)
{
v := "inner"
fmt.Println(v)
{
fmt.Println(v)
}
}
{
fmt.Println(v)
}
fmt.Println(v)

Outputs:

> ./bin/sandbox
outer
inner
inner
outer
outer

Resources:

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.