Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Go  Module 2 – Basics: values, types and control flow

Variables, short declarations and zero values

Declare Go variables with var and :=, predict the zero value of every type, swap two values in one assignment and find the := that hides an error.

  • Beginner
  • 15 minutes
  • Examples run with Go 1.27.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Declare variables with var and := and choose the right form for each place
  • Predict the zero value of numbers, strings, pointers, slices, maps and structs
  • Swap two values in one assignment and discard a value with the blank identifier
  • Detect a variable shadowed by := in an inner block and fix it

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare variables with var and := and choose the right form for each place
  • Predict the zero value of numbers, strings, pointers, slices, maps and structs
  • Swap two values in one assignment and discard a value with the blank identifier
  • Detect a variable shadowed by := in an inner block and fix it

Before you start

On this page

A Go function asked to read the limit “25O”, with a letter O where a zero belongs, returned 100 and no error. The failure did happen: it was stored in a variable and lost on the way out. Nothing crashed, because every Go variable holds a value from the moment it exists, and the variable the caller saw still held its default.

That bug comes from the ideas of this lesson: how var and := declare variables, the zero value each type starts with, and the blocks that decide which variable a name means. You will choose the right declaration for each place, predict zero values, swap two values in one assignment, and find and fix a shadowed variable.

var gives a variable a type, a value or both

A declaration names a variable and gives it a type and a first value. With var you can write the type, the value or both, and Go works out the rest. This program declares eight variables for a train booking, five inside main and three at package level, outside every function, and prints each one’s type with %T and its value with %v:

declare.go: var in all its forms Go · declare.go
// declare.go: the three forms of var, a var block, and package-level variables.
package main

import "fmt"

// Package-level variables may use each other in any order: Go sets fare and
// seats first, because total depends on them.
var total = fare * seats
var fare = 1999 // rupees
var seats = 3

func main() {
	var berth int         // a type and no value: berth starts at 0
	var coach = "B2"      // a value and no type: coach gets the value's type
	var rate float64 = 18 // both: the constant 18 becomes a float64
	var (
		booked  bool
		waiting = 4
	)
	fmt.Printf("berth   %-7T %v\n", berth, berth)
	fmt.Printf("coach   %-7T %v\n", coach, coach)
	fmt.Printf("rate    %-7T %v\n", rate, rate)
	fmt.Printf("booked  %-7T %v\n", booked, booked)
	fmt.Printf("waiting %-7T %v\n", waiting, waiting)
	fmt.Printf("total   %-7T %v\n", total, total)
}

Output

berth   int     0
coach   string  B2
rate    float64 18
booked  bool    false
waiting int     4
total   int     5997

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run declare.go

The first three lines of output show the three forms:

You write The variable’s type Its first value
var berth int the type you name, int the zero value of int, 0
var coach = "B2" the type of the value, string "B2"
var rate float64 = 18 the type you name; the constant 18 converts to it 18

Without a type, a variable takes the type of its value. A whole number such as 4 gives int, a number with a decimal point such as 4.5 gives float64, and text in quotes gives string. At least one of the two must be there, and var n = nil does not compile, because nil has no type to give. Parentheses group several declarations under one var, as booked and waiting show.

total, fare and seats are package-level variable: A variable declared outside every function. It exists for the whole run, and Go gives it its value before main starts, setting each one after the variables its value depends on.. Go gives them their values before main starts, and it orders that work by what each value needs, not by the order of the lines: total comes first in the file, but its value uses fare and seats, so those two are set first and total comes out as 5997. Inside a function the order of the lines is the order of the work, and a variable can be used only after its declaration.

Checkpoint

Check: var with a type, a value or both

  1. Question 1 of 2 Which type does var price = 1999.0 give price?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: float64

    Without a type, a variable takes its value's type. An untyped floating-point constant such as 1999.0 converts to its default type, float64.

  2. Question 2 of 2 What does this program print?

    Read the code, then choose one answer.

    package main
    
    import "fmt"
    
    var discount = price / 10
    var price = 500
    
    func main() {
        fmt.Println(discount)
    }
    Show the answer: Question 2 of 2

    Answer: 50

    Package-level variables are set before main runs, each one once the variables its value uses are set. price has no dependencies, so it is set first; then discount is 500 / 10, which is 50.

