Go interfaces
Skill ctoth/golang-skills-plugin/plugins/golang/skills/go-interfaces
Agent skill plugins for Go code quality and performance work.
npx -y skills add ctoth/golang-skills-plugin --skill go-interfacesAssembled 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.
What its author says it does
Copied from the file, not written here
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.md
15.2 KB, as published. Nobody here has run it
Go Interfaces
"The bigger the interface, the weaker the abstraction." · "interface{} says nothing." — Go Proverbs
"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
"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
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). 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).
// 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) |
| 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); "The bigger the interface, the weaker the abstraction" (Proverbs) |
| 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) |
| Not before they're used | No interface without a real second use | "Do not define interfaces before they are used" (CodeReviewComments) |
| Not "for mocking" | Test against the real API | "Do not define interfaces on the implementor side of an API 'for mocking'" (CodeReviewComments) |
any says nothing | Avoid any/interface{} params | "interface{} says nothing" (Proverbs) |
| Compose by embedding | Build big interfaces from small ones | io.ReadWriter = Reader + Writer (Effective Go) |
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) |
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); "one-method interfaces are named by the method name plus an -er suffix ... Reader, Writer, Formatter, CloseNotifier" (Effective Go). The canonical example is io.Reader — one method, used everywhere:
// 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). 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).
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). "Avoid creating interfaces until a real need exists" (Google Decisions).
// 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). Reach for a concrete type, a small behavioral interface, or — when the logic is genuinely identical across types — a generic type parameter, not any.
// 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):
// 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). 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).
// 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). 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). 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:
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); "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). And critically: "An interface value is nil only if the V and T are both unset" (Go FAQ).
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). The caller's if err != nil is then unexpectedly true on the success path.
// 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). 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!= nilwas lying the whole time (Section 9). - The reviewer forced to navigate a
StoreInterface→store→mockStoremaze 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) 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— declaringerror(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:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml