Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
B

betteralign

> 编程语言
Open source

Detect and fix struct field alignment to reduce memory usage in Go programs

1.1K stars0 likes0 views
WebsiteGitHub

About

Detect and fix struct field alignment to reduce memory usage in Go programs

betteralign

About

betteralign is a tool that detects structs that would use less memory if their fields were reordered, and optionally rewrites them.

It is a fork of the official Go fieldalignment tool, with the bulk of the alignment logic unchanged. Its notable differences group into a few themes:

Why this exists

  • preserves comments (field comments, doc comments, and floating comments) — the original fieldalignment erases them on rewrite.

Safety and robustness

  • detects positional composite literals (T{1, 2, 3} rather than T{a: 1, b: 2, c: 3}) anywhere in the package and reports the would-be saving without rewriting the struct — reordering its fields would silently re-map the literal's elements at the next build,
  • analyzes only named struct types declared with type T struct { ... }; anonymous structs (nested struct-typed fields, struct literals, var x struct{...} declarations) are skipped, so it never rewrites the riskier unnamed shapes,
  • when -apply rewrites a struct, fields declared with multiple names (A, B int) are kept together as a single grouped declaration and moved as a unit — the byte-span reorder never splits them,
  • performs atomic file writes to prevent corruption or data loss on rewrite (not on Windows),
  • uses a stable sort for field ordering, so fields of equal sort rank preserve their original relative order; upstream's sort.Slice is unstable and may permute equal-ranked fields differently between runs,
  • fixes a crash on packages with generic struct fields — upstream's ptrdata implementation ends with panic("impossible"), which fires when an uninstantiated type parameter appears as a field type (because (*types.TypeParam).Underlying() returns itself, falling through every switch case); betteralign replaces the panic with a conservative one-word fallback.

Opt-in / opt-out controls

  • skips generated files, identified either by a known suffix (_generated.go, _gen.go, .gen.go, .pb.go, .pb.gw.go) or by a package-level comment containing Code generated by ... DO NOT EDIT.,
  • skips test files (_test.go suffix),
  • skips structs annotated with // betteralign:ignore placed inside the struct body,
  • supports opt-in mode, where only structs annotated with // betteralign:check on the type declaration are checked; placing the directive on a surrounding type ( ... ) block opts in every spec inside the group.

Other improvements

  • caches per-type size, alignment, and pointer-scan results within each analysis pass, avoiding redundant recomputation for types that recur across many struct fields (common in protobuf-generated code),
  • prefixes diagnostics with the actual byte saving (8 bytes saved: struct of size 24 could be 16) rather than only the before/after sizes,
  • includes more thorough tests comparing expected versus golden output,
  • adapts automatically to CPU and memory constraints in containerized environments (Docker, Kubernetes, LXC, LXD, etc.).

Comment preservation is achieved by a small in-tree package, internal/dstmin, that decorates the parser's *ast.File with byte-range spans for each field's lead-doc, body and trailing blanks, then reprints the file by byte-splicing the synthesized struct bodies into the original source and finalising with go/format.Source for column realignment. It replaces a previous dependency on sirkon/dst with ~900 lines of focused code; see internal/dstmin/README.md for the design rationale and benchmark comparison. The whole-file rewrite trade-off still applies: partial rewrites via SuggestedFixes are not possible, so the -fix flag from the analysis package is treated as an alias for -apply, and auto-fix integration with golangci-lint is not supported.

Go's standard AST does not associate comments with nodes — it only stores byte offsets, so the original fieldalignment tool erases all comments. A proposed fix exists as an open CL but has not yet been merged.

Note: This is a single-pass tool. Achieving a fully optimal layout may require running it more than once.

This tool builds upon the following prior work:

  • fieldalignment by the Go Authors
  • maligned by Matthew Dempsky
  • structslop by orijtech

Deep dive

betteralign pursues two goals:

  1. Minimize struct size by sorting fields in descending alignment order and placing zero-sized fields first, reducing internal padding (and avoiding the one-byte tail the runtime adds when a struct ends in a zero-sized field).
  2. Lower the ptrdata-based scan-work estimate the GC uses for pacing by grouping pointer-bearing fields before pointer-free ones, shrinking each value's ptrdata (the head prefix the runtime records as potentially holding pointers). On Go before 1.26 this also directly reduced the bytes scanned per value, since the collector stopped scanning at the last pointer in a value; under the Green Tea GC (default in Go 1.26) marking is span-based rather than per-value, so the win is the smaller ptrdata feeding GC pacing rather than fewer bytes physically scanned.

With those goals in mind, fields are sorted stably by the following criteria, in order:

  1. Zero-sized types first.
  2. Higher alignment first.
  3. Pointer-bearing types before pointer-free types.
  4. Among pointer-bearing types, those with fewer trailing non-pointer bytes first (minimizing GC ptrdata).
  5. Larger size first.

Size: sorting by descending alignment

