Go Variables: Declaration and Initialization Guide

Go is statically typed, meaning every variable has a type known at compile time. The `var` keyword is Go's fundamental way to declare variables, with syntax that puts the type after the variable name.

Key Insights

  • Go offers three declaration styles—var, :=, and const—each with specific use cases that affect readability and scope
  • Every uninitialized variable receives a zero value (0, “”, false, nil) rather than being undefined, eliminating a major class of bugs
  • Type inference is powerful but explicit types improve clarity for complex types, public APIs, and when the intended type differs from the literal’s default

Variable Declaration Basics

Go is statically typed, meaning every variable has a type known at compile time. The var keyword is Go’s fundamental way to declare variables, with syntax that puts the type after the variable name.

var name string
var age int
var isActive bool

This declaration creates variables with their zero values. You can also initialize variables at declaration:

var name string = "Alice"
var age int = 30
var isActive bool = true

When initializing, Go can infer the type, allowing you to omit it:

var name = "Alice"  // string inferred
var age = 30        // int inferred
var count = 42.5    // float64 inferred

Multiple variables of the same type can be declared together:

var x, y, z int
var firstName, lastName string = "John", "Doe"

For grouping related variables, use a declaration block:

var (
    host     string = "localhost"
    port     int    = 8080
    timeout  int    = 30
    useHTTPS bool   = false
)

Package-level variables (declared outside functions) must use var and are accessible throughout the package. Function-level variables are scoped to their containing block.

package main

var globalCounter int = 0  // package-level

func main() {
    var localCounter int = 0  // function-level
    // globalCounter and localCounter both accessible here
}

Short Variable Declaration (:=)

The := operator provides a concise way to declare and initialize variables inside functions. It combines declaration, type inference, and initialization:

func processUser() {
    name := "Alice"
    age := 30
    isActive := true
}

This is equivalent to var name string = "Alice" but more idiomatic for local variables. Multiple variables work naturally:

func parseResponse() {
    statusCode, body, err := makeRequest()
}

The := operator has specific redeclaration rules. In a multi-variable declaration, at least one variable must be new:

func example() {
    x := 10
    // x := 20        // Error: no new variables
    x, y := 20, 30    // OK: y is new, x is reassigned
}

Critical pitfall: variable shadowing. The := operator creates a new variable in the current scope, even if one exists in an outer scope:

func shadowExample() {
    count := 0
    
    if true {
        count := 10  // New variable, shadows outer count
        count++      // Modifies inner count (now 11)
    }
    
    fmt.Println(count)  // Prints 0, not 11
}

To modify the outer variable, use assignment:

func noShadow() {
    count := 0
    
    if true {
        count = 10  // Assigns to outer count
        count++
    }
    
    fmt.Println(count)  // Prints 11
}

The := operator only works inside functions. Package-level variables require var.

Zero Values and Initialization

Unlike languages where uninitialized variables contain garbage or undefined values, Go guarantees every variable has a meaningful zero value:

var i int        // 0
var f float64    // 0.0
var s string     // ""
var b bool       // false
var p *int       // nil
var slice []int  // nil
var m map[string]int  // nil
var ch chan int  // nil

This eliminates entire categories of bugs. You can safely use any declared variable:

var counter int
counter++  // counter is now 1, not undefined

For strings, the zero value is the empty string, not nil:

var message string
if message == "" {  // Safe comparison
    fmt.Println("No message")
}

Slices, maps, and channels have nil as their zero value, but behavior differs. Nil slices can be read (length 0) but nil maps cause panics on write:

var slice []int
fmt.Println(len(slice))  // 0, safe

var m map[string]int
// m["key"] = 1  // Panic! Must initialize with make()
m = make(map[string]int)
m["key"] = 1  // Now safe

Zero values are often useful defaults. For example, a zero-value bytes.Buffer is ready to use:

var buf bytes.Buffer
buf.WriteString("Hello")  // Works immediately

When zero values aren’t appropriate, initialize explicitly:

var (
    retryCount   int    = 3      // Not 0
    timeout      int    = 30     // Not 0
    enableCache  bool   = true   // Not false
)