A short declaration declares and assigns in one step

Inside functions there is a shorter form, :=, the short variable declaration: The form name := value, which declares new variables with the types of their values. It may appear only inside functions, and it must declare at least one new variable on its left.. w, h := 1920, 1080 means the same as var w, h = 1920, 1080: two new variables, each with the type of its value. It is shorter than var, and it has two rules of its own. It works only inside functions, so a package-level variable always needs var. And it must declare at least one new variable on its left:

short.go: new variables, and one assigned again Go · short.go
// short.go: := declares new variables, and may assign one that exists.
package main

import (
	"fmt"
	"strconv"
)

func main() {
	w, h := 1920, 1080 // two new variables, typed by their values
	fmt.Printf("w %T %v, h %T %v\n", w, w, h, h)

	n, err := strconv.Atoi("42")  // n and err are new
	m, err := strconv.Atoi("4x2") // m is new; err is assigned again
	fmt.Println(n, m, err)
}

Output

w int 1920, h int 1080
42 0 strconv.Atoi: parsing "4x2": invalid syntax

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run short.go

In m, err := strconv.Atoi("4x2"), m is new and err already exists in the same block, so err is not declared a second time: it is the same variable, given a new value, the error that Atoi returned for the text “4x2”. The specification calls this redeclaration, and it is what lets one err serve a whole series of calls. With nothing new on the left, the line does not compile:

no_new.go: a second := with nothing new Go · no_new.go
// no_new.go: a second := that declares nothing new.
package main

import "fmt"

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

Output (exit status 1)

Printed as an error (standard error)

# command-line-arguments
./no_new.go:8:8: no new variables on left side of :=

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run no_new.go

The compiler stopped at line 8 with no new variables on left side of :=. To give a variable that exists a new value, write a plain assignment, count = 2.

So which form should a variable get? It depends on where it stands and what it starts with. Answer the three questions about one variable of your own, and the table rules out the forms that do not fit:

Interactive · answer and compare Which declaration fits your variable? Answer the three questions about one variable; the table rules out the forms that do not fit and says why.
Where is the variable declared?
What should it hold first?
Must it have a type its value would not give it, such as int64 or float32?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Four ways to declare a variable, against where each may stand and what it starts with
Declaration At package level Starts at zero A type the value lacks In an if or for header
var n int Its type comes from: the type you name; the value is zero Good allowed in and out of functions Good says plainly that 0, "" or nil is the start Good names any type you need Poor an if, for or switch header takes := only
var n = 5 Its type comes from: the value Good allowed in and out of functions Mixed works, but spells the zero value out Poor takes its value's default type, such as int, float64 or string Poor an if, for or switch header takes := only
var n int64 = 5 Its type comes from: the type you name; the value converts Good allowed in and out of functions Mixed works, but spells the zero value out Good the constant 5 becomes an int64 Poor an if, for or switch header takes := only
n := 5 Its type comes from: the value Poor not allowed outside a function Mixed needs the value written: n := 0 Mixed needs a conversion: n := int64(5) Good if n, err := f(); err != nil { … } works
How the answers decide
  • “Where is the variable declared?” At package level, outside every function: rules out n := 5. Short declarations are allowed only inside functions; at package level every declaration starts with a keyword such as var.
  • “Where is the variable declared?” In the header of an if, for or switch: rules out var n int, var n = 5 and var n int64 = 5. The header of an if, for or switch can declare variables only with :=, and they exist until the end of that statement.
  • “What should it hold first?” Its zero value, set later: adds a caveat to var n = 5, var n int64 = 5 and n := 5. These forms must write the zero value out; var n int starts at zero without one.
  • “What should it hold first?” A value you have now: rules out var n int. var n int gives no value, so the variable starts at zero and needs a second line to take yours.
  • “Must it have a type its value would not give it, such as int64 or float32?” Yes: rules out var n = 5 and adds a caveat to n := 5. Without a named type a variable takes the default type of its value, so := needs a conversion such as int64(5) to get another.

