Wandering Thoughts archives

2016-04-01

A surprise to watch out for with Go's expvar package (in expvar.Var)

The standard expvar package is a handy thing for easily exposing counters, values, and so on in a way that can be queried from outside your running program. As you might expect, it ultimately works through an interface type, expvar.Var. This interface is very simple:

type Var interface {
    String() string
}

If you see this definition, your eyes may light up with familiarity (as mine did), because this is exactly the extremely standard fmt.Stringer interface, where everything that has a .String() method can be handled by a lot of things. So of course you might well write code like this:

m := expvar.NewMap("myapp")
// the time.Time type has a String()
// method, so this will totally work.
m.Set("startTime", time.Now())

If you do this, everything will work right up to the point where programs that parse the JSON returned by the /debug/vars endpoint start failing with weird errors. If you look at the raw JSON, what you will see is something like this:

[...], "startTime": 2016-04-01 17:49:09.385829528 -0400 EDT, "anothervar": "something", [...]

In case you don't see the problem (as I didn't for some time), the string value for "startTime" doesn't have quotes around it, which makes it very invalid JSON. Go's current JSON parser starts trying to interpret the starting '2016-' bit as a number, then runs into the '-' and complains about it.

What is happening is that the expvar.Var String() interface method is misnamed; it should really be called something like JSON(). What the Var.String() method is actually required to do is produce the JSON representation of the Var as a string; for strings, this requires them to be quoted. A normal Stringer .String() method doesn't do this quoting, of course, because it would get in the way. The two interpretations of .String() are not really compatible, but there is no way to tell them apart and Go's implicitly satisfied interfaces will let you substitute one for the other (as I did when I tried to use time.Time as a Var).

So the takeaway here is that just because something has a .String() doesn't mean you can use it as an expvar.Var; in fact, you probably can't. Anything that's designed to be used as an expvar.Var will specifically say so in its documentation (or at least it should). Anything that has a .String() but doesn't mention expvar should be assumed to be satisfying the far more common fmt.Stringer interface instead.

(I don't have any clever solution for this. And I think the Go 1 API compatibility guarantee will keep this from changing, as expvar.Var was in its current form in the initial Go 1 release.)

programming/GoExpvarVarGotcha written at 23:31:08; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.