Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

The zerolint linter checks usage patterns of pointers to zero-size types in Go

License

NotificationsYou must be signed in to change notification settings

fillmore-labs/zerolint

 
 

Repository files navigation

Go ReferenceTestCodeQLCoverageGo Report CardLicense

zerolint is a Go static analysis tool (linter) that detects unnecessary or potentially incorrect usage of pointers tozero-sized types.

Motivation

Go's zero-sized types (such asstruct{} or[0]byte) occupy no memory and are useful in scenarios like channelsignaling or as map keys. However, creating pointers to these types (e.g.,&struct{} ornew(struct{})) is almostalways unnecessary and can introduce subtle bugs and overhead.

Since all values of a zero-sized type are identical, pointers to them rarely convey unique state or identity. This canmake code less clear, as readers might incorrectly assume state or identity is being managed. Furthermore, pointersthemselves are not zero-sized, leading to minor memory and performance overhead.

zerolint helps identify these patterns, promoting cleaner and potentially more efficient code.

Quickstart

Installation

Install the linter:

Homebrew

brew install fillmore-labs/tap/zerolint

Go

go install fillmore-labs.com/zerolint@latest

Eget

Installeget, then

eget fillmore-labs/zerolint

Usage

Run the linter on your project:

zerolint ./...

See below for descriptions of available command-line flags.

Optional Flags

Usage:zerolint [-flag] [package]

