Learn Go · Part 3 of 11

Functions and Control Structures

Writing functions, returning more than one thing, handling errors the Go way, and if, else and switch.

Video 13:14–19:30 · 7 topics · 16 exercises

All parts · this is part 3
  1. 1.Introduction to Go
  2. 2.Constants, Variables and Basic Data Types
  3. 3.Functions and Control Structures
  4. 4.Arrays, Slices, Maps and Loops
  5. 5.Strings, Runes and Bytes
  6. 6.Structs and Interfaces
  7. 7.Pointers
  8. 8.Goroutines
  9. 9.Channels
  10. 10.Generics
  11. 11.Building an API

Extras

This part starts at 13:14 in the video.

Defining and calling functions

We've already seen one function, main. Let's make another. You use the func keyword, then the name, then parentheses for any parameters, then the return type if there is one. Same as before, the opening curly brace has to go on the first line or you get an error.

main.gogo
package main

import "fmt"

func main() {
	printMe("Hello from main")
	fmt.Println(add(2, 3))
}

func printMe(printValue string) {
	fmt.Println(printValue)
}

// Parameters next to each other with the same type can share it.
func add(a, b int) int {
	return a + b
}

The parameter types are enforced, so if you try passing an int into printMe, it won't compile. Also notice main calls printMe even though it's defined further down. In Go, the order you put functions in doesn't matter.

Something you'll see a lot: functions are values too. You can stick one in a variable, pass it into another function, or just write one inline without a name:

main.gogo
package main

import "fmt"

func apply(n int, f func(int) int) int {
	return f(n)
}

func main() {
	double := func(x int) int { return x * 2 }
	fmt.Println(apply(5, double))                           // 10
	fmt.Println(apply(5, func(x int) int { return x * x })) // 25
}

Practice

  • Warm-upSay hi

    Write greet(name string) string that returns "Hello, <name>!".

    Practice with AI
  • PracticeWait, it didn't change?

    The template has a function that adds 10 to an int you pass it and prints it, then prints the original variable back in main. What does it print, and why didn't it change?

    Practice with AI
  • StretchDo it twice

    Write twice(f func(int) int, n int) int that runs f on n two times, and call it with an inline function.

    Practice with AI

Returning one or more values

Let's make a function that does integer division and gives us back the result. And while we're at it, the remainder too, because in Go you can return more than one value. Just put the types in parentheses and return the values separated by commas:

main.gogo
package main

import "fmt"

func intDivision(numerator, denominator int) (int, int) {
	result := numerator / denominator
	remainder := numerator % denominator
	return result, remainder
}

func main() {
	result, remainder := intDivision(11, 2)
	fmt.Println(result, remainder) // 5 1

	onlyResult, _ := intDivision(11, 2) // _ throws a value away
	fmt.Println(onlyResult)
}

What if you only care about one of them? Go makes you use every variable, so you can't just leave one hanging. That's what the underscore is for. _ basically means "yeah, I don't need this one".

You can also give your return values names. They start out as their zero values, and if you write return with nothing after it, it sends back whatever they're set to:

main.gogo
package main

import "fmt"

func minMax(numbers []int) (lowest, highest int) {
	lowest, highest = numbers[0], numbers[0]
	for _, n := range numbers {
		lowest = min(lowest, n)
		highest = max(highest, n)
	}
	return // returns lowest and highest
}

func main() {
	fmt.Println(minMax([]int{4, 9, 1, 7})) // 1 9
}

Practice

  • Warm-upSmallest and biggest

    Write minMax(a, b, c int) (int, int) that gives back the smallest and biggest of three numbers.

    Practice with AI
  • PracticeSplit a name

    Write splitName(full string) (first, last string) using strings.Cut from the strings package. What happens with a name that has no space in it?

    Practice with AI

Formatting output with Printf

fmt.Println is fine for printing a few values, but when you want more control over how things look, use fmt.Printf. You write your string with some placeholders in it (called verbs), and the values you pass in fill them in, in order. Just remember it doesn't add a new line for you, so stick a \n on the end.

main.gogo
package main

import "fmt"

