skip to content

Readability Conventions

How Go code is made readable before it is made clever: names that read at the call site, doc comments the tooling understands, and flat control flow that returns early on failure.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

20

Why does Go write a multi-word identifier as maxRetryCount rather than max_retry_count?

level: juniorimportance: must knowfreq 72%

answer

  1. case is Go's word separator
  2. constants get no special spelling
  3. underscores live in file names
  4. the first letter already means something
  5. gofmt formats, it never renames

basics

~20 s

Go's convention is MixedCaps: each following word is capitalised and underscores are dropped. Case is already meaningful, since the first letter decides package visibility, so it doubles as the word separator. Underscores belong to file names.

solid answer

~40 s

Go names multi-word identifiers in MixedCaps or mixedCaps: `maxRetryCount`, `ParseRequest`, `deadLetterURL`. Underscores in identifiers are legal to the compiler but non-idiomatic, and no Go codebase you will read uses them. The convention has a structural reason: Go already gives the first letter of a name a meaning, so case is load-bearing rather than cosmetic, and using it as the word separator keeps names short and uniform. Nothing in the toolchain fixes a bad name for you — `gofmt` reformats layout and never renames identifiers, and `go vet` looks for correctness bugs, not casing. Underscores do have idiomatic homes in Go source: file names such as `handler_test.go` or `poll_linux.go`, and the blank identifier `_`. They just do not appear inside the names you declare.

code

go · 13 lines
go
const maxRetryCount = 3

type Account struct {
	CreatedAt time.Time `json:"created_at"`
}

func parseDeadLetter(raw []byte) (*Account, error) {
	var a Account
	if err := json.Unmarshal(raw, &a); err != nil {
		return nil, err
	}
	return &a, nil
}

go deeper

for a junior

Recall the rule and one example each way: maxRetryCount, not max_retry_count, and DefaultTimeout, not DEFAULT_TIMEOUT. Be ready to say that constants follow the same spelling as variables in Go.

for a middle

Explain why case is the separator: the first letter's case already carries package visibility, so Go readers are parsing case anyway. Know that gofmt formats layout and never renames identifiers.

for a senior

Show where the convention gets violated in real code — names copied in from database columns, JSON keys or environment variables — and that the fix is to canonicalise at the boundary and keep the external spelling in a tag or a query.

for a principal

Own the position that naming is enforced by review and by generators, not by hope. If a team keeps leaking foreign spellings into Go identifiers, the durable answer is a boundary that canonicalises names once rather than a style paragraph nobody rereads.

## The rule Go writes multi-word identifiers in **MixedCaps** (also called CamelCase): every word after the first begins with a capital letter, and the words are simply run together with nothing between them. `maxRetryCount`, `parseRequestBody`, `DeadLetterQueue`, `serveIndex`. The exported form starts with a capital (`MaxRetryCount`), the unexported form with a lowercase letter (`maxRetryCount`) — but in both, the *joining* of words is done with case, never with an underscore. This applies to every kind of name you declare: local variables, parameters, struct fields, methods, functions, types, constants and package-level variables. It also applies to constants, which is worth calling out because engineers arriving from C, Java or Python often expect `MAX_RETRY_COUNT`. Go does not use SCREAMING_SNAKE_CASE for constants at all. A constant is named exactly like any other identifier: `MaxRetryCount` if it is exported, `maxRetryCount` if it is not. There is no visual marker distinguishing a constant from a variable in Go, and none is wanted. ## Why case rather than underscores The usual answer is "because that is the convention", and for a junior interview that is nearly enough. But there is a structural reason worth being able to give. In Go, the case of a name's first letter is not a style choice — it is part of the language. A name beginning with an uppercase letter is visible to other packages; a name beginning with a lowercase letter is not. That means every Go engineer is already reading the case of identifiers for meaning, on every line, all day. Once case carries information, using it as the word separator too is cheap: the reader is already looking at it. Adding underscores on top would produce names like `Max_Retry_Count`, where the reader has to parse two separators that mean different things. The secondary reason is length. Go's culture favours short names, especially in short scopes. Underscores make names longer without making them clearer. ## What the toolchain does and does not do This is the part candidates most often get wrong. `gofmt` is not a linter and not a renamer. It normalises whitespace, indentation, alignment, the placement of braces and the grouping of imports. It will happily format a file full of `user_id` fields and change nothing about them. `go vet` reports likely bugs — a `Printf` verb that does not match its argument, a lock copied by value, an unreachable return — and says nothing about identifier casing. So MixedCaps is enforced by **people**: code review, and the fact that the standard library is written that way and every Go engineer has read it. If your team wants machine enforcement, that is a linter question, not a toolchain one. In an interview, saying "gofmt will fix it" is a concrete factual error and a bad look, because it suggests you have never actually watched gofmt run on a badly named file. ## Where underscores legitimately live Underscores are not banned from Go source — they are banned from *identifiers you declare*. They appear in: - **File names.** `user_service.go`, `user_service_test.go`, and the build-constrained suffixes the `go` command understands, such as `poll_linux.go` or `asm_arm64.s`. The `_test.go` suffix and the `_GOOS`/`_GOARCH` suffixes are part of the toolchain's own naming grammar. - **The blank identifier `_`**, used to discard a value (`_, err := f()`), to import a package purely for its side effects, or to assert at compile time that a type satisfies an interface (`var _ io.Reader = (*myReader)(nil)`). - **Generated and machine-derived code**, where names sometimes mirror an external system (C symbols reached through cgo, for example). This is tolerated precisely because a human did not choose the name. ## Where the mistake usually comes from The recurring source is a name imported from somewhere else: a database column `created_at`, a JSON key `user_id`, an environment variable `MAX_RETRIES`. The Go answer is that the *external* name stays in its own spelling — in a struct tag, in a query, in an `os.Getenv` call — and the Go identifier is spelled the Go way. So a struct field is `CreatedAt time.Time` with a tag mapping it to the external `created_at`; it is never a field named `Created_At`. The second source is a script or generator emitting Go from another system's names. That is the same problem at scale, and the fix is the same: canonicalise the name into Go's spelling at the boundary, once, in the generator. ## What an interviewer is checking That you have internalised the convention rather than memorised it, that you know case is semantically load-bearing in Go, and that you do not believe a formatter will rescue you. A candidate who says "MixedCaps, no underscores, and note that constants follow the same rule — there is no `MAX_SIZE` in idiomatic Go" has answered fully in two sentences.

  • Where do underscores legitimately appear in Go source?
    In file names — `user_service_test.go`, and the build-constrained suffixes such as `poll_linux.go` — and in the blank identifier `_`, used to discard a result, to import a package for its side effects, or to assert a type satisfies an interface. Machine-generated code that mirrors an external system's symbols is also tolerated. None of these are identifiers a human chose to declare.
  • How would you name an exported constant for a default timeout?
    `DefaultTimeout`. Go has no separate spelling for constants — they follow the same MixedCaps rule as everything else, so `DEFAULT_TIMEOUT` marks the author as arriving from C or Java. If the constant should not leave the package, it becomes `defaultTimeout`.
  • Does anything in the Go toolchain enforce this convention?
    No. `gofmt` normalises layout, indentation and import grouping but never renames an identifier; `go vet` looks for likely bugs, not casing. MixedCaps is held up by code review and by the standard library being written that way. Machine enforcement, if a team wants it, is a linter's job rather than the toolchain's.

saying these in an interview costs you the question

  • Says gofmt will rewrite snake_case identifiers for you
  • Thinks underscores in identifiers are a compile error
  • Names constants MAX_SIZE in SCREAMING_SNAKE_CASE
  • Mirrors database column spelling into Go struct field names
  • Calls the convention purely cosmetic, missing that case controls visibility
open as a page

Why should a Go doc comment on an exported function begin with the function's name?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Go's documentation tools lift the comment away from the declaration and show it on its own, so the sentence has to name its own subject. Starting with the identifier also makes the first sentence a self-contained one-line summary in package listings.

open as a page

In Go, why is the success path never put inside the `else` of an `if err != nil` check?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Idiomatic Go handles the failure inside the if and returns, leaving the success path unindented below it. Because Go checks an error after nearly every call, putting each success in an else pushes the real work rightward.

open as a page

In Go, why is a type named http.HTTPClient worse than http.Client?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Every reference outside the package already carries the package name, so http.HTTPClient says HTTP twice at the call site. Go names a type for how it reads after the qualifier, so inside package http the type is simply Client.