Flags:

  • -level<level>: Set analysis depth:
    • Basic: Basic detection of pointer issues (Default)
    • Extended: Additional checks for more complex patterns
    • Full: Most comprehensive analysis, recommended with-fix
  • -match<regex>: Limit zero-sized type detection to types matching the regex. Useful with-fix.
  • -excluded<filename>: Read types to be excluded from analysis from the specified file. The file should containfully qualified type names, one per line. See the“Exclusion File” section for more details.
  • -generated: Analyze files that contain code generation markers (e.g.,// Code generated ... DO NOT EDIT.). Bydefault, these files are skipped.
  • -zerotrace: Enable verbose logging of which typeszerolint identifies as zero-sized. Useful for building a listof excluded types.
  • -c<N>: Display N lines of context around the offending line (default: -1 for no context, 0 for only theoffending line).
  • -test: Indicates whether test files should be analyzed, too. (default: true).
  • -fix: Apply all suggested fixes automatically. Use with caution and always review the changes made by-fix.
  • -diff: With-fix, don't update the files, but print a unified diff.

Example

Consider the following Go code:

package mainimport ("errors""testing")typeDivisionByZeroErrorstruct{}func (*DivisionByZeroError)Error()string {return"division by zero"}funcReciprocal(xfloat64) (float64,error) {ifx==0 {return0,&DivisionByZeroError{}}return1/x,nil}funcTestDivisionByZero(t*testing.T) {_,err:=Reciprocal(0)if!errors.Is(err,&DivisionByZeroError{}) {t.Errorf("Expected division by zero error, but got: %v",err)}}

The test passes (Go Playground):

=== RUN   TestDivisionByZero--- PASS: TestDivisionByZero (0.00s)PASS

Runningzerolint . would produce output similar to:

/path/to/your/project/main_test.go:10:7: error interface implemented on pointer to zero-sized type "example.com/project.DivisionByZeroError" (zl:err)/path/to/your/project/main_test.go:25:6: comparison of pointer to zero-size type "example.com/project.DivisionByZeroError" with error interface (zl:cme)

Understanding the Output and Zero-Sized Diagnostics

Themain_test.go example and itszerolint output highlight a common pitfall with zero-sized types in Go. In theTestDivisionByZero function:

if!errors.Is(err,&DivisionByZeroError{}) {

the expression&DivisionByZeroError{} creates a new pointer to a zero-sized struct. Similarly, theReciprocalfunction, whenx is 0, returns another&DivisionByZeroError{}. The critical point is how these pointers arecompared.

The checkerrors.Is(err, &DivisionByZeroError{}) might not behave as intuitively expected. When the target errorpassed toerrors.Is is a pointer type (*DivisionByZeroError in this case),errors.Is first performs a directpointer address comparison, before even checking whether the error implements anIs(error) bool method.

To illustrate that these are treated as distinct pointers for comparison purposes, we can modifyDivisionByZeroErrorto be non-zero-sized:

typeDivisionByZeroErrorstruct{_int }// Make it non-zero-sized

With this change, the testTestDivisionByZero fails, confirming thaterrors.Is was indeed comparing distinctinstances based on their pointer values.

Pitfalls of Zero-Sized Pointer Comparisons

Internally, Go's runtime optimizes allocations of zero-sized types. It achieves this byreturning a pointer to a common static variable(known asruntime.zerobase) rather than allocating new memory on the heap for each instance. A consequence of thisoptimization is that different pointers to zero-sized types (e.g., multiple uses of&DivisionByZeroError{} ornew(DivisionByZeroError)) end up pointing to the same memory address. This can create the illusion that such pointerswill always compare as equal.

Despite this common runtime behavior, the Go language specificationexplicitly states that the equality of pointers to distinct zero-sizevariables is unspecified:

“pointers to distinct zero-size variables may or may not be equal.”

This means the observed consistency in pointer comparisons is an internal implementation detail of the Go runtime, not aguaranteed language feature. Relying on this behavior is a classic example ofHyrum's Lawin action:

“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observablebehaviors of your system will be depended on by somebody.”

Consequently, code that tests the equality (or inequality) of pointers to zero-sized types might compile and appear tofunction correctly under current Go versions. However, its logic is fundamentally unsound because it depends on animplementation detail not guaranteed by the language specification. Such code is at risk of breaking unexpectedly withfuture Go updates or in different compilation environments.zerolint identifies and flags these problematic usagepatterns, helping developers write more robust code that avoids this undefined behavior.

Potential Fixes

Whenzerolint flags an issue, consider these approaches:

Use a sentinel error variable (most idiomatic for errors)

This is often the clearest and most common Go practice for handling specific error conditions.

// ErrDivisionByZero is returned when attempting to divide by zero.varErrDivisionByZero=errors.New("division by zero")

This approach is preferred because comparisons likeerrors.Is(err, ErrDivisionByZero) work reliably with sentinelerror values, avoiding the pitfalls of comparing pointers to zero-sized types.

Applying Fixes withzerolint (automatic refactoring)

For many common issues identified byzerolint, you can attempt an automatic fix:

zerolint -level=full -fix ./...

The-fix flag will try to apply corrections, such as changing pointer receivers to value receivers where appropriateor modifying how zero-sized types are instantiated or compared. For the most comprehensive automatic fixing, using-level=full with-fix is recommended. This combination helps ensure thatzerolint addresses all detected issuesrelated to a specific zero-sized type, promoting consistency across its usages once the fixes are applied.

Caution: Always review changes made by-fix carefully before committing them, as automatic refactoring cansometimes have unintended consequences, especially in complex codebases.

For instance, runningzerolint -level=full -fix . on the example above transforms the code as follows:

package mainimport ("errors""testing")typeDivisionByZeroErrorstruct{}func (DivisionByZeroError)Error()string {return"division by zero"}funcReciprocal(xfloat64) (float64,error) {ifx==0 {return0,DivisionByZeroError{}}return1/x,nil}funcTestDivisionByZero(t*testing.T) {_,err:=Reciprocal(0)if!errors.Is(err,DivisionByZeroError{}) {t.Errorf("Expected division by zero error, but got: %v",err)}}

This program is correct, since the errors are compared by value, and two zero-sized variables of the same type alwayscompare equal.

Make the Type Non-Zero-Sized

If you need to maintain the custom error type structure for specific reasons (e.g., backward compatibility), or if it'snot an error type but another zero-sized struct, you can make it non-zero-sized. For errors, you can optionally provideanIs method to restore the previous behavior oferrors.Is when comparing against this error type:

typeDivisionByZeroErrorstruct{_int }// Add a non-zero fieldfunc (*DivisionByZeroError)Is(errerror)bool {// Optional for error types_,ok:=err.(*DivisionByZeroError)returnok}

While this approach is more verbose than usingerrors.New (for errors) or the original pointer-based zero-sized errorimplementation, it ensures correct, defined behavior for comparisons, making it valid Go code. This might be consideredif backward compatibility with an existing pointer-based error API is a concern, though migrating away frompointer-based zero-sized errors is generally preferable.

Exclude the Type from Analysis

If pointer usage for a specific zero-sized type is intentional, unavoidable (e.g., due to external library constraints),or you've assessed the risk and accept it, you can exclude the type fromzerolint's analysis. See the next section“Excluding Types” for details.

Excluding Types

You can instructzerolint to ignore specific zero-sized types in several ways:

Exclusion File

If you have specific zero-sized types where pointer usage is intentional or required (e.g., due to external libraryconstraints), you can exclude them using the-excluded flag with a file path. The file should contain fully qualifiedtype names, one per line.

Exampleexcludes.txt:

# zerolint excludes for my projectcompany.example/service/client.RequestOptionsexample.com/project.DivisionByZeroError

Then run:zerolint -excluded=excludes.txt ./...

This is especially useful when running with the-fix flag and dealing with types from external libraries you don'tcontrol.

Source Code Comment

If you control the source code where the zero-sized type is defined, you can add a special comment directly above thetype definition:

//zerolint:excludetypeMyIntentionalZeroSizedTypestruct{}

This comment will tellzerolint to ignore any issues forMyIntentionalZeroSizedType.

To exclude a type defined in an external package, you can declare the exclusion in your own package using avardeclaration with the blank identifier (_):

//zerolint:excludevar_ external.ZeroSizedType

Using these exclusion methods allows you to tailorzerolint's behavior to your project's specific needs.

Linter Scope and External Types

By default,zerolint analyzes all types encountered, not just those declared within your current package or module.This includes types imported from external packages (dependencies).

Whilezerolint (especially at its default analysis level) aims to flag only genuinely problematic patterns, theremight be situations where a zero-sized type from an external package is used in a way that, while flagged, is legitimateor required by that external package's API. For example, an external library might require you to pass a pointer to azero-sized option structure or for interface satisfaction in a way that cannot be altered.

zerolint itself cannot automatically determine if such a flagged usage of an external type is intentional orunavoidable within the constraints of that external library. It reports based on the general principle of avoidingunnecessary pointers to zero-sized types.

If you encounter such a scenario with an external type you cannot modify with a//zerolint:exclude comment, therecommended approach to manage these legitimate cases is:

  1. Runzerolint with the-zerotrace flag. This will provide a detailed log of all types thatzerolint identifiesas zero-sized during its analysis.
  2. Inspect this log to find the fully qualified names of the specific external types that are being flagged but whoseusage you've determined is valid.
  3. Manually add these fully qualified type names to an exclusion file, as described in the“Excluding Types” section. This will instructzerolint to ignore these specific types in futureanalyses.

This approach allows you to maintain the benefits ofzerolint for your own codebase and other dependencies whileselectively bypassing checks for specific external types where pointer usage is justified.

Diagnostic Codes

zerolint output includes diagnostic codes to help categorize the type of issue found. In the examples for eachdiagnostic code,zst is used as a placeholder for a zero-sized type definition (e.g.,type zst struct{}), andzsvrepresents a variable of that zero-sized type (e.g.,var zsv zst).

Basic Level

  • zl:cme: Comparison of pointer to zero-size type with an error interface (errors.Is(err, &zsv))
  • zl:cmp: Comparison of pointers to zero-size type (&zsv == &zsv)
  • zl:cmi: Comparison of pointer to zero-size type with interface (&zsv == any(&zst{}))
  • zl:err: Error interface implemented on pointer to zero-sized type (func (*zst) Error() string)
  • zl:emb: Embedded pointer to zero-sized type (struct{ *zst })
  • zl:der: Dereferencing pointer to zero-size variable (zsp := &zsv; _ = *zsp)
  • zl:dcl: Type declaration to pointer to zero-sized type (type zstPtr *zst)

Extended Level

  • zl:new:new called on zero-sized type (new(zst))
  • zl:nil: Cast of nil to pointer to zero-size type ((*zst)(nil))
  • zl:ret: Explicitly returning nil as pointer to zero-sized type (func f() *zst { return nil })
  • zl:cst: Cast to pointer to zero-size type ((*zst)(&struct{}{}))
  • zl:var: Variable is pointer to zero-sized type (var _ *zst)
  • zl:fld: Field points to zero-sized type (struct{ f *zst })
  • zl:rcv: Method has pointer receiver to zero-sized type (func (*zst) f())
  • zl:mex: Method expression receiver is pointer to zero-size type ((*zst).Error(nil))

Full Level

  • zl:add: Address of zero-size variable (&zsv)
  • zl:ast: Type assert to pointer to zero-size variable (var a any; a.(*zst))
  • zl:typ: Pointer to zero-sized type (map[string]*zst)
  • zl:arg: Passing explicit nil as parameter pointing to zero-sized type (func f(*zst); f(nil))
  • zl:par: Function parameter points to zero-sized type (func f(*zst))
  • zl:res: Function has pointer result to zero-sized type (func f() *zst)

Integration

Seezerolint-golangci-plugin.

Known Bugs

We are aware of a number of minor bugs in the analyzer's fixes. For example, it may sometimes prevent a type fromcorrectly implementing an interface or cause a non-pointer type to be checked for nil. The known bugs are low-risk andeasy to fix, as they result in a broken build or are obvious during a code review; none cause latent behavior changes.Please report any additional problems you encounter.

License

This project is licensed under the Apache License 2.0. See theLICENSE file for details.

About

The zerolint linter checks usage patterns of pointers to zero-size types in Go

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp