# Go basics

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

Backend with Go roadmap · Foundations · 16 min read · https://levelcode.org/roadmaps/go-backend/go-basics

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")
}
```

> [!NOTE]
> Since Go 1.22, each iteration of a `for` loop gets a fresh copy of the loop variable. Older code sometimes contains `i := i` inside loops to work around the previous shared-variable behaviour; you no longer need it.

## 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.

> [!TIP]
> Maps and slices are reference-like: passing one to a function copies the small header, not the data. A function that modifies map entries or slice elements changes the caller's data. A function that `append`s must return the new slice, because the caller's header does not see the new length.

## 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.

**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.

## Quiz

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

   A. m now contains a: 1
   B. It fails to compile
   C. It panics at runtime
   D. The assignment is silently ignored

   **Answer: C.** The zero value of a map is nil. Reading a nil map is fine (it returns zero values), but writing to one panics with 'assignment to entry in nil map'. Initialise it with make or a literal first.

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

   A. 5
   B. 4
   C. 0
   D. It panics: index out of range

   **Answer: A.** s has spare capacity, so both appends write into the same backing array at index 3. The second append overwrites the 4 with 5, and t still views that array, so t[3] is 5. This is slice aliasing.

3. Which statement about switch in Go is true?

   A. Each case must end with break or execution falls into the next case
   B. Cases can only compare against constants
   C. A switch must have an expression after the keyword
   D. Only the first matching case runs; there is no implicit fall-through

   **Answer: D.** Go runs the first matching case and stops. Fall-through only happens with the explicit fallthrough keyword. Cases can be any expressions, and a bare switch evaluates boolean cases top to bottom.

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

   A. Keys in insertion order both times
   B. Keys in an unspecified order that may differ between the two loops
   C. Keys sorted alphabetically both times
   D. A compile error: maps must be sorted before ranging

   **Answer: B.** Map iteration order is unspecified, and the runtime deliberately randomises it so code cannot depend on it. To print in a stable order, collect and sort the keys, e.g. slices.Sorted(maps.Keys(m)).
