Saltar a contenido

Week 7 - Interface Values, itabs, and Dispatch Cost

7.1 Conceptual Core

  • A Go interface value is a two-word header:
  • For non-empty interfaces (io.Reader, error, etc.): (itab, data).
  • For empty interfaces (any/interface{}): (type, data).
  • itab = interface table = the dynamic dispatch vtable for one (interface, concrete type) pair. Holds the type pointer, the interface type pointer, a hash, and an array of function pointers (the methods).
  • data is a pointer to the concrete value, or the value itself if it fits in a word (small integer types, etc., as an optimization in some Go versions). Modern Go (>=1.4) always uses an indirect-be careful with old assumptions.

7.2 Mechanical Detail

  • Read src/runtime/iface.go. Key functions: getitab, convT2I, assertI2I, assertE2I. The getitab cache is keyed by `(interface_type, concrete_type) - first call may allocate the itab; subsequent calls hit the cache.
  • Cost of an interface call:
  • Load itab from interface header (cache hit).
  • Load function pointer from itab.fun[N].
  • Indirect call. Plus: the call cannot be inlined. So an interface call is 1 indirect call + lost inlining opportunities. On hot paths, this matters.
  • Boxing cost: assigning a non-pointer concrete value to any allocates if the value is larger than a word. var x any = 42 may allocate (depends on Go version's int boxing optimization); var x any = SomeBigStruct{} definitely allocates.
  • Type assertions:
  • v.(T) panics on failure; allocates an itab if T is an interface.
  • v, ok := v.(T) is the same but no panic.
  • switch v := v.(type) is the same machinery, optimized for multiple cases.
  • Type switches are faster than chains of type assertions because the compiler may emit a hashed dispatch table.
  • Generics vs interfaces: generics (since 1.18) compile to a single generic body parameterized by the GC shape ("GCShape stenciling"), with a per-shape dictionary. Generics are not specialized like Rust monomorphization-there is still indirection for method calls on type parameters. The performance vs interface tradeoff is subtle and workload-dependent. Read compiler/internal/types2/ and Russ Cox's GCShape blog post.

7.3 Lab-"Interface Bench"

  1. Build a tight loop calling a method via three paths: concrete type, interface, generic type parameter. Benchmark with - benchmem`.
  2. Inspect the disassembly with go tool objdump -s 'main\.benchInterface'. Identify the indirect call.
  3. Refactor a real-world pattern (a Logger interface used 10× in a hot path) into a concrete type or a type-parameterized version. Measure the win or non-win.
  4. Build a worst-case allocation example: passing a stack int into fmt.Println(...). Show with - gcflags=-mthat the int escapes (boxing intoany). Replace withfmt.Println(strconv.Itoa(x))` and re-measure.

7.4 Idiomatic & golangci-lint Drill

  • gocritic: typeAssertChain, gosimple S1034 (omit comma-ok in type-switch). Re-read the Go FAQ on "Why no implicit type conversions?"-the answer informs API design.

7.5 Production Hardening Slice

  • Add a benchmark to CI that asserts 0 allocs/op on critical paths (e.g., the request-handling hot path of your service template). Use testing.B.ReportAllocs() and a script that diffs allocs/op against a committed baseline. Any PR that introduces an allocation on a 0-alloc path fails CI.

Comments