How to Declare Variables in Go

var vs :=

Use var for package-level or typed declarations and := for short, inferred declarations inside functions.

The lease versus the coffee cup

You open a fresh Go file and type name = "Alice". The compiler immediately rejects it. You switch to name := "Alice" and it still complains. You finally try var name string = "Alice" and the file compiles. Coming from Python or JavaScript, where assignment and declaration blur together, Go's strict separation feels like a speed bump. It is not a speed bump. It is a boundary line that keeps your program's memory layout predictable.

Go splits variable creation into two distinct tools. The var keyword handles formal, structural declarations. It lives at the package level, sets explicit types, and guarantees a zero value even when you skip initialization. The := operator handles local, inferred declarations inside functions. It requires at least one new variable on the left side, guesses the type from the right side, and refuses to work outside a function body. One is architectural. The other is tactical.

Think of var like drawing a blueprint for a building. You define the rooms, their dimensions, and their materials before construction begins. := is like handing a contractor a set of tools and saying "build whatever fits in this box." The contractor looks at the tools, figures out the job, and sets up a temporary workspace. When the job is done, the workspace vanishes. The blueprint stays.

How the compiler reads your intent

The Go compiler processes files in two passes. The first pass builds the package namespace. It collects every var, const, type, and func declaration at the top level. During this phase, the compiler needs to know exactly what memory shape each variable occupies. That is why var requires an explicit type or an initializer that reveals the type. The compiler cannot use := here because := is syntactic sugar that expands into a var declaration plus an assignment, and that expansion is only valid inside a function scope.

Inside a function, the compiler switches to stack frame allocation. Variables declared with := live on the stack. The compiler looks at the right-hand expression, determines its type, and allocates exactly enough bytes for it. If you write count := 42, the compiler sees an untyped integer constant, matches it to the default int type for the platform, and reserves 8 bytes on a 64-bit machine. If you write ratio := 3.14, it reserves 8 bytes for a float64. The inference is strict. It never guesses int for a float or string for a byte slice.

Go also enforces a capitalization convention that ties directly to visibility. Names starting with a capital letter are exported to other packages. Names starting with a lowercase letter are private to the current package. This is not a compiler hint. It is a hard rule. The compiler uses the first character to decide linkage. You do not need public or private keywords. The casing does the work.

Declare at the highest scope that actually needs the variable. Scope is your first defense against accidental state mutation.

Minimal example

Here is the simplest contrast between the two declaration styles. The code shows package-level setup versus local function scope.

package main

// count is a package-level variable. It gets a zero value automatically.
var count int

func main() {
	// name is local to main. The compiler infers string from the literal.
	name := "Go"
	// age is local to main. The compiler infers int from the literal.
	age := 25

	// Print demonstrates the difference in scope and initialization.
	println(name, age, count)
}

The var count int line allocates memory in the package's data segment. It starts at 0 because Go guarantees zero values for all types. The := lines allocate stack space inside main. They only exist while main runs. When main returns, the stack frame is discarded. The package variable survives until the process exits.

What happens under the hood

Type inference with := follows a strict set of rules. The compiler examines the right-hand side expression. If it is a constant, the compiler picks the default type for that constant category. Untyped integers become int. Untyped floats become float64. Untyped strings become string. If the right side is a function call or a variable, the compiler uses the exact return type or stored type. You cannot force := to pick a different type. If you need a specific type, you must use var with a type annotation or a type conversion.

Multiple assignment works differently for the two forms. With var, you declare each variable individually or group them in a block. With :=, you can declare several variables at once, and the compiler infers each type independently. You can also mix new variables with existing ones in a single := statement. The compiler only requires that at least one variable on the left side is brand new. This pattern appears constantly when unpacking function returns.

package main

import "fmt"

// splitText returns two strings. The caller decides which to keep.
func splitText(input string) (string, string) {
	return "left", "right"
}

