The Reflect Package | Internals for Internsπ<br>Understanding the Go Runtime (10 of 10)<br>βΌ1.<br>The Bootstrap<br>2.<br>The Memory Allocator<br>3.<br>The Scheduler<br>4.<br>The Garbage Collector<br>5.<br>The System Monitor<br>6.<br>The Network Poller<br>7.<br>Slices, Maps, and Channels<br>8.<br>The select Statement<br>9.<br>Stacktraces<br>10.<br>The Reflect Package<br>You are here
In the previous article<br>we watched the runtime rebuild an entire stack trace out of metadata the compiler and linker had frozen into the binary at build time. I told you at the end that reflect works on exactly the same trick β metadata baked into the binary, only pointed at your data instead of your call stack. Today we’re going to cash that promise in.<br>Let’s start with a program that, the first time you see it, feels like it shouldn’t be possible:<br>package main
import (<br>"fmt"<br>"reflect"
type User struct {<br>Name string `json:"name"`<br>Email string `json:"email,omitempty"`<br>age int
func main() {<br>t := reflect.TypeOf(User{})<br>for i := 0; i t.NumField(); i++ {<br>f := t.Field(i)<br>fmt.Printf("%-6s %-8s %q\n", f.Name, f.Type, f.Tag)
Run it and out comes:<br>Name string "json:\"name\""<br>Email string "json:\"email,omitempty\""<br>age int ""<br>Field names. Field types. Even the struct tags, character for character. At runtime.<br>Now sit with how strange that is. Go is statically typed and compiled to native machine code . There’s no virtual machine keeping class objects around, no interpreter that still has your source code in hand. By the time this program runs, User was supposed to be gone β boiled down to 40 bytes sitting in your memory and nothing more. The names Name, Email, and age are things you wrote for humans; the CPU never needed them. So where is this information coming from?<br>That’s the whole question for today, and the answer has a satisfying shape: the reflect package doesn’t compute any of this. It just reads it. The real work happened weeks ago, on your machine, when you typed go build.<br>A note on scope : This article focuses on the reading side of reflect β inspecting types and values. The package can also build things at runtime β new structs, function values, slices, and so on β and that’s a whole story of its own that I’ll probably come back to in a future article.
But a claim like that deserves a proper trace through the source β so let’s start at the surface and dig down.<br>The Doorway: TypeOf and ValueOf<br>Most of our exploration starts at one of two doors: reflect.TypeOf<br>gives you a reflect.Type to ask questions about a type, and reflect.ValueOf<br>gives you a reflect.Value to inspect (and sometimes modify) an actual value. Both take an any as their argument. You’d expect the entry points of something as powerful as reflection to be where the magic happens β so let’s look at TypeOf:<br>func TypeOf(i any) Type {<br>return toType(abi.TypeOf(i))
Hmm, one level down then. Here’s abi.TypeOf<br>func TypeOf(a any) *Type {<br>eface := *(*EmptyInterface)(unsafe.Pointer(&a))<br>return (*Type)(NoEscape(unsafe.Pointer(eface.Type)))
And that’s it. No table lookup, no runtime call, no allocation. Take the address of the any parameter, reinterpret the memory sitting there as a struct called EmptyInterface, and return its first field. reflect.TypeOf is, at bottom, one pointer load .<br>Which can only mean one thing: the complete description of your type was already there, inside the any, before reflect ever got involved. The interesting part isn’t the function β it’s what an any actually is.<br>Inside an Interface<br>Here’s what that EmptyInterface struct looks like (src/internal/abi/iface.go<br>):<br>type EmptyInterface struct {<br>Type *Type<br>Data unsafe.Pointer
Every any in your program β every empty interface value β is exactly this: two pointers . The second one points at the data. The first one points at a type descriptor : a struct that fully describes the dynamic type of whatever was stored in the interface (we’ll get deeper into it in a moment). The runtime has its own mirror of this layout, called eface<br>So when you pass user to a function that takes an any β which is exactly what calling TypeOf is:<br>func TypeOf(i any) Type // the signature we just saw
TypeOf(user) // user becomes an any right here
the compiler emits code at the call site that stores two pointers: the address of User’s type descriptor, and a pointer to the value. That’s essentially the whole conversion β the data pointer just points at the value sitting somewhere in memory (on the stack or the heap), but the type pointer involves no lookup and no registration: it’s a constant address the compiler knew at compile time.<br>This reframes our two entry points completely. TypeOf and ValueOf don’t inspect your value at all. They read the interface header β the two pointers the compiler already put there.<br>But that just pushes the question one step back. The type pointer leads to a descriptor that knows field names and struct tags. Who built that descriptor,...