Reflection

Reflection and a Tiny Config Decoder

My programs often begin with three configuration fields and end with a parser that believes it is a database. I tried using reflection to make one small key-value decoder reusable without building another private language.

The input is deliberately boring:

host=buildbox
workers=4
verbose=true

The destination is a structure. The decoder walks exported fields with the early reflect package, matches lowercased field names, converts the text according to each field’s kind, and sets the value. The exact calls have changed across weekly snapshots, so the important part is the control flow rather than names copied from my May 2011 tree:

parse lines
  -> find structure field
  -> inspect field kind
  -> convert text
  -> set addressable field or return os.Error

Passing a pointer is required because the decoder must modify the original structure. It checks that the argument points to a structure before examining fields. A non-pointer, unknown key, unsupported kind, or failed conversion returns an ordinary failure value with the key name.

I initially ignored unknown keys for forward compatibility. That converted a misspelled workres setting into a silent default of zero workers. Strict input is friendlier here: configuration is written by a person who would rather receive a precise complaint now than investigate strange behavior later.

The decoder supports strings, booleans, and signed integers. That is all this program needs. Slices, nested structures, aliases, default tags, and custom conversion hooks can wait until an actual configuration requires them. Reflection makes adding features easy in the same way an empty notebook makes writing a trilogy easy.

There is runtime cost, but startup configuration is not a hot path. The larger cost is weaker static checking inside the decoder, which I address with table tests over valid and invalid input. The application still uses an ordinary typed structure after startup.

I would not use this mechanism for a protocol or large data stream. Gob already solves typed Go-to-Go encoding, and explicit parsing is clearer for a public format. For a dozen local settings, a constrained reflective decoder removes repetitive assignment while keeping failures visible. The constraint is the feature, not an embarrassing first release.

Reflection With the Lights On

I wanted a diagnostic printer for configuration structures and reached for reflection. That is normally the moment a small helper begins applying for framework status, so I kept the experiment deliberately narrow.

An interface value carries a dynamic type and value. The reflect package exposes descriptions of those two parts. In my weekly snapshot the entry points and names differ from examples written against other snapshots, so I will not pretend this code is timeless:

v := reflect.NewValue(x)
t := reflect.Typeof(x)
fmt.Println(t.String(), v.Kind())

From there I can switch on kind, inspect structure fields, and recursively print supported values. The mechanism is not source-code introspection. It is runtime examination of type information retained for a value.

The first rule I learned is to separate validity, kind, and concrete type. A pointer is not its element. An interface can contain a pointer. A nil interface and an interface containing a nil pointer are different states. Reflection faithfully presents these distinctions, even when I would prefer it to edit my mistake.

The second rule is that settable values are special. Inspecting a copy obtained from an interface does not grant permission to modify the original. To change a caller’s value, reflection needs an addressable value, usually reached through a pointer and then its element. This is good friction. Generic mutation should look dangerous because it is.

I ended with a read-only printer supporting strings, integers, slices, and exported structure fields. Unsupported kinds produce an explicit marker. I did not add automatic conversion, tag syntax, or clever cycle handling because the tool does not need them.

Reflection trades compiler knowledge for runtime decisions. Misspelled field assumptions become execution-path problems, and every branch is harder to read than direct code. It is justified when the types truly vary, as in encoders and diagnostics. It is not justified merely to avoid writing three straightforward assignments.

My opinion after using it is positive but cautious. Go’s reflection model follows the interface representation and type system rather than inventing a parallel object universe. Still, if ordinary interfaces can express the job, they are clearer and checked earlier. Reflection is a sharp tool. Keeping the lights on means seeing both the blade and the fingers.