In short: := for most variables inside a function, var name Type when the variable should start at its zero value, var with a type and a value when the value’s own type is the wrong one, and var for everything at package level.

Checkpoint

Check: Short declarations and their new-variable rule

  1. Question 1 of 2 What happens when this function body runs?

    Read the code, then choose one answer.

    total := 10
    total := total + 5
    fmt.Println(total)
    Show the answer: Question 1 of 2

    Answer: It does not compile: no new variables on left side of :=

    A short declaration must declare at least one new non-blank variable on its left. Write total = total + 5, or total += 5, to update the variable.

  2. Question 2 of 2 In one block, a, err := f() is followed by b, err := g(). What does the second line do with err?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It assigns g's error to the same err

    This is redeclaration: in a short declaration with at least one new variable, the names already declared in the same block are assigned, not declared again. It lets one err serve a series of calls.

One assignment can set several variables at once

w, h = h, w exchanges two variables in one line, with no temporary variable. It works because an assignment runs in two phases: first every expression on the right is evaluated, then the variables on the left are assigned, from left to right. Step through the swap, and then the same exchange written as two statements:

Interactive · step by step Why w, h = h, w swaps and two lines do not Step through one assignment that swaps two values, then the same exchange written as two statements.
Why w, h = h, w swaps and two lines do not, step 7 of 7. Showing w (1080), h (1920), First value read, Second value read, x (1080), y (1080). In focus: y.x = yy = xw1080h1920First value readh, 1080Second value readw, 1920x1080y10801080

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Two variables. w holds 1920 and h holds 1080, the width and the height of a screen. One statement should exchange them, w, h = h, w.
  2. Phase 1: read. Go first evaluates every expression on the right, in order, before it changes anything. It reads h, which is 1080, then w, which is still 1920, and keeps both values.
  3. Phase 2: assign w. Only then are the assignments carried out, from left to right. w takes the first value read, 1080.
  4. Phase 2: assign h. h takes the second value, 1920, which was read before w changed. The two have swapped with no temporary variable, and the program prints one assignment, 1080 1920.
  5. Two lines instead. The same exchange as two statements uses x and y, again holding 1920 and 1080. Each statement runs to its end before the next one starts.
  6. x = y. x = y copies 1080 into x. The 1920 that x held is gone, and no variable holds it any more.
  7. y = x. y = x copies x into y, and x already holds 1080, so y keeps 1080. The program prints two assignments, 1080 1080.

The two-statement version lost a value: after x = y, the 1920 that x held is gone, so y = x copies 1080 back. The program prints both results:

swap.go: one assignment against two Go · swap.go
// swap.go: one assignment that swaps, and two that lose a value.
package main

import (
	"fmt"
	"strconv"
)

func main() {
	w, h := 1920, 1080
	w, h = h, w // both values on the right are read before either is assigned
	fmt.Println("one assignment:", w, h)

	x, y := 1920, 1080
	x = y
	y = x // x already holds 1080, so y gets 1080 as well
	fmt.Println("two assignments:", x, y)

	_, err := strconv.Atoi("7") // _ throws the number away
	fmt.Println("error:", err)
}

Output

one assignment: 1080 1920
two assignments: 1080 1080
error: <nil>

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run swap.go

When a call returns two values and you need only one, the blank identifier: The name _ (an underscore), which takes a value and throws it away. It lets code ignore a result on purpose, or import a package only for what it does when it starts. More in “Your first Go program: go run and go build” _ takes the place of the other: _, err := strconv.Atoi("7") keeps the error and throws the number away. _ is never a new variable. In a block that already has an err, _, err := … fails like no_new.go, because nothing on its left is new; write _, err = … there.

Checkpoint

Check: Assigning several variables at once

  1. Question 1 of 2 a holds 3 and b holds 8. After the statement a = b and then the statement b = a, what do they hold?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a is 8 and b is 8

    Each statement finishes before the next starts, so the second one reads the value the first one wrote. One assignment, a, b = b, a, reads both values before it assigns either.

  2. Question 2 of 2 Why does this function body not compile?

    Read the code, then choose one answer.

    n, err := strconv.Atoi("12")
    _, err := strconv.Atoi("7")
    fmt.Println(n, err)
    Show the answer: Question 2 of 2

    Answer: _ is never a new variable and err already exists, so the second := declares nothing new

    A short declaration must declare at least one new non-blank variable. Here the only non-blank name is err, which the block already has.

