Break and Continue in Go

How the break and continue statements give you precise control over loop and switch execution in Go, including labeled forms for nested loops

When a loop is running, you sometimes need to stop it immediately—not at the top of the next iteration, but right in the middle of an iteration, because the rest of the work no longer matters. Other times you want to skip only the current iteration but let the loop keep going. Go gives you two keywords for exactly these situations: break and continue. They are small statements with a single job each, and understanding them fully means you can write loops that stop exactly when they should, without awkward boolean flags or extra condition checks.

This document covers both statements inside for loops, switch statements, and select statements, plus labeled break and continue for controlling nested loops.

The break Statement

break terminates the innermost enclosing for, switch, or select statement immediately. Control passes to the first statement after that block. You use it when continuing the loop serves no purpose—for example, when you have already found the item you were searching for, or when an error makes the remaining iterations meaningless.

Breaking out of a Loop Early

Consider a slice of integers. You want to find the first negative number and then stop. Without break, you would need a flag variable or a convoluted condition in the loop header. With break, the intent stays clean.

func firstNegative(nums []int) int {
    for _, n := range nums {
        if n < 0 {
            return n // this also exits the loop, but via function return
        }
    }
    return 0
}

The example above uses return, which works if the loop is inside its own function. But when the loop is part of a larger function and you need to keep doing work after the loop, break is the right tool.

package main
import "fmt"
func main() {
    numbers := []int{4, 7, -1, 3, 5}
    found := 0
    for _, n := range numbers {
        if n < 0 {
            found = n
            break
        }
    }
    fmt.Println("First negative:", found)
    // Rest of the program continues here
}

When you run this, the loop iterates through 4, then 7. At -1, the condition triggers, found is set, and break immediately ends the for loop. The next line prints First negative: -1. The element 3 and 5 are never visited.

break vs. return:

A break inside a loop jumps only to the statement after the loop, while return exits the entire function. If you have cleanup or additional logic after the loop, break is what you need.

break in switch and select

In Go, switch and select statements do not fall through by default. So why would you need break inside them? Because a switch or select may be nested inside a loop, and a plain break only exits the innermost enclosing switch/select—not the loop that contains it.

This program prints the value of each channel message except the one that signals an abort.

package main
import "fmt"
import "time"
func main() {
    data := make(chan int)
    abort := make(chan bool)
    go func() {
        for i := 1; i <= 5; i++ {
            data <- i
            time.Sleep(50 * time.Millisecond)
        }
        abort <- true
    }()
loop:
    for {
        select {
        case v := <-data:
            fmt.Println("Received:", v)
        case <-abort:
            fmt.Println("Abort signal, stopping.")
            break loop // without label, this would only break the select
        }
    }
}

Without the label loop, the break would only exit the select body, and the for loop would begin another iteration immediately, hanging forever because no further values arrive. The labeled break loop jumps to the statement after the labeled for loop, which is exactly the behaviour we want.

Unlabeled break inside select or switch only leaves that block:

A plain break inside a select or switch nested within a loop will not exit the loop. It breaks only the select/switch. To break the loop, use a label as shown above.

The continue Statement

continue skips the remaining body of the current iteration and jumps directly to the next iteration of the loop. In a for loop with a post statement (like for i := 0; i < n; i++), the post statement still executes before the next condition check.

This is useful when a particular iteration requires no further processing but you do not want to stop the entire loop.

Skipping Unwanted Values

Imagine you have a slice of numbers and you want to print only the even ones. You could nest the print inside an if, but continue lets you express the filter as an early return from the iteration body.

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

The output is all even numbers from 2 to 10. When i is odd, continue skips the fmt.Println line and the loop moves to the increment (i++) and condition check. The loop runs all ten iterations, but the body only executes fully for even numbers.

Clarity through early skips:

Using continue to filter out unwanted cases often makes the loop logic easier to follow than a deeply nested if-else. The reader immediately knows that after the continue, only the “good” values proceed.

continue in for-range Loops

The same behaviour applies to for range loops. Suppose you are processing file names and want to ignore any entry that is a directory.

for _, entry := range entries {
    if entry.IsDir() {
        continue
    }
    // process file
}

The iteration variable entry is still advanced to the next element, and the loop continues normally.

Labeled break and continue

When you have nested loops, break and continue without a label affect only the innermost loop. This is often sufficient, but not always. If the innermost loop detects a condition that should stop or skip an outer loop, you need a label.

A label in Go is an identifier followed by a colon (:) placed before the statement you want to refer to. Labels can only be used with break and continue—and goto, which is covered separately—and must be placed on a statement within the same function.

Labels must be on a statement, not a declaration:

A label like outer: must be followed by a statement that it labels, such as a for loop. Placing a label on its own line with nothing after it is a compile error.

Breaking Out of Nested Loops

Searching a two-dimensional slice for a specific value is a classic case. When you find the value, you want to stop both loops—not just the inner one.

package main
import "fmt"
func main() {
    grid := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }
    target := 5
    found := false
    var row, col int
outer:
    for i, r := range grid {
        for j, val := range r {
            if val == target {
                row, col = i, j
                found = true
                break outer
            }
        }
    }
    if found {
        fmt.Printf("Found %d at row %d, column %d\n", target, row, col)
    } else {
        fmt.Println("Not found")
    }
}

break outer transfers control to the statement immediately after the outer for loop. Without the label, break would only exit the inner loop, the outer loop would continue to the next row, and the search would waste iterations.

Continuing an Outer Loop

Labels also work with continue. Consider a scenario where processing each row in a table can be skipped entirely if any cell in that row is invalid.

rowLoop:
    for _, row := range rows {
        for _, cell := range row {
            if !isValid(cell) {
                fmt.Println("Invalid row detected, skipping")
                continue rowLoop
            }
        }
        processRow(row)
    }

When an invalid cell is found, continue rowLoop jumps to the next iteration of the outer rowLoop, skipping processRow for the current row. The inner loop does not continue checking the remaining cells of that row.

continue only for for loops:

You cannot use continue inside a switch or select unless they are inside a for loop, because continue means “start the next loop iteration” and a switch is not a loop.

How Beginners Should Think About break and continue

Think of a loop as a conveyor belt. Each item comes to you, you do some work, and the belt moves to the next item.

  • break is the emergency stop button. You press it, the belt halts, and you walk away from the machine entirely.
  • continue is the “skip” button. The current item passes you by untouched, but the belt keeps moving and the next item arrives normally.

This mental model holds for nested loops, too. A label simply says “I want to stop (or skip) the conveyor belt that I named, not just the one I am standing next to.”

Common Mistakes

  • Using break in a select inside a for loop without a label. As shown earlier, the break only leaves the select, not the for. The loop spins forever. Always label the outer loop when you intend to exit it from a select or switch.
  • Putting a label on an empty line. A label must be followed by a statement. This is a compile-time error.
  • Using continue outside a loop. Go’s compiler will refuse; continue is only valid inside a for.
  • Assuming continue skips the post statement in a classic for loop. In for i := 0; i < 10; i++, a continue still runs i++ before the next condition check. This is different from some other languages where continue might jump directly to the condition.
  • Confusing break with return. If you break from a loop but the function still needs to do something (close a file, print a summary, send a response), breaking is correct. Returning would skip that remaining work.

Infinite loops from unlabeled break in select:

A for loop containing a select with only a plain break will never terminate unless there is an external mechanism like a channel close. Always double-check the target of your break when inside select.

Practical Real-World Usage

Break and continue appear in almost every non-trivial Go program.

  • Search and early termination – scanning a slice, map, or channel until a condition is met.
  • Input validation loops – skipping malformed input lines and continuing with the rest.
  • Concurrent patterns – using a for-select loop to handle multiple channels and breaking out on a done signal.
  • Nested data processing – iterating over matrices, trees, or JSON structures where an inner condition affects the outer loop.

They replace awkward boolean flags and make the loop’s control flow explicit. When a reader sees break, they know the loop ends here. When they see continue, they know the current iteration ends here but the loop goes on. That clarity is the whole point.

Summary

break and continue are precise instruments for loop control. break ends a loop or switch immediately; continue jumps to the next iteration. Both can be directed at a specific enclosing loop using labels, which is essential when working with nested loops or for-select patterns.

The key insight is that these statements eliminate the need for temporary booleans or complex loop conditions when the decision to stop or skip is made deep inside the loop body. Using them well makes loops shorter, clearer, and easier to reason about.