工厂函数应返回接口类型以实现多态,如shape而非*circle;接口定义要窄,参数用常量避免反射,禁副作用,用functional option模式支持字段覆盖与可重入。

工厂函数返回接口类型才能实现多态
Go 没有继承,多态只能靠接口落地。工厂函数如果返回具体类型(比如 *Circle),调用方就必须知道底层结构体,一旦新增 Square 就得改所有调用点——这等于没解耦。
正确做法是让工厂函数返回接口类型,例如 Shape,而所有实现该接口的结构体(Circle、Rectangle)各自提供满足接口的方法。这样业务代码只依赖 Shape.Area(),不关心是谁算的。
- 接口定义要窄:只放真正共用的方法,比如日志器只需
Log(string),别加Flush()或SetLevel()这类非通用行为 - 工厂函数参数建议用字符串或
int常量(如const TypeCircle = 1),避免传入结构体名或反射,否则破坏静态类型安全 - 别在工厂里做副作用操作(比如注册全局 logger 实例),除非明确需要单例语义;否则测试时无法隔离
测试数据工厂必须支持可重入与字段覆盖
写单元测试时直接 User{ID: 1, Name: "test"} 看似省事,但时间字段、UUID、递增 ID 会污染测试状态,导致用例间相互干扰。工厂函数的核心不是“封装 new”,而是“收口变化”。
推荐用 functional option 模式:定义 type UserOption func(*User),再提供 WithID()、WithName() 等选项函数,最后在工厂内部按顺序应用。
- 每个
UserOption只负责赋值,不 new 新对象,也不读写外部变量(如全局计数器) - 工厂函数签名应为
func User(opts ...UserOption) *User,支持零个或多个选项,调用方决定覆盖哪些字段 - 嵌套结构(如
Order包含User)不能在工厂内部互相调用,否则易触发无限递归;应通过WithUser(u *User)显式注入
基准测试必须用 b.N 控制循环,不能手写固定次数
想测工厂函数性能?别写 for i := 0; i —— 这会让 <code>go test -bench 失效,结果不可比、不复现。
基准测试函数签名必须是 func BenchmarkNewShape(b *testing.B),且循环必须用 for i := 0; i 。框架会自动调整 <code>b.N 使总耗时 ≥ 1 秒,从而得出稳定可靠的 ns/op。
- 如果工厂涉及 I/O 或网络初始化(比如从配置加载默认值),记得加
b.ReportAllocs()和运行时加-benchmem查内存分配 - 避免在循环内做非被测逻辑(如构造测试输入),应提前提前准备好,或用
b.ResetTimer()排除初始化开销 - 不同工厂变体(如字符串参数 vs 枚举参数)要分开写
BenchmarkNewShapeWithString和BenchmarkNewShapeWithConst,便于横向对比
工厂函数的性能瓶颈常藏在接口转换和初始化逻辑里
表面上看 NewShape("circle") 就是一次结构体字面量构造,但实际可能暗藏两处开销:一是返回接口时的隐式转换(尤其当结构体未直接实现接口,而是靠指针接收者实现时),二是初始化过程中的字段计算或依赖注入。
比如 FileLogger 构造时打开文件句柄,或 DBClient 工厂里解析 DSN 字符串并校验格式——这些都不是零成本操作,但容易被忽略。
- 用
go test -bench=. -benchmem -cpuprofile=cpu.out抓热点,重点关注runtime.convT2I(接口转换)和工厂函数内部的字符串处理、正则匹配等 - 如果工厂函数只是简单返回已初始化好的单例(如
var defaultLogger Logger = NewConsoleLogger()),那它本身几乎没开销,瓶颈其实在后续方法调用上 - 别为了“看起来像工厂”而强行封装;无差异化逻辑的构造(如
func NewPoint(x, y float64) *Point)直接暴露更清晰
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











