ZetCode

Go if & else statements

last modified October 30, 2026

In this article we show how to create conditions and branches in Go with if, else and else if. We also cover the initialization statement, logical operators, map lookups, error checks, type assertions, scoping and guard clauses.

Go does not use parentheses around the condition, but the braces are mandatory, and the else keyword must appear on the same line as the closing brace. A condition must have the type bool; unlike C, a number is not accepted as a truth value.

All examples were compiled and run with Go 1.27.0.

The if statement

The block after if runs when the condition is true. When the condition is false, the block is skipped and the program continues after it.

main.go
package main

import "fmt"

func main() {

    num := 4

    if num > 0 {
        fmt.Println("The number is positive")
    }
}

Because 4 is greater than zero, the message is printed. Nothing is printed for a condition that evaluates to false, and the variable num is still available after the block.

$ go run main.go
The number is positive

Adding else

The else block runs when the if condition is false. The two blocks are mutually exclusive.

main.go
package main

import "fmt"

func main() {

    num := -4

    if num > 0 {
        fmt.Println("The number is positive")
    } else {
        fmt.Println("The number is not positive")
    }
}

Zero and negative numbers both take the else branch. To distinguish them, another condition is needed.

$ go run main.go
The number is not positive

Chaining else if

An else if adds another condition that is tested only when the previous ones failed. The chain can have any number of branches, and the final else is optional.

main.go
package main

import "fmt"

func main() {

    nums := []int{5, 0, -3}

    for _, num := range nums {

        if num > 0 {
            fmt.Println(num, "is positive")
        } else if num == 0 {
            fmt.Println(num, "is zero")
        } else {
            fmt.Println(num, "is negative")
        }
    }
}

The conditions are evaluated from top to bottom, and the first true one wins. The values come from a slice instead of rand.Intn, so the output is the same on every run.

$ go run main.go
5 is positive
0 is zero
-3 is negative

The init statement

An if may start with a short statement whose variables are scoped to the whole chain. This is commonly used for a value that is needed only by the condition.

main.go
package main

import "fmt"

func main() {

    if n := len("falcon"); n > 5 {
        fmt.Println("long word:", n)
    }

    word := "sky"

    if n := len(word); n > 5 {
        fmt.Println("long word:", n)
    } else {
        fmt.Println("short word:", n)
    }
}

n exists in both branches, but not after the statement. The first word has six bytes, the second one three, so the two branches print different texts.

$ go run main.go
long word: 6
short word: 3

Logical operators

Conditions are combined with && (and), || (or) and negated with !. Evaluation is short-circuit: the right operand is not evaluated when the result is already known.

main.go
package main

import (
    "fmt"
    "strings"
)

func main() {

    age := 25
    member := true

    if age >= 18 && member {
        fmt.Println("adult member")
    }

    if age < 18 || !member {
        fmt.Println("not an adult member")
    }

    word := "falcon"

    if len(word) > 0 && strings.HasPrefix(word, "f") {
        fmt.Println("starts with f:", word)
    }
}

The second condition is false, so nothing is printed for it. In the last condition the length is checked first, and && stops evaluating as soon as an operand is false.

$ go run main.go
adult member
starts with f: falcon

Comparing strings

Strings are compared with ==, which is case-sensitive. strings.EqualFold compares them without regard to case, and len returns the number of bytes.

main.go
package main

import (
    "fmt"
    "strings"
)

func main() {

    word := "Falcon"

    if word == "Falcon" {
        fmt.Println("exact match")
    }

    if strings.EqualFold(word, "falcon") {
        fmt.Println("case-insensitive match")
    }

    if len(word) > 5 {
        fmt.Println("longer than five bytes:", len(word))
    }
}

The exact comparison succeeds, and so does the case-insensitive one. For non-ASCII text, utf8.RuneCountInString gives the number of characters instead of bytes.

$ go run main.go
exact match
case-insensitive match
longer than five bytes: 6

Map key existence

A map lookup can return two values: the value and a boolean that reports whether the key was present. The short form declares both inside the if.

main.go
package main

import "fmt"

func main() {

    grades := map[string]int{
        "Lucia": 2,
        "Paul":  1,
        "Jane":  3,
    }

    if g, ok := grades["Jane"]; ok {
        fmt.Println("Jane:", g)
    }

    if g, ok := grades["Merry"]; ok {
        fmt.Println("Merry:", g)
    } else {
        fmt.Println("Merry has no grade")
    }
}

Without the boolean, a missing key would return the zero value 0, which cannot be told apart from a real grade of zero.

$ go run main.go
Jane: 3
Merry has no grade

Checking errors

The if err != nil pattern is the standard way to handle errors in Go. errors.Is compares an error against a sentinel value even when the error has been wrapped.

main.go
package main

import (
    "errors"
    "fmt"
    "io/fs"
    "os"
)

func main() {

    f, err := os.Open("missing.txt")
    if err != nil {
        fmt.Println("open failed:", err)

        if errors.Is(err, fs.ErrNotExist) {
            fmt.Println("the file does not exist")
        }

        return
    }

    defer f.Close()
}

The function returns after the error, so the rest of the body only runs with a valid file. fs.ErrNotExist matches the underlying ENOENT error from the operating system.

$ go run main.go
open failed: open missing.txt: no such file or directory
the file does not exist

Type assertions

A type assertion on an interface returns the value and a boolean. The two-value form does not panic when the dynamic type is different; the single-value form does.

main.go
package main

import "fmt"

func main() {

    var value any = "hello"

    if s, ok := value.(string); ok {
        fmt.Println("string:", s)
    } else {
        fmt.Println("not a string")
    }

    value = 42

    if n, ok := value.(int); ok {
        fmt.Println("int:", n)
    }

    value = 3.14

    if _, ok := value.(int); !ok {
        fmt.Println("3.14 is not an int")
    }
}

The same interface variable holds values of three different types. Each assertion checks the dynamic type, and the last one takes the negated branch because a float64 is not an int.

$ go run main.go
string: hello
int: 42
3.14 is not an int

Scope and shadowing

A variable declared with := inside a block hides a variable with the same name from the outer block. Assignments with = modify the outer variable instead.

main.go
package main

import "fmt"

func main() {

    x := 10

    if x > 5 {
        x := 20
        fmt.Println("inner:", x)
    }

    fmt.Println("outer:", x)

    if x := 5; x > 0 {
        fmt.Println("init:", x)
    }
}

The inner x := 20 creates a new variable, so the outer value stays 10. The init statement at the end declares another x, which is visible only inside that if.

$ go run main.go
inner: 20
outer: 10
init: 5

Guard clauses

A function is easier to read when the error cases return early and the normal path stays unindented. The conditions are written as separate if statements instead of nested else blocks.

main.go
package main

import (
    "errors"
    "fmt"
)

func checkAge(age int) error {

    if age < 0 {
        return fmt.Errorf("age %d is negative", age)
    }

    if age < 18 {
        return errors.New("too young")
    }

    return nil
}

func main() {

    for _, age := range []int{25, 12, -1} {

        if err := checkAge(age); err != nil {
            fmt.Printf("age %d: %v\n", age, err)
        } else {
            fmt.Printf("age %d: accepted\n", age)
        }
    }
}

Each guard handles one invalid case and returns immediately. The caller checks the returned error with the if err != nil pattern, so the success message is written only in the else branch.

$ go run main.go
age 25: accepted
age 12: too young
age -1: age -1 is negative

Source

The Go Programming Language Specification - If statements

In this article we have covered conditions in Go: if, else, else if, the init statement, logical operators, strings, maps, errors, type assertions, scoping and guard clauses.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.