The expvar Package — Specification¶
Table of Contents¶
- Introduction
- Where
expvarIs Specified - The
VarInterface - Exported Types
- Exported Functions
- The HTTP Handler Contract
- The Default Variables
- The JSON Output Contract
- Concurrency Guarantees
- The Duplicate-Name Contract
- Behaviour Across Go Versions
- References
Introduction¶
The Go language specification (go.dev/ref/spec) does not mention expvar. It is a standard library package, not a language feature. The authoritative reference is the package documentation at pkg.go.dev/expvar, backed by the implementation in src/expvar/expvar.go of the Go distribution.
Sources of truth, in decreasing formality:
- Package documentation —
pkg.go.dev/expvar, the godoc for every exported symbol. - The standard library source —
src/expvar/expvar.go, the de-facto specification where the godoc is terse. runtime.MemStatsdocumentation —pkg.go.dev/runtime#MemStats, for thememstatsdefault.
This file separates "what the documented API guarantees" from implementation detail. Where the godoc is silent, the source is the specification.
Where expvar Is Specified¶
expvar is documented entirely by its godoc. The package comment states its purpose:
Package
expvarprovides a standardized interface to public variables, such as operation counters in servers. It exposes these variables via HTTP at/debug/varsin JSON format.
The godoc further specifies that importing the package registers the HTTP handler and publishes cmdline and memstats. There is no separate prose reference (unlike the Go Modules Reference for go mod); the godoc is the reference.
The Var Interface¶
The central type:
The documented contract: String() returns a valid JSON value. Every published variable must satisfy this interface, and the returned string is used verbatim as the JSON value in the /debug/vars document. The godoc is explicit that the returned string must be valid JSON.
A type satisfies Var by implementing this single method. The built-in types (Int, Float, String, Map, Func) all implement it correctly; custom types must honour the valid-JSON requirement.
Exported Types¶
| Type | Definition | Key methods |
|---|---|---|
Int | A 64-bit integer variable | Add(delta int64), Set(value int64), Value() int64, String() string |
Float | A 64-bit float variable | Add(delta float64), Set(value float64), Value() float64, String() string |
String | A string variable | Set(value string), Value() string, String() string |
Map | A string-keyed map of Var | Add, AddFloat, Set, Get, Delete, Do, Init, String |
Func | func() any adapted to Var | Value() any, String() string |
KeyValue | struct{ Key string; Value Var } | (a plain struct; argument to Do) |
Documented details:
Int,Float—AddandSetare documented as safe for concurrent use.Valuereturns the current numeric value;Stringreturns its JSON-number rendering.String—Value()returns the raw Go string;String()returns the JSON-quoted string. The godoc notesStringreturns a quoted form suitable for JSON.Map—Add(key, delta int64)adds to theIntunderkey, creating it if needed.AddFloat(key, delta float64)does the same with aFloat.Set(key, av Var)assigns an arbitraryVar.Get(key) Varreturns the value ornil.Delete(key)removes it.Do(f func(KeyValue))iterates in sorted key order.Init() *Mapremoves all keys and returns the map.Func— defined astype Func func() any. ItsString()calls the function and JSON-marshals the result. The value is recomputed on each call.
Exported Functions¶
| Function | Signature | Behaviour |
|---|---|---|
Publish | func Publish(name string, v Var) | Registers v under name in the global registry. Calls log.Fatal (terminates) if name is already registered. |
Get | func Get(name string) Var | Returns the Var registered under name, or nil if none. |
NewInt | func NewInt(name string) *Int | Creates an Int, publishes it under name, returns it. |
NewFloat | func NewFloat(name string) *Float | Creates a Float, publishes it, returns it. |
NewString | func NewString(name string) *String | Creates a String, publishes it, returns it. |
NewMap | func NewMap(name string) *Map | Creates a Map, publishes it, returns it. |
Do | func Do(f func(KeyValue)) | Calls f for each published variable, in sorted key order, while holding the registry lock. |
Handler | func Handler() http.Handler | Returns the HTTP handler that serves /debug/vars. |
Documented properties:
- The
New*functions are convenience wrappers overPublish; they inherit the duplicate-name termination. Doholds the registry lock during iteration; the godoc cautions against operations that would require the lock from withinf.Handlerwas added so the endpoint can be mounted on a mux other thanhttp.DefaultServeMux.
The HTTP Handler Contract¶
On import, the package's init:
- Registers a handler at the path
/debug/varsonhttp.DefaultServeMux(equivalent tohttp.Handle("/debug/vars", Handler())). - Publishes the
cmdlineandmemstatsvariables.
The handler:
- Responds with
Content-Type: application/json; charset=utf-8. - Writes a single JSON object whose members are every published variable: the key is the variable name (JSON-quoted), the value is the verbatim output of that variable's
String(). - Iterates variables in sorted key order, producing deterministic output.
Handler() returns this same handler so callers can mount it explicitly:
The godoc notes that the package registers on the default mux and that Handler() is provided for callers who want to register elsewhere. There is no documented way to unregister the default-mux handler.
The Default Variables¶
Two variables are published at init:
cmdline— documented as the program's command-line arguments (os.Args), as a JSON array of strings. Implemented as aFunc.memstats— documented as the result ofruntime.ReadMemStats, i.e. aruntime.MemStatsvalue serialized to JSON. Implemented as aFunc, so it is recomputed (a freshReadMemStats) on every read.
The exact field set of memstats is defined by runtime.MemStats, not by expvar; consult pkg.go.dev/runtime#MemStats for field meanings.
The JSON Output Contract¶
The documented output guarantees:
- The response body is a single JSON object.
- Each member key is a published variable name.
- Each member value is the raw string returned by that variable's
String()method — the handler does not re-encode it. - Members appear in sorted key order.
The critical consequence (specified by the "must be valid JSON" requirement on Var.String()): because the value is spliced verbatim, a Var whose String() returns invalid JSON produces an invalid overall document. The contract places the burden of valid JSON on each Var implementation, not on the handler.
The built-in types satisfy this: - Int/Float emit a JSON number. - String emits a JSON string (quoted, escaped). - Map emits a JSON object. - Func emits json.Marshal of the function's result.
Concurrency Guarantees¶
The godoc documents these safety properties:
Int,Float,String—Add/Set/Valueare safe for concurrent use. (The implementation usessync/atomic.)Map—Add,AddFloat,Set,Get,Delete, andDoare safe for concurrent use.- The registry —
Publish,Get, andDoare safe for concurrent use (guarded by a mutex). Func— the adapter itself adds no synchronization; the function body's thread-safety is the caller's responsibility, since it may run concurrently with other goroutines reading or mutating shared state.
No built-in type requires the caller to add a mutex. The documentation's concurrency guarantees cover all the type-level operations.
The Duplicate-Name Contract¶
Publish (and therefore the New* constructors) terminates the program if the name is already registered. The godoc states that Publish logs via log.Fatal (or equivalently panics in a way that terminates) on a reused name, with a message of the form Reuse of exported var name: <name>.
Specified consequences:
- Names are effectively permanent for the process lifetime; there is no
Unpublish. - A name collision is a fatal startup condition, not a recoverable error —
Publishhas no error return. - Two packages publishing the same name, or a test republishing a name, will terminate the process.
This is a deliberate API contract: publication has no error channel (it occurs in init/var initializers), and the package chooses loud termination over silent shadowing.
Behaviour Across Go Versions¶
expvar has been remarkably stable since Go 1.0. Notable points:
- Go 1.0 —
expvarintroduced withInt,Float,String,Map,Func,Publish,Get, theNew*constructors,Do, and the/debug/varsregistration on the default mux. - Go 1.8 —
Handler()added, enabling explicit mounting on a non-default mux. - Various releases — internal concurrency hardening of
StringandMap(e.g. moving to atomic-backed storage so reads are never torn) without changing the documented API.Float.Addis implemented as a CAS loop. - Go 1.18+ —
Funcis documented asfunc() any(theanyalias forinterface{}); behaviour unchanged. Map.Initreturns*Mapfor chaining;Map.Deleteis available for key removal.
The documented surface — the Var interface, the five types, the Publish/Get/New*/Do/Handler functions, the /debug/vars endpoint, and the two default variables — has been stable for the entire modern history of Go. New code on Go 1.21+ uses the same API as code from a decade earlier.
References¶
expvarpackage documentation — authoritative.expvar.Varinterfaceexpvar.Handlerexpvar.Publish/Get/Doruntime.MemStats— thememstatsfield set.- Source:
src/expvar/expvar.go net/http.ServeMux— the default-mux registration target.
In this topic