Every variable starts at its type’s zero value

Go never hands you an uninitialised variable. A declaration without a value, a struct field you did not set, the elements of a new array: each starts at the zero value: The value a variable holds when it is given none: 0 for numbers, false for booleans, the empty string for strings, nil for pointers, slices, maps, channels, functions and interfaces, and zeroed fields or elements for structs and arrays. of its type. That is 0 for numbers, false for booleans, the empty string for strings, and nil: The zero value of pointers, slices, maps, channels, functions and interfaces: nothing is there yet. A nil slice or map has length 0 and can be read, but writing to a nil map panics. for pointers, slices, maps, channels, functions and interfaces. Structs and arrays are zeroed part by part. This program declares twelve variables, one of each kind, and gives none of them a value:

zero_values.go: twelve variables, no values Go · zero_values.go
// zero_values.go: what each kind of variable holds before you give it a value.
package main

import "fmt"

type Ticket struct {
	PNR   string
	Seat  int
	Paid  bool
	Fares []int
}

func main() {
	var (
		i   int
		f   float64
		b   bool
		s   string
		p   *int
		sl  []int
		m   map[string]int
		ch  chan int
		fn  func()
		e   error
		arr [3]int
		t   Ticket
	)
	fmt.Println("int, float64, bool:", i, f, b)
	fmt.Printf("string: %q, length %d\n", s, len(s))
	fmt.Println("pointer, slice, map are nil:", p == nil, sl == nil, m == nil)
	fmt.Println("channel, func, error are nil:", ch == nil, fn == nil, e == nil)
	fmt.Println("array:", arr)
	fmt.Printf("struct: %+v\n", t)
	fmt.Println("len of the nil slice and map:", len(sl), len(m))
	fmt.Println("reading a missing key of the nil map:", m["B2"])
	sl = append(sl, 7)
	fmt.Println("append to the nil slice:", sl)
}

Output

int, float64, bool: 0 0 false
string: "", length 0
pointer, slice, map are nil: true true true
channel, func, error are nil: true true true
array: [0 0 0]
struct: {PNR: Seat:0 Paid:false Fares:[]}
len of the nil slice and map: 0 0
reading a missing key of the nil map: 0
append to the nil slice: [7]

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run zero_values.go

Kind of type Zero value In the output
Numbers (int, float64, byte …) 0 0 0
bool false false
string the empty string, length 0 ""
Pointers, slices, maps, channels, functions, interfaces (error too) nil true for each == nil
Arrays every element at its zero value [0 0 0]
Structs every field at its zero value {PNR: Seat:0 Paid:false Fares:[]}

%+v prints a struct with its field names: PNR: is followed by nothing because it holds the empty string, and Fares:[] is a nil slice, printed as empty brackets. The last three lines show that nil is not broken: the nil slice and the nil map both have length 0, reading a key that the nil map lacks gives the zero value of its values, 0, and append turns the nil slice into [7].

Checkpoint

Check: The zero value of each kind of type

  1. Question 1 of 2 Which of these variables are nil before they are given a value?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • var s []string
    • var m map[string]int
    • var p *int

    nil is the zero value of pointers, slices, maps, channels, functions and interfaces. Numbers, booleans, strings, arrays and structs start at zero values of their own.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    type Order struct {
        ID    int
        Note  string
        Items []string
        Paid  bool
    }
    
    var o Order
    fmt.Printf("%+v\n", o)
    Show the answer: Question 2 of 2

    Answer: {ID:0 Note: Items:[] Paid:false}

    A struct's zero value has every field at its own zero value: 0, the empty string, a nil slice and false.

A useful zero value, and a map that refuses writes

Because every variable starts at zero, Go code tries to make the zero value useful. Effective Go points to the standard library: the zero value of bytes.Buffer is an empty buffer ready to use, and the zero value of sync.Mutex is a mutex that nobody holds, ready to lock, so neither needs a constructor. strings.Builder works the same way. A nil map is the case beginners meet first: you can read it, but the first write stops the program.

