为什么 golang 提倡 「接收接口,返回结构体」这样的原则呢?

175 天前
 shinelamla

看到很多 go 代码在构造对象的时候,Newxxx()的时候,都喜欢接收接口,然后返回结构体,查阅了一些资料,始终无法理解这一操作的精髓,所以想问问大家,对这个 go 惯例的理解是怎么样的,希望得到一些指点

8438 次点击
所在节点    程序员
84 条回复
xguanren
174 天前
@cenbiq 是的 差不多 你自定义的结构体 B 不管他有别的方法.比如 C()D()
和 C 结构体 有 M()B() 各种方法
但是只要满足了接口 A 的 get()set() 这两个接口.
那我这个函数 NewStruct(a A) 就可以用做这个 A 接口来当做参数类型,我只需要去保证我传参进去的不管是 A 还是 B 还是 C.
满足了 get 和 set 这两个方法.那么我就直接通过参数 a 去调用.返回一个我自己的结构体.
leonshaw
174 天前
@kuanat 没有预定义的情况下,两个独立的包不约而同地实现了一组相同签名的方法,从而能让调用者用自己定义的 interface 引用—— 这种情况我从来没遇到过。
如果一个包在开发时没有想到要实现某种通用接口,那它方法的签名大概率会包含某个具体类型(更不要说五花八门的命名)。例如你举的 A.Get 很可能是这样:
func (a *A) Get() *AObject {}
这个签名几乎不可能在 B 包里复用,最后不得不加一层适配。
xguanren
174 天前
就比如我有一个签到程序.或者爬虫程序.
我写了一个任务池.可以抽象为 3 大功能
登陆()
查询()
签到()
我把这个任务池 我内部实现的毕竟完美.我可以定时.判断是否签到成功.种种的实现我感觉比较好.这个时候我发给你.你需要 new 进去一个你自己的签到任务.
登陆.查询.签到 这三个就可以视为接口的三个方法
这样具体的三个方法的实现.我不去操心.因为每个人的网站不同.你可能是签到 v2ee.你的登陆接口可能是直接返回一个 cookie.可能签到梯子网站.你是账号密码直接登陆.然后返回 cookie.
这样我的任务池.我只需要去执行你传参进来的任务.我就可以去签到了.
aababc
174 天前
@kuanat #55 这里怎么感觉有点矛盾,这里如果互相不负责,那么怎么确定实现实现了接口。可以去掉 implement 关键字,但是还是要实现同一个接口。golang 采用的是 标准库的方式提供了一个接口,php 现在的做法是针对通用组件在 PSR 提供了接口,不同的组件可以基于相同的一个接口了互通。 个人感觉工程上没有办法做了 接口和实现完全独立。
xguanren
174 天前
@xguanren 至于我说什么内部实现毕竟完美..只是举个例子..我的意思是对于开发者来说.只需要去操心我的登陆是返回 cookie 还是账号密码登陆.我的查询是 get 还是 post.我的签到是日签还是几个小时一次.(如果是需要不同定时的话.还需要一个 Interval() time.Duration 接口.来返回定时)
这样你只需要满足这三个方法.你可以传递进来.这样我的任务池到最后.只需要返回一个结构体.
你调用的时候直接 Task.start().
这样具体的实现部分.就是交给我了我通过你的登陆.拿到 cookie.我就可以不用关心 cookie 是怎么来的.然后如果是定时类的话 我可以通过 Interval()拿到时间.比如说是 time.Hour * 24 一天.time.Minute * 10 十分钟一次.我就可以通过这个去循环了.剩下的查询用户信息.你是希望返回银币铜币.还是返回余额.剩余流量.这个你自己实现.我只需要去负责循环.签到.即可.剩下的比如.断网.签到失败推送.签到成功推送.然后多协程并发处理任务而不是轮询.种种这种小功能吧.你就不用操心了..
不过有人可能说那我直接定义一个结构体.包含这三个方法.不是也可以吗.但是这样的话.这个类的成员.方法.不就被局限死了吗.我如果用结构的话.那我可以给我的比如 v2ee 签到.我可以给他封装进去个滑块识别.或者其他各种方法.
唔 这是我自己的一点认知.不知道对不对
sagaxu
174 天前
@kuanat

“Java 里面接口的实现和定义总是在一起的,或者说总是由同一个代码所有者完成的”

Java 的世界里,很多接口是权威定义的,如 JDK 和 J2EE 。JDBC 和 JPA 都是 J2EE 定义的接口,实现则由不同厂商提供。JDK 中定义的数据结构如 List ,Map ,Set 等也都是接口,API 签名中一般也用接口类型,像 Hibernate 中的 List 就有支持 lazyload 的特定实现版。slf4j 的实现,有 logback 也有 log4j2 。

