if and else

Run code only when a test is true, and learn the initialiser form that checks an error in the same line that produces it.

Syntaxif init; condition { ... } else if condition { ... } else { ... }

An if statement runs a block of code only when a test is true. The test goes after the keyword, with no brackets around it. The braces are always required, even for a single line inside.

if n > 10 {
	fmt.Println("too many")
} else {
	fmt.Println("fine")
}

You can chain tests with else if. Go checks them in written order and runs the first block whose test is true. It never runs two.

The initialiser form

Most Go functions that can fail return two things: a result, and an error. An error is a value that is nil when nothing went wrong, and nil means "nothing here". You check it immediately. An if may declare a variable first, then test it, separated by a semicolon. These two pieces of code mean the same thing:

// longhand: declare, then test, inside a block
{
	err := save()
	if err != nil {
		return err
	}
}

// shorthand: the same declaration, moved into the if
if err := save(); err != nil {
	return err
}

The shorthand is what most Go code looks like. The variable it declares lives only inside the if and every else attached to it, which is why the longhand needs the extra braces to match.

What trips people up

If you need the value after the statement, declare it on its own line instead. And the test must be a true-or-false value: Go will not accept a number or a string where a condition belongs.

Example

Example · go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	// strconv.Atoi turns text into a whole number. It returns two
	// values: the number, and an error that is nil on success.
	// Both are declared in the if, and both stay visible in the else.
	if n, err := strconv.Atoi("42"); err != nil {
		fmt.Println("not a number:", err)
	} else if n > 40 {
		fmt.Println("big:", n)
	} else {
		fmt.Println("small:", n)
	}

	// The underscore throws away the number: here only the error matters.
	if _, err := strconv.Atoi("twelve"); err != nil {
		fmt.Println("failed:", err)
	}
}

// Output:
// big: 42
// failed: strconv.Atoi: parsing "twelve": invalid syntax

When to use it

  • Stop a function early when the value it was given is unusable.
  • Check the error from a file or network call on the line that makes the call.
  • Pick one of several messages to print based on a number's range.
  • Only use a parsed value when parsing actually succeeded.

More examples

Guard clauses keep the real work unindented

Every bad case is handled and returned at once, so the successful path stays on one level of indentation instead of sinking inside nested else blocks. Reach for this shape in any function that validates its input before doing work; it is the standard layout of Go functions that return an error.

Example · go
package main

import (
	"errors"
	"fmt"
	"strconv"
)

// Each failing case leaves the function straight away.
func parsePort(raw string) (int, error) {
	if raw == "" {
		return 0, errors.New("port is empty")
	}
	n, err := strconv.Atoi(raw)
	if err != nil {
		return 0, err
	}
	if n < 1 || n > 65535 {
		return 0, fmt.Errorf("port %d is out of range", n)
	}
	return n, nil
}

The initialiser variable does not outlive the if

Shows the scope rule that catches everyone once: a variable declared in the initialiser exists only for that if and its else branches. Use the initialiser form when the value is only needed for the check, and a plain declaration when later lines need it too.

Example · go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	if n, err := strconv.Atoi("12"); err == nil {
		fmt.Println(n + 1) // n is usable here
	}
	// fmt.Println(n) // will not compile: undefined: n

	// Need the value afterwards? Declare it on its own line.
	n, err := strconv.Atoi("12")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("still have it:", n)
}

Two results and a true-or-false flag

Many standard library functions end with a true-or-false value that says whether the operation found anything. The initialiser form tests that flag in the same line that produces the results, which is why this pattern appears so often in real code.

Example · go
package main

import (
	"fmt"
	"strings"
)

func main() {
	// strings.Cut splits text at the first separator. It returns the part
	// before, the part after, and found, which is false if the separator
	// is absent. Added in Go 1.18.
	if host, port, found := strings.Cut("localhost:8080", ":"); found {
		fmt.Printf("host=%s port=%s\n", host, port)
	} else {
		fmt.Println("no colon in address")
	}
}

Discussion

  • Be the first to comment on this lesson.