Type Inference

Go’s compiler infers types from the right-hand side of assignments. With numeric literals, understanding default types is crucial:

var i = 42      // int (platform-specific: int32 or int64)
var f = 42.0    // float64
var c = 'A'     // rune (int32)
var s = "text"  // string

For specific numeric types, use explicit typing:

var port int32 = 8080
var timeout uint = 30
var percentage float32 = 0.95

Type inference works with function returns:

func getConfig() (string, int) {
    return "localhost", 8080
}

host, port := getConfig()  // host is string, port is int

For complex types, explicit declaration improves clarity:

// Unclear
data := map[string]interface{}{
    "users": []map[string]string{},
}

// Better
var data map[string]interface{}
data = map[string]interface{}{
    "users": []map[string]string{},
}

// Best: custom type
type UserData map[string]interface{}
var data UserData

When the literal type differs from your intent, be explicit:

var timeout int64 = 30  // Want int64, not int
var pi float32 = 3.14   // Want float32, not float64

Constants and the const Keyword

Constants are immutable values known at compile time. They’re declared with const:

const Pi = 3.14159
const AppName = "MyApp"
const MaxRetries = 3

Like var, constants support grouping:

const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)

The iota identifier generates enumerated constants, resetting to 0 in each const block:

const (
    Sunday = iota  // 0
    Monday         // 1
    Tuesday        // 2
    Wednesday      // 3
    Thursday       // 4
    Friday         // 5
    Saturday       // 6
)

Use iota with expressions for bit flags:

const (
    FlagNone   = 0
    FlagRead   = 1 << iota  // 1
    FlagWrite                // 2
    FlagExecute              // 4
)

Or skip values:

const (
    _ = iota  // Skip 0
    KB = 1 << (10 * iota)  // 1024
    MB                      // 1048576
    GB                      // 1073741824
)

Untyped constants are flexible and assume the type needed by context:

const Timeout = 30  // Untyped

var seconds int = Timeout      // Becomes int
var duration time.Duration = Timeout * time.Second  // Becomes time.Duration

Typed constants are more restrictive:

const Timeout int = 30

var seconds int = Timeout  // OK
// var duration time.Duration = Timeout * time.Second  // Error: type mismatch

Use untyped constants for flexibility, typed constants when you need type safety.

Best Practices and Common Patterns

Use := for local variables, var for clarity. The short declaration is idiomatic for most function-level variables:

func processOrder(orderID string) error {
    order := fetchOrder(orderID)
    total := calculateTotal(order)
    tax := total * 0.08
    // ...
}

Use var when zero values are meaningful or for clarity:

func handleRequest() {
    var response []byte  // Explicit: starts nil
    var err error        // Clear: will be assigned in conditions
    
    if useCache {
        response, err = getFromCache()
    } else {
        response, err = fetchFromAPI()
    }
}

Group related declarations at package or function level:

var (
    // Database configuration
    dbHost     = os.Getenv("DB_HOST")
    dbPort     = os.Getenv("DB_PORT")
    dbName     = os.Getenv("DB_NAME")
    
    // Cache configuration
    cacheHost  = os.Getenv("CACHE_HOST")
    cachePort  = os.Getenv("CACHE_PORT")
)

Follow naming conventions. Use camelCase for unexported names, PascalCase for exported:

var internalCounter int  // unexported
var GlobalConfig string  // exported

const maxRetries = 3     // unexported
const DefaultTimeout = 30  // exported

Declare variables close to their use:

// Poor: declared far from use
func processData() {
    var result string
    var count int
    var items []Item
    
    // 50 lines of code...
    
    items = fetchItems()
    count = len(items)
}

// Better: declare when needed
func processData() {
    // ... code ...
    
    items := fetchItems()
    count := len(items)
}

Be consistent in similar contexts. If you use := for one configuration variable, use it for all:

// Inconsistent
host := "localhost"
var port = 8080

// Consistent
host := "localhost"
port := 8080

Understanding these declaration styles and their trade-offs makes your Go code more idiomatic and maintainable. Choose the right tool for each situation: := for brevity, var for clarity, and const for immutability.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.