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/catalog.md

The Fowler Catalog, Mapped to Go

Each entry below follows the same structure: Motivation (why the refactoring earns its keep), Smell trigger (the code shape that signals it's time), Go mechanics (what the transform actually looks like in Go), Tool (what performs it), and Risk (matching the Risk Stratification table in SKILL.md). Entries are grouped by family, following the shape of Fowler's Refactoring catalog.

Table of Contents

  • Extract Function / Extract Method

  • Inline Function / Inline Call

  • Extract Variable / Inline Variable, Extract Constant

  • Rename

  • Change Function Declaration (Signature)

  • Move Function / Move Field / Move Type

  • Split Package / Merge Package

  • Replace Nested Conditional with Guard Clauses

  • Introduce Parameter Object

  • Replace Conditional with Polymorphism

  • Hide Delegate / Remove Middle Man

  • Sprout Method / Wrap Method

  • Replace Temp with Query

  • Smell → Refactoring Quick Reference

  • Cross-References

Extract Function / Extract Method

  • Motivation: A function doing more than one job is harder to name, test, and reuse than two functions each doing one job — splitting it restores a name for the piece that was previously anonymous.

  • Smell trigger: Long Function — a function whose body mixes several levels of abstraction, or where a comment introduces a block that could instead be the function's name.

  • Go mechanics: Select the statements to extract; the tool infers the parameter list from free variables and the return list from variables used after the extracted block. For a method, it additionally infers the receiver.

  • Tool:

gopls codeaction -exec -kind=refactor.extract.function file.go:#start,#end
gopls codeaction -exec -kind=refactor.extract.method   file.go:#start,#end

Editor-integrated as "Extract function"/"Extract method" in the code actions menu.

  • Risk: Medium. gopls's own documentation describes Extract as "considerably less rigorous" than Rename or Inline, and it is known to drop comments attached to the extracted statements (golang/go#20744). Always diff the result and re-read the extracted body — don't trust it as behavior-preserving by construction the way Rename and Inline are.

Inline Function / Inline Call

  • Motivation: A wrapper that no longer adds a distinct name, or a level of indirection that has stopped paying for itself, is pure navigation overhead for the reader — inlining removes the detour.

  • Smell trigger: A trivial one-line forwarding function, or a Middle Man that has accreted no behavior of its own since it was introduced.

  • Go mechanics: The call site is replaced with the callee's body, with real substitution rather than naive text-splicing: arguments that have side effects are hoisted into var temporaries instead of being duplicated wherever the parameter is used, implicit conversions at the call boundary are made explicit, and a callee body containing defer is wrapped in an immediately-invoked function literal so the deferred call still fires at the right point. Inline cannot cross a dynamic dispatch (interface method call, function value) or inline a generic function.

  • Tool:

gopls codeaction -exec -kind=refactor.inline.call file.go:#offset
  • Risk: Low. Alongside Rename, this is one of the two gopls operations that is provably behavior-preserving by construction — it refuses rather than produce a semantically wrong inline.

Extract Variable / Inline Variable, Extract Constant

  • Motivation: A repeated or unexplained expression forces every reader to re-derive its meaning at each occurrence; giving it a name states the meaning once.

  • Smell trigger: The same non-trivial expression appears more than once, or a single occurrence is opaque enough that a reader has to pause and work out what it computes.

  • Go mechanics: Extract Variable introduces a := binding immediately above the first use; the -all variant rewrites every syntactic occurrence of the expression in scope to reference the new variable. Extract Constant does the same for a literal that deserves a name and compile-time immutability. Inline Variable is the reverse: substitute the variable's value at its use site(s) and remove the binding.

  • Tool:

gopls codeaction -exec -kind=refactor.extract.variable     file.go:#start,#end
gopls codeaction -exec -kind=refactor.extract.variable-all file.go:#start,#end
gopls codeaction -exec -kind=refactor.extract.constant     file.go:#start,#end
gopls codeaction -exec -kind=refactor.inline.variable      file.go:#offset
  • Risk: Low.

Rename

  • Motivation: A misleading identifier costs every future reader the same confusion, repeatedly — fixing the name once removes that tax for good. A naming fix is often the trigger for an entire refactor, because a name that's wrong for the current shape of the code cascades into every call site that reads it.

  • Smell trigger: Any identifier — variable, function, type, field, package — whose name no longer describes what it holds or does.

  • Go mechanics:

    • gopls Rename is workspace-wide: it updates every reference across every file and package that imports the renamed symbol, and it type-checks the result.

    • It refuses rather than proceed when the rename would introduce a shadowing conflict, when renaming a method would break an interface satisfaction relationship elsewhere in the workspace, or when the surrounding code doesn't currently type-check.

    • Two special cases are easy to get backwards: renaming the p in package p moves the entire package directory and rewrites every import path that referred to it; renaming a method's receiver declaration (the s in func (s *Store) Get(...)) propagates to every method of that type, while renaming a single use of a receiver variable inside one method body touches only that method.

  • Tool:

gopls rename file.go:#offset newName

Editor-integrated as "Rename Symbol" (F2 in most gopls-backed editors).

  • Risk: Low — but treat any refusal as a real semantic hazard to investigate, not friction to route around by hand-editing instead.

→ See samber/cc-skills-golang@golang-naming skill for what to rename identifiers to. This entry covers only how to apply the rename safely at scale.

Change Function Declaration (Signature)

  • Motivation: A parameter list that has grown past what the function actually needs, or that no longer matches how the function is used, makes every call site harder to read and easier to call wrong.

  • Smell trigger: Long Parameter List, or a single parameter that has stopped being relevant to the function's job.

  • Go mechanics:

    • gopls has partial, single-purpose support: removing a parameter nobody passes meaningfully, or reordering two adjacent parameters, are each mechanical.

    • Adding a new parameter across every call site, or a broader signature rewrite, is not a single gopls action today. For that case, use an eg-staged migration: write a new function variant with the added or generalized parameter, migrate call sites to the new form via an eg template (eg -t template.go -w ./...), verify, then Rename the old function out of the way (or delete it) once nothing calls it anymore.

    • This keeps the migration mechanical and reviewable instead of a set of manual edits scattered across the tree.

  • Tool:

gopls codeaction -exec -kind=refactor.rewrite.removeUnusedParam file.go:#offset
gopls codeaction -exec -kind=refactor.rewrite.moveParamLeft      file.go:#offset
gopls codeaction -exec -kind=refactor.rewrite.moveParamRight     file.go:#offset
eg -t template.go -w ./...   # staged migration for adding/generalizing a parameter
  • Risk: Medium for a single-parameter add/remove with a handful of call sites; High when the signature changes across many callers with no mechanical one-shot action available.

→ See samber/cc-skills-golang@golang-design-patterns skill for converting a long parameter list into an options struct rather than just reordering or trimming it.

Move Function / Move Field / Move Type

  • Motivation: A function, field, or type placed in the wrong package is a standing invitation to reach across a boundary that shouldn't be crossed — moving it to where its data lives removes that temptation.

  • Smell trigger: Feature Envy — a function reads and manipulates another package's data more than its own — or a type whose responsibilities clearly belong to a different package than the one it currently lives in.

  • Go mechanics:

    • No one-shot gopls action moves a symbol across package boundaries yet.

    • The gradual-repair sequence: introduce the symbol in its new home; leave a type alias (type Old = pkg.New) for a moved type, or a thin wrapper function/forwarding variable for a moved function, in the old location so both the old and new names keep working; migrate callers to the new name incrementally, one small commit at a time; delete the old name (and the alias/wrapper) only once nothing references it.

    • Splitting a large file into a new file within the same package is a distinct, one-shot gopls action.

  • Tool:

gopls codeaction -exec -kind=refactor.extract.toNewFile file.go:#start,#end   # same-package file split

Cross-package moves are the manual type-alias/wrapper sequence above — there is no equivalent one-shot command.

  • Risk: High.

→ See samber/cc-skills-golang@golang-project-layout skill for target package layout. See structural.md for the full type-alias gradual-repair recipe.

Split Package / Merge Package

  • Motivation: A package that has grown to serve many unrelated responsibilities is hard to review, test, and reason about as a unit — splitting it along its natural seams restores each piece's ability to be understood on its own. The reverse move, merging, is occasionally right when two packages have become so mutually dependent that the boundary between them adds ceremony without adding isolation.

  • Smell trigger: A "god package" (the package-level analogue of a Large Class), or Divergent Change — the same package keeps changing for several unrelated reasons because it hosts several unrelated concerns.

  • Go mechanics:

    • gopls exposes an experimental code action that partitions a package's top-level declarations into groups with no cyclic dependency between them, proposing a split into acyclic components. Treat its proposal as a starting point to review, not a final answer — package boundaries also encode API and ownership decisions the tool can't see.

    • Merging has no dedicated tool: move the declarations into the target package and resolve whatever import cycle results with a consumer-side interface (see structural.md) before falling back to a bigger restructuring.

  • Tool:

gopls codeaction -exec -kind=source.splitPackage file.go   # experimental; splitting only
  • Risk: High.

→ See samber/cc-skills-golang@golang-project-layout skill for target package layout.

Replace Nested Conditional with Guard Clauses

  • Motivation: Deep if/else nesting forces a reader to hold every outer condition in mind to understand an inner branch; guard clauses let each precondition exit on its own line and leave only the main path in the body.

  • Smell trigger: An if/else if/else chain, or nested if blocks, where most branches are actually preconditions or error cases rather than alternatives of equal weight.

  • Go mechanics: Each branch that isn't the primary path becomes an early return/continue/break, flattening the remaining logic to a single nesting level. gopls's invert-if action flips a single if/else in place and is a useful mechanical building block for this, but it operates on one condition at a time — turning a whole nested chain into guard clauses is a structural pass with the assistance of that action, not a single tool invocation.

  • Tool:

gopls codeaction -exec -kind=refactor.rewrite.invertIf file.go:#offset
  • Risk: Low.

→ See samber/cc-skills-golang@golang-code-style skill for the full early-return style rule this refactor works toward.

Introduce Parameter Object

  • Motivation: When the same group of parameters keeps showing up together across several functions, the group itself is a concept that deserves a name and a single place to add validation or a new field.

  • Smell trigger: Data Clumps — the same cluster of parameters (or fields) recurring together — or a Long Parameter List where several parameters are conceptually one unit.

  • Go mechanics: Define a struct that holds the recurring parameter group, then change each affected function's signature to take the struct instead of the individual values. gopls has a planned "Extract parameter struct" action tracked as golang/go#65552; as of this writing it is not yet generally available. Until it lands, treat this as an Extract-Variable-style manual step (define the struct, construct it at each call site) followed by a signature change at each call site — the same staged approach as Change Function Declaration above.

  • Tool: No dedicated gopls action yet (golang/go#65552 tracks it). Combine manual struct extraction with the signature-change tooling above.

  • Risk: Medium.

→ See samber/cc-skills-golang@golang-design-patterns skill for functional options as the alternative when the struct exists to configure construction rather than to group a plain data clump.

Replace Conditional with Polymorphism

  • Motivation: A switch on a type or state constant that shows up in more than one place forces every new case to be added in lock-step at every one of those places; an interface with one implementation per case collects each case's behavior in one place instead.

  • Smell trigger: Repeated Switches — the same switch on a type tag or state value recurs at multiple call sites, and adding a new case means finding and updating every one of them.

  • Go mechanics: Define an interface with one method per behavior that currently varies by case, then give each case its own implementing type. Callers that used to switch on the tag now just call the interface method, and dispatch happens through Go's interface mechanism instead of a repeated switch.

  • Tool: Manual — no mechanical tool performs this transform; use Extract Function/Interface steps to carve out each case's behavior into its own type, then Rename/Inline to clean up the seams.

  • Risk: Medium.

→ See samber/cc-skills-golang@golang-design-patterns skill for the target interface-dispatch pattern.

Hide Delegate / Remove Middle Man

  • Motivation: A caller that reaches through one object to call methods on another is coupled to both the shape of the first object and the shape of everything downstream of it; hiding the delegate collapses that chain to a single call. The opposite failure — a method that does nothing but forward to another object — adds a hop with no behavior to show for it, and removing it lets callers reach the real implementation directly.

  • Smell trigger: Message Chains (a.B().C().D() reaching through several objects to get to the one that matters) call for Hide Delegate; a Middle Man (a method whose entire body is return x.SameMethod(...)) calls for Remove Middle Man.

  • Go mechanics: Struct embedding is Go's usual mechanism for Hide Delegate — embedding the delegate promotes its methods onto the containing type, so callers invoke them directly on the outer struct instead of chaining through an accessor. Remove Middle Man is the inverse: delete the pass-through method (after an Inline Call at each of its call sites, or a Rename-and-redirect) and let callers reach the real implementation directly, whether through an exported field or the embedded type.

  • Tool: Manual for the embedding decision; gopls codeaction -exec -kind=refactor.inline.call mechanizes the "delete the middle man" step once callers are ready to call through directly.

  • Risk: Low-Medium.

Sprout Method / Wrap Method

  • Motivation: Feathers's core insight: code with no tests is code you can't safely edit in place, because there's no way to notice if you broke it. Both techniques add new behavior without touching the untested code directly, so the new behavior can be tested even when the old code still can't be.

  • Smell trigger: A need to add new behavior to a function or method that currently has little or no test coverage.

  • Go mechanics:

    • Sprout Method — write the new behavior as a brand-new, fully-tested function or method, and call it from the one call site that needs it, leaving the original code otherwise untouched.

    • Wrap Method — rename the original method (e.g. Save → saveInternal), then add a new method with the old name (Save) that calls the renamed original and adds the new behavior around it; this is a manual decorator, since Go has no built-in method-wrapping mechanism.

  • Tool: gopls rename for the rename step in Wrap Method; the new code itself is hand-written and tested like any other new function.

  • Risk: Low — by design, this is how Feathers's approach avoids touching untested code directly.

→ See safety-net.md for when low/zero coverage should push you toward this pattern instead of editing in place.

Replace Temp with Query

  • Motivation: A local variable computed once and reused several times hides the fact that it's a derived value — reading the variable's name doesn't tell you it's a computation, only that it's a value, which obscures where the real logic lives.

  • Smell trigger: A local variable assigned once from an expression and then read multiple times later in the function, where the variable's name doesn't make clear it's derived rather than an input.

  • Go mechanics: Replace the variable with a call to a small unexported function or method that recomputes the same expression, then remove the variable and each of its reads becomes a call instead. This is a manual step — Go's function-call cost is cheap enough that the transform is rarely a performance concern, so the only judgment call is readability, not cost.

  • Tool: Manual. If the resulting function is only ever called once, gopls's inline action can fold it back at any point without losing the readability gain that motivated extracting it.

  • Risk: Low.

Smell → Refactoring Quick Reference

SmellGo-specific fix
Long FunctionExtract Function/Method
Large/God PackageSplit Package
Long Parameter ListIntroduce Parameter Object, or an options struct
Data ClumpsExtract a struct for the recurring group
Primitive ObsessionIntroduce a named type instead of a bare string/int
Divergent Change (one package, many unrelated reasons to change)Split Package
Shotgun Surgery (one conceptual change touches many packages)Move Function/Field to consolidate the concept into one package
Feature EnvyMove Function to the package whose data it actually uses
Repeated SwitchesReplace Conditional with Polymorphism
Message ChainsHide Delegate
Middle ManRemove Middle Man

Divergent Change and Shotgun Surgery point to opposite fixes even though both are "too much change ripples around": Divergent Change is one package changing for many reasons — the fix is to split it apart. Shotgun Surgery is one reason to change rippling across many packages — the fix is to consolidate that concept into one package so a single change touches one place.

Cross-References

  • → See structural.md for the full type-alias gradual-repair recipe used by Move Function/Field/Type, and for breaking import cycles ahead of a package split or merge.

  • → See safety-net.md for the coverage-adaptive strategy that determines when Sprout/Wrap Method should replace an in-place edit.

  • → See go-tooling.md for the full gopls code-action reference, gofmt -r, eg, and gopatch invocation details.

  • → See samber/cc-skills-golang@golang-naming skill for what to rename identifiers to.

  • → See samber/cc-skills-golang@golang-project-layout skill for target package/directory layout.

  • → See samber/cc-skills-golang@golang-design-patterns skill for options structs, consumer-side interfaces, and interface-dispatch patterns referenced throughout this catalog.

  • → See samber/cc-skills-golang@golang-code-style skill for the guard-clause/early-return style rule.

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