# 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.

- Skill: `ctoth/go-zero-values-and-construction` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-zero-values-and-construction`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-zero-values-and-construction/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-zero-values-and-construction

---


# Go Zero Values and Construction

> "Make the zero value useful."
> — [Go Proverbs](https://go-proverbs.github.io/)

> "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 `new` and get right to work."
> — [Effective Go](https://go.dev/doc/effective_go#data)

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](https://go.dev/doc/effective_go#data)). 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](https://go.dev/doc/effective_go#data)). The property is transitive: a struct made of useful-zero-value fields is itself useful at zero.

```go
// 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](https://google.github.io/styleguide/go/best-practices#vardeclzero)). 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.

```go
// 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](https://go-proverbs.github.io/)) — 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](https://go.dev/doc/effective_go#allocation_new)); "The expressions `new(File)` and `&File{}` are equivalent" ([Effective Go](https://go.dev/doc/effective_go#composite_literals)). `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](https://go.dev/doc/effective_go#allocation_make)). 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:** `new` also accepts a value expression: "`new(int64(300))` allocates a new variable of type `int64`, initialized to 300, and returns its address" ([Effective Go](https://go.dev/doc/effective_go#allocation_new)). Gated by the `go` directive — see **`go-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](https://go.dev/doc/effective_go#composite_literals)). A positional literal silently breaks — or silently misassigns — the day someone reorders or adds a field.

```go
// 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](https://google.github.io/styleguide/go/decisions#literal-field-names)). `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](https://google.github.io/styleguide/go/decisions#literal-zero-value-fields)).

---

## 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](https://github.com/uber-go/guide/blob/master/style.md#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."

```go
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](https://github.com/uber-go/guide/blob/master/style.md#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](https://google.github.io/styleguide/go/decisions#global-variables)).

```go
// 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](https://go.dev/ref/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](https://go.dev/ref/spec#Constant_declarations)).

```go
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](https://github.com/uber-go/guide/blob/master/style.md#start-enums-at-one)).

```go
// 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](https://go.dev/ref/spec#Iota)).

```go
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](https://pkg.go.dev/golang.org/x/tools/cmd/stringer)). Drive it from a `go:generate` directive:

```go
//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 a `Timeout` field 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 `New` that only returns `&T{}`, a config plumbed through a mutable package global, and a 3-method type wrapped in five `With…` options — three layers of machinery around a value the zero already modeled (Sections 2, 5, 6). "Make the zero value useful" ([Go Proverbs](https://go-proverbs.github.io/)) 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-value `sync.Mutex`, and why a struct holding one must not be copied.
- **`go-slices-and-maps`** — the `nil` map write panic, and `nil` vs empty slices (the zero value of the collection types).
- **`go-tooling-and-static-analysis`** — `go vet` `composites` for unkeyed literals, the `exhaustive` linter, and `go:generate stringer`.
- **`go-naming-and-style`** — constant and identifier naming (MixedCaps), and constructor naming (`NewT`).
- **`go-json`** — (un)marshaling enums by name and `omitempty`/`omitzero` for zero values.
- **`go-version-feature-map`** — `new(expr)` (1.26) and which idiom the module's `go` directive permits.

---

## 13. Reference Files

High-frequency construction and enum anti-patterns in LLM-generated Go, each with wrong/right code and citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

