What restrictions does Go place on `goto`, and when is it still the right tool?
answer
- it cannot leave the function
- you may not land inside a block
- a skipped declaration is the usual error
- jumping backwards is still allowed
basics
~20 sA goto may only target a label in the same function, may not jump into a block, and may not skip a variable declaration in scope at the label. It survives for shared cleanup and generated code.
solid answer
~50 sGo kept `goto`, but the spec fences it so it cannot break the compiler's scoping guarantees. Three rules: the label must be declared in **the same function**, so you cannot jump out of a function literal or into another function; the jump may not **enter a block** from outside it, so you cannot land in the middle of a `for` body or an `if` branch; and it may not **skip a variable declaration** that is still in scope at the label, which is the error people actually hit — `goto done jumps over declaration of buf`. Backward jumps are legal. In practice it earns its place in two situations: one shared error or cleanup tail in a function with several failure points, and machine-generated code where structured control flow would be contorted. For leaving nested loops a labeled `break` reads better, because it names the loop it terminates.
code
go · 7 linesif !ok {
goto done // compile error: goto done jumps over declaration of buf
}
buf := make([]byte, 0, 64)
use(buf)
done:
returngo deeper
Just know that Go still has goto, that it is rare in ordinary code, and that a labeled break is the normal way to leave a nested loop.
Explain the three restrictions — same function, no jumping into a block, no skipping an in-scope declaration — and recognise the jumps-over-declaration compile error and its two fixes.
Justify the narrow cases where goto is clearest, such as one shared cleanup tail in a hot path, and show you would restructure scopes rather than fight the compiler's declaration rule.
Decide whether goto is permitted in your codebase at all and where the exception lives — typically generated code and measured hot paths — so reviewers are not relitigating it in every pull request.
## Go still has `goto` It is a real keyword, not a reserved-but-unused one. `goto Label` transfers control to the statement carrying `Label:`. What makes it tolerable is that the spec restricts it to jumps the compiler can still reason about, so it cannot produce a variable that is in scope but never initialised, nor land in the middle of a construct that expects to have been entered from the top. ## The three restrictions **1. The label must be in the same function.** A label's scope is the body of the function that declares it, excluding any nested function literal. So you cannot `goto` out of a closure into its enclosing function, and there is no such thing as a cross-function jump. This alone keeps `goto` from being the unstructured jump it is in older languages. **2. You may not jump into a block.** A `goto` outside a block cannot target a label inside it. Landing inside a `for` body, an `if` branch, or a bare `{ ... }` would skip whatever entering that block normally establishes, so the compiler refuses. Jumping *out* of a block to a label in an enclosing one is fine. **3. You may not jump over a variable declaration in scope at the label.** This is the rule that actually bites: ``` if !ok { goto done // error: goto done jumps over declaration of buf } buf := make([]byte, 0, 64) use(buf) done: return ``` At `done`, `buf` is in scope — the code after the label is entitled to read it — but the jump never executed its declaration. Rather than define that hole away, the spec forbids the jump. There are two fixes, and which one you pick is a design decision: **move the declaration above the `goto`**, so the jump no longer skips it, or **wrap the skipped region in its own block** so `buf`'s scope ends before the label. The second is usually the honest one, because it states that the variable belongs to that section only. Backward jumps are legal and are not restricted by rule 3 — going back to an earlier label reaches a point where fewer variables are in scope, not more — though rule 2 still applies: you cannot jump backwards into a block you are outside of. ## Where it is still the right tool Two places, and interviewers are listening for you to name them rather than to say "never". **A single shared tail.** A function with several failure points that must all run the same short unwinding sequence can jump to one `fail:` label instead of repeating it or building a nested-`if` staircase. In Go this is rarer than in C, because `defer` covers most cleanup and multiple returns are idiomatic — but in hot paths where a `defer` is measurable, or where the tail needs to run only on some paths, the label is the clearer expression. **Generated code.** A code generator emitting a state machine does not benefit from structured control flow; it benefits from a direct correspondence between states and labels. The standard library itself uses `goto` in a handful of tight scanners and parsers for exactly this reason. ## Where it is the wrong tool Leaving nested loops. A labeled `break` names the statement it terminates, and the compiler checks that the label really is a loop, so a reader knows precisely where control resumes. A `goto` can land anywhere in the function, so the reader has to go find the label and then work out what is in scope there. Prefer `break Label` for loop exit and reserve `goto` for the shared-tail shape. Also wrong: emulating a loop. `goto` backwards to build iteration is legal and unreadable; `for` exists. ## The review posture Because the honest set of uses is so small, most codebases treat `goto` as something that needs a sentence of justification in the review, not as a banned construct. The compiler's own restrictions mean a `goto` that compiles is at least scope-safe — the risk is readability, not undefined behaviour. What a reviewer should check is whether the same function could have returned early, deferred the cleanup, or extracted the tail into a small helper, because in ordinary application code one of those three is almost always available. ## Summary Same function, no jumping into a block, no skipping an in-scope declaration; backward jumps allowed; keep it for a single shared cleanup tail and for generated code, and use a labeled `break` for nested-loop exit.
- Why does Go reject a jump that skips a variable declaration?Because at the label the variable is in scope and the following code may read it, yet the jump never ran its declaration — the value would be undefined. Rather than define that away, the spec forbids the jump. The fixes are to move the declaration above the goto, or to wrap the skipped section in its own block so the variable's scope ends before the label.
- Is a backward goto legal in Go?Yes. Jumping back to an earlier label in the same function is allowed, provided the label is not inside a block you are currently outside of. The declaration rule cannot be violated going backwards, because you arrive where fewer variables are in scope. In hand-written code a plain `for` loop expresses the same thing far more clearly.
- How does goto compare with a labeled break for leaving nested loops?A labeled `break` names the loop it terminates and the compiler verifies the label really is an enclosing loop, so a reader knows exactly where control resumes. A `goto` may land anywhere in the function, so the reader must find the label and work out what is in scope there. Use the labeled break for loop exit; keep goto for a single shared cleanup or error tail.
saying these in an interview costs you the question
- Says Go removed goto from the language
- Thinks goto can target a label in another function
- Believes every forward jump is rejected
- Uses goto where a labeled break would be clearer
- Cannot explain the jumps-over-declaration error