How do you attach an X-Trace-Id header to an outbound *http.Request, and how does Header.Set differ from Header.Add?
answer
- one name, a slice of values
- replace or append
- the key is rewritten before the map write
- Get reads only the first value
- Del is not Set with an empty string
basics
~20 sBuild the request, then call req.Header.Set("X-Trace-Id", id) before sending it. Set replaces every value already stored under that key; Add appends another value and keeps the earlier ones. Both store the key in canonical form.
solid answer
~40 s`http.Header` is a `map[string][]string` with helper methods, so you set a trace header by calling `req.Header.Set("X-Trace-Id", id)` on the `*http.Request` after `http.NewRequestWithContext` returns it and before `client.Do` sends it. `Set` replaces whatever was stored under that key; `Add` appends, so the key ends up holding a slice — right for headers that may legitimately repeat, almost never right for a trace id. `Get` returns the first value, `Values` returns all of them, `Del` removes the key. All of those canonicalise the key first, so `Set("x-trace-id", …)` stores `X-Trace-Id`. Assigning straight into the map stores the key exactly as you typed it and skips that canonicalisation, which is how a header becomes invisible to code that reads it with `Get`.
code
go · 7 linesreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return err
}
req.Header.Set("X-Trace-Id", traceID) // replaces any existing value
req.Header.Add("X-Debug-Tag", "retry") // appends alongside existing values
resp, err := http.DefaultClient.Do(req)go deeper
Be ready to write the lines cold: build the request, call Header.Set with the name and value, then send it. Know that Set replaces the value and Add appends another one.
Explain that http.Header is a map from name to a slice of values, and that Set, Add, Get, Values and Del all rewrite the key into canonical form while a direct map write does not.
Show judgment about which headers may legitimately repeat and which must be single-valued, and argue for setting outbound headers in one shared place rather than repeating the call at every site.
Own the convention itself: which correlation header names your services agree on, whether the edge validates or regenerates them, and what a service does when a request arrives carrying none.
### What an http.Header is In `net/http`, `http.Header` is declared as `map[string][]string` with methods attached to it. The key is a header name; the value is a slice because HTTP permits the same header name to appear on more than one line. The same type is used on both sides: the `*http.Request` you build for an outbound call and the `*http.Request` your handler receives both expose it as `req.Header`. ### Setting a header on a request you are about to send The order is always the same: create the request, mutate its header, then hand it to a client. ```go req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return err } req.Header.Set("X-Trace-Id", traceID) resp, err := http.DefaultClient.Do(req) ``` Once `Do` has been called the request has been written; mutating the header afterwards changes nothing on the wire and, if the request is still in flight, is a data race. ### Set versus Add - `Set(key, value)` — the key ends up with exactly one value. Any values already there are discarded. - `Add(key, value)` — the value is appended to whatever slice is already under the key. For a trace or request identifier you want `Set`. Two values under a correlation header is not "more information", it is an ambiguity every downstream reader has to resolve, and readers that call `Get` will silently take the first one. `Add` earns its place for headers that are defined as lists — `Accept`, `Set-Cookie` on a response, forwarding headers that each hop appends to. ### The rest of the small API - `Get(key) string` — the first value, or `""` when the key is absent. It never errors and never panics. - `Values(key) []string` — every value stored under the key, `nil` when absent. - `Del(key)` — removes the key entirely. This is not the same as `Set(key, "")`, which leaves the key present with one empty value and still writes a header line. - `Clone()` — a deep copy of the map, useful when you want to hand a modified header set to something else without mutating the original. ### Canonicalisation, in one paragraph `Set`, `Add`, `Get`, `Values` and `Del` all run the key through the same canonicalisation before touching the map: the first letter and every letter following a hyphen is upper-cased and the rest are lower-cased, so `x-trace-id`, `X-TRACE-ID` and `x-Trace-ID` all address the single map key `X-Trace-Id`. That is why those five methods look case-insensitive. Direct map assignment — `req.Header["x-trace-id"] = []string{id}` — is an ordinary Go map write and does no such thing: the map now has a second, non-canonical key, `req.Header.Get("X-Trace-Id")` will not find it, and on an HTTP/1 connection your literal casing goes out on the wire. Use the methods unless you have a deliberate reason not to. ### Where the call belongs Setting the header at every call site is how a chain acquires a hole: one new outbound call is written, nobody remembers the line, and that hop starts a fresh trace. Route outbound requests through one constructor or one client-side helper that sets the header, so adding a call is not also a chance to forget it. ### Common mistakes Using `Add` where `Set` was meant, and only discovering it when a downstream service logs two identifiers. Assigning into the map and then wondering why `Get` returns `""`. Expecting `Set(key, "")` to delete. Mutating the header after `Do`. All four are cheap to avoid once you know the type is a plain map with five well-behaved methods on top.
- What does req.Header.Get return when the header was never set?The empty string. `Get` canonicalises the key, looks it up and returns `""` when the key is missing or its slice is empty — it never errors and never panics. That also means `Get` cannot distinguish a header that was absent from one sent with an empty value; when that difference matters, check the length of `req.Header.Values(key)` instead.
- An upstream helper already set a header you must remove. What do you call?`req.Header.Del("X-Trace-Id")`, which canonicalises the key and deletes it from the map. Setting it to the empty string is not equivalent: the key stays in the map with one empty value, and the header line is still written on the wire, so a receiver checking only for presence still sees it.
- Is it safe to set headers on a request after passing it to client.Do?No. By the time `Do` returns, the request has been written; a later mutation changes nothing that was sent. Worse, while the call is in flight the transport may still be reading the header map, so mutating it concurrently is a genuine data race that the race detector will flag. Finish building the request before you send it.
Set is assigning to a variable; Add is appending to a list. The header map holds a list under every name, and the two methods differ only in whether they clear it first.
saying these in an interview costs you the question
- Treats Add and Set as interchangeable for any header
- Assigns into req.Header directly, then reads with Get
- Thinks Set with an empty string removes the header
- Believes Get matches header keys case-sensitively
- Sets headers after the client has already sent the request