A team proposes a shared Go generics utility package. As tech lead, how do you decide?
answer
- subtract the standard library first
- somebody has to own it
- every importer inherits the go directive
- write the bar for admission down
- how does a helper get removed
basics
~20 sDecide on ownership and cost, not elegance. Ask what slices, maps and cmp already cover, who will own and review the package, what Go version floor it imposes on importers, and how a helper gets removed.
solid answer
~50 sI would not refuse generics; I would refuse an unowned junk drawer. Four questions settle it. **What is left after subtracting the standard library** — once `slices`, `maps` and `cmp` are taken out, the proposal is usually a third of its size. **Who owns it** — a package every team imports needs a named owner, a written rule for admitting a helper and a removal path, or it becomes where one-off functions go to die. **What version floor it imposes** — importers inherit the module's `go` directive, so a package built on the newest feature pins everyone's toolchain. **What the exit looks like** — can a helper be inlined back into its call sites later? My usual outcome is neither yes nor no: a small owned package with a written bar, and helpers that leave the way they arrived.
code
mod · 3 linesmodule example.com/platform/internal/generics
go 1.23go deeper
Know that shared code is a dependency with a cost, and that the standard library's slices, maps and cmp packages already cover most generic helpers a team is tempted to write.
Be able to argue both sides: what a shared helper saves, and what importing another team's package costs in coupling and version floor. Naming the stdlib subtraction step is a strong start.
Bring a criterion and an exit: which helpers encode an invariant worth sharing, what bar admits a new one, and how a helper is inlined back into its callers when it stops earning its place.
Own the whole trade: the named owner, the written admission rule, the Go version floor other teams inherit, the API commitment a published type parameter creates, and a scheduled review with evidence rather than taste.
## This is an ownership decision, not a language decision The technical question — may a type parameter be used here — was settled by the compiler in 2022. What is actually on the table is a **shared dependency every team will import**, and shared dependencies are governed by who maintains them, what they cost to change, and how they are retired. Answer it as you would answer a proposal for any internal library. ## Question 1: what is left after subtracting the standard library? Go 1.21 brought `slices`, `maps` and `cmp`. Ask for the list of proposed helpers and strike everything covered by `slices.Contains`, `slices.Index`, `slices.SortFunc`, `slices.Max` and their neighbours. In most proposals a third to a half disappears immediately, and the remainder is a much easier conversation. Anything that survives should be describable in one sentence naming a rule of *your domain*, not a generic list operation. ## Question 2: who owns it, and what is the admission rule? A utility package without an owner accumulates. Each addition is individually reasonable and the total is a package nobody can read, delete or refactor, imported by forty others. The countermeasures are cheap if applied on day one: - **A named owner**, in the package doc comment, who reviews additions. - **A written bar**, also in the doc comment, so reviewers apply it without escalating: three real call sites in at least two packages; constraints from the standard library (`any`, `comparable`, `cmp.Ordered`) rather than an interface invented for one caller; and type arguments inferable from the arguments at every call site. - **A removal path** — a helper that falls below the bar goes back into its caller's package. If the proposing team will not accept an owner and a bar, that is the answer to the proposal. ## Question 3: what version floor does it impose? This is the constraint people forget, and it is genuinely organisational. A module's `go` directive is a floor for everyone who imports it: a package written around iterators requires Go 1.23 or newer, and every importing team's build must have a toolchain at least that new or download one. In an estate with an older service that cannot move, a shared package is how one team's language-feature preference becomes another team's forced upgrade. Decide the floor deliberately and write it down, rather than discovering it when a build fails. ## Question 4: what is the API commitment? A type parameter and its constraint are part of the exported signature. - **Adding a type parameter** breaks every call site. - **Tightening a constraint** breaks callers whose type argument no longer qualifies. - **Widening a constraint** is usually safe for callers but may break the body, which can no longer rely on what the old constraint promised. That is a stiffer commitment than a package of concrete functions, where a new parameter can often arrive as a new function alongside the old one. Shared generic code should therefore be *narrow*: the fewer type parameters exported, the less you have promised. ## Question 5: what does the alternative actually cost? The alternative is duplication across teams, and it is not free — a bug fixed in one copy is not fixed in the other. But it is *reversible*, locally verifiable, and it needs no owner, no release process and no version negotiation. For helpers that are shapes rather than invariants, that trade is often correct, and saying so out loud keeps the discussion honest. ## What not to let drive the decision **A new language feature.** Go 1.27 permits a method to declare its own type parameters, which removes a workaround people had been living with. It does not change the calculus here: such a method still cannot implement an interface method, so it unlocks no plugin-shaped API, and building on it raises the module's floor. A feature is a reason to revisit a specific design, never a reason to open a utility package. **Elegance.** "We keep writing this loop" is an observation, not a case. The case is that a copy of the loop could be silently wrong. **Benchmarks alone.** A type parameter that measures faster in a microbenchmark has still not earned a cross-team dependency if the call is not hot in production. ## The decision I would actually record Approve a small, owned package; publish the bar in its doc comment; set and record the Go version floor; and schedule a review one quarter out with two numbers on the table — how many helpers it holds and how many call sites each has. Helpers below the bar go home. That is a decision a reviewer can be overruled on, which is the point: it is written down, it has an owner, and it can be revisited with evidence instead of taste.
- Go 1.27 lets a method declare its own type parameters — does that change the policy?Not by itself. It removes a workaround rather than a constraint: such a method still cannot implement an interface method, so it unlocks no plugin-shaped API, and depending on it raises the module's Go floor for every importer. A new feature justifies revisiting one design, never opening a utility package.
- How would you word the bar so reviewers apply it without you?Three sentences in the package doc comment: a helper needs three real call sites in at least two packages; its constraints come from the standard library — `any`, `comparable`, `cmp.Ordered` — rather than an interface invented for one caller; and its type arguments must be inferable from the arguments at every call site. Anything failing one goes back to the caller's package.
- What is the case for refusing outright and letting teams duplicate?Cross-team dependencies cost more than lines. A shared package needs an owner, a release story and a review queue, and every change becomes everyone's change. If the candidate helpers are shapes rather than invariants, duplication is genuinely cheaper — and it is reversible, whereas an imported package accumulates callers quickly.
- How would you tell a year later whether the decision was right?Two numbers and one question. How many helpers does it hold, and how many real call sites does each have — anything at one or two failed the bar. Then ask the teams whether they import it or copy from it; copying is the same verdict here that it is anywhere else, and it means the dependency cost outweighed the code.
saying these in an interview costs you the question
- Approves it because generics are modern Go
- Names no owner and no rule for admitting a helper
- Ignores the go directive floor that importers inherit
- Treats cross-team duplication as always worse than a dependency
- Plans no way to remove a helper once it stops paying
- Lets a newly released language feature drive the decision