Sometimes we need to write a function capable of dealing uniformly with values of types that don’t satisfy a common interface, don’t have a known representation, or don’t exist at the time we design the function—or even all three.
A familiar example is the formatting
logic within fmt.Fprintf
, which can usefully print an arbitrary
value of any type, even a user-defined one. Let’s try to implement a
function like it using what we know already.
For simplicity, our function will accept one argument and will return
the result as a string like fmt.Sprint
does, so we’ll call it
Sprint
.
We start with a type switch that tests whether the argument defines
a String
method, and call it if so.
We then add switch cases that test the value’s dynamic type against
each of the basic types—string
, int
, bool
, and
so on—and perform the appropriate formatting operation in each case.
func Sprint(x interface{}) string { type stringer interface { String() string } switch x := x.(type) { case stringer: return x.String() case string: return x case int: return strconv.Itoa(x) // ...similar cases for int16, uint32, and so on... case bool: if x { return "true" } return "false" default: // array, chan, func, map, pointer, slice, struct return "???" } }
But how do we deal with other types, like []float64
,
map[string][]string
, and so on?
We could add more cases, but the number of such types is infinite.
And what about named types, like url.Values
?
Even if the type switch had a case for its underlying type
map[string][]string
, it wouldn’t match url.Values
because the two types are not identical, and the type switch cannot
include a case for each type like url.Values
because that would
require this library to depend upon its clients.
Without a way to inspect the representation of values of unknown types, we quickly get stuck. What we need is reflection.