if and else

An if statement lets a Go program choose which block of code to run based on whether a condition is true, and else together with else if let you chain alternative branches for when it isn’t. Every program that makes a decision — validating input, branching on an error, picking a response based on a value — relies on this construct, so getting comfortable with Go’s particular flavor of it (mandatory braces, no parentheses, no ternary operator, and an optional init statement) is one of the first steps to writing idiomatic Go.

Overview / How it works

Go’s if statement looks similar to C, Java, or JavaScript at first glance, but it has several deliberate differences that trip up newcomers.

First, the condition must be a genuine boolean expression — a value of type bool. Go has no concept of “truthy” integers, pointers, or strings the way C or Python do. You cannot write if x { ... } when x is an int; the compiler rejects it with a type error. You must write an explicit comparison, such as if x != 0 { ... }. This is a deliberate design choice: it removes an entire class of bugs where a stray assignment or an unintended non-zero value silently makes a branch run.

Second, the condition is never wrapped in parentheses. if (x > 0) { ... } is valid syntax, but gofmt — the standard formatter every Go project runs — strips the parentheses automatically, so idiomatic Go always reads if x > 0 { ... }.

Third, the opening brace must be on the same line as the if, else if, or else keyword. This isn’t just a style preference — it’s required by the language grammar. Go’s lexer automatically inserts a semicolon at the end of certain lines (any line that ends in an identifier, a literal, a closing bracket, or a few other tokens). If you put the opening brace on its own line after if x > 0, the lexer inserts an invisible semicolon after the condition, which turns the block into a syntax error. So braces on the same line aren’t optional style — they’re load-bearing.

Fourth, an if can carry an optional short init statement before the condition, separated by a semicolon: if init; condition { ... }. The init statement runs once, before the condition is evaluated, and anything it declares with := is scoped to the entire if/else if/else chain — visible in every branch, but invisible outside the chain. This is the idiomatic home for the classic Go error-check pattern: if err := doSomething(); err != nil { ... }. It keeps the error variable from leaking into the surrounding function, where it might accidentally get reused or shadowed.

Fifth, Go deliberately has no ternary operator (no condition ? a : b). The language designers left it out on purpose, judging that a compact conditional expression tends to get abused for unreadable one-liners. In Go, a conditional value assignment is always spelled out as an explicit if/else assigning to a variable, or wrapped in a small helper function if you need it repeatedly.

Under the hood, once the compiler has checked that a condition is a bool, it compiles an if/else chain down to ordinary conditional jump instructions — evaluate the condition, jump past the block if false, otherwise fall through and run it, then jump past the remaining branches. There’s no runtime type coercion or boxing involved, because the type checking already happened at compile time; this is one reason Go’s conditionals are as fast as hand-written low-level branching.

Logical operators && (AND), || (OR), and ! (NOT) combine boolean expressions inside a condition, and &&/|| both short-circuit: in a() && b(), if a() returns false, b() is never called, because the overall result is already known to be false. The same applies to || — if the left operand is true, the right operand is skipped. This matters when the right-hand expression has side effects or could panic (like dereferencing a pointer that might be nil): put the safety check first, e.g. if p != nil && p.Value > 0 { ... }.

Syntax

The general forms of an if statement in Go:

// simple form
if condition {
	// runs when condition is true
}

// with an alternative branch
if condition {
	// runs when condition is true
} else {
	// runs when condition is false
}

// chained branches, evaluated top to bottom
if condition1 {
	// ...
} else if condition2 {
	// ...
} else {
	// runs only if none of the above matched
}

// with a short init statement, scoped to the whole chain
if initStatement; condition {
	// ...
}
Part Meaning
condition A bool expression — no parentheses, no truthy values
{ } Mandatory braces around every branch, opening brace on the same line
initStatement; Optional short statement (often :=) run once before the condition; its variables are scoped to the whole chain
else if Chains another condition, checked only if the previous ones were false
else Optional catch-all branch when no condition matched

Examples

Example 1: a basic if/else

package main

import "fmt"

func main() {
	number := 7

	if number%2 == 0 {
		fmt.Println("even")
	} else {
		fmt.Println("odd")
	}
}

Output:

odd

The condition number%2 == 0 evaluates the remainder of 7 / 2, which is 1, so the comparison to 0 is false and the else branch runs.

Example 2: chaining with else if

package main

import "fmt"

func main() {
	score := 82

	if score >= 90 {
		fmt.Println("Grade: A")
	} else if score >= 80 {
		fmt.Println("Grade: B")
	} else if score >= 70 {
		fmt.Println("Grade: C")
	} else {
		fmt.Println("Grade: F")
	}
}

Output:

Grade: B

Go checks each condition in order. score >= 90 is false, so it moves on; score >= 80 is true, so "Grade: B" prints and the remaining else if/else branches are skipped entirely — only one branch in a chain ever runs.

Example 3: if with an init statement

package main

import (
	"fmt"
	"strconv"
)

