Right, so you’ve met the := operator, our cheerful little friend who infers types for us. But sometimes, you need to be more explicit. You need to declare a variable with a specific type, but you’re not quite ready to give it a value yet. Or maybe you are. This is where the var keyword comes in. It’s the more formal, declarative cousin of :=.

The basic syntax is straightforward: you start with var, then the variable name, then the type. If you want to initialize it right away, you tack on = and the value.

var totalCount int          // Declare an int named 'totalCount'
var isEnabled = true        // Declare and initialize; type inferred as bool
var message string = "Hello" // Explicit type AND initializer (a bit redundant)

Let’s break down the different ways you’ll use this and why you’d choose one over the other.

When You Need a Package-Level Scope

Here’s the first rule, and it’s a big one: Outside of a function, every variable declaration needs to start with var, func, or another keyword. The short declaration operator := is not allowed at the package level. The language designers decided the global scope was no place for such informal shenanigans, and honestly, I agree with them. It forces you to be explicit about what you’re exposing.

package main

// This is perfectly legal and normal at the package level.
var GlobalConfig = loadConfig()

// This is a syntax error. The compiler will yell at you.
// appName := "My Cool App"

func main() {
    // But down here in function-land, := is perfectly happy.
    appName := "My Cool App"
    // ... use appName
}

The Power of the Explicit Zero Value

This is arguably the most important reason to use var. When you declare a variable with a type but no explicit initializer, Go will automatically initialize it to its zero value. This isn’t random garbage data from memory; it’s a sensible, type-specific default. This is a core tenet of Go’s philosophy: variables should be safe to use immediately.

func demonstrateZeroValues() {
    var i int       // 0
    var f float64   // 0.0
    var s string    // "" (an empty string)
    var b bool      // false
    var slice []int // nil (no underlying array allocated yet)

    fmt.Printf("int: %d, float64: %f, string: '%s', bool: %t\n", i, f, s, b)
    // Prints: int: 0, float64: 0.000000, string: '', bool: false

    // This is safe. It won't panic... yet.
    fmt.Println("Slice length:", len(slice)) // Prints: Slice length: 0

    // This, however, would cause a runtime panic because the slice is nil.
    // fmt.Println(slice[0])
}

Why is this so brilliant? It eliminates a whole class of bugs. You never have to wonder if you “remembered” to initialize something. The act of declaring it is the initialization. It’s the difference between building a house on a prepared foundation versus a random patch of dirt.

The Occasionally Useful Explicit Type and Initializer

You can, of course, do both: declare the type and assign a value. You’ll see this most often when the type you want isn’t the type the value appears to be. My favorite example is handing a float64 literal to a function that expects a float32. Without the explicit type, the compiler will rightfully complain.

func drawText(x, y float32) {
    // ... draw some text at the coordinates
}

func main() {
    // This doesn't work. 10.5 is an untyped constant, but by default it becomes float64.
    // var x = 10.5
    // drawText(x, 20.0) // Compile Error: cannot use x (type float64) as type float32

    // This works. We explicitly tell the compiler: "This is a float32".
    var x float32 = 10.5
    drawText(x, 20.0)

    // You could also use a conversion, but this is often cleaner.
    drawText(float32(10.5), 20.0)
}

The Subtle Art of Grouping Declarations

If you’re declaring a bunch of related variables, especially at the package level, Go lets you group them together in a var block. This makes your code significantly more readable than having a dozen lines all starting with var.

var (
    // These are all uninitialized, so they get their zero values.
    defaultPort int
    defaultHost string
    isTLS       bool

    // And you can mix in initialized ones. The style is consistent.
    appVersion = "1.0.0"
    buildTime  = getBuildTime() // This function runs at init time.
)

The common pitfall here? It’s mostly visual. Don’t create a massive, sprawling block of 50 variables. Group logically related items. If the block gets too long, it’s a sign that your package might be trying to do too much.

So, when do you reach for var instead of :=? Follow this simple rule: Use var when you need the zero value, when you’re at package scope, or when you need to be explicit about the type for the compiler. For everything else inside a function, the brevity of := is usually your best bet.