func main() {
	name, age, height := "Ada", 36, 1.68

	fmt.Printf("%v is %v\n", name, age)         // Ada is 36
	fmt.Printf("%s is %d\n", name, age)         // same, with the type spelled out
	fmt.Printf("%q\n", name)                    // "Ada"
	fmt.Printf("%.1f m\n", height)              // 1.7 m
	fmt.Printf("%T %T %T\n", name, age, height) // string int float64
	fmt.Printf("[%5d] [%-5d]\n", age, age)      // [   36] [36   ]

	line := fmt.Sprintf("%s (%d)", name, age) // the same, but returned as a string
	fmt.Println(line)
}
VerbWhat it prints
%vPretty much anything, formatted the default way. If you're not sure, use this one.
%+vA struct, with the field names (part 6)
%dAn integer
%s / %qA string, without or with quotes
%.2fA float with two decimal places
%TThe value's type. Super handy when you're not sure what you're dealing with
%5d / %-5dPadded out to 5 characters, lined up right or left

There's also fmt.Sprintf, which works the same way but gives you back the string instead of printing it, and fmt.Errorf, which makes an error out of it. More on that next.

Practice

  • Warm-upPrint a receipt

    Read some items and their prices, and print them with the prices lined up in two columns, prices to two decimals, and a total at the bottom.

    Practice with AI

Errors are values

Okay, so what happens if someone passes a zero into our division function? We get an integer divide by zero error and the program crashes. Not great.

Go doesn't have exceptions. The pattern instead is: if your function can run into an error, it returns an error along with its other values. error is a built-in type, and its zero value is nil, which means nothing went wrong. To make one, you can use errors.New:

main.gogo
package main

import (
	"errors"
	"fmt"
)

func intDivision(numerator, denominator int) (int, int, error) {
	if denominator == 0 {
		return 0, 0, errors.New("cannot divide by zero")
	}
	return numerator / denominator, numerator % denominator, nil
}

func main() {
	result, remainder, err := intDivision(11, 0)
	if err != nil {
		fmt.Println(err.Error()) // cannot divide by zero
		return
	}
	fmt.Println(result, remainder)
}

Then it's the caller's job to check if they got an error back. You'll write if err != nil a lot in Go. Like, a lot. But the upside is you can always see exactly where things can go wrong, instead of an exception popping out of nowhere. And this isn't just for your own code. Tons of functions you import from other packages return an error too, and you handle them the exact same way.

When you get an error back and want to pass it up to whoever called you, it's a good idea to add a bit of info about what you were doing. You do that with fmt.Errorf and %w, and the caller can still check what the original error was with errors.Is:

main.gogo
package main

import (
	"errors"
	"fmt"
)

var ErrDivideByZero = errors.New("cannot divide by zero")

func intDivision(numerator, denominator int) (int, error) {
	if denominator == 0 {
		return 0, ErrDivideByZero
	}
	return numerator / denominator, nil
}

func average(total, count int) (int, error) {
	avg, err := intDivision(total, count)
	if err != nil {
		return 0, fmt.Errorf("average of %d items: %w", count, err)
	}
	return avg, nil
}

func main() {
	_, err := average(10, 0)
	fmt.Println(err)                             // average of 0 items: cannot divide by zero
	fmt.Println(errors.Is(err, ErrDivideByZero)) // true: %w kept the original inside
}

Practice

  • Warm-upA square root that can fail

    Write safeSqrt(x float64) (float64, error) that returns an error for negative numbers, and handle it in main.

    Practice with AI
  • PracticeParse an age

    Write parseAge(s string) (int, error) with strconv.Atoi that also says no to anything under 0 or over 150. Wrap Atoi's error with %w.

    Practice with AI
  • StretchWhich error was it?

    Starting from the parseAge in the template, give it two errors, ErrNotNumber and ErrOutOfRange, and write describe(s string) string that gives back a different message for each using errors.Is.

    Practice with AI

if, else if and else

We already used if to check the denominator. You write the keyword, the condition (no parentheses needed), then the code inside curly braces. And you always need the braces, even if it's just one line.

Let's add some more checks. An else if only gets checked if everything above it was false, and else is the default if nothing matched. One Go thing to know: else if and else have to be on the same line as the closing brace before them.

main.gogo
package main

import (
	"errors"
	"fmt"
)

func intDivision(numerator, denominator int) (int, int, error) {
	if denominator == 0 {
		return 0, 0, errors.New("cannot divide by zero")
	}
	return numerator / denominator, numerator % denominator, nil
}

