Back to Blog

The Go var Keyword: Zero Values, := and Package-Level Variables

How Go's var keyword works: declaration forms, zero values, var vs :=, shadowing, package-level variables and init order, sentinel errors, and compile-time interface checks.

The Go var Keyword: Zero Values, := and Package-Level Variables

The var keyword declares a variable. You give it a name and a type, an initial value, or both, as in var port int, var host = "localhost" or var ratio float64 = 0.25. A variable declared without a value starts at its type's zero value, which is 0 for numbers, "" for strings, false for booleans and nil for pointers, slices, maps, channels, functions and interfaces. Inside functions most Go code uses the short form name := value instead, but var is the only way to declare a variable at package level (Go spec).

TL;DR

  • var name Type declares a variable at its zero value. var name = value infers the type from the value. var name Type = value does both. A var ( ... ) block groups several declarations.
  • Every variable in Go is initialized. There is no uninitialized memory, and types like sync.Mutex, sync.WaitGroup and bytes.Buffer are ready to use at their zero value.
  • := declares and assigns in one step, but only inside functions. At package level you need var.
  • := needs at least one new name on its left side. Names that already exist in the same scope are reused, and names from an outer scope are shadowed.
  • Shadowing an outer variable with := inside an if block is a common bug. The compiler accepts it, and the shadow analyzer finds it.
  • An unused local variable is a compile error, declared and not used. Unused package-level variables compile.
  • Package-level variables are initialized in dependency order before any init() function runs. Sentinel errors and compiled regular expressions are typical package-level variables.
  • var _ http.Handler = (*Server)(nil) makes the compiler check that a type implements an interface.

How do you declare a variable in Go?

There are three forms, and one grouped block that holds any mix of them:

example.gogo
package main

import (
	"fmt"
	"time"
)

var (
	serviceName        = "billing-api"
	maxRetries   int64 = 3
	requestCount int
)

func main() {
	var port int             // type only: starts at 0
	var host = "localhost"   // value only: the type is inferred as string
	var ratio float64 = 0.25 // both
	var readTimeout, writeTimeout time.Duration
	fmt.Println(serviceName, maxRetries, requestCount, port, host, ratio, readTimeout, writeTimeout)
}

This prints billing-api 3 0 0 localhost 0.25 0s 0s.

With a type and no value, the variable gets the zero value of that type. With a value and no type, the variable takes the value's type, and for an untyped constant like "localhost" or 0.25 that is the default type: string, int, float64, rune, bool or complex128. With both, the value is converted to the declared type, which is how you get an int64 or a float32 from a plain number literal. var maxRetries int64 = 3 is an int64. var maxRetries = 3 is an int.

One declaration can list several names. var readTimeout, writeTimeout time.Duration declares two variables of the same type, both 0s. The grouped var ( ... ) block is common at package level, where it keeps related settings together. Unlike a const block, it doesn't repeat the previous line's expression, so every name without a value gets the zero value.

Like any other name in Go, a variable declared at package level is exported when its name starts with a capital letter. http.DefaultClient and os.Args are exported package-level variables. The Go package keyword covers how exported names work across packages.

What is the zero value in Go?

The zero value is the value a variable holds when you declare it without initializing it. Go has no uninitialized memory, so every variable has a defined value from the moment it exists:

TypeZero value
boolfalse
int, float64 and the other numeric types0
string"" (the empty string)
pointers, slices, maps, channels, functions, interfacesnil
arraysevery element set to its zero value
structsevery field set to its zero value

The same rule applies to memory from new, to struct fields left out of a composite literal and to the elements of a slice created with make. A User struct decoded from JSON that has no email field ends up with Email == "", not with garbage.

Why are zero values useful in Go?

Well-designed types work at their zero value, with no constructor, and the standard library relies on this. A sync.Mutex at its zero value is an unlocked mutex, a sync.WaitGroup is a group with a count of zero and a bytes.Buffer is an empty buffer ready for writes. A struct built from such fields is ready to use as soon as it is declared:

