Go zero values and construction
Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-zero-values-and-construction
Guides how a Go value comes into existence honestly — design types whose zero value is already useful (a zero sync.Mutex, bytes.Buffer, or nil slice just works) so you don't write a New that only zeroes fields; use keyed composite literals over positional ones; pick new vs &T{} vs make correctly; reach for functional options only when a type has many optional params; never stash mutable state in package globals; and model enums as typed iota constants that start at one (or reserve zero as an explicit Unknown) with a String() method. Auto-invokes when writing or editing struct construction, New constructors, composite literals, functional options, iota enums/typed constants, Stringer, or on "do I need a constructor", "how should this enum work", or "is this zero value safe". The zero value is part of your API; the compiler does not check enum exhaustiveness.From its SKILL.md
npx -y skills add ctoth/golang-skills-plugin --skill go-zero-values-and-constructionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
15.3 KB, ~3.7k tokens by cl100k_base, as published. Nobody here has run it
Go Zero Values and Construction
"Make the zero value useful." — Go Proverbs
"it's helpful to arrange when designing your data structures that the zero value of each type can be used without further initialization. This means a user of the data structure can create one with
newand get right to work." — Effective Go
A Go value is born one of two ways: a struct is allocated and its fields take their zero values, or a constant names a fixed value at compile time. This skill covers both ends — designing structs so the zero value is ready to use (and only adding construction machinery when it earns its place), and modeling enumerations as typed iota constants that don't make the zero value a silent trap. Both are the same discipline: bring a value into existence honestly.
PART A — Zero Values and Construction
1. The Headline Rule: Make the Zero Value Useful
Go zeroes every allocation, so the most idiomatic types need no constructor at all. "the zero value of each type can be used without further initialization" (Effective Go). The standard library is built this way: "the zero value for Buffer is an empty buffer ready to use," and "sync.Mutex does not have an explicit constructor or Init method. Instead, the zero value for a sync.Mutex is defined to be an unlocked mutex" (Effective Go). The property is transitive: a struct made of useful-zero-value fields is itself useful at zero.
// RIGHT — usable the instant it is declared; no New, no Init.
// A zero sync.Mutex is unlocked; the map is created lazily on first write.
type Counter struct {
mu sync.Mutex
counts map[string]int64
}
func (c *Counter) Inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.counts == nil {
c.counts = make(map[string]int64)
}
c.counts[name]++
}
var c Counter // ready to use — c.Inc("x") works
The Counter holds a sync.Mutex value (not a pointer), which is why callers must pass *Counter, and its methods take pointer receivers to avoid copying the lock (Google Best Practices). Copying a struct with a sync.Mutex is owned by go-sync-primitives.
2. Don't Write a Constructor That Only Zeroes Fields
If New does nothing but return &T{}, delete it — the caller can write &T{}, new(T), or var t T. A constructor earns its keep only when it does real work the zero value cannot: enforce an invariant, validate input, or wire a required dependency.
// WRONG — adds an import and a call for nothing the zero value can't do.
func NewBuffer() *Buffer { return &Buffer{} }
// RIGHT — a constructor that enforces an invariant the zero value cannot.
// The zero Ratio would divide by zero, so validation is justified.
func NewRatio(num, den int) (Ratio, error) {
if den == 0 {
return Ratio{}, fmt.Errorf("new ratio: denominator must be non-zero")
}
return Ratio{num: num, den: den}, nil
}
"Make the zero value useful" (Go Proverbs) — the needless New is an axis 2 over-abstraction in go-idiomatic-discipline. Returning the concrete *T (not an interface) from a real constructor is owned by go-interfaces ("accept interfaces, return structs").
3. new(T) vs &T{} vs make
Three allocators, with one job each:
| Form | Use it for | Returns |
|---|---|---|
new(T) / &T{} | any type; gives a zeroed T | *T |
&T{Field: x} | a struct with some fields set | *T |
make(T, …) | only slices, maps, channels | initialized T (not *T) |
"new(T) allocates zeroed storage for a new variable of type T and returns its address" (Effective Go); "The expressions new(File) and &File{} are equivalent" (Effective Go). make is different: "It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T)" — because these are "references to data structures that must be initialized before use" (Effective Go). A new([]int) gives "a pointer to a nil slice value," which is almost never what you want; use make([]int, …). Writing to a nil map panics — the nil-map trap is owned by go-slices-and-maps.
Go 1.26:
newalso accepts a value expression: "new(int64(300))allocates a new variable of typeint64, initialized to 300, and returns its address" (Effective Go). Gated by thegodirective — seego-version-feature-map.
4. Keyed Composite Literals, Not Positional
Always name fields in a struct literal. With labels, "the initializers can appear in any order, with the missing ones left as their respective zero values" (Effective Go). A positional literal silently breaks — or silently misassigns — the day someone reorders or adds a field.
// WRONG — positional; a new field or a reorder shifts every value.
r := csv.Reader{',', '#', 4, false, false, false, false}
// RIGHT — keyed; resilient to reordering and to fields added later.
r := csv.Reader{Comma: ',', Comment: '#', FieldsPerRecord: 4}
The Google guide makes it a rule across package boundaries: "Struct literals must specify field names for types defined outside the current package," because "the position of fields in a struct and the full set of fields … are not usually considered to be part of a struct's public API" (Google Decisions). go vet's composites check flags unkeyed literals of imported structs — see go-tooling-and-static-analysis. Omitting zero-value fields is fine and often clearer: "Zero-value fields may be omitted from struct literals when clarity is not lost" (Google Decisions).
5. Functional Options — Only When You Have Many Optional Params
When a constructor has several optional settings that you expect to grow, the functional-options pattern keeps the call site clean: "declare an opaque Option type that records information in some internal struct. You accept a variadic number of these options" (Uber Go Style Guide — Functional Options). Use it "for optional arguments in constructors and other public APIs that you foresee needing to expand, especially if you already have three or more arguments."
type Option func(*Dialer)
func WithTimeout(d int) Option { return func(o *Dialer) { o.timeout = d } }
func WithRetries(n int) Option { return func(o *Dialer) { o.retries = n } }
func NewDialer(opts ...Option) *Dialer {
d := &Dialer{timeout: 30, retries: 3} // defaults
for _, opt := range opts {
opt(d)
}
return d
}
d := NewDialer(WithTimeout(5)) // retries stays at its default
Do not over-apply this. For a type with one or two fields, options are ceremony — a plain keyed literal or a small config struct reads better. Options are the answer to "too many positional params you foresee expanding," not to every constructor.
6. No Mutable Package-Level Globals
State belongs in a struct field or a function argument, not a package variable that any code can mutate. "Avoid mutating global variables, instead opting for dependency injection" (Uber Go Style Guide — Avoid Mutable Globals). The Google guide allows exported globals only "much less frequently and under the strictest of scrutiny," preferring "explicit function arguments or struct field assignment" (Google Decisions).
// WRONG — package-level mutable state; tests and callers fight over it.
var timeNow = time.Now
// RIGHT — inject the dependency as a field.
type signer struct{ now func() time.Time }
func newSigner() *signer { return &signer{now: time.Now} }
A mutable global is axis 1 (fighting Go) in go-idiomatic-discipline: it makes code untestable and order-dependent. Immutable, exported constants are fine.
PART B — Enums via Typed iota Constants
7. Typed Constants with iota
Go has no enum keyword. The idiom is a named type plus a const block driven by iota: "the predeclared identifier iota represents successive untyped integer constants. Its value is the index of the respective ConstSpec in that constant declaration, starting at zero" (Go Spec — Iota). Omitting the expression after the first line repeats it: "Omitting the list of expressions is therefore equivalent to repeating the previous list" (Go Spec — Constant declarations).
type Weekday int
const (
UnknownDay Weekday = iota // 0
Sunday // 1
Monday // 2
// ...
)
A typed constant (Weekday, not a bare int) gives the compiler something to check: a function taking a Weekday won't accept a stray int. Don't model an enum as untyped string or int constants — that throws the type safety away. Constant naming (MixedCaps, no ALL_CAPS) is owned by go-naming-and-style.
8. Start at One — or Reserve Zero as an Explicit Unknown
The zero value is whatever a var or a missing struct field defaults to, so a member valued 0 can appear without anyone choosing it. "Since variables have a 0 default value, you should usually start your enums on a non-zero value" (Uber Go Style Guide — Start Enums at One).
// WRONG — Add is 0, so a zero-valued Operation is silently "Add".
const ( Add Operation = iota; Subtract; Multiply )
// RIGHT — either start at one with iota+1 ...
const ( Add Operation = iota + 1; Subtract; Multiply )
// ... or make the zero an explicit, checkable sentinel.
const ( UnknownOp Operation = iota; Add; Subtract; Multiply )
Reserving zero as Unknown lets you distinguish "unset" from a real choice. The exception Uber names: start at zero when "the zero value case is the desirable default behavior" (e.g. LogToStdout). Use _ to skip a slot, and shift expressions for bit flags: 1 << iota yields independent powers of two (Go Spec — Iota).
type Permission uint8
const (
Read Permission = 1 << iota // 1
Write // 2
Execute // 4
)
9. Give the Enum a String() — Often via stringer
A bare Weekday(2) prints as 2. Implement String() string so it satisfies fmt.Stringer and prints its name. Don't hand-maintain the mapping if you can generate it: stringer is "a tool to automate the creation of methods that satisfy the fmt.Stringer interface" (stringer). Drive it from a go:generate directive:
//go:generate stringer -type=Weekday
Running go generate writes a weekday_string.go with the String() method, kept in sync with the constants (wiring it in is owned by go-tooling-and-static-analysis). For enums crossing a wire or JSON boundary, add MarshalText/UnmarshalText so the name, not the int, is the stable form — JSON enum handling and omitzero are owned by go-json.
10. Go Does Not Check Enum Exhaustiveness
A switch over an enum that misses a case compiles silently — the compiler does not know your const block is meant to be closed. Adding a new Weekday won't flag the switch statements that forgot it. There is no language fix; the tool is the exhaustive linter, which reports a switch (or map literal) that omits a member of an enum type. Wiring it into CI is owned by go-tooling-and-static-analysis. Until then, a default: that returns an error or panics on an unexpected value is the honest fallback.
11. Who Suffers When This Is Done Badly
- The teammate who copies a positional
Server{addr, port}literal, then loses an hour when aTimeoutfield is inserted in the middle and every value silently shifts one slot to the right (Section 4). - The on-call engineer debugging why every freshly-decoded request is treated as
Add: the enum started at zero, an unset field defaulted to the first member, and nothing ever said "unknown" (Section 8). - The reviewer who reads a
Newthat only returns&T{}, a config plumbed through a mutable package global, and a 3-method type wrapped in fiveWith…options — three layers of machinery around a value the zero already modeled (Sections 2, 5, 6). "Make the zero value useful" (Go Proverbs) is the empathy rule: the value you construct carelessly is one the next reader must reverse-engineer.
12. Routing to the Specific Skills
go-idiomatic-discipline— the policy root. The needless constructor and the mutable global are its named axis 2 / axis 1 tells; this skill holds the depth.go-interfaces— "accept interfaces, return structs": a real constructor returns the concrete*T, not an interface.go-sync-primitives— the zero-valuesync.Mutex, and why a struct holding one must not be copied.go-slices-and-maps— thenilmap write panic, andnilvs empty slices (the zero value of the collection types).go-tooling-and-static-analysis—go vetcompositesfor unkeyed literals, theexhaustivelinter, andgo:generate stringer.go-naming-and-style— constant and identifier naming (MixedCaps), and constructor naming (NewT).go-json— (un)marshaling enums by name andomitempty/omitzerofor zero values.go-version-feature-map—new(expr)(1.26) and which idiom the module'sgodirective permits.
13. Reference Files
High-frequency construction and enum anti-patterns in LLM-generated Go, each with wrong/right code and citations:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml
What ships with it: 2 files
16.0 KB alongside SKILL.md
references/
- common-mistakes.md11.2 KB
- sources.yaml4.8 KB