func main() {
	result, remainder, err := intDivision(11, 2)
	if err != nil {
		fmt.Println(err)
	} else if remainder == 0 {
		fmt.Printf("The result is %v\n", result)
	} else {
		fmt.Printf("The result is %v with remainder %v\n", result, remainder)
	}
}

You can also run a short statement right before the condition, separated with a semicolon. Any variables you make there only exist inside that if and its else, so they don't clutter up the rest of your function:

main.gogo
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// err only exists inside this if and its else branches.
	if n, err := strconv.Atoi("42"); err != nil {
		fmt.Println("not a number:", err)
	} else {
		fmt.Println("got", n)
	}
}

Practice

  • Warm-upLetter grades

    Write grade(score int) string: A for 90 and up, B for 80 and up, C for 70 and up, F for everything else.

    Practice with AI
  • PracticeUn-nest it

    Take a function with three nested if err == nil blocks and rewrite it so it returns early on each error and nothing's nested.

    Practice with AI

Combining conditions

OperatorWhat it means
== !=Equal, not equal
< <= > >=Less than, greater than, and so on
&&And: both sides have to be true
||Or: only one side has to be true
!Not: flips true to false and false to true

Go only checks what it has to. With ||, if the first check is true there's no point checking the second, so it doesn't. Same with &&: if the first one is false, it stops right there. You can use that to your advantage, like making sure a string isn't empty before you look at its first letter:

main.gogo
package main

import "fmt"

func main() {
	word := ""

	// && stops at the first false, so word[0] never runs on an empty string.
	if len(word) > 0 && word[0] == 'g' {
		fmt.Println("starts with g")
	}

	isWeekend, isHoliday := false, true
	if isWeekend || isHoliday {
		fmt.Println("day off")
	}
	if !isWeekend {
		fmt.Println("it's a weekday")
	}
}

&& gets checked before ||, so a || b && c is really a || (b && c). I'd just throw the parentheses in anyway though. They don't cost anything and nobody has to remember the rule.

Practice

  • Warm-upLeap years

    Write isLeap(year int) bool in one return line: divisible by 4, except every 100 years, except every 400.

    Practice with AI
  • PracticeFlip it around

    The template swaps the two sides of len(word) > 0 && word[0] == 'g' and runs it with an empty string. What happens, and why?

    Practice with AI

switch statements

We could have written our if/else stuff with a switch statement instead. With nothing after the switch keyword, each case gets checked in order, the first one that's true runs, and the rest get skipped. Put a value after the keyword, and each case compares against that value instead:

main.gogo
package main

import "fmt"

func describe(result, remainder int, err error) {
	switch {
	case err != nil:
		fmt.Println(err)
	case remainder == 0:
		fmt.Printf("The result is %v\n", result)
	default:
		fmt.Printf("The result is %v with remainder %v\n", result, remainder)
	}
}

func main() {
	describe(5, 1, nil)

	remainder := 2
	switch remainder {
	case 0:
		fmt.Println("The division was exact")
	case 1, 2:
		fmt.Println("The division was close")
	default:
		fmt.Println("The division was not close")
	}
}

If you're coming from another language with switch statements, a few differences:

  • No break. It's implied, so you don't need to write it at the end of every case.
  • One case can match a bunch of values, like case 1, 2:.
  • If you actually want to fall into the next case, you have to ask for it with fallthrough. You probably won't need this much.
main.gogo
package main

import "fmt"

func main() {
	switch level := 2; level {
	case 2:
		fmt.Println("level 2 setup")
		fallthrough // runs the next case's code too, without checking it
	case 1:
		fmt.Println("level 1 setup")
	}
}

Practice

  • Warm-upWeekday or weekend?

    Write dayType(day string) string with a switch on the day name that gives back weekend for Saturday and Sunday (in one case) and weekday for everything else.

    Practice with AI
  • PracticeFizzBuzz, switch edition

    Read a number n and print 1 to n, but Fizz for multiples of 3, Buzz for multiples of 5, and FizzBuzz for both. Use a switch with no value.

    Practice with AI
  • Stretchif chain to switch

    The template has a grade function written as an if chain, in a comment. Rewrite it as a switch with no value so the tests still pass.

    Practice with AI