A declaration associates a name with a program entity, such as a function or a variable. The scope of a declaration is the part of the source code where a use of the declared name refers to that declaration.
Don’t confuse scope with lifetime. The scope of a declaration is a region of the program text; it is a compile-time property. The lifetime of a variable is the range of time during execution when the variable can be referred to by other parts of the program; it is a run-time property.
A syntactic block is a sequence of statements enclosed
in braces like those that surround the body of a function or loop.
A name declared inside a syntactic block is not visible outside that
block.
The block encloses its declarations and determines their scope.
We can generalize this notion of blocks to include other groupings of
declarations that are not explicitly surrounded by braces in the
source code; we’ll call them all lexical blocks.
There is a lexical block for the entire source code, called the
universe block; for each package; for each file; for each
for
, if
, and switch
statement; for each case
in a switch
or select
statement; and, of course, for each
explicit syntactic block.
A declaration’s lexical block determines its scope, which may be large
or small.
The declarations of built-in types, functions, and
constants like int
, len
, and true
are in the universe block and
can be referred to throughout the entire program.
Declarations outside any function, that is, at package level,
can be referred to from any file in the same package.
Imported packages, such as fmt
in the tempconv
example,
are declared at the file level, so they can be referred to from
the same file, but not from another file in the same package without
another import
.
Many declarations, like that of the variable c
in the
tempconv.CToF
function, are local, so they can be
referred to only from within the same function or perhaps just a part
of it.
The scope of a control-flow label, as used by
break
, continue
, and goto
statements, is
the entire enclosing function.
A program may contain multiple declarations of the same name so long as
each declaration is in a different lexical block.
For example, you can declare a local variable with the same name as a
package-level variable.
Or, as shown in Section 2.3.3, you can declare a
function parameter called new
, even though a function of
this name is predeclared in the universe block.
Don’t overdo it, though; the larger the scope of the redeclaration,
the more likely you are to surprise the reader.
When the compiler encounters a reference to a name, it looks for a declaration, starting with the innermost enclosing lexical block and working up to the universe block. If the compiler finds no declaration, it reports an “undeclared name” error. If a name is declared in both an outer block and an inner block, the inner declaration will be found first. In that case, the inner declaration is said to shadow or hide the outer one, making it inaccessible:
func f() {} var g = "g" func main() { f := "f" fmt.Println(f) // "f"; local var f shadows package-level func f fmt.Println(g) // "g"; package-level var fmt.Println(h) // compile error: undefined: h }
Within a function, lexical blocks may be nested to arbitrary depth, so
one local declaration can shadow another.
Most blocks are created by control-flow constructs like
if
statements and for
loops.
The program below has three different variables called
x
because each declaration appears in a different lexical block.
(This example illustrates scope rules, not good style!)
func main() { x := "hello!" for i := 0; i < len(x); i++ { x := x[i] if x != '!' { x := x + 'A' - 'a' fmt.Printf("%c", x) // "HELLO" (one letter per iteration) } } }
The expressions x[i]
and x + 'A' - 'a'
each refer to
a declaration of x
from an outer block; we’ll explain that in a moment.
(Note that the latter expression is not equivalent to unicode.ToUpper
.)
As mentioned above, not all lexical blocks correspond to explicit brace-delimited
sequences of statements; some are merely implied.
The for
loop above creates two lexical blocks: the
explicit block for the loop body,
and an implicit block that additionally encloses the
variables declared by the initialization clause, such as i
.
The scope of a variable declared in the implicit block is the
condition, post-statement (i++
), and body of the for
statement.
The example below also has three variables named x
, each
declared in a different block—one in the function body, one in the
for
statement’s block, and one in the loop body—but only two
of the blocks are explicit:
func main() { x := "hello" for _, x := range x { x := x + 'A' - 'a' fmt.Printf("%c", x) // "HELLO" (one letter per iteration) } }
Like for
loops, if
statements and switch
statements
also create implicit blocks in addition to their body blocks.
The code in the following if
-else
chain shows the scope
of x
and y
:
if x := f(); x == 0 { fmt.Println(x) } else if y := g(x); x == y { fmt.Println(x, y) } else { fmt.Println(x, y) } fmt.Println(x, y) // compile error: x and y are not visible here
The second if
statement is nested within the first,
so variables declared within the first statement’s initializer
are visible within the second.
Similar rules apply to each case of a switch statement:
there is a block for the condition and a block for each
case body.
At the package level, the order in which declarations appear has no effect on their scope, so a declaration may refer to itself or to another that follows it, letting us declare recursive or mutually recursive types and functions. The compiler will report an error if a constant or variable declaration refers to itself, however.
In this program:
if f, err := os.Open(fname); err != nil { // compile error: unused: f return err } f.Stat() // compile error: undefined f f.Close() // compile error: undefined f
the scope of f
is just the if
statement, so f
is not
accessible to the statements that follow, resulting in compiler
errors.
Depending on the compiler, you may get an additional error reporting
that the local variable f
was never used.
Thus it is often necessary to declare f
before the condition so
that it is accessible after:
f, err := os.Open(fname) if err != nil { return err } f.Stat() f.Close()
You may be tempted to avoid declaring f
and err
in the
outer block by moving the calls to ReadByte
and Close
inside an else
block:
if f, err := os.Open(fname); err != nil { return err } else { // f and err are visible here too f.Stat() f.Close() }
but normal practice in Go is to deal with the error in the if
block and then return, so that the successful execution path is not
indented.
Short variable declarations demand an awareness of scope.
Consider the program below, which starts by obtaining its current working
directory and saving it in a package-level variable. This could be
done by calling os.Getwd
in function main
, but it might
be better to separate this concern from the primary logic, especially
if failing to get the directory is a fatal error.
The function log.Fatalf
prints a message and calls
os.Exit(1)
.
var cwd string func init() { cwd, err := os.Getwd() // compile error: unused: cwd if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
Since neither cwd
nor err
is already declared in
the init
function’s block, the :=
statement declares
both of them as local variables.
The inner declaration of cwd
makes the outer one inaccessible,
so the statement does not update the package-level cwd
variable
as intended.
Current Go compilers detect that the local cwd
variable is never
used and report this as an error, but they are not strictly required
to perform this check. Furthermore, a minor change, such as the
addition of a logging statement that refers to the local cwd
would defeat the check.
var cwd string func init() { cwd, err := os.Getwd() // NOTE: wrong! if err != nil { log.Fatalf("os.Getwd failed: %v", err) } log.Printf("Working directory = %s", cwd) }
The global cwd
variable remains
uninitialized, and the apparently normal log output obfuscates the
bug.
There are a number of ways to deal with this potential problem.
The most direct is to avoid :=
by declaring err
in a separate var
declaration:
var cwd string func init() { var err error cwd, err = os.Getwd() if err != nil { log.Fatalf("os.Getwd failed: %v", err) } }
We’ve now seen how packages, files, declarations, and statements express the structure of programs. In the next two chapters, we’ll look at the structure of data.
3.137.218.215