All parts · this is part 3
- 1.Introduction to Go
- 2.Constants, Variables and Basic Data Types
- 3.Functions and Control Structures
- 4.Arrays, Slices, Maps and Loops
- 5.Strings, Runes and Bytes
- 6.Structs and Interfaces
- 7.Pointers
- 8.Goroutines
- 9.Channels
- 10.Generics
- 11.Building an API
Extras
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.
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:
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
- Practice with AI
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.
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:
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:
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
- Practice with AI
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?
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.
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)
}| Verb | What it prints |
|---|---|
| %v | Pretty much anything, formatted the default way. If you're not sure, use this one. |
| %+v | A struct, with the field names (part 6) |
| %d | An integer |
| %s / %q | A string, without or with quotes |
| %.2f | A float with two decimal places |
| %T | The value's type. Super handy when you're not sure what you're dealing with |
| %5d / %-5d | Padded 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
- Practice with AI
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.
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:
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:
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
- Practice with AI
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.
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.
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:
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
- Practice with AI
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.
Combining conditions
| Operator | What 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:
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
- Practice with AI
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?
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:
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.
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
- Practice with AI
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.