Goto Hell With Labels in Golang
Exploring goto statement in Go and learning how to use them and how the standard library handles flow control with goto and labels
by Percy Bolmér, August 22, 2021
![Photo by [Jeswin Thomas] on Unsplash](https://d33wubrfki0l68.cloudfront.net/7c8cfbf392dd6b62bc7e86defd2fa06edc0bfa77/3b2ab/_app/immutable/assets/img0-2a3f4637.webp)
Many programming languages have some sort of goto statement. If you’re not familiar with goto it is a way of jumping in code into a certain position. It can be used to control the flow of execution. This kind of statement is usually very hated in developer communities because it can lead to hard-to-follow code. I have always liked the goto statement, and I started to wonder, is it used in the standard library of Go and decided to take a deep dive.
I’ve met many Go developers who do not know that goto exists. However it does, and it is used quite a bit in the standard library. A simple search in the standard library reveals that the goto statement is used 493 times in 98 different files if we skip comments and non-Go files. We will look at some of these later in the article, let’s first learn how the statement works.
Goto statement in Go
The way it works is that you can write down a label which is a custom keyword that can be recognized by the goto statement. A label is a string and is created by setting a semicolon after the label.
label:
// Code to execute in label
Go has some safety in its implementation in that a label has to be created in the same function that the goto statement. A label has to be used, the compiler will catch this for you. The same goes if you create a variable after a label, all variables used in the label have to be defined earlier in the scope.
The best way to understand it is probably to see it, here is a super simple example that shows how to set a label and then jump to it, skipping any code in between. We will create a for loop that will execute 10 times, but when i is equal to 5 we will use a goto to break the execution. Note that the label has to be created inside the same function, or the compiler will fail. The same goes if the label is not used, then the compiler will fail again. In the example, I have named the label into the exit.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, Reader! Your learning about 'goto' statement")
// We create a for loop which runs until i is 10
for i := 0; i < 10; i++ {
fmt.Printf("Index: %d\n", i)
if i == 5 {
// When i is 5, lets exit by using goto
goto exit
}
}
fmt.Println("Skip this line here")
// Create the exit label and insert code that should be executed when triggered
exit:
fmt.Println("We are now exiting the program")
}
That’s it! There is no other magic, it is that simple. Remember that you cannot jump to labels outside of the same function. This is a nice safeguard that removes a lot of the confusion.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, Reader! Your learning about 'goto' statement")
// We create a for loop which runs until i is 10
for i := 0; i < 10; i++ {
// printInteger will try to goto to exit, which will not work
printInteger(i)
}
fmt.Println("Skip this line here")
// Create the exit label and insert code that should be executed when triggered
exit:
fmt.Println("We are now exiting the program")
}
func printInteger(i int) {
fmt.Printf("Index: %d\n", i)
if i == 5 {
// When i is 5, lets exit by using goto
goto exit
}
}
A thing to look out for is that labels will be executed if your regular code flow traverses into them without returning. This can happen if you have multiple labels at the end of a function.
I’ll add an if statement to showcase this, if the I counter becomes 6 it will print. I will never become 6 because when it is 5 we will break out of the for loop, so we will never trigger the second label, right? The first time I used labels that were how I thought it would work, but it will run anything below the first label, like normal code execution flow.
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello, Reader! Your learning about 'goto' statement")
// We create a for loop which runs until i is 10
for i := 0; i < 10; i++ {
fmt.Printf("Index: %d\n", i)
switch i {
case 5:
goto exit
case 6:
goto second
case 7: // I have to reference third label otherwise compiler wont run
goto third
}
}
fmt.Println("Skip this line here")
// Create the exit label and insert code that should be executed when triggered
exit:
fmt.Println("We are now exiting the program")
second:
fmt.Println("This is a second label executed because it comes after exit")
return
third:
fmt.Println("this wont print")
}
Goto in standard library
Let’s explore how the goto statement is used inside the standard library so that we can get a better grip of good usage. I’m going to assume that the std lib is considered good usage.
One great example I’ve found is the go/scanner which is a package for scanning Go source code. Scanning text files can be a really hard task, especially if you don’t know what is inside them. It is common to use a technique called Lexical analysis. I won’t describe the exact internal workings, but you create strings called tokens that can be used to identify the current location of the text. Imagine yourself writing a text scanner that reads source code and validates it yourself, It is no trivial task.
The section we are going to look at which is fairly basic is the one where the Go team scans for comments in the source code. The function is scanComment and contains a label named exit, just like the example we used before.
To make the code a bit more understandable I’d like to give some context. s.ch is the current character being parsed. s.next() reads the next Unicode character into s.ch There is a goto at the first if when it’s a regular comment and another when it is a multi-line comment. This is a great example of how to skip code blocks without using multiple flags to control the state. They could have done this without a goto using a few booleans, such as an isSingleLineComment and if that is true, then skip going into the multiline parsing. But they avoid a lot of complicated flow logic but using a simple goto and I like that solution.
The second goto is used when they are parsing a multiline comment. In it, they keep iterating until they find a comment closure marked by */. It is a loop that runs until the end of the file if no closing appears. Instead of using a break to cancel the for loop when the condition is met, they use a goto which is another great way to control the flow. They also bypass the s.error which is used to set errors found.
func (s *Scanner) scanComment() string {
// initial '/' already consumed; s.ch == '/' || s.ch == '*'
offs := s.offset - 1 // position of initial '/'
next := -1 // position immediately following the comment; < 0 means invalid comment
numCR := 0
if s.ch == '/' {
//-style comment
// (the final '\n' is not considered part of the comment)
s.next()
for s.ch != '\n' && s.ch >= 0 {
if s.ch == '\r' {
numCR++
}
s.next()
}
// if we are at '\n', the position following the comment is afterwards
next = s.offset
if s.ch == '\n' {
next++
}
goto exit
}
/*-style comment */
s.next()
for s.ch >= 0 {
ch := s.ch
if ch == '\r' {
numCR++
}
s.next()
if ch == '*' && s.ch == '/' {
s.next()
next = s.offset
goto exit
}
}
s.error(offs, "comment not terminated")
exit:
lit := s.src[offs:s.offset]
// On Windows, a (//-comment) line may end in "\r\n".
// Remove the final '\r' before analyzing the text for
// line directives (matching the compiler). Remove any
// other '\r' afterwards (matching the pre-existing be-
// havior of the scanner).
if numCR > 0 && len(lit) >= 2 && lit[1] == '/' && lit[len(lit)-1] == '\r' {
lit = lit[:len(lit)-1]
numCR--
}
// interpret line directives
// (//line directives must start at the beginning of the current line)
if next >= 0 /* implies valid comment */ && (lit[1] == '*' || offs == s.lineOffset) && bytes.HasPrefix(lit[2:], prefix) {
s.updateLineInfo(next, offs, lit)
}
if numCR > 0 {
lit = stripCR(lit, lit[1] == '*')
}
return string(lit)
}
The second example of usage is found in go/types in the exprInternal function which is used in the core Go to type check. This is a great example of when we have a switch that needs to check many conditions and grows a bit too big. They use continue and break as usual to manage the flow, but they also use goto to standardize the error handling and avoid duplicating code.
// exprInternal contains the core of type checking of expressions.
// Must only be called by rawExpr.
//
func (check *Checker) exprInternal(x *operand, e ast.Expr, hint Type) exprKind {
// make sure x has a valid state in case of bailout
// (was issue 5770)
x.mode = invalid
x.typ = Typ[Invalid]
switch e := e.(type) {
case *ast.BadExpr:
goto Error // error was reported before
case *ast.Ident:
check.ident(x, e, nil, false)
case *ast.Ellipsis:
// ellipses are handled explicitly where they are legal
// (array composite literals and parameter lists)
check.error(e, _BadDotDotDotSyntax, "invalid use of '...'")
goto Error
case *ast.BasicLit:
x.setConst(e.Kind, e.Value)
if x.mode == invalid {
// The parser already establishes syntactic correctness.
// If we reach here it's because of number under-/overflow.
// TODO(gri) setConst (and in turn the go/constant package)
// should return an error describing the issue.
check.errorf(e, _InvalidConstVal, "malformed constant: %s", e.Value)
goto Error
}
case *ast.FuncLit:
if sig, ok := check.typ(e.Type).(*Signature); ok {
// Anonymous functions are considered part of the
// init expression/func declaration which contains
// them: use existing package-level declaration info.
decl := check.decl // capture for use in closure below
iota := check.iota // capture for use in closure below (#22345)
// Don't type-check right away because the function may
// be part of a type definition to which the function
// body refers. Instead, type-check as soon as possible,
// but before the enclosing scope contents changes (#22992).
check.later(func() {
check.funcBody(decl, "<function literal>", sig, e.Body, iota)
})
x.mode = value
x.typ = sig
} else {
check.invalidAST(e, "invalid function literal %s", e)
goto Error
}
case *ast.CompositeLit:
var typ, base Type
switch {
case e.Type != nil:
// composite literal type present - use it
// [...]T array types may only appear with composite literals.
// Check for them here so we don't have to handle ... in general.
if atyp, _ := e.Type.(*ast.ArrayType); atyp != nil && atyp.Len != nil {
if ellip, _ := atyp.Len.(*ast.Ellipsis); ellip != nil && ellip.Elt == nil {
// We have an "open" [...]T array type.
// Create a new ArrayType with unknown length (-1)
// and finish setting it up after analyzing the literal.
typ = &Array{len: -1, elem: check.typ(atyp.Elt)}
base = typ
break
}
}
typ = check.typ(e.Type)
base = typ
case hint != nil:
// no composite literal type present - use hint (element type of enclosing type)
typ = hint
base, _ = deref(typ.Underlying()) // *T implies &T{}
default:
// TODO(gri) provide better error messages depending on context
check.error(e, _UntypedLit, "missing type in composite literal")
goto Error
}
switch utyp := base.Underlying().(type) {
case *Struct:
if len(e.Elts) == 0 {
break
}
fields := utyp.fields
if _, ok := e.Elts[0].(*ast.KeyValueExpr); ok {
// all elements must have keys
visited := make([]bool, len(fields))
for _, e := range e.Elts {
kv, _ := e.(*ast.KeyValueExpr)
if kv == nil {
check.error(e, _MixedStructLit, "mixture of field:value and value elements in struct literal")
continue
}
key, _ := kv.Key.(*ast.Ident)
// do all possible checks early (before exiting due to errors)
// so we don't drop information on the floor
check.expr(x, kv.Value)
if key == nil {
check.errorf(kv, _InvalidLitField, "invalid field name %s in struct literal", kv.Key)
continue
}
i := fieldIndex(utyp.fields, check.pkg, key.Name)
if i < 0 {
check.errorf(kv, _MissingLitField, "unknown field %s in struct literal", key.Name)
continue
}
fld := fields[i]
check.recordUse(key, fld)
etyp := fld.typ
check.assignment(x, etyp, "struct literal")
// 0 <= i < len(fields)
if visited[i] {
check.errorf(kv, _DuplicateLitField, "duplicate field name %s in struct literal", key.Name)
continue
}
visited[i] = true
}
} else {
// no element must have a key
for i, e := range e.Elts {
if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
check.error(kv, _MixedStructLit, "mixture of field:value and value elements in struct literal")
continue
}
check.expr(x, e)
if i >= len(fields) {
check.error(x, _InvalidStructLit, "too many values in struct literal")
break // cannot continue
}
// i < len(fields)
fld := fields[i]
if !fld.Exported() && fld.pkg != check.pkg {
check.errorf(x,
_UnexportedLitField,
"implicit assignment to unexported field %s in %s literal", fld.name, typ)
continue
}
etyp := fld.typ
check.assignment(x, etyp, "struct literal")
}
if len(e.Elts) < len(fields) {
check.error(inNode(e, e.Rbrace), _InvalidStructLit, "too few values in struct literal")
// ok to continue
}
}
case *Array:
// Prevent crash if the array referred to is not yet set up. Was issue #18643.
// This is a stop-gap solution. Should use Checker.objPath to report entire
// path starting with earliest declaration in the source. TODO(gri) fix this.
if utyp.elem == nil {
check.error(e, _InvalidTypeCycle, "illegal cycle in type declaration")
goto Error
}
n := check.indexedElts(e.Elts, utyp.elem, utyp.len)
// If we have an array of unknown length (usually [...]T arrays, but also
// arrays [n]T where n is invalid) set the length now that we know it and
// record the type for the array (usually done by check.typ which is not
// called for [...]T). We handle [...]T arrays and arrays with invalid
// length the same here because it makes sense to "guess" the length for
// the latter if we have a composite literal; e.g. for [n]int{1, 2, 3}
// where n is invalid for some reason, it seems fair to assume it should
// be 3 (see also Checked.arrayLength and issue #27346).
if utyp.len < 0 {
utyp.len = n
// e.Type is missing if we have a composite literal element
// that is itself a composite literal with omitted type. In
// that case there is nothing to record (there is no type in
// the source at that point).
if e.Type != nil {
check.recordTypeAndValue(e.Type, typexpr, utyp, nil)
}
}
case *Slice:
// Prevent crash if the slice referred to is not yet set up.
// See analogous comment for *Array.
if utyp.elem == nil {
check.error(e, _InvalidTypeCycle, "illegal cycle in type declaration")
goto Error
}
check.indexedElts(e.Elts, utyp.elem, -1)
case *Map:
// Prevent crash if the map referred to is not yet set up.
// See analogous comment for *Array.
if utyp.key == nil || utyp.elem == nil {
check.error(e, _InvalidTypeCycle, "illegal cycle in type declaration")
goto Error
}
visited := make(map[interface{}][]Type, len(e.Elts))
for _, e := range e.Elts {
kv, _ := e.(*ast.KeyValueExpr)
if kv == nil {
check.error(e, _MissingLitKey, "missing key in map literal")
continue
}
check.exprWithHint(x, kv.Key, utyp.key)
check.assignment(x, utyp.key, "map literal")
if x.mode == invalid {
continue
}
if x.mode == constant_ {
duplicate := false
// if the key is of interface type, the type is also significant when checking for duplicates
xkey := keyVal(x.val)
if _, ok := utyp.key.Underlying().(*Interface); ok {
for _, vtyp := range visited[xkey] {
if check.identical(vtyp, x.typ) {
duplicate = true
break
}
}
visited[xkey] = append(visited[xkey], x.typ)
} else {
_, duplicate = visited[xkey]
visited[xkey] = nil
}
if duplicate {
check.errorf(x, _DuplicateLitKey, "duplicate key %s in map literal", x.val)
continue
}
}
check.exprWithHint(x, kv.Value, utyp.elem)
check.assignment(x, utyp.elem, "map literal")
}
default:
// when "using" all elements unpack KeyValueExpr
// explicitly because check.use doesn't accept them
for _, e := range e.Elts {
if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
// Ideally, we should also "use" kv.Key but we can't know
// if it's an externally defined struct key or not. Going
// forward anyway can lead to other errors. Give up instead.
e = kv.Value
}
check.use(e)
}
// if utyp is invalid, an error was reported before
if utyp != Typ[Invalid] {
check.errorf(e, _InvalidLit, "invalid composite literal type %s", typ)
goto Error
}
}
x.mode = value
x.typ = typ
case *ast.ParenExpr:
kind := check.rawExpr(x, e.X, nil)
x.expr = e
return kind
case *ast.SelectorExpr:
check.selector(x, e)
case *ast.IndexExpr:
check.expr(x, e.X)
if x.mode == invalid {
check.use(e.Index)
goto Error
}
valid := false
length := int64(-1) // valid if >= 0
switch typ := x.typ.Underlying().(type) {
case *Basic:
if isString(typ) {
valid = true
if x.mode == constant_ {
length = int64(len(constant.StringVal(x.val)))
}
// an indexed string always yields a byte value
// (not a constant) even if the string and the
// index are constant
x.mode = value
x.typ = universeByte // use 'byte' name
}
case *Array:
valid = true
length = typ.len
if x.mode != variable {
x.mode = value
}
x.typ = typ.elem
case *Pointer:
if typ, _ := typ.base.Underlying().(*Array); typ != nil {
valid = true
length = typ.len
x.mode = variable
x.typ = typ.elem
}
case *Slice:
valid = true
x.mode = variable
x.typ = typ.elem
case *Map:
var key operand
check.expr(&key, e.Index)
check.assignment(&key, typ.key, "map index")
// ok to continue even if indexing failed - map element type is known
x.mode = mapindex
x.typ = typ.elem
x.expr = e
return expression
}
if !valid {
check.invalidOp(x, _NonIndexableOperand, "cannot index %s", x)
goto Error
}
if e.Index == nil {
check.invalidAST(e, "missing index for %s", x)
goto Error
}
check.index(e.Index, length)
// ok to continue
case *ast.SliceExpr:
check.expr(x, e.X)
if x.mode == invalid {
check.use(e.Low, e.High, e.Max)
goto Error
}
valid := false
length := int64(-1) // valid if >= 0
switch typ := x.typ.Underlying().(type) {
case *Basic:
if isString(typ) {
if e.Slice3 {
check.invalidOp(x, _InvalidSliceExpr, "3-index slice of string")
goto Error
}
valid = true
if x.mode == constant_ {
length = int64(len(constant.StringVal(x.val)))
}
// spec: "For untyped string operands the result
// is a non-constant value of type string."
if typ.kind == UntypedString {
x.typ = Typ[String]
}
}
case *Array:
valid = true
length = typ.len
if x.mode != variable {
check.invalidOp(x, _NonSliceableOperand, "cannot slice %s (value not addressable)", x)
goto Error
}
x.typ = &Slice{elem: typ.elem}
case *Pointer:
if typ, _ := typ.base.Underlying().(*Array); typ != nil {
valid = true
length = typ.len
x.typ = &Slice{elem: typ.elem}
}
case *Slice:
valid = true
// x.typ doesn't change
}
if !valid {
check.invalidOp(x, _NonSliceableOperand, "cannot slice %s", x)
goto Error
}
x.mode = value
// spec: "Only the first index may be omitted; it defaults to 0."
if e.Slice3 && (e.High == nil || e.Max == nil) {
check.invalidAST(inNode(e, e.Rbrack), "2nd and 3rd index required in 3-index slice")
goto Error
}
// check indices
var ind [3]int64
for i, expr := range []ast.Expr{e.Low, e.High, e.Max} {
x := int64(-1)
switch {
case expr != nil:
// The "capacity" is only known statically for strings, arrays,
// and pointers to arrays, and it is the same as the length for
// those types.
max := int64(-1)
if length >= 0 {
max = length + 1
}
if _, v := check.index(expr, max); v >= 0 {
x = v
}
case i == 0:
// default is 0 for the first index
x = 0
case length >= 0:
// default is length (== capacity) otherwise
x = length
}
ind[i] = x
}
// constant indices must be in range
// (check.index already checks that existing indices >= 0)
L:
for i, x := range ind[:len(ind)-1] {
if x > 0 {
for _, y := range ind[i+1:] {
if y >= 0 && x > y {
check.errorf(inNode(e, e.Rbrack), _SwappedSliceIndices, "swapped slice indices: %d > %d", x, y)
break L // only report one error, ok to continue
}
}
}
}
case *ast.TypeAssertExpr:
check.expr(x, e.X)
if x.mode == invalid {
goto Error
}
xtyp, _ := x.typ.Underlying().(*Interface)
if xtyp == nil {
check.invalidOp(x, _InvalidAssert, "%s is not an interface", x)
goto Error
}
// x.(type) expressions are handled explicitly in type switches
if e.Type == nil {
// Don't use invalidAST because this can occur in the AST produced by
// go/parser.
check.error(e, _BadTypeKeyword, "use of .(type) outside type switch")
goto Error
}
T := check.typ(e.Type)
if T == Typ[Invalid] {
goto Error
}
check.typeAssertion(x, x, xtyp, T)
x.mode = commaok
x.typ = T
case *ast.CallExpr:
return check.call(x, e)
case *ast.StarExpr:
check.exprOrType(x, e.X)
switch x.mode {
case invalid:
goto Error
case typexpr:
x.typ = &Pointer{base: x.typ}
default:
if typ, ok := x.typ.Underlying().(*Pointer); ok {
x.mode = variable
x.typ = typ.base
} else {
check.invalidOp(x, _InvalidIndirection, "cannot indirect %s", x)
goto Error
}
}
case *ast.UnaryExpr:
check.expr(x, e.X)
if x.mode == invalid {
goto Error
}
check.unary(x, e, e.Op)
if x.mode == invalid {
goto Error
}
if e.Op == token.ARROW {
x.expr = e
return statement // receive operations may appear in statement context
}
case *ast.BinaryExpr:
check.binary(x, e, e.X, e.Y, e.Op, e.OpPos)
if x.mode == invalid {
goto Error
}
case *ast.KeyValueExpr:
// key:value expressions are handled in composite literals
check.invalidAST(e, "no key:value expected")
goto Error
case *ast.ArrayType, *ast.StructType, *ast.FuncType,
*ast.InterfaceType, *ast.MapType, *ast.ChanType:
x.mode = typexpr
x.typ = check.typ(e)
// Note: rawExpr (caller of exprInternal) will call check.recordTypeAndValue
// even though check.typ has already called it. This is fine as both
// times the same expression and type are recorded. It is also not a
// performance issue because we only reach here for composite literal
// types, which are comparatively rare.
default:
panic(fmt.Sprintf("%s: unknown expression type %T", check.fset.Position(e.Pos()), e))
}
// everything went well
x.expr = e
return expression
Error:
x.mode = invalid
x.expr = e
return statement // avoid follow-up errors
}
When to Use goto in Go
Using the experience we got from the standard library we can see that usage sometimes makes sense. And considering that Go only allows labels within the same function I think it does not make code hard to follow.
The occasions where I would use it is when the regular flow control mechanisms do not cut it without becoming complex, here are a few bullet points of cases.
Avoiding conditional flags for storing states and jumping over code blocks like in the go/scanner
Standardize an exit plan, in cases where you have a switch or many for loops that all can cause the same standard exit like in go/types/expr
Complex logic that would require nested for loops can be made a lot easier the read and understand by avoiding nested loops by using goto
Avoid duplicate code by handling common pattern in a label as in the Error label in go/types/expr
What is your opinion about the goto statement? Do you think it increases complexity and confusion, or do you think it is a good solution to handle the flow?
This is it for this article, thank you for reading. And as always I appreciate feedback, or just reach out if you have questions. You find me here on any of my social medias linked below.
If you like my writing feel free to follow me for more. I recently wrote about structured logs in Go. How To Use Structured JSON Logging in Golang Applications
If you enjoyed my writing, please support future articles by buying me an Coffee