Move Go declarations between files without changing what your program means.
sflit moves or copies top-level Go declarations between files through the
AST, and refuses any operation that could change what the program means.
It is a declaration mover, and split (one file → many) and merge
(many → one) are its two directions. The output has semantic accuracy,
not byte-for-byte fidelity: the AST is re-parsed and reprinted through
gofmt, and imports are updated in written files.
File boundaries are not important in Go and a tool to split files sounds questionable.
Fair. Then a file grew past 5000 lines. Refactoring it consumed so many tokens and doing it manually was pretty painful. It is not about code quality or taste, just pure efficiency and speed. Therefore this tool exists.
Here is what moving declarations buys you:
- Agent-efficient codebases. After a split, an agent reads only the file that matters. Small files make string-matching edits reliable, and when an agent changes one file, the rest of the package stays cached. The filenames become a map of the package.
- Parallel editing without contention. A 5000-line file is a serialization point: two agents — or an agent and a human — editing it collide with merge conflicts and stomped edits. After a split, work on disjoint features touches disjoint files.
- Reviewable pure-move commits. Semantic accuracy plus blocked splits mean a move can land as a commit that is verifiably behavior-free. The reviewer checks the partition, not the code; the behavioral change lands separately.
- Merging and reorganizing. Because sflit is a declaration mover, the reverse direction works too: merge over-split files back into one, move a stray declaration to the file where it belongs, or re-partition a package by feature. A move that turned out wrong is fixed by a reversal — the same move with source and sink swapped.
- Test-file parity.
_test.gofiles move the same way, so the test layout can mirror the source layout —foo.goandfoo_test.gostay aligned. - Enforcing a file-size policy. When the team rule says files over N lines must be split, sflit is the remediation: the linter flags, sflit moves, nothing changes meaning.
For a worked example — a 1208-line file moved into 11 files, tests mirrored to match — see docs/splitting-a-real-file.md.
sflit is a self-contained binary with no external runtime dependencies.
Import management (goimports) is compiled in via golang.org/x/tools/imports — no separate installation required.
Building from source requires Go 1.26.2+.
go install github.com/veggiemonk/sflit@latestOr clone and build locally:
git clone https://github.com/veggiemonk/sflit.git
cd sflit
make build # produces ./sflit
make install # installs to $GOPATH/binsflit - moves Go declarations between files
Moves or copies top-level Go declarations between files through the AST, and
refuses any operation that could change what the program means. Files are
re-parsed and reprinted through gofmt; imports are updated in written files.
Comments associated with moved declarations travel with them.
Usage:
sflit <command> [flags] [args]
Commands:
move Move declarations from source to sink (deletes them from source)
copy Copy declarations into a sink in a DIFFERENT directory
analyze Print a read-only structural map of a file or package (split planning)
plan Apply N splits from one source in one atomic commit (JSON plan)
schema Print the JSON tool definition with examples (for agent integration)
version Print version information
help Print this message
Run 'sflit <command> -h' for a command's flags.
Concurrency:
Safe to fan out N concurrent invocations on the same files with no external
coordination. Each run hashes source and sink at parse and verifies them
under a short per-file lock at commit; a conflicting write (sflit or any
other tool) triggers a re-run against the fresh content, up to -retries
times (default 16). Sidecar lock files (.<name>.sflit.lock) are removed on
release; on windows they are left behind (safe to ignore).
Exit codes:
0 Success
1 Operation error (collision, package mismatch, same-directory copy,
build-constraint mismatch, generated/cgo/dot-import file, parse error,
no matches, write error, conflict retries exhausted)
2 Usage error (invalid flags, missing required arguments, bad plan shape)sflit schema emits a JSON tool definition (name, description, per-command
parameters, selection rules, worked examples, exit codes) suitable for
LLM tool-use loops. Pipe it straight into your agent's tool registry:
sflit schema | jq .See internal/mover/schema.go for the
schema source.
- On collision (a selected Go package-namespace name already exists in the sink),
sflitbails before writing. - On package mismatch (sink's package differs from source's),
sflitbails before writing. - On copy, only the sink is written; on move, source and sink are written via temp-file + rename.
- Concurrent invocations on the same files are safe without external coordination — fan out N agents freely. Each run hashes source and sink at parse and verifies both under a short per-file lock at commit; a conflicting write (by sflit or any other tool) triggers a re-run against the fresh content, up to
-retriestimes (default 16; 0 or negative uses the default — retry cannot be disabled). See ADR-0001. Sidecar lock files (.<name>.sflit.lock) are removed on release; on windows (best-effort platform) they are left behind and are safe to ignore or gitignore. - Copying into the source's own directory is rejected before writing: the source keeps every selected declaration, so the package would gain duplicate names and stop compiling. Use the
movecommand for same-directory splits;copytargets a sink in a different directory. - A copy or move that could silently change semantics or produce invalid Go is a blocked split — rejected before any write:
initfunctions, narrowing ofiota/implicit const blocks, narrowing of unsafe multi-name value specs, cross-directory operations that would strand package-internal references (file-local check: sibling files of the source are not seen), sinks importing a different path under an alias the source also uses, and cross-directory operations on declarations carrying//go:embedor//go:linkname. - Otherwise, a selector that matches only part of a grouped var/const/type block narrows it: the matching specs travel, the siblings stay in the source.
sflitrejects generated files (source or sink), cgo files, dot-import files (source or sink), and build-constraint mismatches rather than guessing at file-sensitive semantics.- Blank identifier declarations such as interface assertions do not collide with each other.
- Comments associated with moved declarations travel with them: doc comments,
//go:directives, free-floating lead comments, in-body comments, inline spec/statement comments, and trailing orphan comments when the matched declaration is at the end of the file.