Golang Refactoring
Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe
- Skill
- Workflow
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a rigorous methodology for restructuring Go code while preserving existing behavior. It emphasizes a safety-first approach, requiring a test-based safety net before any modifications. The process relies on automated tooling like gopls for behavior-preserving transforms and mandates a staged PR workflow to ensure changes are reviewable and atomic. It includes specific guidance on mapping blast radius, managing dependencies, and using type aliases for gradual code repair.
What it does
- Maps the blast radius of proposed changes using gopls.
- Enforces a strict separation between structural and behavioral code changes.
- Provides a catalog of Fowler-style refactoring patterns mapped to Go mechanics.
- Automates repetitive code transformations using tools like gofmt, eg, and gopatch.
- Requires human sign-off for high-risk operations like package splits or API changes.
- Maintains a green test suite throughout the refactoring lifecycle.
When to use it
- When functions or types have grown too large or complex to maintain.
- When code smells block the implementation of new features.
- When performing large-scale renames or moving code between packages.
- When planning a multi-step refactor that requires careful sequencing.
- When you need to break import cycles in a Go project.
Advantages
- Reduces the risk of breaking changes through tool-verified transformations.
- Ensures code remains reviewable by enforcing small, atomic PRs.
- Provides a clear, repeatable process for complex structural changes.
- Minimizes merge conflicts by sequencing changes logically.
- Integrates directly with standard Go tooling and development workflows.
Usage examples
- “Refactor the large User service in my Go project to improve maintainability.”
- The agent will initiate a planning phase, map the service's dependencies, identify code smells, and propose a sequence of small, atomic PRs to extract functions or interfaces.
- “Rename the 'Customer' struct to 'Client' across the entire repository.”
- The agent will use gopls to perform a safe, type-aware rename, verify that no compilation errors are introduced, and check for potential desyncs in struct tags or reflection.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/samber/golang-refactoring/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip golang-refactoring.zip -d .agents/skills
Documentation by samber
Community default. A company skill that explicitly supersedes
samber/cc-skills-golang@golang-refactoringskill takes precedence.
Persona: You are a Go refactoring engineer. You never change structure and behavior in the same step — you keep a green test net, prefer behavior-preserving tools over hand-edits, and land changes as small, reviewable PRs.
Thinking mode: Reason as thoroughly as possible for the planning/ordering step — mapping blast radius, sequencing PRs to avoid merge conflicts, and deciding where a refactor can safely go parallel all punish shallow reasoning, since a wrong ordering call surfaces as a broken build or a conflict-riddled merge, not as an obviously wrong plan. On Claude Code, use ultrathink to trigger extended thinking explicitly.
Orchestration mode: Use ultracode/Workflows only for a simple single-pass mechanical sweep — one gofmt -r/eg/modernize fixer applied tree-wide, verified green, with no step depending on another. Do NOT use it for a multi-step refactor needing progressive human review between merges: Workflows run agent-to-agent with no human checkpoint between stages, which is exactly what a staged refactor requires between every merge.
Modes:
Plan mode (mandatory gate before any edit) — use gopls to map structure and blast radius, build a refactoring inventory, decide ordering, and get explicit user sign-off before touching code. See workflow.md.
Execute mode (human-in-the-loop) — one sub-agent, one worktree, one branch, one PR per atomic change, landed on a refactoring branch; parallel when file-disjoint, sequential when overlapping. Dispatch each change to a sub-agent and keep only its result — the orchestrating session's context is what has to last across every row in the inventory. See workflow.md.
Simple-sweep mode — a single mechanical, behavior-preserving transform applied tree-wide; may use
ultracode.Review mode — reviewing a refactoring PR: verify structural/behavioral separation and behavior preservation before approving.
Questions: Sign-off gates in this skill (Plan mode's initial approval, and every mid-refactor checkpoint below) are asked through the environment's question tool, never as plain-text prose the reader might skim past — a refactor is exactly the kind of workflow where an unnoticed "assumed yes" is expensive to undo. These are approval gates on irreversible decisions, not casual clarifying questions, so re-stating "ask via the question tool" at each one below is intentional, not boilerplate.
Dependencies: gopls (primary actuator) — go install golang.org/x/tools/gopls@latest. Optional: golangci-lint, benchstat, deadcode, eg, gopatch. Full gopls setup and MCP registration → See samber/cc-skills-golang@golang-gopls skill — this is the only place this skill explains how to get gopls; every other reference to it in this skill assumes it's already installed.
Go Refactoring — Safe Change at Scale
Refactoring (Fowler) is changing code's internal structure to make it easier to understand or cheaper to modify, without changing observable behavior.
Go tooling can prove several transforms are behavior-preserving by construction — e.g. gopls refuses a Rename rather than risk a broken build.
That guarantee is silent on anything reflection can reach (struct tags,
text/templatefield references) — a safety net still matters.
The Core Loop
Understand → Safety net → Small tool-driven step → Verify → Atomic single-category commit. Repeat.
Understand — map the change's blast radius with gopls (references, call hierarchy, package API) before touching anything.
Safety net — before touching code with inadequate coverage, add tests first.
Gate the strategy on the blast radius's test coverage, not global coverage.
Treat writing that test as your own mechanism for checking the change — not a formality left for the reviewer. A green suite you wrote yourself is what actually lets you tell "this is behavior-preserving" from "I hope this is behavior-preserving."
See safety-net.md for the HIGH/MEDIUM/LOW thresholds and characterization-testing recipes for untested code.
Small tool-driven step — prefer a mechanical, tool-driven transform over a hand-edit. See go-tooling.md and catalog.md.
Verify —
go build ./... && go vet ./... && go test ./...; add-racefor concurrency changes andbenchstat-backed-benchfor hot paths.Atomic single-category commit — the commit is purely structural or purely behavioral, never both.
Hard Rules
Never mix structural and behavioral changes in one commit or PR.
A reviewer scrutinizing a rename for correctness and a reviewer scrutinizing a feature for side effects need different postures.
Mixing them forces one reviewer to wear both hats at once, and the fast, low-scrutiny review a pure rename deserves gets lost.
Split a code move from a code optimization into two sequential PRs, even though both are structural.
They need different verification — the move is proven safe by gopls plus build/test, the optimization needs benchmarks and a closer correctness read.
They touch the same code, so run them one after another rather than in parallel worktrees; parallelizing just moves the conflict to merge time.
Aim for 100–500 lines per PR: small enough to review in one sitting, large enough to still read as one coherent change.
Prefer gopls Rename/Inline over LLM hand-edits.
Both are behavior-preserving by construction — Rename refuses on shadowing, interface-satisfaction breakage, or malformed code rather than silently producing a bad diff; Inline substitutes side-effect-bearing arguments into
vartemporaries rather than duplicating them.A hand-edit across dozens of call sites has no such guarantee and measurably misses cases.
When a change recurs across many sites, generate a rewrite tool instead of hand-editing each site.
Escalate
gofmt -r→eg→gopatch→ ago/analysisfixer, in order of increasing power (see go-tooling.md).A generated tool is reviewable, re-runnable, and testable against golden files — dozens of individual hand-edits are none of those things.
Use a type alias (
type A = B) for every type moved across packages.This is the officially-blessed mechanism for gradual code repair: the old and new names stay interchangeable while callers migrate incrementally, so no commit has to touch every call site at once.
See structural.md.
Break import cycles with a consumer-side interface first, before considering a package split or a shared leaf package.
Go resolves interfaces implicitly, so the producer package never has to import the consumer's interface — the cheapest, most surgical fix.
See structural.md.
Pause for human sign-off before: any cross-package move or package split, any exported-API change or deprecation, any deletion, introducing a new major version, or whenever the code you're about to touch has no tests.
These are the moves a wrong call is expensive to undo.
Grep for tag and reflection references after any rename.
gopls Rename only guards against compilation breakage — it cannot see a struct tag, a
text/templatefield reference, or areflect-driven dispatch that still points at the old name.Renaming a field silently desyncs it from its
json/dbtag.
Load
samber/cc-skills-golang@golang-security(andgolang-safetyfor internal-correctness risk) whenever a step changes code logic, not just its shape.A mechanical, tool-verified transform can't introduce a vulnerability, but a behavioral change can.
Treat "changes what the code does" as the trigger for a security-and-safety pass, not an afterthought reserved for the final review.
Start every step from a clean, committed baseline, and revert rather than debug forward when it goes red.
Version control is the safety net underneath the test safety net.
If a mechanical step leaves
go testred, reverting to the last green commit and re-attempting is faster and safer than patching forward inside a state you no longer fully trust.Commit the moment a step goes green, before starting the next one — that commit is what you'd revert to.
When Not to Refactor
Refactoring is an investment that only pays off if a future change is coming to spend it on. Question it — or skip it — when:
The code works and nothing planned will touch it again.
A stable, rarely-read package earns nothing from being restructured for its own sake.
The risk of even a small staged refactor has to be repaid by an easier next change, and there may not be one.
It's critical production code with no tests. Don't refactor it directly.
The human checkpoint above already requires a characterization-test baseline and explicit sign-off before touching untested code — for a genuinely critical path, treat that gate as non-negotiable, not a formality to rush past.
The deadline is tight.
A staged, human-reviewed refactor needs review bandwidth between every PR.
Starting one under time pressure either stalls (PRs pile up unreviewed) or gets rushed (the review discipline this skill depends on gets skipped to hit the date).
Make the minimal safe change now and stage the larger refactor for when there's room for it.
There's no clear purpose.
"Refactor this" with no reason behind it — no upcoming feature it'll make easier, no bug class it'll close off, no smell a review actually flagged — is refactoring for its own sake.
Confirm the purpose during the planning gate's sign-off rather than assuming one.
Risk Stratification
| Risk | Transforms | Safety requirement |
|---|---|---|
| Low | gopls Rename, Extract Variable/Constant, Inline Variable, gofmt -s, organize imports, local refactor.rewrite.* actions | Build/vet/test after the step is enough |
| Medium | Extract Function/Method (Extract is best-effort — verify comments/behavior survived), Inline Call across packages, single-parameter add/remove, introducing generics | Add or confirm targeted tests over the blast radius first |
| High | Change signature across many callers, moving types/functions across packages, splitting/merging packages, breaking import cycles, exported-API or major-version changes | Full safety net + human checkpoint before landing |
Diagnose: 1- gopls refusing a Rename or Inline is a real semantic hazard, not a tool bug — investigate the shadowing/interface conflict before forcing the change by hand 2- go vet ./... / golangci-lint run flagging a new issue after a step — fix before committing, don't accumulate lint debt mid-refactor 3- go test -race ./... reporting any race — stop, the concurrency behavior changed 4- benchstat old.txt new.txt reporting anything other than ~ on a hot path — stop and revert or optimize, a "refactor" that regresses performance is a behavior change 5- go tool cover -func on the touched packages, scoped with -coverpkg=./... — this is the strategy gate for how aggressively you can proceed (see safety-net.md)
Workflow: Plan → Stage → Land
A refactor of any real size does not land as one commit or even one PR — it lands as an ordered sequence of small, independently reviewable PRs, staged on a refactoring branch, with a human approving each merge.
workflow.md covers the full choreography — read it before planning any multi-step refactor:
the planning gate and refactoring inventory
the three interacting orderings (structural-before-behavioral, conflict-avoidance, dependency order)
the
refactor/<topic>branch and per-change worktree/PR git modelwhen to run steps in parallel versus sequentially
the
// REFACTOR(step N): ...marker conventionwhy Workflows/
ultracodeare the wrong tool for this
Detailed References
workflow.md — the planning gate, PR ordering, git model, parallel/sequential decision, and TODO-marker convention.
catalog.md — the Fowler refactoring catalog mapped to Go, with the code-smell trigger, mechanics, tool, and risk for each entry.
go-tooling.md — gopls code actions, CLI invocation,
gofmt -r,eg,gopatch,go/analysis///go:fix inline,dave/dst, and the deprecated-tool notes.safety-net.md — the coverage-adaptive strategy, characterization/golden-testing libraries, and the verification command reference.
structural.md — breaking import cycles, package-boundary design, type-alias gradual code repair, and exported-API/versioning moves.
Cross-References
→ See
samber/cc-skills-golang@golang-namingskill for what to rename identifiers to — this skill owns how to apply a rename safely at scale.→ See
samber/cc-skills-golang@golang-project-layoutskill for target directory/package layout — this skill owns the mechanics of moving code there without breaking callers.→ See
samber/cc-skills-golang@golang-modernizeskill for version-driven idiom updates (interface{}→any,slices/maps) — a distinct concern from structural refactoring, though it shares the same tool-first discipline.→ See
samber/cc-skills-golang@golang-code-styleskill for control-flow clarity and function-shape rules this skill helps you apply mechanically.→ See
samber/cc-skills-golang@golang-design-patternsskill for target patterns (options struct, DI, consumer-side interfaces) this skill helps you migrate toward.→ See
samber/cc-skills-golang@golang-testingskill for the test-writing practices that make the safety net in this skill trustworthy.→ See
samber/cc-skills-golang@golang-lintskill for configuringgolangci-lint, run here only as a post-step verification gate.→ See
samber/cc-skills-golang@golang-securityskill (andgolang-safety) for reviewing any step that changes code logic, not just its shape.
If you encounter a bug or unexpected behavior in gopls, open an issue at https://github.com/golang/go/issues.
Files
- ATTRIBUTION.mdAdded by AutoGineer911 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md16.0 KiB
- references/catalog.md20.8 KiB
- references/go-tooling.md9.0 KiB
- references/safety-net.md10.0 KiB
- references/structural.md17.6 KiB
- references/workflow.md18.8 KiB
references/structural.md
Structural Constraints: Import Cycles, Package Boundaries, Type Moves, API Evolution
Go enforces a handful of structural rules at compile time that other languages leave to convention or linting. This file covers the load-bearing ones: why import cycles are a hard error rather than a warning, how to design a package boundary so it doesn't need to be redesigned again, the officially-blessed mechanism for moving a type across packages without breaking every caller at once, and how to evolve an exported API without a flag day.
Table of Contents
Breaking Import Cycles
1. Consumer-side interface (dependency inversion)
2. Extract shared types to a new/lower package
3.
internal/packages4. Mediator/bridge package
Package Boundary Design
Splitting a god package
Moving Types Across Packages: Type Aliases for Gradual Code Repair
Exported API Surface and Versioning
init(), Global State, and Package-Level Vars as a Refactoring TargetGenerics — When a Refactor Toward Them Is Warranted
Common Mistakes
Cross-References
Breaking Import Cycles
Go compiles packages leaf-to-root in dependency order: before compiling package
X, the compiler must have already finished compiling everythingXimports, because it needs their compiled type information to type-checkX.An import cycle —
XimportsY,YimportsX(directly or transitively) — has no valid compilation order, sogo buildrejects it outright asimport cycle not allowed. This is not a style preference; there is no fallback behavior to fall back to.
Four strategies fix a cycle, in preference order:
| # | Strategy | Call-site cost | Best when |
|---|---|---|---|
| 1 | Consumer-side interface | None — no call site changes anywhere | The consumer only calls one or two methods on the producer's type |
| 2 | Extract shared type to a leaf package | Import path changes on both sides | Both packages genuinely need the same concrete type, not just its behavior |
| 3 | internal/ package | Import path changes for the shared code only | The shared code should never become part of the public API |
| 4 | Mediator/bridge package | New package, both sides delegate to it | 1–3 don't fit the shape of the coupling |
1. Consumer-side interface (dependency inversion)
The idiomatic first move, and the cheapest one, because it requires zero changes at any call site anywhere in the codebase.
If package
xonly uses behavior from packagey— it calls a method or two ony's concrete type, it doesn't need the type itself — define a small interface inxnaming just those methods.Go's implicit interface satisfaction means
y's existing type already satisfies that interface withoutyimporting anything fromxor even being awarex's interface exists:
// package x (consumer)
type Storer interface {
Store(ctx context.Context, key string, val []byte) error
}
func Process(s Storer) error { /* uses s.Store, no import of package y */ }// package y — unchanged, already satisfies x.Storer implicitly
type Store struct{ /* ... */ }
func (s *Store) Store(ctx context.Context, key string, val []byte) error { /* ... */ }xno longer importsyat all;ynever importedxto begin with.The cycle is gone because one direction of the dependency graph was never real —
xnever neededy's concrete type, only a name for the behavior it called.
2. Extract shared types to a new/lower package
Works because Go's package model is flat within a module — a nested subdirectory is still a fully distinct, independently importable package — so pulling the types both
xandyneed into a small new leaf package that both can import breaks the cycle by construction: the leaf package imports neither.Be honest about the cost: in a real codebase a single cycle can span five or more packages once you trace every type both sides share, so this is the "correct but sometimes painful" option next to the surgical, call-site-free consumer-side interface above.
3. internal/ packages
Share code between related packages without widening the public API.
Anything rooted under an
internal/directory is importable only by packages rooted at the parent of thatinternal/directory — this lets two sibling packages share implementation detail through a commoninternal/package without either becoming part of the module's public surface, and without pulling either sibling into the other.
4. Mediator/bridge package
A last resort when 1–3 don't fit the shape of the dependency: a new package holding the shared functionality that both
xandydelegate to, absorbing the coupling neither side wants to own.Reach for this only after confirming a consumer-side interface can't express the relationship — it usually can.
An internal/ layout for strategy 3 looks like this — note that both billing and shipping can import order/internal/model, but nothing outside the order tree can:
order/
├── billing/
│ └── billing.go // imports order/internal/model
├── shipping/
│ └── shipping.go // imports order/internal/model
└── internal/
└── model/
└── order.go // shared type, invisible outside order/Package Boundary Design
Accept interfaces, return structs.
Accepting a narrow interface as a parameter maximizes what a caller can pass — including a test fake that implements only the one or two methods the function actually calls — without the function ever needing to import the caller's concrete types.
Returning a concrete struct preserves full type information at the call site, which matters because that caller's own consumers get to define their own narrow interface later, on their side, without the original producer ever having had to anticipate what subset of behavior a future caller would need.
Define interfaces where they are consumed, not where they are implemented. The companion rule, and the one that actually prevents cycles rather than just describing good taste.
A consumer package declares the interface naming only the methods it calls; the producer package never imports it, never knows it exists, and satisfies it purely because Go's interface satisfaction is structural.
This is exactly the mechanism in the consumer-side-interface fix above — it isn't a separate rule, it's the same rule applied proactively during design instead of reactively during a cycle break.
Caveat: this is a heuristic, not dogma.
Don't mechanically split every struct into an interface-plus-implementation pair on the theory that it's "more testable" — a concrete struct with no interface is simpler to read, and an interface with exactly one implementation and no test-double need is pure indirection.
Don't return an interface from a constructor just because "it might be more flexible later" — that flexibility has a name (YAGNI) and a cost (the caller loses type information it might have wanted).
→ See
samber/cc-skills-golang@golang-project-layoutskill for the directory/package layout conventions this section assumes, andsamber/cc-skills-golang@golang-design-patternsskill for judging when introducing an interface is the right call versus premature abstraction.
Splitting a god package
Before reaching for a full package split, gopls's
refactor.extract.toNewFilecode action handles the lighter-weight case: moving a top-level declaration to a new file in the same package, which is often enough to make a bloated package navigable without touching its import graph at all.gopls also has an experimental
source.splitPackagecode action that assigns top-level declarations to acyclic components as a starting point for an actual package split — treat its output as a draft partition to review, not a final answer, since it can't know which grouping matches the domain boundaries you actually want.
Moving Types Across Packages: Type Aliases for Gradual Code Repair
This is the single most load-bearing Go-specific refactoring technique, and it exists because of a problem unique to Go's type system: type identity is tied to the fully-qualified name, so
pkg2.Tis a genuinely different type frompkg1.Teven when their underlying definitions are byte-for-byte identical.You cannot assign one to the other, cannot use one where the other is expected, and — unlike a moved function (re-exportable as a thin wrapper) or a moved variable/constant (re-declarable pointing at the new location) — there was no way to migrate callers gradually.
Before Go 1.9 this blocked real large-scale refactors: moving a type meant a single atomic commit touching every call site in the module, because there was no intermediate state where both the old and new names worked.
The fix is type A = B — a type alias, not a new named type.
It declares that
AandBare the same type, not merely convertible: code written against the old name and code written against the new name interoperate exactly, with zero runtime cost and no wrapper function needed anywhere.This was added to the language specifically, per its own design proposal, to "enable gradual code repair during large-scale refactorings, in particular moving a type from one package to another in such a way that code referring to the old name interoperates with code referring to the new name."
The migration recipe, as a fixed sequence:
// Step 1 — new package: introduce the real definition in its new home.
package newpkg
type NewName struct {
// ... real fields
}// Step 2 — old package: replace the original declaration with an alias,
// and mark it deprecated so tooling and IDEs surface the migration.
package oldpkg
// Deprecated: use newpkg.NewName instead.
type OldName = newpkg.NewNameIntroduce the type in its new home package with its real definition.
In the old package, replace the original type declaration with an alias to the new one, and mark it
// Deprecated: use newpkg.NewName instead— a doc comment recognized by tooling and editors.Migrate callers to the new import path incrementally, one PR or one package at a time. Both names remain fully valid and interchangeable throughout this entire period — there is no flag day, no big-bang commit, and no window where some callers are broken while others are fixed.
Once nothing references the old name, delete the alias.
Go 1.24 extended type aliases to carry type parameters, so this same recipe applies unchanged when the type being moved is generic.
Exported API Surface and Versioning
Deprecate before deleting. A doc comment beginning
// Deprecated: ...is recognized by tooling and IDEs and surfaces as a strikethrough or warning at every call site, which gives callers time to migrate before the symbol disappears — deleting an exported identifier outright breaks every downstream module at their nextgo buildwith no warning beforehand.Prefer additive changes. Go's own compatibility promise sets the default posture: prefer additive changes (new function, new optional field, new method) over changing an existing signature, because additive changes never break an existing caller. A change that must break existing callers is not a minor version bump — it's a new major version.
Semantic import versioning is how Go expresses that: a v2+ module carries a
/vNsuffix in both its module path and every importer's import path (e.g.example.com/mod/v2).This is what lets v1 and v2 of the same module coexist in the same build — the two are, to the toolchain, simply different packages with different import paths.
It's what lets callers migrate one package at a time rather than all at once, the same gradual-migration property a type alias gives you within one version, now applied across major versions.
During the transition, the v2 implementation can be written as a thin wrapper over v1 (or vice versa, whichever side holds the canonical logic) to avoid maintaining two divergent copies of the same behavior.
retractdirectives mark an already-shipped defective version as unfit for use ingo.mod—go getandgo list -m -usurface the retraction to anyone who depends on it, without requiring the broken version to be deleted from the module proxy:
module example.com/mod
go 1.24
retract (
v1.2.0 // published with a data-loss bug, see #123
[v1.2.1, v1.2.3] // range retraction — a whole span of bad releases
)init(), Global State, and Package-Level Vars as a Refactoring Target
init()ordering and mutable package-level state are a common source of hidden coupling: a caller of a function has no way to see, from the call site, that the function's behavior depends on some other package'sinit()having already run, or on a global variable some unrelated code path mutated earlier in the program's lifetime.That coupling doesn't show up in a signature, so it doesn't show up in a diff, and it's exactly the kind of dependency that makes a piece of code unsafe to move or test in isolation.
The refactor is toward explicit construction: a constructor function that returns a struct, with dependencies passed in as parameters rather than reached for through a package-level variable or
init()-populated singleton.This doesn't remove the dependency — the code still needs what it needed before — it makes the dependency an explicit, visible seam in the function signature instead of an implicit one buried in the package's
init().→ See
samber/cc-skills-golang@golang-design-patternsskill for constructor and dependency-injection patterns, and safety-net.md in this skill for how this same seam is what a Feathers-style characterization test exploits to get coverage before a risky change.
Generics — When a Refactor Toward Them Is Warranted
Narrower case, briefly: introduce a type parameter only when the logic is genuinely identical across types, not merely similar.
When the behavior differs per type — even by one branch — that's an interface, not a generic; a generic with a type switch inside it is usually an interface wearing a disguise.
A practical litmus test: reach for a generic when a type parameter would eliminate a type assertion that's currently in the code, and the constraint it needs stays narrow —
comparable,cmp.Ordered, or a small one-method interface.Write the concrete version first; refactor to generic only once the duplication is real and already committed in two or more places, not anticipated for a future third caller that may never arrive.
As of this writing, Go has no method-level type parameters, which blocks fluent generic method chaining (
Mapreturning a differently-typed receiver) as a design option — plan around that limitation rather than discovering it mid-refactor.Verify a generics migration the same way as any other refactor, no special-casing:
go build ./... && go vet ./...plus the full test suite for the touched packages.
Common Mistakes
| Mistake | Fix | Why |
|---|---|---|
Moving a type by defining type OldName NewName (a new named type) instead of type OldName = NewName | Use = — a real type alias | Without = this declares a distinct type; every existing value of the old type now fails to assign to the new one, which is the exact break the alias was supposed to avoid |
| Breaking a cycle by moving the producer's concrete type into the consumer's package | Define the interface in the consumer instead, leave the producer's type where it is | Moving the concrete type usually just relocates the cycle to whatever else the producer's type depends on |
| Deleting an exported symbol in the same PR that deprecates it | Deprecate first, land, wait for a release cycle, delete later | Callers outside the module have no chance to react to a deprecation notice they never saw before the symbol vanished |
Bumping a module to v2 without adding /v2 to the module path | Add the /v2 suffix to both go.mod's module line and every import path | Without the suffix, Go's module resolution can't tell the new major version apart from the old one, and existing v1 importers silently get pulled onto breaking code on their next go get -u |
| Reaching for a generic the first time a second, similar-looking function appears | Wait for a third real occurrence with identical logic, or an existing type assertion the generic would remove | Two occurrences are often coincidentally similar rather than logically identical; a premature generic ossifies an abstraction around a coincidence |
Cross-References
→ See
samber/cc-skills-golang@golang-project-layoutskill for directory/package layout conventions.→ See
samber/cc-skills-golang@golang-design-patternsskill for when an interface is the right design choice versus premature abstraction, and for constructor/DI patterns.→ See catalog.md in this skill for the Fowler catalog entries (Extract Interface, Change Function Declaration, Move Function) these structural moves build on.
→ See workflow.md in this skill for staging a cross-package move or package split as an ordered sequence of small PRs.
Requirements and compatibility
Written for: Claude Code, OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Claude Code frontmatter
The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang. Requires gopls and git.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- samber
- Publisher
- Samuel Berthe (Community)
- Distributed by
- Samuel Berthe
- Source
- samber/cc-skills-golang · skills/golang-refactoring · commit bb1d43d
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-refactoring.zip