Last modified: Jan 23, 2026 By Alexander Williams
Go Global Variables: Safe Usage Guide
Global variables are available everywhere in your Go package. They can be convenient. But they are also dangerous if misused.
This guide explains the risks. It shows safe patterns. You will learn to avoid common mistakes.
What Are Global Variables in Go?
A global variable is declared at the package level. It is outside any function. Any function in that package can read or modify it.
Here is a simple example.
package main
import "fmt"
// Global variable declaration
var AppVersion = "1.0.0"
func main() {
fmt.Println("Version:", AppVersion) // Can access it here
printVersion()
}
func printVersion() {
fmt.Println("Also Version:", AppVersion) // And here
}
Version: 1.0.0
Also Version: 1.0.0
The variable AppVersion is global. Both main and printVersion use it.
The Dangers of Global State
Global variables create shared state. This leads to several problems.
Concurrency issues are the biggest risk. Go programs often use goroutines. Multiple goroutines reading and writing a global variable cause data races. The output becomes unpredictable.
Testing becomes difficult. A test that changes a global variable can affect other tests. Tests must run in isolation. Global state breaks that isolation.
Code becomes hard to reason about. Any function might change the global variable. You must trace through the entire program to understand its value.
It creates hidden dependencies. Functions don't declare what they need. They just use the global variable. This makes code rigid and hard to refactor.
Safe Pattern 1: Use Constants for Immutable Values
If a value never changes, use a constant. Constants are safe globally. They cannot be modified, so there is no race condition.
Use this for configuration that is set at compile time.
package config
// Safe: A global constant
const DefaultPort = 8080
// Also safe: A typed constant
const MaxTimeout time.Duration = 30 * time.Second
Constants are your first choice for global values. For more on when to choose them, see our guide on Go Constants vs Variables: When and How to Use.
Safe Pattern 2: Unexported Globals with Accessors
Make the global variable unexported. Control access through exported functions. This is called encapsulation.
An unexported variable starts with a lowercase letter. It is only visible inside its package.
package logger
import "sync"
// unexported global, safe from outside modification
var (
logLevel = "INFO"
levelMutex sync.RWMutex // Protects logLevel
)
// Exported getter function
func GetLogLevel() string {
levelMutex.RLock()
defer levelMutex.RUnlock()
return logLevel
}
// Exported setter function
func SetLogLevel(newLevel string) {
levelMutex.Lock()
defer levelMutex.Unlock()
logLevel = newLevel
}
Now, other packages use logger.GetLogLevel(). They cannot directly change logLevel. The mutex ensures safe concurrent access.
This pattern gives you control. You can add validation or logging in the setter. Learn more about visibility in our Go Exported vs Unexported Variables Guide.
Safe Pattern 3: Dependency Injection (The Best Alternative)
Avoid globals altogether. Pass dependencies as arguments to functions. This is called dependency injection.
It makes dependencies explicit. It makes testing trivial.
package main
import "fmt"
// Config is a struct holding configuration.
type Config struct {
Port int
Env string
}
// NewServer takes its config as an argument.
func NewServer(cfg Config) {
fmt.Printf("Starting server on port %d in %s mode\n", cfg.Port, cfg.Env)
}
func main() {
cfg := Config{Port: 8080, Env: "production"}
NewServer(cfg) // Dependency is injected
}
Starting server on port 8080 in production mode
The Config is not global. It is created in main and passed down. To test NewServer, just pass a test config.
This is the most recommended pattern in Go.
Handling Concurrency with sync.Once
Sometimes you need a global instance initialized once. Use sync.Once. It guarantees initialization runs only one time, even with multiple goroutines.
This is useful for database connections or logger setup.
package database
import (
"database/sql"
"fmt"
"sync"
)
var (
dbConn *sql.DB
once sync.Once
)
// GetDB returns a singleton database connection.
func GetDB() *sql.DB {
once.Do(func() {
var err error
dbConn, err = sql.Open("postgres", "connection_string")
if err != nil {
panic(fmt.Sprintf("Failed to connect: %v", err))
}
})
return dbConn
}
The once.Do function is key. The initialization code inside it will execute exactly once. All callers get the same, safely initialized *sql.DB.
When Are Global Variables Acceptable?
There are a few limited cases where globals are okay.
1. Standard library singletons: Like http.DefaultClient or os.Stdout. These are well-known, stable points.
2. Top-level program configuration: Set via flags in main() and then treated as read-only. Use a mutex if it must change.
3. Package-level caches or registries: Where the shared state is the entire point of the package.
Even in these cases, prefer the unexported pattern with accessors.
Common Pitfall: Accidental Shadowing
Be careful not to shadow a global variable. This happens when you declare a new local variable with the same name.
package main
import "fmt"
var count = 10 // Global variable
func main() {
fmt.Println("Global count:", count) // Prints 10
// Oops! This creates a new local 'count', shadowing the global one.
count := 5
fmt.Println("Local count:", count) // Prints 5
increment()
fmt.Println("Global count after increment:", count) // Still 5! Wrong.
}
func increment() {
count++ // Modifies the global variable
}
Global count: 10
Local count: 5
Global count after increment: 5
The local count hides the global one. The increment function modifies the global, but main reads the local. This causes bugs. Read our guide on Go: Avoid Variable Shadowing Mistakes to prevent this.
Conclusion
Global variables are powerful but risky. They introduce hidden dependencies and concurrency bugs.
Follow these rules for safety. Use constants for immutable values. Use unexported variables with thread-safe accessors. Best of all, use dependency injection and pass values explicitly.
This makes your Go code predictable. It makes it easy to test and maintain. Always question the need for a global. There is usually a better, safer way.
For related topics, explore our guides on Go Variable Declaration: var vs := Explained and Fix Common Go Variable Errors.