CLI tool
dkorunic/betteralign avatar
dkorunic/betteralign

dkorunic/betteralign: a fieldalignment fork that reorders your structs without eating the comments

Detect and fix suboptimal struct field layout in Go: reduces padding and GC pointer bytes, rewrites files while preserving comments. A drop-in fieldalignment replacement.

1,087 stars22 forksGoBSD-3-Clause

At a glance

What is it?
betteralign is a Go static analysis tool that finds and fixes suboptimal struct field layouts, reducing padding and the pointer-scan bytes the garbage collector has to consider. It is a fork of the official fieldalignment pass with the bulk of the alignment logic unchanged, and its distinguishing feature is a whole-file rewrite that preserves field, doc and floating comments, plus a set of safety rules upstream does not have.
Who is it for?
Adopt betteralign if your Go code base has large numbers of structs whose field order is arbitrary, since the size and pointer-scan savings are real and the fork removes the comment erasure that makes the upstream pass hard to run on a real repository.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The upstream pass erases your comments

betteralign detects and fixes suboptimal struct field layout in Go, reducing padding and GC pointer bytes, and rewrites files while preserving comments. It presents itself as a drop-in replacement for `fieldalignment`.

The origin matters. It is a fork of the official Go fieldalignment tool from the x/tools analysis passes, with the bulk of the alignment logic unchanged. The listed prior work is broader still, covering fieldalignment by the Go Authors, maligned by Matthew Dempsky and structslop by orijtech, so the ideas are well trodden and the contribution is a different set of trade-offs around rewriting files.

The reason the fork exists is stated bluntly: the original fieldalignment erases comments on rewrite. That is not a cosmetic issue. Go's standard AST does not associate comments with nodes, it only stores byte offsets, so any tool that reorders fields by regenerating the declaration loses the documentation attached to those fields. A proposed fix to the Go AST exists as an open change request but has not been merged, which means until it is, the fix has to happen in the tool.

betteralign's answer is a small in-tree package, `internal/dstmin`, which 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 finishing with `go/format.Source` for column realignment. It replaced a previous dependency on the `sirkon/dst` project with roughly 900 lines of focused code, and the design rationale and benchmark comparison live in `internal/dstmin/README.md`.

Five sort criteria, applied stably

The tool pursues two goals. The first is minimizing struct size by sorting fields in descending alignment order and placing zero-sized fields first, which reduces internal padding and avoids the one-byte tail the runtime adds when a struct ends in a zero-sized field. The second is lowering the ptrdata-based scan-work estimate the collector uses for pacing, by grouping pointer-bearing fields before pointer-free ones.

The sort itself has five criteria, applied in order. Zero-sized types come first. Then higher alignment first. Then pointer-bearing types before pointer-free types. Among pointer-bearing types, those with fewer trailing non-pointer bytes come first, which minimizes ptrdata. Finally, larger size first.

The example in the documentation shows how much this matters. A struct declared as:

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
}

is 24 bytes on a 64-bit platform as declared. The `Flag` sits at offset 0, seven 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.

Two implementation details are worth noting. The sort is stable, so fields of equal sort rank keep their original relative order, whereas upstream uses `sort.Slice`, which is unstable and may permute equal-ranked fields differently between runs. That means running the upstream tool twice on the same file can produce different output, which is a specific kind of unpleasant in a CI pipeline.

ptrdata, and what the Green Tea GC changed

The second goal needs some context, because the mechanism is not simply fewer bytes to scan.

The `ptrdata` value is the head prefix of a value that the runtime records as potentially holding pointers, and grouping pointer-bearing fields at the front shrinks it. On Go versions before 1.26 this also directly reduced the bytes scanned per value, because the collector stopped scanning at the last pointer in a value. Under the Green Tea GC, which the documentation says is the default in Go 1.26, marking is span-based rather than per-value, so the benefit is the smaller ptrdata feeding GC pacing rather than fewer bytes physically scanned.

That is a genuinely important nuance for anyone deciding whether the tool is worth running. The struct size win is unchanged. The GC win changes character with the runtime: before 1.26 it was a direct reduction in scanning work, afterwards it is an input to pacing behaviour. Anyone benchmarking this on Go 1.26 or later should be measuring the right thing.

Diagnostics carry the same information in a friendlier form. Instead of only showing the before and after sizes, betteralign prefixes the message with the actual byte saving, for example `8 bytes saved: struct of size 24 could be 16`.

Positional literals are reported, never rewritten

The safety rules are the most useful part of the fork for a real repository, and they all point the same way: report, do not rewrite.

Positional composite literals are detected anywhere in the package, a literal written as `T{1, 2, 3}` rather than `T{a: 1, b: 2, c: 3}`. When one is found, the tool reports the would-be saving without rewriting the struct, because reordering the fields would silently re-map the literal's elements at the next build. That is a data corruption bug, not a compile error, which is exactly the kind of thing a rewriting tool must not do silently.

The scope is narrowed the same way. Only named struct types declared as `type T struct { ... }` are analyzed. Anonymous structs, whether nested struct-typed fields, struct literals or `var x struct{...}` declarations, are skipped, so the tool never rewrites the riskier unnamed shapes.

