golang-structs-interfaces

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
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类型系统设计师。你偏好小巧、可组合的接口和具体返回类型——你的设计以可测试性和清晰性为目标,而非为了抽象而抽象。
社区默认规则:如果存在明确替代
samber/cc-skills-golang@golang-structs-interfaces
的企业技能,将优先使用该企业技能。

Go Structs & Interfaces

Go结构体与接口

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)
go
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:
go
type ReadWriteCloser interface {
    io.Reader
    io.Writer
    io.Closer
}
"接口越大,抽象性越弱。" —— Go箴言
接口应该包含1-3个方法。小巧的接口更易于实现、模拟和组合。如果需要更复杂的契约,可以通过组合多个小接口来实现:
→ 关于接口命名规范(方法名+"-er"后缀、标准命名),请参考
samber/cc-skills-golang@golang-naming
技能
go
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
}
通过小接口组合成更大的接口:
go
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.
go
// 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
.
接口属于消费方。
接口必须在消费它的地方定义,而非实现它的地方。这样可以让消费方掌控契约,避免仅仅为了导入接口而引入整个包。
go
// notification包——仅定义自身所需的接口
type Sender interface {
    Send(to, body string) error
}

type Service struct {
    sender Sender
}
email
包对外暴露具体的
Client
结构体——它不需要知道
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.
go
// Good — accepts interface, returns concrete
func NewService(store UserStore) *Service { ... }

// BAD — NEVER return interfaces from constructors
func NewService(store UserStore) ServiceInterface { ... }
函数应该接受接口类型的参数以提升灵活性,返回具体类型以保证清晰性。调用者可以完全访问返回类型的字段和方法;上游消费方如果需要,仍可将结果赋值给接口变量。
go
// 推荐——接受接口,返回具体类型
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.
go
// 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 }
"不要为设计而设计接口,要在实践中发现接口。"
绝对不要过早创建接口——等到有2个及以上实现,或是出于可测试性需求时再创建。过早的接口会增加不必要的间接层。先从具体类型开始;当出现第二个消费方或需要测试模拟时,再提取接口。
go
// 不推荐——仅有一个实现的过早接口
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:
go
// 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
}
设计结构体时,要确保其无需显式初始化即可工作。设计良好的零值可以减少构造函数的样板代码,避免空指针相关的bug:
go
// 推荐——零值可直接使用
var buf bytes.Buffer
buf.WriteString("hello")

var mu sync.Mutex
mu.Lock()

// 不推荐——零值不可用,必须使用构造函数
type Registry struct {
    items map[string]Item // 空map,写入时会panic
}

// 推荐——延迟初始化保证零值可用
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

能用具体类型时避免使用
any
/
interface{}

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):
go
// Bad — loses type safety
func Contains(slice []any, target any) bool { ... }

// Good — generic, type-safe
func Contains[T comparable](slice []T, target T) bool { ... }
从Go 1.18+开始,对于类型安全的操作,必须优先使用泛型而非
any
。仅在类型确实未知的真正边界场景(如JSON解码、反射)中使用
any
go
// 不推荐——失去类型安全性
func Contains(slice []any, target any) bool { ... }

// 推荐——泛型,类型安全
func Contains[T comparable](slice []T, target T) bool { ... }

Key Standard Library Interfaces

关键标准库接口

InterfacePackageMethod
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()
.
接口包名方法签名
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
内置包
Error() string
Handler
net/http
ServeHTTP(ResponseWriter, *Request)
Marshaler
encoding/json
MarshalJSON() ([]byte, error)
Unmarshaler
encoding/json
UnmarshalJSON([]byte) error
必须遵循标准方法签名——如果你的类型有
String()
方法,它必须匹配
fmt.Stringer
的签名。不要自定义
ToString()
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:
go
var _ io.ReadWriter = (*MyBuffer)(nil)
This costs nothing at runtime. If
MyBuffer
ever stops satisfying
io.ReadWriter
, the build fails immediately.
通过空白标识符赋值,在编译时验证类型是否实现了接口。将该代码放在类型定义附近:
go
var _ io.ReadWriter = (*MyBuffer)(nil)
这在运行时不会产生任何开销。如果
MyBuffer
不再满足
io.ReadWriter
,构建会立即失败。