func main() {
	// first is new. second is new. Both get inferred as string.
	first, second := splitText("hello")
	// first is reused. third is new. The compiler allows this because third is new.
	first, third := splitText("world")
	// _ discards the second return value intentionally. It says "I considered it and dropped it."
	first, _ = splitText("final")

	fmt.Println(first, second, third)
}

The underscore _ is a special blank identifier. It does not allocate memory. It tells the compiler to evaluate the expression and throw away the result. Use it when a function returns multiple values but you only need one. Do not use it for errors. Swallowing errors silently breaks the visible failure path that Go relies on. The community accepts the verbose if err != nil { return err } pattern precisely because it forces you to acknowledge failures instead of hiding them behind a blank identifier.

Capitalization controls visibility. Keep implementation details lowercase and export only what other packages actually need.

Realistic example

Package-level configuration and request-scoped data show how the two declaration styles divide labor in production code. Configuration lives at the top level because it is shared across handlers. Request data lives inside functions because it is transient and tied to a single HTTP call.

package main

import (
	"fmt"
	"net/http"
)

// serverPort is package-level. It holds a default value that handlers can read.
var serverPort = 8080

// handleRequest processes an incoming HTTP request.
func handleRequest(w http.ResponseWriter, r *http.Request) {
	// method is local. The compiler infers string from r.Method.
	method := r.Method
	// path is local. The compiler infers string from r.URL.Path.
	path := r.URL.Path

	// status is reused from a previous scope in a real app. Here it is new.
	status := 200

	fmt.Fprintf(w, "%s %s %d", method, path, status)
}

func main() {
	http.HandleFunc("/", handleRequest)
	// fmt.Println is used to demonstrate package-level access.
	fmt.Println("Starting on", serverPort)
}

The var serverPort = 8080 line uses a shorthand var form. You can omit the type when the initializer provides it. This is valid at the package level and inside functions. It is cleaner than var serverPort int = 8080 and avoids the := restriction. The function scope uses := because every variable is fresh, local, and inferred from standard library types. The compiler tracks lifetimes precisely. Nothing leaks.

Let the compiler infer types for local variables. Reserve explicit types for boundaries where clarity matters.

Pitfalls and compiler rejections

The := operator fails in three common scenarios. The first is using it at the package level. The compiler rejects this with non-declaration statement outside function body. The second is reassigning without introducing a new variable. If you write count := 10 after already declaring count, the compiler stops with no new variables on left side of :=. You must use = for plain assignment. The third is type inference collisions. If you try to declare two variables with := but the right-hand side has an ambiguous type, the compiler demands an explicit type annotation.

Shadowing is the most frequent runtime confusion. When you use := inside a nested block like an if or for, you can accidentally create a new variable that hides the outer one. The compiler allows it because a new variable exists in the inner scope. The outer variable remains unchanged. This breaks expectations when you think you are updating a flag. Always check the scope boundaries. If you need to modify an existing variable inside a block, use = instead of :=.

Zero values also cause subtle bugs when developers assume uninitialized variables are nil. In Go, every variable gets a zero value. Slices and maps start as nil. Using a nil slice for append works fine. Using a nil map for assignment panics at runtime with assignment to entry in nil map. The compiler cannot catch this because nil is a valid map value. You must initialize maps with make or a literal before writing to them.

The compiler will not save you from shadowing. Read the scope, not just the name.

When to reach for which

Use var when you need a package-level variable that other files can read or modify. Use var when you want to declare a variable without an immediate value and rely on Go's zero-value guarantee. Use var when you must specify an explicit type that differs from the default inference, such as var count int64 = 0. Use var when you want to group multiple declarations in a single block for readability. Use := when you are inside a function and the type is obvious from the right-hand side. Use := when you are unpacking multiple return values from a function call. Use := when you need to update an existing variable while simultaneously declaring a new one in the same statement. Use plain = when you are reassigning a variable that already exists in the current scope. Use var name Type when you want to document the expected type for future maintainers. Trust the compiler's type checker. Write the declaration that matches the lifetime of the data.

Match the declaration style to the variable's lifetime. Short-lived data gets :=. Long-lived data gets var.

Where to go next