GaijinEntertainment/go-exhaustruct

golang analyzer that finds structures with uninitialized fields

Go

207

223 commits

updated Sep 4, 2026

See the code

README

exhaustruct

Package Version Go version GitHub Workflow Status (with branch) License


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.

Installation

go install dev.gaijin.team/go/exhaustruct/v5/cmd/exhaustruct@latest

How It Works

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
}

Modes of Operation

Implicit Mode (Default)

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.

Explicit Mode

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

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.

On Type Definitions

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
}

On Struct Literals

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{}
}

On Fields

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
}

Directive Priority

When multiple directives or patterns apply, priority is (highest first):

  1. Literal //exhaustruct:ignore
  2. Literal //exhaustruct:enforce
  3. Type-level ignore (directive or -ignore-rx pattern)
  4. Type-level enforce (directive or -enforce-rx pattern)
  5. Mode default (implicit=check, explicit=skip)

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.*' ./...

Configuration

Type Selection Flags

FlagDescription
-explicitEnable explicit mode (opt-in checking)
-enforce-rxRegex for types to check under -explicit, and for Type#Field paths to require (repeatable; see the priority rules above)
-ignore-rxRegex pattern for types to skip (repeatable)
-optional-rxRegex pattern for types/fields to mark optional (repeatable)

Empty Literal Allowances

FlagDescription
-allow-emptyAllow all empty struct literals globally
-allow-empty-rxRegex pattern for types allowed to be empty (repeatable)
-allow-empty-returnsAllow empty literals in return statements
-allow-empty-declarationsAllow empty literals in var and := declarations
-allow-empty-blank-assignmentsAllow empty literals the blank identifier receives, as in var _ Iface = T{}

Output Flags

FlagDescription
-report-full-type-pathShow full package path in errors (e.g., net/http.Cookie)

Pattern Format

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 type
  • github\.com/user/repo/pkg\..* — matches all types in a package
  • .*\.<anonymous> — matches all anonymous structs
  • mypackage\.<anonymous>#Field — matches field Field in anonymous structs in mypackage

Special Behaviors

Error Returns

Empty 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
}

Unexported Fields

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
}

Blank Fields

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
}

Embedded Fields

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.

Derived Types and Aliases

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.

Type Parameters

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.

Migration from v4

New Features in v5

  • Explicit mode (-explicit): Opt-in checking instead of check-all
  • Optional patterns (-optional-rx): Mark all fields of matching types as optional
  • Field patterns: -enforce-rx and -optional-rx can now match individual fields using Type#Field syntax
  • Type-level directives: //exhaustruct:enforce, //exhaustruct:ignore, and //exhaustruct:optional can be placed on type definitions
  • Field-level enforce: //exhaustruct:enforce on fields forces them to be required even when the type is optional
  • Blank assignments (-allow-empty-blank-assignments): Allow empty literals the blank identifier receives, such as the compile-time interface check var _ Iface = T{}

Flag Renames

v4v5
-include-rx / -i-enforce-rx
-exclude-rx / -e-ignore-rx

[!IMPORTANT] -include-rx and -enforce-rx are 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 — -explicit sets the scope, -enforce-rx selects 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 -explicit leaves 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 superseded exhaustruct:

linters:
  settings:
    exhaustruct_v5:
      explicit-mode: true
      enforce-patterns:
        - '.*\.Config'

Struct Tags Deprecated

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.

analysis
golang
lint
structures

Significant stargazers

Maria Ines Parnisari

212 followers · starred Oct 2025

strager

1,039 followers · starred Sep 2024

Sigrid

262 followers · starred Mar 2023

Marat Reimers

38 followers · starred Jun 2022

GaijinEntertainment/go-exhaustruct

golang analyzer that finds structures with uninitialized fields

Go

207

223 commits

updated Sep 4, 2026

See the code

README

exhaustruct

Package Version Go version GitHub Workflow Status (with branch) License


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.

Installation

go install dev.gaijin.team/go/exhaustruct/v5/cmd/exhaustruct@latest

How It Works

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
}

Modes of Operation

Implicit Mode (Default)

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.

Explicit Mode

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

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.

On Type Definitions

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
}

On Struct Literals

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{}
}

On Fields

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
}

Directive Priority

When multiple directives or patterns apply, priority is (highest first):

  1. Literal //exhaustruct:ignore
  2. Literal //exhaustruct:enforce
  3. Type-level ignore (directive or -ignore-rx pattern)
  4. Type-level enforce (directive or -enforce-rx pattern)
  5. Mode default (implicit=check, explicit=skip)

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.*' ./...

Configuration

Type Selection Flags

FlagDescription
-explicitEnable explicit mode (opt-in checking)
-enforce-rxRegex for types to check under -explicit, and for Type#Field paths to require (repeatable; see the priority rules above)
-ignore-rxRegex pattern for types to skip (repeatable)
-optional-rxRegex pattern for types/fields to mark optional (repeatable)

Empty Literal Allowances

FlagDescription
-allow-emptyAllow all empty struct literals globally
-allow-empty-rxRegex pattern for types allowed to be empty (repeatable)
-allow-empty-returnsAllow empty literals in return statements
-allow-empty-declarationsAllow empty literals in var and := declarations
-allow-empty-blank-assignmentsAllow empty literals the blank identifier receives, as in var _ Iface = T{}

Output Flags

FlagDescription
-report-full-type-pathShow full package path in errors (e.g., net/http.Cookie)

Pattern Format

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 type
  • github\.com/user/repo/pkg\..* — matches all types in a package
  • .*\.<anonymous> — matches all anonymous structs
  • mypackage\.<anonymous>#Field — matches field Field in anonymous structs in mypackage

Special Behaviors

Error Returns

Empty 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
}

Unexported Fields

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
}

Blank Fields

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
}

Embedded Fields

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.

Derived Types and Aliases

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.

Type Parameters

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.

Migration from v4

New Features in v5

  • Explicit mode (-explicit): Opt-in checking instead of check-all
  • Optional patterns (-optional-rx): Mark all fields of matching types as optional
  • Field patterns: -enforce-rx and -optional-rx can now match individual fields using Type#Field syntax
  • Type-level directives: //exhaustruct:enforce, //exhaustruct:ignore, and //exhaustruct:optional can be placed on type definitions
  • Field-level enforce: //exhaustruct:enforce on fields forces them to be required even when the type is optional
  • Blank assignments (-allow-empty-blank-assignments): Allow empty literals the blank identifier receives, such as the compile-time interface check var _ Iface = T{}

Flag Renames

v4v5
-include-rx / -i-enforce-rx
-exclude-rx / -e-ignore-rx

[!IMPORTANT] -include-rx and -enforce-rx are 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 — -explicit sets the scope, -enforce-rx selects 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 -explicit leaves 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 superseded exhaustruct:

linters:
  settings:
    exhaustruct_v5:
      explicit-mode: true
      enforce-patterns:
        - '.*\.Config'

Struct Tags Deprecated

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.

analysis
golang
lint
structures

Significant stargazers

Maria Ines Parnisari

212 followers · starred Oct 2025

strager

1,039 followers · starred Sep 2024

Sigrid

262 followers · starred Mar 2023

Marat Reimers

38 followers · starred Jun 2022

Languages

Go

100.0%