Type Assertions & Type Switches

类型断言与类型开关

Safe Type Assertion

安全的类型断言

Type assertions MUST use the comma-ok form to avoid panics:
go
// Good — safe
s, ok := val.(string)
if !ok {
    // handle
}

// Bad — panics if val is not a string
s := val.(string)
类型断言必须使用逗号-ok形式以避免panic:
go
// 推荐——安全
s, ok := val.(string)
if !ok {
    // 处理异常
}

// 不推荐——如果val不是string类型会panic
s := val.(string)

Type Switch

类型开关

Discover the dynamic type of an interface value:
go
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)
}
用于发现接口值的动态类型:
go
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("意外类型 %T\n", v)
}

Optional Behavior with Type Assertions

基于类型断言的可选行为

Check if a value supports additional capabilities without requiring them upfront:
go
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
).
检查值是否支持额外功能,而无需预先强制要求:
go
type Flusher interface {
    Flush() error
}

func writeData(w io.Writer, data []byte) error {
    if _, err := w.Write(data); err != nil {
        return err
    }
    // 仅当writer支持时才执行Flush
    if f, ok := w.(Flusher); ok {
        return f.Flush()
    }
    return nil
}
这种模式在标准库中被广泛使用(如
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:
go
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.
嵌入可以将内部类型的方法和字段提升到外部类型——这是组合,而非继承:
go
type Logger struct {
    *slog.Logger
}

type Server struct {
    Logger
    addr string
}

// s.Info(...) 可直接调用——通过Logger从slog.Logger提升而来
s := Server{Logger: Logger{slog.Default()}, addr: ":8080"}
s.Info("starting", "addr", s.addr)
被提升方法的接收器是内部类型,而非外部类型。外部类型可以通过定义同名方法来覆盖被提升的方法。

When to Embed vs Named Field

何时使用嵌入 vs 命名字段

UseWhen
EmbedYou want to promote the full API of the inner type — the outer type "is a" enhanced version
Named fieldYou only need the inner type internally — the outer type "has a" dependency
go
// 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
}
方式适用场景
嵌入你希望暴露内部类型的完整API——外部类型是内部类型的增强版本
命名字段你仅在内部使用内部类型——外部类型「拥有」一个依赖
go
// 嵌入——Server暴露所有http.Handler方法
type Server struct {
    http.Handler
}

// 命名字段——Server使用store但不暴露其方法
type Server struct {
    store *DataStore
}

Dependency Injection via Interfaces

基于接口的依赖注入

Accept dependencies as interfaces in constructors. This decouples components and makes testing straightforward:
go
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.
在构造函数中接受接口类型的依赖。这可以解耦组件,让测试变得简单:
go
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}
}
在测试中,只需传入一个满足
UserStore
的模拟或桩实现——无需真实数据库。

Struct Field Tags

结构体字段标签

Use field tags for serialization control. Exported fields in serialized structs MUST have field tags:
go
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:"-"`
}
DirectiveMeaning
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)
使用字段标签控制序列化行为。可序列化结构体中的导出字段必须添加字段标签:
go
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:"-"`
}
指令含义
json:"name"
JSON输出中的字段名
json:"name,omitempty"
字段为零值时忽略
json:"-"
始终从JSON中排除
json:",string"
将数字/布尔值编码为JSON字符串
db:"column"
数据库列映射(如sqlx等框架)
yaml:"name"
YAML字段名
xml:"name,attr"
XML属性
validate:"required"
结构体验证(go-playground/validator框架)

Pointer vs Value Receivers

指针接收器 vs 值接收器

