skip to content

A downstream team's build breaks after upgrading your Go SDK's new minor tag. How do you find the cause and stop a repeat?

level: seniorimportance: should knowfreq 32%

answer

  1. reproduce from the consumer's side
  2. the compiler names the break for you
  3. your tests live inside the package
  4. diff the two exported surfaces
  5. a canary importer in CI

basics

~20 s

Reproduce from the consumer side: point a real importer's requirement at the new tag and run go build ./..., because the compiler names the break. Then diff the two exported surfaces, patch, and add a consumer build to CI.

solid answer

~50 s

Start where the failure is visible, not in your own repository: take a consumer module, set its requirement to the new tag and run `go build ./...` and `go test ./...`. The compiler error is usually the whole diagnosis — "missing method", "too few values in struct literal", "undefined", "cannot be compared" each point at a specific class of change. Bisect across the tags if the diff is large. Then compare the exported surfaces of the two versions — `go doc` output of every package, or an API-diff tool — and you will see the added interface method or the removed symbol that your own green test run never noticed. The fix is a patch release restoring the old surface, and retracting the bad tag so nobody else selects it. The prevention is structural: a canary consumer module built against the library's HEAD in CI, and an exported-API diff on every pull request.

code

text · 4 lines
text
$ go build ./...
./cache.go:41:22: cannot use memStore{} (value of type memStore) as sdk.Store
	value in argument to sdk.Register: memStore does not implement
	sdk.Store (missing method Delete)

go deeper

for a junior

Know that the consumer's compiler is the authority: building an importing module against the new version reproduces the failure and names it.

for a middle

Be able to map each error message to its cause — missing method, too few values in struct literal, undefined, cannot be compared — and to bisect across the release's commits.

for a senior

Show the operational answer: reproduce, patch and retract the bad tag, then add a canary consumer build and a surface diff so the class of defect cannot escape again.

for a principal

Own the release gate itself — who signs off an incompatible surface change, what the SDK promises its importers, and how much of that promise you automate rather than trust.

## Why your own tests were green A library's test suite is the worst possible detector for this class of break, for two reasons. First, most Go tests live *inside* the package they test, where unexported identifiers are in scope, so they never see the surface an importer sees. Second, the tests are compiled against the same source as the change, so a modified interface and its modified implementations always agree. Nothing in that build represents a type written by someone else against last month's contract. That is the whole reason this failure reaches a downstream team rather than your CI. ## Reproducing it Work from the consumer's side: 1. Clone or copy the failing consumer module. 2. Point its requirement at the new tag and run `go build ./...` then `go test ./...`. 3. Read the error. The message class usually identifies the change immediately: `does not implement X (missing method Y)` means a method was added to an exported interface; `too few values in struct literal` means a field was added to an all-exported struct that the consumer built positionally; `undefined: pkg.Foo` means something was removed or renamed; `invalid operation ... cannot be compared` means a non-comparable field entered a struct used with `==` or as a map key; a signature mismatch means a parameter or result changed. 4. If the release contained many commits, bisect: build the consumer against successive commits until the first failing one. Remember that the consumer did not necessarily choose to upgrade. Minimal version selection picks the highest version that any module in the graph requires, so a *third* dependency bumping its requirement on your SDK can pull a team onto your new tag without them editing anything. "They opted in" is not a defence. ## Diffing the two surfaces The durable artefact is a diff of the exported API between the two tags: every exported package, type, field, method, function signature and constant. You can approximate it with the documentation output of each version and a plain text diff, or use a dedicated API-diff tool that classifies each change as compatible or incompatible. Either way the output is short and readable, and it is what turns "something broke" into "we added Delete to the Store interface on Tuesday". Run that diff as a gate, not as a forensic tool. Compare the pull request's surface with the surface of the last released tag; if any change is classified incompatible, the pull request needs an explicit decision rather than an approval. ## Fixing it now The immediate fix is a patch release that restores the old surface — put the interface method back into a second interface, restore the removed symbol as a thin wrapper marked `// Deprecated:` — and then retracting the broken tag so no new build selects it. Both matter: the patch unblocks the team that reported it, and the retraction stops the next four teams from hitting the same wall. Telling everyone to pin an older version is not a fix; it is a request that other people work around your mistake. ## Stopping the repeat Three measures, in order of value: - **A canary consumer in CI.** Keep a small module in the library's repository, or a checkout of a real downstream service, that imports the library the way a customer does — implementing your interfaces with its own types, constructing your structs, calling your constructors. Build and test it against every commit. This is the only check that reproduces the importer's compiler. - **An exported-surface diff on every pull request.** Cheap, mechanical, and it turns a class of accident into a conscious decision with a reviewer's name on it. - **Structural prevention.** Keep exported interfaces one or two methods wide, seal the ones you intend to grow with an unexported method, give growable structs an unexported guard field, and make constructors variadic over options so the signature never changes. Most breaks are avoidable by shape rather than by policing. ## The part that is not a tool Also fix the release habit that let it out. Someone tagged a version believing the change was additive. The lesson is that "additive" in Go is a precise, checkable property — does any existing importer's source stop compiling — and not a judgement made by looking at a diff and seeing only added lines. One added line inside an interface block is a breaking change; twenty added lines in a new file are not.

  • Why can a library's own test suite be green while every importer fails to compile?
    In-package tests see unexported identifiers and are compiled against the same source as the change, so an interface and its implementations always agree there. No test represents a type written elsewhere against the previous contract. Only building a separate consumer module — one that implements your interfaces with its own types — exercises the importer's view.
  • The team says they never upgraded your SDK, yet they are on the new tag. How?
    Minimal version selection resolves each module to the highest version any requirement in the graph asks for. Another dependency of theirs bumped its requirement on your SDK, which raised the selected version for the whole build. Publishing a tag therefore does reach people indirectly, which is why retracting a broken one matters more than telling teams to pin.
  • What would you put in CI to catch this class of break before tagging?
    Two gates. A canary consumer module built and tested against HEAD, importing the library exactly as a customer does — its own implementations of your interfaces, its own literals of your structs. And an exported-surface diff against the last released tag, failing the pull request when it reports an incompatible change unless someone explicitly signs off.

saying these in an interview costs you the question

  • Debugs only inside the library, never building a consumer
  • Treats a green library test suite as proof of compatibility
  • Tells downstream teams to pin an old version and calls it fixed
  • Assumes nobody gets the new tag unless they edit go.mod
  • Judges additivity by whether the diff only adds lines