map[string]interface{} in Go
What is a map[string]interface{}
in Go, and why is it so
useful? How do we deal with maps of string
to
interface{}
in our programs? What the heck is an
interface{}
, anyway? Let’s find out.
In my book For the Love of Go, I’ll
introduce you to the Go language and its fundamental data types,
including maps and strings. A Go map lets us store and retrieve data
efficiently by key, and those keys are often strings. A map
with string
keys means we can address any piece of data by
name, which is very convenient.
But what kind of data? Well, any kind! The Go type that expresses
this idea is map[string]any
, meaning a map that relates
string keys to data of any type.
The any
here is a shorthand for
interface{}
, otherwise known as the empty
interface. You’ll see the latter form in older code, but we’ll use
any
in this tutorial; it’s just shorter.
Golang ‘map string interface’ example
Following our diner theme for these tutorials, or perhaps channeling
Ron Swanson,
here’s an example of a map[string]interface{}
literal:
:= map[string]any{
foods "bacon": "delicious",
"eggs": struct {
string
source float64
price }{"chicken", 1.75},
"steak": true,
}
What is a
map[string]any
?
If you’ve read the earlier tutorial in this series on map types, you’ll know how to read this code
right away. The type of the foods
variable in the above
example is a map where the keys are strings, and the values are of type
any
.
So what’s that? Go interfaces are worthy of a tutorial series in themselves, though it’s one of those topics that seems a lot more complicated than it actually is; it’s just a little unfamiliar to most of us at first.
Suffice it to say here that an interface is a way of referring to a
value without specifying its type. Instead, the interface specifies what
methods it has; for example, the widely-used
io.Reader
interface type tells you that a value of that
type has a Read()
method with a certain signature.
We know that any
is a shorthand for
interface{}
, but what is that? Pronounced ‘empty
interface’, it’s the interface that specifies no methods at all! Note
that this doesn’t mean that interface{}
values must
have no methods; it simply doesn’t say anything at all about
what methods they may or may not have. In the words of a Go proverb, interface{}
says
nothing.
map[string]any
in Go
So what data type would satisfy the empty interface? Well, any.
Because interface{}
puts no constraints at all on the
values it accepts, any type is okay. That’s why Go recently added the
predeclared identifier any
, as a synonym for
interface{}
.
When you need to store a collection of arbitrary values of any type,
then, identified by strings, a map[string]interface{}
or
map[string]any
is the ideal choice.
Why is interface{}
so
useful?
What’s the point of interface{}
, then, if it doesn’t
tell us anything about the value? Well, that’s precisely why it’s
useful: it can refer to anything! The type interface{}
(or,
as we’d now say, any
) applies to any value.
A variable declared as interface{}
can hold a string
value, an integer, any kind of struct, a pointer to an
os.File
, or indeed anything you can think of.
Suppose we need to write a function that prints out the value passed to it, but we don’t know in advance what type this value would be. This is a job for the empty interface:
func printAnything(v any)
Indeed, fmt.Println
is defined in a very similar way,
for exactly this reason:
func Println(a ...any) { ... }
map[string]any
and
arbitrary data
Similarly, if we want a collection of different kinds of
thing, each one identified by a string, which is a convenient way
to organise arbitrary data, we can do that with a
map[string]interface{}
. In fact, we just described the
schema of JSON objects, for example. Take this raw JSON data:
{
"name":"John",
"age":29,
"hobbies":[
"martial arts",
"breakfast foods",
"piano"
]
}
Overlooking the obviously fictitious age for the moment, we can see that this is a collection of things identified by string keys, but what kind of things? We have a string, an integer, and an array of strings.
Supposing we needed to translate this into a Go struct value, we could define a type like this:
type Person struct {
string
Name int
Age []string
Hobbies }
Great. But this requires that we know the schema of the object in advance. What if someone gives us arbitrary JSON data, and we need to unmarshal it into a Go value? How can we possibly do that, given that all we know is that it’s a map of strings to objects of any type?
Decoding JSON data to
map[string]any
Suppose that we have the biographically questionable JSON data about
me stored in a variable called data
. How can we unmarshal
this into a Go variable so that we can start looking at it? What type
would that variable need to be?
:= map[string]any{}
p := json.Unmarshal(data, &p)
err // check error
Provided there are no errors, the p
variable now
contains our arbitrary data. Success! But, given that we know nothing at
all about the type of each value in the map, what can we usefully do
with it?
Using map[string]any
data
One thing we can do is use a type switch to do different things depending on the type of the value. Here’s an example:
for k, v := range p {
switch c := v.(type) {
case string:
.Printf("Item %q is a string, containing %q\n", k, c)
fmtcase float64:
.Printf("Looks like item %q is a number, specifically %f\n", k, c)
fmtdefault:
.Printf("Not sure what type item %q is, but I think it might be %T\n", k, c)
fmt}
}
The special syntax switch c := v.(type)
tells us that
this is a type switch, meaning that Go will try to match the type of
v
to each case in the switch statement. For example, the
first case will be executed if v
is a string:
Item "name" is a string, containing "John"
In each case, the variable c
receives the value of
v
, but converted to the relevant type. So in the
string
case, c
will be of type
string
.
The float64
case will match when v
is a
float64
:
Looks like item "age" is a number, specifically 29.000000
You might be puzzled that the whole-number value 29
was
unmarshaled into a float64
, but that’s normal. All JSON
numbers are treated as float64
by
json.Unmarshal
. It’s the most general of Go’s numeric
types.
Finally, if no other case matches, the default
case is
activated:
Not sure what type item "hobbies" is, but I think it might be []interface {}
The format specifier %T
to fmt.Printf
prints the type of its value, which is sometimes handy. In this
case we can see that the value of "hobbies"
is a slice of
arbitrary data, which makes sense.
When to use
map[string]any
As we’ve seen, the “map of string to empty interface” type is very useful when we need to deal with data that comes from outside the Go world; for example, arbitrary JSON data of unknown schema. Many web APIs return data like this, for example.
It’s also extremely common when writing Terraform providers, which
makes sense; Terraform resources are also essentially maps of strings to
arbitrary data. It’s recursive, too; the ‘arbitrary data’ is also often
a map of strings to more arbitrary data. It’s
map[string]interface{}
all the way down!
Configuration files, too, generally have this kind of schema. You can think of YAML or CUE files as being maps of string to empty interface, just like JSON. So when we’re dealing with structured data of any kind, we’ll often use this type in Go programs.
And when not to
A map[string]any
is like one of those universal travel
adapters, that plugs into any kind of socket and works with any voltage.
You can use it to protect your own vulnerable programs from damage
caused by weird, alien data.
Should you use map[string]interface{}
values within your
own programs, when there’s no need to handle arbitrary input data? No,
you shouldn’t. While it might seem convenient to not have to explicitly
define the schema of your objects, that can lead to all kinds of
problems. It also contravenes one of my Ten Commandments of Go.
For one thing, since interface{}
proverbially says
nothing, whenever we deal with a value of this type, we have to use
protective type assertions to prevent panics:
if _, ok := x.(string); !ok {
.Fatal("oh no")
log}
In other words, it’s a lot more difficult to write safe, reliable programs that operate on such maps. If your library produces data of this kind, it will hardly endear you to users. Instead, just use a plain old struct, which enables compile-time type checking and is much more convenient to deal with. Simple, straightforward, and easy to understand: that’s the Tao of Go.
Just like a travel adapter, map[string]any
is a bit
wonky and awkward to use when you’re at home and you can rely on your
sockets all having the expected voltage and pin schema.
But when you’re in contact with other worlds, outside the warm, safe
cocoon of Go’s type system, map[string]any
is perhaps the
ultimate travel accessory. Use it well!
Next
This is part 6 of a series on maps. To wrap up this series, we’ll look at a bunch of frequently asked questions about Go maps.
If you enjoyed this, check out For the Love of Go, my guide to learning Go for beginners. You’ll find everything you need to know to get started writing useful, delightful programs and packages in Golang, without all the baffling jargon.