nil_map.go: a ready builder and a nil map Go · nil_map.go
// nil_map.go: a useful zero value, and a zero value that cannot take writes.
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder // the zero value is an empty builder, ready to use
	b.WriteString("PNR ")
	b.WriteString("4521")
	fmt.Println(b.String())

	var stock map[string]int // the zero value is a nil map: it can be read
	fmt.Println("pens:", stock["pens"], "keys:", len(stock))
	stock["pens"] = 12 // but not written
	fmt.Println("never printed")
}

Output (exit status 1)

PNR 4521
pens: 0 keys: 0

Printed as an error (standard error)

panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
	nil_map.go:17 +0x1cc
exit status 2

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run nil_map.go

The builder needed no setting up, and reading the map worked, but stock["pens"] = 12 stopped the program at line 17 with panic: assignment to entry in nil map. A panic: A run-time error that stops the normal run of a program, such as writing to a nil map or dividing an integer by zero. Unless the program recovers, Go prints the error and the calls that led to it, and the program exits. ends the run, and go run reported the program’s exit status, 2. The specification says that a nil map is like an empty map except that no elements may be added. Give a map storage before its first write, with stock := map[string]int{} or make(map[string]int).

Declaring a map, then writing to it

var stock map[string]int followed by stock["pens"] = 12 panics, because the declaration made a nil map. Write stock := map[string]int{} instead, which makes an empty map that takes writes.

A nil slice has no such problem, because append allocates the storage it needs, as zero_values.go showed with [7]. So of the two collections, only the map needs storage before its first write.

Key idea

Every variable starts at its type’s zero value. Design types whose zero value is a usable start, and give a map storage before you write to it: a nil map can be read but not written.

Checkpoint

Check: Useful zero values and nil maps

  1. Question 1 of 2 var counts map[string]int is followed by counts["a"]++. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The program panics: assignment to entry in nil map

    A nil map is like an empty map that no elements may be added to. The first write panics, so make the map, with a literal or make, before you write to it.

  2. Question 2 of 2 Which declaration lets the next line, stock["pens"] = 12, run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: stock := map[string]int{}

    A map needs storage before its first write: a composite literal such as map[string]int{} or a call to make.

A short declaration inside a block hides the outer variable

A block is the code between a pair of braces, such as the body of an if. A variable declared in a block exists only until its closing brace, and while it exists its name hides any variable of the same name outside, which is called shadowing: Declaring a variable in an inner block with a name that a variable outside already has. Until the inner block's closing brace, the name means the inner variable, and the outer one is hidden.. A short declaration declares new variables whenever the block itself has none of those names yet. So inside an if, limit, err := … makes two new variables, even when the function already has a limit and an err. This is the function from the start of the lesson:

shadow_bug.go: the limit and the error go missing Go · shadow_bug.go
// shadow_bug.go: an inner := makes new variables, and the result is lost.
package main

import (
	"fmt"
	"strconv"
)

func parseLimit(s string) (int, error) {
	limit := 100 // the default when s is empty
	var err error
	if s != "" {
		limit, err := strconv.Atoi(s) // a new limit and a new err
		fmt.Println("  inside the if:", limit, err)
	}
	return limit, err
}

func main() {
	fmt.Println(parseLimit("250"))
	fmt.Println(parseLimit("25O")) // a letter O, not a zero
}

Output

  inside the if: 250 <nil>
100 <nil>
  inside the if: 0 strconv.Atoi: parsing "25O": invalid syntax
100 <nil>

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run shadow_bug.go

Inside the if, the parse worked for “250” and failed for “25O”, yet both calls returned 100 <nil>: the values the function’s own variables started with, which the block never touched. Step through the second call:

