21. Functions and errors

You have called functions since Chapter 07: fmt.Println, strings.ToUpper, strconv.Atoi, rand.Intn. Now you write your own. A function names a chunk of work you can reuse, which is the named step from Chapter 05 turned into code.

This chapter also keeps a promise from Part 3. It explains the second value from strconv.Atoi and ReadString, the one you have been throwing away with _.

Defining and calling

The simplest definition:

func greet() {
    fmt.Println("Hello!")
}

func main() {
    greet() // Hello!
    greet() // Hello!
}

Read it as: define a function greet that takes nothing and runs its body when called. func starts the definition, the () holds what it takes, and the body sits between { and }. Call it by writing its name with ().

Now you can see what the frame from Chapter 07 was. func main() { ... } is an ordinary function definition, and main is the one Go calls for you when the program starts. Every other function runs only when something calls it.

From here on, code blocks that define functions show main too, but still leave out the package and import lines.

Functions go outside main, at the top level of the file, and their order does not matter: main can call a function defined further down. A function inside main does not compile:

func main() {
    func greet() {
        fmt.Println("Hello!")
    }

    greet()
}
syntax error: unexpected greet, expected (
syntax error: non-declaration statement outside function body

The second line is only a result of the first. Move greet out of main, and both go away.

Parameters

A function takes inputs as parameters in the parentheses. Each one has a name and a type:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    greet("Keiko") // Hello, Keiko!
    greet("Arif")  // Hello, Arif!
}

name lives for one call, and the caller supplies its value, which is called the argument. With more than one parameter, separate them with commas:

func add(a int, b int) {
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

func main() {
    add(3, 4) // 3 + 4 = 7
}

When neighbouring parameters have the same type, Go lets you write the type once. You will see this in other people's code, and it means the same thing:

func add(a, b int) {

The types are checked before the program runs. Passing the wrong type stops with an error like the ones in Chapter 11:

func add(a int, b int) {
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

func main() {
    add(3, "four")
}
cannot use "four" (untyped string constant) as int value in argument to add

Every argument is required

Go has no optional parameters and no default values for them. A call must give exactly the arguments the function asks for. Too few:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    greet()
}
not enough arguments in call to greet
    have ()
    want (string)

Too many stops in the same way, and the message says too many arguments, with the same have and want lines to compare:

too many arguments in call to greet
    have (string, string)
    want (string)

If you want a fallback value, write it yourself. A parameter is a variable inside the function, so the check from Chapter 17 works on it. A second function with a clear name can then supply the value:

func greet(name string) {
    if name == "" {
        name = "World"
    }
    fmt.Printf("Hello, %s!\n", name)
}

func greetEveryone() {
    greet("")
}

func main() {
    greet("Keiko")  // Hello, Keiko!
    greetEveryone() // Hello, World!
}

return: handing a value back

To produce a value instead of printing, use return, and write the type of the value after the parentheses:

func square(n int) int {
    return n * n
}

func main() {
    result := square(5)
    fmt.Println(result)        // 25
    fmt.Println(square(7))     // 49
    fmt.Println(square(3) + 1) // 10
}

return ends the function at once and hands the value back to the caller. A function that promises a value must reach a return on every path. Otherwise:

func isPositive(n int) bool {
    if n > 0 {
        return true
    }
}

func main() {
    fmt.Println(isPositive(5))
}
missing return

Go reports it at the closing }, because that is where the function could end with no value.

A function with no type after its parentheses gives back nothing, and Go will not let you use its result as a value:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    result := greet("Keiko")
    fmt.Println(result)
}
greet("Keiko") (no value) used as value

Open exercises/21/01-square.go. Add a function cube that returns n * n * n, then print cube(3).

Multiple return values

A Go function can give back several values at once. List their types in parentheses, and separate the values with commas in the return:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    quotient, remainder := divide(17, 5)
    fmt.Println(quotient, remainder) // 3 2
}

You have been on the receiving end of this since Chapter 14, when strconv.Atoi gave you two values. It is the same two-variable assignment you met in Chapter 11. You must take all of them, because one variable cannot hold two values:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    result := divide(17, 5)
    fmt.Println(result)
}
assignment mismatch: 1 variable but divide returns 2 values

If you want only some of them, put _ in place of the others. That is all the _ in text, _ := ever did:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    _, remainder := divide(20, 6)
    fmt.Println(remainder) // 2
}

This is exercises/21/02-divmod.go.

Early return for guard checks

Since return ends the function at once, it is handy for the easy cases first:

func absolute(n int) int {
    if n >= 0 {
        return n
    }
    return -n
}

func main() {
    fmt.Println(absolute(5))  // 5
    fmt.Println(absolute(-7)) // 7
}

No else is needed. If the first branch matches, the function exits there, and the line after the if never runs. This is a guard, or early return.

Scope: where variables live

A variable created inside a function lives only for that call. Once the function returns, it is gone:

func counterStep() int {
    count := 0
    count = count + 1
    return count
}

