golang-structs-interfaces by samber/cc-skills-golang
npx skills add https://github.com/samber/cc-skills-golang --skill golang-structs-interfaces角色: 你是一名 Go 类型系统设计师。你偏爱小巧、可组合的接口和具体的返回类型——你的设计是为了可测试性和清晰性,而不是为了抽象而抽象。
社区默认。 明确取代
samber/cc-skills-golang@golang-structs-interfaces技能的 company skill 具有优先权。
"接口越大,抽象越弱。" — Go 箴言
接口应该只有 1-3 个方法。小巧的接口更容易实现、模拟和组合。如果你需要一个更大的契约,请从小接口组合而成:
→ 关于接口命名约定(方法 + "-er" 后缀,规范名称),请参见 samber/cc-skills-golang@golang-naming 技能。
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
}
从小接口组合出更大的接口:
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
接口属于消费者。
接口必须在消费它们的地方定义,而不是在实现它们的地方。这使消费者能够控制契约,并避免仅仅为了接口而导入一个包。
// package notification — 仅定义它需要的内容
type Sender interface {
Send(to, body string) error
}
type Service struct {
sender Sender
}
email 包导出一个具体的 Client 结构体——它不需要知道 Sender。
函数应该接受接口参数以获得灵活性,并返回具体类型以获得清晰性。调用者可以完全访问返回类型的字段和方法;如果需要,上游消费者仍然可以将结果赋值给接口变量。
// 良好 — 接受接口,返回具体类型
func NewService(store UserStore) *Service { ... }
// 错误 — 构造函数**绝不**返回接口
func NewService(store UserStore) ServiceInterface { ... }
"不要用接口来设计,去发现它们。"
绝不过早创建接口——等待出现 2 个或更多实现,或者出现可测试性需求。过早的接口在没有价值的情况下增加了间接性。从具体类型开始;当第二个消费者或测试模拟需要时,再提取接口。
// 错误 — 只有一个实现时的过早接口
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type userRepository struct { db *sql.DB }
// 良好 — 从具体类型开始,在需要时再提取接口
type UserRepository struct { db *sql.DB }
设计结构体,使其无需显式初始化即可工作。一个设计良好的零值可以减少构造函数样板代码,并防止与 nil 相关的错误:
// 良好 — 零值立即可用
var buf bytes.Buffer
buf.WriteString("hello")
var mu sync.Mutex
mu.Lock()
// 错误 — 零值无效,需要构造函数
type Registry struct {
items map[string]Item // nil map,写入时会 panic
}
// 良好 — 惰性初始化保护零值
func (r *Registry) Register(name string, item Item) {
if r.items == nil {
r.items = make(map[string]Item)
}
r.items[name] = item
}
any / interface{}自 Go 1.18+ 起,对于类型安全的操作,必须优先使用泛型而非 any。仅在类型确实未知的真正边界处(例如 JSON 解码、反射)使用 any:
// 错误 — 失去类型安全性
func Contains(slice []any, target any) bool { ... }
// 良好 — 泛型,类型安全
func Contains[T comparable](slice []T, target T) bool { ... }
| 接口 | 包 | 方法 |
|---|---|---|
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 |
规范的方法签名必须被遵守——如果你的类型有一个 String() 方法,它必须匹配 fmt.Stringer。不要发明 ToString() 或 ReadData()。
使用空白标识符赋值在编译时验证一个类型是否实现了接口。将其放在类型定义附近:
var _ io.ReadWriter = (*MyBuffer)(nil)
这在运行时没有任何开销。如果 MyBuffer 不再满足 io.ReadWriter,构建会立即失败。
类型断言必须使用逗号-ok 形式来避免 panic:
// 良好 — 安全
s, ok := val.(string)
if !ok {
// 处理
}
// 错误 — 如果 val 不是字符串会 panic
s := val.(string)
发现接口值的动态类型:
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)
}
检查一个值是否支持额外的能力,而无需提前要求它们:
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
}
这种模式在标准库中被广泛使用(例如 http.Flusher、io.ReaderFrom)。
嵌入将内部类型的方法和字段提升到外部类型——这是组合,而不是继承:
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)
被提升方法的接收者是内部类型,而不是外部类型。外部类型可以通过定义自己的同名方法来覆盖。
| 用途 | 时机 |
|---|---|
| 嵌入 | 你想要提升内部类型的完整 API —— 外部类型"是一个"增强版本 |
| 命名字段 | 你只需要在内部使用该类型 —— 外部类型"有一个"依赖 |
// 嵌入 — Server 暴露所有 http.Handler 方法
type Server struct {
http.Handler
}
// 命名字段 — Server 使用 store 但不暴露其方法
type Server struct {
store *DataStore
}
在构造函数中接受接口作为依赖。这解耦了组件,并使测试变得简单:
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 的模拟或存根——不需要真实的数据库。
使用字段标签来控制序列化。已导出的序列化结构体字段必须有字段标签:
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) |
使用指针 (s *Server) | 使用值 (s Server) |
|---|---|
| 方法修改接收者 | 接收者小巧且不可变 |
接收者包含 sync.Mutex 或类似物 | 接收者是基本类型 (int, string) |
| 接收者是大型结构体 | 方法是只读访问器 |
| 一致性:如果任何方法使用指针,则所有方法都应使用 | Map 和函数值(已经是引用类型) |
接收者类型在类型的所有方法中必须保持一致——如果一个方法使用指针接收者,所有方法都应该使用。
noCopy 防止结构体复制某些结构体在首次使用后绝不能复制(例如,包含互斥锁、通道或内部指针的结构体)。嵌入一个 noCopy 哨兵,让 go vet 捕获意外的复制:
// 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 和其他类型的相同技术。
始终通过指针传递这些结构体:
// 良好
func process(pool *ConnPool) { ... }
// 错误 — go vet 会标记这个
func process(pool ConnPool) { ... }
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 技能| 错误 | 修复 |
|---|---|
| 大型接口(5+ 方法) | 拆分为专注的 1-3 方法接口,如果需要则组合 |
| 在实现者包中定义接口 | 在消费的地方定义 |
| 从构造函数返回接口 | 返回具体类型 |
| 没有逗号-ok 的裸类型断言 | 始终使用 v, ok := x.(T) |
| 当你只需要几个方法时却使用嵌入 | 使用命名字段并显式委托 |
| 序列化结构体缺少字段标签 | 为所有已导出的可序列化字段添加标签 |
| 在类型上混合使用指针和值接收者 | 选择一种并保持一致 |
| 忘记编译时接口检查 | 添加 var _ Interface = (*Type)(nil) |
使用 ToString() 而不是 String() | 遵守规范方法名 |
| 只有一个实现时的过早接口 | 从具体类型开始,需要时再提取接口 |
| 零值结构体中的 nil map/slice | 在方法中使用惰性初始化 |
对类型安全的操作使用 any | 改用泛型 ([T comparable]) |
每周安装次数
125
仓库
GitHub 星标数
276
首次出现
4 天前
安全审计
安装于
opencode104
gemini-cli101
codex101
cursor101
kimi-cli100
amp100
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 |
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 |
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 |
Weekly Installs
125
Repository
GitHub Stars
276
First Seen
4 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode104
gemini-cli101
codex101
cursor101
kimi-cli100
amp100
Perl安全编程指南:输入验证、注入防护与安全编码实践
1,100 周安装
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 |
xml:"name,attr" | XML attribute |
validate:"required" | Struct validation (go-playground/validator) |
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 |