Escaping nested loops without flags
You are scanning a two-dimensional grid for a specific value. The outer loop walks rows. The inner loop walks columns. You find the value at row 3, column 7. You break the inner loop. The outer loop keeps running. You add a boolean flag. You check the flag at the top of the outer loop. It works, but the logic is cluttered with state management that exists only to exit.
Go offers a cleaner tool. Labels let break and continue target specific loops or blocks by name. You mark a statement with a label, then reference that label in your control flow command. The jump is direct. No flags. No helper functions. No extra variables.
How labels work
A label is an identifier followed by a colon, placed immediately before a statement. The label does not change the statement's behavior. It simply gives the statement a handle that break and continue can grab.
When you write break labelName, execution jumps to the statement immediately following the labeled statement. The label must be in the same function. The break must be nested inside the labeled statement. The compiler enforces these rules strictly.
Labels work on any statement, not just loops. You can label an if, a switch, or a block. This is a feature many Go developers overlook. You can break out of a labeled if block to skip the rest of the conditional logic and resume after the block. You cannot continue a labeled if, because continue only applies to loops.
Minimal example: breaking out
Here's the simplest label usage: mark the outer loop, break to it from inside.
package main
import "fmt"
func main() {
// Label marks the outer loop so break can target it directly.
// The label name follows standard identifier rules.
searchGrid:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
// Check for the target value.
if i == 2 && j == 3 {
// Found it. Break the outer loop immediately.
// Execution jumps to the line after the labeled loop.
break searchGrid
}
fmt.Printf("Checking (%d, %d)\n", i, j)
}
}
// This runs after the break exits the labeled loop.
fmt.Println("Search complete.")
}
The output stops printing coordinates once the condition matches. The break searchGrid statement exits both loops in one step. The code after the loop runs immediately.
Labels on non-loop statements
Labels are not limited to loops. You can attach a label to an if statement. This lets you break out of a complex conditional block without wrapping it in a function.
This pattern is useful for validation or configuration checks where multiple conditions must pass, and you want to bail out early if any fail. Instead of nesting if statements deeply, you can break out of the validation block.
package main
import "fmt"
type Config struct {
Host string
Port int
Debug bool
}
func validateConfig(cfg Config) bool {
// Label on the if block allows breaking out of the conditional section.
// This avoids deep nesting for multiple validation checks.
validate:
if cfg.Host != "" {
// Check port range.
if cfg.Port < 1 || cfg.Port > 65535 {
// Invalid port. Break out of the validation block.
break validate
}
// Check debug flag consistency.
if cfg.Debug && cfg.Port == 80 {
// Debug mode on port 80 is forbidden.
break validate
}
// All checks passed.
return true
}
// If we break here, or if Host is empty, we reach this point.
// The label break jumps to the statement after the if block.
return false
}
func main() {
cfg := Config{Host: "localhost", Port: 80, Debug: true}
fmt.Println("Valid:", validateConfig(cfg))
}
The break validate statement exits the if block. Execution continues at the return false statement. This keeps the validation logic flat and readable. The compiler ensures the break is inside the labeled block.
Realistic example: skipping rows with continue
Labels also work with continue. continue labelName jumps to the next iteration of the labeled loop. This is useful when you need to skip the rest of a nested structure and move to the next outer iteration.
Imagine processing a grid of data where any bad value in a row invalidates the entire row. You want to skip processing the rest of the row and move to the next one.
package main
import "fmt"
func processGrid(grid [][]int) {
// Label marks the row loop for skipping entire rows.
// The inner loop checks cells, the outer loop processes rows.
rowLoop:
for _, row := range grid {
// Check each cell in the row.
for _, cell := range row {
if cell < 0 {
// Negative cell found. Skip the rest of this row.
// Continue jumps to the next iteration of rowLoop.
continue rowLoop
}
}
// Only reached if no negative cells were found.
fmt.Println("Processing valid row:", row)
}
}
func main() {
data := [][]int{
{1, 2, 3},
{4, -1, 6},
{7, 8, 9},
}
processGrid(data)
}
The continue rowLoop statement aborts the inner loop and the rest of the current outer iteration. The next row starts immediately. This avoids a flag variable and keeps the row-processing logic intact.
Pitfalls and compiler checks
Labels are powerful but constrained. The compiler prevents misuse.
You cannot break to a label that is not defined. The compiler rejects the code with label X not defined if you reference a label that doesn't exist in the current function. Labels do not cross function boundaries.
You cannot break to a label from outside its scope. If you write break labelName but the break statement is not nested inside the labeled statement, the compiler complains with break label X outside of loop/if. The break must be physically inside the block controlled by the label.
You cannot continue a non-loop label. If you label an if block and try continue labelName, the compiler rejects it with continue label X outside of loop. The continue statement only applies to loops.
Labels can hurt readability if overused. A function with multiple labeled breaks and continues becomes hard to follow. Use labels for clear escape hatches, not for complex flow control. If the logic requires jumping around in multiple directions, refactor into smaller functions.
Go removed goto to prevent unstructured code. Labels provide a middle ground. You can jump out of nested structures, but only to the end of a statement. You cannot jump into the middle of a block. You cannot jump across functions. The compiler enforces structure. Labels are structured control flow, not spaghetti.
When to use labels
Use a label when you need to exit multiple nested loops or blocks immediately and a helper function would add unnecessary indirection.
Use a boolean flag when the nesting is shallow or the exit condition is complex and benefits from a named variable that carries state across iterations.
Use a helper function when the nested logic is substantial enough to warrant its own scope and return value, or when the early exit logic is reused elsewhere.
Use return when you are already inside a function and want to exit the function entirely rather than just a loop or block.
Use plain sequential code when you don't need early exit; let the loops finish naturally.
Labels are escape hatches. Use them to exit, not to dance around.