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-interfaces skill takes precedence.
Go Structs & Interfaces
Interface Design Principles
Keep Interfaces Small
"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)
}
type ReadWriter interface {
Reader
Writer
}
Compose larger interfaces from smaller ones:
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
Define Interfaces Where They're Consumed
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.
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.
Accept Interfaces, Return Structs
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.
func NewService(store UserStore) *Service { ... }
func NewService(store UserStore) ServiceInterface { ... }
Don't Create Interfaces Prematurely
"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.
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type userRepository struct { db *sql.DB }
type UserRepository struct { db *sql.DB }
Make the Zero Value Useful
Design structs so they work without explicit initialization. A well-designed zero value reduces constructor boilerplate and prevents nil-related bugs:
var buf bytes.Buffer
buf.WriteString("hello")
var mu sync.Mutex
mu.Lock()
type Registry struct {
items map[string]Item
}
func (r *Registry) Register(name string, item Item) {
if r.items == nil {
r.items = make(map[string]Item)
}
r.items[name] = item
}
Avoid any / interface{} When a Specific Type Will Do
Since 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):
func Contains(slice []any, target any) bool { ... }
func Contains[T comparable](slice []T, target T) bool { ... }
Key Standard Library Interfaces
| 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().
Compile-Time Interface Check
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 & Type Switches
Safe Type Assertion
Type assertions MUST use the comma-ok form to avoid panics:
s, ok := val.(string)
if !ok {
}
s := val.(string)
Type Switch
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)
}
Optional Behavior with Type Assertions
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
}
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).
Struct & Interface Embedding
Struct Embedding
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 := Server{Logger: Logger{slog.Default()}, addr: ":8080"}
s.Info("starting",