Go Guide

Go, chapter by chapter · Lesson 12 of 62 · about 50 min

Next steps: Go since the book

Error wrapping, generics, iterators and the newer standard library: everything added to Go since 2016 that you need for the rest of this course.

On this page · 10 sections
  1. 01What changed, by version
  2. 02Errors that carry context
  3. 03Generics
  4. 04Iterators
  5. 05Smaller conveniences
  6. 06The full program
  7. 07You are ready for algorithms
  8. 08Summary
  9. 09Check your understanding
  10. 10Practice in Go

What changed, by version#

VersionYearFeatureYou use it for
1.11–1.162018–21modules, go.modevery project (replaces GOPATH)
1.132019error wrapping: %w, errors.Is, errors.Asadding context to errors
1.162021os.ReadFile, embedfiles; ioutil deprecated
1.182022genericsreusable data structures
1.212023min, max, clear built-ins; slices, maps, cmp packageseveryday code
1.222024range over integers; fresh loop variable per iteration; HTTP patternsloops, servers
1.232024iterators: range over functions, iter.Seqcustom collections
1.242025b.Loop() in benchmarks; generic type aliasestesting

Errors that carry context#

A bare error like file not found is hard to debug. Wrap it with what you were doing, using %w:

Go
if err != nil {
	return fmt.Errorf("load config %s: %w", path, err)
}

The error now reads load config app.json: open app.json: no such file or directory, and the original is still inside. Two functions look through the chain:

FunctionQuestion it answersExample
errors.Is(err, target)is this specific error anywhere in the chain?errors.Is(err, fs.ErrNotExist)
errors.As(err, &target)is there an error of this type, and give it to meextract a *ValidationError to read its fields

Conventions: error strings are lowercase with no trailing period (they get wrapped into sentences), and sentinel errors are package-level variables named ErrSomething.

Generics#

Before generics, a stack of ints and a stack of strings needed two copies of the same code. Now a type parameter in square brackets stands for "some type, chosen by the caller":

Go
type Stack[T any] struct{ items []T }
 
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
 
var ints Stack[int]
var words Stack[string]

A constraint says what a type parameter must support:

ConstraintAllowsExample types
anystoring and passing values onlyeverything
comparable== and !=, map keysints, strings, structs of those
cmp.Ordered<, >, <=, >=integers, floats, strings
your own interfacethe methods it listsanything with those methods

Inside generic code, var zero T produces the zero value of whatever T is, which is how a generic Pop returns "nothing" alongside an error.

Go usually infers the type argument: Max(3, 9, 4) knows T is int.

Iterators#

An iterator is a function that pushes values to a yield callback. Go 1.23 lets for … range loop over it directly:

