peczenyj/structalign
GitHub: peczenyj/structalign
Stars: 6 | Forks: 0
# structalign
[](https://github.com/peczenyj/structalign/releases/latest)

[](http://pkg.go.dev/github.com/peczenyj/structalign)
[](https://github.com/peczenyj/structalign/actions/workflows/ci.yml)
[](https://codecov.io/gh/peczenyj/structalign)
[](https://goreportcard.com/report/github.com/peczenyj/structalign)
[](https://github.com/peczenyj/structalign/actions/workflows/github-code-scanning/codeql)
[](https://github.com/peczenyj/structalign/actions/workflows/dependency-review.yml)
[](./LICENSE)
[](https://github.com/peczenyj/structalign/releases/latest)
[](https://github.com/peczenyj/structalign/commit/HEAD)
[](https://github.com/peczenyj/structalign/blob/main/CONTRIBUTING.md#pull-request-process)
[](https://github.com/peczenyj/structalign/attestations)
[](https://scorecard.dev/viewer/?uri=github.com/peczenyj/structalign)
[](https://bestpractices.coreinfrastructure.org/projects/13027)
[](https://github.com/avelino/awesome-go#code-analysis)
A read-only companion to `golang.org/x/tools`'s `fieldalignment`: it shows the
memory-optimal struct as a unified or side-by-side diff built for human review,
rather than rewriting your files or emitting a machine-applicable patch, and can
also print any struct's offset/size/align/padding layout. The analysis comes
straight from the upstream analyzer, so results match `fieldalignment` exactly —
only the presentation is new.

## Quick start
Install:
go install github.com/peczenyj/structalign@latest
Or grab a prebuilt binary for your OS/arch from the
[Releases](https://github.com/peczenyj/structalign/releases) page. Check the
installed version with `structalign -version`.
Then point it at a file, a package, or any Go package pattern:
structalign ./... # every package in the module
It accepts whatever the `go` tool does — `./...`, import paths, directories, and
single `.go` files — and you can pass several at once. By default it skips
**generated files** (`// Code generated … DO NOT EDIT.`) and `_test.go` files; use
`-generated` / `-tests` to include them (see [Scanning scope](#scanning-scope)).
Pointed at the bundled sample (`./_example`), it reports the reordering and exits
non-zero so it can gate CI:
$ structalign -type=Mixed ./_example
_example/types.go:6:12: Mixed: struct of size 24 could be 16, saving 8 bytes (33.33% smaller)
type Mixed struct {
+ B int64
A bool
- B int64
C bool
}
$ echo $?
1
## Why it exists
`golang.org/x/tools/.../fieldalignment` can already detect a misaligned struct
and rewrite it for you. It offers three things:
- **report** (default) — prints a terse message like `struct of size 24 could be 16` and nothing else;
- **`-fix`** — rewrites your source in place;
- **`-fix -diff`** — instead of writing, prints the change as a unified patch.
So the change *can* be shown — but only as a patch built for `patch`/`git apply`,
not for a person to read. It answers "how do I apply this?", not "what would the
optimal struct look like, and is the saving worth it?" And none of these modes let
you inspect a struct's *existing* layout — offsets, sizes, padding — at all.
`structalign` is the readability layer over that same upstream analysis: it shows
the reordering as output meant for people — a review-oriented diff (unified or
side-by-side, with color, summary, threshold, and tag-stripping) — plus a
per-field layout inspector.
| | [fieldalignment][fa] | [betteralign][ba] | [structlayout][sl] | **structalign** |
|------------------------------|:--:|:--:|:--:|:--:|
| Report the misalignment | ✅ | ✅ | — | ✅ |
| **Human-readable** diff | — | — | — | ✅ |
| Machine-applicable patch | `-fix -diff` | `-fix -diff` | — | — |
| Rewrite files in place | `-fix` | `-fix` | — | — |
| Inspect field layout | — | — | ✅ | ✅ |
| CI-friendly exit code | ✅ | ✅ | — | ✅ |
## Usage
`structalign` is a read-only companion to
[`fieldalignment`](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment):
it prints the reordered struct plus a diff (or, with `-inspect`, a struct's
memory layout) for review, and never edits files. The analysis matches
`fieldalignment` exactly; for an in-place rewrite, use `fieldalignment -fix`.
`packages` are whatever the go tool understands: `./...`, import paths,
directories, or single `.go` files. Generated and `_test.go` files are skipped
unless `-generated` / `-tests` are given; only named structs are considered (a
non-empty `-type` also skips anonymous structs and struct literals).
In diff mode `structalign` exits **1 when any reordering is found** and **0
otherwise**, so it drops into CI as a check; `-inspect` always exits 0. Note the
most compact ordering is not always the most efficient — beware false sharing
(see `-skip-cache-padded`).
structalign ./... # scan every package in the module
structalign -diff=side -summary ./... # side-by-side diff plus a total
structalign -inspect -type=Config ./pkg # one struct's per-field layout
structalign [flags] [packages]
packages Go package patterns: ./..., import paths, directories, or
single .go files (defaults the go tool understands)
-diff value diff style: unified|side|none (default "unified")
-format value output format: text|json (default "text")
-width int column width per side for -diff=side (default: auto from terminal)
-color value colorize: auto|always|never (default "auto")
-inspect inspect layout instead of diffing: print each struct as
annotated Go source with size/align/padding comments
-verbose in -inspect mode, show padding on its own `_` line
-tags preserve struct field tags in output (default: strip them)
-summary in diff mode, print a one-line summary after the diffs
-sort present results largest-first (diff: by bytes saved;
inspect: by struct size)
-threshold int in diff mode, only show structs that save at least N bytes
(default 0; negatives treated as 0)
-type string only consider named structs matching these comma-separated
glob patterns (e.g. "*Request,Config"); empty means all
-exclude string exclude packages whose import path matches this regexp
(default "^unsafe$|^builtin$")
-generated also analyze generated files (skipped by default)
-tests also analyze _test.go files (skipped by default)
-skip-cache-padded
skip structs with a golang.org/x/sys/cpu.CacheLinePad field
-show-nolint show structs even when their type carries a recognized
//nolint directive (directives are respected by default)
-nolint-linters string
//nolint tokens that suppress a finding (default
"fieldalignment"; a bare //nolint always counts)
-no-rc skip loading .structalignrc files
-version print version and exit
In the default `-color=auto`, color is emitted only when stdout is a terminal and
the [`NO_COLOR`](https://no-color.org) environment variable is unset. `NO_COLOR`
(any non-empty value) disables color; an explicit `-color=always` overrides it.
### Configuration
`structalign` supports persistent defaults via environment variables and
`.structalignrc` files. Precedence (highest wins):
1. **CLI flags** (e.g. `structalign -sort`)
2. **Environment variables**: `STRUCTALIGN_`, e.g. `STRUCTALIGN_SORT=true`.
3. **Local config**: `.structalignrc` in the current directory.
4. **Global config**: `~/.structalignrc`.
The configuration files use a simple `key = value` format:
# .structalignrc example
sort = true
threshold = 8
skip-cache-padded = true
Keys map directly to flag names. To skip loading configuration files (e.g. in
CI), use the `-no-rc` flag. Note that **theme** is not an RC key; set it via the
`STRUCTALIGN_THEME` environment variable.
#### Configuration Reference
| Feature | CLI Flag | Environment Variable | RC Key | Default |
|---------|----------|----------------------|--------|---------|
| Diff style | `-diff` | `STRUCTALIGN_DIFF` | `diff` | `unified` |
| Output format | `-format` | `STRUCTALIGN_FORMAT` | `format` | `text` |
| Column width | `-width` | `STRUCTALIGN_WIDTH` | `width` | `0` (auto) |
| Color mode | `-color` | `STRUCTALIGN_COLOR` | `color` | `auto` |
| Theme palette | — | `STRUCTALIGN_THEME` | — | `default` |
| Inspect mode | `-inspect` | `STRUCTALIGN_INSPECT` | `inspect` | `false` |
| Verbose inspect | `-verbose` | `STRUCTALIGN_VERBOSE` | `verbose` | `false` |
| Keep tags | `-tags` | `STRUCTALIGN_TAGS` | `tags` | `false` |
| Show summary | `-summary` | `STRUCTALIGN_SUMMARY` | `summary` | `false` |
| Largest-first sort | `-sort` | `STRUCTALIGN_SORT` | `sort` | `false` |
| Min bytes saved | `-threshold` | `STRUCTALIGN_THRESHOLD` | `threshold` | `0` |
| Type filter | `-type` | `STRUCTALIGN_TYPE` | `type` | (empty) |
| Package exclude | `-exclude` | `STRUCTALIGN_EXCLUDE` | `exclude` | `^unsafe$\|^builtin$` |
| Include generated | `-generated` | `STRUCTALIGN_GENERATED` | `generated` | `false` |
| Include tests | `-tests` | `STRUCTALIGN_TESTS` | `tests` | `false` |
| Skip cache padded | `-skip-cache-padded` | `STRUCTALIGN_SKIP_CACHE_PADDED` | `skip-cache-padded` | `false` |
| Show //nolint | `-show-nolint` | `STRUCTALIGN_SHOW_NOLINT` | `show-nolint` | `false` |
| Nolint linters | `-nolint-linters` | `STRUCTALIGN_NOLINT_LINTERS` | `nolint-linters` | `fieldalignment` |
The palette can be switched with the `STRUCTALIGN_THEME` environment variable —
... Applied fuzzy match at line 147.
`default` (the standard colors), `cga` (the iconic cyan/magenta/white CGA palette,
with a reverse-video header bar), or `green` / `amber` (single-hue phosphor-monitor
emulations). It only affects *which* colors
are used when color is on; it does not turn color on by itself. An unknown value
warns and falls back to `default`.
## Modes
### Diff (default)
Unified diff:
$ structalign -type=Mixed ./_example
_example/types.go:6:12: Mixed: struct of size 24 could be 16, saving 8 bytes (33.33% smaller)
type Mixed struct {
+ B int64
A bool
- B int64
C bool
}
Side-by-side:
$ structalign -diff=side -width=28 -type=Mixed ./_example
_example/types.go:6:12: Mixed: struct of size 24 could be 16, saving 8 bytes (33.33% smaller)
current │ proposed
─────────────────────────────┼─────────────────────────────
type Mixed struct { │ type Mixed struct {
│ B int64
A bool │ A bool
B int64 │
C bool │ C bool
} │ }
Print the reordered struct only (no diff): `structalign -diff=none ./_example`.
With `-summary`, a one-line aggregate is appended after the diffs (counting only
the structs shown, and the bytes their reorderings would save):
$ structalign -summary ./_example
... (diffs above) ...
Summary: 5 structs affected, 56 bytes saved total
### Inspect layout
`-inspect` skips the alignment analyzer entirely and prints each (filtered) named
struct as annotated Go source: the declaration with per-field `// size: N, align: M`
comments, column-aligned, plus a size/align/padding summary on the opening line.
Padding is folded onto the field comment by default:
$ structalign -inspect -type=Mixed ./_example
type Mixed struct { // size: 24, align: 8, padding: 14
A bool // size: 1, align: 1, padding: 7
B int64 // size: 8, align: 8
C bool // size: 1, align: 1, padding: 7
}
With `-verbose`, padding moves onto its own `_` line:
$ structalign -inspect -verbose -type=Mixed ./_example
type Mixed struct { // size: 24, align: 8, padding: 14
A bool // size: 1, align: 1
_ // 7 byte padding
B int64 // size: 8, align: 8
C bool // size: 1, align: 1
_ // 7 byte padding
}
The layout comes from the same `go/types` sizing the diff modes use
(`types.Sizes.Offsetsof` / `Sizeof` / `Alignof`), driven by the toolchain's
target sizes (your host `GOOS`/`GOARCH` by default). This is similar to
[`honnef.co/go/tools/cmd/structlayout`][sl], but stays inside
this one tool and honors the same `-type` filter.
#### Inspecting generic types
A generic struct has **no single layout** — `type Box[T any] struct{ … }` is laid
out differently for every type argument (`Box[bool]` and `Box[[64]byte]` share
nothing), so there is no concrete type to measure. Inspect therefore shows a
**best-effort approximation**: each type parameter is measured as a representative
type — its constraint's core type (e.g. `~int` → `int`), or `interface{}` when the
constraint is unbounded (`any`, `comparable`, unions). Fields keep their source
form (`Value T`, not `Value any`), and every field whose size depends on a type
parameter is annotated with the assumption it was measured under (`-- assume
T=any`). The output is also prefixed with a disclaimer. Treat the numbers as
indicative only; the real layout depends on how the type is instantiated.
$ structalign -inspect -type=Generic ./_example
// generic type — layout assumes T=any; the real layout depends on the type argument(s)
type Generic[T] struct { // size: 32, align: 8, padding: 11
Flag bool // size: 1, align: 1, padding: 7
Value T // size: 16, align: 8 -- assume T=any
Count uint32 // size: 4, align: 4, padding: 4
}
A field can depend on a type parameter indirectly — through a composite or a
nested generic — and the marker follows it: `map[K]V` reports `-- assume K=any,
V=any`, and `Inner[V]` reports `-- assume V=any`.
#### Inspecting types you don't own
structalign resolves its package arguments through `go/packages`, so you can
point `-inspect` (and the diff modes) at types you didn't write — as long as the
package is reachable from the **current directory's `go.mod`**.
Standard-library structs work out of the box — give the import path and a
`-type` filter:
$ structalign -inspect -type=Time time
type Time struct { // size: 24, align: 8, padding: 0
wall uint64 // size: 8, align: 8
ext int64 // size: 8, align: 8
loc *Location // size: 8, align: 8
}
Dependencies already in your `go.mod` resolve the same way:
$ structalign -inspect -type=Group golang.org/x/sync/errgroup
type Group struct { // size: 64, align: 8, padding: 4
cancel func(error) // size: 8, align: 8
wg sync.WaitGroup // size: 16, align: 8
sem chan token // size: 8, align: 8
errOnce sync.Once // size: 12, align: 4, padding: 4
err error // size: 16, align: 8
}
Any other library must be *required* by the module you run in — resolution is
against the current `go.mod`, **not** arbitrary packages sitting in `$GOPATH` or
the module cache. A package the module doesn't require fails with `no required
module provides package …`. The quickest way to inspect an arbitrary library is
a throwaway module:
mkdir /tmp/inspect && cd /tmp/inspect
go mod init scratch
go get github.com/rs/zerolog
structalign -inspect -type=Logger github.com/rs/zerolog
Built-in **scalar** types (`int`, `bool`, `string`, …) can't be inspected:
inspect prints a *struct field layout*, and scalars have no fields. (The
`builtin` pseudo-package is in the default `-exclude` for the same reason.) To
see a scalar's size, inspect a struct that contains it — a `string` field shows
`size: 16` on a 64-bit target.
### JSON output
`-format=json` (or `STRUCTALIGN_FORMAT=json`, or `format = json` in
`.structalignrc`) emits a single structured document instead of the rendered
text, for both diff and inspect modes. It carries the same data the text
renderers show — findings include `original` / `proposed`, `oldSize` /
`newSize` / `bytesSaved`; inspect layouts include per-field
`offset` / `size` / `align` / `padding` and the generic `assume` notes.
$ structalign -format=json -type=Mixed ./_example
{
"version": "...",
"mode": "diff",
"findings": [ ... ],
"summary": { "structsAffected": 1, "bytesSaved": 8 }
}
Two things differ from text mode by design:
- **The diff document always includes the `summary` block** (so a machine
consumer always gets the totals). `-summary` only governs the text renderer's
trailing summary line.
- **The presentation flags don't apply.** `-diff`, `-summary`, `-verbose`,
`-color`, and `-width` shape the *text* output only; in JSON mode they are
ignored, since the consumer renders from the structured fields itself.
`-tags` still applies — it gates whether the inspect document's per-field
`tag` field is emitted (see [Field tags](#field-tags)).
### Filtering by type name
`-type` takes a comma-separated list of glob patterns (`path.Match` syntax: `*`,
`?`, `[...]`) matched against the *declared* name of each struct type. Anonymous
structs and struct literals are never matched by a non-empty filter. It applies to
every mode:
structalign -type='*Request' ./... # only structs ending in Request
structalign -type='Record,Config' ./pkg # exact names
structalign -inspect -type='*ID*' ./pkg # inspect just ID-related structs
### Scanning scope
By default, structalign analyzes the regular, hand-written source of each package.
A few flags adjust what's in scope:
structalign -generated ./... # include generated files (skipped by default)
structalign -tests ./... # include _test.go files (skipped by default)
structalign -exclude='/internal/' ./... # drop packages whose import path matches the regexp
structalign -skip-cache-padded ./... # skip structs guarded by cpu.CacheLinePad
- **Generated files** (`// Code generated … DO NOT EDIT.`) are skipped by default —
you usually can't hand-edit them, so a reorder suggestion would be noise.
- **`_test.go` files** are skipped by default; `-tests` includes them.
- **`-exclude`** takes a regexp matched against the *import path* (default
`^unsafe$|^builtin$`); it complements `-type`, which matches struct names.
- **`-skip-cache-padded`** leaves structs with a
[`cpu.CacheLinePad`](https://pkg.go.dev/golang.org/x/sys/cpu#CacheLinePad) field
alone, since reordering would move the pad and defeat its false-sharing guard.
- **`//nolint` directives are respected by default** (diff mode): a struct whose
type declaration carries a recognized `//nolint` — `//nolint:fieldalignment` or
a bare `//nolint` — is suppressed, matching golangci-lint. `-nolint-linters`
customizes which named tokens count (default `fieldalignment`; e.g.
`-nolint-linters=fieldalignment,betteralign`); a bare `//nolint` always counts.
`-show-nolint` reveals suppressed structs (audit mode). Inspect mode ignores
these directives.
### Field tags
By default the tool **strips struct field tags** from all output, so the focus
stays on field order and layout rather than tag text. This matters most in diff
mode: reordering changes column widths, which makes `gofmt` re-align tags, and
those re-spacing changes would otherwise show up as diff noise unrelated to the
actual reorder. Stripping tags from both sides removes that distraction.
Pass `-tags` to keep tags. In diff mode they stay bound to their fields as the
fields move; in inspect mode they are appended to each field declaration (with
comments still column-aligned):
$ structalign -inspect -tags -type=Tagged ./_example
type Tagged struct { // size: 48, align: 8, padding: 18
Flag bool `json:"flag"` // size: 1, align: 1, padding: 7
ID string `json:"id" db:"id"` // size: 16, align: 8
Count uint32 `json:"count"` // size: 4, align: 4, padding: 4
Ptr *uint64 // size: 8, align: 8
Enabled bool `json:"enabled"` // size: 1, align: 1, padding: 7
}
Tags never affect the layout numbers (size/offset/alignment are independent of
tags), so stripping them changes only the display, never the analysis. The same
flag governs JSON output: with `-format=json`, the inspect document's `tag`
field is emitted only when `-tags` (or `STRUCTALIGN_TAGS=true`, or `tags = true`
in `.structalignrc`) is in effect.
## How it works
`structalign` does **not** reimplement the alignment algorithm. It runs the
**unmodified** `fieldalignment.Analyzer`, intercepts the `analysis.SuggestedFix`
it already produces (a single `TextEdit` replacing the whole struct node with the
optimally-ordered, gofmt'd version), and diffs that against your original source.
Because all the alignment logic — including the GC pointer-bytes optimization and
size calculations — comes straight from upstream, results match `fieldalignment`
exactly. Only the *presentation* is new.
## Building from source
Requires **Go 1.25+** (the floor set by `golang.org/x/tools`). The repo uses
[Task](https://taskfile.dev) ([`golangci-lint`](https://golangci-lint.run) handles
both linting and formatting); the `Makefile` just delegates to `task`.
git clone https://github.com/peczenyj/structalign
cd structalign
task build # -> ./structalign (or: go build -o structalign .)
task ci # lint, build, test, and a smoke test against ./_example
task --list # list all tasks
`main.go` (at the module root) is a thin entrypoint; the implementation lives in
small packages under `pkg/common` (contracts) and `internal/` (loader, align,
layout, ui, app, …). `_example/` holds sample structs for manual testing — the leading
underscore keeps the Go tool from treating it as a package, so it stays out of
`go build ./...` and friends.
## Caveats inherited from fieldalignment
- The most compact order is not always the most efficient — packing fields tightly
can occasionally induce false sharing between goroutines. For deliberately
cache-line-padded structs, use `-skip-cache-padded`.
- Reordering can hurt logical grouping/readability; treat the output as advice,
most valuable for hot, frequently-allocated structs.
- Sizes are computed for the toolchain's target (your host `GOOS`/`GOARCH` by
default). To analyze another target, set them in the environment, e.g.
`GOARCH=386 structalign ./...`.
- For **generic** structs both modes work from the type parameters' assumed
(constraint) sizes, so the result may not match a particular instantiation —
diff may suggest a non-optimal order, and inspect's numbers are approximate (it
prints a disclaimer; see [Inspecting generic types](#inspecting-generic-types)).
## Design notes
### Pipeline
1. Load the target packages with `golang.org/x/tools/go/packages` (mode
including syntax, types, type info, and `TypesSizes`). This resolves `./...`,
import paths, directories, and single files the way the `go` tool does, and
supplies the analyzer's size math from the real build target.
2. Satisfy the analyzer's only dependency — the `inspect` pass — by building an
`inspector.New(pkg.Syntax)` and placing it in `Pass.ResultOf`.
3. Provide a custom `Pass.Report` that captures each diagnostic's `NewText` (the
proposed struct) and reads the original source slice between `Pos` and `End`.
4. Diff the two with `github.com/aymanbagabas/go-udiff` (a maintained standalone
port of the Myers diff packages gopls uses, via `udiff.Lines`) and render the
result as a unified or side-by-side diff, or just print the reordered struct.
### Dependencies and the internal-package rule
This tool lives in its own standalone module (`github.com/peczenyj/structalign`)
and pulls two dependencies as ordinary `go get`-able modules:
- `golang.org/x/tools` — for the public `.../passes/fieldalignment` analyzer.
- `github.com/aymanbagabas/go-udiff` — for line diffing.
Go's internal-package rule says a package may import `/internal/...` only
if the **importing package's own path** is rooted at `/`. That is why
diffing uses `go-udiff` rather than x/tools' own diff package:
- `fieldalignment` imports `golang.org/x/tools/internal/astutil` — fine, because
the importer is itself under `golang.org/x/tools/`. This tool only touches
`fieldalignment`'s public API, so importing the analyzer from any module works.
- `golang.org/x/tools/internal/diff`, by contrast, **cannot** be imported from
`github.com/peczenyj/structalign` (not under `golang.org/x/tools/`), so the
compiler rejects it. `go-udiff` is a public port of the same gopls diff code,
so the results are equivalent.
## Easter eggs
A few hidden flags are intentionally kept out of `-help` and the flag table above:
**`-cga`, `-green`, `-amber`** — shortcuts for the retro-theme palettes otherwise
selected with `STRUCTALIGN_THEME=…`. Pick one per invocation; the egg flag wins
over the environment variable.
structalign -cga ./... # cyan/magenta/white CGA palette
structalign -green -inspect ./_example # single-hue green-phosphor look
structalign -amber -diff=side ./... # amber-phosphor side-by-side diff
They are stripped before flag parsing, so they never trip *"flag provided but not
defined"* when combined with normal flags.
## Changelog
task changelog # regenerate CHANGELOG.md
task changelog:unreleased # preview pending entries
task release TAG=v0.1.0 # stamp the changelog for a release
## Prior work
`structalign` builds upon — and is indebted to — the following prior work:
- [**fieldalignment**](https://github.com/golang/tools/tree/master/go/analysis/passes/fieldalignment)
by the Go Authors — the upstream analyzer structalign wraps; all the alignment
math comes straight from it.
- [**betteralign**](https://github.com/dkorunic/betteralign) by Dinko Korunić —
a maintained successor to `fieldalignment` that also applies fixes; structalign
recognizes its `//nolint:betteralign` directives via `-nolint-linters`.
- [**maligned**](https://github.com/mdempsky/maligned) by Matthew Dempsky — the
original struct field-alignment detector, since superseded by `fieldalignment`.
- [**structslop**](https://github.com/orijtech/structslop) by orijtech — suggests
struct field rearrangements to reduce memory footprint.
## License
[MIT](LICENSE) © Tiago Peczenyj
标签:EVTX分析