README
¶
structfmt
structfmt is a structural Go source formatter that expands selected composite literals without enforcing a maximum line length.
It expands keyed structs and maps by default, followed by an optional base formatter. Slices and arrays stay compact.
structfmt does not enforce a maximum line length and does not wrap arbitrary Go expressions.
It is not a line-length formatter such as golines. It does not introduce line
wrapping for calls, method chains, arguments, signatures, conditions, boolean
expressions, assignments, returns, imports, or strings. The selected base
formatter can still apply its normal Go formatting rules.
Before
transport := &fallbackTransport{primary: primary, backup: backup, clock: clock, proxy: primary.Proxy, logger: logger, states: diagnostics.NewStates(logger), purpose: purpose}
After
transport := &fallbackTransport{
primary: primary,
backup: backup,
clock: clock,
proxy: primary.Proxy,
logger: logger,
states: diagnostics.NewStates(logger),
purpose: purpose,
}
The fallbackTransport struct declaration must be available in the file or its
package directory. An isolated snippet with an unknown type is conservatively
skipped. Input must be a complete Go file, including its package clause.
These lines stay on one line, regardless of their width:
result := doSomething(argumentOne, argumentTwo, argumentThree, argumentFour, argumentFive, argumentSix)
result := client.WithFoo(foo).WithBar(bar).WithBaz(baz).Execute(ctx)
Maps inside calls are expanded without splitting the other arguments:
// Before
f.addZIP("r1", "1.0", map[string]string{"Client.exe": "内部客户端|1.0", "config.ini": "默认配置", "old.dll": "旧文件"})
// After (also with --base-formatter=none)
f.addZIP("r1", "1.0", map[string]string{
"Client.exe": "内部客户端|1.0",
"config.ini": "默认配置",
"old.dll": "旧文件",
})
Named maps such as type Files map[string]string are supported when their
declarations can be resolved, just like named structs.
Installation
Requires Go 1.23 or later to build. The binary formats the Go syntax supported
by the toolchain that built it; build with a newer Go version when formatting
newer language syntax. There is no toolchain directive or runtime Go command
dependency for the default formatter.
go install github.com/litjourney/structfmt@latest
From a checkout (also works before the first published version):
go install .
Optional gofumpt installation:
go install mvdan.cc/gofumpt@latest
Ensure the Go installation's binary directory, usually $(go env GOPATH)/bin,
is in PATH.
Usage
Recommended:
structfmt --base-formatter=gofumpt -w .
The default uses the standard library's go/format, without starting an
external gofmt executable. These commands are equivalent:
structfmt -w .
structfmt --base-formatter=gofmt -w .
Only perform composite literal expansion:
structfmt --base-formatter=none -w .
none inserts the required newlines and trailing commas, copies surrounding
indentation with one tab for each new element level, and uses the standard
text/tabwriter to align simple key columns. These edits stay inside selected
literals. It does not reindent expression bodies or normalize unrelated source;
use gofmt or gofumpt for general Go formatting.
Select the literal kinds explicitly:
structfmt --expand=struct,map -w . # default
structfmt --expand=struct -w . # only structs
structfmt --expand=map -w . # only maps
The first version accepts only struct and map. Empty, duplicate, or unsupported
entries (including slice and array) are errors.
Other examples:
structfmt --min-elements=2 -w foo.go
structfmt -w ./...
structfmt -w ./foo ./bar
structfmt -w foo.go bar.go
structfmt -l .
structfmt -d .
structfmt --check .
cat foo.go | structfmt
cat foo.go | structfmt --base-formatter=gofumpt
Flags must precede paths. A directory and a trailing /... pattern both recurse
through local directories; there is no package loading or Go import-pattern
expansion. With no paths, stdin is used. Stdin does not read declarations from
the working directory; it can resolve declarations in the supplied source and
readable standard-library imports.
| Flag | Behavior |
|---|---|
-w |
Write final results; do not write unchanged files. |
-l |
Print changed filenames, one per line. |
-d |
Print standard unified diffs, with three context lines. |
--check |
Print changed filenames; exit 1 if any formatting differs. |
--min-elements=N |
Expand literals with at least N elements (len(lit.Elts)); default 3, must be positive. |
--expand=struct,map |
Comma-separated kinds; default struct,map; either kind may be selected alone. |
--base-formatter=gofmt|gofumpt|none |
Select the final formatting stage; default gofmt. |
--include-generated |
Include generated files, which are skipped by default. |
Mode combinations are explicit:
-wconflicts with-dand--check, and is forbidden for stdin.-lcombines with all modes.-w -lwrites and lists successfully changed files.-d --checkprints each changed filename followed by its diff and exits 1.-l --checklists each changed filename once.- Without these four mode flags, formatted source goes to stdout. For multiple
files, output is concatenated in sorted filename order, like
gofmt. -l,-d, and--checkdo not write files unless-wwas explicitly supplied in an allowed combination. Stdin reporting uses<standard input>as its name.
Every comparison uses original source versus the final base-formatter result, including changes caused solely by the base formatter.
| Exit code | Meaning |
|---|---|
| 0 | Success, including a clean check. -l and -d alone return 0 for differences. |
| 1 | --check found formatting differences. |
| 2 | Invalid arguments, source/encoding errors, filesystem errors, cancellation, or formatter failures. |
Errors take precedence over check differences. Batch processing reports filenames and specific errors, continues with other readable files, and returns 2 if any file failed. A batch is not a transaction: earlier successful writes remain.
Detection and correctness
The pipeline is:
read and parse source → classify composites → edit selected element layout
→ base formatter → validate final syntax → optional write
All implementation is independent; no GPL formatter code is used.
- Uses
go/parser,go/ast, andgo/scanner, not regular expressions to classify literals. Kind classification is independent of whether elements are keyed. Every element must be aKeyValueExpr; struct keys must be identifiers, while map keys may be arbitrary expressions. - Follows local and same-package type declarations, aliases, and defined types to an underlying struct, map, slice, or array. Block-local shadowing and type parameters are respected. Cyclic or duplicate declarations are skipped.
- Recognizes pointers, named types, anonymous structs, selectors, and generic
ast.IndexExpr/ast.IndexListExprtypes when their declarations resolve. - Selectors resolve through imports whose source is inside the same module or
readable under the binary's
GOROOT. Explicit import aliases and package names that differ from directory names are supported. - Does not interpret
go.work, dependency versions,replacedirectives, vendored dependencies, or the module cache. Imports outside those two source locations are skipped, even if their names look like structs. Nested modules, dot imports, missing source, and ambiguous package context are also skipped. - No
go/typesorgo/packagescheck,go list, module download, or workspace compilation runs at formatter runtime. Unrelated type errors do not prevent formatting a syntactically valid file. - Explicit and named maps are classified as maps and honor
--expand=map. Explicit and named slices and arrays are never expanded. Empty, positional, mixed, and below-threshold literals are left alone. Elided types such as[]Foo{{A: 1, B: 2, C: 3}}are conservatively skipped. - Nested structs and maps are visited independently, even inside maps, slices, closures, or an outer literal that is itself skipped.
- Source declarations are cached for a single serial run. Target files and sibling declarations are parsed directly, regardless of GOOS, GOARCH, or build tags. Test files are formatted normally; their declarations do not affect non-test files. Generated declarations can identify types even when their files are excluded from output changes.
This deliberately favors avoiding false positives over formatting every possible
literal. For example, Foo{A: 1, B: 2, C: 3} with no available Foo declaration
is unchanged. Conflicting platform-specific Foo declarations are skipped rather
than choosing the current platform's definition. An unreadable or syntactically
broken sibling makes package-level identification unavailable; explicitly
selected broken files still produce an error.
Comments and element values
Edits affect whitespace at literal/element boundaries, simple key-column padding, and a missing final comma. They are collected against original byte offsets and applied in one ordered copy, so nested edits cannot invalidate offsets. Raw strings, closure bodies, and other element expressions are copied unchanged.
Line comments before elements and after commas keep their existing line boundaries.
Boundary block comments cause the entire literal to be skipped, including both
A: 1 /* foo */, and A: 1, /* foo */ B: 2. This deliberately conservative rule
also covers the layout produced when gofmt moves a block comment across a comma,
so a second run cannot suddenly expand a previously skipped literal. Comments
inside expressions are copied intact. Nested literals can still be formatted
independently.
Composite expansion preserves comment text. A selected base formatter retains its normal behavior, including Go doc-comment and build-tag formatting.
Generated files and encoding
Generated detection uses the standard library's
ast.IsGenerated, following the
Go generated-code convention: a matching
// Code generated ... DO NOT EDIT. line before the package clause. Similar
phrases in strings, block comments, or after the package clause are not markers.
Skipped generated files bypass the base formatter as well. Malformed source and
invalid UTF-8 are reported before formatting, including in generated files.
none preserves existing bytes outside its edits. Inserted newlines use CRLF
when the first existing newline is CRLF, otherwise LF. Existing mixed endings
are not normalized. gofmt uses standard go/format line endings (normally LF);
gofumpt uses the installed formatter's behavior. No custom conversion is applied
to raw strings or comment contents.
Filesystem behavior
Recursive scanning skips .git, vendor, node_modules, hidden directories,
hidden files, and all symlinks. Explicitly supplied symlink files or directories
are rejected with exit 2; pass the real path. An explicitly supplied hidden or
vendor directory can be used as a scan root. Ordinary *_test.go files are
included. Paths are deduplicated by their resolved absolute path and processed
serially in deterministic filename order.
Writing stages the complete, validated final output in the same directory, preserves permission bits (including executable bits), syncs and closes the temporary file, checks for changed original contents, and renames it over the destination. On Unix it preserves owner/group or fails without replacing the file. Temporary files are cleaned up on failures. No temporary files are used for the formatter subprocess itself.
The replacement uses Go's cross-platform os.Rename; rename failures are reported
without deleting the destination first. Atomicity depends on the operating system
and filesystem (particularly on Windows). Non-Unix ownership follows the
directory's defaults. ACLs, extended attributes, and hard-link relationships are
not preserved by replacement. Avoid concurrent editors writing during -w; the
contents check reduces lost updates but is not a filesystem lock.
gofumpt execution
gofumpt is located with exec.LookPath and invoked directly, once per processed
file, using stdin/stdout. There is no shell, arbitrary command option, or silent
fallback to gofmt. A missing executable is an error:
structfmt: gofumpt not found in PATH; install it with:
go install mvdan.cc/gofumpt@latest
Nonzero exits include captured stderr. Parse errors, formatter failures, and
invalid formatter output leave that file's original bytes untouched. none and
gofmt never launch a formatter subprocess.
Editor and automation examples
GoLand / JetBrains External Tool or File Watcher:
Program: structfmt
Arguments: --base-formatter=gofumpt -w $FilePath$
Working directory: $ProjectFileDir$
Pre-commit command:
structfmt --base-formatter=gofumpt -w .
The base formatter is already included; no second gofumpt -w . command is needed.
CI:
go install mvdan.cc/gofumpt@latest
go install github.com/litjourney/structfmt@latest
structfmt --base-formatter=gofumpt --check .
Pin the tool versions in CI when reproducible formatting across upgrades matters.
Development
go test ./...
go vet ./...
go test -race ./...
go test ./internal/formatter -fuzz=FuzzExpansion -fuzztime=10s
go run . --check .
Table-driven tests cover struct/map selection, named maps, map arguments with
byte-exact none output, scope/type resolution, comments,
CRLF, generated/build-tagged files, all CLI modes, exit codes, safe writes, and
subprocess behavior. Fake Go helper executables exercise stdin/stdout, stderr,
failures, and idempotency without needing an installed gofumpt. An additional
integration test uses the real executable when available.
Two small runtime libraries are used: BSD-licensed
go-difflib for unified diffs, and
golang.org/x/mod/modfile for reading
the enclosing module's path. Neither invokes external commands.
Licensed under MIT. Dependency notices are in THIRD_PARTY_NOTICES.md.
Documentation
¶
Overview ¶
Command structfmt expands selected composite literals without a line width.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
cli
Package cli implements deterministic serial processing and stable exit codes.
|
Package cli implements deterministic serial processing and stable exit codes. |
|
formatter
Package formatter performs conservative composite literal expansion, followed by a separately configured base formatter.
|
Package formatter performs conservative composite literal expansion, followed by a separately configured base formatter. |