{"id":34163644,"url":"https://github.com/fillmore-labs/zerolint","last_synced_at":"2026-03-11T07:01:37.215Z","repository":{"id":247461514,"uuid":"824256280","full_name":"fillmore-labs/zerolint","owner":"fillmore-labs","description":"The zerolint linter checks usage patterns of pointers to zero-size types in Go","archived":false,"fork":false,"pushed_at":"2026-03-03T07:34:15.000Z","size":238,"stargazers_count":20,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-03-03T10:59:06.919Z","etag":null,"topics":["go","linter","static-analysis","zero-sized-types"],"latest_commit_sha":null,"homepage":"https://fillmore-labs.com/zerolint","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fillmore-labs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-07-04T17:40:33.000Z","updated_at":"2026-02-05T15:01:33.000Z","dependencies_parsed_at":"2025-01-01T18:18:32.141Z","dependency_job_id":"c77fa175-5240-4e15-a731-61db9f566256","html_url":"https://github.com/fillmore-labs/zerolint","commit_stats":null,"previous_names":["fillmore-labs/zerolint"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/fillmore-labs/zerolint","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fillmore-labs%2Fzerolint","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fillmore-labs%2Fzerolint/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fillmore-labs%2Fzerolint/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fillmore-labs%2Fzerolint/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fillmore-labs","download_url":"https://codeload.github.com/fillmore-labs/zerolint/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fillmore-labs%2Fzerolint/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30373507,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-11T06:09:32.197Z","status":"ssl_error","status_checked_at":"2026-03-11T06:09:17.086Z","response_time":84,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","linter","static-analysis","zero-sized-types"],"created_at":"2025-12-15T09:21:34.459Z","updated_at":"2026-03-11T07:01:37.209Z","avatar_url":"https://github.com/fillmore-labs.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zerolint\n\n[![Go Reference](https://pkg.go.dev/badge/fillmore-labs.com/zerolint.svg)](https://pkg.go.dev/fillmore-labs.com/zerolint)\n[![Test](https://github.com/fillmore-labs/zerolint/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/fillmore-labs/zerolint/actions/workflows/test.yaml?query=branch%3Amain)\n[![CodeQL](https://github.com/fillmore-labs/zerolint/actions/workflows/github-code-scanning/codeql/badge.svg?branch=main)](https://github.com/fillmore-labs/zerolint/actions/workflows/github-code-scanning/codeql?query=branch%3Amain)\n[![Coverage](https://codecov.io/gh/fillmore-labs/zerolint/branch/main/graph/badge.svg?token=TUE1BL1QZV)](https://codecov.io/gh/fillmore-labs/zerolint)\n[![Go Report Card](https://goreportcard.com/badge/fillmore-labs.com/zerolint)](https://goreportcard.com/report/fillmore-labs.com/zerolint)\n[![License](https://img.shields.io/github/license/fillmore-labs/zerolint)](https://www.apache.org/licenses/LICENSE-2.0)\n\n`zerolint` is a Go static analysis tool (linter) that detects unnecessary or potentially incorrect usage of pointers to\nzero-sized types.\n\n## Motivation\n\nGo's zero-sized types (such as `struct{}` or `[0]byte`) occupy no memory and are useful in scenarios like channel\nsignaling or as map keys. However, creating pointers to these types (e.g., `\u0026struct{}` or `new(struct{})`) is almost\nalways unnecessary and can introduce subtle bugs and overhead.\n\nSince all values of a zero-sized type are identical, pointers to them rarely convey unique state or identity. This can\nmake code less clear, as readers might incorrectly assume state or identity is being managed. Furthermore, pointers\nthemselves are not zero-sized, leading to minor memory and performance overhead.\n\n`zerolint` helps identify these patterns, promoting cleaner and potentially more efficient code.\n\n## Quickstart\n\n### Installation\n\nInstall the linter:\n\n### Homebrew\n\n```console\nbrew install fillmore-labs/tap/zerolint\n```\n\n### Go\n\n```console\ngo install fillmore-labs.com/zerolint@latest\n```\n\n### Eget\n\n[Install `eget`](https://github.com/zyedidia/eget?tab=readme-ov-file#how-to-get-eget), then\n\n```console\neget fillmore-labs/zerolint\n```\n\n## Usage\n\nRun the linter on your project:\n\n```console\nzerolint ./...\n```\n\nSee below for descriptions of available command-line flags.\n\n## Optional Flags\n\nUsage: `zerolint [-flag] [package]`\n\nFlags:\n\n- **-level** `\u003clevel\u003e`: Set analysis depth:\n  - **Basic**: Basic detection of pointer issues (Default)\n  - **Extended**: Additional checks for more complex patterns\n  - **Full**: Most comprehensive analysis, recommended with `-fix`\n- **-match** `\u003cregex\u003e`: Limit zero-sized type detection to types matching the regex. Useful with `-fix`.\n- **-excluded** `\u003cfilename\u003e`: Read types to be excluded from analysis from the specified file. The file should contain\n  fully qualified type names, one per line. See the [“Exclusion File”](#exclusion-file) section for more details.\n- **-generated**: Analyze files that contain code generation markers (e.g., `// Code generated ... DO NOT EDIT.`). By\n  default, these files are skipped.\n- **-zerotrace**: Enable verbose logging of which types `zerolint` identifies as zero-sized. Useful for building a list\n  of excluded types.\n- **-c** `\u003cN\u003e`: Display N lines of context around the offending line (default: -1 for no context, 0 for only the\n  offending line).\n- **-test**: Indicates whether test files should be analyzed, too. (default: true).\n- **-fix**: Apply all suggested fixes automatically. Use with caution and always review the changes made by `-fix`.\n- **-diff**: With `-fix`, don't update the files, but print a unified diff.\n\n## Example\n\nConsider the following Go code:\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\ntype DivisionByZeroError struct{}\n\nfunc (*DivisionByZeroError) Error() string {\n\treturn \"division by zero\"\n}\n\nfunc Reciprocal(x float64) (float64, error) {\n\tif x == 0 {\n\t\treturn 0, \u0026DivisionByZeroError{}\n\t}\n\n\treturn 1 / x, nil\n}\n\nfunc TestDivisionByZero(t *testing.T) {\n\t_, err := Reciprocal(0)\n\n\tif !errors.Is(err, \u0026DivisionByZeroError{}) {\n\t\tt.Errorf(\"Expected division by zero error, but got: %v\", err)\n\t}\n}\n```\n\nThe test passes ([Go Playground](https://go.dev/play/p/7Zyi1SsiSqI)):\n\n```console\n=== RUN   TestDivisionByZero\n--- PASS: TestDivisionByZero (0.00s)\nPASS\n```\n\nRunning `zerolint .` would produce output similar to:\n\n```text\n/path/to/your/project/main_test.go:10:7: error interface implemented on pointer to zero-sized type \"example.com/project.DivisionByZeroError\" (zl:err)\n/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)\n```\n\n### Understanding the Output and Zero-Sized Diagnostics\n\nThe `main_test.go` example and its `zerolint` output highlight a common pitfall with zero-sized types in Go. In the\n`TestDivisionByZero` function:\n\n```go\n\tif !errors.Is(err, \u0026DivisionByZeroError{}) {\n```\n\nthe expression `\u0026DivisionByZeroError{}` creates a new pointer to a zero-sized struct. Similarly, the `Reciprocal`\nfunction, when `x` is 0, returns another `\u0026DivisionByZeroError{}`. The critical point is how these pointers are\ncompared.\n\nThe check `errors.Is(err, \u0026DivisionByZeroError{})` might not behave as intuitively expected. When the target error\npassed to `errors.Is` is a pointer type (`*DivisionByZeroError` in this case), `errors.Is` first performs a direct\npointer address comparison, before even checking whether the error implements an `Is(error) bool` method.\n\nTo illustrate that these are treated as distinct pointers for comparison purposes, we can modify `DivisionByZeroError`\nto be non-zero-sized:\n\n```go\ntype DivisionByZeroError struct{ _ int } // Make it non-zero-sized\n```\n\nWith this change, the test `TestDivisionByZero` fails, confirming that `errors.Is` was indeed comparing distinct\ninstances based on their pointer values.\n\n#### Pitfalls of Zero-Sized Pointer Comparisons\n\nInternally, Go's runtime optimizes allocations of zero-sized types. It achieves this by\n[returning a pointer to a common static variable](https://cs.opensource.google/go/go/+/refs/tags/go1.26.0:src/runtime/malloc.go;l=1126-1129)\n(known as `runtime.zerobase`) rather than allocating new memory on the heap for each instance. A consequence of this\noptimization is that different pointers to zero-sized types (e.g., multiple uses of `\u0026DivisionByZeroError{}` or\n`new(DivisionByZeroError)`) end up pointing to the same memory address. This can create the illusion that such pointers\nwill always compare as equal.\n\nDespite this common runtime behavior, the Go language specification\n[_explicitly_ states](https://go.dev/ref/spec#Comparison_operators) that the equality of pointers to distinct zero-size\nvariables is unspecified:\n\n\u003e _“pointers to distinct zero-size variables may or may not be equal.”_\n\nThis means the observed consistency in pointer comparisons is an internal implementation detail of the Go runtime, not a\nguaranteed language feature. Relying on this behavior is a classic example of [Hyrum's Law](https://www.hyrumslaw.com)\nin action:\n\n\u003e _“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable\n\u003e behaviors of your system will be depended on by somebody.”_\n\nConsequently, code that tests the equality (or inequality) of pointers to zero-sized types might compile and appear to\nfunction correctly under current Go versions. However, its logic is fundamentally unsound because it depends on an\nimplementation detail not guaranteed by the language specification. Such code is at risk of breaking unexpectedly with\nfuture Go updates or in different compilation environments. `zerolint` identifies and flags these problematic usage\npatterns, helping developers write more robust code that avoids this undefined behavior.\n\n### Potential Fixes\n\nWhen `zerolint` flags an issue, consider these approaches:\n\n#### Use a sentinel error variable (most idiomatic for errors)\n\nThis is often the clearest and most common Go practice for handling specific error conditions.\n\n```go\n// ErrDivisionByZero is returned when attempting to divide by zero.\nvar ErrDivisionByZero = errors.New(\"division by zero\")\n```\n\nThis approach is preferred because comparisons like `errors.Is(err, ErrDivisionByZero)` work reliably with sentinel\nerror values, avoiding the pitfalls of comparing pointers to zero-sized types.\n\n#### Applying Fixes with `zerolint` (automatic refactoring)\n\nFor many common issues identified by `zerolint`, you can attempt an automatic fix:\n\n```console\nzerolint -level=full -fix ./...\n```\n\nThe `-fix` flag will try to apply corrections, such as changing pointer receivers to value receivers where appropriate\nor modifying how zero-sized types are instantiated or compared. For the most comprehensive automatic fixing, using\n`-level=full` with `-fix` is recommended. This combination helps ensure that `zerolint` addresses all detected issues\nrelated to a specific zero-sized type, promoting consistency across its usages once the fixes are applied.\n\n\u003e **Caution:** Always review changes made by `-fix` carefully before committing them, as automatic refactoring can\n\u003e sometimes have unintended consequences, especially in complex codebases.\n\nFor instance, running `zerolint -level=full -fix .` on the example above transforms the code as follows:\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\ntype DivisionByZeroError struct{}\n\nfunc (DivisionByZeroError) Error() string {\n\treturn \"division by zero\"\n}\n\nfunc Reciprocal(x float64) (float64, error) {\n\tif x == 0 {\n\t\treturn 0, DivisionByZeroError{}\n\t}\n\n\treturn 1 / x, nil\n}\n\nfunc TestDivisionByZero(t *testing.T) {\n\t_, err := Reciprocal(0)\n\n\tif !errors.Is(err, DivisionByZeroError{}) {\n\t\tt.Errorf(\"Expected division by zero error, but got: %v\", err)\n\t}\n}\n```\n\nThis program is correct, since the errors are compared by value, and two zero-sized variables of the same type always\ncompare equal.\n\n#### Make the Type Non-Zero-Sized\n\nIf you need to maintain the custom error type structure for specific reasons (e.g., backward compatibility), or if it's\nnot an error type but another zero-sized struct, you can make it non-zero-sized. For errors, you can optionally provide\nan `Is` method to restore the previous behavior of `errors.Is` when comparing against this error type:\n\n```go\ntype DivisionByZeroError struct{ _ int } // Add a non-zero field\n\nfunc (*DivisionByZeroError) Is(err error) bool { // Optional for error types\n\t_, ok := err.(*DivisionByZeroError)\n\n\treturn ok\n}\n```\n\nWhile this approach is more verbose than using `errors.New` (for errors) or the original pointer-based zero-sized error\nimplementation, it ensures correct, defined behavior for comparisons, making it valid Go code. This might be considered\nif backward compatibility with an existing pointer-based error API is a concern, though migrating away from\npointer-based zero-sized errors is generally preferable.\n\n#### Exclude the Type from Analysis\n\nIf pointer usage for a specific zero-sized type is intentional, unavoidable (e.g., due to external library constraints),\nor you've assessed the risk and accept it, you can exclude the type from `zerolint`'s analysis. See the next section\n[“Excluding Types”](#excluding-types) for details.\n\n## Excluding Types\n\nYou can instruct `zerolint` to ignore specific zero-sized types in several ways:\n\n### Exclusion File\n\nIf you have specific zero-sized types where pointer usage is intentional or required (e.g., due to external library\nconstraints), you can exclude them using the `-excluded` flag with a file path. The file should contain fully qualified\ntype names, one per line.\n\nExample `excludes.txt`:\n\n```text\n# zerolint excludes for my project\ncompany.example/service/client.RequestOptions\nexample.com/project.DivisionByZeroError\n```\n\nThen run: `zerolint -excluded=excludes.txt ./...`\n\nThis is especially useful when running with the `-fix` flag and dealing with types from external libraries you don't\ncontrol.\n\n### Source Code Comment\n\nIf you control the source code where the zero-sized type is defined, you can add a special comment directly above the\ntype definition:\n\n```go\n//zerolint:exclude\ntype MyIntentionalZeroSizedType struct{}\n```\n\nThis comment will tell `zerolint` to ignore any issues for `MyIntentionalZeroSizedType`.\n\nTo exclude a type defined in an external package, you can declare the exclusion in your own package using a `var`\ndeclaration with the blank identifier (`_`):\n\n```go\n//zerolint:exclude\nvar _ external.ZeroSizedType\n```\n\nUsing these exclusion methods allows you to tailor `zerolint`'s behavior to your project's specific needs.\n\n## Linter Scope and External Types\n\nBy default, `zerolint` analyzes all types encountered, not just those declared within your current package or module.\nThis includes types imported from external packages (dependencies).\n\nWhile `zerolint` (especially at its default analysis level) aims to flag only genuinely problematic patterns, there\nmight be situations where a zero-sized type from an external package is used in a way that, while flagged, is legitimate\nor required by that external package's API. For example, an external library might require you to pass a pointer to a\nzero-sized option structure or for interface satisfaction in a way that cannot be altered.\n\n`zerolint` itself cannot automatically determine if such a flagged usage of an external type is intentional or\nunavoidable within the constraints of that external library. It reports based on the general principle of avoiding\nunnecessary pointers to zero-sized types.\n\nIf you encounter such a scenario with an external type you cannot modify with a `//zerolint:exclude` comment, the\nrecommended approach to manage these legitimate cases is:\n\n1. Run `zerolint` with the `-zerotrace` flag. This will provide a detailed log of all types that `zerolint` identifies\n   as zero-sized during its analysis.\n2. Inspect this log to find the fully qualified names of the specific external types that are being flagged but whose\n   usage you've determined is valid.\n3. Manually add these fully qualified type names to an exclusion file, as described in the\n   [“Excluding Types”](#excluding-types) section. This will instruct `zerolint` to ignore these specific types in future\n   analyses.\n\nThis approach allows you to maintain the benefits of `zerolint` for your own codebase and other dependencies while\nselectively bypassing checks for specific external types where pointer usage is justified.\n\n## Diagnostic Codes\n\n`zerolint` output includes diagnostic codes to help categorize the type of issue found. In the examples for each\ndiagnostic code, `zst` is used as a placeholder for a zero-sized type definition (e.g., `type zst struct{}`), and `zsv`\nrepresents a variable of that zero-sized type (e.g., `var zsv zst`).\n\n### Basic Level\n\n- **zl:cme**: Comparison of pointer to zero-size type with an error interface (`errors.Is(err, \u0026zsv)`)\n- **zl:cmp**: Comparison of pointers to zero-size type (`\u0026zsv == \u0026zsv`)\n- **zl:cmi**: Comparison of pointer to zero-size type with interface (`\u0026zsv == any(\u0026zst{})`)\n- **zl:err**: Error interface implemented on pointer to zero-sized type (`func (*zst) Error() string`)\n- **zl:emb**: Embedded pointer to zero-sized type (`struct{ *zst }`)\n- **zl:der**: Dereferencing pointer to zero-size variable (`zsp := \u0026zsv; _ = *zsp`)\n- **zl:dcl**: Type declaration to pointer to zero-sized type (`type zstPtr *zst`)\n\n### Extended Level\n\n- **zl:new**: `new` called on zero-sized type (`new(zst)`)\n- **zl:nil**: Cast of nil to pointer to zero-size type (`(*zst)(nil)`)\n- **zl:ret**: Explicitly returning nil as pointer to zero-sized type (`func f() *zst { return nil }`)\n- **zl:cst**: Cast to pointer to zero-size type (`(*zst)(\u0026struct{}{})`)\n- **zl:var**: Variable is pointer to zero-sized type (`var _ *zst`)\n- **zl:fld**: Field points to zero-sized type (`struct{ f *zst }`)\n- **zl:rcv**: Method has pointer receiver to zero-sized type (`func (*zst) f()`)\n- **zl:mex**: Method expression receiver is pointer to zero-size type (`(*zst).Error(nil)`)\n\n### Full Level\n\n- **zl:add**: Address of zero-size variable (`\u0026zsv`)\n- **zl:ast**: Type assert to pointer to zero-size variable (`var a any; a.(*zst)`)\n- **zl:typ**: Pointer to zero-sized type (`map[string]*zst`)\n- **zl:arg**: Passing explicit nil as parameter pointing to zero-sized type (`func f(*zst); f(nil)`)\n- **zl:par**: Function parameter points to zero-sized type (`func f(*zst)`)\n- **zl:res**: Function has pointer result to zero-sized type (`func f() *zst`)\n\n## Integration\n\n### `golangci-lint` Module Plugin\n\nAdd a file `.custom-gcl.yaml` to your source with\n\n```YAML\n---\nversion: v2.10.1\n\nname: golangci-lint\ndestination: .\n\nplugins:\n  - module: fillmore-labs.com/zerolint\n    import: fillmore-labs.com/zerolint/gclplugin\n    version: v0.0.16\n```\n\nRun `golangci-lint custom` to build a custom executable. Configure in `.golangci.yaml`:\n\n```YAML\n---\nversion: \"2\"\nlinters:\n  enable:\n    - zerolint\n  settings:\n  settings:\n    custom:\n      zerolint:\n        type: module\n        description: zerolint checks usage patterns of pointers to zero-size types.\n        original-url: https://fillmore-labs.com/zerolint\n        settings:\n          level: \"full\"\n          excluded:\n            - \"struct{}\"\n          generated: true\n          match: \"^your\\\\.domain/package/path\"\n```\n\nand can be used like `golangci-lint`:\n\n```shell\n./golangci-lint run .\n```\n\nSee also the golangci-lint\n[module plugin system documentation](https://golangci-lint.run/plugins/module-plugins/#the-automatic-way).\n\n## Links\n\n- [Blog post about zero-sized types](hthttps://blog.fillmore-labs.com/posts/zerosized-1/)\n\n## Known Bugs\n\nWe are aware of a number of minor bugs in the analyzer's fixes. For example, it may sometimes prevent a type from\ncorrectly implementing an interface or cause a non-pointer type to be checked for nil. The known bugs are low-risk and\neasy to fix, as they result in a broken build or are obvious during a code review; none cause latent behavior changes.\nPlease report any additional problems you encounter.\n\n## License\n\nThis project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffillmore-labs%2Fzerolint","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffillmore-labs%2Fzerolint","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffillmore-labs%2Fzerolint/lists"}