Go中适配器模式通过组合和接口隐式实现:定义新类型嵌入旧类型,实现目标接口方法并转发调用;函数适配器适用于无状态简单转换;需注意空指针、接收者类型匹配及循环依赖问题。

适配器模式在 Go 里不是靠继承,而是靠组合和接口隐式实现
Go 没有类继承,所以传统 OOP 中的「适配器继承已有类并实现新接口」行不通。Go 的适配器本质是:写一个新类型,内部持有旧类型实例,再通过方法转发(wrapping)把旧行为“翻译”成新接口要求的签名。关键在于——interface 是隐式满足的,只要新类型实现了目标接口所有方法,它就自动是该接口的实例。
用结构体嵌入 + 方法转发实现接口适配
最常用、最清晰的方式:定义一个适配器结构体,字段嵌入被适配对象,然后为该结构体实现目标接口的方法,在方法体内调用嵌入字段的对应方法(必要时做参数/返回值转换)。
type LegacyLogger struct{}
func (l *LegacyLogger) LogMessage(msg string) {
fmt.Println("[LEGACY] " + msg)
}
type Logger interface {
Print(msg string)
}
type LoggerAdapter struct {
*LegacyLogger // 嵌入,复用原有方法
}
func (a *LoggerAdapter) Print(msg string) {
a.LogMessage("ADAPTED: " + msg) // 转换调用逻辑
}
使用时:var l Logger = &LoggerAdapter{&LegacyLogger{}} 即可,l.Print("hello") 会走适配逻辑。
- 嵌入
*LegacyLogger后,LoggerAdapter自动获得LogMessage方法,但不暴露给Logger接口使用者 - 适配器方法中可任意修改输入输出,比如加前缀、拆包、合并字段、错误映射等
- 避免直接嵌入原始类型(如
LegacyLogger),否则其方法会泄露到外部,破坏封装
函数适配器:适合轻量级、无状态的接口转换
当目标接口只有一个方法,且适配逻辑简单(比如只改个参数名或格式),用函数类型包装更简洁。Go 的函数是一等公民,可以直接实现接口(只要它有方法集)。
立即学习“go语言免费学习笔记(深入)”;
type Writer interface {
Write([]byte) (int, error)
}
func NewWriterAdapter(writeFunc func(string) error) Writer {
return &writerFuncAdapter{writeFunc}
}
type writerFuncAdapter struct {
write func(string) error
}
func (w *writerFuncAdapter) Write(p []byte) (int, error) {
err := w.write(string(p))
if err != nil {
return 0, err
}
return len(p), nil
}
这种写法省去结构体定义,适合胶水层快速对接第三方回调函数或旧日志库的 func(string) 类型。
- 注意:函数适配器无法保存状态,若需缓存、计数、配置等,必须回到结构体方式
- 返回的
Writer实例是匿名结构体指针,调用方无法对其做类型断言回原函数,安全性更高
容易踩的坑:空指针、方法集丢失、循环依赖
适配器代码看似简单,但几个细节错一点就 panic 或静默失效:
-
nil嵌入字段未判空:如果LoggerAdapter字段是*LegacyLogger,但初始化传了nil,调用a.LogMessage就 panic —— 必须在适配方法里加if a.LegacyLogger == nil { ... } - 值接收者 vs 指针接收者:若
LegacyLogger.LogMessage是值接收者,嵌入LegacyLogger(非指针)才可用;但若它是指针接收者,就必须嵌入*LegacyLogger,否则编译报错 “cannot call pointer method on …” - 循环 import:适配器常位于独立包(如
adapters),若它 import 了业务接口包,而业务包又 import 了适配器,就会循环依赖 —— 解法是把目标接口定义在公共基础包,或让适配器只依赖最小契约(如只 importio.Writer而非具体业务接口)
适配器真正的难点不在语法,而在厘清谁是“旧”,谁是“新”,以及哪一层该承担转换责任——多一层包装就多一层维护成本,别为了模式而模式。










