Go中适配器应保持原接口签名、无状态、只做协议翻译,统一错误映射,复用而非新建依赖实例,且生命周期须与被包装对象严格对齐。

Go 里怎么写一个不破坏原有接口的适配器
适配器不是为了“加功能”,而是为了让新旧两套东西能咬合上。比如你用的第三方库升级了,DoRequest 函数签名从 func(string) error 变成 func(context.Context, string) error,但你的业务代码还在满世界传 DoRequest("xxx") —— 这时候就得在中间塞个适配器,把老调用转成新调用。
关键不是“写个 wrapper”,而是让老代码完全无感。所以适配器函数名最好和原函数一致(比如仍叫 DoRequest),类型也保持一致(返回 error,不暴露 context):
func DoRequest(url string) error {
return doRequestWithContext(context.Background(), url)
}
注意:别在这里做 context 超时控制,那是调用方该决定的事;适配器只负责“翻译”,不越权。
- 如果原接口有多个方法(比如一个
Client接口),就封装整个 struct,字段保留原 client 实例,方法逐个转发 - 别在适配器里初始化新资源(如重开 HTTP client),复用上游已有的实例
- 如果第三方库用了泛型,而你的老代码是具体类型,适配器里要做显式类型断言,别依赖空接口
第三方库 API 差异大,怎么避免自己代码被绑死
不同库对同一类能力(比如 HTTP 请求、JSON 解析、日志输出)的抽象方式天差地别。硬写 if lib == "fasthttp" { ... } else if lib == "net/http" { ... } 是自找麻烦。
立即学习“go语言免费学习笔记(深入)”;
正确做法是定义你自己的最小接口,只暴露业务真正需要的行为:
type HTTPDoer interface {
Do(*http.Request) (*http.Response, error)
}
然后为每个第三方库写一个薄薄的实现:
-
fasthttpClient实现HTTPDoer:把*http.Request转成fasthttp.Request,再调Do -
netHTTPClient实现HTTPDoer:直接透传,可能只包一层 - 所有业务逻辑只依赖
HTTPDoer,不 import 任何第三方 http 库
这样换库时,只需改一行注入代码(比如从 newNetHTTPClient() 换成 newFastHTTPClient()),其余零改动。
适配器里要不要处理错误映射
要,而且必须做。第三方库抛的错误类型五花八门:fasthttp.ErrTimeout、net/http.ErrServerClosed、json.SyntaxError……你的业务层不该也不必知道这些细节。
适配器应该统一转成你定义的错误类型,比如:
- 网络失败 →
ErrNetwork(带原始 error 作为 cause) - 解析失败 →
ErrParse - 认证失败 →
ErrAuth
别用字符串匹配判断错误类型(比如 strings.Contains(err.Error(), "timeout")),脆弱又慢;优先用 errors.Is 或类型断言:
if errors.Is(err, fasthttp.ErrTimeout) {
return fmt.Errorf("%w: %v", ErrNetwork, err)
}
否则上线后某次库升级改了错误信息文案,你的“字符串匹配”就 silently 失效。
为什么适配器不能有状态或缓存
因为调用方默认认为它是纯函数/无副作用对象。一旦你在适配器里加了 map 缓存响应、或者用 sync.Once 延迟初始化,就会埋下并发 bug 和内存泄漏隐患。
比如这个常见错误:
var once sync.Once
var cachedClient *fasthttp.Client
func GetClient() *fasthttp.Client {
once.Do(func() {
cachedClient = &fasthttp.Client{...}
})
return cachedClient
}
表面看没问题,但如果你的适配器被注入到多个 service 中,它们共享这个 client,而 client 的 timeout / maxIdleConns 等配置却只按第一个 service 的需求设了——后面的服务就可能被拖慢甚至超时。
- 适配器本身应该是无状态的,所有可变配置(超时、重试、限流)由调用方传入
- 如果真需要复用底层资源(如 HTTP client),把它作为依赖注入进来,而不是在适配器里 new
- 缓存逻辑放在调用方或独立的 cache layer,别混进适配器职责
最常被忽略的一点:适配器的生命周期必须和它所包装的第三方实例严格对齐。你 new 了一个 redis.Client,又写了个 RedisAdapter 包着它——那 RedisAdapter.Close() 就得调 redis.Client.Close(),漏掉这句,连接就永远不释放。










