How does Go's linker decide which methods and itabs it can drop from the final binary?
answer
- the linker keeps what it can reach
- an interface call names no function
- boxed types crossed with used method names
- a name read at run time is unknowable
- when in doubt, it keeps everything exported
basics
~20 sThe linker keeps what it can reach. A method survives if it is called directly, or if its name and signature match a method of an interface that reachable code uses and its type is converted to an interface. Reflective lookup by method name breaks that reasoning and forces it to keep far more.
solid answer
~50 sGo's linker does reachability-based dead-code elimination, and methods are the hard case because a method can be invoked through an interface without any direct call site. So the linker tracks two sets: the types that reachable code converts into interfaces, and the method names and signatures that reachable interface types actually use. A method is kept when it is called directly, or when it belongs to a boxed type and matches a used interface method; the method table for a (type, interface) pair is kept only if that pair is reachable. Everything else — the code, the entry in the type's method list, and the name string — goes. The reasoning collapses when reflective lookup by method name is reachable, because the linker cannot know which names might be passed as strings; it then conservatively retains exported methods of the types reachable through reflection, along with their metadata.
code
go · 7 linesfunc invoke(target any, method string, args []reflect.Value) []reflect.Value {
m := reflect.ValueOf(target).MethodByName(method)
if !m.IsValid() {
return nil
}
return m.Call(args)
}go deeper
Know that the Go linker removes code nothing can reach, and that dispatching through an interface or by a method name given as a string makes that decision harder for it.
Be ready to state the rule: a method survives if it is called directly, or if its type is converted to an interface and some reachable interface uses a method of that name and signature.
Explain the cascade — retained methods keep their callees reachable — and describe how you would restructure a name-keyed registry so the linker can prune again.
Own the guidance: decide where in the codebase dynamic lookup is permitted, given that it silently converts a compile-time guarantee into an unbounded retention obligation.
## Why methods are the hard case Dead-code elimination in a linker is normally a graph walk: start at the entry point, follow calls, keep what you touch. Direct calls make this easy. Interface dispatch does not: `w.Write(b)` names no concrete function, and the actual target is decided at run time by the method table sitting inside the interface value. If the linker dropped a method just because nothing calls it by name, an interface call could land on a hole. ## The reachability rule The linker solves this with a two-sided condition, and it is worth stating precisely because it is what makes Go binaries as small as they are: 1. Track which **types get converted to an interface** anywhere in reachable code. Only such types can ever be the dynamic type behind an interface call. 2. Track which **method names and signatures are used by reachable interface types** — that is, the method sets of the interfaces the program actually dispatches through. A method is marked live when it is called directly, or when its receiver type is in set (1) and its name and signature are in set (2). Everything else is unreachable and is deleted, along with the entry that would name it in the type's metadata. The method tables built for a (concrete type, interface) pair follow the same logic. Such a table is generated where the conversion appears in the source, and the linker keeps it only when that conversion is itself reachable. Interfaces you declared but never dispatch through, and conversions in dead branches, cost nothing in the final file. The rule is deliberately conservative in one direction: it is name-and-signature based, not a full call-graph proof. If your type has a method `Close()` and *any* reachable interface in the program has a `Close()` method, your type's `Close` is kept once the type is boxed — even if nobody ever calls it through that particular interface. This is a sound over-approximation, and in practice a cheap one. ## What breaks it The rule depends on the linker being able to enumerate the method names the program can ask for. Reflective lookup of a method **by name string** removes that ability: the name may be read from a configuration file, a network message, or a registry keyed by type name, none of which the linker can see. Faced with that, it stops pruning: once the reflection machinery that looks methods up by name is reachable, the linker retains the exported methods of the types reachable through reflection, together with their code, their signatures and their name strings. The cost is not one method. It is a cascade: retained method bodies are reachable code, so whatever *they* call is now reachable too, and so on. A single registry that dispatches by name can pull a surprising amount of a program back into the binary. The same is true of anything that reaches reflection indirectly. A package that marshals arbitrary values, or a logging call that formats a struct, brings the reflection machinery into the reachable set even when your own code never mentions it — which is one reason the effect is often noticed only when the artifact is measured, not when the offending line is written. ## What this means when you write code - Adding an exported method to a type that nothing boxes into an interface costs nothing in the binary. The linker drops it. - Deleting an unused method rarely shrinks anything for the same reason. - Replacing string-keyed dynamic dispatch with an explicit table — a map from a name to a function value, populated by the code that owns each type — keeps everything statically reachable, so the linker's ordinary rule applies again and the compiler checks the wiring. - The effect is measurable rather than theoretical: build the artifact both ways and compare. ## What interviewers are checking That you understand dead-code elimination is a *reachability* argument, that you can explain why interface dispatch complicates it and how the name-and-signature rule resolves that, and that you can state precisely what a name-based lookup takes away from the linker — the ability to enumerate which methods can be asked for.
- Does deleting an exported method that nobody calls shrink the binary?Usually not. If the method was never called directly and never matched a used interface method on a boxed type, the linker had already dropped it. The saving only appears when the method was actually being retained.
- How would you get dynamic dispatch by name without giving up dead-code elimination?Register explicitly: each package adds its own entry to a map from a string key to a function or constructor value. The references are ordinary static ones, so the linker's normal reachability rule still applies and the compiler type-checks the wiring.
- Why can a method be kept even though no code calls it through the interface it matches?Because the rule matches on name and signature, not on a proven call path. If the receiver type is boxed anywhere and some reachable interface declares a method with that name and signature, the method is retained as a sound over-approximation.
The linker is packing for a trip it has planned in advance. Tell it the itinerary and it packs light; tell it someone may phone later and name any item, and it packs the whole wardrobe.
saying these in an interview costs you the question
- Says exported identifiers are always kept in the binary
- Thinks the linker tracks which method-name strings appear as literals
- Claims interface dispatch is resolved entirely at compile time
- Believes unused interfaces still cost space in the artifact
- Assumes reflection only affects run-time speed, not binary size