golang analyzer that finds structures with uninitialized fields
Go
207
223 commits
updated Sep 4, 2026
exhaustruct is a golang analyzer that finds structures with uninitialized fields.
If you're using golangci-lint, refer to the linters settings for the most up-to-date configuration guidance.
go install dev.gaijin.team/go/exhaustruct/v5/cmd/exhaustruct@latest
The analyzer inspects struct literals in your code and reports when required fields are not initialized:
type User struct {
Name string
Email string
Age int
}
func example() {
_ = User{Name: "alice"} // ERROR: missing fields Email, Age
_ = User{Name: "alice", Email: "alice@example.com", Age: 30} // OK
}
By default, all struct literals are checked. This ensures complete initialization across your codebase. Use ignore patterns or directives to exclude specific types or literals.
With -explicit flag, the analyzer only checks structs that are explicitly
marked for enforcement — either via //exhaustruct:enforce directive or
-enforce-rx patterns. This is useful for large codebases where you want
opt-in checking for critical types only.
// Only checked in explicit mode when marked
//exhaustruct:enforce
type Config struct {
Host string
Port int
}
// Not checked in explicit mode (unless matched by pattern)
type Options struct {
Timeout int
}
Comment directives provide fine-grained control over checking behavior.
They can be placed on the line above or on the same line as the target, in
either comment form: //exhaustruct:optional or /*exhaustruct:optional*/.
The block form can also stand ahead of code on its own line, which is how a
field name is annotated inline.
The directive opens the comment, as //go:build does. A space after the
comment marker makes the comment prose, and so does a space inside the
directive list: //exhaustruct:optional, enforce reads as optional followed
by prose, and is reported because the prose names a directive.
Available directives: enforce, ignore, optional
// All literals of this type will be checked (useful in explicit mode)
//exhaustruct:enforce
type Config struct {
Host string
Port int
}
// All literals of this type will be skipped
//exhaustruct:ignore
type InternalState struct {
cache map[string]any
}
// All fields of this type are optional
//exhaustruct:optional
type Options struct {
Timeout int
MaxConns int
}
Available directives: enforce, ignore
func example() {
//exhaustruct:ignore — skip this specific literal
_ = Config{}
_ = Config{} //exhaustruct:ignore — inline form also works
//exhaustruct:enforce — check even if type is normally ignored
_ = InternalState{}
}
Available directives: optional, enforce
type Server struct {
Host string
Port int
//exhaustruct:optional — this field is not required
Timeout int
MaxConns int //exhaustruct:optional — inline form also works
//exhaustruct:enforce — required even if type is marked optional
Logger Logger
}
When multiple directives or patterns apply, priority is (highest first):
//exhaustruct:ignore//exhaustruct:enforce-ignore-rx pattern)-enforce-rx pattern)Rules 1 and 2 meet when both reach one literal from different lines: an
ignore above a statement and an enforce above a literal nested inside it,
or a single directive list such as //exhaustruct:ignore,enforce. Two
directives targeting the same line are a conflict instead, reported rather than
resolved by this order.
A type-level -enforce-rx selects types only under -explicit: in implicit
mode rule 5 already checks the type. Rule 3 applies in both modes, which is why
-ignore-rx wins wherever the two overlap.
Field patterns (Type#Field) are a separate decision: they choose which fields
are required inside a type that is already being checked, in either mode. A
pattern matching the field path and not the path of the type holding it is a
rule written for that field, and it outranks what the type says. One pattern
broad enough to match both names no field in particular, and the type-level
decision stands:
# Host is required: the pattern names the field and not the type
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config#Host' ./...
# Host is required still: a second pattern for the type says nothing about
# which of its fields the first one names
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config#Host' \
-enforce-rx 'pkg\.Config' ./...
# Host is not required: one pattern matching the type as well as the field
# names no field in particular, so -optional-rx on the type decides
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config.*' ./...
| Flag | Description |
|---|---|
-explicit | Enable explicit mode (opt-in checking) |
-enforce-rx | Regex for types to check under -explicit, and for Type#Field paths to require (repeatable; see the priority rules above) |
-ignore-rx | Regex pattern for types to skip (repeatable) |
-optional-rx | Regex pattern for types/fields to mark optional (repeatable) |
| Flag | Description |
|---|---|
-allow-empty | Allow all empty struct literals globally |
-allow-empty-rx | Regex pattern for types allowed to be empty (repeatable) |
-allow-empty-returns | Allow empty literals in return statements |
-allow-empty-declarations | Allow empty literals in var and := declarations |
-allow-empty-blank-assignments | Allow empty literals the blank identifier receives, as in var _ Iface = T{} |
| Flag | Description |
|---|---|
-report-full-type-path | Show full package path in errors (e.g., net/http.Cookie) |
All regex patterns (-*-rx flags) match against full paths.
For types:
package/path.TypeName
For fields:
package/path.TypeName#FieldName
For anonymous structs, use <anonymous> as the type name:
package/path.<anonymous>
Examples:
net/http\.Request — matches type http.Request.*\.Config — matches any type named Config.*\.Server#Timeout — matches field Timeout in any Server typegithub\.com/user/repo/pkg\..* — matches all types in a package.*\.<anonymous> — matches all anonymous structsmypackage\.<anonymous>#Field — matches field Field in anonymous structs in mypackageEmpty struct literals are automatically allowed in return statements when accompanied by a non-nil error value:
func LoadConfig() (Config, error) {
if err := validate(); err != nil {
return Config{}, err // OK: error return
}
return Config{Host: "localhost", Port: 8080}, nil
}
Fields that are unexported and belong to external packages are never required, as they cannot be initialized from outside the package:
import "external/pkg"
func example() {
// If pkg.Server has unexported fields, they are not required
_ = pkg.Server{Host: "localhost"} // OK
}
A blank field (_) has no name a keyed literal can write, so a keyed or empty
literal is never reported for leaving it out. A positional literal has to supply
a value for it, as Go requires:
type NoCompare struct {
_ [0]func()
A int
}
func example() {
_ = NoCompare{A: 1} // OK
_ = NoCompare{} // missing field A
_ = NoCompare{[0]func(){}, 1} // OK
}
From Go 1.27 a composite literal may name a promoted field in place of the embedded field carrying it (golang/go#77245), so an embedded field a promoted name reaches is reported field by field. One no promoted name reaches is reported whole:
type Base struct {
ID string
Name string
}
type Server struct {
Base
Port int
}
func example() {
_ = Server{ID: "1", Name: "a", Port: 8080} // OK: promoted names fill Base
_ = Server{ID: "1", Port: 8080} // missing field Name
_ = Server{Port: 8080} // missing field Base
}
The Go version of the file holding the literal decides this, not the version the module declares. Below 1.27 the embedded field is the only way in, so it is reported whole in every case.
An embedded field the enclosing type leaves unrequired is descended into all
the same, because a field marked //exhaustruct:enforce below it outranks the
type holding it. The embedded field itself stays unreported where a promoted
name reaches the enforced field; below 1.27 it is the one key that does, so it
is what is reported. An embedded field marked optional in its own right is not
descended into, since that excludes what it promotes along with it:
type Inner struct {
ID string
//exhaustruct:enforce
Must string
}
//exhaustruct:optional
type Holder struct {
Inner
Port int
}
//exhaustruct:optional
type Own struct {
//exhaustruct:optional
Inner
Port int
}
func example() {
_ = Holder{Port: 8080} // missing field Must
_ = Own{Port: 8080} // OK: Inner is optional in its own right
}
A field no literal can write is required by nothing, whatever a directive or a pattern says about it: an unexported field of another package's struct, a promoted field a shallower one shadows, and a name two embedded fields promote at one depth, which Go resolves to nothing at all.
Type aliases and derived types inherit field-level directives from their underlying struct, but type-level directives are not inherited:
//exhaustruct:enforce
type Config struct {
Host string
//exhaustruct:optional
Timeout int
}
type MyConfig = Config // alias: inherits optional Timeout, but NOT enforce
type ExtConfig Config // derived: inherits optional Timeout, but NOT enforce
func example() {
// Config is enforced (has type-level directive)
_ = Config{} // ERROR in explicit mode
// MyConfig inherits field optionality but not type enforcement
_ = MyConfig{Host: "localhost"} // OK: Timeout is optional (inherited)
_ = MyConfig{} // OK in explicit mode: type not enforced
}
To enforce checking on derived or aliased types, add directives on their definitions:
//exhaustruct:enforce
type StrictConfig = Config // now enforced independently
//exhaustruct:enforce
type StrictExtConfig Config // now enforced independently
Field-level directives (//exhaustruct:optional, //exhaustruct:enforce on fields)
apply to the struct's field definitions and are shared by all types using that
underlying struct. Type-level directives control whether literals of that
specific type are checked and must be specified separately for each type.
A name given to a pointer follows the same rule. type P = *Config and
type Q *Config each carry their own type-level directives, and a literal that
elides &Config under one of them is checked and reported as that type. A plain
*Config declares nothing, so Config names such a literal.
A literal of a type parameter is checked against the struct its constraint's terms share, and reported under the parameter's name:
type configish interface {
~struct {
Host string
Port int
}
}
func New[T configish]() T {
return T{Host: "localhost"} // T is missing field Port
}
Field-level directives on a term's declaration apply. Two declarations of one shape whose fields are annotated differently give the constraint no single answer, and literals of such a parameter are not checked. A type parameter is declared in a signature, so it carries no type-level directive of its own.
-explicit): Opt-in checking instead of check-all-optional-rx): Mark all fields of matching types
as optional-enforce-rx and -optional-rx can now match individual
fields using Type#Field syntax//exhaustruct:enforce, //exhaustruct:ignore,
and //exhaustruct:optional can be placed on type definitions//exhaustruct:enforce on fields forces them to be
required even when the type is optional-allow-empty-blank-assignments): Allow empty literals
the blank identifier receives, such as the compile-time interface check
var _ Iface = T{}| v4 | v5 |
|---|---|
-include-rx / -i | -enforce-rx |
-exclude-rx / -e | -ignore-rx |
[!IMPORTANT]
-include-rxand-enforce-rxare not the same control, so the rename is not the whole migration.v4 folded two decisions into
-include-rx: how wide the checking is, and which types it covers. v5 separates them —-explicitsets the scope,-enforce-rxselects types within it — so a v4 pattern list maps onto both flags:# v4 exhaustruct -include-rx '.*\.Config' ./... # v5 exhaustruct -explicit -enforce-rx '.*\.Config' ./...Carrying the patterns over without
-explicitleaves the analyzer in implicit mode, where every type is checked and the patterns select nothing.In golangci-lint the same pair goes under
exhaustruct_v5, which is a different linter from the supersededexhaustruct:linters: settings: exhaustruct_v5: explicit-mode: true enforce-patterns: - '.*\.Config'
Struct tags like exhaustruct:"optional" are no longer supported. Use comment
directives instead:
// v4 (deprecated)
type Server struct {
Host string
Timeout int `exhaustruct:"optional"`
}
// v5
type Server struct {
Host string
//exhaustruct:optional
Timeout int
}
Run with -fix to automatically migrate struct tags to comment directives:
exhaustruct -fix ./...
-fix writes the directive where the tag stood when the tag ends the field's
line, and on the line above the field otherwise. Both forms name the same
target, so the field above migrates to:
type Server struct {
Host string
Timeout int //exhaustruct:optional
}
A field that already carries an exhaustruct directive keeps it: the deprecated tag is removed and nothing is written over it.
Every deprecated tag is reported and migrated, including one on a type
-ignore-rx or //exhaustruct:ignore excludes from checking. The move off v4
syntax happens once, and a tag left behind outlives the setting that hid it.
212 followers · starred Oct 2025
1,039 followers · starred Sep 2024
262 followers · starred Mar 2023
38 followers · starred Jun 2022
Go
100.0%
golang analyzer that finds structures with uninitialized fields
Go
207
223 commits
updated Sep 4, 2026
exhaustruct is a golang analyzer that finds structures with uninitialized fields.
If you're using golangci-lint, refer to the linters settings for the most up-to-date configuration guidance.
go install dev.gaijin.team/go/exhaustruct/v5/cmd/exhaustruct@latest
The analyzer inspects struct literals in your code and reports when required fields are not initialized:
type User struct {
Name string
Email string
Age int
}
func example() {
_ = User{Name: "alice"} // ERROR: missing fields Email, Age
_ = User{Name: "alice", Email: "alice@example.com", Age: 30} // OK
}
By default, all struct literals are checked. This ensures complete initialization across your codebase. Use ignore patterns or directives to exclude specific types or literals.
With -explicit flag, the analyzer only checks structs that are explicitly
marked for enforcement — either via //exhaustruct:enforce directive or
-enforce-rx patterns. This is useful for large codebases where you want
opt-in checking for critical types only.
// Only checked in explicit mode when marked
//exhaustruct:enforce
type Config struct {
Host string
Port int
}
// Not checked in explicit mode (unless matched by pattern)
type Options struct {
Timeout int
}
Comment directives provide fine-grained control over checking behavior.
They can be placed on the line above or on the same line as the target, in
either comment form: //exhaustruct:optional or /*exhaustruct:optional*/.
The block form can also stand ahead of code on its own line, which is how a
field name is annotated inline.
The directive opens the comment, as //go:build does. A space after the
comment marker makes the comment prose, and so does a space inside the
directive list: //exhaustruct:optional, enforce reads as optional followed
by prose, and is reported because the prose names a directive.
Available directives: enforce, ignore, optional
// All literals of this type will be checked (useful in explicit mode)
//exhaustruct:enforce
type Config struct {
Host string
Port int
}
// All literals of this type will be skipped
//exhaustruct:ignore
type InternalState struct {
cache map[string]any
}
// All fields of this type are optional
//exhaustruct:optional
type Options struct {
Timeout int
MaxConns int
}
Available directives: enforce, ignore
func example() {
//exhaustruct:ignore — skip this specific literal
_ = Config{}
_ = Config{} //exhaustruct:ignore — inline form also works
//exhaustruct:enforce — check even if type is normally ignored
_ = InternalState{}
}
Available directives: optional, enforce
type Server struct {
Host string
Port int
//exhaustruct:optional — this field is not required
Timeout int
MaxConns int //exhaustruct:optional — inline form also works
//exhaustruct:enforce — required even if type is marked optional
Logger Logger
}
When multiple directives or patterns apply, priority is (highest first):
//exhaustruct:ignore//exhaustruct:enforce-ignore-rx pattern)-enforce-rx pattern)Rules 1 and 2 meet when both reach one literal from different lines: an
ignore above a statement and an enforce above a literal nested inside it,
or a single directive list such as //exhaustruct:ignore,enforce. Two
directives targeting the same line are a conflict instead, reported rather than
resolved by this order.
A type-level -enforce-rx selects types only under -explicit: in implicit
mode rule 5 already checks the type. Rule 3 applies in both modes, which is why
-ignore-rx wins wherever the two overlap.
Field patterns (Type#Field) are a separate decision: they choose which fields
are required inside a type that is already being checked, in either mode. A
pattern matching the field path and not the path of the type holding it is a
rule written for that field, and it outranks what the type says. One pattern
broad enough to match both names no field in particular, and the type-level
decision stands:
# Host is required: the pattern names the field and not the type
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config#Host' ./...
# Host is required still: a second pattern for the type says nothing about
# which of its fields the first one names
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config#Host' \
-enforce-rx 'pkg\.Config' ./...
# Host is not required: one pattern matching the type as well as the field
# names no field in particular, so -optional-rx on the type decides
exhaustruct -optional-rx 'pkg\.Config' -enforce-rx 'pkg\.Config.*' ./...
| Flag | Description |
|---|---|
-explicit | Enable explicit mode (opt-in checking) |
-enforce-rx | Regex for types to check under -explicit, and for Type#Field paths to require (repeatable; see the priority rules above) |
-ignore-rx | Regex pattern for types to skip (repeatable) |
-optional-rx | Regex pattern for types/fields to mark optional (repeatable) |
| Flag | Description |
|---|---|
-allow-empty | Allow all empty struct literals globally |
-allow-empty-rx | Regex pattern for types allowed to be empty (repeatable) |
-allow-empty-returns | Allow empty literals in return statements |
-allow-empty-declarations | Allow empty literals in var and := declarations |
-allow-empty-blank-assignments | Allow empty literals the blank identifier receives, as in var _ Iface = T{} |
| Flag | Description |
|---|---|
-report-full-type-path | Show full package path in errors (e.g., net/http.Cookie) |
All regex patterns (-*-rx flags) match against full paths.
For types:
package/path.TypeName
For fields:
package/path.TypeName#FieldName
For anonymous structs, use <anonymous> as the type name:
package/path.<anonymous>
Examples:
net/http\.Request — matches type http.Request.*\.Config — matches any type named Config.*\.Server#Timeout — matches field Timeout in any Server typegithub\.com/user/repo/pkg\..* — matches all types in a package.*\.<anonymous> — matches all anonymous structsmypackage\.<anonymous>#Field — matches field Field in anonymous structs in mypackageEmpty struct literals are automatically allowed in return statements when accompanied by a non-nil error value:
func LoadConfig() (Config, error) {
if err := validate(); err != nil {
return Config{}, err // OK: error return
}
return Config{Host: "localhost", Port: 8080}, nil
}
Fields that are unexported and belong to external packages are never required, as they cannot be initialized from outside the package:
import "external/pkg"
func example() {
// If pkg.Server has unexported fields, they are not required
_ = pkg.Server{Host: "localhost"} // OK
}
A blank field (_) has no name a keyed literal can write, so a keyed or empty
literal is never reported for leaving it out. A positional literal has to supply
a value for it, as Go requires:
type NoCompare struct {
_ [0]func()
A int
}
func example() {
_ = NoCompare{A: 1} // OK
_ = NoCompare{} // missing field A
_ = NoCompare{[0]func(){}, 1} // OK
}
From Go 1.27 a composite literal may name a promoted field in place of the embedded field carrying it (golang/go#77245), so an embedded field a promoted name reaches is reported field by field. One no promoted name reaches is reported whole:
type Base struct {
ID string
Name string
}
type Server struct {
Base
Port int
}
func example() {
_ = Server{ID: "1", Name: "a", Port: 8080} // OK: promoted names fill Base
_ = Server{ID: "1", Port: 8080} // missing field Name
_ = Server{Port: 8080} // missing field Base
}
The Go version of the file holding the literal decides this, not the version the module declares. Below 1.27 the embedded field is the only way in, so it is reported whole in every case.
An embedded field the enclosing type leaves unrequired is descended into all
the same, because a field marked //exhaustruct:enforce below it outranks the
type holding it. The embedded field itself stays unreported where a promoted
name reaches the enforced field; below 1.27 it is the one key that does, so it
is what is reported. An embedded field marked optional in its own right is not
descended into, since that excludes what it promotes along with it:
type Inner struct {
ID string
//exhaustruct:enforce
Must string
}
//exhaustruct:optional
type Holder struct {
Inner
Port int
}
//exhaustruct:optional
type Own struct {
//exhaustruct:optional
Inner
Port int
}
func example() {
_ = Holder{Port: 8080} // missing field Must
_ = Own{Port: 8080} // OK: Inner is optional in its own right
}
A field no literal can write is required by nothing, whatever a directive or a pattern says about it: an unexported field of another package's struct, a promoted field a shallower one shadows, and a name two embedded fields promote at one depth, which Go resolves to nothing at all.
Type aliases and derived types inherit field-level directives from their underlying struct, but type-level directives are not inherited:
//exhaustruct:enforce
type Config struct {
Host string
//exhaustruct:optional
Timeout int
}
type MyConfig = Config // alias: inherits optional Timeout, but NOT enforce
type ExtConfig Config // derived: inherits optional Timeout, but NOT enforce
func example() {
// Config is enforced (has type-level directive)
_ = Config{} // ERROR in explicit mode
// MyConfig inherits field optionality but not type enforcement
_ = MyConfig{Host: "localhost"} // OK: Timeout is optional (inherited)
_ = MyConfig{} // OK in explicit mode: type not enforced
}
To enforce checking on derived or aliased types, add directives on their definitions:
//exhaustruct:enforce
type StrictConfig = Config // now enforced independently
//exhaustruct:enforce
type StrictExtConfig Config // now enforced independently
Field-level directives (//exhaustruct:optional, //exhaustruct:enforce on fields)
apply to the struct's field definitions and are shared by all types using that
underlying struct. Type-level directives control whether literals of that
specific type are checked and must be specified separately for each type.
A name given to a pointer follows the same rule. type P = *Config and
type Q *Config each carry their own type-level directives, and a literal that
elides &Config under one of them is checked and reported as that type. A plain
*Config declares nothing, so Config names such a literal.
A literal of a type parameter is checked against the struct its constraint's terms share, and reported under the parameter's name:
type configish interface {
~struct {
Host string
Port int
}
}
func New[T configish]() T {
return T{Host: "localhost"} // T is missing field Port
}
Field-level directives on a term's declaration apply. Two declarations of one shape whose fields are annotated differently give the constraint no single answer, and literals of such a parameter are not checked. A type parameter is declared in a signature, so it carries no type-level directive of its own.
-explicit): Opt-in checking instead of check-all-optional-rx): Mark all fields of matching types
as optional-enforce-rx and -optional-rx can now match individual
fields using Type#Field syntax//exhaustruct:enforce, //exhaustruct:ignore,
and //exhaustruct:optional can be placed on type definitions//exhaustruct:enforce on fields forces them to be
required even when the type is optional-allow-empty-blank-assignments): Allow empty literals
the blank identifier receives, such as the compile-time interface check
var _ Iface = T{}| v4 | v5 |
|---|---|
-include-rx / -i | -enforce-rx |
-exclude-rx / -e | -ignore-rx |
[!IMPORTANT]
-include-rxand-enforce-rxare not the same control, so the rename is not the whole migration.v4 folded two decisions into
-include-rx: how wide the checking is, and which types it covers. v5 separates them —-explicitsets the scope,-enforce-rxselects types within it — so a v4 pattern list maps onto both flags:# v4 exhaustruct -include-rx '.*\.Config' ./... # v5 exhaustruct -explicit -enforce-rx '.*\.Config' ./...Carrying the patterns over without
-explicitleaves the analyzer in implicit mode, where every type is checked and the patterns select nothing.In golangci-lint the same pair goes under
exhaustruct_v5, which is a different linter from the supersededexhaustruct:linters: settings: exhaustruct_v5: explicit-mode: true enforce-patterns: - '.*\.Config'
Struct tags like exhaustruct:"optional" are no longer supported. Use comment
directives instead:
// v4 (deprecated)
type Server struct {
Host string
Timeout int `exhaustruct:"optional"`
}
// v5
type Server struct {
Host string
//exhaustruct:optional
Timeout int
}
Run with -fix to automatically migrate struct tags to comment directives:
exhaustruct -fix ./...
-fix writes the directive where the tag stood when the tag ends the field's
line, and on the line above the field otherwise. Both forms name the same
target, so the field above migrates to:
type Server struct {
Host string
Timeout int //exhaustruct:optional
}
A field that already carries an exhaustruct directive keeps it: the deprecated tag is removed and nothing is written over it.
Every deprecated tag is reported and migrated, including one on a type
-ignore-rx or //exhaustruct:ignore excludes from checking. The move off v4
syntax happens once, and a tag left behind outlives the setting that hid it.
212 followers · starred Oct 2025
1,039 followers · starred Sep 2024
262 followers · starred Mar 2023
38 followers · starred Jun 2022
Go
100.0%