Conditional Statements in Go

Learn how to use if, switch, and type switch statements to control program flow based on conditions in Go.

Programs that always follow the same sequence of instructions are rare. Most software needs to react: if the user is logged in, show the dashboard; otherwise, redirect to the login page. If a file exists, read it; if not, create a new one. In Go, this kind of decision-making comes from conditional statements: constructs that execute different blocks of code depending on whether an expression evaluates to true.

Go provides three conditional control flow tools:

  • if – the fundamental branching statement.
  • switch – multi-way branching that is often cleaner than chains of if/else if.
  • type switch – a specialised form of switch that branches based on the concrete type behind an interface value.

The first two are the bread and butter of everyday Go. The type switch appears when working with interfaces, and understanding it unlocks polymorphic code.

What Conditional Statements Are

A conditional statement is a control flow structure that evaluates a boolean expression and executes a code block only if that expression is true. The simplest mental model: a fork in the road. Go forces the decision to be explicit – the expression after if must produce a bool. Numbers, strings, pointers, and slices have no automatic “truthiness” in Go.

age := 21
if age >= 18 {
    fmt.Println("you are allowed")
}

The snippet works because age >= 18 yields true or false. If the condition were if age { ... }, the compiler would reject it with a type error.

Why Programs Need Decisions

Without conditional logic, every run of a program would be identical. Conditionals let you handle:

  • User input and state: show different UI, apply different permission levels.
  • Error checking: almost every function returning (value, error) is followed by if err != nil { … }.
  • Data validation: reject invalid input before processing.
  • Business rules: apply discounts, route orders, decide which notification to send.

Go’s design keeps these decisions readable. There is exactly one way to write a condition – no implicit coercion, no alternative syntaxes that look similar but behave differently. That consistency reduces the mental overhead when reading unfamiliar codebases.

How Go Executes Conditions

Four rules govern every conditional block in Go:

  1. The condition must be a boolean expression. The compiler will refuse if x when x is an int, string, pointer, or anything other than bool.
  2. Parentheses around the condition are allowed but idiomatic Go omits them. The Go formatter (gofmt) will remove unnecessary parentheses.
  3. Curly braces {} are mandatory, and the opening brace must appear on the same line as if, else if, or else. This is enforced by the language specification and eliminates entire categories of style debates.
  4. An if may include an initial simple statement – a short variable declaration or assignment – that runs before the condition is evaluated.

Only booleans are allowed:

Writing if someNumber { … } or if str { … } will not compile. Go has no truthiness for non‑boolean types. Always compare explicitly, e.g., if someNumber != 0 or if len(slice) > 0.

The initial statement is one of Go’s most distinctive features. It limits variable scope to the if/else block and encourages short, focused checks.

if err := doWork(); err != nil {
    // handle error; err is in scope only here and in the else block
    return err
}
  • Why this pattern exists: error handling in Go is explicit; checking err != nil after function calls is extremely common. The initial statement avoids declaring an error variable in the outer scope that might never be used again.
  • What the code does: calls doWork(), captures the returned error in err, then checks whether err is not nil. If err exists, the block returns it.
  • What to observe when you run it: if doWork fails, the function returns early with the error. If it succeeds, execution continues past the if block. The variable err disappears once the closing brace of the if or else block is reached.
  • Non‑obvious detail: any variables declared in the initial statement are visible in both the if branch and any connected else if or else blocks. They are also visible inside nested blocks within those branches.

Idiomatic Go:

Using a short variable declaration in the initial statement is the standard way to scope error variables. If you see if err := … in production code, it is not a workaround – it is the intended pattern.

The else and else if branches behave as you would expect: the first condition that evaluates to true runs its block, and all remaining branches are skipped.

num := -3
if num > 0 {
    fmt.Println("positive")
} else if num < 0 {
    fmt.Println("negative")
} else {
    fmt.Println("zero")
}

The conditions are checked from top to bottom. Once num < 0 matches, the final else is ignored. This is identical logic to other C‑family languages, but without the ability to accidentally test a non‑boolean.

switch Statements – Many Branches Without the Noise

When a single value needs to be compared against many possible constants or expressions, a long chain of if/else if quickly becomes unwieldy. Go’s switch handles that scenario with a clean, table‑like syntax.

