Code Blocks And Identifier Scopes

This article will explain the code blocks and declaration scopes in Go.

(Please note, the definitions of code blocks and declaration scopes in this article are a little different from the ones in Go specification.)

Code Blocks

In a Go project, there are four kinds of code blocks: Some keywords will open some implicit code blocks.

The local blocks which aren't nested in any other local blocks are called top-level (or package-level) local blocks. Top-level local blocks are all function bodies.

Block hierarchies:

(The differences to Go specification are to make the explanation of identifier shadowing simpler below.)

Here is a picture shows the block hierarchies in a program:

Code blocks are mainly used to explain scopes of declared source code element identifiers.

Source Code Element Declaration Places

There are six kinds of source code elements which can be declared:

Labels are used in the break, continue, and goto statements.

The following table shows which code blocks all sorts of source code elements can be directly declared in:
the universe block package blocks file blocks local blocks
predeclared (built-in elements) (1) Yes
package imports Yes
types (non-builtin) Yes Yes Yes
constants (non-builtin) Yes Yes Yes
variables (non-builtin) (2) Yes Yes Yes
functions (non-builtin) (3) Yes Yes
labels Yes

(1) predeclared elements are documented in builtin package.
(2) excluding struct field variables.
(3) excluding type methods.

Please note,

(BTW, the go/* standard packages think file code blocks can only contain package import declarations.)

The source code elements declared in package blocks but outside any local blocks are called package-level source code elements. Package-level source code elements can be constants, types, variables, or functions, but not labels and package imports.

Each package-level source code element identifier is directly contained both in a package block and a file block.

Scopes Of Declared Source Code Elements

The scope of an declared identifier is the identifiable range of the identifier. Here are the scope definitions for all kinds of source code element identifiers:

The elements declared with the blank identifier are not really declared and have no scopes.

(The predeclared iota is only visible in constant declarations.)

Yoy may have noticed the minor difference of declared identifiers between local type definitions and local variable/constant/alias declarations. The difference means a defined type may be able to reference itself in its definition body. Here is an example to show it more clearly:
package main

func main() {
	// var v int = v   // error: undefined: v
	// const C int = C // error: undefined: C
	/*
	type T = struct {
		*T // invalid recursive type alias T
	}
	*/
	
	// Following type definitions are all valid.
	type T struct {
		*T
		x []T
	}
	type A [5]*A
	type S []S
	type M map[int]M
	type F func(F) F
	type Ch chan Ch
	type P *P
	
	// ...
	var s = make(S, 3)
	s[0] = s
	s = s[0][0][0][0][0][0][0][0]
	
	var m = M{}
	m[1] = m
	m = m[1][1][1][1][1][1][1][1]
	
	var p P
	p = &p
	p = ***********************p
}
And the scope difference between package-level and local declarations:
package main

// Here the two identifiers at each line are the same one.
// They are all not the predeclcared identifiers.
/*
const iota = iota // error: constant definition loop
var true = true   // error: typechecking loop
*/

var a = b   // can reference variables declared later
var b = 123

func main() {
	// The right identifiers in the next two lines
	// are the predecalred ones.
	const iota = iota // ok
	var true = true   // ok
	_ = true
	
	// The following lines fail to compile.
	/*
	var c = d // can't reference variables declared later
	var d = 123
	_ = c
	*/
}

Identifier Shadowing

Except labels, an identifier declared in an outer code blocks can be shadowed by the same identifier declared in code blocks nested in the outer code block.

Labels can’t be shadowed.

If an identifier is shadowed, its scope will exclude the scopes of its shadowing identifiers.

Below is an interesting example. The code contains 4 declared variables named x. A x declared in a deeper block will shadow the xs declared in shallower blocks.
package main

import "fmt"

var p0, p1, p2, p3, p4, p5 *int
var x = 9999 // x#0

func main() {
	p0 = &x
	var x = 888  // x#1
	p1 = &x
	for x := 70; x < 77; x++ {  // x#2
		p2 = &x
		x := x - 70 //  // x#3
		p3 = &x
		if x := x - 3; x > 0 { // x#4
			p4 = &x
			x := -x // x#5
			p5 = &x
		}
	}
	
	fmt.Println(*p0, *p1, *p2, *p3, *p4, *p5) // 9999 888 77 6 3 -3
}

For some circumstances, when identifier shadowings meet short variable declarations, some new gophers may get confused on whether or not a variable in a short variable declaration is new declared. The following example (which has bugs) shows the famous trap in Go. Almost every gopher has ever fallen into the trap in the early days using Go.
package main

import "fmt"
import "strconv"

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		// Some new gophers may think err is an already declared
		// variabled in the following short variable declaration.
		// However, both b and err are new declared here in fact.
		b, err := strconv.ParseBool(s)
		if err != nil {
			return 0, err
		}
		// If exection goes here, a nil error is expected to be
		// returned. But in fact, the outer non-nil error will be
		// returned. The scope of the inner "err" ends at the end
		// of the if clause.
		if b {
			n = 1
		}
	}
	return n, err
}

func main() {
	// Output: 1 strconv.Atoi: parsing "TRUE": invalid syntax
	fmt.Println(parseInt("TRUE"))
}

We can use the go vet -shadow command to list possible mis-shadowing uses.

Go only has 25 keywords. Keywords can't be used as identifiers. Many familiar words in Go are not keywords, such as int, bool, string, len, cap, nil, etc. They are just predeclared (built-in) identifiers. These predeclared idenfitiers are declared in the universe block, so custom defined identifiers can shadow them. Here is a werid example in which many predeclared idenfitiers are shadowed. Its compiles and runs okay.
package main

import (
	"fmt"
)

const len = 3      // shadows the built-in function identifier "len"
var true = 0       // shadows the built-in const identifier "true"
type nil struct {} // shadows the built-in variable identifier "nil"
func int(){}       // shadows the built-in type identifier "int"

func main() {
	fmt.Println("a weird program")
	var print = fmt.Println
	
	var fmt = [len]nil{{}, {}, {}} // shadows the package import "fmt".
	// var n = len(fmt) // sorry, "len" is a constant.
	var n = cap(fmt)    // use built-in cap function instead, :(

	// the "for" keyword will open one implicit local code block and
	// one explicit local code block.
	for true := 0; true < n; true++ {
		var false = fmt[true] // shadows the built-in const
		                      // the built-in identifier "false".
		var true = true+1 // the new declared "true" variable
		                  // shadows the iteration variable "true".
		// fmt.Println(true, false) // sorry, "fmt" is an array.
		print(true, false)
	}
}

/* output:
a weird program
1 {}
2 {}
3 {}
*/

Yes, this example is extreme. It contains many bad practices. Identifier shadowing is useful, but please don't abuse it.


The Go 101 project is hosted on both Github and Gitlab. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

Support Go 101 by playing Tapir's games. Cryptocurrency donations are also welcome:
Bitcoin: 1xucQbv5jujFPPwhyg395ri5yV71hx9g9
Ethereum: 0x5dc4aa2c2bbfaadae373dadcfca11b3358912212