What is an itab, when is it built, and what does a method call through an interface cost?
An itab pairs one (interface type, concrete type) combination with the method pointers the concrete type provides for that interface. When the compiler can see a conversion statically (var r io.Reader = f where f is *os.File), it emits the itab into the binary as read-only data. When the pairing is only known at runtime (a type assertion or switch to an interface, such as x.(io.Writer)), the runtime calls getitab. That function builds the itab by matching the sorted method lists (roughly O(m+n)) and caches it in a global hash table, so the next lookup is cheap. Since Go 1.22, type switches and assertions to interfaces also get per-call-site caches.
A method call through an interface loads tab.fun[i] and makes an indirect call with data as the receiver. The indirection itself costs little. The real cost is that the compiler usually cannot inline the callee, and escape analysis often gives up, so arguments move to the heap.
// Assertion to a concrete type: one pointer comparison (tab._type == &type.T)
f, ok := r.(*os.File)
// Assertion to an interface: itab lookup (cached)
w, ok := r.(io.Writer)
Profile-guided optimization (PGO, Go 1.21+) can devirtualize hot interface calls: it adds a guarded direct call to the most common concrete type, which can then be inlined.
More on Interfaces, Methods & Embedding
- Q77How is an interface value represented at runtime? What is the difference between
ifaceandeface? - Q79What does this print? (nil interface vs interface holding a nil pointer)
- Q80Why does this function return a non-nil error even when nothing failed? How do you fix it?
- Q81Explain method sets. Why does
*Tsatisfy an interface whenTdoes not? - Q82Why doesn't
m["k"].Inc()compile whenInchas a pointer receiver? What other values are not addressable? - Q83How do you decide between value and pointer receivers?