example.gogo
package main

import (
	"bytes"
	"fmt"
	"sync"
)

type RequestCounter struct {
	mu    sync.Mutex
	total int
}

func (c *RequestCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.total++
}

func main() {
	var counter RequestCounter // ready to use, no constructor needed
	var wg sync.WaitGroup
	for range 10 {
		wg.Go(counter.Inc)
	}
	wg.Wait()

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "handled %d requests", counter.total)
	fmt.Println(buf.String())
}

This prints handled 10 requests. None of the three variables needed a New function. Effective Go recommends designing your own types the same way, so that var x T is enough to start using a T.

Which zero values can't you use right away?

Maps, and to a lesser extent pointers and channels. A nil slice works with append, len and range, so var tags []string followed by tags = append(tags, "go") is fine. A nil map can be read but not written, and writing to one panics with assignment to entry in nil map. Dereferencing a nil pointer panics, and sending on a nil channel blocks forever. var vs make in Go goes through these cases and when to reach for make instead.

What is the difference between var and := in Go?

:= is the short variable declaration. retries := 3 means the same as var retries = 3: it declares a new variable and infers its type from the value. It saves writing the keyword and the type, and it is the usual choice inside functions, especially for values returned by a function call:

example.gogo
resp, err := http.Get(healthURL)

The difference is where each one is allowed. := only works inside a function body. At package level every declaration has to start with a keyword, so this fails:

example.gogo
package main

port := 8080

func main() {}
example.texttext
./main.go:3:1: syntax error: non-declaration statement outside function body

var port = 8080 at package level compiles.

Inside functions both forms compile, and the choice comes down to a few conventions:

  1. Use := when you have a value to assign, like cfg := loadConfig() or n, err := w.Write(body).
  2. Use var when you want the zero value on purpose, like var buf bytes.Buffer, var wg sync.WaitGroup or var users []User for a slice you fill with append. var tells the reader that the zero value is the starting point.
  3. Use var with a type when the default type of a literal is wrong. var limit int64 = 1 << 40 or var threshold float32 = 0.8 reads better than limit := int64(1 << 40).
  4. Use var when the variable is assigned later in different branches, as in var status string followed by an if/else that sets it.

Can you redeclare a variable with := in Go?

Partly. := needs at least one new variable on its left side, and any name already declared in the same scope is assigned instead of declared again. This is what lets a function reuse one err across several calls:

example.gogo
package main

import "os"

func main() {
	f, err := os.Open("config.yaml")
	if err != nil {
		return
	}
	defer f.Close()

	info, err := f.Stat() // ok: info is new, err is reused
	_ = info
	f, err := os.Open("secrets.yaml")
	_ = f
}
example.texttext
./main.go:14:9: no new variables on left side of :=

info, err := f.Stat() compiles because info is new. On the last :=, both f and err already exist, so there is nothing to declare. Write f, err = os.Open("secrets.yaml") with a plain = to assign to them. var has no such exception. var err error twice in the same scope fails with err redeclared in this block.

What is variable shadowing in Go?

Shadowing happens when a declaration in an inner scope uses the same name as a variable in an outer scope. The inner variable hides the outer one until the inner scope ends. Every if, for, switch and { } block opens a new scope, and := always declares in the current one. Together, those two rules make this bug easy to write:

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func listenAddr(rawPort string) (string, error) {
	port := 8080
	if rawPort != "" {
		port, err := strconv.Atoi(rawPort)
		if err != nil {
			return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
		}
		fmt.Println("using PORT", port)
	}
	return fmt.Sprintf(":%d", port), nil
}

func main() {
	addr, err := listenAddr("9090")
	fmt.Println(addr, err)
}

This prints using PORT 9090 and then :8080 <nil>. Inside the if block, err is new, so := is allowed, and it declares a new port as well. The parsed value goes into the inner port, which disappears at the closing brace. The function returns the outer port, still 8080. The Go if keyword covers the scope rules of if statements that make this possible.

