Branching Statements

A complete guide to break, continue, goto, and labels in Go - controlling flow within loops and functions

Branching statements let you change the normal flow of execution inside loops and functions. Instead of running every line in order, you can jump out of a loop early, skip the rest of an iteration, or transfer control to a labeled point. Go provides three branching keywords—break, continue, and goto—and a label mechanism that works with all three. This page covers how each one behaves, when to use them, and the mistakes that trip up even experienced developers.

Break and Continue

break and continue are the two statements you reach for inside loops. They alter what the loop does next. break stops the loop entirely. continue stops only the current iteration and moves on to the next one.

How break Works

A break statement immediately terminates the innermost for, switch, or select statement that contains it. Execution resumes at the first line after that block.

Think of break as an emergency exit. When the program hits it, the loop stops—no more iterations, no cleanup inside the loop body.

package main
import "fmt"
func main() {
    for i := 1; i <= 10; i++ {
        if i == 5 {
            break
        }
        fmt.Println(i)
    }
    fmt.Println("Loop finished.")
}

Running this prints numbers 1 through 4, then "Loop finished." The loop condition i <= 10 would have allowed 10 iterations, but the break at i == 5 cuts it short. The check i == 5 happens before the fmt.Println(i) call, so 5 never prints.

break Exits Only the Innermost Block:

A break inside a nested loop only exits the loop it directly sits in. The outer loop keeps running. If you need to break out of an outer loop from inside a nested one, you must use a label.

How continue Works

continue skips the remainder of the current loop iteration and jumps straight to the loop's next iteration check. In a for loop with a post statement (like i++), the post statement still runs before the condition is tested again.

Use continue when you want to ignore some values in a loop without ending the loop entirely.

package main
import "fmt"
func main() {
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue
        }
        fmt.Println(i)
    }
}

The output is 1 3 5 7 9. Every even number hits continue, which skips the fmt.Println call and causes i++ to run, moving to the next odd number. The loop still finishes all 10 iterations; it just prints only the odd ones.

continue Is Only Valid Inside for Loops:

In Go, continue cannot appear inside a switch or select statement unless those are themselves inside a for loop. Placing a bare continue in a switch that is not inside a loop causes a compile error. This is different from break, which works in for, switch, and select.

Break and Continue with Labels

A label gives a name to a loop statement. When you use a label with break or continue, you can control which loop gets affected—essential for nested loops.

A label is written as an identifier followed by a colon, placed immediately before the loop statement:

