关于 go-interface-design
面向 Go 开发者和编码 Agent 的接口设计技能,介绍接口的定义位置、规模控制及常见设计陷阱。
主要功能
- 01在使用方定义接口
- 02保持接口小巧且可组合
- 03接收接口并返回具体类型
- 04在编译期验证接口实现
- 05避免使用接口指针
- 06识别嵌入与类型断言陷阱
适用场景
- 01依据使用方需求设计包边界
- 02添加测试替身而不扩大接口
- 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+)永远无法满足接口方法;将你定义为接口的任何类型中的方法都保持为非泛型。
决策清单
- 这里需要接口吗? — 只有在有 2 个或更多实现、需要测试接缝,或跨越包边界时才需要。
- 应该在哪里定义? — 在使用方,而非提供方。
- 应该有多少个方法? — 越少越好。理想数量是 1~3 个。
- 我是在返回接口吗? — 通常不应该。返回具体类型。
- 我是否已验证符合要求? —
var _ Interface = (*Type)(nil) - 我是否为了方便而嵌入? — 使用具名字段,除非所有提升的方法都属于外层类型。