# Go Interfaces

> Guides Go interface design and the typed-nil-error gotcha — accept interfaces and return concrete structs, keep interfaces small (1–3 methods, `-er` names), define them on the consumer side not the producer, avoid `any`/`interface{}` as a parameter type, compose with embedding, and never return a concrete `*MyError`/pointer type where the value can be nil. Auto-invokes when writing or editing interface definitions, function signatures that take or return interfaces, `any`/`interface{}` parameters, interface embedding, or functions returning concrete error/pointer types — and on "why is this nil check failing", "is this always non-nil", or "should this be an interface". An interface holds a (type, value) pair, so a nil pointer inside one is not a nil interface.

- Skill: `ctoth/go-interfaces` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-interfaces`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-interfaces/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-interfaces

---


# Go Interfaces

> "The bigger the interface, the weaker the abstraction." · "interface{} says nothing."
> — [Go Proverbs](https://go-proverbs.github.io/)

> "Interfaces in Go provide a way to specify the behavior of an object: if something can do *this*, then it can be used *here*."
> — [Effective Go](https://go.dev/doc/effective_go#interfaces)

> "Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values."
> — [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments#interfaces)

A Go interface is a set of method signatures, satisfied *structurally* — a type implements it just by having the methods, with no `implements` keyword. That makes interfaces cheap to add and easy to over-add. The discipline below is about adding them where they earn their place, keeping them small, and understanding the one representation fact (an interface is a `(type, value)` pair) that turns a returned nil pointer into a non-nil error.

---

## 1. The Headline Rule: Accept Interfaces, Return Structs

Take the *narrowest interface* you need as input; return the *concrete type* as output. The Google Style Guide states it directly: "Functions should take interfaces as arguments but return concrete types" ([Google Go Style Guide — Decisions](https://google.github.io/styleguide/go/decisions#interfaces)). The reason is asymmetric: accepting an interface lets every caller — including a test fake — pass whatever satisfies it, while returning a concrete type means "new methods can be added to implementations without requiring extensive refactoring" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#interfaces)).

```go
// WRONG — returns an interface, hiding the concrete type and freezing the method set
func NewStore() StoreInterface { return &store{} }

// RIGHT — accept the small interface you use; return the concrete *Store
func NewStore() *Store { return &Store{data: map[string]string{}} }

func describe(g itemGetter, id string) (string, error) { // accepts an interface
	v, err := g.Get(id)
	if err != nil {
		return "", fmt.Errorf("describing %s: %w", id, err)
	}
	return "item=" + v, nil
}
```

---

## 2. The Rules and Their Sources

| Rule | The discipline | Source |
|---|---|---|
| Accept interfaces, return structs | Narrow interface in; concrete type out | "Functions should take interfaces as arguments but return concrete types" ([Google Decisions](https://google.github.io/styleguide/go/decisions#interfaces)) |
| Keep interfaces small | 1–3 methods; `-er` names | "one-method interfaces are named by the method name plus an `-er` suffix ... `Reader`, `Writer`, `Formatter`" ([Effective Go](https://go.dev/doc/effective_go#interface_names)); "The bigger the interface, the weaker the abstraction" ([Proverbs](https://go-proverbs.github.io/)) |
| Define on the consumer side | The user declares it, not the implementor | "Go interfaces generally belong in the package that uses values of the interface type" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#interfaces)) |
| Not before they're used | No interface without a real second use | "Do not define interfaces before they are used" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#interfaces)) |
| Not "for mocking" | Test against the real API | "Do not define interfaces on the implementor side of an API 'for mocking'" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#interfaces)) |
| `any` says nothing | Avoid `any`/`interface{}` params | "interface{} says nothing" ([Proverbs](https://go-proverbs.github.io/)) |
| Compose by embedding | Build big interfaces from small ones | `io.ReadWriter` = `Reader` + `Writer` ([Effective Go](https://go.dev/doc/effective_go#embedding)) |
| Declare `error` returns as `error` | Never return concrete `*MyError` | "use the `error` type in their signature ... rather than a concrete type such as `*MyError`" ([Go FAQ](https://go.dev/doc/faq#nil_error)) |

---

## 3. Keep Interfaces Small — the `-er` Convention

Idiomatic Go interfaces are tiny. "Interfaces with only one or two methods are common in Go code, and are usually given a name derived from the method" ([Effective Go](https://go.dev/doc/effective_go#interfaces)); "one-method interfaces are named by the method name plus an `-er` suffix ... `Reader`, `Writer`, `Formatter`, `CloseNotifier`" ([Effective Go](https://go.dev/doc/effective_go#interface_names)). The canonical example is `io.Reader` — one method, used everywhere:

```go
// io.Reader, the most-used interface in the standard library:
type Reader interface {
	Read(p []byte) (n int, err error)
}
```

A small interface is a strong abstraction because almost anything can satisfy it, and almost nothing breaks when an implementation changes. A large one — a "manager" interface mirroring a whole struct — is the opposite: "The bigger the interface, the weaker the abstraction" ([Go Proverbs](https://go-proverbs.github.io/)). Prefer the stdlib interfaces (`io.Reader`, `io.Writer`, `fmt.Stringer`, `error`) before inventing your own.

---

## 4. Define Interfaces on the Consumer Side

The package that *uses* a behavior declares the interface; the package that *provides* it returns a concrete type. "The consumer of the interface should define it (not the package implementing the interface), ensuring it includes only the methods they actually use" ([Google Go Style Guide — Decisions](https://google.github.io/styleguide/go/decisions#interfaces)).

The most common LLM anti-pattern here is the producer-side interface created "for mocking." Go rejects it twice: "Do not define interfaces on the implementor side of an API 'for mocking'; instead, design the API so that it can be tested using the public API of the real implementation"; and "Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#interfaces)). "Avoid creating interfaces until a real need exists" ([Google Decisions](https://google.github.io/styleguide/go/decisions#interfaces)).

```go
// WRONG — producer package exports an interface mirroring its own struct
package producer