在这里,Go 比 Java 省事的例子也有,比如 AutoCloseable ,在 Java 中,即使某个类有 close()方法,也不能隐式的转换成 AutoCloseable ,需要自己写 wrapper 。使用 try-with-resources 的时候,就很不方便了。但是隐式转换也有利弊,好处是方便,弊端是这个 close()未必适用于 try-with-resources 场景,使用者可能要经过仔细研究才知道是不是合适。
kuanat
174 天前
@shinelamla #44

你说的不啰嗦就是我也感同身受,随便举几个例子。

从读的方面说,项目选型的时候有多个开源库备选,选哪个总要花很久调研。Go 在做同样的事情的时候,再复杂的项目,很快就能梳理清楚架构,了解代码质量。

我也是 Java 过来的人,写 Java 的时候我很讨厌写测试。原因是项目依赖很多都是非接口化的,真正用的时候要自己再封装一层。没有接口化的代码是很难做 mock 测试的,所以有很多测试框架使用了运行时动态生成 mock 代码的方式来解决这个问题,但是我内心还是不情愿写。

接触 Go 之后我反倒非常习惯写测试,不论依赖质量高低如何,mock 就是接口套一下的事情,代码很少。很早之前标准库想要提供 mock 的,后来废弃了,原因就是 mock 这个事情其实用不到再搞个库。还有个意外的副作用是甩锅的时候很有底气,接口内侧是我负责,外侧该找谁找谁。

所以我体会到最重要的事情是,只有机制上足够简洁便利,大家才会愿意用主动用,人性使然。大多数时间我并不想辩论“XXX 也可以”这种能不能的问题,大家都是图灵完备的换个表达方式而已,但是好不好用愿不愿意用才更重要。
iosyyy
174 天前
@aababc #64 本来就没法做..无论是 sdk 还是接口本身都是一种约束完全不约束本身就是不可控和没意义的.
kuanat
174 天前
@leonshaw #62

经过这么一整个帖子的讨论,我越来越意识到之前的举例不恰当。

不论是 Go 还是 Java 都需要适配,区别更多是在难易程度上。

我设想了一个新例子,比如我一个已经存在的项目,实现了批量上传功能,调用方法入参是个包含 batchUpload() 方法的接口。

如果需要增加 A 作为云服务后端,而 A 的 sdk 只有单文件 put 功能,那么我适配的时候可以直接 func (a *A) batchUpload() { ... } 然后调用 a.put() 完成实现。

也就是说 Go 支持给我并没有所有权的代码里的结构体添加新的方法。换到 Java 里不能修改 A 的实现,就需要子类实现接口过渡一下。

在 Go 里 A 永远是那个 A ,而 Java 里子类和父类就要额外考虑类型兼容的问题。如果再有下游项目引用了我的包,或者需要 mock 一下做测试,Go 都是肉眼可见比 Java 简单很多。
kuanat
174 天前
@sagaxu #66

我在这个帖子反复讨论中突然意识到一个问题,就是 Go 的接口其实并不是等价于 Java 中的接口的。在 Go 实现泛型之前,Go 的接口承担了很大的抽象作用,而这个问题在 Java 中并不存在。

我在构思文章的时候一直很纠结,总感觉说不到重点上。现在看我更应该回答的问题是,Go 这样的设计到底带来了哪些实质的好处,而不是执着于辩论这个设计是否先进。
kuanat
174 天前
@aababc #64

没办法确定“实现”了接口。

在 Java 这种 strongly typed 语言中,这个判定过程发生在编译时,implements 就是告诉编译器做这个验证工作的。在 Go 这种 weakly typed 语言中,这个判定被推迟到运行时,如果没能真正实现,调用的那一刻会产生运行时错误。

于是 Java 的思维模型就是要先说清楚,即库和包的作者主动声明并接口化。而 Go 的思维模型是用到的时候再说,即调用方来定义到底需要什么接口(我定义的我自己当然知道谁实现了谁没实现)。