outerLoop:
for i := 1; i <= 3; i++ {
    for j := 1; j <= 3; j++ {
        if i*j > 4 {
            break outerLoop
        }
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

Without the outerLoop label, the break would only exit the inner j loop, and the outer i loop would continue with the next i. With the label, it breaks out of the outer loop entirely. The output stops once i*j exceeds 4.

continue works the same way. A continue outerLoop inside the inner loop would skip the rest of the current inner loop iteration and move the outer loop to its next iteration—effectively restarting the inner loop with the next value of i.

Label Breaks Keep Nested Logic Clean:

When you need to exit a deeply nested loop based on a condition found deep inside, a labeled break is the cleanest way. The alternative—setting a flag variable and checking it at each level—adds clutter and error-prone state. Labeled breaks make the intent explicit and the code linear.

Labels Only Apply to Loop, Switch, or Select:

A label used with break or continue must be defined on a for, switch, or select statement. You cannot label an arbitrary block or an if statement and use break to jump to it. The compiler rejects any label that does not sit on one of those three statement types.

The goto Statement

goto transfers control to a labeled statement within the same function. It is unconditional—no condition is evaluated—and can jump forward or backward. goto has a reputation for creating tangled code, but Go restricts it enough to keep misuse limited while still allowing useful patterns.

Basic goto Usage

You define a label somewhere in the function and then jump to it with goto.

package main
import "fmt"
func main() {
    i := 0
loopStart:
    if i < 3 {
        fmt.Println(i)
        i++
        goto loopStart
    }
    fmt.Println("Done")
}

This prints 0, 1, 2, then "Done". The goto loopStart sends execution back to the label loopStart: before the if check. This is a manual loop, but real for loops are almost always clearer.

A more practical use of goto appears in error handling when you need to clean up resources before returning.

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    // ... read data ...
    if someCondition {
        goto closeFile
    }
    // ... more processing ...
closeFile:
    f.Close()
    return nil
}

Here, goto closeFile jumps to the cleanup code at the bottom of the function, skipping any remaining processing. This avoids duplicating the f.Close() call in every early return path.

goto Can Produce Correct but Inscrutable Code:

A single goto used for error cleanup is fine. Multiple goto statements jumping across a function make the control flow nearly impossible to follow. As a rule, if you find yourself writing more than one goto in a function, restructure the logic into smaller functions or use defer for cleanup instead.

Rules That Constrain goto

Go's compiler enforces several restrictions that prevent goto from creating malformed code:

  • You cannot jump over a variable declaration that introduces a new variable into the scope. The compiler reports an error: goto jumps over declaration of variable.
  • You cannot jump into a block from outside it. Labels must be at the same scope level or outer.
  • goto must stay within the same function. You cannot jump between functions.

These rules mean you cannot use goto to bypass initialization, skip past variable definitions, or enter an inner block through the back door. The language was designed to allow goto but prevent the worst abuses.

goto Across Variable Declarations Causes Compile Errors:

This code will not compile:

goto skip
x := 10
skip:
fmt.Println(x)

The label skip appears after x's declaration, but the goto jump skips over it. Go forbids this because it would leave x uninitialized yet in scope. You must declare variables before the goto target or restructure to avoid jumping over declarations.

Labels

Labels in Go are identifiers that mark a statement as a target for break, continue, or goto. A label is defined by writing the identifier followed by a colon on the same line as the statement it marks, or on the line immediately before it.

myLabel:
for i := 0; i < 10; i++ {
    // ...
}

The label myLabel now refers to this for statement. Any break myLabel, continue myLabel, or goto myLabel elsewhere in the same function can reference it.

Label Scope Rules

A label's scope is the entire function body in which it is declared. This means you can jump to a label from anywhere within the function—earlier lines, later lines, inside nested blocks—provided you obey the goto restrictions about variable declarations.

Unlike many languages that restrict label visibility to the surrounding block, Go gives labels function-wide scope. A label declared inside a nested block is reachable from outside that block, but goto cannot jump into that block from the outside.

func demo() {
    if true {
        inner: // this label is visible to the whole function
        fmt.Println("inside")
    }
    goto inner // valid jump? No, because goto cannot jump into a block from outside.
}

The label inner is defined, but goto inner from outside the if block fails to compile because Go prohibits jumping into a block.

Unused Labels Are Illegal:

If you declare a label and never reference it with break, continue, or goto, the compiler will flag it as an error. Go enforces that every declared label must be used somewhere in the function.

When to Use Labels

Labels are most valuable with break and continue inside nested loops, as shown earlier. They let you explicitly name which loop you want to exit or continue, removing ambiguity and making the code self-documenting.

With goto, labels serve as jump targets for error cleanup or rare control flow patterns where structured constructs would be overly verbose. A well-placed label improves readability when the alternative is deep nesting or repeated cleanup code.

Labels Clarify Intent in Nested Loops:

A label on an outer loop tells anyone reading the code exactly which loop a break or continue affects. Without the label, a reader must trace braces to determine the innermost loop—trivial for simple code, but error-prone in longer functions. Adding a label with a descriptive name (like rowLoop or searchCompleted) acts as built-in documentation.

Summary

Branching statements in Go give you fine-grained control over loops and function flow. break exits a loop or switch early, continue skips an iteration, and goto performs an unconditional jump. Labels tie these together, letting you target specific loops or statements by name instead of relying on block nesting alone.

The most common daily use is break and continue in for loops. When nested loops appear, labeled break and continue keep the logic straightforward and avoid flag variables. goto is rare but has a legitimate niche for error handling in functions that need cleanup before returning. The language's restrictions on goto—no jumping over variable declarations, no entering inner blocks from outside—protect you from the worst consequences of unstructured jumps.