skip to content

Why is Go's handler method named ServeHTTP rather than ServeHttp, and how is an initialism cased in an unexported name?

level: middleimportance: must knowfreq 62%

answer

  1. all up or all down, never title case
  2. Id abbreviates identifier
  3. the first letter still controls export
  4. leading initialism goes fully lowercase
  5. xmlHTTPRequest shows both positions at once

basics

~10 s

Go keeps an initialism in one uniform case, all upper or all lower, never title case: ServeHTTP, URL, userID. In an unexported name a leading initialism goes fully lowercase, as in xmlHTTPClient.

solid answer

~40 s

Go's rule is that a word which is an initialism or acronym keeps a consistent case throughout: `HTTP`, not `Http`; `URL`, not `Url`; `ID`, not `Id`. That gives `ServeHTTP` on `http.Handler`, the `URL` field on `http.Request`, and `userID` for an unexported field. The case of the *first* letter of the whole identifier still decides whether the name is exported, so an unexported name that begins with an initialism lowercases the entire initialism rather than just its first letter: `urlPool`, `xmlHTTPClient` — not `uRLPool`. Interior initialisms keep their uppercase form even in unexported names, which is why `xmlHTTPClient` mixes the two. The trap is `Id`: `userId` and `userID` both appear in the wild, but only `userID` follows the convention, and mixing the two inside one codebase is the readability cost the rule exists to prevent.

code

go · 14 lines
go
type Session struct {
	UserID string
}

func (s *Session) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, s.UserID)
}

var xmlHTTPClient = &http.Client{}

func parseJSON(b []byte) (map[string]any, error) {
	m := map[string]any{}
	return m, json.Unmarshal(b, &m)
}

go deeper

for a junior

Memorise the shape: ServeHTTP, URL, userID. If you take one thing away, take that ID is an abbreviation and is written ID, never Id.

for a middle

Explain the interaction with export: the first letter's case decides visibility, so an unexported name beginning with an initialism lowercases the whole initialism, while an interior one stays uppercase. xmlHTTPClient shows both.

for a senior

Argue the cost. An exported UserId is API surface, so the mistake is permanent in a way a local variable's is not, and a codebase carrying both spellings defeats grep. Say who catches it, since no toolchain command does.

for a principal

Decide how the convention is held: a written list of the initialisms your generators and adapters recognise, applied at every boundary that mints Go names, beats a style note. Own the migration cost when the list changes.

## The convention A word inside a Go identifier that is an initialism or acronym — `HTTP`, `URL`, `ID`, `API`, `JSON`, `XML`, `SQL`, `TLS`, `UUID`, `CPU`, `DB` — is written in one **consistent case**. Either every letter is uppercase or every letter is lowercase. It is never title-cased. So: - `ServeHTTP`, not `ServeHttp` - `URL`, not `Url` - `userID`, not `userId` - `parseJSON`, not `parseJson` - `apiKey` or `APIKey`, never `ApiKey` The standard library is the reference implementation of this rule. `http.Handler` declares exactly one method, `ServeHTTP`. The type in `net/url` is `url.URL`. `http.Request` has a field named `URL`. `json.RawMessage`, `tls.Config`, `sql.DB` — all of them keep the initialism whole. ## The interaction with export Go decides whether a name is visible outside its package by looking at the case of its **first letter**. That creates a question the naive rule does not answer: what happens when an unexported name *starts* with an initialism? You cannot write `URLPool` and still have it unexported, and `uRLPool` is unreadable. The answer is that the leading initialism goes fully lowercase: - exported: `URLPool`, `HTTPClient`, `IDToken` - unexported: `urlPool`, `httpClient`, `idToken` Because the rule is about *each initialism* rather than about the identifier as a whole, an initialism that appears in the interior of an unexported name still keeps its uppercase form. Hence the canonical illustration: `xmlHTTPRequest` — the leading `xml` is lowercased because it starts the unexported name, while the interior `HTTP` stays uppercase. Both halves are internally consistent; neither is title-cased. ## Why it matters more than it looks Three reasons come up in review. **Searchability.** If half the codebase says `userID` and half says `userId`, no single grep finds the concept. The value of a naming convention is that a mechanical search works. **The boundary is permanent.** An exported field name is the API. Renaming `Id` to `ID` on a type other packages already reference is a breaking change to every caller, so the cost of getting it wrong is not paid on the day you write it — it is paid every time somebody wants to fix it and cannot. **It is a fast proxy for Go literacy.** `ServeHttp` in a diff tells a reviewer instantly that the author has not read much Go, in the same way `String.equals` misuse does in Java. That is unfair as a judgment about the engineer, but it is real, and it is why the question gets asked. ## The Id versus ID trap This is the single most common violation, because `Id` looks like an ordinary word and reads naturally. It is not: `ID` abbreviates *identifier*, so it is an initialism and takes uniform case. `userID`, `orderID`, `TenantID`. It also compounds. Once a codebase contains both `Id` and `ID`, generated code, ORM-shaped mappings and hand-written adapters each pick a side, and you end up with `UserId` in one struct and `UserID` in another describing the same field. The fix is expensive precisely because the names are exported. ## What the toolchain does Nothing. `gofmt` reformats and never renames; `go vet` looks for likely bugs, not naming. This convention lives in code review and in the reflex of having read the standard library. There is no `go` subcommand that will find `Url` for you. ## The edge cases worth knowing **Two-letter initialisms are still initialisms.** `ID`, `DB`, `OK`, `IO`. `dbConn` unexported, `DBConn` exported. **Adjacent initialisms are simply concatenated.** `HTTPSURL` is legal by the rule but unreadable, which is a signal to restructure the name rather than to break the rule — often the package or the surrounding type already carries half of it. **Not every capitalised abbreviation is an initialism.** `Ok` versus `OK` is settled in favour of `OK`; but a word like `Postgres` or `Redis` is a name, not an initialism, and is title-cased normally. **Struct tags are unaffected.** The Go field is `UserID`; the JSON key in the tag stays whatever the wire format says, typically `user_id`. The tag is where the foreign spelling lives. ## Answering it well State the rule in one line — an initialism keeps uniform case, all-up or all-down, never title case — then immediately give the unexported-leading case, because that is the part that distinguishes someone who has actually written Go from someone who has read a style guide summary. `userID`, `urlPool`, `xmlHTTPClient` covers all three positions in about four seconds.

  • How do you name an unexported variable that begins with an initialism?
    Lowercase the whole initialism: `urlPool`, `httpClient`, `idToken`. Writing `uRLPool` to force the first letter down is never done. An initialism appearing later in the same name keeps its uppercase form, which is why `xmlHTTPClient` is correct — the leading `xml` is down because the name is unexported, the interior `HTTP` stays up.
  • Why is userId a worse mistake on an exported field than on a local variable?
    An exported field name is part of the package's API. Other packages reference it, so correcting `UserId` to `UserID` later breaks every caller and every reflective or tag-driven consumer that names it. A local variable can be renamed in one commit by one person, which is why reviewers push hardest on the exported surface.
  • Does the initialism rule change how the JSON key is spelled?
    No. The Go identifier follows Go's convention and the wire format keeps its own spelling in the struct tag: a field `UserID string` with a tag mapping it to `user_id`. Keeping the foreign spelling confined to the tag is exactly what lets the Go side stay uniform.

saying these in an interview costs you the question

  • Writes Url, Http or Id as ordinary title-cased words
  • Thinks ID is a normal word rather than an abbreviation
  • Writes uRLPool to make a leading initialism unexported
  • Believes gofmt or go vet flags an initialism cased wrongly
  • Mixes UserId and UserID across packages and calls it harmless