我前面举的例子可能不是特别恰当,但是由于 Go 的接口声明在调用方,而实现在上游的包和库,这个隔离或者独立已经是非常大的进步了。从各种开源项目看,引用上游依赖几乎是毫无副作用的事情。
xywanghb
174 天前
@kuanat 感觉长脑子了, 我如下理解大佬看看对不对
如果我是提供方
1. go, 我只需要关注我能提供什么能力, 专注于做好自己的东西, 你消费方爱咋用咋用
2. java, 我提供一个功能这个东西以后可能会怎么演变, 我希望提供一个尽可能考虑很多场景的功能或者标准, 更丰富的易用性和扩展性, 并且考虑做大做强以后其他类似厂商都参考我这个标准来实现, 有点像市面上很多厂商提供的功能慢慢都是一些大而全的东西, 占领市场建立标准
如果我是消费方
1. go, 我就像一个海淘客, 找到各种各样的功能提供方, 自己非常灵活的进行组合调用, 自由度更强
2. java, 我就像一个傻子, 更多的是学习标准, 然后找到标准中占有率更多更成熟的, 或者说提供的功能更丰富的, 因为我也不知道以后会扩展成啥样, 所以我现在可以不用, 但是你提供的内容多, 也降低了我以后因为你无法支持造成的改造成本大的隐患, 比较突出的例子就是用 springboot, 很多三方库的实现我不关心, 只需要看 springboot 的一些标准, 具体实现只用依赖进来即可
kuanat
174 天前
@xywanghb #72

我也是到了 70 楼的回复才意识到关键所在,你说的就是我想表达的。

Java 的接口和 Go 的接口只是有一样的名字,实际上作用完全不一样,根本不能拿来类比的。Java 的接口是用来解决多重继承问题的,而 Go 天然基于组合而非继承,接口的能力和责任范围都更大。

Java 的思维模型里,抽象(动词)设计这个行为越早越好,而且机制上鼓励你尽可能考虑易用性和扩展性,原因是后期做调整很麻烦。这让我想起了上学的时候,万物皆对象,想把整个宇宙都用对象和类描述出来。这个思路导致了 Java 在工程方面是有过度设计和复杂化倾向的,现实里 java 团队往往也比较大。

Go 的思维模型里,越简单越好,不需要考虑额外的东西。责任划分非常清晰,抽象这个行为局限在非常小的业务层面。

这中间的区别我认为可以上升到哲学层面,就是我开头提到的汉语和其他语言的区别。汉语是建立在组合的哲学上的,把全宇宙所有具象、抽象的概念都解构归纳成最基础的元素,大概只有几千个汉字。任何人学会这几千个字,就可以尝试自行描述整个世界。

换到其他语言,简单举例几个,化学、医学和植物学,每个都有自己无限衍生的词汇表,在一个领域的词汇积累是无法平移到另一个领域的(多继承失败)。

从这个意义上说,我认为以 Go/Rust 等等现代语言就是先进生产力的代表,减轻了开发者的心智负担,也就解放了生产力。
shinelamla
174 天前
@kuanat 看下我最新的 append ,这样的代码组织虽然能通,但是否是好的?
Leviathann
174 天前
@kuanat rust trait 也是 nominal 的,只是 trait 是类似 scala 的那种 typeclass
sagaxu
174 天前
@xywanghb #72

你这总结过于极端了,实际上不同语言思考过程没有那么大的差异。“接收接口,返回结构体”本身就是思考了扩展和演变的结论,否则为何要额外定义一个接口呢?直接“接收结构体,返回结构体”代码量更低。

“接收接口,返回结构体”是一种跟语言无关的模式,Java 也经常这么干。
Rehtt
173 天前
loolac
173 天前
规范类的原则看看就行,你可以理解为是为了方便你阅读代码的。不同的项目可以不同,只要不是语法上禁止或警告的没必要深究。
chonh
173 天前
感谢楼主贴的代码,基本认同#14 楼的。
1. 主要是隐藏实现。小写的 service struct 是具体实现不会对外暴露。
2. Service interface 一看就知道提供了哪些功能,方便别人使用。而 struct 可能实现多个接口,会显示“多余”的方法。
3. interface 好扩展。将实现了 Y interface 的 B struct 嵌套在 A struct 里,A 自动实现 Y 。但 A 并不会成为 B 。
4. 如果返回 interface ,那只能 return struct (或 func )。因为 interface 不能实现 interface 。
5. 如果你的 struct 只是充当 data model ,一般会直接写 struct literal ,不需要额外加个 NewXXX 方法。
6. 这跟继承不继承没关系。
kuanat
169 天前
@shinelamla #74

回复比较晚……单就引文那个代码来看,我觉得没有必要写接口,因为还没用到。直接写成 func (c *Conn) ListPosts() []*Post { ... } 就行,Conn 可以 embed 一个 grpc.ClientConn 这样。

等我写完文章吧,这个话题确实不太容易说清楚。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1040434

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX