Awesome Go

structalign

CategoryCode Analysis
SubcategoryCode Analysis
Stars9

Shows how a struct's fields could be reordered to use less memory, printing a diff instead of rewriting files

About structalign

This large README is shown as a plain-text preview. Read the full README at the source

# structalign

[![Latest release](https://img.shields.io/github/release/peczenyj/structalign.svg)](https://github.com/peczenyj/structalign/releases/latest)
![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.25-%23007d9c)
[![GoDoc](https://pkg.go.dev/badge/github.com/peczenyj/structalign)](http://pkg.go.dev/github.com/peczenyj/structalign)
[![CI](https://github.com/peczenyj/structalign/actions/workflows/ci.yml/badge.svg)](https://github.com/peczenyj/structalign/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/peczenyj/structalign/graph/badge.svg)](https://codecov.io/gh/peczenyj/structalign)
[![Report card](https://goreportcard.com/badge/github.com/peczenyj/structalign)](https://goreportcard.com/report/github.com/peczenyj/structalign)
[![CodeQL](https://github.com/peczenyj/structalign/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/peczenyj/structalign/actions/workflows/github-code-scanning/codeql)
[![Dependency Review](https://github.com/peczenyj/structalign/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/peczenyj/structalign/actions/workflows/dependency-review.yml)
[![License](https://img.shields.io/github/license/peczenyj/structalign)](./LICENSE)
[![GitHub Release Date](https://img.shields.io/github/release-date/peczenyj/structalign.svg)](https://github.com/peczenyj/structalign/releases/latest)
[![Last commit](https://img.shields.io/github/last-commit/peczenyj/structalign.svg)](https://github.com/peczenyj/structalign/commit/HEAD)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/peczenyj/structalign/blob/main/CONTRIBUTING.md#pull-request-process)
[![SLSA Build Level 2](https://img.shields.io/badge/SLSA-Build_L2-green.svg)](https://github.com/peczenyj/structalign/attestations)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/peczenyj/structalign/badge)](https://scorecard.dev/viewer/?uri=github.com/peczenyj/structalign)
[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/13027/badge)](https://bestpractices.coreinfrastructure.org/projects/13027)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#code-analysis)

> See how reordering a Go struct's fields could save memory — as a **diff**, not a
> rewrite — plus a per-field **layout inspector**.

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.

![structalign colored unified-diff output against the bundled sample](docs/diff.png)

## Quick start

Install:

```sh
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:

```sh
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        | ✅ | ✅ | — | ✅ |

[fa]: https://github.com/golang/tools/tree/master/go/analysis/passes/fieldalignment
[ba]: https://github.com/dkorunic/betteralign
[sl]: https://github.com/dominikh/go-tools/tree/master/cmd/structlayout

## 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`).

```sh
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_<FLAG>`, 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:

```ini
# .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:

```sh
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:

```sh
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:

```sh
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.

Frequently Asked Questions

What is structalign?

structalign is a Code Analysis library for the Go programming language. Shows how a struct's fields could be reordered to use less memory, printing a diff instead of rewriting files

How do I install structalign?

Install structalign with the Go module system using `go get peczenyj/structalign`. Check the repository for the current installation instructions.

What category does structalign belong to?

structalign is listed under Code Analysis, specifically Code Analysis.

← Back to Code Analysis