Step 02 of 24

Go basics

Write, run and reason about small Go programs using packages, variables, functions, loops, slices and maps.

16 min 8 sections

Go was designed at Google for building networked services: it compiles to a single fast binary, has a small language you can hold in your head, and ships with an excellent standard library. This lesson gives you the core of the language: how a program is organised, how variables and functions work, the control flow statements, and the two collection types you will use constantly, slices and maps.

Modules, packages and your first program

Go code lives in packages, and packages live in a module: a directory tree with a go.mod file at its root that names the module and the Go version. Create one:

bash
mkdir hello && cd hello
go mod init example.com/hello

Now create main.go:

go
package main

import (
	"fmt"
	"strings"
)

func main() {
	name := "gopher"
	fmt.Println("Hello,", strings.ToUpper(name))
}
bash
go run .
text
Hello, GOPHER

Three rules explain everything in that file:

  • Every file starts with package <name>. The package named main with a function main is special: it builds an executable. Any other package is a library.
  • import brings in other packages by path. You then refer to their contents with the last path element: strings.ToUpper.
  • A name starting with an uppercase letter is exported (visible outside its package); lowercase names are private to the package. fmt.Println is exported; you could never call a lowercase fmt.println.

Go is strict in helpful ways: an unused import or an unused local variable is a compile error, not a warning. go build produces a binary, go fmt (or your editor) formats code to the one standard style, and go vet catches suspicious constructs. Run both often.

Variables, constants and types

Go is statically typed: every variable has a type fixed at compile time. You can declare variables two ways:

go
var count int       // explicit type, starts at its zero value: 0
var greeting = "hi" // type inferred: string
port := 8080        // short declaration, only inside functions
host, debug := "localhost", true

Every type has a zero value, and variables are never uninitialised: 0 for numbers, "" for strings, false for booleans, and nil for pointers, slices, maps, channels, functions and interfaces. Idiomatic Go leans on this; you will often see types designed so their zero value is ready to use.

The basic types are bool, string, int (64 bits on modern machines), int64, uint8 (alias byte), int32 (alias rune, a Unicode code point), float64 and a few more sizes. Go never converts between numeric types implicitly:

go
var n int = 10
var f float64 = 2.5
// total := n * f // compile error: mismatched types int and float64
total := float64(n) * f // 25

Constants are declared with const and can be grouped. iota counts up within a group, which is handy for enumerations:

go
const maxRetries = 3

type Level int

const (
	Debug Level = iota // 0
	Info               // 1
	Warn               // 2
)

Strings are immutable sequences of bytes, usually UTF-8. len(s) counts bytes, not characters, and indexing s[i] gives a byte. Ranging over a string decodes runes:

go
s := "héllo"
fmt.Println(len(s)) // 6: "é" takes two bytes
for i, r := range s {
	fmt.Println(i, string(r)) // 0 h, 1 é, 3 l, 4 l, 5 o
}

Functions

Functions declare parameter types after names, and can return several values. Returning a result plus an error is the most common Go signature you will see:

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

func main() {
	q, err := divide(10, 4)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(q) // 2.5
}

Use the blank identifier _ to discard a value you do not need: _, err := divide(1, 0). A variadic parameter accepts any number of arguments as a slice: func sum(nums ...int) int. Functions are values too, so you can store them in variables and pass them as arguments, which you will use heavily for HTTP handlers.

Control flow

Go has one loop keyword, for, in several shapes:

go
for i := 0; i < 3; i++ { // classic three-part loop
	fmt.Println(i)
}

for i := range 3 { // range over an integer (Go 1.22+): i = 0, 1, 2
	fmt.Println(i)
}

for n < 100 { // like "while"
	n *= 2
}

for { // infinite; leave with break or return
	break
}

if needs no parentheses but always needs braces. It can start with a short statement whose variables are scoped to the if/else chain, which keeps temporary values out of the surrounding scope:

go
if v, ok := cache[key]; ok {
	fmt.Println("hit:", v)
} else {
	fmt.Println("miss")
}

switch compares top to bottom and runs only the first matching case; there is no implicit fall-through, so no break is needed. Cases can list several values, and a switch with no expression is a tidy replacement for long if/else chains:

go
switch method {
case "GET", "HEAD":
	fmt.Println("read")
case "POST":
	fmt.Println("create")
default:
	fmt.Println("other")
}

switch {
case code >= 500:
	fmt.Println("server error")
case code >= 400:
	fmt.Println("client error")
}

Slices

An array has a fixed length that is part of its type: [3]int. You rarely use arrays directly. A slice is a flexible view onto an array, described by a pointer to the array, a length and a capacity.