type Thinger interface{ Thing() bool }       // DO NOT DO IT
func NewThinger() Thinger { return defaultThinger{} }

// RIGHT — producer returns a concrete type; the CONSUMER declares the small interface it needs
package producer
type Thinger struct{ /* ... */ }
func (t Thinger) Thing() bool { /* ... */ }

package consumer
type thinger interface{ Thing() bool }       // only the method this package uses
func Foo(t thinger) string { /* ... */ }
```

Because satisfaction is structural, the consumer's interface needs no cooperation from the producer — a test fake in the consumer's own package satisfies it for free.

---

## 5. `any` / `interface{}` Says Nothing

The empty interface carries no behavior, so a parameter typed `any` tells the caller and the compiler nothing about what is allowed. "interface{} says nothing" ([Go Proverbs](https://go-proverbs.github.io/)). Reach for a concrete type, a small behavioral interface, or — when the logic is genuinely identical across types — a generic type parameter, not `any`.

```go
// WRONG — `any` defeats the type system; every use needs a runtime type assertion
func Write(w io.Writer, v any) error { /* type-switch on v ... */ }

// RIGHT — name the behavior you require
func Write(w io.Writer, v fmt.Stringer) error {
	_, err := io.WriteString(w, v.String())
	return err
}
```

The interface-vs-type-parameter decision is owned by **`go-generics`**: if all you do is call a method, an interface is simpler than a type parameter.

---

## 6. Compose Interfaces by Embedding

Build larger interfaces by embedding smaller ones rather than re-listing methods. "it's easier and more evocative to embed the two interfaces to form the new one" ([Effective Go](https://go.dev/doc/effective_go#embedding)):

```go
// from the standard library:
type ReadWriter interface {
	Reader
	Writer
}
```

"A `ReadWriter` can do what a `Reader` does *and* what a `Writer` does; it is a union of the embedded interfaces" ([Effective Go](https://go.dev/doc/effective_go#embedding)). Embedding keeps each piece small and lets a type satisfy the composite by satisfying the parts.

---

## 7. Satisfaction Is Structural — Verify It at Compile Time

A type satisfies an interface implicitly, just by having the methods. To guarantee a type still satisfies an interface (and to get a clear compile error the moment it stops), use the blank-identifier assignment. "To guarantee that the implementation is correct, a global declaration using the blank identifier can be used" — `var _ json.Marshaler = (*RawMessage)(nil)` — and "that property will be checked at compile time" ([Effective Go](https://go.dev/doc/effective_go#interface_checks)).

```go
// Compile-time proof that *Handler implements http.Handler:
var _ http.Handler = (*Handler)(nil)
```

"The statement `var _ http.Handler = (*Handler)(nil)` will fail to compile if `*Handler` ever stops matching the `http.Handler` interface" ([Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance)). The right-hand side is the zero value of the asserted type: `nil` for pointers, slices, and maps; an empty struct literal for struct types. Use it for exported types whose interface contract is part of their API.

---

## 8. Pointer vs Value Receivers and the Method Set

Whether a type satisfies an interface depends on *which receiver* its methods use. "Methods with value receivers can be called on pointers as well as values. Methods with pointer receivers can only be called on pointers or addressable values" ([Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md#receivers-and-interfaces)). So a value-receiver method puts the method in *both* the value's and the pointer's method set, but a pointer-receiver method is in the *pointer's* method set only:

```go
type F interface{ f() }

