Last Updated: May 22, 2026
Plain break and continue only act on the loop or switch that immediately encloses them. That works fine until you have a for inside another for and you want one statement to exit both at once. Labels are how Go lets you name an enclosing loop, switch, or select and target it directly with break, continue, or goto. This chapter covers all three uses, the rules goto plays by, and why most idiomatic Go code uses labels only when nothing simpler will do.
A label in Go is an identifier followed by a colon, attached to a for, switch, or select statement. The label gives the statement a name so other statements (specifically break, continue, and goto) can refer to it.
The label OuterScan: sits on the outer for loop. When the inner loop hits the condition row == 1 && col == 2, the break OuterScan statement exits the outer loop directly, skipping every remaining iteration of both loops. Without the label, a plain break would only stop the inner loop, and the outer one would continue with row == 2.
A few rules to keep in mind about labels:
for, switch, or select statement. You can't label an if, an assignment, or a function call. (The exception is goto, which can target any labeled point in the same function.)label OuterScan defined and not used.OuterScan, RetryCheckout, ParseLoop. This is to distinguish them from variables, which use camelCase.Cost: Labels themselves have zero runtime cost. The compiler turns break OuterScan into the same kind of jump it would generate for a plain break, just targeting a different point. The cost is purely in readability if labels are overused.
break LabelThe most common reason to use a label is to break out of nested loops in one shot. Imagine an inventory grid where rows represent product categories and columns represent individual products. You want to find the first product whose stock has dropped below a reorder threshold and stop the moment you find one.
The scan starts at row 0, walks through {12, 8, 15, 20}, finds nothing below 3, moves to row 1, and at column 1 finds 0. The break ScanInventory exits both loops at once. Without the label, a plain break would only stop the inner loop, and the outer one would happily move on to row 2 even though we've already found what we wanted.
The control flow is shown in the flowchart below.
The diagram shows the key fact about break Label: it doesn't just exit one loop, it jumps all the way to the statement immediately after the labeled loop. Whatever loops or switches sat between the break and the labeled statement are unwound at once.
Labels work, but they're not the only option. Two other patterns show up often in Go code.
Move the inner logic into a function and use return instead of break:
This is often cleaner. The function name documents what the loops are doing, and return makes the early exit obvious. Many Go style guides prefer this over labels when the nested loop block is doing one logical thing that has a natural name.
A boolean flag plus a plain break works too: set found = true, break the inner loop, and have the outer loop check !found as part of its continuation condition. This adds noise compared to a label, and gets unwieldy with three or more loops, but it's a fallback when readers of your code don't know labels well.
continue Labelcontinue plays the same trick. With a label, it continues the labeled enclosing for-loop instead of the innermost one. This is useful when an inner check decides the current outer iteration is bad and should be skipped entirely.
Consider validating orders. Each order is a list of items, and each item has a quantity. If any item in an order has a non-positive quantity, the whole order is invalid and should be skipped, not partially processed.
For each order, the inner loop checks every quantity. As soon as it finds a non-positive one, continue NextOrder jumps straight to the next iteration of the outer loop, skipping the "valid, processing" line entirely. If the inner loop completes without finding any bad quantity, control falls through to the processing line.
The same logic with a plain continue would only restart the inner loop, which is not what we want. Without a label you'd need a valid flag in the outer loop, set to false inside the inner loop, and then a guarded print after the inner loop ends. The labeled version is shorter and clearer once you know what continue Label does.
Cost: Whether you choose continue Label or a flag, the runtime cost is identical. The compiler emits the same kind of jump either way.
break vs continue with the Same LabelA common point of confusion: what's the difference between break Outer and continue Outer?
break Outer exits the loop labeled Outer entirely. Control resumes at the statement after that loop.continue Outer jumps to the next iteration of the loop labeled Outer. The loop's post-statement runs and the condition is checked again.If you scan the matrix [[1,2,3],[4,5,6],[7,8,9]] printing each value and exiting on 5, break Outer prints 1 2 3 4 and stops. continue Outer prints 1 2 3 4, skips 6 (the rest of the middle row), then prints 7 8 9 from the next row.
goto: Unconditional Jump Within a Functiongoto is the third and rarest member of the labeled-statement family. It performs an unconditional jump to a label within the same function. The label can sit on any statement, not just for/switch/select.
The Retry: label sits on a plain statement. goto Retry jumps back to it unconditionally, and the conditional if decides when to stop jumping.
This particular example is just a for loop in disguise, which is why most Go developers would write:
That's the right call almost every time. goto exists, but its day-to-day use is narrow.
goto Is ReasonableThere are a few genuine cases where goto reads cleanly:
Restarting a parsing state machine. When you're processing a stream of input and a particular condition means "start over from this state", goto can express that more directly than a deeply nested loop and flag.
Single cleanup point in a long function. In C, goto is used heavily to consolidate cleanup at the end of a function. Go has defer for this, which removes most of the need, but the pattern still appears occasionally in the Go standard library itself, especially in performance-sensitive code where defer overhead matters.
Compile-time loop unrolling style code. Code generated by tools sometimes emits goto to avoid loop overhead in tight, generated paths. You won't write this by hand.
A parsing-style example where goto reads naturally: We're walking through a checkout flow with multiple steps. If validation fails at any step, we restart the whole flow from scratch.
You could absolutely rewrite this with a for loop and break / continue. The goto version makes the restart explicit, which some readers find easier to follow when each retry step has different side effects.
Cost: goto is a single machine-level jump, the same as the jump emitted for break or continue. It has no special runtime overhead. The cost is purely in readability and maintenance.
goto Is WrongMost other uses are mistakes. If you find yourself writing goto to:
if would do the same)for is clearer)defer and if err != nil cover this)Then the goto is hiding what's actually going on. Use it only when the alternative is meaningfully worse.
gotoUnlike C, Go's goto is not a free-for-all. The compiler rejects two specific kinds of jumps:
You can't jump over a variable declaration that's in scope at the target.
This fails to compile:
If the goto succeeded, total would never be initialized, but it would still be in scope at the label. That's exactly the kind of zero-value-violating situation Go is designed to prevent.
The fix is usually to move the declaration before the goto or to restructure the code so the jump doesn't cross the declaration:
Now total is declared before the goto, so the jump no longer crosses a declaration that would become accessible at the label.
You can't jump into a block from outside.
This also fails:
You can jump out of a block, but not into one. Jumping into a block could skip declarations and initialization that the block is responsible for, which the language refuses to allow.
These two restrictions plug the main holes that make goto notorious in other languages. You can't accidentally read an uninitialized variable, and you can't accidentally enter a scope without going through its prologue.
break and continue aren't only useful for nested loops. They work with labeled switch and select statements too. This matters most when you have a switch or select inside a for loop and want a break inside the switch to exit the loop, not the switch.
Inside a switch, a plain break exits the switch. That's usually what you want, but if the switch is inside a loop and you want to break out of the loop based on a case, you need a label.
Without the label, break inside the switch would exit only the switch, and the loop would happily process delivered next. With break ProcessOrders, the loop ends as soon as a cancellation appears.
continue Label works the same way for skipping the rest of a loop iteration from inside a switch.
The same principle applies to select statements in concurrent code. A break inside a select exits the select; a break Label referring to an enclosing for exits that loop.
A handful of conventions cover most cases.
Use PascalCase. OuterLoop, RetryCheckout, NextOrder, ScanInventory. Lowercase labels like outer: work but they look like variables, which can be confusing.
**Pick a name that says why the label exists, not where it is.** OuterLoop: tells the reader nothing; ScanInventory: or ValidateOrders: does.
Don't label loops that don't need a label. Adding a label to a loop you never break to or continue to is dead code at best and confusing at worst.
Prefer extracting a function over labeled break. If a labeled break fits naturally, ask whether the nested loop block has a name. If it does (findLowStock, validateOrders), make it a function and use return. Labels work best when the loop body uses several local variables that would be painful to pass around as arguments.
Use `goto` as a last resort. Most labeled-statement uses in Go code are break Label or continue Label. goto shows up rarely, usually in performance-sensitive standard library code or in generated code. Don't avoid it dogmatically when it's the cleanest expression of the logic, but don't use it as a first move either.
The Go standard library uses goto in roughly a few dozen files, almost all in runtime, crypto, math/big, and parsers. Application code uses it far less. If you go a year without writing goto in production Go code, you're in good company.
The following program uses break Label and continue Label together to validate a batch of orders against a 2D inventory grid, then scan the grid for the first low-stock cell.
The first labeled loop uses continue NextOrder to abandon the current order whenever any item fails validation. The second uses break ScanGrid to exit both loops the moment a low-stock cell appears. Together they cover the two main patterns you'll use day to day.