Large monolithic files are a common problem in software projects, especially when AI agents are involved. These files are hard to navigate, difficult to review in pull requests, and violate the single responsibility principle. We had a monster in one of our greenfield projects: internal/gateway/handlers/admin.go
. This file weighed in at 2,371 lines with 44 methods handling everything from user management to audit logging.
Traditional refactoring approaches are painful:
- Manual copy-paste is error-prone and tedious
- You need to carefully track imports and dependencies
- It's easy to miss functions or create duplicates
- The whole process can take hours
We found a better way using ast-grep: a structural search tool that operates on Abstract Syntax Trees. Here's how Claude Code split that massive file into 9 focused, single-responsibility files in less than 5 minutes.
The Technique: Three-Step Refactoring
The pattern is simple and repeatable:
- Extract functions with ast-grep scripts - Use exact function signatures to reliably extract code
- Fix imports automatically - Run
goimports -w .
to add/remove imports - Verify compilation - Run
go build
to catch any issues
This approach is dramatically faster than manual refactoring and much more reliable.
Real-World Example: Breaking Down admin.go
Step 1: Analyze the File Structure
First, we identified logical groupings by listing all methods:
ast-grep --pattern 'func (h *AdminHandler) $NAME($$$) $$$' internal/gateway/handlers/admin.go \
--json=compact | jq -r '.[].text' | grep -E '^func \(h \*AdminHandler\)' | \
sed 's/func (h \*AdminHandler) //' | sed 's/(.*//' | sort
This revealed clear categories like User Management, API Tokens, Sessions, Settings, and Audit Logging.
Step 2: Run Extraction Scripts
Example for user management:
#!/bin/bash
set -e
output="internal/gateway/handlers/admin_users.go"
cat > "$output" << 'EOF'
package handlers
import (
"github.com/gin-gonic/gin"
)
EOF
for func in "ListUsers" "GetUser" "CreateUser" "notifyUserCreated" \
"DeleteUser" "UpdateUserProfile" "UpdateUserRoles" \
"LockUser" "UnlockUser"; do
ast-grep --pattern "func (h *AdminHandler) $func(\$\$\$) \$\$\$" \
internal/gateway/handlers/admin.go --json=compact | \
jq -r '.[0].text' >> "$output"
echo -e "\n" >> "$output"
done
Tips:
- Use exact names for reliability
- Use
$$$
wildcards for params and bodies - Keep imports minimal - let
goimports
handle them
Step 3: Extract the Core Struct
We extracted the AdminHandler
struct and constructor into a lean core file, then let goimports
fix the imports across all files.
Step 4: Verify Everything
Run all scripts, fix imports, and verify with:
goimports -w .
go build ./...
Any missing types or constants can then be added back manually.
Results
Before: internal/gateway/handlers/admin.go
- 2,371 lines
After: Split into 9 focused files:
admin.go
(38 lines)admin_users.go
(413)admin_tokens.go
(292)admin_sessions.go
(166)admin_settings.go
(368)admin_audit.go
(411)admin_rack.go
(28)admin_roles.go
(30)admin_helpers.go
(305)
Why It Works
ast-grep is structure-aware and language-agnostic, allowing precise, scriptable refactors. Combined with goimports
, you get a fast, reliable workflow:
- Extract code structurally
- Fix imports automatically
- Verify correctness
One of the key benefits is that an AI doesn't have to read the entire file. It just needs to know the names of the functions that need to be moved.
When an AI agent knows how to perform these steps, this refactoring workflow can take minutes instead of a lengthy, error-prone process. No "Compacting conversation..." required.
References
- ast-grep: https://ast-grep.github.io/
- goimports: https://go.googlesource.com/tools
go install golang.org/x/tools/cmd/goimports@latest