func main() {
    fmt.Println(counterStep()) // 1
    fmt.Println(counterStep()) // 1
    fmt.Println(counterStep()) // 1
}

Every call gets a fresh count. Parameters work the same way: changing one inside a function does not change the caller's variable. Other functions cannot see it either:

func setUp() {
    count := 0
    count = count + 1
}

func main() {
    setUp()
    fmt.Println(count)
}
undefined: count

To keep a value between calls, declare it outside every function. All the functions in the file can then use it:

var steps = 0

func step() int {
    steps = steps + 1
    return steps
}

func main() {
    fmt.Println(step()) // 1
    fmt.Println(step()) // 2
    fmt.Println(step()) // 3
}

Chapter 11 said that := only works inside a function, and this is where that matters. Outside a function, you must write var. exercises/21/04-outside-function.go has this mistake on purpose, and running it stops with:

# command-line-arguments
exercises\21\04-outside-function.go:11:1: syntax error: non-declaration statement outside function body

Use a variable outside the functions when you need to. Most data is better passed in as parameters and handed back with return, because then you can see from the call what a function reads and changes. Chapter 26 shows a cleaner way to keep related data together. These examples are in exercises/21/03-counter-scope.go.

Errors: the second value

strconv.Atoi gives back two values: the number, and a report of whether the conversion worked. That report has the type error. It holds one of two things: nil, a Go word meaning "nothing here", when all went well, or a description of the problem. Keep it in a variable, usually called err, and check it on the very next line:

number, err := strconv.Atoi(text)
if err != nil {
    fmt.Println("That is not a whole number.")
    return
}

Read err != nil as "there is an error". This is exercises/21/05-check-atoi.go, inside main:

reader := bufio.NewReader(os.Stdin)

fmt.Print("Enter a whole number: ")
text, _ := reader.ReadString('\n')

number, err := strconv.Atoi(strings.TrimSpace(text))
if err != nil {
    fmt.Println("That is not a whole number.")
    fmt.Println("The error says:", err)
    return
}

fmt.Println("You typed the number", number)

A return inside main ends the program. Three runs, typing 0, then abc, then only Enter:

Enter a whole number: 0
You typed the number 0

Enter a whole number: abc
That is not a whole number.
The error says: strconv.Atoi: parsing "abc": invalid syntax

Enter a whole number:
That is not a whole number.
The error says: strconv.Atoi: parsing "": invalid syntax

This fixes the problem from Chapters 14 and 17: a real 0 and bad text no longer look the same, because you ask err and not the number. When err is not nil, the number is not an answer, so do not use it.

strconv.ParseFloat works the same way:

x, err := strconv.ParseFloat("abc", 64)
fmt.Println(x)   // 0
fmt.Println(err) // strconv.ParseFloat: parsing "abc": invalid syntax

The err also catches the exception Chapter 15 mentioned, a whole number too large for an int:

n, err := strconv.Atoi("99999999999999999999")
fmt.Println(n)   // 9223372036854775807
fmt.Println(err) // strconv.Atoi: parsing "99999999999999999999": value out of range

Go will not let you forget about err by accident. A variable you never use stops the program from compiling, as Chapter 10 showed, so you either check err or throw it away on purpose with _:

number, err := strconv.Atoi("42")
fmt.Println(number)
err declared and not used

Returning your own errors

Your own functions can report a problem in the same way. Add error as the last return type. On failure, return an error made with errors.New, from the errors package. On success, return nil:

func safeDivide(a int, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

When a value must go first, 0 fills its place on failure. To put values into the message, use fmt.Errorf, which takes the same verbs as fmt.Sprintf. A function that has only a problem to report returns just the error:

func checkLevel(level int) error {
    if level < 1 || level > 99 {
        return fmt.Errorf("level %d is not from 1 to 99", level)
    }
    return nil
}

The caller checks at once, as it does with strconv.Atoi. exercises/21/06-your-own-errors.go calls both, and prints:

Result: 3
Error: cannot divide by zero
<nil>
level 150 is not from 1 to 99

The last two lines show the two things an error can hold. nil prints as <nil>.

Open exercises/21/06-your-own-errors.go. Add a function checkAge that returns an error when its argument is below 0, and nil otherwise. Call it from main with both kinds of value.

Noticing that the input has run out

Chapter 20 warned that a validation loop prints its message forever when the input runs out. This happens when input comes from a file instead of the keyboard. At the keyboard, on Windows, pressing Ctrl + Z and then Enter ends the input in the same way.

The second value from ReadString reports this. It is an error that is not nil when there is no more to read:

text, err := reader.ReadString('\n')
line := strings.TrimSpace(text)
if err != nil && line == "" {
    fmt.Println("No more input.")
    break
}

The check has two parts because the last line of a file may have no line ending. ReadString then gives you that text and an error together, and the text is still good. Only an error with no text means that the input has run out.

This is exercises/21/07-read-until-end.go, inside main:

reader := bufio.NewReader(os.Stdin)

total := 0
for {
    fmt.Print("Number: ")
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        fmt.Println("No more input.")
        break
    }

    number, err := strconv.Atoi(line)
    if err != nil {
        fmt.Println("Skipped:", line)
        continue
    }
    total += number
}

