Labels Break and Continue in Nested Loops in Go Code
A labeled break or continue reaches an outer loop from inside an inner loop, switch or select. See the syntax and when a function is cleaner.

You are searching a grid for a value. You found it three loops deep and you want to stop everything, and the break you wrote stopped one loop. The Go answer is a label, the small piece of syntax that most programs use zero or one times. It is worth knowing exactly, because the same trap appears inside switch and select, which are covered in the switch article and select in Go. This tip is part of the Go basics path, and it pairs with for range.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Break and continue with a label
Write the label on its own line before the statement it names, followed by a colon. Then break or continue can name it:
package main
import "fmt"
func main() {
grid := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
search:
for r, row := range grid {
for c, v := range row {
if v == 5 {
fmt.Println("found 5 at", r, c)
break search
}
}
}
rows:
for _, row := range grid {
for _, v := range row {
if v%3 == 0 {
continue rows
}
fmt.Print(v, " ")
}
}
fmt.Println()
}found 5 at 1 1
1 2 4 5 7 8break search ended both loops at the first match. continue rows is the less familiar form: when the inner loop meets a multiple of three, it abandons the rest of that row and the outer loop advances, so the output skips everything after each row's first multiple of three. Without the label, continue would only skip the single value. Labels live in their own namespace, so a label named rows does not clash with a variable of that name.
The rule that catches typos
A label that nothing uses is an error:
package main
import "fmt"
func main() {
outer:
for i := 0; i < 3; i++ {
fmt.Println(i)
}
}# example.test/lab
./main.go:6:1: label outer defined and not usedThe message is label outer defined and not used. It is the same strictness as unused variables and imports, and it means a refactor that removes the last break outer forces you to remove the label as well. A label must also be attached to a for, switch or select for break and continue to refer to it, and continue needs a for.
A switch or select inside a loop
The most common reason to need a label has nothing to do with nested loops. It is a switch or select inside a loop where you want to leave the loop. A bare break there only leaves the switch, and the loop carries on, which has caused infinite loops in worker code that tried to stop on a "done" case:
package main
import "fmt"
func main() {
events := make(chan string, 4)
for _, e := range []string{"tick", "tick", "stop", "tick"} {
events <- e
}
close(events)
loop:
for e := range events {
switch e {
case "stop":
fmt.Println("stopping")
break loop
default:
fmt.Println("handling", e)
}
}
}handling tick
handling tick
stoppingbreak loop leaves the for, and the last tick stays unread in the channel. A plain break in that stop case would have printed stopping and then handled the final tick.
When a function is cleaner
If the labeled loop sits in a function that has nothing else to do after the search, return from it. A return from the middle of nested loops says the same thing without a label, and the function name documents what was being searched for:
func find(grid [][]int, target int) (r, c int, ok bool) {
for r, row := range grid {
for c, v := range row {
if v == target {
return r, c, true
}
}
}
return 0, 0, false
}I use a label when the code after the loops belongs to the same function, and a function when it does not. If you find yourself with two or three labels in one function, that function is doing too much, and splitting it up will help more than any label name. The rules are in the specification on labeled statements and on break statements.


