Persona: You are a Go type system designer. You favor small, composable interfaces and concrete return types — you design for testability and clarity, not for abstraction's sake.
Community default. A company skill that explicitly supersedes
samber/cc-skills-golang@golang-structs-interfacesskill takes precedence.
"The bigger the interface, the weaker the abstraction." — Go Proverbs
Interfaces SHOULD have 1-3 methods. Small interfaces are easier to implement, mock, and compose. If you need a larger contract, compose it from small interfaces:
→ See samber/cc-skills-golang@golang-naming skill for interface naming conventions (method + "-er" suffix, canonical names)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Composed from small interfaces
type ReadWriter interface {
Reader
Writer
}
Compose larger interfaces from smaller ones:
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
Interfaces Belong to Consumers.
Interfaces MUST be defined where consumed, not where implemented. This keeps the consumer in control of the contract and avoids importing a package just for its interface.
// package notification — defines only what it needs
type Sender interface {
Send(to, body string) error
}
type Service struct {
sender Sender
}
The email package exports a concrete Client struct — it doesn't need to know about Sender.
Functions SHOULD accept interface parameters for flexibility and return concrete types for clarity. Callers get full access to the returned type's fields and methods; consumers upstream can still assign the result to an interface variable if needed.
// Good — accepts interface, returns concrete
func NewService(store UserStore) *Service { ... }
// BAD — NEVER return interfaces from constructors
func NewService(store UserStore) ServiceInterface { ... }
"Don't design with interfaces, discover them."
NEVER create interfaces prematurely — wait for 2+ implementations or a testability requirement. Premature interfaces add indirection without value. Start with concrete types; extract an interface when a second consumer or a test mock demands it.
// Bad — premature interface with a single implementation
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type userRepository struct { db *sql.DB }
// Good — start concrete, extract an interface later when needed
type UserRepository struct { db *sql.DB }
Design structs so they work without explicit initialization. A well-designed zero value reduces constructor boilerplate and prevents nil-related bugs:
// Good — zero value is ready to use
var buf bytes.Buffer
buf.WriteString("hello")
var mu sync.Mutex
mu.Lock()
// Bad — zero value is broken, requires constructor
type Registry struct {
items map[string]Item // nil map, panics on write
}
// Good — lazy initialization guards the zero value
func (r *Registry) Register(name string, item Item) {
if r.items == nil {
r.items = make(map[string]Item)
}
r.items[name] = item
}
any / interface{} When a Specific Type Will DoSince Go 1.18+, MUST prefer generics over any for type-safe operations. Use any only at true boundaries where the type is genuinely unknown (e.g., JSON decoding, reflection):
// Bad — loses type safety
func Contains(slice []any, target any) bool { ... }
// Good — generic, type-safe
func Contains[T comparable](slice []T, target T) bool { ... }
| Interface | Package | Method |
|---|---|---|
Reader |
io |
Read(p []byte) (n int, err error) |
Writer |
io |
Write(p []byte) (n int, err error) |
Closer |
io |
Close() error |
Stringer |
fmt |
String() string |
error |
builtin | Error() string |
Handler |
net/http |
ServeHTTP(ResponseWriter, *Request) |
Marshaler |
encoding/json |
MarshalJSON() ([]byte, error) |
Unmarshaler |
encoding/json |
UnmarshalJSON([]byte) error |
Canonical method signatures MUST be honored — if your type has a String() method, it must match fmt.Stringer. Don't invent ToString() or ReadData().
Verify a type implements an interface at compile time with a blank identifier assignment. Place it near the type definition:
var _ io.ReadWriter = (*MyBuffer)(nil)
This costs nothing at runtime. If MyBuffer ever stops satisfying io.ReadWriter, the build fails immediately.
Type assertions MUST use the comma-ok form to avoid panics:
// Good — safe
s, ok := val.(string)
if !ok {
// handle
}
// Bad — panics if val is not a string
s := val.(string)
Discover the dynamic type of an interface value:
switch v := val.(type) {
case string:
fmt.Println(v)
case int:
fmt.Println(v * 2)
case io.Reader:
io.Copy(os.Stdout, v)
default:
fmt.Printf("unexpected type %T\n", v)
}
Check if a value supports additional capabilities without requiring them upfront:
type Flusher interface {
Flush() error
}
func writeData(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Flush only if the writer supports it
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
This pattern is used extensively in the standard library (e.g., http.Flusher, io.ReaderFrom).
Embedding promotes the inner type's methods and fields to the outer type — composition, not inheritance:
type Logger struct {
*slog.Logger
}
type Server struct {
Logger
addr string
}
// s.Info(...) works — promoted from slog.Logger through Logger
s := Server{Logger: Logger{slog.Default()}, addr: ":8080"}
s.Info("starting", "addr", s.addr)
The receiver of promoted methods is the inner type, not the outer. The outer type can override by defining its own method with the same name.
| Use | When |
|---|---|
| Embed | You want to promote the full API of the inner type — the outer type "is a" enhanced version |
| Named field | You only need the inner type internally — the outer type "has a" dependency |
// Embed — Server exposes all http.Handler methods
type Server struct {
http.Handler
}
// Named field — Server uses the store but doesn't expose its methods
type Server struct {
store *DataStore
}
Accept dependencies as interfaces in constructors. This decouples components and makes testing straightforward:
type UserStore interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
store UserStore
}
func NewUserService(store UserStore) *UserService {
return &UserService{store: store}
}
In tests, pass a mock or stub that satisfies UserStore — no real database needed.
Use field tags for serialization control. Exported fields in serialized structs MUST have field tags:
type Order struct {
ID string `json:"id" db:"id"`
UserID string `json:"user_id" db:"user_id"`
Total float64 `json:"total" db:"total"`
Items []Item `json:"items" db:"-"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
DeletedAt time.Time `json:"-" db:"deleted_at"`
Internal string `json:"-" db:"-"`
}
| Directive | Meaning |
|---|---|
json:"name" |
Field name in JSON output |
json:"name,omitempty" |
Omit field if zero value |
json:"-" |
Always exclude from JSON |
json:",string" |
Encode number/bool as JSON string |
db:"column" |
Database column mapping (sqlx, etc.) |
yaml:"name" |
YAML field name |
xml:"name,attr" |
XML attribute |
validate:"required" |
Struct validation (go-playground/validator) |
Use pointer (s *Server) |
Use value (s Server) |
|---|---|
| Method modifies the receiver | Receiver is small and immutable |
Receiver contains sync.Mutex or similar |
Receiver is a basic type (int, string) |
| Receiver is a large struct | Method is a read-only accessor |
| Consistency: if any method uses a pointer, all should | Map and function values (already reference types) |
Receiver type MUST be consistent across all methods of a type — if one method uses a pointer receiver, all methods should.
noCopySome structs must never be copied after first use (e.g., those containing a mutex, a channel, or internal pointers). Embed a noCopy sentinel to make go vet catch accidental copies:
// noCopy may be added to structs which must not be copied after first use.
// See https://pkg.go.dev/sync#noCopy
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type ConnPool struct {
noCopy noCopy
mu sync.Mutex
conns []*Conn
}
go vet reports an error if a ConnPool value is copied (passed by value, assigned, etc.). This is the same technique the standard library uses for sync.WaitGroup, sync.Mutex, strings.Builder, and others.
Always pass these structs by pointer:
// Good
func process(pool *ConnPool) { ... }
// Bad — go vet will flag this
func process(pool ConnPool) { ... }
samber/cc-skills-golang@golang-naming skill for interface naming conventions (Reader, Closer, Stringer)samber/cc-skills-golang@golang-design-patterns skill for functional options, constructors, and builder patternssamber/cc-skills-golang@golang-dependency-injection skill for DI patterns using interfacessamber/cc-skills-golang@golang-code-style skill for value vs pointer function parameters (distinct from receivers)| Mistake | Fix |
|---|---|
| Large interfaces (5+ methods) | Split into focused 1-3 method interfaces, compose if needed |
| Defining interfaces in the implementor package | Define where consumed |
| Returning interfaces from constructors | Return concrete types |
| Bare type assertions without comma-ok | Always use v, ok := x.(T) |
| Embedding when you only need a few methods | Use a named field and delegate explicitly |
| Missing field tags on serialized structs | Tag all exported fields in marshaled types |
| Mixing pointer and value receivers on a type | Pick one and be consistent |
| Forgetting compile-time interface check | Add var _ Interface = (*Type)(nil) |
Using ToString() instead of String() |
Honor canonical method names |
| Premature interface with a single implementation | Start concrete, extract interface when needed |
| Nil map/slice in zero value struct | Use lazy initialization in methods |
Using any for type-safe operations |
Use generics ([T comparable]) instead |