fmt.Println("Total:", total)

A session, where the input runs out after the 7:

Number: 5
Number: abc
Skipped: abc
Number: 7
Number: No more input.
Total: 12

Look at number, err := strconv.Atoi(line). err already exists in this round, but number is new, so := is allowed, and err is simply reused. It is the same rule as in Chapter 15.

Now the validation loop can be finished. Chapter 20 said its range check could not accept 0 as a good answer. With err, it can, and the loop stops when the input runs out. This is exercises/21/09-validate-volume.go, inside main:

reader := bufio.NewReader(os.Stdin)

var volume int
for {
    fmt.Print("Volume (0 to 10): ")
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        fmt.Println("No more input.")
        return
    }

    volume, err = strconv.Atoi(line)
    if err == nil && volume >= 0 && volume <= 10 {
        break
    }
    fmt.Println("That is not a volume from 0 to 10.")
}

fmt.Println("Volume set to", volume)

A session:

Volume (0 to 10): abc
That is not a volume from 0 to 10.
Volume (0 to 10): 150
That is not a volume from 0 to 10.
Volume (0 to 10): 0
Volume set to 0

Putting the reading lines in a function

Chapter 15 repeated the reading lines for each value, and promised a better way. Here it is, in exercises/21/08-read-functions.go:

// Chapter 21 example: the input-reading lines, in functions of their own.
// Run with: go run exercises/21/08-read-functions.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

var reader = bufio.NewReader(os.Stdin)

// readLine prints the prompt and gives back one trimmed line. The error is
// not nil when there is no more input.
func readLine(prompt string) (string, error) {
    fmt.Print(prompt)
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        return "", err
    }
    return line, nil
}

// readInt asks until it gets a whole number. The error is not nil when
// there is no more input.
func readInt(prompt string) (int, error) {
    for {
        line, err := readLine(prompt)
        if err != nil {
            return 0, err
        }

        number, err := strconv.Atoi(line)
        if err == nil {
            return number, nil
        }
        fmt.Println("That is not a whole number.")
    }
}

func main() {
    a, err := readInt("First number:  ")
    if err != nil {
        fmt.Println("No more input.")
        return
    }

    b, err := readInt("Second number: ")
    if err != nil {
        fmt.Println("No more input.")
        return
    }

    fmt.Printf("Sum: %d\n", a+b)
}

Several things from this chapter meet here:

  • reader moved outside main, using var, because readLine needs it too. A program should still make only one.
  • readLine does the prompt, the read, the trim and the check. It returns the line and an error.
  • readInt calls readLine, and asks again until it gets a whole number. When err is not nil, it passes the error on to its caller.
  • main now says what it wants, and checks for the input running out.

A session, and a run with no input at all:

First number:  3
Second number: abc
That is not a whole number.
Second number: 4
Sum: 7

First number:  No more input.

The Part 1 mini-project showed a function that was not part of Go:

guess := readGuess()

Now you can write it. It is one line, because a return can pass on all the values from another call:

func readGuess() (int, error) {
    return readInt("Guess: ")
}

Because reading can fail, the real call has two values to receive, and it checks the error:

guess, err := readGuess()
if err != nil {
    fmt.Println("No more input. The number was", target)
    return
}

exercises/21/10-read-guess.go is the whole Number Guessing Game written this way. Unlike the version in Part 4, abc is no longer counted as a guess.

Open exercises/21/08-read-functions.go. Add a function readFloat that works like readInt but uses strconv.ParseFloat, and use it to ask for a price.

Homework

Homework starters are in exercises/21/homework/. Each one runs as it is and prints a placeholder line. Write your functions outside main.

Problem 1 — Greet

Open exercises/21/homework/01-greet.go. Write a function greet that takes a name and prints Hello, <name>!. Call it with three different names.

Problem 2 — isEven

Open exercises/21/homework/02-is-even.go. Write isEven, which takes an int and returns (not prints) true or false for whether it is even. Call it on a few values inside fmt.Println so the bools show.

Problem 3 — Clamp

Open exercises/21/homework/03-clamp.go. Write clamp(x int, lo int, hi int) int, which returns lo if x is below lo, hi if above hi, and x otherwise. Test it with three calls covering each case.

Challenge — Percent

Open exercises/21/homework/04-percent.go. Write parsePercent(text string) (int, error). It converts text to an int and returns that number with a nil error. If the text is not a number, it returns the error from strconv.Atoi. If the number is below 0 or above 100, it returns your own error from errors.New. Write a second function report that calls it, checks the error at once, and prints the result. Call report with "85", "abc" and "150".

Stuck or finished? Open the homework solutions page.