The fix is to assign to the outer variable instead of declaring a new one. Declare err first and use =:

example.gogo
if rawPort != "" {
	var err error
	port, err = strconv.Atoi(rawPort)
	if err != nil {
		return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
	}
}

The default go vet checks don't report shadowing, because a lot of shadowing is deliberate, like err inside a nested if. The shadow analyzer from the Go team does:

example.texttext
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow) ./...
example.texttext
./main.go:11:3: declaration of "port" shadows declaration at line 9

golangci-lint can run the same check through the govet linter with shadow enabled.

Why does Go say "declared and not used"?

The compiler rejects any local variable that is never read. In the listing above, the print line is the only reason the inner port compiles. Without it the compiler reports declared and not used: port and points at the bug. Assigning a value doesn't count as a use:

example.gogo
package main

import "net/http"

func main() {
	resp, err := http.Get("https://example.com")
	if err != nil {
		return
	}
	retries := 0
	retries = 3
}
example.texttext
./main.go:6:2: declared and not used: resp
./main.go:10:2: declared and not used: retries

The rule only covers local variables. An unused package-level variable, an unused function parameter and an unused named result all compile. When you need to keep a value you don't read yet, assign it to the blank identifier with _ = resp, or leave it out entirely with _, err := http.Get(url).

How do package-level variables work in Go?

A variable declared outside any function belongs to the package. It is visible in every file of the package, exists for the whole life of the program and is initialized before main starts. Package-level variables are initialized by dependency, not in the order they appear in the source:

example.gogo
package main

import (
	"errors"
	"fmt"
)

var (
	usersURL = apiBase + "/users"
	apiBase  = apiHost + "/v2"
	apiHost  = "https://api.internal"
)

var ErrNotFound = errors.New("user not found")

func init() {
	fmt.Println("init sees", usersURL)
}

func main() {
	err := fmt.Errorf("load user 42: %w", ErrNotFound)
	fmt.Println(usersURL, errors.Is(err, ErrNotFound))
}

This prints init sees https://api.internal/v2/users and then https://api.internal/v2/users true. usersURL is declared first but depends on apiBase, which depends on apiHost, so the compiler initializes them in the reverse order. Variables that don't depend on each other are initialized in declaration order. Two variables that depend on each other are an error, initialization cycle for a.

After every package-level variable has its value, the init() functions run, in the order they appear in the source, file by file. A package can have several init() functions, even in one file. Imported packages are fully initialized first, so by the time your init() runs, every package you import is ready. The Go import keyword covers how init() interacts with blank imports.

Why are sentinel errors declared with var?

Because errors.New is a function call, and its result can't be a constant. A sentinel error is a package-level variable that callers compare against with errors.Is:

example.gogo
var (
	ErrNotFound   = errors.New("user not found")
	ErrEmailTaken = errors.New("email already registered")
)

errors.Is compares error values, and each errors.New call returns a distinct value, even with the same text. That is why the error has to be created once, at package level, and shared. Standard library examples include io.EOF, sql.ErrNoRows and http.ErrServerClosed.

The same goes for other values that are expensive to build and never change after startup. var slugPattern = regexp.MustCompile("^[a-z0-9-]+$") compiles the regular expression once when the program starts. It also panics at startup if the pattern is invalid, instead of failing on the first request.

Should you use global variables in Go?

For values that are fixed after startup, like sentinel errors, compiled patterns and lookup tables, yes. For mutable state, usually not. A package-level var db *sql.DB or var cfg Config is shared by every goroutine and every test in the package. Concurrent writes need a mutex, tests can't run in parallel with different values and nothing in a function's signature shows that it depends on the global.

The common alternative is to put dependencies in a struct and pass it around:

example.gogo
type Server struct {
	db     *sql.DB
	logger *slog.Logger
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
	// uses s.db and s.logger instead of package-level variables
}

Each test can then build its own Server with its own database and logger.

What does var _ = mean in Go?

