Go中适配器模式通过组合+接口转换实现:用struct嵌入被适配对象,实现目标接口并委托调用,必要时做语义转换;不依赖继承,关键在于满足目标接口方法集。

适配器模式在 Go 中没有接口继承,怎么写
Go 语言不支持传统面向对象中的接口继承或类继承,所以「适配器模式」不能靠「让新类型继承旧接口再扩展」来实现。它依赖的是组合 + 接口转换:把一个已有类型嵌入新结构体中,用新结构体实现目标接口,内部委托调用原类型方法(必要时做参数/返回值转换)。
关键不是「继承接口」,而是「满足目标接口」——只要新类型实现了所有目标接口的方法,它就是该接口的实例。
- 适配器本质是一个
struct,字段里持有被适配对象(可能是第三方库类型、遗留代码类型等) - 适配器为这个字段实现你当前系统需要的接口(
Target),方法体内调用原对象方法,可能加一层包装逻辑 - 如果原类型已实现部分目标方法,可直接转发;若签名不匹配(比如参数多一个 context,或返回值多一个 error),就必须在适配器方法里处理
典型场景:把 io.Reader 适配成自定义的 DataSource 接口
假设你有一个老系统只认 DataSource 接口:
type DataSource interface {
ReadData() ([]byte, error)
}
但你手头只有 *os.File 或 bytes.Reader,它们只实现了 io.Reader 的 Read(p []byte) (n int, err error)。这时就需要适配器:
立即学习“go语言免费学习笔记(深入)”;
type ReaderAdapter struct {
r io.Reader
}
func (a *ReaderAdapter) ReadData() ([]byte, error) {
buf := make([]byte, 4096)
n, err := a.r.Read(buf)
if err == io.EOF {
return buf[:n], nil
}
if err != nil {
return nil, err
}
return buf[:n], nil
}
注意这里做了三件事:分配缓冲区、调用 Read、处理 io.EOF(把它转成成功返回空数据,符合 ReadData 的语义)。这就是适配器的核心工作:桥接语义差异。
为什么不用函数适配?什么时候必须用结构体
简单转换确实可用函数实现,比如:
func ReaderToDataSource(r io.Reader) DataSource {
return &ReaderAdapter{r: r}
}
但真正需要结构体适配器的情况包括:
- 适配逻辑需要保存状态(比如缓存上次读取位置、维护重试计数)
- 要同时适配多个接口(一个结构体可实现多个接口)
- 被适配对象本身需要生命周期管理(如打开/关闭文件),适配器可封装
Close()并同步控制资源 - 第三方类型无法直接赋值给接口变量(比如它没导出全部方法,或你无权修改其源码)
例如,若 io.Reader 来源是网络连接,你希望在 ReadData() 失败时自动重连,那状态(连接句柄、重试次数)就必须存在结构体字段里。
容易踩的坑:nil 指针、方法集混淆、错误传播不一致
常见问题集中在三处:
-
ReaderAdapter方法接收者用值类型(a ReaderAdapter)而非指针(a *ReaderAdapter),导致内部字段a.r无法修改(比如想记录读取偏移),且可能引发 panic(当r是 nil 时调用Read) - 误以为「只要类型有某个方法就能被接口变量接收」——Go 只看方法集:对指针类型定义的方法,只有
*T满足接口;对值类型定义的方法,T和*T都满足。适配器方法务必和字段访问方式一致 - 错误处理风格不统一:原类型返回
io.EOF,你却在适配器里吞掉它或转成nil,上层调用方无法区分「读完了」和「真的失败了」
最稳妥做法是:适配器字段用指针接收者,所有方法也用指针接收者;错误尽量保留原始语义,必要时用自定义 error 包装并保留 cause。