type S1 struct{}
func (s S1) f() {}      // value receiver

type S2 struct{}
func (s *S2) f() {}     // pointer receiver

var i F
i = S1{}    // ok — value receiver
i = &S1{}   // ok
i = &S2{}   // ok — pointer
// i = S2{} // DOES NOT COMPILE: S2 value has no f in its method set
```

This is why "accept interfaces, return structs" usually returns a `*T`: a single pointer value satisfies interfaces whether the methods use value or pointer receivers.

---

## 9. The Typed-Nil Gotcha (Why Your `!= nil` Check Lies)

An interface value is a pair. "Under the covers, interfaces are implemented as two elements, a type `T` and a value `V`" ([Go FAQ](https://go.dev/doc/faq#nil_error)); "A variable of interface type stores a pair: the concrete value assigned to the variable, and that value's type descriptor" ([The Laws of Reflection](https://go.dev/blog/laws-of-reflection)). And critically: "An interface value is `nil` only if the `V` and `T` are both unset" ([Go FAQ](https://go.dev/doc/faq#nil_error)).

So if you return a *concrete* `*MyError` that happens to be nil, the `error` interface that receives it holds `(T=*MyError, V=nil)` — a non-nil interface. "Such an interface value will therefore be non-`nil` *even when the pointer value `V` inside is* `nil`" ([Go FAQ](https://go.dev/doc/faq#nil_error)). The caller's `if err != nil` is then unexpectedly true on the success path.

```go
// WRONG — returns the concrete *MyError; on success p is a nil *MyError,
// but the returned error interface is (T=*MyError, V=nil) — NON-nil.
func buggyValidate(bad bool) error {
	var p *MyError
	if bad {
		p = &MyError{Msg: "bad input"}
	}
	return p // caller's `err != nil` is ALWAYS true
}

// RIGHT — declare the return as error, and return an explicit nil on success.
func correctValidate(bad bool) error {
	if bad {
		return &MyError{Msg: "bad input"}
	}
	return nil
}
```

This is proven, not asserted: a test that calls `buggyValidate(false)` (nothing bad happened) finds `err == nil` is *false* — `BUG reproduced: nothing bad happened, yet (err != nil) == true` — while a type assertion confirms the dynamic value is a nil `*MyError`. The fixes, both from the FAQ: "the function must return an explicit `nil`," and "It's a good idea for functions that return errors always to use the `error` type in their signature ... rather than a concrete type such as `*MyError`" ([Go FAQ](https://go.dev/doc/faq#nil_error)). The standard library follows this: `os.Open` returns `error`, never the concrete `*os.PathError`. Declaring `error` returns (not concrete types) is owned by **`go-error-handling`**.

---

## 10. Who Suffers When Interfaces Are Done Badly

- The **teammate debugging at 2am** who reads `if err != nil { return err }`, sees the error is "set," and burns an hour before learning the function returned a typed nil — the `!= nil` was lying the whole time (Section 9).
- The **reviewer** forced to navigate a `StoreInterface` → `store` → `mockStore` maze generated "for testing," when a concrete return and a three-line consumer interface would have done the job (Section 4). "The bigger the interface, the weaker the abstraction" ([Proverbs](https://go-proverbs.github.io/)) is the empathy rule: every method on a too-big interface is a method the next person must understand to mock or change.
- The **next caller** of a function typed `func(any)`, who gets no compiler help and must read the body to learn which types are actually allowed (Section 5).

---

## 11. Routing to the Specific Skills

- **`go-idiomatic-discipline`** — the policy root. Producer-side interface pollution is its *axis 2* (over-abstraction); this skill holds the depth.
- **`go-generics`** — the interface-vs-type-parameter decision. "If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter."
- **`go-error-handling`** — declaring `error` (not `*MyError`) returns, wrapping with `%w`, and the typed-nil error in the context of error values.
- **`go-zero-values-and-construction`** — "accept interfaces, return structs" on the construction side: returning a useful concrete zero value rather than an interface.

---

## 12. Reference Files

High-frequency interface 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)