It declares a variable with the blank identifier _ as its name. The value is evaluated, then thrown away. The most common use is a compile-time check that a type implements an interface:

example.gogo
type Server struct{}

func (s *Server) ServeHTTp(w http.ResponseWriter, r *http.Request) {}

var _ http.Handler = (*Server)(nil)

The method name above has a typo, and the declaration turns it into a build error:

example.texttext
./main.go:9:22: cannot use (*Server)(nil) (value of type *Server) as http.Handler value in variable declaration: *Server does not implement http.Handler (missing method ServeHTTP)
		have ServeHTTp(http.ResponseWriter, *http.Request)
		want ServeHTTP(http.ResponseWriter, *http.Request)

Without the check, the typo would only show up where a *Server is passed as an http.Handler, which might be in another package or nowhere at all. (*Server)(nil) is a nil pointer converted to *Server, so the check costs no allocation. Use Server{} instead when the methods have value receivers.

The blank identifier works in multi-value declarations too. var _, offset = time.Now().Zone() keeps only the second result.

How do you declare multiple variables at once in Go?

List the names, separated by commas, and give a matching list of values or a function call that returns that many values:

example.gogo
package main

import (
	"fmt"
	"strings"
)

var host, port = "localhost", 8080

func main() {
	var user, domain, found = strings.Cut("[email protected]", "@")
	fmt.Println(host, port, user, domain, found)
}

This prints localhost 8080 ops levelupgo.dev true. Without a type, each variable gets the type of its own value, so host is a string and port is an int. With a type, like var minPort, maxPort int = 1024, 65535, all of them share it.

Assignment works the same way. a, b = b, a swaps two variables, because Go evaluates every value on the right before it assigns any of them.

Since Go 1.22, loop variables declared with := in a for statement are new variables on each iteration. A closure or goroutine that captures job in for _, job := range jobs sees that iteration's job, not the last one. The Go for keyword shows the goroutine bug this fixed and how to check which behavior a module gets.

When should you use var vs const in Go?

Use const for values known when you write the code that never change, like timeouts, limits and header names. Use var for everything else: values computed at runtime, values read from configuration, and any slice, map, struct or pointer, since those can't be constants. The Go const keyword covers untyped constants, iota and enums.

Where LevelUpGo fits

LevelUpGo teaches Go through exercises that run real Go code in the browser. Go Basics introduces var, := and zero values in its first lessons, alongside constants and types. Go Language Deep Dives has a lesson on scope and shadowing where you find and fix shadowed variables. The Training Ground has short standalone exercises for practicing outside a course. For the other 24 reserved words, see Go keywords: all 25 explained.

FAQ

Is var a keyword in Go?

Yes. var is one of Go's 25 reserved keywords, so you can't use it as a variable, function or type name. It starts a variable declaration, either a single var name Type = value or a grouped var ( ... ) block.

What is the difference between var and := in Go?

:= is a short form of var that always infers the type and only works inside functions. x := 5 and var x = 5 both declare an int with the value 5. var is required at package level and is the clearer choice when you want the zero value or a type other than the literal's default. := can also mix new and existing names, as long as at least one name on the left is new.

What is the zero value of a variable in Go?

It is the value a variable holds when it is declared without an initializer: 0 for numbers, "" for strings, false for booleans and nil for pointers, slices, maps, channels, functions and interfaces. Arrays and structs get the zero value in every element or field. Go has no uninitialized variables.

Can you use := outside a function in Go?

No. At package level every declaration must start with a keyword like var, const, type or func, and port := 8080 fails with syntax error: non-declaration statement outside function body. Write var port = 8080 instead.

Why does Go give a "declared and not used" error?

Because an unused local variable often points to a bug, like a shadowed variable or a forgotten error check, so the Go compiler rejects it. Assigning a value doesn't count as using it. Read the variable, remove it or assign it to the blank identifier with _ = x. Unused package-level variables and function parameters are allowed.

Sources

Write Go like a senior engineer

Interactive lessons in your browser. The first ones are free.

Try a free lessonOr create a free account