惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
J
Java Code Geeks
L
LangChain Blog
V
V2EX
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
博客园 - Franky
Microsoft Azure Blog
Microsoft Azure Blog
Jina AI
Jina AI
Blog — PlanetScale
Blog — PlanetScale
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Cloudflare Blog
博客园 - 司徒正美
B
Blog
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog

咸糖 - 自律者自由

2025 年终总结:降噪、重构与长期主义 在新加坡和新山吃过最好的食物(持续更新) 写给还在迷茫的你:我的三本大学回忆 2024 年终总结 Neovim: No Crash Incremental Selection 2022 年终总结 使用 neovim 作为 PDE(个性化开发环境) shell 是一个不错的生产力工具 使用二八法则省力地学习 awk 肉身翻墙新加坡安顿指南 使用 Docker Compose 建立你自己的开发环境 关于编写可维护的代码的一些实践与想法 我为什么使用双向链接做笔记? 关于焦虑和拖延症 使用番茄工作法来更好的利用你的时间 Unix 如何杀死一个进程和它的子孙进程? Golang: 让你的零值更有用 使用 Mock 和 Interface 进行 Golang 单测 关于 Golang Slice 的一些细节 总结一些计算机常用的原则 重新学习英语语法 上班族近期小半年入门投资基金组合的学习与实践经历 疫情期间的肉身翻墙新加坡指南 About me 软技能:大厂底层员工打工指南 软技能:我是如何获取知识与信息的? 分布式的令牌桶算法的实现 实现一个AtomicInteger GC root 在哪里? 什么是 Minor GC/Major GC
Golang: 如何处理日渐膨胀的 interface
xiantang · 2022-02-13 · via 咸糖 - 自律者自由

文章目录

【注意】最后更新于 December 29, 2025,文中内容可能已过时,请谨慎使用。

The bigger the interface,the weaker the abstraction。Go Proverbs

先说结论吧,如果你的 Golang interface 有太多函数导致你很难横向拓展,那就把它按照职责拆分成多个 interface,然后使用 embed 组合起来。

遇到的问题

最近在重构一个管理配置的组件,我们会有一个接口并且有超过 5 个以上的 struct 实现这个接口,遇到了一些问题,就是接口膨胀的问题。当你最开始抽象出来一个 interface 的时候,它可能就只有 1 - 2 个函数,很漂亮,并且你的横向拓展的时候也非常的舒服。 但是很多时候现实的情况和你想象的不一样,因为每个实现可能也有自己想暴露出来的不同方法,这个时候你会把这些函数都暴露出来,每个实现为了满足接口都会去实现这个函数,这样就会导致接口膨胀。

举个例子

最开始我有一个接口叫做 ConfigManager,拥有两个函数一个是 HandleResync,一个是 HandleWatch。:

1
2
3
4
type ConfigManager interface {
 HandleResync()
 HandleWatch()
}

HandleResyncHandleWatch 都是对于 etcd 数据的拉取,拉取到数据之后,我们会将数据落盘,并且在内存里面也存储一份最新的副本。

同时这个接口是有两个实现类,一个是 EtcdConfigManager,一个是 FileConfigManager。这里名字比较随意,主要是要表达一个接口的多个实现。

1
2
3
4
5
6
7
8
type EtcdConfigManager struct {
 config *Config
}

type FileConfigManager struct {
 config *Config
}

但是应为业务不断增长,我们对于 EtcdConfigManager 需要多暴露出来两个接口用来做对内存中副本上报等行为。分别是 GetConfigSetConfig

但是对于 FileConfigManager,这个实现类其实是不需要暴露出内部的 config 的。

这个时候很多同学都会直接添加两个函数到 ConfigManager 接口,同时 FileConfigManager 会变成一个下边的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type ConfigManager interface {
  HandleResync()
  HandleWatch()
  GetConfig() *Config
  SetConfig(*Config)
}

// impl for `ConfigManager`
type FileConfigManager struct {
   config *Config
}
...
func (f *FileConfigManager) GetConfig() *Config {
 // painc here
 panic("implement me")
}

func (f *FileConfigManager) SetConfig(c *Config) {
 // painc here
 panic("implement me")
}

虽然成功了解决了问题,但是会导致一些问题:

  • 如果有一个新的配置需要管理,那么他将需要满足 4 个函数,来实现这个接口。
  • 如果已经有了 10 个实现类实现了 ConfigManager,如果要添加一个新的函数到 ConfigManager 接口,那就需要这 10 个实现类都要更新,会有很多工作量。
  • 对外部暴露太多内部的信息,如果涉及权限问题会导致资损等问题。

其实在公司的 codebase 里面已经出现了 10+ 个函数的 interface,横向拓展简直是一个噩梦。

我是如何解决的

因为受够了接口膨胀的问题,我可真算是绞尽脑汁,虽然 Golang 作者总是说要把接口控制小,但是其实并没告诉我们如何去防止接口膨胀。终于我在 Golang 的 io 包中找到了一个解决方案。

io.go#83io.go#172 定义的接口中。

拆分接口

我们发现他按照职能拆分出了一堆只有 1 - 2 个函数小接口。 like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Reader interface {
 Read(p []byte) (n int, err error)
}
type Writer interface {
 Write(p []byte) (n int, err error)
}
type Closer interface {
 Close() error
}
type Seeker interface {
 Seek(offset int64, whence int) (int64, error)
}

拼接接口

采用增量拼接的方式将这些接口拼接起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Writer interface {
 Write(p []byte) (n int, err error)
}
type WriteSeeker interface {
 Writer
 Seeker
}
type ReadWriteSeeker interface {
 Reader
 Writer
 Seeker
}

转换接口

同时在传参的时候,只传递较小的接口,然后根据需要去转换到其他接口。

1
2
3
4
5
6
func WriteString(w Writer, s string) (n int, err error) {
 if sw, ok := w.(StringWriter); ok {
  return sw.WriteString(s)
 }
 return w.Write([]byte(s))
}

好处

这样好处是

  • 尽量暴露少的信息给用户:对于所有的实现类我们只需要实现必须实现的函数,不需要去满足上面接口的所有函数。
  • 抽象能力更强:因为你的接口只有 1 - 2 个函数所以横向拓展更加的简单,这也是为什么 io.Writer 接口能轻轻松松写出 20+ 个实现类的原因。

重构上面的代码

对于上面的代码,我们可以重构一下:

我们可以把拥有 4 个接口的 ConfigManager 重构成两个小接口,然后进行组合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
type ConfigManager interface {
  HandleResync()
  HandleWatch()
  GetConfig() *Config
  SetConfig(*Config)
}
*/

type EventHandler interface {
   HandleResync()
   HandleWatch()
}

type DataHolder interface {
    GetConfig() *Config
    SetConfig(*Config)
}

type DataEventHandler struct {
     DataHolder
     EventHandler
}

这样 FileConfigManager 就可以只实现 EventHandler 接口了,减少了暴露的信息,并且可以不实现那些不想暴露的函数。

对于主逻辑其实也会比较简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func mainLoop(handler EventHandler) {
    // just example

    // resync
    handler.HandleResync()

    // watch
    handler.HandleWatch()

    // ...
    // report data
    if h,ok := handler.(DataEventHandler); ok {
     report(h.GetConfig())
    }
}

结论

总结一下,有下面几点我自己的最佳实践:

推荐环节

最后最后和大家分享一些最近在看的好文,想过用周刊的方式发送但是因为看的比较零散,就放在每篇博文的最后,希望大家能够收获!

文章作者 xiantang

上次更新 2025-12-29 (4c152d04)

赞赏支持

微信打赏 支付宝打赏