问题修复
设计 Go 结构体与接口时:如何用小接口…
在 Go 中设计类型、定义或实现接口、嵌入结构体、写类型断言/类型切换、加序列化标签、选择指针或值接收者时,用一套可复用的设计准则。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
设计 Go 结构体与接口时:如何用小接口通用
解决思路
- 保持接口小:接口应有 1-3 个方法,小接口更易实现、mock 和组合;需要更大契约时由小接口组合而成(如 ReadWriter 嵌入 Reader 和 Writer)。
- 在消费方定义接口,而不是在实现方定义:接口属于消费者,这样消费者掌控契约,也避免为了接口而导入实现包。
- 接受接口、返回结构体:函数参数用接口以获得灵活性,返回值用具体类型以便调用者访问完整字段和方法。
- 不要过早创建接口:先写具体类型,等出现第二个消费者、第二个实现或测试 mock 时再提取接口。
- 让零值可用:设计结构体使其无需显式初始化即可工作,如用惰性初始化保护 nil map,避免构造函数样板和 nil 相关 bug。
- 能用具体类型或泛型时避免 any/interface{}:Go 1.18+ 优先用泛型做类型安全操作,仅在 JSON 解码、反射等真正未知类型的边界使用 any。
- 遵循标准库规范接口签名:如 io.Reader、io.Writer、io.Closer、fmt.Stringer、error、http.Handler、json.Marshaler/Unmarshaler,不要自创 ToString() 或 ReadData()。
- 用编译期接口检查:在类型定义附近写 var _ io.ReadWriter = (*MyBuffer)(nil),零运行时开销,接口不满足时构建立即失败。
- 类型断言必须用 comma-ok 形式(s, ok := val.(string)),单值形式在类型不匹配时会 panic;用类型切换按动态类型分派,用对小可选接口的断言(if f, ok := w.(Flusher); ok)利用更丰富的实现而不扩大声明参数类型。
- 结构体嵌入用于提升内部类型的方法和字段(组合而非继承),被提升方法的接收者是内部类型,外部类型可通过定义同名方法覆盖。
- 嵌入与命名字段的选择:想暴露内部类型的完整 API 时用嵌入("is a" 增强版),只需内部使用依赖时用命名字段("has a")。
- 通过接口做依赖注入:构造函数接受接口依赖,解耦组件并让测试直接传 mock 或 stub,无需真实数据库。
- 序列化结构体的导出字段必须加字段标签:否则编码器回退到 Go 字段名,重命名字段会静默改变线格式;用 json:"-" 排除内部字段。
- 指针与值接收者的选择:方法修改接收者、接收者含 sync.Mutex、接收者是大型结构体时用指针;接收者小且不可变、是基本类型、方法是只读访问器、是 map 或函数值时用值;同一类型的所有方法接收者必须一致。
- 用 noCopy 哨兵防止结构体拷贝:持有 mutex、channel 或内部指针的结构体被拷贝会破坏不变量,嵌入 noCopy 让 go vet 报告每次值拷贝,并通过指针传递这类结构体。
- 用 go vet ./... 诊断 copylocks 报告含锁结构体的值拷贝。
- 避免常见错误:大接口拆分为 1-3 方法接口、不在实现包定义接口、构造函数不返回接口、不用裸类型断言、不需要全部方法时不用嵌入、序列化结构体不漏标签、不混用指针和值接收者、不忘编译期接口检查、不用 ToString() 代替 String()、不做过早接口、零值结构体不用 nil map/slice、类型安全操作不用 any。
适用边界 · 注意事项
- 项目不使用 Go 语言时。
- 只是简单数据结构、没有接口抽象或序列化需求的小脚本,无需套用完整设计准则。
- 团队已有明确且被显式声明优先于本技能的 Go 类型设计规范时。