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

推荐订阅源

Cloudbric
Cloudbric
Schneier on Security
Schneier on Security
V2EX - 技术
V2EX - 技术
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
O
OpenAI News
S
Security @ Cisco Blogs
Scott Helme
Scott Helme
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
T
Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
Microsoft Azure Blog
Microsoft Azure Blog
Know Your Adversary
Know Your Adversary
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
Forbes - Security
Forbes - Security
NISL@THU
NISL@THU
Security Latest
Security Latest
G
Google Developers Blog
D
Docker
T
Threat Research - Cisco Blogs
N
Netflix TechBlog - Medium
C
CERT Recently Published Vulnerability Notes
H
Help Net Security
B
Blog
Martin Fowler
Martin Fowler
N
News and Events Feed by Topic
Simon Willison's Weblog
Simon Willison's Weblog
Hacker News - Newest:
Hacker News - Newest: "LLM"
L
Lohrmann on Cybersecurity
Y
Y Combinator Blog
PCI Perspectives
PCI Perspectives
F
Fortinet All Blogs
MyScale Blog
MyScale Blog
Project Zero
Project Zero
爱范儿
爱范儿
Cisco Talos Blog
Cisco Talos Blog
博客园 - 聂微东
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
V
Vulnerabilities – Threatpost
P
Proofpoint News Feed
Cyberwarzone
Cyberwarzone
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
N
News | PayPal Newsroom
Recorded Future
Recorded Future

咸糖 - 自律者自由

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 漏桶算法的设计与实现 剑指offer 单例模式 TCP 针对面试学习 Actor 如何处理阻塞消息 Akka 源码解析 How to learn scala AES 需要限制 SEED 长度 Java 如何区分==与.equals()方法 2018年年度总结 Java 集合扩容
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)

赞赏支持

微信打赏 支付宝打赏