DEV Community

Saiful Islam
Saiful Islam

Posted on

Mastering Scopes in Go

Scopes ins and outs

Basics for all advanced Go concepts, it is scope. Without a rock-solid understanding of scope, your programs will feel confusing and unpredictable—if a variable isn't accessible where you expect it, you'll get that dreaded "undefined" error.

It’s important to understand why they exist. One of the best ways to master scope is to simulate how the computer manages memory (RAM) and allocates space for your variables and functions.

Now, let's dive into scope of Go and see how the computer handles each of them.


The Pillars of Scope in Go

Go classifies scope into a few main categories: global scope, local (or block) scope, and package scope. Understanding how these layers interact and which one takes priority is crucial for writing bug-free Go programs.

Global Scope:

When your program starts running, the Go runtime allocates a dedicated portion of memory (RAM) known as the global scope. Any variable or function declared outside of any function (that is, at the package level) is placed in this space.

package main

import "fmt"

// Global variables A and B
var A = 20 
var B = 30 

func add(x int, y int) {
    // ...
}

func main() {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

In this code, A and B are declared in the global scope. During program startup, Go stores A = 20 and B = 30 in global memory. It also registers references to the add function and the main function at the global level.

Search Priority from: The global scope serves as the final fallback. If the compiler can't find a variable or function in the current local scope (or any enclosing local scopes), it then checks the global scope. If it finds the name there, the code compiles successfully. Otherwise, you'll get an undefined error.

Local Scope and Blocks:

Local scope is created whenever a function (or another block) is executed. Each time you call a function, Go allocates a fresh block of memory for that function’s variables, often on the call stack. A block in Go is defined by opening and closing curly braces {}. Each function, as well as conditional (if/else), loop, or switch block, creates a new local scope.

Consider this example:

func main() {
    // Local to main's scope
    X := 18 

    if X >= 18 {
        // Local to the IF block (a nested scope)
        P := 10 
        fmt.Println("I have", P, "points") // Output: 10
    }

    // P is out of scope here!
    // fmt.Println(P) // Uncommenting this will cause a compile error: undefined P
}
Enter fullscreen mode Exit fullscreen mode

In this snippet, X is a local variable in main's scope. When the if condition is true, Go creates a new, nested scope for the if block, and P is a local variable in that inner scope. As soon as the if block completes, Go clears the memory for P. If you try to use P outside that block, the compiler will say it's undefined because P no longer exists in any active scope.

The Crucial Scope Resolution Sequence

Whenever your code needs to look up a name (variable or function), Go follows this sequence: 1->2->3;

  1. Current Block/Function Scope: Check the innermost local scope first.
  2. Enclosing/Parent Scopes: If not found, look in any outer local scopes (for example, the enclosing function's scope).
  3. Global Scope: Finally, check the global (package-level) scope.

If the name isn't found by the time you reach the global scope, the compiler will give an error.

Real-World Scope Failure Example: If you declare a variable Z inside a function add(), it only lives in the local scope of add().

Trying to use Z in main() will fail with undefined: Z, because Z was never in scope in main().

Package Scope: Cross-File Accessibility

Package scope governs how code is visible across different files and packages.

Multiple Files in the Same Package

If you have multiple Go files in the same package (for example, main.go and add.go both with package main), you need to compile or run them together. For example:

go run main.go add.go
Enter fullscreen mode Exit fullscreen mode

If you run only main.go by itself, Go won’t see the contents of add.go. Any functions or variables declared in add.go would be "undefined" in that case. Running both files together ensures all package-level declarations are included.

Importing from a Custom Package

When using separate packages (for example, you create your own package math_library), Go enforces that only identifiers starting with an uppercase letter are exported. For instance:

// Inside math_library/math.go
package math_library

// Exported: accessible from other packages
func Add(x int, y int) int {
    return x + y
}

// Not exported: starts with a lowercase letter
var money = 100
Enter fullscreen mode Exit fullscreen mode

After doing go mod init example.com for this module, you might use math_library from main.go like this:

package main

import (
    "fmt"
    "example.com/math_library"
)

func main() {
    result := math_library.Add(4, 7) // Works, since Add is exported
    fmt.Println(result)              // Output: 11

    // fmt.Println(math_library.money) // Error: money is unexported (undefined outside math_library--// Not exported: starts with a lowercase letter)
}
Enter fullscreen mode Exit fullscreen mode

This ensures that only the parts of your code you intend to share are visible to other packages.

Variable Shadowing:

A common pitfall in Go is variable shadowing, which occurs when an inner scope declares a variable with the same name as one in an outer scope. The inner declaration "shadows" the outer one within its block.

package main

import "fmt"

var A = 10 // Global A

func main() {
    age := 30 

    if age > 18 {
        // This A shadows the global A inside this if-block
        A := 47 
        fmt.Println(A) // Output 1: 47 (prints the inner A)****After printing it will disappear then or end of it's scope tasks.****
    }

    fmt.Println(A) // Output 2: 10 (prints the global A)
}
Enter fullscreen mode Exit fullscreen mode

Shadowing Example

In the code above:

  1. Globally, A is set to 10.
  2. When the if block executes, a new local scope is created. Inside this block, A is redeclared and set to 47.
  3. The fmt.Println(A) inside the block finds the inner A (47) first, so it prints 47.
  4. When the block finishes, the inner A (47) goes out of scope and is removed.
  5. The next fmt.Println(A) (outside the block) now only sees the global A, which is still 10, so it prints 10.

The inner A temporarily takes precedence, but only within the if block.


Conclusion: Master Simulation, Master Go

Scope is not just an abstract idea—it's a direct reflection of how memory is allocated and cleaned up in your program. By mastering scope, you gain insight into your program’s inner workings. Instead of rote learning, focus on simulating execution flow and memory allocation. Visualize exactly when each variable is created and when it goes out of scope. As you write more Go code, keep imagining those memory blocks for your variables. The clearer your mental model, the fewer surprises (like "undefined" errors) you'll encounter.

Start simulating today—your future self (and your future code) will thank you. Pardon me for not visualizing RAM memory allocations.
All credit goes to Habib vai.

Top comments (0)