Go
func (s *Stack[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {
		for i := len(s.items) - 1; i >= 0; i-- {
			if !yield(s.items[i]) {
				return // the caller's loop hit break
			}
		}
	}
}
 
for v := range stack.All() { ... }

How it runs, step by step, for a stack holding a b c and a loop that breaks after b:

StepIterator doesLoop body doesyield returns
1yield("c")prints ctrue
2yield("b")prints b, then breakfalse
3sees false, returnsloop ends

The standard library is full of them now: maps.Keys(m), slices.Values(s), slices.Sorted(seq), strings.SplitSeq. In Module 3 you will write an in-order iterator for a binary tree.

Smaller conveniences#

Go
min(3, 1, 2)          // 1, built-in since 1.21
max(a, b)
clear(m)              // empty a map (or zero a slice)
for i := range 10 {}  // 0..9
slices.Contains(s, x)
slices.Index(s, x)
slices.Reverse(s)
maps.Clone(m)

The full program#

modern.go
Go
package main
 
import (
	"cmp"
	"errors"
	"fmt"
	"iter"
	"maps"
	"slices"
	"strings"
)
 
var ErrEmpty = errors.New("empty stack")
 
type ValidationError struct{ Field string }
 
func (e *ValidationError) Error() string { return "invalid " + e.Field }
 
type Stack[T any] struct{ items []T }
 
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
 
func (s *Stack[T]) Pop() (T, error) {
	var zero T
	if len(s.items) == 0 {
		return zero, ErrEmpty
	}
	v := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return v, nil
}
 
func (s *Stack[T]) All() iter.Seq[T] {
	return func(yield func(T) bool) {
		for i := len(s.items) - 1; i >= 0; i-- {
			if !yield(s.items[i]) {
				return
			}
		}
	}
}
 
func Max[T cmp.Ordered](first T, rest ...T) T {
	m := first
	for _, x := range rest {
		m = max(m, x)
	}
	return m
}
 
func Map[T, U any](xs []T, f func(T) U) []U {
	out := make([]U, 0, len(xs))
	for _, x := range xs {
		out = append(out, f(x))
	}
	return out
}
 
func register(name string) error {
	if name == "" {
		return fmt.Errorf("register: %w", &ValidationError{Field: "name"})
	}
	return nil
}
 
func main() {
	var s Stack[string]
	for _, w := range []string{"a", "b", "c"} {
		s.Push(w)
	}
	for w := range s.All() {
		fmt.Print(w, " ")
		if w == "b" {
			break
		}
	}
	fmt.Println()
 
	for range 3 {
		s.Pop()
	}
	_, err := s.Pop()
	wrapped := fmt.Errorf("undo: %w", err)
	fmt.Println(wrapped, "| is ErrEmpty:", errors.Is(wrapped, ErrEmpty))
 
	var ve *ValidationError
	if err := register(""); errors.As(err, &ve) {
		fmt.Println(err, "| field:", ve.Field)
	}
 
	fmt.Println(Max(3, 9, 4), Max("go", "rust"), min(2.5, 1.5))
	fmt.Println(Map([]string{"go", "dsa"}, strings.ToUpper))
 
	ages := map[string]int{"rina": 31, "adi": 24, "budi": 27}
	fmt.Println(slices.Sorted(maps.Keys(ages)))
	clear(ages)
	fmt.Println(len(ages))
}
Output
c b 
undo: empty stack | is ErrEmpty: true
register: invalid name | field: name
9 rust 1.5
[GO DSA]
[adi budi rina]
0

You are ready for algorithms#

You now know all the Go this course uses. From here, every lesson is about thinking: how to break a problem down, which structure fits, and how fast the solution is. Keep the habits from Module 1: a table test first, gofmt always, and a benchmark whenever you claim something is faster.

Summary#

  • Wrap errors with %w; inspect them with errors.Is (which error) and errors.As (which type).
  • Generics: [T any], [T comparable], [T cmp.Ordered]; var zero T for the zero value.
  • Iterators are func(yield func(T) bool); stop when yield returns false; range over them directly.
  • Use the built-ins min, max, clear and the slices, maps and cmp packages.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.Which verb in fmt.Errorf keeps the original error inspectable with errors.Is?

  2. Q2.Which constraint lets you use < on a type parameter T?

  3. Q3.What does var zero T give you inside a generic function?

  4. Q4.In an iterator func(yield func(T) bool), what must you do when yield returns false?

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

  • Generic Queue[T]build

    Build Queue[T any] with Push, Pop (returning an error when empty), Len and an All() iterator. Test it with ints and strings.

    Show hint

    Use a sentinel ErrEmpty and check it with errors.Is in your test.

  • Generic Set[T]build

    Build Set[T comparable] on a map[T]struct{} with Add, Has, Remove, Union and Intersect, plus a sorted Values for ordered types.

    Show hint

    func Sorted[T cmp.Ordered](s Set[T]) []T can call slices.Sorted(maps.Keys(s.m)).

  • Custom error typebuild

    Create ValidationError{Field string}, return it wrapped from a function, and extract it with errors.As.

    Show hint

    Implement Error() on *ValidationError and pass &ve (a pointer to a pointer) to errors.As.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.