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:

  1. Extract functions with ast-grep scripts - Use exact function signatures to reliably extract code
  2. Fix imports automatically - Run goimports -w . to add/remove imports
  3. 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:

  1. Extract code structurally
  2. Fix imports automatically
  3. 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