Generated banner for Golang Refactoring
Generated logo for Golang Refactoring

Golang Refactoring

Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe

  • Skill
  • Workflow
  • Software development
  • MIT
  • No issues found

From the author

Golang refactoring — safe, at-scale restructuring of existing Go code: a coverage-adaptive safety net, behavior-preserving transforms (gopls Rename/Extract, `gofmt -r`, `gopatch`), the Fowler catalog mapped to Go, breaking import cycles, and small stacked PRs. Apply when a function or type has grown too large, a code smell blocks a feature, or the user asks to refactor Go code — also for renaming at scale, extracting functions or interfaces, moving code between packages, or planning a multi-step refactor. Target styles owned elsewhere → See `samber/cc-skills-golang@golang-naming` (renames), `samber/cc-skills-golang@golang-project-layout` (splits), `samber/cc-skills-golang@golang-modernize` (idioms), `samber/cc-skills-golang@golang-code-style` (control flow), `samber/cc-skills-golang@golang-design-patterns` (patterns/DI).
— the description by samber

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

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/samber/golang-refactoring/download
  2. Unzip 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-refactoring skill 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/template field references) — a safety net still matters.

The Core Loop

Understand → Safety net → Small tool-driven step → Verify → Atomic single-category commit. Repeat.

  1. Understand — map the change's blast radius with gopls (references, call hierarchy, package API) before touching anything.

  2. 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.

  3. Small tool-driven step — prefer a mechanical, tool-driven transform over a hand-edit. See go-tooling.md and catalog.md.

  4. Verify — go build ./... && go vet ./... && go test ./...; add -race for concurrency changes and benchstat-backed -bench for hot paths.

  5. 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 var temporaries 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 → a go/analysis fixer, 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/template field reference, or a reflect-driven dispatch that still points at the old name.

    • Renaming a field silently desyncs it from its json/db tag.

  • Load samber/cc-skills-golang@golang-security (and golang-safety for 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 test red, 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

RiskTransformsSafety requirement
Lowgopls Rename, Extract Variable/Constant, Inline Variable, gofmt -s, organize imports, local refactor.rewrite.* actionsBuild/vet/test after the step is enough
MediumExtract Function/Method (Extract is best-effort — verify comments/behavior survived), Inline Call across packages, single-parameter add/remove, introducing genericsAdd or confirm targeted tests over the blast radius first
HighChange signature across many callers, moving types/functions across packages, splitting/merging packages, breaking import cycles, exported-API or major-version changesFull 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 model

    • when to run steps in parallel versus sequentially

    • the // REFACTOR(step N): ... marker convention

    • why Workflows/ultracode are 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-naming skill for what to rename identifiers to — this skill owns how to apply a rename safely at scale.

  • → See samber/cc-skills-golang@golang-project-layout skill for target directory/package layout — this skill owns the mechanics of moving code there without breaking callers.

  • → See samber/cc-skills-golang@golang-modernize skill 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-style skill for control-flow clarity and function-shape rules this skill helps you apply mechanically.

  • → See samber/cc-skills-golang@golang-design-patterns skill for target patterns (options struct, DI, consumer-side interfaces) this skill helps you migrate toward.

  • → See samber/cc-skills-golang@golang-testing skill for the test-writing practices that make the safety net in this skill trustworthy.

  • → See samber/cc-skills-golang@golang-lint skill for configuring golangci-lint, run here only as a post-step verification gate.

  • → See samber/cc-skills-golang@golang-security skill (and golang-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

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/ packages

    • 4. 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 Target

  • Generics — 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 everything X imports, because it needs their compiled type information to type-check X.

  • An import cycle — X imports Y, Y imports X (directly or transitively) — has no valid compilation order, so go build rejects it outright as import 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:

#StrategyCall-site costBest when
1Consumer-side interfaceNone — no call site changes anywhereThe consumer only calls one or two methods on the producer's type
2Extract shared type to a leaf packageImport path changes on both sidesBoth packages genuinely need the same concrete type, not just its behavior
3internal/ packageImport path changes for the shared code onlyThe shared code should never become part of the public API
4Mediator/bridge packageNew package, both sides delegate to it1–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 x only uses behavior from package y — it calls a method or two on y's concrete type, it doesn't need the type itself — define a small interface in x naming just those methods.

  • Go's implicit interface satisfaction means y's existing type already satisfies that interface without y importing anything from x or even being aware x'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 { /* ... */ }
  • x no longer imports y at all; y never imported x to begin with.

  • The cycle is gone because one direction of the dependency graph was never real — x never needed y'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 x and y need 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 that internal/ directory — this lets two sibling packages share implementation detail through a common internal/ 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 x and y delegate 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-layout skill for the directory/package layout conventions this section assumes, and samber/cc-skills-golang@golang-design-patterns skill 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.toNewFile code 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.splitPackage code 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.T is a genuinely different type from pkg1.T even 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 A and B are 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.NewName
  1. Introduce the type in its new home package with its real definition.

  2. 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.

  3. 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.

  4. 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 next go build with 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 /vN suffix 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.

  • retract directives mark an already-shipped defective version as unfit for use in go.mod — go get and go list -m -u surface 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's init() 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-patterns skill 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 (Map returning 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

MistakeFixWhy
Moving a type by defining type OldName NewName (a new named type) instead of type OldName = NewNameUse = — a real type aliasWithout = 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 packageDefine the interface in the consumer instead, leave the producer's type where it isMoving 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 itDeprecate first, land, wait for a release cycle, delete laterCallers 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 pathAdd the /v2 suffix to both go.mod's module line and every import pathWithout 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 appearsWait for a third real occurrence with identical logic, or an existing type assertion the generic would removeTwo 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-layout skill for directory/package layout conventions.

  • → See samber/cc-skills-golang@golang-design-patterns skill 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