day := "Wednesday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("weekday")
case "Saturday", "Sunday":
    fmt.Println("weekend")
default:
    fmt.Println("not a day")
}
  • What it does: evaluates day once, then compares it (in order) against the values listed in each case. The first matching case runs; all others are skipped.
  • Output: weekday because day is "Wednesday".
  • Non‑obvious detail: unlike C, Java, or JavaScript, Go does not fall through automatically. After a case block finishes, the switch exits. This eliminates a whole class of bugs where a missing break causes unintended case execution.

The default case is optional. If no case matches and there is no default, the switch simply does nothing – it is not an error.

No implicit fallthrough:

Developers coming from C‑family languages often expect the switch to “fall through” to the next case. In Go, it stops immediately. If you genuinely want fallthrough behaviour, you must use the explicit fallthrough keyword, which transfers control to the next case unconditionally. Use it sparingly – it surprises most readers.

Go also supports an expression‑less switch, sometimes called a “true switch.” The switch keyword is followed only by the initial statement (optional) and the block, and each case contains a boolean expression.

score := 84
switch {
case score >= 90:
    fmt.Println("Excellent")
case score >= 75:
    fmt.Println("Good")
case score >= 60:
    fmt.Println("Fair")
default:
    fmt.Println("Needs Improvement")
}

This form is a direct, readable replacement for a multi‑branch if/else if ladder. The conditions are evaluated top to bottom, and the first true case wins.

Type Switches – Deciding on Concrete Type

A type switch is a conditional construct that operates on an interface value. It lets you ask, “What concrete type does this interface hold right now?” and branch accordingly.

var i interface{} = "hello"
switch v := i.(type) {
case int:
    fmt.Printf("int: %d\n", v)
case string:
    fmt.Printf("string: %s\n", v)
default:
    fmt.Printf("unknown type: %T\n", v)
}
  • What it does: extracts the dynamic type of i and tests it against the listed types. In each case, the variable v holds the value with its concrete type (so v is an int inside case int, a string inside case string).
  • Output: string: hello.
  • Non‑obvious detail: the declared variable (v) in switch v := i.(type) is scoped to the switch block. Inside each case, v has a different static type. This is the only place in Go where a variable changes type inside the same scope; the compiler handles it safely.

Type switches are indispensable for interface‑based designs (error inspection, handling unknown JSON values, building dispatchers). Because they deserve dedicated treatment, the next section explores them in depth.

Note:

The type switch is formally a switch variant, not a separate statement. It is introduced here as part of the conditional family, and you will find a full walkthrough in the Type Switches page of this chapter.

Common Pitfalls and Misunderstandings

Confusing truthiness with an explicit condition is the most frequent stumbling block for newcomers. Accept that if x means nothing unless x is a bool – no exceptions.

Variable shadowing in the initial statement can cause subtle bugs when the same name is reused in the outer scope.

err := loadConfig()
if err := validate(); err != nil {
    // 'err' here is the one from validate(), not loadConfig()
}

The outer err is untouched. This is by design but sometimes catches people who assume the inner err is the same variable.

Shadowing loses values:

When you write if err := … inside a function that already has an err, the new variable shadows the outer one. Any value stored in the outer err will not be visible inside the if block. Use a different name if you need both errors.

Omitting a default in a switch is safe but can leave a logical hole. If no case matches, the program silently skips the switch. This is intentional – Go does not force you to handle every possibility – but for critical paths (e.g., routing an API request), consider adding a default that logs or returns an error.

Using fallthrough by accident is rare because it must be explicit, but it can appear in code written by developers who miss that Go’s switch auto‑breaks. The fallthrough keyword bypasses the next case’s condition check entirely – it executes the block regardless. If you see fallthrough in code, pause to confirm it was really intended.

Summary

Go’s conditional statements share the language’s larger philosophy: no magic, one obvious way to do things, and strict compile‑time checks that catch whole categories of mistakes before the program runs. The if statement, with its optional initial statement, keeps error handling tight and scoped. switch eliminates fallthrough accidents and works with any comparable type or boolean expression. The type switch extends that model to the world of interfaces, giving you runtime type information without sacrificing type safety.