func main() {
	input := "42"

	if value, err := strconv.Atoi(input); err != nil {
		fmt.Println("could not parse:", err)
	} else {
		fmt.Println("parsed value:", value)
	}
}

Output:

parsed value: 42

strconv.Atoi converts a string to an int, returning the value and an error. The init statement value, err := strconv.Atoi(input) runs once, and both value and err are visible in both the if and else branches. Since "42" parses cleanly, err is nil, the condition err != nil is false, and the else branch prints the parsed value.

How it works step by step

Walking through Example 3’s execution:

  • The init statement value, err := strconv.Atoi(input) runs exactly once, before anything else — this happens regardless of which branch ultimately executes.
  • value and err come into existence at this point, scoped to the entire if/else chain that follows — not to the surrounding main function.
  • The condition err != nil is evaluated using the err just produced.
  • Because err is nil, the condition is false, so Go skips the if block entirely and jumps to the else block.
  • The else block runs, printing the value. Once the chain ends, value and err go out of scope — referencing them after the closing brace would be a compile error.

Common Mistakes

1. Assuming a variable from the init statement survives outside the if/else chain

A variable declared in an if‘s init statement (or inside one of its blocks) only lives inside that chain — not after it:

if x := 10; x > 0 {
	fmt.Println("positive:", x)
}
fmt.Println(x) // compile error: undefined: x

The fix is to declare the variable in the enclosing scope first, then assign to it inside the if if you need the value afterward:

package main

import "fmt"

func main() {
	var x int
	if computed := 10; computed > 0 {
		x = computed
		fmt.Println("positive:", x)
	}
	fmt.Println("still visible:", x)
}

Output:

positive: 10
still visible: 10

2. Writing = instead of == in a condition

Coming from a C-family background, it’s easy to type a single = when you mean a comparison. In C this silently compiles (and is a classic bug); in Go it’s a compile-time error, because = is an assignment statement, not a boolean expression:

count := 5

if count = 10 {
	fmt.Println("count is ten")
}
// compile error: count = 10 (untyped assignment) cannot be used as a bool value

Go’s grammar and its refusal to treat anything but bool as a condition catch this immediately, but the fix is still worth internalizing — always use == to compare:

package main

import "fmt"

func main() {
	count := 5

	if count == 10 {
		fmt.Println("count is ten")
	} else {
		fmt.Println("count is not ten:", count)
	}
}

Output:

count is not ten: 5

3. Writing an else after a return (or break/continue)

When every branch of an if ends in return, an accompanying else is redundant and adds a needless level of nesting:

func classify(n int) string {
	if n < 0 {
		return "negative"
	} else {
		return "non-negative"
	}
}

Idiomatic Go drops the else and lets execution simply fall through to the next statement, which reads more like a guard clause:

func classify(n int) string {
	if n < 0 {
		return "negative"
	}
	return "non-negative"
}

Both versions behave identically, but Go style (and tools like golint/staticcheck) strongly prefer the second: fewer nested blocks make the common case easier to scan.

Best Practices

  • Prefer the if init; condition form for values you only need inside the branch — especially the if err := f(); err != nil { ... } pattern — so temporary variables don’t pollute the enclosing scope.
  • Drop the else when the if branch always returns, breaks, or continues; let the rest of the function read as the “normal” path (this is often called using guard clauses).
  • Keep conditions readable: if a condition needs several &&/|| combined, consider assigning it to a well-named bool variable or extracting a small function, e.g. isEligible := age >= 18 && hasConsent.
  • Order else if chains from most specific to most general, the way Example 2 checks the highest grade boundary first.
  • Put nil/bounds checks first in a combined condition so short-circuit evaluation protects the rest, e.g. if p != nil && p.Value > 0.
  • Never silently discard an error to make a condition simpler — check it explicitly, even if that means an extra line.

Practice Exercises

  • Write a program that declares an integer variable and prints "positive", "negative", or "zero" depending on its value, using an if/else if/else chain.
  • Write a program that loops from 1 to 15 and, for each number, prints "Fizz" if divisible by 3, "Buzz" if divisible by 5, "FizzBuzz" if divisible by both, and the number itself otherwise. (Hint: check divisibility by both first, since a number divisible by both 3 and 5 is also divisible by each individually.)
  • Using strconv.Atoi and the if init; condition pattern, write a program that tries to parse the string "abc" into an integer and prints an appropriate message for the parse failure. (Expected output starts with could not parse:.)

Summary

  • if conditions must be a bool — Go has no truthy values and no parentheses around the condition.
  • Braces are mandatory, and the opening brace must stay on the same line as if/else if/else because of automatic semicolon insertion.
  • else if chains are checked top to bottom; only the first true branch (or the final else) runs.
  • The optional init statement (if init; condition) scopes its variables to the whole if/else chain — ideal for error checks like if err := f(); err != nil.
  • Go has no ternary operator by design — use an explicit if/else or a small helper function instead.
  • && and || short-circuit, so ordering nil/safety checks first in a combined condition avoids panics.
  • Drop unnecessary else blocks after a return to keep code flat and idiomatic.