go
type Record struct {
	Flag  bool  // 1 byte,  1-byte aligned
	ID    int64 // 8 bytes, 8-byte aligned
	Count int32 // 4 bytes, 4-byte aligned
}

As declared, Record is 24 bytes on a 64-bit platform: Flag sits at offset 0, then 7 padding bytes appear so ID can land on an 8-byte boundary; Count follows at offset 16, and the struct is rounded up to the next multiple of its highest alignment (8), adding 4 trailing padding bytes.

After optimalOrder sorts by descending alignment, the layout becomes ID, Count, Flag and shrinks to 16 bytes: ID at offset 0, Count at offset 8, Flag at offset 12, plus 3 trailing padding bytes to round to 16. No internal padding is needed because each field already lands on a correctly-aligned offset. Same data, 33% less memory.

GC ptrdata: ordering pointer-bearing fields by trailing non-pointer bytes

go
type IPAddr struct {
	Zone string // 16 bytes, 8-byte aligned
	IP   []byte // 24 bytes, 8-byte aligned
}

Zone comes first because string has fewer trailing non-pointer bytes than []byte: Sizeof(string) - ptrdata(string) = 8 versus Sizeof([]byte) - ptrdata([]byte) = 16. The struct is 40 bytes either way on a 64-bit platform, but with Zone first the value's ptrdata is only 24 bytes; reversing the fields would push it to 32. That ptrdata feeds the GC's scan-work estimate used for pacing — and, on Go before 1.26, the number of bytes the collector scanned per value. Under the Green Tea GC (default in Go 1.26) marking is span-based, so the smaller ptrdata lowers the pacing estimate rather than the bytes physically scanned.

The pointer-bytes diagnostic reflects this: betteralign reads the Go version of the project being analyzed (its go.mod go directive, or a per-file //go:build go1.x constraint). For a target on Go 1.26 or newer it reports the reorder as lowering the GC scan-work estimate; for older targets it keeps the classic N bytes saved framing, which there still maps to bytes physically scanned. Struct-size diagnostics are unaffected — that saving is real memory regardless of GC.

Runtime tuning with GOGC

The analyzer itself is allocation-light, but loading and type-checking a package graph (go/types, the AST inspector, and the parser) produces a large volume of short-lived garbage — on a real-world run, well over 1 GiB of churn against only a few MiB of live heap at exit. Because betteralign is a short-lived batch process, the default GOGC=100 spends CPU collecting garbage that would be freed at exit anyway. Running with garbage collection disabled trades that work for higher peak memory:

bash
GOGC=off betteralign ./...

On a large warm-cache run this measured roughly a 15–16% wall-clock improvement (and GOGC=400 about 13%), since most of the runtime's GC work simply disappears.

[!CAUTION] GOGC=off lets the heap grow unbounded for the duration of the run. In memory-constrained containers (e.g. a Kubernetes pod with a low memory limit) this can trigger an OOM kill on a large package graph. betteralign already sets a soft GOMEMLIMIT (90% of the cgroup/system memory) which acts as a backstop, but on tight limits prefer a high finite value such as GOGC=400 over fully disabling collection, or leave the default.

Installation

Manual: Download the appropriate binary from the releases page and place it in your PATH, typically /usr/local/bin/betteralign.

Via go install:

bash
go install github.com/dkorunic/betteralign/cmd/betteralign@latest

Usage

…

To check all packages in the current module:

bash
betteralign ./...

To automatically rewrite files (excluding test and generated files):

bash
betteralign -apply ./...

Generated and test files can be included with the -generated_files and -test_files flags respectively. Use -exclude_dirs and -exclude_files to skip specific paths. For -exclude_dirs, a separator-free pattern (vendor, internal, test*) matches a directory of that name at any depth, gitignore-style, while a pattern containing a separator (a/internal) matches only that exact path relative to the working directory. Use -opt_in to check only structs explicitly annotated with // betteralign:check.

Structs constructed via positional composite literals are reported but never rewritten under -apply; the diagnostic points at the offending literal so it can be converted to keyed form (T{Field: value}), after which a rerun will enable the reorder.

Limitations

  • Cross-package positional literals are not detected. The positional-literal guard sees only the package that declares the struct plus its in-package tests. A positional literal of an exported struct used from another package (an ordinary importer, or an external package p_test) is invisible to the pass that rewrites the struct, because Go's static-analysis facts flow with the import graph, not against it. If such a literal exists, -apply will reorder the struct and either break the build or — when the reordered field types still accept the old element types — silently mis-assign values with a clean compile. This is the most dangerous outcome the tool can produce. Unexported structs are safe (their positional usage is confined to the defining package). When running -apply ./... across a multi-package module, gate it with a go build ./... (and your test suite) afterwards, or convert exported structs' positional literals to keyed form first.
  • In-package test literals require the test variant to be loaded. The deferral that protects a positional literal living only in an in-package *_test.go keys on test files actually loaded by the driver. Running with the driver's -test=false (which loads no test files) defeats it, so -apply may rewrite a struct pinned by suc

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Goalignmentanalyzergarbage-collectorgolang

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言