skip to content

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

level: middleimportance: should knowfreq 45%

answer

  1. the name prefixes every call site
  2. what does this package provide?
  3. a bucket has no boundary to defend
  4. everything imports it; cycles follow

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.

solid answer

~50 s

A Go package name is not just a directory: it is the first word of every call its importers write, so it should name what the package provides. `utils`, `common`, `base` and `helpers` name a bucket rather than a capability, which fails twice. At the call site, `utils.Retry(ctx, fn)` tells the reader nothing about the domain, whereas `retry.Do(ctx, fn)` does. And in the repository, a name with no subject gives nobody a reason to say no, so everything unrelated accumulates there and every package ends up importing it — which is precisely the shape that produces import cycles the moment you try to split it. Good Go package names are short, lower-case, single words that name a thing: `bytes`, `bufio`, `strconv`, `time`. Plurality is not the issue — `strings` and `errors` are fine — having no subject is.

code

go · 4 lines
go
r := bufio.NewReader(f)
s := strconv.Itoa(42)
d, err := time.ParseDuration("1500ms")
var buf bytes.Buffer

go deeper

for a junior

Remember the concrete consequence: callers always type the package name, so utils.Something reads as noise. Be able to name two standard library packages whose names say exactly what they provide.

for a middle

Explain both halves — the call-site cost and the missing boundary that lets unrelated code accumulate — and state the actual conventions: short, lower-case, one word, naming a thing rather than a layer.

for a senior

Show that you have watched one of these rot. Talk about the grab bag becoming a universal import, the layering defect it conceals, and how you would decide what a helper's real home is instead.

for a principal

Own the policy angle: what the repository's package taxonomy is, who reviews a new package's name before it is imported widely, and why a bucket package is an architectural liability rather than a style nit.

## The package name is half of every identifier Go has no wholesale import that drops the qualifier, so outside its own directory a package's exported identifiers are always spelled `package.Identifier`. That makes the package name a permanent prefix on every call site in every consuming repository. Choosing it is therefore an API decision, not a filing decision, and the test for a candidate name is the same as for a type: write down the calls it will produce and read them. - `strconv.Itoa(42)` — the package says "string conversion", the function says "integer to ASCII". - `bufio.NewReader(f)` — buffered I/O, give me a reader. - `utils.IntToString(42)` — the package contributes nothing; the function had to carry the whole meaning by itself. The third example shows the real cost of a bucket name. Because the qualifier is dead weight, every function inside is forced to be self-describing, names get longer, and readers gain nothing from the import path. ## What a good Go package name looks like The conventions are narrow on purpose: - **Short and lower-case, one word.** No underscores, no mixed case, no long compounds. `bufio`, `strconv`, `httptest`. - **A noun that names what the package provides**, not what layer it sits in. `time` provides time. `json` provides JSON encoding. `models` and `types` provide nothing — they describe a category of code. - **Chosen with the exported names together.** The package and its members are designed as phrases; if you find yourself repeating the package word in every member, the package name is wrong or the members are. - **Plural is fine when the word genuinely names a thing.** `bytes`, `strings` and `errors` are plural and excellent. "Never plural" is a folk rule; "must name a subject" is the real one. ## Why the grab bag is worse than an ugly name A package called `utils` fails as a name, but the deeper damage is that it fails as a boundary. A package with a real subject answers the question "does this belong here?" — a date parser does not belong in `retry`. A package called `common` cannot answer that question at all, so the answer is always yes. Over a year it accumulates string helpers, a retry loop, a logging wrapper, a feature-flag reader and three constants nobody can delete. That has a specifically Go consequence. Because everything ends up importing the grab bag, and the grab bag ends up needing types from the domain it serves, the dependency arrows point both ways through it. Inside one package that is invisible — files in a package share one scope and may reference each other freely. The moment somebody tries to split the bucket into real packages, Go's compiler refuses the result with `import cycle not allowed`, and the team discovers that the bucket was hiding a genuine layering defect rather than causing a new one. ## Renaming does not fix it The common non-fix is to rename `utils` to `helpers`, `common` to `shared`, or `base` to `core`. None of those names a capability either, so nothing changes: the boundary is still unenforceable and the call site is still uninformative. Watch, too, for the stutter that bucket names attract — a package `common` that ends up exporting a type `Common`, so call sites read `common.Common`, is a reliable sign that nobody could say what the package is for. ## So where do genuine one-offs live? Most helper functions have exactly one caller, and the right home is the package that calls them, unexported. Go's culture is comfortable with a small amount of duplication — "a little copying is better than a little dependency" — so a three-line helper used by two packages does not automatically deserve to be shared. Promote code to its own package when there is a real name for what it provides, and let that name be the thing you argue about in review. If the only name anyone can propose is `utils`, that is direct evidence the grouping is not a thing yet. ## What to say in an interview Lead with the mechanic — the name is on every call site — then the boundary argument, then the Go-specific consequence that the bucket hides cycles the compiler will later refuse. Finish by naming the alternative: package by capability, name it for what the caller wants, and keep single-caller helpers unexported next to their caller.

  • Go's own strings and errors packages are plural. Is plurality what makes utils a bad name?
    No. `bytes`, `strings` and `errors` are plural and idiomatic because each word names a concrete thing the package provides. The defect in `utils` is that it names no subject at all, so it neither informs the call site nor tells you what belongs inside. Judge a package name by whether it answers "what does this provide?", not by its grammatical number.
  • Is package models or package types any better than package utils?
    No — they name a technical layer rather than a capability, so they attract everything the same way. They also breed stutter: `models.UserModel`, `types.ConfigType`. Prefer a package named for its domain, so callers read `user.Account` or `billing.Invoice` and the package name is doing real work in the expression.
  • If there is no utils package, where do one-off helper functions go?
    Unexported, in the package that calls them. Most helpers have a single caller and never need to be shared. When a second package needs one, prefer duplicating a few lines over inventing a shared bucket, and promote it to its own package only when you can give that package a real name for what it provides.

Naming a package utils is like labelling a kitchen drawer miscellaneous. Nothing can be refused entry, and nobody can find anything.

saying these in an interview costs you the question

  • Argues every repository needs one shared utils package
  • Renames utils to helpers and calls the problem solved
  • Groups packages by technical layer such as models or types
  • Thinks the package name is only a directory and never reaches callers
  • Ends up exporting common.Common or util.Util