Go Blank Identifier — Professional / Internals Level¶
1. Overview¶
This document maps the blank identifier patterns to real OSS code you can read today. It also covers team conventions, code review checklists, and lint configurations that production Go shops use.
2. Compile-Time Interface Assertions in OSS¶
2.1 CockroachDB¶
CockroachDB uses compile-time interface assertions extensively. A single grep over the repo turns up hundreds of them. Examples:
pkg/sql/sem/tree/expr.go:
var _ Expr = &AndExpr{}
var _ Expr = &OrExpr{}
var _ Expr = &NotExpr{}
var _ Expr = &ParenExpr{}
var _ Expr = &ComparisonExpr{}
// ... dozens of similar lines
The tree.Expr interface is the AST node base in CRDB's SQL planner. Every concrete AST node has an assertion ensuring it implements Expr. When someone refactors the interface (e.g., adds a new method), the compiler points at every assertion that breaks.
Other CRDB examples:
pkg/sql/types/types.go—var _ TypeBase = ...for each type kind.pkg/storage/engine.go—var _ Engine = (*RocksDB)(nil)style checks for storage backends.
2.2 Kubernetes¶
k8s.io/apimachinery and the API types use the same pattern. Look at staging/src/k8s.io/api/core/v1/types.go and the corresponding zz_generated_deepcopy.go:
var _ runtime.Object = (*Pod)(nil)
var _ runtime.Object = (*Service)(nil)
var _ runtime.Object = (*ConfigMap)(nil)
Each generated deep-copy file ends with assertions that the new type implements runtime.Object. Code generators emit these blank assignments precisely so a generation bug is caught at compile time.
In client-go, you see assertions for the various informer/lister interfaces:
var _ cache.SharedInformer = (*sharedIndexInformer)(nil)
var _ cache.SharedIndexInformer = (*sharedIndexInformer)(nil)
2.3 Prometheus¶
In prometheus/client_golang:
Every metric type has a compile-time assertion that it satisfies the public Metric and/or Collector interfaces.
2.4 Standard Library¶
net/http:
The standard library uses the pattern more sparingly than third-party code (the std lib's tests already cover the interface satisfaction), but the idiom is recognized everywhere in the ecosystem.
2.5 etcd¶
In go.etcd.io/etcd:
Storage backends, transport implementations, and consensus participants all use blank assertions to lock in their interface contracts.
3. Side-Effect Imports in OSS¶
3.1 database/sql Drivers¶
Every Go SQL driver follows the same recipe. Examples:
-
Consumed asgithub.com/lib/pq:import _ "github.com/lib/pq". -
Consumed asgithub.com/go-sql-driver/mysql:import _ "github.com/go-sql-driver/mysql". -
modernc.org/sqlite,github.com/mattn/go-sqlite3,github.com/microsoft/go-mssqldb— same shape.
The official Go wiki page on database drivers (https://github.com/golang/go/wiki/SQLDrivers) lists dozens; all rely on _ import.
3.2 image Decoders¶
The standard library's image package keeps a registry of formats. Each subpackage adds itself in init:
image/png— registers viaimage.RegisterFormat("png", "\x89PNG\r\n\x1a\n", Decode, DecodeConfig).image/jpeg— same pattern.image/gif— same pattern.- Third-party:
golang.org/x/image/webp,golang.org/x/image/tiff,golang.org/x/image/bmp.
A typical app imports them blank:
3.3 net/http/pprof¶
The single most famous side-effect import in Go:
In net/http/pprof/pprof.go:
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}
The handlers attach to http.DefaultServeMux. With one blank import, your binary gets profiling endpoints — assuming you serve DefaultServeMux somewhere (http.ListenAndServe(":6060", nil)).
Production tip: many shops separate pprof onto an internal-only port. The blank import is identical; what differs is the listener.
3.4 expvar¶
Adds /debug/vars to http.DefaultServeMux, exposing cmdline, memstats, and any custom variables registered via expvar.Publish.
3.5 runtime/debug Side-Effect Patterns¶
Less commonly, you see runtime/debug style patterns where loading a package configures GOGC or similar at process start. These are usually wrapped in an explicit function call rather than a blank import.
3.6 OpenTelemetry / Tracer Registration¶
Many tracing libraries use side-effect imports for their default exporter:
The exact policy depends on the library; some prefer explicit registration to avoid the magic of init.
4. Team Conventions¶
4.1 Where to Put Compile-Time Assertions¶
Three styles, all defensible:
A. Right under the type declaration.
type FileLogger struct { /* ... */ }
var _ Logger = (*FileLogger)(nil)
func (f *FileLogger) Log(s string) { /* ... */ }
Pros: Reader sees the contract immediately. Cons: If methods come later in the file, the assertion comes before any visible implementation.
B. At the bottom of the file.
type FileLogger struct { /* ... */ }
func (f *FileLogger) Log(s string) { /* ... */ }
// ...
var (
_ Logger = (*FileLogger)(nil)
_ Closer = (*FileLogger)(nil)
)
Pros: All assertions in one block; easy to spot. Cons: Reader has to scroll to find them.
C. In a dedicated interfaces_test.go or similar.
// types_compile_test.go
package mypkg
var (
_ Logger = (*FileLogger)(nil)
_ Logger = (*StderrLogger)(nil)
)
Pros: Keeps non-runtime checks out of the production source. Cons: Some teams find this surprising.
Pick one and apply it consistently.
4.2 Where to Put Side-Effect Imports¶
The convention is: side-effect imports go in main.go or in a small "linkages" file, not in library code. Library packages should not pull in drivers, decoders, or pprof; that decision belongs to the binary.
cmd/
myapp/
main.go // imports _ "github.com/lib/pq"
pprof_enabled.go // build tag, imports _ "net/http/pprof"
Build tags help here:
Then go build -tags=pprof includes profiling.
4.3 Discarding Errors¶
Some teams forbid _ = err outright, requiring an explicit // nolint:errcheck with a comment. Others accept it for defer cleanup:
A balanced policy:
- Forbid
_ = errfrom any function whose only purpose is checking errors (e.g.,db.Query). - Allow
_ = errfor best-effort cleanup with a comment. - Require an explicit log for ignored errors that affect data integrity (e.g.,
tx.Rollback()).
4.4 Discarding Function Parameters¶
Two camps:
- Camp A: Always name parameters; ignore them by not referencing.
- Camp B: Use
_for parameters that the function genuinely does not use.
Both compile. Camp B is louder; Camp A is more flexible if a refactor needs the parameter later.
5. Code Review Checklist for _¶
Use this list when reviewing PRs:
- Every
_, err := ...is followed by anif err != nilcheck or a deliberate decision to ignore the error. - Every
n, _ := ...has a comment or context proving the second return cannot fail. - Every
_ = expreither has a comment explaining why or is removed. - Every
import _ "..."is inmainor a dedicated linkage file, not in a library package. - Every
var _ Iface = ...is near the type definition. - No
_in variable names that should be properly named (parameters, struct fields). - No
_shadowing pattern that misleads readers (cannot actually shadow, but newcomers may try). - No
_ = nilor other nonsense patterns.
6. Lint Configuration¶
6.1 errcheck¶
errcheck flags discarded errors. Configuration in .errcheck:
Functions on this list have their return values silently dropped without warning. Anything else triggers a finding when _, err := f() ignores the error.
Override per-line:
6.2 golangci-lint¶
In .golangci.yml:
linters:
enable:
- errcheck
- unused
- revive
- unparam
linters-settings:
errcheck:
check-type-assertions: true
check-blank: false # don't flag _, _ = ... assertions
unused:
check-exported: false
revive:
rules:
- name: unused-parameter
disabled: true # too noisy for many teams
check-blank: true would flag every _ = f() — useful in strict shops, noisy in others.
6.3 staticcheck¶
The SA4006 check flags assignments where the result is never used:
This does NOT fire on _ because there is no "previous value". _ = compute() is allowed by staticcheck.
6.4 revive¶
The unused-parameter rule (default off in many configs) suggests _ for parameters not referenced. Some teams enable it; others reject it.
The var-naming rule does not affect _.
6.5 unparam¶
unparam flags parameters that are always passed the same value or never used. It does not fire on _ parameters (since they are explicitly unused).
7. Production Patterns from Real Codebases¶
7.1 Driver Selection at Build Time¶
// drivers_postgres.go
//go:build postgres
package main
import _ "github.com/lib/pq"
// drivers_mysql.go
//go:build mysql
package main
import _ "github.com/go-sql-driver/mysql"
go build -tags=postgres selects the postgres driver. The main code is driver-agnostic.
7.2 Plugin-Style Registration¶
// codec/registry.go
var codecs = map[string]Codec{}
func Register(name string, c Codec) { codecs[name] = c }
// codec/json/json.go
package json
func init() { codec.Register("json", &Codec{}) }
// codec/yaml/yaml.go
package yaml
func init() { codec.Register("yaml", &Codec{}) }
// main.go
import (
_ "myproject/codec/json"
_ "myproject/codec/yaml"
)
Every consumer of codec reads codec.Get("json") without knowing which codec packages are linked in. The binary's main decides.
7.3 Generated Assertion Files¶
Code generators (gRPC, protobuf, deep-copy) emit:
// Generated. Do not edit.
var _ proto.Message = (*MyRequest)(nil)
var _ proto.Message = (*MyResponse)(nil)
This catches breakage when the generator changes.
7.4 Health Check Endpoints via pprof¶
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
log.Fatal(http.ListenAndServe("127.0.0.1:6060", nil))
}()
// serve real traffic on a different listener / mux
}
127.0.0.1:6060 keeps pprof off the public network. The blank import does not gate access.
7.5 Best-Effort Cleanups¶
Common in long-running services where the process is exiting and a stale listener does not matter.
8. References to Real Files¶
To read the patterns in production, look at:
cockroachdb/cockroach:pkg/sql/sem/tree/*.go(interface assertions).kubernetes/kubernetes:staging/src/k8s.io/apimachinery/pkg/runtime/types.goand any generatedzz_generated_deepcopy.go.prometheus/client_golang:prometheus/counter.go,prometheus/gauge.go.lib/pq: top ofconn.goforfunc init()registration.go-sql-driver/mysql: top ofdriver.goforfunc init().- Go standard library:
net/http/pprof/pprof.go,image/png/reader.go(func init()),expvar/expvar.go. etcd-io/etcd:server/storage/wal/wal.goand similar (interface assertions).
These are read-only references; the patterns there are what the OSS Go community considers idiomatic.
9. Anti-Patterns Production Reviews Reject¶
_ = errwith no comment in business logic — rejected; either handle or document._ = json.Unmarshal(b, &v)— rejected; bad input is a real failure.import _ "somepkg"in a library package (notmain) — rejected; lifts the policy decision out of the binary.var _ I = MyType{}for an exported type with no public consumers — accepted; this is exactly the case where the assertion adds value._, _ = io.Copy(dst, src)without comment — rejected; copy errors usually matter.func handler(_ http.ResponseWriter, _ *http.Request)— rejected unless the handler genuinely returns nothing (e.g., a placeholder).
10. Summary¶
The blank identifier is one of the few Go features whose idiomatic uses are concentrated in a small number of patterns that you can name and check for. Treat each _ as a small contract:
_, err := f()— "I want only the error".var _ I = (*T)(nil)— "T must implement I".import _ "p"— "run p'sinit, expose nothing"._ = expr— "I evaluated this on purpose, value not needed".
Anything else deserves a comment, a refactor, or a rejection. Real codebases (CockroachDB, Kubernetes, Prometheus, the standard library) follow these patterns precisely; copying their conventions is a safe default.