How does Go calculate len()..?

The impetus for this post was a question on the Gophers Slack a while back. A fellow developer wanted to know where to find more information on len.

I want to know how the len func gets called.

People chimed in quickly with a correct answer

It doesn’t. Len is compiler magic, not an actual function call.

… all the types len works on have the same header format, the compiler just treats the object like a header and returns the integer representing the length of elements

And while those answers are technically true, I thought it would be nice to unfurl the layers that make up this ‘magic’ in a concise explanation! It was also a nice little exercise into getting more insight about the inner workings of the Go compiler.

FYI, all links in this post point to the soon-to-be-released Go 1.17 branch.

A small interlude

Some background information which may be helpful in understanding the rest of this post.

The Go compiler consists of four main phases. You can start reading about them here. The first two are generally referred to as the compiler ‘frontend’, while the latter two are also called the compiler ‘backend’.

  • Parsing; the source files are tokenized, parsed, and a syntax tree is constructed for each source file.
  • AST transformations and type-checking; the syntax tree is converted to the compiler’s AST representation and the AST tree is type-checked.
  • Generic SSA; the AST tree is converted into Static Single Assignment (SSA) form, a lower-level intermediate representation where optimizations can be implemented.
  • Generating machine code; the SSA goes through another machine-specific optimization process, and is afterwards passed to the assembler to be translated to machine code and written out to the final binary.

Let’s dive back in!

The entrypoint

The entrypoint of the Go compiler is (unsurprisingly) the main() function in the compile/internal/gc package.

As the docstring suggests, this function is responsible for parsing Go source files, type-checking the parsed Go package, compiling everything to machine code and writing the compiled package definition.

One of the things that takes place early on is typecheck.InitUniverse() which defines the basic types, built-in functions and operands.

There, we see how all built-in functions are matched to an ‘operation’ and we can use ir.OLEN to trace the steps of a call to len.

var builtinFuncs = [...]struct {
	name string
	op   ir.Op
}{
	{"append", ir.OAPPEND},
	{"cap", ir.OCAP},
	{"close", ir.OCLOSE},
	{"complex", ir.OCOMPLEX},
	{"copy", ir.OCOPY},
	{"delete", ir.ODELETE},
	{"imag", ir.OIMAG},
	{"len", ir.OLEN},
	{"make", ir.OMAKE},
	{"new", ir.ONEW},
	{"panic", ir.OPANIC},
	{"print", ir.OPRINT},
	{"println", ir.OPRINTN},
	{"real", ir.OREAL},
	{"recover", ir.ORECOVER},
}

Later on in InitUniverse, one can see the initialization of the okfor arrays, which define the valid types for various operands; for example which types should be allowed for the + operator.

	if types.IsInt[et] || et == types.TIDEAL {
		...
		okforadd[et] = true
		...
	}
	if types.IsFloat[et] {
		...
		okforadd[et] = true
		...
		}
	if types.IsComplex[et] {
		...
		okforadd[et] = true
		...
	}

In the same way, we can see all types which will be valid inputs for len()

	okforlen[types.TARRAY] = true
	okforlen[types.TCHAN] = true
	okforlen[types.TMAP] = true
	okforlen[types.TSLICE] = true
	okforlen[types.TSTRING] = true

The compiler ‘frontend’

Moving on to the next major steps in the compilation process, we reach the point where the input is parsed and typechecked starting with noder.LoadPackage(flag.Args()).

A few levels deeper we can see each file being parsed individually and then type-checked in five distinct phases.

Phase 1: const, type, and names and types of funcs.
Phase 2: Variable assignments, interface assignments, alias declarations.
Phase 3: Type check function bodies.
Phase 4: Check external declarations.
Phase 5: Verify map keys, unused dot imports.

Once the len statement is encountered in the last type-checking phase, it’s transformed to a UnaryExpr, as it won’t actually end up being a function call.

The compiler implicitly takes the argument’s address and uses the okforlen array to verify the argument’s legality or emit a relevant error message.

// typecheck1 should ONLY be called from typecheck.
func typecheck1(n ir.Node, top int) ir.Node {
	if n, ok := n.(*ir.Name); ok {
		typecheckdef(n)
	}

	switch n.Op() {
	...
	case ir.OCAP, ir.OLEN:
		n := n.(*ir.UnaryExpr)
		return tcLenCap(n)
	}
}

// tcLenCap typechecks an OLEN or OCAP node.
func tcLenCap(n *ir.UnaryExpr) ir.Node {
	n.X = Expr(n.X)
	n.X = DefaultLit(n.X, nil)
	n.X = implicitstar(n.X)
	...
	var ok bool
	if n.Op() == ir.OLEN {
		ok = okforlen[t.Kind()]
	} else {
		ok = okforcap[t.Kind()]
	}
	if !ok {
		base.Errorf("invalid argument %L for %v", l, n.Op())
		n.SetType(nil)
		return n
	}

	n.SetType(types.Types[types.TINT])
	return n
}

Back to the main compiler flow and after everything is type-checked, all functions are enqueued to be compiled.

