Short Variable Declaration (:=)

The short variable declaration operator, :=, is the way most Go code declares local variables. Instead of writing out a type, you write a name, the := operator, and an initial value, and the compiler figures out the type for you. It is one of the first pieces of syntax every Go programmer learns, but it hides a few rules — about scope, about when it is legal, and about a sneaky bug called shadowing — that trip up even experienced developers coming from other languages.

Overview / How It Works

In Go, every variable has a type, but you don’t always have to write that type out. The statement x := 5 declares a new variable named x, evaluates the expression 5, and infers that x should have the type of that expression — here, int. This is called type inference: the type is determined once, at compile time, from the right-hand side. After that, x is a normal, statically-typed int variable; Go does not do any dynamic typing afterward. If you write x := 5 and later try x = "hello", the compiler rejects it, because x is permanently an int.

The := operator is shorthand for two things happening together: a var declaration and an assignment. Writing x := 5 is roughly equivalent to var x = 5 (which itself infers int), except := can only be used inside a function body — never at package level. At package (top-level) scope, you must use var, const, func, or type declarations; := is a statement, and the package level only allows declarations, not statements.

A second important feature is that := can declare multiple variables at once, and critically, it does not require that all of them be new. The rule the compiler enforces is: at least one variable on the left-hand side must be new in the current block scope. Any other names on the left that already exist in that exact scope are simply assigned to, not re-declared. This is what makes patterns like value, err := someFunc() followed later by value, err := otherFunc() legal even though err already exists — as long as at least one of the two names (here, value, since it’s reused, or more precisely as long as something is new) is fresh in that scope. If every single name on the left already exists in the same scope, the compiler rejects the statement with “no new variables on left side of :=”.

That last phrase, “in the current block scope,” is the key to understanding shadowing. Every { } block — an if, a for, a function body — introduces its own scope. If you use := with a name that already exists in an outer scope, Go does not reuse the outer variable; it creates a brand-new variable that only exists inside the inner block, and that new variable temporarily hides (shadows) the outer one for the rest of that block. This is legal, compiles cleanly, and is one of the most common sources of real Go bugs, covered in detail below.

Syntax

identifier := expression

identifier1, identifier2 := expression1, expression2
  • identifier — the new variable’s name. It must not already be declared in the exact same block (unless combined with at least one genuinely new name in a multi-variable form).
  • := — the short declaration operator. Declares the variable(s) on the left and infers their types from the right.
  • expression — any valid Go expression: a literal, a function call, an arithmetic result, and so on. Its type (or, for untyped constants, its default type — int for integer literals, float64 for decimal literals, string for string literals) becomes the variable’s type.
  • Only valid inside a function body — including inside if, for, and switch initializer statements. Never at package scope.
Form Example When to use
:= x := 5 Inside a function, when Go can infer the type and you want concise, idiomatic code.
var with inferred type var x = 5 Same inference as :=, but works at package level, or when you want var to stand out (e.g. declaring several related variables together).
var with explicit type var x int64 When you need a specific type that differs from the inferred default, or want just the zero value with no initializer.

Examples

The first example shows the most basic use: declaring a couple of variables and letting Go infer string and int.

package main

import "fmt"

func main() {
	name := "Gopher"
	age := 10
	fmt.Println(name, "is", age, "years old")
}
Output:
Gopher is 10 years old

name is inferred as string from the literal "Gopher", and age is inferred as int from the literal 10. No type annotations were needed anywhere.

The second example shows the “reuse an existing variable” rule with a function that returns a value and an error — the single most common use of := in real Go code.

package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 2)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", result)

	result, err = divide(10, 0)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("result:", result)
}
Output:
result: 5
error: division by zero

The first call uses := because result and err don’t exist yet in main‘s scope. The second call reuses the plain = operator, because both result and err already exist in that same scope — using := again here would still be legal (since it’s allowed when at least one side is reused across calls, though here neither is new so it would actually fail) — which is exactly why Go programmers reach for = once the variables already exist and no new name is being introduced.

The third example is more realistic: parsing a mix of valid and invalid strings, using := inside an if statement’s initializer to scope temporary variables tightly.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	inputs := []string{"42", "7", "notanumber", "15"}
	total := 0

	for _, s := range inputs {
		if n, err := strconv.Atoi(s); err == nil {
			total += n
		} else {
			fmt.Println("skipping invalid value:", s)
		}
	}

	fmt.Println("total:", total)
}
Output:
skipping invalid value: notanumber
total: 64

Here n and err are declared with := right inside the if‘s initializer clause. Their scope is limited to the if/else pair — they don’t leak into the rest of the loop body or beyond it. This is a very idiomatic Go pattern: keep helper variables like an error scoped as tightly as possible.

How It Works Step by Step

Walking through what the compiler does for a statement like n, err := strconv.Atoi(s):

  1. The compiler evaluates the right-hand side expression, strconv.Atoi(s), which returns two values: an int and an error.
  2. For each name on the left (n, err), the compiler checks whether that name is already declared in the exact same block. If a name is new, a variable is created with the type of the corresponding return value.
  3. If a name already exists in that same block, no new variable is created — the existing one is simply assigned the new value, just like = would do.
  4. If every single name on the left already exists in that block, the compiler produces a hard error: “no new variables on left side of :=”.
  5. If a name exists only in an outer block (not the current one), it is treated as new in the current block — a fresh variable is created that shadows the outer one for the remainder of the current block.
  6. The new variable(s) only live for the rest of the enclosing block. Once that block ends (e.g. the closing } of the if), the variable goes out of scope and is eligible for garbage collection (unless something else, like a returned closure, still references it).

Common Mistakes

1. Accidental shadowing inside an if or for block

Because := creates a new variable whenever the name doesn’t exist in the current block, it’s easy to accidentally shadow an outer variable instead of updating it:

package main

import "fmt"

func main() {
	count := 0

	if true {
		count := count + 1
		fmt.Println("inside if:", count)
	}

	fmt.Println("outside if:", count)
}
Output:
inside if: 1
outside if: 0

This compiles without any warning, but it is almost certainly not what the author intended: count := count + 1 inside the if creates a brand-new, block-local count that shadows the outer one. The outer count is read once (to compute 0 + 1) and is never actually modified, so it’s still 0 after the block ends. The fix is to use plain assignment when you mean to update an existing variable rather than declare a new one:

package main

import "fmt"

func main() {
	count := 0

	if true {
		count = count + 1
		fmt.Println("inside if:", count)
	}

	fmt.Println("outside if:", count)
}
Output:
inside if: 1
outside if: 1

2. “No new variables on left side of :=”

If every name on the left of := already exists in the same block, the compiler refuses to compile the program at all:

x := 1
x := 2

This fails to compile with no new variables on left side of :=, because x is not new the second time. Once a variable exists in the current block, use = to change its value instead of trying to re-declare it with :=:

package main

import "fmt"

func main() {
	x := 1
	x = 2
	fmt.Println(x)
}
Output:
2

Best Practices

  • Use := for ordinary local variables inside functions — it’s the idiomatic default in Go and what most Go code (and gofmt-formatted code) looks like.
  • Use var name Type instead when you want the variable’s zero value with no initializer (for example var buf bytes.Buffer), or when the inferred type from := would be wrong (e.g. you need int64 but the literal would infer as int).
  • Keep the scope of variables declared with := as tight as possible — declare them inside the if/for initializer when they’re only needed there, as with the strconv.Atoi example above.
  • Be deliberate about reusing := versus =: if you intend to update an existing variable, use =; only use := when at least one name should genuinely be new.
  • Watch for shadowing whenever you use := inside a nested block with a name that also exists outside it — if in doubt, rename the inner variable or switch to = to make the intent explicit.
  • Remember := only works inside function bodies; package-level variables must use var.

Practice Exercises

  • Write a function that returns two values, a string and an error. In main, call it twice using := the first time and = the second time, printing the result each time.
  • Reproduce the shadowing bug from the Common Mistakes section yourself, but with a for loop instead of an if statement: declare a running total outside the loop, then accidentally shadow it with := inside the loop body. Confirm the total outside the loop stays at its initial value, then fix it.
  • Write a small program that reads three strings (hardcode a slice of strings, no need for real input) and uses strconv.Atoi with an if-initializer := to sum only the ones that parse successfully as integers, printing which ones were skipped. Expected behavior: any non-numeric string should be reported as skipped, and the sum should only include the valid numbers.

Summary

  • := declares one or more new variables and infers their types from the right-hand expression, in a single statement.
  • It only works inside function bodies — never at package level, where var is required instead.
  • In a multi-variable :=, at least one name must be new in the current block; existing names in that same block are simply reassigned.
  • If every name already exists in the current block, the compiler rejects the statement with “no new variables on left side of :=”.
  • Using := with a name that exists only in an outer scope creates a new, shadowing variable local to the inner block — a common source of subtle bugs.
  • Prefer := for concise local declarations, but reach for plain = when you mean to update a variable that already exists, and for var when you need an explicit type or a zero-value declaration.