go中adapter的核心目的是隔离底层库变更对上层的影响,通过显式字段而非嵌入避免方法泄露、字段冲突;函数适配器适用于单方法无状态场景;泛型可统一多类型转发逻辑;生命周期须与所持对象一致。

Go 里写 adapter 不是为了套设计模式,而是为了不让 main 或业务逻辑层感知底层换了个库——接口一变,所有调用点都得改,adapter 就是那个“不改上层”的缓冲带。
结构体嵌入 vs 显式字段:别让方法意外暴露
嵌入 *LegacyLogger 看似省事,但它的 Close()、SetLevel() 会自动成为适配器的方法,可能破坏目标接口契约。显式字段(如 logger *LegacyLogger)才是生产首选:
- 所有转发逻辑可控,不会多出无关方法
- 避免因嵌入导致的字段名冲突(比如两个嵌入类型都有
id字段) - 语义清晰:你明确知道这个 adapter 只负责协议转换,不是 LegacyLogger 的子类
函数适配器适用场景:单方法接口 + 无状态转发
当目标接口只有一个方法(比如 io.Writer.Write),而你手头是个独立函数(如 sendSMS(string) error),用函数类型更轻量:
type SMSWriter func(string) error
func (w SMSWriter) Write(p []byte) (int, error) {
err := w(string(p))
return len(p), err
}
writer := SMSWriter(sendSMS)
注意:参数类型不匹配时必须手动转,Go 不会隐式把 []byte 当成 string;也别在闭包里存连接或计数器——函数值本身没字段,状态会丢失。
参数/返回值转换:该加前缀就加,该丢错误就丢,但别越界
适配器只做协议对齐,不是业务逻辑中转站。常见越界行为包括:
- 在
Error()方法里自动重试三次(重试是 client 层的事) - 把
Info()日志按级别过滤掉(应由日志配置控制,非 adapter 职责) - 在
ReadLine()适配为Next() map[string]interface{}时,偷偷做 CSV 解析缓存(缓存属于 reader 实现,不是 adapter) - 加时间戳可以,因为它是格式对齐的一部分;但加 traceID 就要三思——如果上层没传,你从哪来?
泛型适配器:避免为每种类型重复写转发逻辑
当你发现写了 JSONReaderAdapter、XMLReaderAdapter、CSVReaderAdapter,且它们都只是把 Read() ([]byte, error) 转成 Next() map[string]interface{},就可以用泛型统一:
type DataReader[T any] interface {
Next() (T, error)
Close() error
}
func NewMapReader[R io.Reader](r R, unmarshal func([]byte) (map[string]interface{}, error)) DataReader[map[string]interface{}] {
return &mapReader[R]{r: r, unmarshal: unmarshal}
}
这种写法把「读取」和「反序列化」解耦,适配器只管桥接,具体怎么解析交给传入的 unmarshal 函数——它比硬编码支持 JSON/XML 更灵活,也更容易测试。
最常被忽略的一点:adapter 的生命周期要和它持有的对象一致。比如你传进一个 *sql.DB,adapter 不能自己 Close() 它;但如果持有 *os.File,又没暴露 Close() 方法,就等于制造资源泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