And the rewrite itself is careful about grouped declarations. When `-apply` rewrites a struct, fields declared with multiple names such as `A, B int` are kept together as a single grouped declaration and moved as a unit, so the byte-span reorder never splits them. Writes are atomic, to prevent corruption or data loss on rewrite, with the note that this does not apply on Windows, which links to the relevant Go issue.

There is also a crash fix worth knowing about, because it is the kind of bug that turns a tool run into an outage. Upstream's `ptrdata` implementation ends with `panic("impossible")`, which fires when an uninstantiated type parameter appears as a field type, since `(*types.TypeParam).Underlying()` returns itself and falls through every switch case. betteralign replaces the panic with a conservative one-word fallback.

Opt-in, opt-out and generated code

The tool gives you several ways to control its scope, and they are configured with source comments rather than with configuration files.

Generated files are skipped, identified either by a known suffix, `_generated.go`, `_gen.go`, `.gen.go`, `.pb.go` and `.pb.gw.go`, or by a package-level comment containing `Code generated by ... DO NOT EDIT.`. Test files are skipped by the `_test.go` suffix.

Individual structs can be excluded with a `// betteralign:ignore` directive placed inside the struct body. And there is an opt-in mode, where only structs annotated with `// betteralign:check` on the type declaration are checked, with the directive on a surrounding `type ( ... )` block opting in every spec inside the group.

That combination is what makes the tool usable on a large code base. The default is to consider everything named, and the opt-in mode lets you start with one package, or one group of structs, and widen the scope as you trust the results.

The remaining scope rules are about the analysis itself. The tool analyzes only named struct types, and it caches per-type size, alignment and pointer-scan results within each analysis pass, which avoids redundant recomputation for types that recur across many struct fields, something the documentation calls common in protobuf-generated code. It also adapts automatically to CPU and memory constraints in containerized environments such as Docker, Kubernetes, LXC and LXD, and the module depends on `github.com/KimMachineGun/automemlimit` for that purpose.

The whole-file trade-off, and what it rules out

There is one limitation the documentation is upfront about, and it is the reason the whole-file rewrite exists at all. 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.

That is a real cost for a team that runs golangci-lint across its repositories. There is no way to apply the fix through the linter's own autofix path, so the workflow is a separate command that rewrites files and a commit that shows the reordering.

There is a second limitation, stated as a note: this is a single-pass tool, and achieving a fully optimal layout may require running it more than once. Reordering fields changes the struct, which changes the alignment and pointer layout of other structs that embed it, so the result of one pass is the input of the next.

For provenance, the repository also carries the standard Go project furniture: a `betteralign.go` at the root, a `cmd/` directory, `internal/` packages, `testdata/`, unit and fuzz tests including `betteralign_fuzz_test.go` and `fuzz_crashers_test.go`, a `Taskfile.yml`, a `.golangci.yml` and a `.goreleaser.yml` for release packaging. The module is `github.com/dkorunic/betteralign`, it declares Go 1.27, and it is BSD-3-Clause licensed. Releases move quickly, with v0.15.1 on 2026-09-08, v0.15.2 on 2026-09-09 and v0.15.3 on 2026-10-10, and the last push was on 2026-10-10.

Editorial conclusion

Adopt betteralign if your Go code base has large numbers of structs whose field order is arbitrary, since the size and pointer-scan savings are real and the fork removes the comment erasure that makes the upstream pass hard to run on a real repository. Do not adopt it for a codebase where struct field order is load bearing, such as code that relies on positional composite literals, which the tool reports but refuses to rewrite, and do not expect it to integrate with golangci-lint's autofix, which it states is not supported. Verify first that your Go toolchain matches the module, which declares Go 1.27, that you run the analyzer more than once if you want a fully optimal layout because it is a single pass tool, and that the whole-file rewrite is acceptable for your workflow before you use the apply flag.

Frequently asked questions

What is betteralign and how does it differ from fieldalignment?

betteralign is a fork of the official Go fieldalignment analysis pass with the alignment logic mostly unchanged, and it differs by preserving field, doc and floating comments on rewrite, by reporting rather than rewriting structs with positional composite literals, by skipping anonymous structs, by using a stable sort, by writing files atomically, and by fixing an upstream panic on packages with generic struct fields.

How do I tell betteralign to ignore a struct or check only some structs?

Place a `// betteralign:ignore` comment inside the struct body to skip that struct, or use the opt-in mode, where only structs annotated with `// betteralign:check` on the type declaration are checked. A directive on a surrounding `type ( ... )` block opts in every spec in the group.

Does betteralign support golangci-lint autofix?

No. The project states that auto-fix integration with golangci-lint is not supported, and that the `-fix` flag from the analysis package is treated as an alias for `-apply`, because partial rewrites via `SuggestedFixes` are not possible.

Does betteralign change struct field order in code with positional composite literals?

No. It detects positional composite literals such as `T{1, 2, 3}` anywhere in the package and reports the would-be saving without rewriting the struct, since reordering the fields would silently re-map the literal's elements at the next build.

How many times do I need to run betteralign?

The documentation notes that it is a single-pass tool and that achieving a fully optimal layout may require running it more than once, because reordering one struct changes the alignment of structs that contain it.

Official sources

  1. dkorunic/betteralign on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dkorunic-betteralign.svg)](https://hysenlabs.com/projects/dkorunic-betteralign)

Was this useful?