In compileFunctions() each element in the queue is passed through ssagen.Compile

	compile = func(fns []*ir.Func) {
		wg.Add(len(fns))
		for _, fn := range fns {
			fn := fn
			queue(func(worker int) {
				ssagen.Compile(fn, worker)
				compile(fn.Closures)
				wg.Done()
			})
		}
	}
	...
	compile(compilequeue)

where a few layers deep, after buildssa and genssa and we finally get to convert the len expression in the AST tree to SSA.

At this point it’s easy to see how each one of the available types is handled!

// expr converts the expression n to ssa, adds it to s and returns the ssa result.
func (s *state) expr(n ir.Node) *ssa.Value {
	...
	switch n.Op() {
	case ir.OLEN, ir.OCAP:
		n := n.(*ir.UnaryExpr)
		switch {
		case n.X.Type().IsSlice():
			op := ssa.OpSliceLen
			if n.Op() == ir.OCAP {
				op = ssa.OpSliceCap
			}
			return s.newValue1(op, types.Types[types.TINT], s.expr(n.X))
		case n.X.Type().IsString(): // string; not reachable for OCAP
			return s.newValue1(ssa.OpStringLen, types.Types[types.TINT], s.expr(n.X))
		case n.X.Type().IsMap(), n.X.Type().IsChan():
			return s.referenceTypeBuiltin(n, s.expr(n.X))
		default: // array
			return s.constInt(types.Types[types.TINT], n.X.Type().NumElem())
		}
		...
	}
	...
}

Arrays

For arrays we just return an constant integer based on the input array’s NumElem() method which just accesses the Bound field of the input array.

// Array contains Type fields specific to array types.
type Array struct {
	Elem  *Type // element type
	Bound int64 // number of elements; <0 if unknown yet
}

func (t *Type) NumElem() int64 {
	t.wantEtype(TARRAY)
	return t.Extra.(*Array).Bound
}

Slices, Strings

For slices and strings, we have to take a peek at how ssa.OpSliceLen and ssa.OpStringLen are handled.

When either of those calls are lowered in the Late Expansion stage and the rewriteSelect method, slices and strings are recursively walked to find out their size using pointer arithmetic like offset+x.ptrSize

func (x *expandState) rewriteSelect(leaf *Value, selector *Value, offset int64, regOffset Abi1RO) []*LocalSlot {
	switch selector.Op {
	...
	case OpStringLen, OpSliceLen:
		ls := x.rewriteSelect(leaf, selector.Args[0], offset+x.ptrSize, regOffset+RO_slice_len)
		locs = x.splitSlots(ls, ".len", x.ptrSize, leafType)
	...
	}
	return locs

Maps, Channels

Finally for maps and channels we use the referenceTypeBuiltin helper. Its inner workings are a little magical, but what it ultimately does is take the address of the map/chan argument and reference its struct layout with zero offset, much like unsafe.Pointer(uintptr(unsafe.Pointer(s))) which ends up returning the value of first struct field.

// referenceTypeBuiltin generates code for the len/cap builtins for maps and channels.
func (s *state) referenceTypeBuiltin(n *ir.UnaryExpr, x *ssa.Value) *ssa.Value {
	if !n.X.Type().IsMap() && !n.X.Type().IsChan() {
		s.Fatalf("node must be a map or a channel")
	}
	// if n == nil {
	//   return 0
	// } else {
	//   // len
	//   return *((*int)n)
	//   // cap
	//   return *(((*int)n)+1)
	// }
	lenType := n.Type()
	nilValue := s.constNil(types.Types[types.TUINTPTR])
	cmp := s.newValue2(ssa.OpEqPtr, types.Types[types.TBOOL], x, nilValue)
	b := s.endBlock()
	b.Kind = ssa.BlockIf
	b.SetControl(cmp)
	b.Likely = ssa.BranchUnlikely

	bThen := s.f.NewBlock(ssa.BlockPlain)
	bElse := s.f.NewBlock(ssa.BlockPlain)
	bAfter := s.f.NewBlock(ssa.BlockPlain)

	...
	switch n.Op() {
	case ir.OLEN:
		// length is stored in the first word for map/chan
		s.vars[n] = s.load(lenType, x)
	...
	return s.variable(n, lenType)
}

The definitions of the hmap and hchan structs show that their first fields do indeed contain what we need for len i.e. the live map cells and channel queue data respectively.

type hmap struct {
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8
	noverflow uint16
	hash0     uint32

	buckets    unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate  uintptr

	extra *mapextra
}

type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint
	buf      unsafe.Pointer
	elemsize uint16
	closed   uint32
	elemtype *_type
	sendx    uint
	recvx    uint
	recvq    waitq
	sendq    waitq

	lock mutex
}

Parting words

Aaaand that’s all! This post wasn’t as lengthy as I thought it would be; I just hope it was interesting to you as well.

I’ve got little experience with the inner workings of the Go compiler, so some things may be amiss. Also, many things are subject to change in the very near future, especially with generics and the new type system coming in the next couple of Go versions, but at least I hope I’ve provided a way that you can use to start digging around for yourself.

In any case, please don’t hesitate to reach out for comments, suggestions, ideas for new posts or simply talk about Go!

Until next time, bye!

Written on July 31, 2021