mcpField
G
golang-structs-interfaces

golang-structs-interfaces

github.com/samber开发3.7k40.2k 次安装5 天前更新
代码仓库

关于 golang-structs-interfaces

面向开发者和编码 Agent 的 Go 类型设计技能,涵盖结构体、接口、泛型和组合,强调由使用方定义小接口并返回具体类型。

主要功能

  1. 01在使用方定义小型接口
  2. 02优先返回具体类型
  3. 03设计可直接使用的结构体零值
  4. 04用泛型实现类型安全操作
  5. 05使用接口检查和依赖注入

适用场景

  1. 01为使用方程序包设计接口
  2. 02通过注入依赖构建可测试服务
  3. 03审查结构体和接收者设计
  4. 04在编译时检查接口实现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-structs-interfaces

安装后在 Claude Code 中输入/golang-structs-interfaces确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
struct-fields.md
type-assertions.md

详细介绍

**角色:**你是一名 Go 类型系统设计师。你推崇小巧、可组合的接口和具体的返回类型——设计时注重可测试性和清晰度,而非为了抽象而抽象。

**社区默认规则。**明确取代 samber/cc-skills-golang@golang-structs-interfaces 技能的公司技能优先。

Go 结构体与接口

接口设计原则

保持接口小巧

“接口越大,抽象越弱。”——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)
}

// Composed from small interfaces
type ReadWriter interface {
    Reader
    Writer
}

用较小的接口组合成更大的接口:

type ReadWriteCloser interface {
    io.Reader
    io.Writer
    io.Closer
}
在使用接口的地方定义接口

接口属于使用方。

接口必须在使用处定义,而非实现处。这样可以让使用方掌控契约,也避免仅为使用接口而导入某个包。

// package notification — defines only what it needs
type Sender interface {
    Send(to, body string) error
}

type Service struct {
    sender Sender
}

email 包导出一个具体的 Client 结构体——它无需了解 Sender。

接受接口,返回结构体

函数应接受接口参数以提高灵活性,并返回具体类型以确保清晰。调用方可以完整访问返回类型的字段和方法;上游使用方仍可根据需要将结果赋给接口变量。

// Good — accepts interface, returns concrete
func NewService(store UserStore) *Service { ... }

// Bad — an interface return hides every other method of the concrete type from callers
func NewService(store UserStore) ServiceInterface { ... }
不要过早创建接口

“不要用接口来设计,要去发现接口。”

在第二种实现出现之前编写接口,相当于猜测哪些方法会变化——而这种猜测通常是错的,因此无论如何都得重新调整抽象。与此同时,它还增加了一层间接性,使读者和工具难以看到具体类型。从具体类型开始;当第二个使用方、第二种实现或测试模拟需要接口时,再提取接口。

// 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 }

让零值可用

设计结构体时,应让它们无需显式初始化即可工作。设计良好的零值可以减少构造函数的样板代码,并避免与 nil 相关的错误:

// 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{}

从 Go 1.18 起,对于类型安全的操作,必须优先使用泛型而非 any。仅在类型确实未知的边界处使用 any(例如 JSON 解码、反射):

// Bad — loses type safety
func Contains(slice []any, target any) bool { ... }

// Good — generic, type-safe
func Contains[T comparable](slice []T, target T) bool { ... }

重要的标准库接口

接口包方法
ReaderioRead(p []byte) (n int, err error)
WriterioWrite(p []byte) (n int, err error)
CloserioClose() error
StringerfmtString() string
errorbuiltinError() string
Handlernet/httpServeHTTP(ResponseWriter, *Request)
Marshalerencoding/jsonMarshalJSON() ([]byte, error)
Unmarshalerencoding/jsonUnmarshalJSON([]byte) error

必须遵循规范的方法签名——如果你的类型有 String() 方法,它必须符合 fmt.Stringer。不要自行创造 ToString() 或 ReadData()。

编译时接口检查

使用空白标识符赋值,在编译时验证某个类型是否实现了接口。将其放在类型定义附近:

var _ io.ReadWriter = (*MyBuffer)(nil)

这不会产生任何运行时开销。如果 MyBuffer 不再满足 io.ReadWriter,构建会立即失败。

类型断言与类型 switch