Interactive · step by step How := in the if block loses the error Step through parseLimit("25O") and watch which limit and which err each line reads and writes.
How := in the if block loses the error, step 7 of 7. Showing limit (function) (0), err (function) (invalid syntax), return limit (0, invalid syntax). In focus: limit (function), err (function), return limit.limit (function)0err (function)invalid syntaxreturn limit0, invalid syntax

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The function's pair. parseLimit("25O") starts with two variables in the function's block. limit holds the default, 100, and err, declared with var, holds nil.
  2. A new block. s is not empty, so the if block runs. Its line limit, err := strconv.Atoi(s) finds neither name declared in this block yet, so := declares two new variables there.
  3. Atoi fails. Atoi cannot read "25O", which has a letter O where a zero belongs. It returns 0 and an error, and both go into the if block's pair.
  4. The print uses them. The print inside the block reads the inner pair and shows 0 and the error. Every variable is used, so the compiler has nothing to report.
  5. The block ends. At the closing brace the inner limit and err stop existing, and the error goes with them. The function's own pair was never touched.
  6. The return. return limit, err reads the function's variables, 100 and nil. The caller gets a limit that looks valid and no error.
  7. The fix: =. With = instead of :=, the same line assigns the function's limit and err. Atoi's 0 and its error then reach the return, and the caller sees the failure.

The fix is one character, = instead of :=, so that the line assigns the variables the function already has:

shadow_fixed.go: = keeps the function's variables Go · shadow_fixed.go
// shadow_fixed.go: = assigns the variables the function already has.
package main

import (
	"fmt"
	"strconv"
)

func parseLimit(s string) (int, error) {
	limit := 100 // the default when s is empty
	var err error
	if s != "" {
		limit, err = strconv.Atoi(s) // = writes the outer limit and err
	}
	return limit, err
}

func main() {
	fmt.Println(parseLimit("250"))
	fmt.Println(parseLimit("25O"))
}

Output

250 <nil>
0 strconv.Atoi: parsing "25O": invalid syntax

Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run shadow_fixed.go

Now “250” comes back as 250 and “25O” as 0 with its error. The compiler accepted the bug because every variable was used: the print inside the block reads the inner pair. The standard checks of go vet do not look for shadowing either, so the habit that protects you is a question at every := inside a block: do you mean a new variable?

Key idea

Inside a block, := makes new variables that hide the outer ones until the closing brace; to update a variable that already exists, use =.

Checkpoint

Check: Shadowing by := in an inner block

  1. Question 1 of 2 What does this function body print?

    Read the code, then choose one answer.

    total := 0
    if true {
        total := 50
        fmt.Println("inside:", total)
    }
    fmt.Println("after:", total)
    Show the answer: Question 1 of 2

    Answer: inside: 50, then after: 0

    The block's total hides the function's total until the closing brace, so the last line reads the function's variable, still 0.

  2. Question 2 of 2 Which change makes parseLimit in shadow_bug.go return the parse result and its error?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Replace := with = on the line that calls strconv.Atoi

    The bug is a short declaration that made new variables in the block. Assigning with = updates the variables the function already has.

Practise: parse a screen size

Write ParseDimensions, which reads a size such as “1920x1080” and returns the width, the height and an error. Put the two files below in a folder of their own with a go.mod file whose first line is module example.com/dims, and run go test there. The starter returns zeros and no error, so all four sample tests fail, as the run shows:

dims/dims.go: the starter Go · dims/dims.go
// Package dims reads sizes written as width x height.
package dims

// ParseDimensions reads a size such as "1920x1080" and returns the width
// and the height. It returns an error when s is not two whole numbers
// joined by an x.
func ParseDimensions(s string) (w, h int, err error) {
	// Your code: cut s at the "x", convert each part with strconv.Atoi,
	// and return the first error you meet.
	return 0, 0, nil
}
dims/dims_test.go: four sample tests Go · dims/dims_test.go
package dims

import "testing"

func TestParseDimensions(t *testing.T) {
	cases := []struct {
		in      string
		w, h    int
		wantErr bool
	}{
		{"1920x1080", 1920, 1080, false},
		{"640x480", 640, 480, false},
		{"1920y1080", 0, 0, true},
		{"x1080", 0, 0, true},
	}
	for _, c := range cases {
		w, h, err := ParseDimensions(c.in)
		if (err != nil) != c.wantErr {
			t.Errorf("ParseDimensions(%q): err = %v, want an error: %v", c.in, err, c.wantErr)
			continue
		}
		if !c.wantErr && (w != c.w || h != c.h) {
			t.Errorf("ParseDimensions(%q) = %d, %d, want %d, %d", c.in, w, h, c.w, c.h)
		}
	}
}
go test on the starter Python · test_session.py
from session import run

