Back

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
Photo by [Jeswin Thomas] on Unsplash

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")
}
main.go — Breaking a for loop by using the goto statement. Try it at Playground

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
	}
}
Trying to goto a label outside of scope will crash Try it at Playground

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")
}
Make sure to not fall into the multi label execution trap Try it at Playground

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)
}
go/scanner.scanComments — A standard library example of using goto to skip state flags

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
}
go/types/expr.go — Example of standardized error handling using goto

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

Sign up for my Awesome newsletter