golang-structs-interfaces
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePersona: 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 supersedesskill takes precedence.samber/cc-skills-golang@golang-structs-interfaces
角色定位: 你是一名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 skill for interface naming conventions (method + "-er" suffix, canonical names)
samber/cc-skills-golang@golang-naminggo
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-naminggo
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 package exports a concrete struct — it doesn't need to know about .
emailClientSender接口属于消费方。
接口必须在消费它的地方定义,而非实现它的地方。这样可以让消费方掌控契约,避免仅仅为了导入接口而引入整个包。
go
// notification包——仅定义自身所需的接口
type Sender interface {
Send(to, body string) error
}
type Service struct {
sender Sender
}emailClientSenderAccept 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
anyinterface{}能用具体类型时避免使用any
/interface{}
anyinterface{}Since Go 1.18+, MUST prefer generics over for type-safe operations. Use only at true boundaries where the type is genuinely unknown (e.g., JSON decoding, reflection):
anyanygo
// 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+开始,对于类型安全的操作,必须优先使用泛型而非。仅在类型确实未知的真正边界场景(如JSON解码、反射)中使用:
anyanygo
// 不推荐——失去类型安全性
func Contains(slice []any, target any) bool { ... }
// 推荐——泛型,类型安全
func Contains[T comparable](slice []T, target T) bool { ... }Key Standard Library Interfaces
关键标准库接口
| Interface | Package | Method |
|---|---|---|
| | |
| | |
| | |
| | |
| builtin | |
| | |
| | |
| | |
Canonical method signatures MUST be honored — if your type has a method, it must match . Don't invent or .
String()fmt.StringerToString()ReadData()| 接口 | 包名 | 方法签名 |
|---|---|---|
| | |
| | |
| | |
| | |
| 内置包 | |
| | |
| | |
| | |
必须遵循标准方法签名——如果你的类型有方法,它必须匹配的签名。不要自定义或这类方法。
String()fmt.StringerToString()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 ever stops satisfying , the build fails immediately.
MyBufferio.ReadWriter通过空白标识符赋值,在编译时验证类型是否实现了接口。将该代码放在类型定义附近:
go
var _ io.ReadWriter = (*MyBuffer)(nil)这在运行时不会产生任何开销。如果不再满足,构建会立即失败。
MyBufferio.ReadWriterType 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.Flusherio.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.Flusherio.ReaderFromStruct & 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 命名字段
| 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 |
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 — no real database needed.
UserStore在构造函数中接受接口类型的依赖。这可以解耦组件,让测试变得简单:
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}
}在测试中,只需传入一个满足的模拟或桩实现——无需真实数据库。
UserStoreStruct 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:"-"`
}| Directive | Meaning |
|---|---|
| Field name in JSON output |
| Omit field if zero value |
| Always exclude from JSON |
| Encode number/bool as JSON string |
| Database column mapping (sqlx, etc.) |
| YAML field name |
| XML attribute |
| 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输出中的字段名 |
| 字段为零值时忽略 |
| 始终从JSON中排除 |
| 将数字/布尔值编码为JSON字符串 |
| 数据库列映射(如sqlx等框架) |
| YAML字段名 |
| XML属性 |
| 结构体验证(go-playground/validator框架) |
Pointer vs Value Receivers
指针接收器 vs 值接收器
Use pointer | Use value |
|---|---|
| Method modifies the receiver | Receiver is small and immutable |
Receiver contains | 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.
使用指针接收器 | 使用值接收器 |
|---|---|
| 方法需要修改接收器 | 接收器体积小且不可变 |
接收器包含 | 接收器是基础类型(int、string) |
| 接收器是大型结构体 | 方法是只读访问器 |
| 一致性:如果有一个方法使用指针接收器,所有方法都应使用 | 映射和函数值(本身已是引用类型) |
同一类型的所有接收器类型必须保持一致——如果有一个方法使用指针接收器,所有方法都应使用指针接收器。
Preventing Struct Copies with noCopy
noCopy使用noCopy
防止结构体复制
noCopySome structs must never be copied after first use (e.g., those containing a mutex, a channel, or internal pointers). Embed a sentinel to make catch accidental copies:
noCopygo vetgo
// 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 vetConnPoolsync.WaitGroupsync.Mutexstrings.BuilderAlways pass these structs by pointer:
go
// Good
func process(pool *ConnPool) { ... }
// Bad — go vet will flag this
func process(pool ConnPool) { ... }有些结构体在首次使用后绝对不能被复制(如包含互斥锁、通道或内部指针的结构体)。嵌入标记可以让捕获意外的复制操作:
noCopygo vetgo
// 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
}如果值被复制(按值传递、赋值等),会报告错误。标准库中的、、等类型都使用了相同的技术。
ConnPoolgo vetsync.WaitGroupsync.Mutexstrings.Builder始终通过指针传递这些结构体:
go
// 推荐
func process(pool *ConnPool) { ... }
// 不推荐——go vet会标记错误
func process(pool ConnPool) { ... }Cross-References
交叉引用
- → See skill for interface naming conventions (Reader, Closer, Stringer)
samber/cc-skills-golang@golang-naming - → See skill for functional options, constructors, and builder patterns
samber/cc-skills-golang@golang-design-patterns - → See skill for DI patterns using interfaces
samber/cc-skills-golang@golang-dependency-injection - → See skill for value vs pointer function parameters (distinct from receivers)
samber/cc-skills-golang@golang-code-style
- → 接口命名规范(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
常见错误
| 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 |
| 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 |
Using | 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 | Use generics ( |
| 错误 | 修复方案 |
|---|---|
| 大型接口(5个以上方法) | 拆分为专注的1-3方法接口,必要时组合 |
| 在实现方包中定义接口 | 在消费方定义接口 |
| 从构造函数返回接口 | 返回具体类型 |
| 未使用逗号-ok形式的裸类型断言 | 始终使用 |
| 仅需要少数方法时使用嵌入 | 使用命名字段并显式委托 |
| 可序列化结构体缺少字段标签 | 为所有序列化的导出字段添加标签 |
| 同一类型混合使用指针和值接收器 | 选择一种并保持一致 |
| 忘记编译时接口检查 | 添加 |
使用 | 遵循标准方法名 |
| 仅有一个实现的过早接口 | 先从具体类型开始,必要时再提取接口 |
| 结构体零值中的空map/切片 | 在方法中使用延迟初始化 |
类型安全操作中使用 | 改用泛型( |