Go Exercises
Practise what you've learned. Type the missing piece into each highlighted gap and press Check. Further down are programming challenges with editors and model solutions — copy them into a main.go file and run them with go run.
Fill in the blanks
Basics
Every executable program's files belong to this package.
package
(lesson 03)
Import the formatting package.
import ""
(lesson 03)
Declare and initialise a variable inside a function, letting Go infer its type.
count 10
The short variable declaration (lesson 06).
Declare a variable with an explicit type and its zero value.
total float64
(lesson 06)
Print a value's type with Printf.
fmt.Printf("\n", x)
(lesson 06)
Convert an int to a float64 before dividing.
avg := (sum) / float64(n)
Go never converts between numeric types automatically (lesson 09).
A counter that starts at 0 in a const block.
const ( Small = Medium Large )
(lesson 10)
Print a float with two decimal places.
fmt.Printf("\n", price)
(lesson 12)
Print a string with quotes and escapes, useful for debugging.
fmt.Printf("\n", s)
(lesson 12)
Control flow and strings
Declare a variable that exists only inside the if statement.
if n, err := strconv.Atoi(s) err != nil {
return err
}
(lesson 14)
Count from 0 to 4 with the shortest loop form.
for i := 5 {
fmt.Println(i)
}
Range over an integer (lesson 16).
Go's only loop keyword, used here as a while loop.
balance < 2000 {
balance *= 1.07
}
(lesson 16)
Continue into the next case of a switch.
case 2:
fmt.Println("reports")
(lesson 15)
Leave both loops at once.
outer:
for _, row := range grid {
for _, v := range row {
if v < 0 {
break
}
}
}
(lesson 18)
Count the characters (not bytes) in a string.
n := utf8.(s)
(lesson 19)
Split a string around the first "=".
key, value, ok := strings.(line, "=")
(lesson 20)
Convert text to an int.
n, err := strconv.("42")
(lesson 21)
The efficient way to build a string in a loop.
var b strings.
(lesson 22)
Collections and functions
Add an element to a slice.
names = (names, "Ana")
Always assign the result back (lesson 25).
Make a slice with length 0 and room for 100 elements.
buf := make([]byte, 0, )
The third argument is the capacity (lesson 26).
Check whether a key is in a map.
price, := prices["apple"]
The "comma ok" form (lesson 28).
Remove a key from a map.
(stock, "pens")
(lesson 28)
Sort a slice of ints in place.
slices.(nums)
(lesson 27)
Accept any number of int arguments.
func sum(nums int) int
A variadic parameter (lesson 32).
Close the file when the function returns.
f.Close()
(lesson 34)
Types, methods and errors
Get the address of a variable.
p := score
(lesson 37)
A method that can modify its receiver.
func (a Account) Deposit(n int) { a.Balance += n }
A pointer receiver (lesson 38).
Declare an interface type.
type Shape {
Area() float64
}
(lesson 40)
The method that makes fmt print your type nicely.
func (d Weekday) () string
fmt.Stringer (lesson 42).
Branch on the dynamic type of an interface value.
switch v := x.() {
case int:
// ...
}
A type switch (lesson 41).
Create a simple error value.
return .New("not found")
(lesson 44)
Wrap an error so it can still be inspected.
return fmt.Errorf("loading config: ", err)
(lesson 45)
Check for a sentinel error anywhere in the chain.
if errors.(err, os.ErrNotExist) {
(lesson 45)
Stop a panic inside a deferred function.
if r := (); r != nil {
(lesson 46)
Generics and concurrency
A type parameter for values that can be compared with == .
func Index[T ](items []T, v T) int
(lesson 47)
A constraint for values that support < and >.
func Largest[T cmp.](items []T) T
(lesson 47)
The type of an iterator that yields single values.
func Countdown(n int) iter.[int]
(lesson 50)
Start a function in a new goroutine.
worker(jobs)
(lesson 51)
Start a goroutine and track it in a WaitGroup.
wg.(func() { process(item) })
Added in Go 1.25 (lesson 51).
Create an unbuffered channel of strings.
ch := (chan string)
(lesson 52)
Tell receivers no more values are coming.
(results)
Only the sender closes a channel (lesson 52).
Wait on several channel operations at once.
{
case msg := <-ch:
fmt.Println(msg)
case <-time.After(time.Second):
fmt.Println("timeout")
}
(lesson 53)
Lock a mutex and unlock it when the function returns.
c.mu.Lock() defer c.mu.()
(lesson 54)
A context that is cancelled automatically after two seconds.
ctx, cancel := context.(ctx, 2*time.Second) defer cancel()
(lesson 56)
Standard library and tools
Read a whole file into memory.
data, err := os.("notes.txt")
(lesson 57)
Encode a value as JSON.
data, err := json.(order)
(lesson 59)
The reference layout for a date like 2026-10-08.
t.Format("")
(lesson 60)
Route only GET requests with an id wildcard.
mux.HandleFunc(" /tasks/{id}", getTask)
(lesson 61)
The name of every test function starts with this word.
func Initials(t *testing.T)
(lesson 65)
Run tests with the race detector.
go test ./...
(lesson 54)
Programming challenges
1. Word frequencies
Count how often each word appears in a text, ignoring case and punctuation, and print the words with their counts — most frequent first, alphabetical for ties.
Show a model solution
package main import ( "cmp" "fmt" "maps" "slices" "strings" "unicode" ) func main() { text := "The cat and the hat. The bat and the cat!" words := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool { return !unicode.IsLetter(r) }) counts := map[string]int{} for _, w := range words { counts[w]++ } keys := slices.SortedFunc(maps.Keys(counts), func(a, b string) int { return cmp.Or(cmp.Compare(counts[b], counts[a]), cmp.Compare(a, b)) }) for _, w := range keys { fmt.Printf("%-4s %d\n", w, counts[w]) } }
the 4 and 2 cat 2 bat 1 hat 1
2. A bank account
Write an Account type with Deposit and Withdraw methods that return errors for non-positive amounts and insufficient funds (use sentinel errors), and a transaction history.
Show a model solution
package main import ( "errors" "fmt" ) var ( ErrInvalidAmount = errors.New("amount must be positive") ErrInsufficient = errors.New("insufficient funds") ) type Account struct { Owner string balance int history []string } func (a *Account) Deposit(n int) error { if n <= 0 { return ErrInvalidAmount } a.balance += n a.history = append(a.history, fmt.Sprintf("+%d", n)) return nil } func (a *Account) Withdraw(n int) error { if n <= 0 { return ErrInvalidAmount } if n > a.balance { return fmt.Errorf("withdrawing %d from %d: %w", n, a.balance, ErrInsufficient) } a.balance -= n a.history = append(a.history, fmt.Sprintf("-%d", n)) return nil } func (a *Account) Balance() int { return a.balance } func main() { a := &Account{Owner: "Ana"} fmt.Println(a.Deposit(100), a.Withdraw(30), a.Deposit(-5)) err := a.Withdraw(500) fmt.Println(err, errors.Is(err, ErrInsufficient)) fmt.Println(a.Balance(), a.history) }
<nil> <nil> amount must be positive withdrawing 500 from 70: insufficient funds true 70 [+100 -30]
3. A generic Filter and Map
Write generic Filter and Map functions for slices, and use them to turn a list of temperatures into labels for the days above 20°C.
Show a model solution
package main import "fmt" func Filter[T any](items []T, keep func(T) bool) []T { var out []T for _, v := range items { if keep(v) { out = append(out, v) } } return out } func Map[T, U any](items []T, f func(T) U) []U { out := make([]U, 0, len(items)) for _, v := range items { out = append(out, f(v)) } return out } func main() { temps := []float64{18.5, 22.1, 19.9, 25.0, 20.4} warm := Filter(temps, func(t float64) bool { return t > 20 }) labels := Map(warm, func(t float64) string { return fmt.Sprintf("%.1f°C", t) }) fmt.Println(labels) }
[22.1°C 25.0°C 20.4°C]
4. Concurrent URL checker
Check a list of "URLs" concurrently with at most two checks running at once, and print the results in the original order. Simulate each check with a function that sleeps and reports whether the name contains "down".
Show a model solution
package main import ( "fmt" "strings" "sync" "time" ) func check(url string) bool { time.Sleep(30 * time.Millisecond) return !strings.Contains(url, "down") } func main() { urls := []string{"a.example", "down.example", "b.example", "c.example"} ok := make([]bool, len(urls)) sem := make(chan struct{}, 2) // at most two at a time var wg sync.WaitGroup for i, u := range urls { wg.Go(func() { sem <- struct{}{} defer func() { <-sem }() ok[i] = check(u) }) } wg.Wait() for i, u := range urls { status := "up" if !ok[i] { status = "DOWN" } fmt.Printf("%-13s %s\n", u, status) } }
a.example up down.example DOWN b.example up c.example up
5. A tested function
Write IsPalindrome(s string) bool that ignores case, spaces and punctuation and works with accented letters, plus a table-driven test. Run it with go test -v.
Show a model solution
package main import ( "strings" "unicode" ) func IsPalindrome(s string) bool { var letters []rune for _, r := range strings.ToLower(s) { if unicode.IsLetter(r) || unicode.IsDigit(r) { letters = append(letters, r) } } for i, j := 0, len(letters)-1; i < j; i, j = i+1, j-1 { if letters[i] != letters[j] { return false } } return true }
package main import "testing" func TestIsPalindrome(t *testing.T) { tests := []struct { in string want bool }{ {"racecar", true}, {"A man, a plan, a canal: Panama!", true}, {"Été", true}, {"gopher", false}, {"", true}, } for _, tt := range tests { if got := IsPalindrome(tt.in); got != tt.want { t.Errorf("IsPalindrome(%q) = %v, want %v", tt.in, got, tt.want) } } }
=== RUN TestIsPalindrome --- PASS: TestIsPalindrome (0.00s) PASS ok example 1.691s