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
What changed, by version#
| Version | Year | Feature | You use it for |
|---|---|---|---|
| 1.11–1.16 | 2018–21 | modules, go.mod | every project (replaces GOPATH) |
| 1.13 | 2019 | error wrapping: %w, errors.Is, errors.As | adding context to errors |
| 1.16 | 2021 | os.ReadFile, embed | files; ioutil deprecated |
| 1.18 | 2022 | generics | reusable data structures |
| 1.21 | 2023 | min, max, clear built-ins; slices, maps, cmp packages | everyday code |
| 1.22 | 2024 | range over integers; fresh loop variable per iteration; HTTP patterns | loops, servers |
| 1.23 | 2024 | iterators: range over functions, iter.Seq | custom collections |
| 1.24 | 2025 | b.Loop() in benchmarks; generic type aliases | testing |
Errors that carry context#
A bare error like file not found is hard to debug. Wrap it with what you were doing, using %w:
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:
| Function | Question it answers | Example |
|---|---|---|
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 me | extract 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":
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:
| Constraint | Allows | Example types |
|---|---|---|
any | storing and passing values only | everything |
comparable | == and !=, map keys | ints, strings, structs of those |
cmp.Ordered | <, >, <=, >= | integers, floats, strings |
| your own interface | the methods it lists | anything 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:
func (s *Stack[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for i := len(s.items) - 1; i >= 0; i-- {
ifyield(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:
| Step | Iterator does | Loop body does | yield returns |
|---|---|---|---|
| 1 | yield("c") | prints c | true |
| 2 | yield("b") | prints b, then break | false |
| 3 | sees false, returns | loop 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#
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#
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-- {
ifyield(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))
}c b
undo: empty stack | is ErrEmpty: true
register: invalid name | field: name
9 rust 1.5
[GO DSA]
[adi budi rina]
0You 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 witherrors.Is(which error) anderrors.As(which type). - Generics:
[T any],[T comparable],[T cmp.Ordered];var zero Tfor 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,clearand theslices,mapsandcmppackages.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Which verb in
fmt.Errorfkeeps the original error inspectable witherrors.Is?Q2.Which constraint lets you use
<on a type parameter T?Q3.What does
var zero Tgive you inside a generic function?Q4.In an iterator
func(yield func(T) bool), what must you do whenyieldreturns 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 anAll()iterator. Test it with ints and strings.Show hint
Use a sentinel
ErrEmptyand check it witherrors.Isin your test. - Generic Set[T]build
Build
Set[T comparable]on amap[T]struct{}with Add, Has, Remove, Union and Intersect, plus a sortedValuesfor ordered types.Show hint
func Sorted[T cmp.Ordered](s Set[T]) []Tcan callslices.Sorted(maps.Keys(s.m)). - Custom error typebuild
Create
ValidationError{Field string}, return it wrapped from a function, and extract it witherrors.As.Show hint
Implement
Error()on*ValidationErrorand 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.