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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
List all Go tutorials.