Skip to main content

Scoping

Scope is the region of the program where a variable, constant, function, or type is accessible.

In Go, scope also impacts whether a value is stored on the stack (short-lived, inside function scope) or on the heap (long-lived, escaped variables).

Local Scope (Block Scope)

  • Variables declared inside a block { ... } exist only within that block.
  • Stored on the stack by default.
  • Destroyed (memory reclaimed) when the function exits, unless the variable escapes (referenced outside).
package main
import "fmt"

func main() {
if true {
x := 10 // local to this if-block
fmt.Println("Inside block:", x)
}
// fmt.Println(x) // ❌ ERROR: x not in scope here
}

RAM Impact:

  • x lives on the stack.
  • When the block ends, stack memory is reclaimed.
  • No heap allocation unless a pointer/reference escapes the block.

Function Scope (Local to a Function)

  • Variables declared inside a function are visible only in that function.
  • By default stored on the stack, unless escape analysis moves them to the heap.
package main
import "fmt"

func add(a, b int) int {
result := a + b // scoped to add()
return result
}

func main() {
fmt.Println(add(2, 3))
// fmt.Println(result) // ❌ ERROR: result not accessible here
}

RAM Impact:

  • a, b, and result are stack variables.
  • When add() finishes, stack memory is freed.
  • If we return a pointer to result, Go allocates it on the heap.

Package Scope

  • Variables declared outside any function are visible throughout the package.
  • Stored in the global memory (data segment / heap).
  • Persist for the entire runtime of the program.
  • If the variable name starts with a capital letter, it’s exported and visible to other packages.
var globalMessage = "I am global!" // package scope

func printMessage() {
fmt.Println(globalMessage)
}

func main() {
printMessage() // ✅ Works
fmt.Println(globalMessage) // ✅ Works
}
  • globalMessage is declared outside all functions, so any function in the same file (or package) can use it.

greetings/greetings.go

package greetings

import "fmt"

// Exported variable (starts with Capital letter)
var Message = "Hello from Greetings package"

// Unexported variable (starts with lowercase letter)
var secret = "This is hidden"

// Exported function
func SayHello(name string) {
fmt.Println(Message, name)
}

// Unexported function
func whisper() {
fmt.Println(secret)
}

main.go

package main

import (
"fmt"
"myproject/greetings"
)

func main() {
fmt.Println(greetings.Message) // ✅ Allowed, exported
greetings.SayHello("Masum") // ✅ Allowed, exported function

// ❌ Not allowed (unexported)
// fmt.Println(greetings.secret)
// greetings.whisper()
}

File Scope (Imports, Unexported identifiers)

  • If an identifier starts with a lowercase letter, it is only visible inside the same package file.
  • Works like "private" access modifier.
// file1.go
package main
var hidden = 42 // only inside this package

// file2.go
package main
import "fmt"

func main() {
fmt.Println(hidden) // ✅ accessible within same package
}

RAM Impact:

  • Same as package scope — stored globally in heap, persists for program runtime.

Global Scope (Exported Identifiers)

  • If a name starts with a capital letter, it’s exported and visible to other packages.
// mylib/mylib.go
package mylib
var GlobalVar = 100 // Exported

// main.go
package main
import (
"fmt"
"mylib"
)

func main() {
fmt.Println(mylib.GlobalVar) // ✅ accessible globally
}

RAM Impact:

  • Same as package scope — stored in global heap memory, persists for full runtime.

Lexical Scope (Nested Functions / Closures)

  • Inner functions can access variables from outer functions.
  • If captured, the variables are stored on the heap (escape analysis).
package main
import "fmt"

func outer() func() int {
x := 0
return func() int {
x++ // closure captures x
return x
}
}

func main() {
counter := outer()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
}

RAM Impact:

  • x escapes to the heap because the inner function (closure) needs it after outer() ends.
  • Normally, x would die with the stack frame of outer(), but escape analysis moves it to heap.

Summary of Scopes and Memory

Scope TypeLifetimeMemory LocationExample Use Case
Block ScopeUntil block endsStack (unless escapes)Temporary variables in if, for, {...}
Function ScopeUntil function returnsStack (or heap if escapes)Function-local variables
Package ScopeEntire program runtimeHeap (global data segment)Shared state across package
File ScopeEntire program runtimeHeapPackage-private variables
Global ScopeEntire program runtimeHeapExported vars/constants
Lexical ScopeAs long as closure existsHeapClosures holding variables

Variable Shadowing

  • When a variable with the same name is declared in an inner scope, it hides the variable from the outer scope.
  • The outer variable still exists, but is inaccessible in the shadowed area.
