agentleFS
h0rn3th0rn3t/golang-skills/skills/go-interfaces/SKILL.md

go-interfaces

Skill7 starsChanged 9 days agoApache-2.0View on GitHub

Use when designing or implementing Go interfaces, abstractions, mockable boundaries, embedding, assertions, type switches, pointer versus value receiver types, or interface versus concrete API types. Generics belong to go-generics.

1 other project uses a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 5 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Avoid embedding types in public structs — the inner type's full method set becomes part of your public API, so adding, removing, or …

    line 104
  2. Distinctive:

    Use a blank identifier assignment to verify a type implements an interface at compile time:

    line 114
  3. Distinctive:

    Use this pattern when:

    line 123
  4. Distinctive:

    Don't: add these checks for every interface — only when no other static conversion would catch the error.

    line 129
  5. Distinctive:

    Before changing an interface's method set, list its implementations: the build reports each one assigned or passed as the interface, but a …

    line 132
  6. Very specific:

    Default to a pointer receiver.

    line 153

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go Interfaces and Composition

Compatibility: Baseline Go 1.27 (see COMPATIBILITY.md). Generic methods require Go 1.27+.

Resource Routing

  • scripts/check-interface-compliance.sh - Run as a heuristic; Interface Satisfaction Checks says what it lists.
  • scripts/check-interface-compliance.go - Implementation helper built and run by check-interface-compliance.sh; not run or read directly.
  • references/EMBEDDING.md - Read when embedding a type in a public struct.
  • references/RECEIVER-TYPE.md - Read when choosing pointer or value receivers.

Accept Interfaces, Return Concrete Types

Interfaces belong in the package that consumes values, not the package that implements them. Return concrete (usually pointer or struct) types from constructors so new methods can be added without refactoring.

// Good: consumer defines the interface it needs
package consumer

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { ... }
// Good: producer returns concrete type
package producer

type Thinger struct{ ... }
func (t Thinger) Thing() bool { ... }
func NewThinger() Thinger { return Thinger{ ... } }
// Bad: producer defines and returns its own interface
package producer

type Thinger interface { Thing() bool }
type defaultThinger struct{ ... }
func NewThinger() Thinger { return defaultThinger{ ... } }

Do not define interfaces before they are used. Identify the consumer and the substitution it needs, including a test double. One production implementation neither requires nor rules out an interface.


Generality: Hide Implementation, Expose Interface

Return an interface from a constructor only when it is a pre-existing one owned by the consumer or the standard library, and the type has no exported methods beyond it:

func NewHash() hash.Hash32 {
    return &myHash{}  // unexported type; hash.Hash32 is the stdlib's interface
}

An interface declared beside the constructor in order to be returned is the Bad case above, whatever the type's method set.


Type Assertions: Comma-Ok Idiom

Without checking, a failed assertion panics. Use the comma-ok idiom whenever the dynamic type is data-dependent; a bare x.(T) is acceptable only where a mismatch is a programming error that should panic — a value the package itself stored under a key only it writes. A recover guard is not one: a bare assertion replaces a foreign panic value with a *runtime.TypeAssertionError, so it uses comma-ok and re-panics p unchanged (PANIC-RECOVER.md). Reflection code uses reflect.TypeAssert[T] (Go 1.25+) instead of v.Interface().(T).


Generic Methods Cannot Satisfy Interfaces

Go 1.27 lets a method declare its own type parameters, but interface methods still cannot. A type whose only implementation of Do is Do[T any]() does not satisfy interface{ Do() } — the compile-time assertion var _ I = (*T)(nil) is how you find out. When callers dispatch through an interface, keep a non-generic method (or a free generic function) as the entry point. See go-generics.


Embedding

Avoid embedding types in public structs — the inner type's full method set becomes part of your public API, so adding, removing, or replacing the embedded type is a breaking change. Use an unexported field and forward the methods you mean to export; EMBEDDING.md has the before/after. This skill owns the rule; go-defensive routes here.


Interface Satisfaction Checks

Use a blank identifier assignment to verify a type implements an interface at compile time:

var _ json.Marshaler = (*RawMessage)(nil)

This causes a compile error if *RawMessage doesn't implement json.Marshaler.

Use this pattern when:

  • There are no static conversions that would verify the interface automatically
  • The type must satisfy an interface for correct behavior (e.g., custom JSON marshaling)
  • Interface changes should break compilation, not silently degrade

Don't add these checks for every interface — only when no other static conversion would catch the error.

Before changing an interface's method set, list its implementations: the build reports each one assigned or passed as the interface, but a type matched only through a type assertion or a type switch stops matching at run time, and nothing fails to compile.

Validation: Run bash <installed-skill-dir>/scripts/check-interface-compliance.sh from the project when a heuristic scan would help. It lists an exported interface implemented in its own package when nothing in the package converts a value to the interface (an assignment, return, argument, composite-literal element, send, or conversion), and one an exported function returns while no function or method in the package takes it as a parameter (the Bad case above; returned_by in --json). A generic interface is matched through the instantiations the package names; generated files and what go ./... skips (testdata, vendor, _ and . names) are not scanned. Ask first whether a consumer needs the interface, then review the conditions above. A finding is not a requirement to add an assertion.


Receiver Type

Default to a pointer receiver. Don't mix receiver types on a single type (recvcheck in the gate reports it) — if any method needs a pointer, use pointers for all methods. Value receivers fit maps, funcs, channels, slices that are not resliced or reallocated, and small immutable structs or basic types; RECEIVER-TYPE.md has the full decision list.

One exception: a value type whose only pointer methods are decoders keeps its encoders and String on the value, as time.Time and netip.Addr do. Moved to *T, they are skipped for a value that is not addressable — a field of a struct passed by value, a map element — and encoding/json v1 and fmt print the raw value with no error and no vet finding:

func (c Celsius) MarshalJSON() ([]byte, error) { return json.Marshal(c.String()) } // value
func (c *Celsius) UnmarshalJSON(b []byte) error { ... }                            // writes *c
var _ json.Marshaler = Celsius{} // a value: (*Celsius)(nil) passes even when values are skipped

Related Skills

  • go-naming: the -er suffix, receiver names.
  • go-error-handling: implementing error, custom error types, errors.As matching.
  • go-generics: whether generics are needed or an interface suffices.
  • go-functions: interface-based Option patterns for constructors.
  • go-defensive: a typed nil stored in an interface and the other API-boundary pitfalls; compile-time assertions stay here.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.