Go's net/http.Headers

One probably knows that net/http.Headers is no more than map[string][]string with extra specific methods. A usual way to initialize and populate such data-structure from an external representation is something like that:

type Header map[string][]string

func (h Header) Add(key, val string) {
    if val == "" {
        return
    }
    h[key] = append(h[key], val)
}

func main() {
    h := make(Header)
    h.Add("Host", "example.com")
    h.Add("Via", "a.example.com")
    h.Add("Via", "b.example.com")
}

From the code above, one can notice that we allocated a new slice of strings for every unique key that we added to headers. For things like HTTP headers, that’re automatically parsed for every incoming request, this bunch of tiny allocations is something we’d like to avoid.

I was curious to know if Go’s standard library cares about that.

Looking at the implementation of net/textproto.Reader.ReadMIMEHeader(), which is used in the standard HTTP server, or Go 1.13’s new net/http.Header.Clone(), it turned out they solve the problem quite elegantly.

We know that for a majority of cases, HTTP headers are an immutable key-value pair, where most of the keys have a single value. Instead of allocating a separate slice for a unique key, Go pre-allocates a continues slice for values and refers to a sub-slice of this slice for all keys.

Knowing that, we can refactor the initial Header.Add as the following:

type Header map[string][]string

func (h Header) add(vv []string, key, val string) []string {
    if val == "" { ··· }

    // fast path for KV pair of a single value
    if h[key] == nil {
        vv = append(vv, value)
        h[key] = vv[:1:1]
        return vv[1:]
    }

    // slow path, when KV pair has two or more values
    h[key] = append(h[key], val)
    return vv
}

func main() {
    h := make(Header)
    // net/textprotocol pre-counts total number of request's headers
    // to allocate the slice of known capacity
    vv := make([]string, 0)

    vv = h.add(vv, "Host", "example.com")
    vv = h.add(vv, "Via", "a.example.com")
}

Note that we use vv[:1:1] to create a sub-slice of a fixed capacity (length 1, capacity 1).

If there is a KV-pair that has several values, e.g. “Via” header, Add will allocate a separate slice for that key, doubling its capacity.

All topics

applearduinoarm64askmeawsberlinbookmarksbuildkitcgocoffeecontinuous profilingCOVID-19designdockerdynamodbe-paperenglishenumesp8266firefoxgithub actionsGogooglegraphqlhomelabIPv6k3skuberneteslinuxmacosmaterial designmDNSmusicndppdneondatabaseobjective-cpasskeyspostgresqlpprofprofeferandomraspberry pirusttravis civs codewaveshareµ-benchmarks