7.9 KiB
Go Tooling for Refactoring
This file is the tool reference for samber/cc-skills-golang@golang-refactoring: every mechanical-rewrite tool worth reaching for, from the primary actuator (gopls) down to hand-rolled go/analysis fixers, ordered so you can pick the least-powerful tool that solves the problem. See catalog.md for which tool maps to which Fowler refactoring, and workflow.md for how a tool-driven step fits into the staged-PR process.
1. gopls — the Primary Actuator
- gopls performs most of this skill's Low- and Medium-risk transforms — Rename, Inline, Extract, and the
refactor.rewrite.*family. - See the Risk Stratification table in SKILL.md and the
Tool:line on each entry in catalog.md for which gopls action maps to which refactoring. - The full code-action reference, CLI invocation, safety behavior, and MCP server setup belong to
samber/cc-skills-golang@golang-gopls— this file covers the tools that skill doesn't.
2. Bulk Mechanical Rewrite Tools
When a change recurs across many call sites, reach for a generated rewrite tool instead of hand-editing each one. The tools below are ordered by increasing power — start at the top and move down only when the current tool's limits block the rewrite you need.
gofmt -r — syntactic, single-expression
- Purely syntactic and type-unaware: it matches expression shape, not the types involved, and a rewrite is limited to a single expression.
- Wildcards are single lowercase identifiers that match any sub-expression.
gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)' -w file.go
gofmt -r 'bytes.Compare(a, b) != 0 -> !bytes.Equal(a, b)' -w file.go
gofmt -s -w file.go # -s additionally simplifies (e.g. s[a:len(s)] -> s[a:])
gofmt -l . # list non-conforming files — useful as a CI gate
gofmt -d file.go # show the diff without writing
- Because it can't see types, it cannot target "every
bytes.Comparecall on a[]byte, but not a look-alike function of the same name from another package" — that distinction needseg.
eg — type-aware, example-based
golang.org/x/tools/cmd/eg rewrites by example, à la Refaster: a template file declares before/after functions of identical type, each with a single-expression body.
// template.go
package template
import (
"errors"
"fmt"
)
func before(s string) error { return fmt.Errorf("%s", s) }
func after(s string) error { return errors.New(s) }
eg -t template.go -w ./...
- Matching is semantic, not textual —
func(x int)in the template also matchesfunc(y int)at the call site, sinceegmatches by type and structure. - Limits: expressions only, no statements or function-literal patterns; a rewrite can't change the expression's type; imports are added but never removed (run
goimportsafterward); and duplicating a wildcard variable in theaftertemplate duplicates whatever side effect the matched expression had.
gopatch — statement-level, import-aware
github.com/uber-go/gopatch operates at the statement level and tracks imports as part of the patch, so it can work on code that doesn't fully compile mid-refactor — useful for the messy in-between states of a large migration. A patch declares metavariables between @@ markers, then a diff-like body:
@@
var x expression
@@
-errors.New(fmt.Sprintf(x))
+fmt.Errorf(x)
gopatch -p rewrite.patch ./...
gopatch -d -p rewrite.patch ./... # dry-run — show the diff only
- Still beta; the project frames it as covering roughly 80% of a migration, not 100%.
- A pattern can't match an import statement in isolation — something must follow it in the pattern for a match to occur.
go/analysis + SuggestedFixes — bespoke, testable
- For a rewrite too specific for the above three, write an
analysis.Analyzer. ItsRun(pass)walks the type-checked AST and reportsanalysis.Diagnostic{SuggestedFixes: [...]}at each match. - Test it against
.goldenfiles withanalysistest.RunWithSuggestedFixes, then ship it as asinglechecker/multichecker -fixbinary, or run it throughgo vet -vettool=<path>.
go fix — the go/analysis-based fixer suite
- As of Go 1.26,
go fixis rewritten onto thego/analysisframework and has converged withgo vet— this is where themodernizefixer suite lives (rangeint,mapsloop,minmax,any,stringscut,fmtappendf,omitzero, and more; → Seesamber/cc-skills-golang@golang-modernizeskill for the idiom-by-idiom breakdown). - On an older toolchain without this convergence, run the equivalent analyzers through
singlechecker/multichecker -fixinstead ofgo fix. - The
//go:fix inlinedirective marks a function or constant so thatgo fixinlines every call site into its replacement — a machine-executable way to complete a deprecation migration once the replacement exists:
go fix ./...
go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...
dave/dst — comment- and formatting-preserving AST edits
go/aststores comments in a side table keyed by byte offset, so reordering, moving, or deleting nodes desyncs comments from the code they were attached to — the root cause of the Extract/Inline comment-loss caveat above.github.com/dave/dst(Decorated Syntax Tree) attaches comments and blank-line spacing as node-local decorations instead, so a hand-rolled AST rewrite round-trips them correctly viadecorator.Parse/decorator.Print.dstutil.Applymirrorsastutil.Apply's visitor API, so existinggo/astrewrite logic ports over directly.- Reach for this only when a bespoke
go/analysisfixer needs to preserve comments thatgo/ast-based rewriting would otherwise scatter.
Always run after a bulk rewrite
goimports -w . # organizes imports added/left dangling by gofmt -r, eg, or a hand-rolled fixer
deadcode ./... # find code orphaned by a removal-heavy refactor
deadcode -test ./... # include test binaries — unreached public API here signals a coverage gap, not dead code
deadcode -whylive=funcName ./... # shortest reachability path proving a function is still live
golang.org/x/tools/cmd/deadcodebuilds its reachability graph with Rapid Type Analysis frommain/init, so it is unsound with respect to assembly,go:linkname, and reflection-driven dispatch — treat a "dead" verdict as a strong hint, not a proof, on code that uses any of those.- Never
sed/perla structural Go change by hand. None of these text tools have grammar awareness, so a pattern that happens to match inside a string literal or a comment gets rewritten right alongside real code. - Always finish a bulk rewrite with
goimports, even after a tool that claims to manage imports itself.
4. Structure-Discovery Tools (blast-radius mapping)
These feed the planning gate in workflow.md — map the blast radius before choosing a tool from the sections above.
| Tool | What it answers |
|---|---|
golang.org/x/tools/go/callgraph (rta/cha/static/vta algorithms) |
Who can reach this function, statically, across the whole build — algorithms trade precision for speed differently |
gopls call hierarchy (textDocument/prepareCallHierarchy) |
Incoming/outgoing calls for one symbol, interactively |
go_references / go_symbol_references (gopls MCP) |
Every reference to a symbol, from an agent context, without a full callgraph build |
go mod graph |
Module-level dependency edges — which modules require which, for blast radius that crosses module boundaries |
Cross-References
- catalog.md — the Fowler refactoring catalog mapped to Go, with the tool from this file that mechanizes each entry.
- workflow.md — the planning gate this section's structure-discovery tools feed, and where a tool-driven step fits into the staged-PR process.