var msg = "Global message"

func main() {
fmt.Println(msg) // "Global message"

msg := "Local message" // shadows global msg
fmt.Println(msg) // "Local message"

{
msg := "Block message" // shadows local msg
fmt.Println(msg) // "Block message"
}

fmt.Println(msg) // "Local message" (block scope ended)
}

Execution with Memory & Scope

package main

import ("fmt")

var first = 5
var second = 10

func display(p int){
p = 15
first = 20
fmt.Println("Value of p is (In - Display) : ",p)
fmt.Println("Value of first is (In - Display) : ",first)
}

func anotherShow(m int){
fmt.Println("Value of a is (In - Another) : ",m)
}

func show(a int){
fmt.Println("Value of a is (In - Show) : ",a)
anotherShow((a))
}

func main(){
x := 5
fmt.Println("Value of x is (Pre - Main) : ",x)
{
fmt.Println("Value of x is (Pre - Local) : ",x)
x := 10
fmt.Println("Value of x is (Post - Local) : ",x)
}
fmt.Println("Value of x is (Post - Main) : ",x)

fmt.Println("Value of first is (Pre - Main) : ",first)
fmt.Println("Value of second is (Pre - Main) : ",second)
show(x)
display(x)
show(x)

fmt.Println("Value of first is (Pre - Main) : ",first)
fmt.Println("Value of second is (Pre - Main) : ",second)

fmt.Println("Value of x is (Last - Main) : ",x)
}

func init(){
fmt.Println("I'm init function")
}

1. Program Load (before main() runs)

Go’s runtime loads your package. It sets up package-scope variables and functions in global memory:

In RAM now (Global Context):

  • Global Variables in Data Segment
    • first = 5
    • second = 10
  • Functions(code pointers, not variables) in Code Segment
    • display
    • anotherShow
    • show
    • main

These live for the entire program lifetime. These don’t live on the stack, they live in the global memory region.

2. Run init() Function Automatically

Before main(), Go always executes init().

  • A stack frame for init() is created.
  • Inside it, no local variables (except temporary for string printing).
  • After printing, the stack frame is destroyed.

RAM after init():

  • Global still has first=5, second=10, functions intact.
  • Stack for init() is popped (freed).

3. Start Execution (main function call)

Go runtime looks for main.main() and starts execution.

A new stack frame for main() is created.

  • Stack (main’s frame)
    • local variable x := 5

4. Enter Block Scope { ... } inside main

  • Creates a new inner block context within main’s stack frame.
  • At entry: it sees the outer x = 5 and prints that.
  • Then x := 10 → new variable shadows outer x.

So now stack has:

Outer x = 5 (main)
Inner x = 10 (block scope, shadows outer)
  • Prints 10.
  • When block ends → inner x is destroyed, only outer x=5 remains.

5. Back in main after block

  • Still has x=5 (outer one).
  • Prints 5.

Then prints first=5, second=10 from global scope.

6. Call show(x)

At this point x=5.

So check weather show() is exist in code segment or not, if exist show(5) is called.

  • New stack frame for show is created

7. Call anotherShow(m)

New stack frame for anotherShow

Prints:

Value of a is (In - Another): 5
  • Then returns → stack frame destroyed.

Back to show, then return → show frame destroyed.

8. Call display(x)

Now still x=5 in main.

So display(5) is called.

  • New stack frame for display

Inside display:

p = 15         // local only, doesn’t affect main’s x
first = 20 // modifies global "first"

Now memory is:

  • Global: first=20, second=10
  • Stack (display): p=15

After return → p destroyed, but global first=20 stays.

9. Call show(x) again

Main’s x is still 5.

So again show(5)anotherShow(5) like before.

10. Back to main

Value of first is (Pre - Main): 20   // because display() changed it
Value of second is (Pre - Main): 10
Value of x is (Last - Main): 5

Execution Summary

  1. Global Context (lives whole program):
    first = 20 (modified during display)
    second = 10
    display(), anotherShow(), show(), main(), init()
    → stored in global memory/heap.
  2. Stack during runtime:
    • main() creates x=5.
    • Inner block creates temporary x=10 (shadow), destroyed after block.
    • Each function call (show, anotherShow, display) makes new stack frames.
    • On return, stack frames are freed.
  3. Variable Lifetime:
    • first, second → live till program ends.
    • x → lives till main ends.
    • Inner block x → lives till block ends.
    • Parameters like a, m, p → live only during function calls.

Resource