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 —
intfor integer literals,float64for decimal literals,stringfor string literals) becomes the variable’s type. - Only valid inside a function body — including inside
if,for, andswitchinitializer 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):
- The compiler evaluates the right-hand side expression,
strconv.Atoi(s), which returns two values: anintand anerror. - 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. - 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. - 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 :=”.
- 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.
- The new variable(s) only live for the rest of the enclosing block. Once that block ends (e.g. the closing
}of theif), 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 (andgofmt-formatted code) looks like. - Use
var name Typeinstead when you want the variable’s zero value with no initializer (for examplevar buf bytes.Buffer), or when the inferred type from:=would be wrong (e.g. you needint64but the literal would infer asint). - Keep the scope of variables declared with
:=as tight as possible — declare them inside theif/forinitializer when they’re only needed there, as with thestrconv.Atoiexample 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 usevar.
Practice Exercises
- Write a function that returns two values, a
stringand anerror. Inmain, 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
forloop instead of anifstatement: 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.Atoiwith anif-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
varis 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 forvarwhen you need an explicit type or a zero-value declaration.