# The sample tests of the exercise, run against the starter code.
run(
    """
cd dims && go test
""",
    expect_failure=["cd dims && go test"],
)

Output

$ cd dims && go test
--- FAIL: TestParseDimensions (0.00s)
    dims_test.go:23: ParseDimensions("1920x1080") = 0, 0, want 1920, 1080
    dims_test.go:23: ParseDimensions("640x480") = 0, 0, want 640, 480
    dims_test.go:19: ParseDimensions("1920y1080"): err = <nil>, want an error: true
    dims_test.go:19: ParseDimensions("x1080"): err = <nil>, want an error: true
FAIL
exit status 1
FAIL	example.com/dims	1.741s
[exit status 1]

This output changes from run to run: go test prints how long the run took, which differs from one run to the next.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 test_session.py

Cut the text at the x with strings.Cut, convert each part with strconv.Atoi, and return the first error you meet. Use the named results w, h and err with =, not :=, inside any if. When the four tests pass, try inputs the tests leave out, such as “ 1920x1080”, “-5x3” and “1920x”, and decide what each one should return.

Last comes a self-check on the six ideas above. Questions on the idea you answered worst so far come first, and each choice explains itself the moment you pick it:

Practice that adapts to you

Practise this lesson

14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 Which types do var a = 7, var b = 7.0 and var c = "7" give the three variables?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: int, float64 and string

    With no type named, a variable takes its value's type, and an untyped constant converts to its default type: int, float64 or string here.

  2. Question 2 of 14 Why does var conn = nil not compile?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: nil has no type for conn to take

    A variable without a named type takes its value's type, and nil does not have one, so the specification forbids using nil to initialise a variable with no explicit type.

  3. Question 3 of 14 What happens with this function body?

    Read the code, then choose one answer.

    fmt.Println(fee)
    fee := 20
    fmt.Println(fee)
    Show the answer: Question 3 of 14

    Answer: It does not compile: undefined: fee

    The scope of a variable declared in a function starts at the end of its declaration. Package-level variables behave differently, which is why total could come before fare and seats in declare.go.

  4. Question 4 of 14 Where can a short variable declaration, name := value, appear?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: Only inside functions, including the header of an if, for or switch

    At package level every declaration starts with a keyword, so a package-level variable always needs var.

  5. Question 5 of 14 What does this function body print?

    Read the code, then choose one answer.

    a, b := 1, 2
    b, c := 3, 4
    fmt.Println(a, b, c)
    Show the answer: Question 5 of 14

    Answer: 1 3 4

    With at least one new variable on the left, the names already declared in the block are only assigned.

  6. Question 6 of 14 After a, b, c := 1, 2, 3 and then a, b, c = b, c, a, what do a, b and c hold?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: 2, 3 and 1

    An assignment evaluates every expression on its right before it assigns any variable, so the values rotate without a temporary variable.

  7. Question 7 of 14 What does _ do in _, err := strconv.Atoi(s)?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: It takes the number and throws it away, so only err is kept

    _ is the blank identifier. It is never a new variable, which is why _, err := needs err to be new.

  8. Question 8 of 14 var s string is declared and never given a value. What is len(s)?

    Type a number.

    Show the answer: Question 8 of 14

    Answer: 0 bytes

    The zero value of a string is the empty string, "", whose length is 0 bytes, as zero_values.go printed.

  9. Question 9 of 14 What does this print?

    Read the code, then choose one answer.

    var seats [3]bool
    fmt.Println(seats)
    Show the answer: Question 9 of 14

    Answer: [false false false]

    Zeroing is recursive: an array's elements and a struct's fields each get the zero value of their own type.

  10. Question 10 of 14 What does this print?

    Read the code, then choose one answer.

    var names []string
    names = append(names, "Asha")
    fmt.Println(len(names), names == nil)
    Show the answer: Question 10 of 14

    Answer: 1 false

    A nil slice works with len, range and append, as zero_values.go showed with [7]. Only a nil map refuses writes.

  11. Question 11 of 14 Which of these are ready to use right after var x T, with no setting up?

    Choose every answer that is right.

    Show the answer: Question 11 of 14

    Answer:

    • strings.Builder, to append text
    • bytes.Buffer, to write bytes
    • sync.Mutex, to lock

    The standard library designs types so that the zero value works. Maps are the common case where it does not, for writing.

  12. Question 12 of 14 A Counter struct has a field hits map[string]int. A caller writes var c Counter, then c.hits["home"]++. What happens, and what is the fix?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: It panics; allocate the map before the first write, for example in the code that writes to it

    A struct is zeroed field by field, so a map field starts nil. Either give it storage in a constructor or allocate it in the method that writes, which lets var c Counter work as it is.

  13. Question 13 of 14 A variable declared with := inside an if block exists until…

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: the closing brace of that block

    While it exists, the block's variable hides any variable of the same name outside, and at the closing brace it is gone with its value.

  14. Question 14 of 14 What happens with this function body?

    Read the code, then choose one answer.

    if n, err := strconv.Atoi("8"); err == nil {
        fmt.Println("parsed", n)
    }
    fmt.Println(n)
    Show the answer: Question 14 of 14

    Answer: It does not compile: undefined: n

    Each if, for and switch statement is its own block, so a variable from its header is gone after the statement. Declare n before the if when you need it afterwards.