open as a page

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%

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.

open as a page

In Go, what should a method's receiver be named, and why is this or self discouraged?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Use one or two lowercase letters derived from the type — b for Buffer, v for Vec2 — and the same name in every method of that type. Go has no receiver keyword: the receiver is an ordinary parameter you name yourself, so this and self read as imports from another language.

open as a page

Why does Go name a getter Owner() instead of GetOwner(), while the setter stays SetOwner()?

level: middleimportance: should knowfreq 54%

basics

~20 s

Go drops Get because a method with a return value already implies it, so the accessor is named for the value it returns: Owner(). Set stays because a mutator needs a verb. Get survives only where a real lookup happens.

open as a page

In a Go package spread over several files, where does the package comment belong?

level: middleimportance: should knowfreq 42%

basics

~20 s

In exactly one file, as a comment block directly above that file's package clause with no blank line between. When the overview grows long, the convention is a doc.go file holding only that comment and the package clause.

open as a page

In Go, why is `defer` placed on the line right after the call that acquires a resource?

level: middleimportance: should knowfreq 56%

basics

~20 s

Putting the release right below the acquisition lets a reader check the pair at a glance, and it covers every exit added below. It belongs after the error check, so it never releases what was never acquired.

open as a page

Why does Go style reject package names like utils, common, or base?

level: middleimportance: should knowfreq 45%

basics

~20 s

A Go package name prefixes every identifier its callers write, so it has to say what the package provides. utils, common and base name nothing, so call sites read utils.Format with no domain in sight and the package grows into an unrelated grab bag.

open as a page

ErrNotFound or NotFoundError: how do you name errors in a Go package other teams import, and what does exporting one commit you to?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Go prefixes an error value with Err and suffixes an error type with Error: ErrNotFound is a sentinel, NotFoundError a struct. Exporting either makes it API, because callers branch on it and you cannot then stop returning it.

open as a page

A Go library's doc comment shows a usage snippet that no longer compiles. How do you stop that drift?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Move the snippet out of the comment into a runnable Example function in a _test.go file. It is compiled with the package, so an API change breaks the build, and a // Output: comment makes go test check what it prints.

open as a page

A Go HTTP handler nests error checks five levels deep and panicked on its deepest branch. How would you restructure it in review?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Invert each check into a guard that returns, so every branch sits at one level, then lift the middle into a helper returning a value and an error. The deep branch panicked because no reader and no test reached it.

open as a page

Splitting a repo-wide Go util package produces an import cycle; how do you carve it up and name the pieces?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Group by the capability callers want, not by file, and name each new package so its call sites read as phrases. The cycle is not new damage: references that were legal inside one package become a cycle Go's compiler rejects, exposing layering the bucket was hiding.

open as a page

One method on a mutex-holding Go type takes a value receiver while its other thirty take pointers — what goes wrong?

level: seniorimportance: should knowfreq 45%

basics

~20 s

That one method is handed a copy of the whole struct, mutex included. It locks the copy, so callers get no mutual exclusion, and anything it writes to a field is thrown away when it returns. go vet's copylocks check flags the copied lock; the fix is one receiver kind for the whole type.

open as a page

When would you replace a long if/else-if chain in Go with a tagless `switch`?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

A Go switch with no expression after the keyword compares each case against true, so every case is an ordinary condition. Use it when a function classifies: the conditions align in one column and default names the catch-all.

open as a page

In Go, why are one-letter locals like i, r and buf idiomatic rather than sloppy?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Go sizes a name to its scope. A variable declared and used within a few lines needs only enough letters to be unambiguous there, because its declaration is visible; a name read far from its declaration, such as an exported one, gets a full word.

open as a page

When may a Go method declaration leave the receiver unnamed, and why would you?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Whenever the body never uses it. Only the receiver's type is required, so func (Celsius) Unit() string is legal. Omitting the name — or writing an underscore — tells a reader the method's answer does not depend on the value it was called on.

open as a page

Your schema-to-Go generator maps field names to identifiers: how do you apply initialism casing and catch two fields collapsing to one name?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Split the schema name into words, canonicalise each through one explicit initialism list, then join in MixedCaps. Before writing any file, fail the run when two source fields produce the same Go identifier, naming both.

open as a page