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://mp-staging.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/safety-net.md
The Coverage-Adaptive Safety Net
The right amount of caution before a refactor is not a fixed policy — it is gated on how well-tested the blast radius already is, not on the project's global coverage number. A codebase sitting at 90% coverage overall can still have the one function you're about to touch at 0%, and a codebase at 30% overall can have your target function fully pinned by table-driven tests. Measure the code you're actually going to change, then pick the tier below.
The Three Tiers
HIGH coverage on the blast radius (roughly ≥80% function coverage via
go tool cover -func) — refactor aggressively with tools, and trust the green bar.Prefer gopls Rename/Inline/Extract,
eg/gofmt -rfor bulk mechanical changes, and generatedgo/analysisfixers over hand-edits.Run the fast net (build/vet/test) after each step, and escalate to
-race/-benchonly when the step touches concurrency or a hot path.Larger steps are acceptable here because a well-covered blast radius means the net catches a regression within one test run — the cost of being wrong is cheap and immediate, so there is little reason to slow down.
MEDIUM coverage (~40-80%) — harden the blast radius before refactoring it, not after.
First measure exactly what the change touches: gopls references and call hierarchy tell you the real call graph, not the one you remember from reading the code (see the planning gate in workflow.md).
Add targeted table-driven or golden tests covering precisely those paths, confirm they pass against the current code, and only then refactor.
The step that's easy to skip and shouldn't be: re-run with
-coverprofileafterward and check that the new tests actually exercise the lines you're about to change. A test that imports the right package but never reaches the branch you're editing gives false confidence — it looks like safety net from the outside and catches nothing.
LOW/ZERO coverage (<40%, or the specific touched lines are uncovered even if the package average looks fine) — Feathers mode — write characterization (a.k.a. golden or pinning) tests first, capturing what the code actually does today, warts and all, before changing a single line.
This is deliberately not a correctness test — you are not asserting the code is right, only recording what it currently does so a refactor can be checked against it.
Find a seam (below) and introduce the minimum needed to make the code testable.
Restrict yourself to the safest, tool-verified refactorings only — gopls Rename and Inline, both behavior-preserving by construction — and avoid Extract or any cross-package move until a real net exists to check them against.
Prefer Sprout/Wrap (see catalog.md) to add new behavior in a new, tested function rather than editing untested code in place.
Running
deadcode -testfirst is worth the two minutes — some of what looks like "untested code that needs a net" turns out to be exported API nothing actually calls, in which case the honest fix is deletion, not testing.
| Tier | Blast-radius coverage | Strategy | Allowed transforms |
|---|---|---|---|
| High | ≥80% function coverage | Refactor first, verify after each step | gopls Rename/Inline/Extract, eg/gofmt -r, generated go/analysis fixers |
| Medium | ~40-80% | Harden the touched paths, confirm green, then refactor | Same as High, once targeted tests exist and -coverprofile confirms they hit the touched lines |
| Low/Zero | <40%, or the touched lines specifically | Characterize first (Feathers mode), introduce a seam, refactor last | gopls Rename/Inline only; Sprout/Wrap for new behavior; no Extract or cross-package move until a net exists |
Diagnose: 1- go test -covermode=atomic -coverpkg=./... -coverprofile=cover.out ./... — runs the suite and produces a coverage profile scoped to the blast radius's packages 2- go tool cover -func=cover.out — ranks every function by coverage percentage; scan this for the specific functions you're about to touch, not the package-level average 3- go tool cover -html=cover.out — a visual red/green view of the exact touched lines, useful when -func's percentage for a function is ambiguous about which branches are actually green
Two caveats worth internalizing before trusting any number this produces:
Go's coverage is statement coverage, not branch coverage — a line inside an
ifblock that ran once counts as fully covered even if theelsenever executed and even if aswitchonly ever hit onecase. A function reporting 100% can still have an untested branch; treat the percentage as a floor on how much is exercised, not proof that the logic is correct, and read the actual branches in the code you're about to touch rather than trusting the summary.go test ./...silently drops any package that has no_test.gofile from the aggregate — it isn't counted as 0%, it simply isn't in the report at all, which makes an untested package invisible instead of visibly red. Passing-coverpkg=./...(as in the Diagnose command above) forces every package in the module into the profile so a silently-untested dependency doesn't slip past the tier decision unnoticed.
Seams — What to Introduce When There's No Net Yet
A seam, in Michael Feathers's sense, is a place in the code where you can alter behavior without editing that exact spot.
Seams are how Feathers mode gets a fake into a test without first performing the larger refactor the test is meant to protect against.
Two seam types matter in Go:
An object seam is an interface, or a function-typed field or parameter, injected at the point of construction — a test substitutes a fake implementation through that injection point instead of exercising the real dependency. This is the seam type that matters most in Go, because interfaces are satisfied implicitly: introducing one at the point of use requires touching only the consumer, never the producer package, which means you can add a seam to legacy code without an invasive edit to whatever it depends on.
A link/build-tag seam swaps an entire implementation at build time via
//go:buildconstraints; it's used far more rarely, mostly for platform- or environment-specific substitutions where an interface would be overkill.
The enabling move for untested code with no seam yet: extract the smallest possible interface — often just one method — at the exact call site where the untested code depends on something external (a database client, the filesystem, a clock), and inject the concrete implementation through a constructor parameter instead of constructing it inline.
This single move does two things at once: it breaks a potential import cycle between the consumer and whatever concrete type it depended on, and it opens the door for a fake in a characterization test, without requiring any change to the producer side at all.
// Before — no seam: NewReport constructs its own client, so a test
// exercising Generate has no way to substitute a fake and is stuck
// hitting a real database.
func NewReport(dsn string) *Report {
db, _ := sql.Open("postgres", dsn)
return &Report{db: db}
}
func (r *Report) Generate(ctx context.Context, id int) (Summary, error) {
row := r.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
// ...
}
// After — a one-method interface extracted at the point of use;
// the concrete *sql.DB already satisfies it implicitly, so the
// producer package needs no change at all.
type rowQuerier interface {
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
func NewReport(db rowQuerier) *Report {
return &Report{db: db}
}
func (r *Report) Generate(ctx context.Context, id int) (Summary, error) {
row := r.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id)
// ... unchanged — a characterization test can now inject a fake rowQuerier
}→ See samber/cc-skills-golang@golang-design-patterns skill for constructor and dependency-injection patterns this move builds on, and catalog.md in this skill for the Sprout/Wrap mechanics that typically pair with a freshly introduced seam.
Verification Command Reference
This is the fast net from the Core Loop in SKILL.md, escalated only as far as the change actually requires:
go build ./... # fastest gate — compile errors
go vet ./... # correctness checks the compiler doesn't do
go test ./... # full test suite
go test -run TestName ./pkg/... # target one test while iterating
go test -race ./... # concurrency changes — see samber/cc-skills-golang@golang-testing for race-detector and testing/synctest mechanics
go test -covermode=atomic -coverpkg=./... -coverprofile=cover.out ./...
go tool cover -func=cover.out # per-function and total coverage, ranked
go tool cover -html=cover.out # visual red/green source view
go test -bench=. -benchmem -count=10 > new.txt # capture before AND after with the same command, then:
benchstat old.txt new.txt # `~` means no statistically significant difference — the desired result for a behavior-preserving refactor; anything else is a signal to stop and investigate, not noise to shrug offCross-References
→ See
samber/cc-skills-golang@golang-testingskill for general test-writing craft, race-detector mechanics, andtesting/synctest— this file assumes them as a baseline and only covers when a refactor's safety net needs to reach for them.→ See
samber/cc-skills-golang@golang-benchmarkskill for interpreting abenchstatdelta and the full profiling methodology.catalog.md — the Fowler refactoring catalog mapped to Go, including the Sprout/Wrap entries referenced in the Feathers-mode and seams sections above.
workflow.md — the planning gate that measures the blast radius this file's tiers are gated on, and the human-checkpoint rule for touching untested code.
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