In a Go HTTP handler, what does r.URL.Query().Get("page") return when the URL carries no page parameter?
answer
- no error, no comma-ok
- missing and blank look the same
- url.Values is a map of slices
- Has, or index the map, to tell them apart
- Query re-parses RawQuery every call
basics
~10 sIt returns the empty string and never an error, so a missing parameter and a present-but-empty one look identical. Use url.Values.Has, or index the url.Values map directly, when that difference matters.
solid answer
~40 s`r.URL.Query()` parses the raw query string into a `url.Values`, which is a `map[string][]string`. `Get` returns the first value for a key, or `""` when the key is absent — there is no error and no second `ok` return, so `/items` and `/items?page=` are indistinguishable through `Get`. If you need to tell them apart, call `Has("page")` or index the map (`q["page"]`, which is a nil slice when absent). Indexing is also how you read a repeated parameter: `?tag=a&tag=b` gives a two-element slice that `Get` would flatten to `"a"`. Two more things bite people: `Query()` re-parses `r.URL.RawQuery` on every call, so hoist it into a variable in a hot handler, and it silently drops malformed pairs — use `url.ParseQuery` when you want that error.
code
go · 14 linesq := r.URL.Query() // url.Values, i.e. map[string][]string
page := q.Get("page") // "" for /items AND for /items?page=
present := q.Has("page") // true only when the key appears at all
tags := q["tag"] // ?tag=a&tag=b -> ["a" "b"]; nil when absent
limit := 20
if s := q.Get("limit"); s != "" {
n, err := strconv.Atoi(s)
if err != nil {
http.Error(w, "limit must be an integer", http.StatusBadRequest)
return
}
limit = n
}go deeper
Remember the shape: Query returns url.Values, Get returns a string that is empty when the key is missing, and there is no error to check. Be ready to convert with strconv and handle the empty case before converting.
Explain why url.Values is map[string][]string, what Get does to repeated keys, and how Has or direct map indexing recovers the missing-versus-blank distinction that Get erases.
Show the API judgment: decide and document whether a blank parameter is a 400 or a default, read the query once per request, and use url.ParseQuery when malformed input must be rejected rather than silently dropped.
Frame it as contract design — optional-with-default versus required, repeated keys versus comma-separated lists, and the fact that a lenient reader today is a compatibility promise you cannot tighten later without breaking clients.
## What `r.URL.Query()` actually is In a `net/http` handler, `r` is an `*http.Request` and `r.URL` is a `*url.URL`. The query string lives on that URL as the raw, still-encoded text in `r.URL.RawQuery` — for `/items?page=2&tag=go` that field holds `page=2&tag=go`. Calling `r.URL.Query()` parses that text and returns a `url.Values`, which is declared as `map[string][]string`. The slice is there because HTTP query strings may repeat a key: `?tag=a&tag=b` is legal and produces `["a", "b"]`. ## Why `Get` returns `""` and nothing else `url.Values.Get(key)` is a convenience over that map: it returns the first element of the slice for `key`, or the empty string if the key is not present at all. Its signature returns a single `string` — no error, no comma-ok. That is a deliberate design: query parameters are optional by nature, and Go's standard library leaves the policy (is empty acceptable? is missing acceptable?) to you. The practical consequence is that three different URLs collapse to the same result: - `/items` — no key at all - `/items?page=` — key present, empty value - `/items?page` — key present, empty value (a bare key parses to an empty string) All three make `q.Get("page")` return `""`. If your handler wants to say "you sent `page` but left it blank, that is a 400", `Get` alone cannot express it. `url.Values.Has(key)` reports whether the key appears at all, and indexing the map gives you the ground truth: `q["page"]` is a `nil` slice when the key is absent and `[]string{""}` when it is present and empty. ## Reading typed parameters Everything in a query string is text. A numeric parameter is a two-step operation, and the empty-string case has to be decided first: 1. Read the raw value. 2. If it is empty, apply your default (or reject). 3. Otherwise convert with `strconv.Atoi` / `strconv.ParseInt` / `strconv.ParseBool` and handle the conversion error as a client error, not a server error. Skipping step 2 is the classic bug: `strconv.Atoi("")` returns an error, so a handler that treats every conversion failure as a 400 will reject requests that simply omitted an optional parameter. ## Repeated keys and ordering Because `url.Values` is a map of slices, repeats are preserved in the order they appeared in the query string, but the keys themselves have no order — it is a Go map. If your API accepts `?id=1&id=2&id=3`, read `q["id"]` and iterate; if it accepts `?id=1,2,3`, read `q.Get("id")` and split. Decide which shape your API uses and document it, because clients will guess otherwise. ## Two behaviours that surprise people **`Query()` is not cached.** Each call re-parses `r.URL.RawQuery` and allocates a fresh map. Calling it once per parameter in a handler is correct but wasteful; assign `q := r.URL.Query()` once and read from `q`. **Malformed pairs are dropped silently.** `Query()` discards pairs it cannot parse and returns whatever it could. If you need to know that the client sent nonsense, call `url.ParseQuery(r.URL.RawQuery)` yourself, which returns `(url.Values, error)`, and answer 400 on the error. ## Query values versus form values `r.URL.Query()` reads the URL only — it never touches the request body, so it works identically for GET, POST and everything else, and it cannot consume anything. That makes it the safe way to read URL parameters even in a handler that also decodes a body. The `r.Form` field, by contrast, is populated by `r.ParseForm()` and merges query parameters with urlencoded body parameters; reach for it only when you actually want that merge. ## Interview-ready summary `Get` is a first-value-or-empty-string helper over a `map[string][]string`. It cannot report absence, it cannot report a parse failure, and it flattens repeats. Know the three escape hatches — `Has`, direct map indexing, and `url.ParseQuery` — and you can answer every follow-up in this area.
- How do you read a query parameter that a client may repeat, like ?tag=a&tag=b?Index the map instead of calling `Get`: `tags := r.URL.Query()["tag"]` gives `[]string{"a", "b"}` in the order they appeared, and a `nil` slice when the key is absent. `Get` would return only `"a"` and silently hide the second value, which is how duplicated-parameter bugs get shipped.
- Is it a problem to call r.URL.Query() several times in one handler?It is correct but wasteful. `Query()` parses `r.URL.RawQuery` and allocates a new `url.Values` map on every call — nothing is cached on the `*url.URL`. In a hot handler, assign it once (`q := r.URL.Query()`) and read every parameter from that variable.
- How would you detect a client sending a malformed query string?`r.URL.Query()` silently discards pairs it cannot parse, so it never tells you. Call `url.ParseQuery(r.URL.RawQuery)` directly instead: it returns `(url.Values, error)`, giving you both the values it recovered and the parse error, which you can answer with 400 if your API wants to be strict.
saying these in an interview costs you the question
- Claims Get returns an error or a comma-ok pair for a missing key
- Thinks a missing parameter and an empty one can be told apart by Get
- Expects a missing map key to panic in Go
- Uses Get on a repeated parameter and never sees the extra values
- Assumes Query() is parsed once and cached on the request