mcpField
G
go-interface-design

go-interface-design

github.com/eduardo-sl开发83182 次安装2 周前更新
代码仓库

关于 go-interface-design

面向 Go 开发者和编码 Agent 的接口设计技能,介绍接口的定义位置、规模控制及常见设计陷阱。

主要功能

  1. 01在使用方定义接口
  2. 02保持接口小巧且可组合
  3. 03接收接口并返回具体类型
  4. 04在编译期验证接口实现
  5. 05避免使用接口指针
  6. 06识别嵌入与类型断言陷阱

适用场景

  1. 01依据使用方需求设计包边界
  2. 02添加测试替身而不扩大接口
  3. 03检查 Go 代码中的接口反模式

安装

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

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-interface-design

安装后在 Claude Code 中输入/go-interface-design确认已加载。

附带文件references/embedding-and-assertions.md

详细介绍

Go 接口设计

Go 接口是隐式的。这是这门语言最重要的设计特性,大多数从 Java 或 C# 转过来的人一开始都会理解错。

社区默认规则。如果某个项目技能或 Agent 配置规则明确取代了 go-interface-design,则以该规则为准。

按需加载的详细参考资料:

  • references/embedding-and-assertions.md — 嵌入与命名字段、接口嵌入式测试替身、可选能力、封闭接口。

仅当下文摘要不够时,才阅读参考文件。

1. 首要原则:在使用方定义接口

行为的使用方定义接口,而不是提供方:

// ❌ Wrong — producer defines interface (Java thinking)
// package store
type UserStore interface {      // defined alongside implementation
    GetByID(ctx context.Context, id string) (*User, error)
    Create(ctx context.Context, user *User) error
    // ... 15 more methods
}

type PostgresStore struct { ... }
func (s *PostgresStore) GetByID(...) { ... }
func (s *PostgresStore) Create(...) { ... }

// ✅ Right — consumer defines what it needs
// package service
type UserReader interface {     // only what THIS service needs
    GetByID(ctx context.Context, id string) (*domain.User, error)
}

type UserService struct {
    store UserReader  // depends on narrow interface
}

// package store (no interface defined here)
type PostgresStore struct { db *sql.DB }
func (s *PostgresStore) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }
func (s *PostgresStore) Create(ctx context.Context, user *domain.User) error { ... }

// PostgresStore satisfies service.UserReader implicitly — no declaration needed

这样做很重要,因为:

  • 使用方仅依赖实际使用的内容(接口隔离原则)。
  • 提供方可以添加方法,而不会破坏使用方。
  • 测试只需实现使用方调用的方法。
  • 不会产生导入循环:使用方无需导入提供方的包。

2. 保持接口精简

接口越大,抽象能力越弱。

// ✅ Good — focused, composable
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
}

// ❌ Bad — kitchen sink interface
type FileManager interface {
    Read(path string) ([]byte, error)
    Write(path string, data []byte) error
    Delete(path string) error
    List(dir string) ([]string, error)
    Move(src, dst string) error
    Copy(src, dst string) error
    Stat(path string) (os.FileInfo, error)
    Watch(path string) (<-chan Event, error)
}

指导原则:理想情况下为 1~3 个方法。如果需要更多方法,就组合较小的接口。

3. 接受接口,返回结构体

// ✅ Good — accepts interface, returns concrete type
func NewUserService(store UserReader, logger Logger) *UserService {
    return &UserService{store: store, logger: logger}
}

// ❌ Bad — returns interface (hides the concrete type for no reason)
func NewUserService(store UserReader) UserServiceInterface {
    return &UserService{store: store}
}

返回具体类型,让调用方可以完整访问该类型的方法。 只有当函数确实会根据输入返回不同的具体类型时(工厂模式),返回接口才有意义。

4. 在编译时验证接口兼容性

使用空白标识符赋值,及早发现契约失效:

// Verify *PostgresStore implements service.UserReader at compile time
var _ service.UserReader = (*PostgresStore)(nil)

// Verify LogHandler implements http.Handler
var _ http.Handler = (*LogHandler)(nil)

// For value receivers:
var _ fmt.Stringer = Status(0)

将这些语句紧跟在类型声明之后。它们不会产生任何运行时开销,还能防止契约悄然失效。

5. 不要使用接口指针

// ❌ Bad — pointer to interface is almost never correct
func process(r *io.Reader) { ... }

// ✅ Good — interface is already a pointer internally
func process(r io.Reader) { ... }

接口值内部由两个指针组成(类型 + 数据)。 接口的指针就是指向指针的指针,是不必要的间接层。

唯一的例外是需要替换接口值本身时(在运行时切换实现),但这种情况极为罕见。

6. 空接口

interface{}(或 Go 1.18+ 中的 any)表示你放弃了类型安全。 应谨慎使用:

// ✅ Acceptable — generic container before generics / stdlib compatibility
func Marshal(v any) ([]byte, error)

// ✅ Better (Go 1.18+) — use generics instead of any
func Map[T, U any](slice []T, fn func(T) U) []U { ... }

// ❌ Bad — lazy interface design
func Process(data any) any { ... } // what does this even do?

7. 可选配置不是接口问题

可选的构造函数设置并不是引入接口的理由。 使用函数选项或配置结构体;go-design-patterns 技能负责介绍这两种方式及其权衡。

8. 常见的接口反模式

过早引入接口:
// ❌ Bad — interface defined before second implementation exists
type Processor interface {
    Process(ctx context.Context, data []byte) error
}

type processor struct { ... }  // only one implementation ever

// ✅ Good — use concrete type until you need the abstraction
type Processor struct { ... }
// Add interface when you have 2+ implementations or need testing seam

“不要用接口来设计,而要在实践中发现接口。”—— Rob Pike

接口泛滥:
// ❌ Bad — wrapping every struct in an interface "for testability"
type UserServiceInterface interface { ... }
type OrderServiceInterface interface { ... }
type PaymentServiceInterface interface { ... }
// 50 more interfaces with exactly one implementation each

// ✅ Good — define interfaces where they're consumed
// Each consumer declares only the methods IT needs
封闭接口与枚举:

封闭接口——通过未导出的标记方法,使只有当前包能添加变体——适用于一组携带不同数据、通过类型 switch 消费的封闭变体。如果变体不携带数据,则使用带类型的常量枚举。示例: references/embedding-and-assertions.md。

9. 嵌入与类型断言

  • 嵌入会提升整个方法集。在导出的结构体中嵌入 sync.Mutex 会公开 Lock/Unlock;除非外层类型确实是扩展后的内层类型,否则应使用命名字段。
  • 嵌入到结构体中的接口会通过调用时触发 panic 的方法满足该接口——用于测试替身没有问题,但在其他场景中会造成隐蔽的运行时故障。
  • 对可选能力断言为接口(w.(http.Flusher)),并且始终使用逗号 ok 形式。
  • 泛型方法(Go 1.27+)永远无法满足接口方法;将你定义为接口的任何类型中的方法都保持为非泛型。

决策清单

  1. 这里需要接口吗? — 只有在有 2 个或更多实现、需要测试接缝,或跨越包边界时才需要。
  2. 应该在哪里定义? — 在使用方,而非提供方。
  3. 应该有多少个方法? — 越少越好。理想数量是 1~3 个。
  4. 我是在返回接口吗? — 通常不应该。返回具体类型。
  5. 我是否已验证符合要求? — var _ Interface = (*Type)(nil)
  6. 我是否为了方便而嵌入? — 使用具名字段,除非所有提升的方法都属于外层类型。

评论

支持 Markdown