Scope and Shadowing
Braces create scope in Go, and a := inside an if can quietly create a second err that the outer code never sees.
if v, err := f(); err != nil { /* v and err exist only inside this if */ }Scope is the part of the program where a name can be used. In Go, scope follows braces. A pair of { } makes a block, and a name declared inside a block is visible from its declaration to the closing brace, then gone.
The levels
- Names declared outside every function are at package scope. Every file in the package sees them.
- A function's parameters and its body form a block.
- Every
{ }inside a function makes another block. ifandformay declare a variable before their condition. That variable belongs to the statement, so it is visible in the condition and the body, and nowhere else.ifruns a block only when its condition is true.
Shadowing
An inner block may declare a name that already exists outside it. That is legal. The inner declaration makes a second, separate variable, and the name now refers to the inner one until the block ends. The outer variable still exists and still holds its old value. This is called shadowing.
Sometimes that is exactly right: a short-lived value stays out of the rest of the function. Sometimes it is a bug, and the classic one is an error. You write if err := save(); err != nil inside a function that has its own err. The := declares a new err for the if only. The function's own err is never touched, so whatever checks it later sees nothing wrong.
What trips people up
Shadowing compiles cleanly, and the names look identical when you read the code.
Example
package main
import "fmt"
func main() {
message := "outer"
// A bare pair of braces is a block. A name declared inside it is
// gone at the closing brace.
{
message := "inner" // a second, separate variable with the same name
fmt.Println(message)
}
// The outer message was never changed.
fmt.Println(message)
count := 0
// The part before the semicolon declares doubled. It exists only
// inside this if statement.
if doubled := count + 2; doubled > 1 {
fmt.Println("doubled:", doubled)
}
// fmt.Println(doubled) here would not compile: doubled is out of scope.
fmt.Println("count:", count)
}
// Output:
// inner
// outer
// doubled: 2
// count: 0When to use it
- Working out why an error check passed but the failure was never reported.
- Keeping a temporary value confined to a few lines of a long function.
- Reviewing a change where a new := inside an if hides the function's existing err.
- Choosing variable names that do not hide an imported package in the same file.
More examples
The shadowed error, and the fix
In a longer function the outer err is usually what gets returned to the caller, so the shadowed version means a real failure is reported as success. When a function keeps one err for its whole body, use plain = inside the if, or move the call onto its own line.
package main
import (
"errors"
"fmt"
)
func save() error {
return errors.New("disk full")
}
func main() {
var err error
// := declares a NEW err for this if only. The outer err is untouched.
if err := save(); err != nil {
fmt.Println("inside:", err)
}
fmt.Println("outer:", err)
// The fix: assign to the existing err, then test it.
err = save()
if err != nil {
fmt.Println("fixed:", err)
}
}
// Output:
// inside: disk full
// outer: <nil>
// fixed: disk fullShadowing a package name
Imported package names live in the file's scope, so any parameter or variable with the same name hides the package for the rest of that block. This one at least fails to compile. Common collisions to avoid as variable names are sort, url, path, time, user and context.
package report
import "sort"
// The parameter is called sort, so inside this function the name sort
// means a string, not the imported package.
func writeAll(lines []string, sort string) {
// sort.Strings(lines)
// compile error: sort.Strings undefined (type string has no field or method Strings)
_ = sort
_ = lines
}Finding shadowed variables in a project
The shadow check is not part of go vet's default set, because plenty of shadowing is deliberate and the check reports those too. Running it occasionally over a package that handles errors, or in continuous integration with the noise reviewed once, is how teams catch the silent err case.
# Build the shadow analyzer from the golang.org/x/tools module,
# then hand the binary to go vet.
go vet -vettool="$(go env GOPATH)/bin/shadow" ./...
# The default checks do not include it, so a plain run stays quiet:
go vet ./...
Discussion