notexOPENOverview
Notex/Languages/Blocks
02.01LESSON 01 OF 19 · 6 MIN READ

Blocks

Always free to read

Go lets you declare variables in lots of places. You can declare them outside of functions, as the parameters to functions, and as local variables within functions.

Note

So far, you’ve written only the main function, but you’ll write functions with parameters in this topic.

Each place where a declaration occurs is called a block. Variables, constants, types, and functions declared outside of any functions are placed in the package block. You’ve used import statements in your programs to gain access to printing and math functions. They define names for other packages that are valid for the file that contains the import statement. These names are in the file block. All the variables defined at the top level of a function (including the parameters to that function) are in a block. Within a function, every set of braces ({}) defines another block, and in a bit you will see that the control structures in Go define blocks of their own.

You can access an identifier defined in any outer block from within any inner block. This raises the question: what happens when you have a declaration with the same name as an identifier in a containing block? If you do that, you shadow the identifier created in the outer block.

Shadowing Variables

Before I explain what shadowing is, let’s take a look at some code (see Example 3-1). You can run it on The Go Playground.

Example 3-1. Shadowing variables

func main() {
    x := 10
    if x > 5 {
        fmt.Println(x)
        x := 5
        fmt.Println(x)
    }
    fmt.Println(x)
}

Before you run this code, try to guess what it’s going to print out:

  • Nothing prints; the code does not compile

  • 10 on line one, 5 on line two, 5 on line three

  • 10 on line one, 5 on line two, 10 on line three

Here’s what happens:

10
5
10

A shadowing variable is a variable that has the same name as a variable in a containing block. For as long as the shadowing variable exists, you cannot access a shadowed variable.

In this case, you almost certainly didn’t want to create a brand-new x inside the if statement. Instead, you probably wanted to assign 5 to the x declared at the top level of the function block. At the first fmt.Println inside the if statement, you can access the x declared at the top level of the function. On the next line, though, you shadow x by declaring a new variable with the same name inside the block created by the if statement’s body. At the second fmt.Println, when you access the variable named x, you get the shadowing variable, which has the value of 5. The closing brace for the if statement’s body ends the block where the shadowing x exists, and at the third fmt.Println, when you access the variable named x, you get the variable declared at the top level of the function, which has the value of 10. Notice that this x didn’t disappear or get reassigned; there was just no way to access it once it was shadowed in the inner block.

I mentioned in this topic that in some situations I avoid using := because it can make it unclear what variables are being used. That’s because it is very easy to accidentally shadow a variable when using :=. Remember, you can use := to create and assign to multiple variables at once. Also, not all variables on the lefthand side have to be new for := to be legal. You can use := as long as there is at least one new variable on the lefthand side. Let’s look at another program (see Example 3-2), which you can find on The Go Playground.

Example 3-2. Shadowing with multiple assignment

func main() {
    x := 10
    if x > 5 {
        x, y := 5, 20
        fmt.Println(x, y)
    }
    fmt.Println(x)
}

Running this code gives you the following:

5 20
10

Although there was an existing definition of x in an outer block, x was still shadowed within the if statement. That’s because := reuses only variables that are declared in the current block. When using :=, make sure that you don’t have any variables from an outer scope on the lefthand side unless you intend to shadow them.

You also need to be careful to ensure that you don’t shadow a package import. I’ll talk more about importing packages in the related discussion, but you’ve been importing the fmt package to print out the results of your programs. Let’s see what happens when you declare a variable called fmt within your main function, as shown in Example 3-3. You can try to run it on The Go Playground.

Example 3-3. Shadowing package names

func main() {
    x := 10
    fmt.Println(x)
    fmt := "oops"
    fmt.Println(fmt)
}

When you try to run this code, you get an error:

fmt.Println undefined (type string has no field or method Println)

Notice that the problem isn’t that you named your variable fmt; it’s that you tried to access something that the local variable fmt didn’t have. Once the local variable fmt is declared, it shadows the package named fmt in the file block, making it impossible to use the fmt package for the rest of the main function.

The Universe Block

One more block is a little weird: the universe block. Remember, Go is a small language with only 25 keywords. What’s interesting is that the built-in types (like int and string), constants (like true and false), and functions (like make or close) aren’t included in that list. Neither is nil. So, where are they?

Rather than make them keywords, Go considers these predeclared identifiers and defines them in the universe block, which is the block that contains all other blocks.

Because these names are declared in the universe block, they can be shadowed in other scopes. You can see this happen by running the code in Example 3-4 on The Go Playground.

Example 3-4. Shadowing true

fmt.Println(true)
true := 10
fmt.Println(true)

When you run it, you’ll see:

true
10

You must be very careful to never redefine any identifiers in the universe block. If you accidentally do so, you will get some strange behavior. If you’re lucky, you’ll get compilation failures; if not, you’ll have a harder time tracking down the source of your problems.

The ability to shadow identifiers in the universe block has one advantage: It allows Go to add new predeclared identifiers without breaking existing code. Go 1.21 added three new functions to the universe block: clear, min, and max. If you wrote code with Go 1.20 or earlier that used those names for variable or functions, it would violate the Go compatibility guarantee for the code to not compile under Go 1.21 or later. Shadowing the universe block identifiers allows old code to continue working.

Since shadowing is useful in a few instances (later chapters will point them out), go vet does not report it as a likely error. In the related discussion, you’ll learn about third-party tools that can detect accidental shadowing in your code.

Progress is saved on this device.
KEEP EXPLORING

One note leads to another.

Back to the library →
⌕
Search across the entire library ⌘ K