| name | go-refactor |
| description | Use when refactoring Go code, cleaning up legacy codebases, optimizing performance, or enforcing clean architecture boundaries in a Go/Fiber backend |
Go Refactoring & Performance
Overview
Safe refactoring patterns and performance optimization for Go backends. Always characterize before changing.
Core principle: Never refactor without characterization tests. Never optimize without profiling.
3-strikes rule: If the same refactoring approach fails 3 times, stop. The problem is architectural — escalate to system-design for a deeper review instead of thrashing.
Safe Refactoring Process
- Write characterization tests — capture current behavior
- Refactor — change structure, not behavior
- Verify — all characterization tests still pass
- Clean up — remove temporary tests if redundant
Architecture Enforcement
The dependency rule: Dependencies point inward. Domain imports nothing external.
interfaces → application → domain
infrastructure → domain
Violations to check:
grep -r "infrastructure\|interfaces\|fiber\|pgx\|sqlalchemy" internal/domain/
grep -r "infrastructure\|pgx\|redis" internal/application/
Common Refactoring Moves
| Smell | Move |
|---|
| Fat handler | Extract use case |
| Duplicate validation | Extract domain value object |
| Concrete dependency | Extract interface + inject |
| Sequential I/O | errgroup for concurrent calls |
| God struct | Split by responsibility |
| Copy-paste handlers | Extract middleware |
Extract Interface
type Handler struct { repo *postgres.UserRepo }
type Handler struct { repo domain.UserRepository }
Concurrent I/O with errgroup
g, ctx := errgroup.WithContext(ctx)
var users []domain.User
var posts []domain.Post
g.Go(func() error {
var err error
users, err = userRepo.List(ctx)
return err
})
g.Go(func() error {
var err error
posts, err = postRepo.List(ctx)
return err
})
if err := g.Wait(); err != nil {
return err
}
Performance Profiling
import _ "net/http/pprof"
go http.ListenAndServe(":6060", nil)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof http://localhost:6060/debug/pprof/heap
curl http://localhost:6060/debug/pprof/goroutine?debug=2
SQL Optimization
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@test.com';
N+1 detection: If you see N queries for N items in logs, fix with JOIN or batch query.
for _, user := range users {
posts, _ := postRepo.GetByUserID(ctx, user.ID)
}
posts, _ := postRepo.GetByUserIDs(ctx, userIDs)
pgxpool Tuning
config, _ := pgxpool.ParseConfig(connStr)
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 30 * time.Minute
config.MaxConnIdleTime = 5 * time.Minute
Legacy Code Rescue
When working with legacy Go code that has no tests, bad structure, or mixed concerns:
Step 1: Characterize Before Touching
Never change legacy code without characterization tests. Write tests that capture current behavior — even if the behavior is wrong. You need a safety net before refactoring.
func TestLegacy_CreateUser_CurrentBehavior(t *testing.T) {
result, err := legacyCreateUser(input)
assert.NoError(t, err)
assert.Equal(t, expectedOutput, result)
}
Step 2: Identify the Worst Offenders
Prioritize by risk, not by ugliness:
- Code handling user input without validation — security risk, fix first
- Code with no error handling — silent failures, data corruption risk
- God files (500+ lines) — impossible to test, split by responsibility
- Circular dependencies — extract interfaces to break cycles
- Dead code — remove to reduce cognitive load
Step 3: Incremental Strangler Fig
Don't rewrite — wrap and replace incrementally:
type UserService interface {
Create(ctx context.Context, input CreateInput) (*User, error)
}
type legacyUserService struct { db *sql.DB }
type cleanUserService struct { repo domain.UserRepository }
func NewUserService(useLegacy bool) UserService {
if useLegacy { return &legacyUserService{} }
return &cleanUserService{}
}
Step 4: Add Missing Error Handling
result, _ := db.Query(query)
result, err := db.Query(query)
if err != nil {
return fmt.Errorf("query users: %w", err)
}
Step 5: Remove Dead Code
grep -rn "^func " --include="*.go" | while read line; do
func_name=$(echo "$line" | grep -oP 'func \K\w+')
count=$(grep -rn "$func_name" --include="*.go" | wc -l)
[ "$count" -le 1 ] && echo "UNUSED: $line"
done
Delete it. Don't comment it out. Git has history.
Chains
- REQUIRED: Use
superpowers:systematic-debugging for performance investigation
- REQUIRED: Write characterization tests before any refactoring — no exceptions
- REQUIRED: Update CLAUDE.md with discovered gotchas and conventions (
claude-md)
- Legacy codebases: Run
fullstack-healthcheck first to prioritize what to fix