go
nums := []int{3, 1, 2}       // slice literal
nums = append(nums, 5, 4)    // append returns the updated slice
fmt.Println(nums, len(nums)) // [3 1 2 5 4] 5
fmt.Println(nums[1:3])       // [1 2]: from index 1 up to, not including, 3

buf := make([]string, 0, 10) // length 0, capacity 10
for _, n := range nums {     // range gives index and value; _ discards the index
	buf = append(buf, strconv.Itoa(n))
}

append writes into the existing array if there is spare capacity; otherwise it allocates a bigger one, copies, and returns a slice pointing at the new array. That is why you must always assign its result: s = append(s, x).

The flip side is aliasing: slicing does not copy. Two slices can share an array, and writing through one changes what the other sees:

go
a := []int{1, 2, 3, 4}
b := a[:2] // shares a's array
b[0] = 99
fmt.Println(a) // [99 2 3 4]

b = append(b, 7) // capacity left, so this overwrites a[2]!
fmt.Println(a)   // [99 2 7 4]

When you need an independent copy, make one explicitly with slices.Clone(a). The slices package also covers everyday operations: slices.Sort, slices.Contains, slices.Index, slices.Reverse and slices.Max.

A nil slice has length 0 and works with len, range and append, so you rarely need to check for it.

Maps

A map is a hash table from keys of one type to values of another:

go
ages := map[string]int{"ana": 31, "bo": 27}
ages["cy"] = 45          // insert or overwrite
delete(ages, "bo")       // remove (no-op if absent)
fmt.Println(ages["zed"]) // 0: missing keys return the zero value

if age, ok := ages["ana"]; ok { // "comma ok" distinguishes missing from zero
	fmt.Println("ana is", age)
}

Keys must be comparable with == (strings, numbers, booleans, and structs or arrays of those); slices and maps cannot be keys. Three behaviours catch everyone at least once:

  1. Iteration order is unspecified and deliberately varies between runs. If you need sorted output, sort the keys: for _, k := range slices.Sorted(maps.Keys(ages)).
  2. A nil map can be read but not written. var m map[string]int gives a nil map; m["a"] = 1 panics. Create maps with a literal or make(map[string]int).
  3. Maps are not safe for concurrent writes. A web server handles requests in parallel, so a shared map needs a lock, a topic for the concurrency lesson.

Practice

Write a program that counts word frequencies in a block of text and prints the three most frequent words. Words are separated by whitespace and should be compared case-insensitively, with surrounding punctuation .,!? removed. Ties are broken alphabetically. Use this input:

go
const text = `The cat sat. The dog sat! A cat and a dog? The end.`

Expected output:

text
the 3
a 2
cat 2

Hints: strings.Fields splits on whitespace, strings.ToLower and strings.Trim clean words, and slices.SortFunc sorts with a comparison function that returns a negative number, zero or a positive number.

Show solution
go
package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
	"strings"
)

const text = `The cat sat. The dog sat! A cat and a dog? The end.`

func main() {
	counts := make(map[string]int)
	for _, w := range strings.Fields(text) {
		w = strings.ToLower(strings.Trim(w, ".,!?"))
		if w != "" {
			counts[w]++
		}
	}

	words := slices.Collect(maps.Keys(counts))
	slices.SortFunc(words, func(a, b string) int {
		if c := cmp.Compare(counts[b], counts[a]); c != 0 {
			return c // higher count first
		}
		return cmp.Compare(a, b) // then alphabetical
	})

	for _, w := range words[:min(3, len(words))] {
		fmt.Println(w, counts[w])
	}
}

counts[w]++ works even for new words because a missing key yields 0. maps.Keys returns an iterator, which slices.Collect turns into a slice. Comparing counts[b] with counts[a] (reversed) sorts by descending count. The built-in min (Go 1.21+) guards against texts with fewer than three distinct words. "sat" and "dog" also have a count of 2, but "a" and "cat" come first alphabetically.

Key takeaways

  • A module (go.mod) contains packages; package main with func main builds a program; uppercase names are exported.
  • Every type has a zero value; := declares and infers inside functions; numeric conversions are always explicit.
  • for is the only loop (including range 3 over integers), if can take an init statement, and switch never falls through.
  • Slices are views onto arrays: always assign append's result, and remember that sub-slices share memory.
  • Maps return the zero value for missing keys (use comma-ok to tell), have no stable order, and panic on writes when nil.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.What happens when this code runs: var m map[string]int; m["a"] = 1

  2. 2.Given s := make([]int, 3, 10); t := append(s, 4); s = append(s, 5) — what is t[3]?

  3. 3.Which statement about switch in Go is true?

  4. 4.You range over the same map twice in one program and print its keys. What should you expect?

Finished reading?

Mark it complete to fill in your roadmap.