Use pointer
(s *Server)
Use value
(s Server)
Method modifies the receiverReceiver is small and immutable
Receiver contains
sync.Mutex
or similar
Receiver is a basic type (int, string)
Receiver is a large structMethod is a read-only accessor
Consistency: if any method uses a pointer, all shouldMap 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.
使用指针接收器
(s *Server)
使用值接收器
(s Server)
方法需要修改接收器接收器体积小且不可变
接收器包含
sync.Mutex
或类似类型
接收器是基础类型(int、string)
接收器是大型结构体方法是只读访问器
一致性:如果有一个方法使用指针接收器,所有方法都应使用映射和函数值(本身已是引用类型)
同一类型的所有接收器类型必须保持一致——如果有一个方法使用指针接收器,所有方法都应使用指针接收器。

Preventing Struct Copies with
noCopy

使用
noCopy
防止结构体复制

Some 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:
go
// 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:
go
// Good
func process(pool *ConnPool) { ... }

// Bad — go vet will flag this
func process(pool ConnPool) { ... }
有些结构体在首次使用后绝对不能被复制(如包含互斥锁、通道或内部指针的结构体)。嵌入
noCopy
标记可以让
go vet
捕获意外的复制操作:
go
// noCopy可添加到首次使用后不能复制的结构体中。
// 参考: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
}
如果
ConnPool
值被复制(按值传递、赋值等),
go vet
会报告错误。标准库中的
sync.WaitGroup
sync.Mutex
strings.Builder
等类型都使用了相同的技术。
始终通过指针传递这些结构体:
go
// 推荐
func process(pool *ConnPool) { ... }

// 不推荐——go vet会标记错误
func process(pool ConnPool) { ... }

Cross-References

交叉引用

  • → See
    samber/cc-skills-golang@golang-naming
    skill for interface naming conventions (Reader, Closer, Stringer)
  • → See
    samber/cc-skills-golang@golang-design-patterns
    skill for functional options, constructors, and builder patterns
  • → See
    samber/cc-skills-golang@golang-dependency-injection
    skill for DI patterns using interfaces
  • → See
    samber/cc-skills-golang@golang-code-style
    skill for value vs pointer function parameters (distinct from receivers)
  • → 接口命名规范(Reader、Closer、Stringer)请参考
    samber/cc-skills-golang@golang-naming
    技能
  • → 函数选项、构造函数和构建器模式请参考
    samber/cc-skills-golang@golang-design-patterns
    技能
  • → 基于接口的依赖注入模式请参考
    samber/cc-skills-golang@golang-dependency-injection
    技能
  • → 值参数与指针参数的对比(与接收器不同)请参考
    samber/cc-skills-golang@golang-code-style
    技能

Common Mistakes

常见错误

MistakeFix
Large interfaces (5+ methods)Split into focused 1-3 method interfaces, compose if needed
Defining interfaces in the implementor packageDefine where consumed
Returning interfaces from constructorsReturn concrete types
Bare type assertions without comma-okAlways use
v, ok := x.(T)
Embedding when you only need a few methodsUse a named field and delegate explicitly
Missing field tags on serialized structsTag all exported fields in marshaled types
Mixing pointer and value receivers on a typePick one and be consistent
Forgetting compile-time interface checkAdd
var _ Interface = (*Type)(nil)
Using
ToString()
instead of
String()
Honor canonical method names
Premature interface with a single implementationStart concrete, extract interface when needed
Nil map/slice in zero value structUse lazy initialization in methods
Using
any
for type-safe operations
Use generics (
[T comparable]
) instead
错误修复方案
大型接口(5个以上方法)拆分为专注的1-3方法接口,必要时组合
在实现方包中定义接口在消费方定义接口
从构造函数返回接口返回具体类型
未使用逗号-ok形式的裸类型断言始终使用
v, ok := x.(T)
仅需要少数方法时使用嵌入使用命名字段并显式委托
可序列化结构体缺少字段标签为所有序列化的导出字段添加标签
同一类型混合使用指针和值接收器选择一种并保持一致
忘记编译时接口检查添加
var _ Interface = (*Type)(nil)
使用
ToString()
而非
String()
遵循标准方法名
仅有一个实现的过早接口先从具体类型开始,必要时再提取接口
结构体零值中的空map/切片在方法中使用延迟初始化
类型安全操作中使用
any
改用泛型(
[T comparable]