类型断言必须使用 comma-ok 形式(s, ok := val.(string))——单值形式在类型不匹配时会触发 panic,而不是进行分支处理。使用类型 switch 根据动态类型进行分派,并断言为小型可选接口(if f, ok := w.(Flusher); ok),以利用更丰富的实现,而不必扩大已声明的参数类型。

→ 参见 类型断言与类型 switch,了解类型 switch 的顺序、nil 情况以及可选行为模式。

结构体与接口嵌入

结构体嵌入

嵌入会将内部类型的方法和字段提升到外部类型——这是组合,不是继承:

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)

提升方法的接收者是_内部_类型,而不是外部类型。外部类型可以定义同名方法来覆盖它。

何时使用嵌入,何时使用命名字段
用途时机
嵌入希望提升内部类型的完整 API——外部类型是增强版的“内部类型”
命名字段只需在内部使用内部类型——外部类型“拥有一个”依赖项
// 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
}

通过接口进行依赖注入

在构造函数中将依赖项作为接口接收。这样可以解耦组件,并简化测试:

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 的 mock 或 stub——无需真实数据库。

结构体字段标签

序列化结构体中的导出字段必须带有字段标签——如果没有,编码器会回退到 Go 字段名,因此重命名字段会悄无声息地改变传输格式:

type Order struct {
    ID        string    `json:"id"         db:"id"`
    Total     float64   `json:"total"      db:"total"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
    Internal  string    `json:"-"          db:"-"`
}

→ 参见 结构体字段:标签与复制安全,了解完整的标签指令表、omitempty 与 omitzero 的陷阱,以及 go vet 诊断信息。

指针接收者与值接收者

使用指针 (s *Server)使用值 (s Server)
方法会修改接收者接收者较小且不可变
接收者包含 sync.Mutex 或类似内容接收者是基本类型(int、string)
接收者是大型结构体方法是只读访问器
保持一致:如果有任何方法使用指针,所有方法都应使用指针Map 和函数值(本身已是引用类型)

同一类型的所有方法必须保持接收者类型一致——如果某个方法使用指针接收者,所有方法都应如此。

使用 noCopy 防止结构体复制

持有互斥锁、通道或内部指针的结构体在复制后会出问题:副本会复制锁状态,因此两个 goroutine 会分别保护不同的互斥锁,而不变量会悄无声息地失效。嵌入 noCopy 哨兵,让 go vet 报告每一次值复制,并通过指针传递此类结构体。

诊断: 1- go vet ./... — copylocks 会报告包含锁的结构体被按值复制的情况

→ 参见 结构体字段:标签与复制安全,了解 noCopy 的实现以及 vet 如何检测它。

交叉引用

  • → 有关接口命名约定(Reader、Closer、Stringer),请参见 samber/cc-skills-golang@golang-naming skill
  • → 有关函数式选项、构造函数和构建器模式,请参见 samber/cc-skills-golang@golang-design-patterns skill
  • → 有关使用接口实现 DI 的模式,请参见 samber/cc-skills-golang@golang-dependency-injection skill
  • → 有关值参数与指针参数(与接收者不同),请参见 samber/cc-skills-golang@golang-code-style skill
  • → 有关安全重命名和 implementInterface 代码操作,请参见 samber/cc-skills-golang@golang-gopls skill——重命名参与接口满足关系的方法或接收者时,会更新所有调用点,并拒绝会悄然破坏接口的重命名;grep/sed 无法检测这种情况

常见错误

错误修正方法
接口过大(5 个以上方法)拆分为专注于 1-3 个方法的接口,并在需要时组合
在实现方的包中定义接口在使用接口的地方定义
构造函数返回接口返回具体类型
类型断言未使用逗号-ok 形式始终使用 v, ok := x.(T)
只需要少数几个方法,却使用嵌入使用具名字段并显式委托
序列化结构体缺少字段标签为序列化类型中的所有导出字段添加标签
同一类型混用指针接收者和值接收者选择一种并保持一致
忘记编译时接口检查添加 var _ Interface = (*Type)(nil)
使用 ToString() 而非 String()遵循规范的方法名称
只有一个实现时就过早定义接口从具体类型开始,按需提取接口
零值结构体中的 map/slice 为 nil在方法中使用延迟初始化
使用 any 进行类型安全操作改用泛型([T comparable])

评论

支持 Markdown