Interview questions

Warm-up (fresher): What is the zero value of a map, and what can you do with it?

The zero value of every map type is nil. A nil map can be read: indexing it gives the zero value of its value type, its length is 0, a range loop over it runs no times, and deleting from it does nothing. It cannot be written: the first assignment to a key panics with “assignment to entry in nil map”. So a map that will be written needs storage first, a literal such as map[string]int{} or a call to make. A map field of a struct starts nil too, so give it storage in a constructor, or allocate it in the method that writes to it, before the first write.

Two longer questions of the module bank build on this lesson:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A function that loads a configuration file declares var err error at the top, calls os.ReadFile inside an if block and returns err at the end. Callers get a nil error even when the file is missing. What is the likely bug, how do you confirm it, and how do you keep it out of the codebase?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    You are designing a WordTally type that counts words. Callers should be able to write var t WordTally and call t.Add("go") at once, the way they use strings.Builder. How do you make its zero value useful, and what goes wrong if it holds a map?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • var names a type, a value or both; without a type, a variable takes its value’s type, and package-level variables get their values before main, in the order their values need.
  • := declares and assigns in one step, only inside functions, and it must declare at least one new variable on its left; the variables that already exist in the block are assigned.
  • An assignment evaluates everything on its right before it assigns, so a, b = b, a swaps two variables, and _ throws away a value you do not need.
  • Every variable starts at its type’s zero value: 0, false, the empty string, nil, or zeroed fields and elements.
  • A nil slice can be appended to and a nil map can be read, but the first write to a nil map panics, so give a map storage before you write to it.
  • Inside a block, := makes new variables that hide the outer ones until the closing brace; use = to update the variables you already have.

Terms in this lesson

nil
The zero value of pointers, slices, maps, channels, functions and interfaces: nothing is there yet. A nil slice or map has length 0 and can be read, but writing to a nil map panics.
package-level variable
A variable declared outside every function. It exists for the whole run, and Go gives it its value before main starts, setting each one after the variables its value depends on.
panic
A run-time error that stops the normal run of a program, such as writing to a nil map or dividing an integer by zero. Unless the program recovers, Go prints the error and the calls that led to it, and the program exits.
shadowing
Declaring a variable in an inner block with a name that a variable outside already has. Until the inner block's closing brace, the name means the inner variable, and the outer one is hidden.
short variable declaration
The form name := value, which declares new variables with the types of their values. It may appear only inside functions, and it must declare at least one new variable on its left.
zero value
The value a variable holds when it is given none: 0 for numbers, false for booleans, the empty string for strings, nil for pointers, slices, maps, channels, functions and interfaces, and zeroed fields or elements for structs and arrays.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.