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

198 天前
 shinelamla

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

8713 次点击
所在节点    程序员
84 条回复
kaf
198 天前
不是 Newxxx()的时候,都喜欢接收接口,然后返回结构体,而是接口类型推荐这样使用
lasuar
198 天前
不是固定模式,如果场景不需要抽象,Newxxx() 返回的是结构体,反之是接口。Newxxx()也是 Go 的工厂函数。
gowk
198 天前
@kuanat #10
看完你的回复挺感慨的,Go 和 Java 都写过,特别能理解你说的这些
所以当许式伟说 Go 开启了现代编程语言的先河的时候
下面跟帖一堆冷嘲热讽的。根本没有完美的语言,每个语言都有长处和短处
说实在的,现在你不掌握个几门语言,出门都不好意思打招呼
不要限定自己是 Java 程序员、Go 程序员、JavaScript 程序员。。。
用适合的语言去实现你的业务,你的想法和创意,这些才是最重要的
语言只是实现手段而已,其实真的没有那么重要。。起码没有你想象中的重要

https://twitter.com/xushiwei/status/1783302674492055863
lolizeppelin
198 天前
对应现实中标准的自动化生成流程

非标转标

非标不能太宽泛..所以有接口限制
wwhontheway
198 天前
弱弱的问一句,如果是为了解藕,接收接口返回接口不是更好?
yuancoder
198 天前
你可以理解就是构造函数,接受接口类型就不是 go 独有的了,换别的语言也可以这样
kkbblzq
198 天前
@wwhontheway 因为组合,一个结构体里是可以实现多个接口的
yusheng88
198 天前
@kuanat
当你不熟悉其它语言( Java )时,就不要硬扯在一起了。
1 、Java 的类型是在变量前面的
2 、Java 对于接口的实现,做兼容升级或变更时,通常是修改 maven 的 pom.xml 中依赖包的版本号就好了。

下游(服务|接口调用方)不用关注接口实现的变动点,一般是上游(服务|接口提供方)通知变更,下游再在 pom.xml 中修改依赖包的版本号即可
ChristopherWu
198 天前
我也理解为什么提问者不清楚这个问题的答案, 因为不写 mock, 不写测试, 尤其是写库给其他服务用, 根本不会遇到问题, 接口跟结构体使用上没两样.

答案是: 当用接口时, 其他服务使用你的接口, 接口一但改变时, 其他服务也需要重新更新 mock. 而结构不用

当公司几十个上百个服务都用, 又没有统一的 mock 脚本时..是大灾难
yusheng88
198 天前
所有的设计模式,都是有适用场景的。
在写类库,框架之类的场景中,使用设计模式,会更方便阅读、拓展性更好,更容易维护。
在写简单 curd 业务代码的场景中, 由于这些代码基本不会拓展,复用 [可以在变复杂,需要复用时,再使用设计模式相关写法] ,使用设计模式就没啥用,写接口可能也就是方便做单元测试,切面相关的操作了。

golang 提倡使用 接口 ? 这个是不是真的?保留疑问。
whitedroa
198 天前
@kuanat 没太懂你写的 Go 的代码,库提供了一个 Use 方法,入参是*A 类型,那使用这个库的调用方,入参不也应该是*A 类型吗。NewStorage 又是给谁用的呢
YuuuuuuH
198 天前
@sagaxu 因为这是他刻意举的例子。根本没有现实意义。
kuanat
198 天前
@sagaxu #19

你提到“我们”的时候,还是没有跳出固有的思维。

在 Go 的设计思想里,包或者说库的作者,应当( should )假定自己的包有一天可能成为别人的依赖。然而你并不需要假想别人要以何种方式使用你的包,你只管写你的实现完成功能就可以了。别人可能仅仅因为一个结构定义,或者一个导出方法就引用你的包作为依赖。

如果我自己要同时完成某个功能的 A/B/C 三个实现,我当然会提前把接口写好。但是如果我只有 A 的需求,那我完全可以不写接口,写成接口的形式是为了某一天我要添加 B/C 支持、或者我知道这个包可能被别人拿去用而做的预先设计。

无论如何,受限于 Java 的类型检查机制,如果最初没有写成接口,任何人除了包的作者都没有很简单的办法复用一个没有接口的包,因为把接口抽象出来这个操作只能由包的所有者完成。虽然你觉得原作者对于 A 功能的实现写得很好,但想拿过来用,才不得不选择复制粘贴,否则就要求助原包的作者将代码改成接口的形式。复制粘贴的问题是一旦上游更新,你就会面临是不是要手动跟随更新的问题。提 PR 是考虑到,减轻原作者的工作量,提高原作者接口化的意愿,避免你自己跟随更新的麻烦。

这两个都是在尽量减少对原包作者的依赖,但是在人的层面解耦不够彻底。
kuanat
198 天前
@yusheng88 #28

你有没有考虑过,为什么上游要提供接口呢?这是下游的事情。上游 preemptively 接口化是个违反工程实践的行为。

你的思路还停留在“Java 可以用 XXX 的方式来做同样的事情”,不好意思,就本文讨论的话题,Java 真做不到。
kuanat
198 天前
@whitedroa #31

我尝试用注释里带文件名的方式来区分是谁写的某个文件,看起来还是不够清晰表达意图。

现在假设我是一个包的作者,我只关心赶快完成我的功能。这时候 A.go 和 main.go 都是我自己写的,我的 use 方法入参就用我的 *A ,简单粗暴。

现在另一个人看到我写的包,他觉得你竟然把这么复杂的 A 业务给抽象出来了,那他就借你写的 A.go 里面的 get/put/... 方法一用吧,这样他就只需要写个 B 支持,就能同时支持 A/B 了。这里的重点是,他其实只想用我写的 get/put 方法,对其他的不感兴趣。

考虑到对他来说,他的 main 里面不希望为 A/B 写不同的调用方法,于是就写成了 func New(s Storage) *MyStorage 的形式,需要调用 get/put 就在 *MyStorage 上面调用。这个 type Storage interface 里面只包含 get/put 两个方法即可。

这样他的 B.go 就可以简单封装一下 sdk 满足接口就可以了。

这个事情还可以继续下去,第三个人看见了第二个人的代码,还可以添加 C 支持。甚至当他需要 get/put 之外第三个 delete 方法的时候,可以用到 embedding 机制:

```go
type C struct {
a A
}
func (c C) delete() { ... }
```

调用的时候接受一个三个方法 get/put/delete 的接口即可。

全程下游都不需要上游配合。
kuanat
198 天前
@gowk #23

就事论事,我不是很认可这位作者的说法。即使我和他得到了相同的结论,也不代表我们有一样的推理过程。
yusheng88
198 天前
@kuanat
1 、我不推荐你说上下游,这个东西是有歧义的。
2 、我熟悉 Java , 所以我反驳你用错误的方案( code )做示例,这没问题吧。
3 、我遇到的业务场景都是上游提供接口|接口依赖包的,你的上游是什么我就不知道了。

你写的挺多的,但我真不知道你想说什么。
你的案例,我是真看不懂 Java 有什么不能实现的?
我也不明白为什么 go 的话题下,你要引用 Java 干嘛。

你的意思是调用方先定义接口, 服务方根据接口去实现?
这个只是 maven 中 接口包 谁提供的权责问题。

这个话题讨论的是 goland 为什么提倡使用接口。
我个人理解:
接口设计涉及的原则,都是为了方便后续维护(可拓展、阅读性等),这些是设计模式的内容,是等你无意中使用后,体验到了其好处之后才会有深刻体验的,你才会明白什么场景中使用比较合适,写个 hello world 都用这些东西的话,就是脱裤子放屁
aababc
198 天前
@kuanat #34
个人感觉这个说法怪怪的,从 golang 的 interface 来看 io.Reader, io.Writer 的设计也是自顶向下,如果没有这个从顶层开始的约束,下层在使用的时候根本就不知道会设计成啥样。
jqknono
198 天前
这种做法应该是为了实现两个设计原则:
1. 里式替换
2. 依赖倒置

你需要想象在团队开发里,先约定接口,再各自针对接口实现业务。返回不总是结构体,也可能还是接口,要看下游是自己还是别的开发团队。

以单线程个人开发体验是不那么容易理解,使用接口属于为了协作而多做的事,并行任务虽然总是降低单线程任务的效率,但能降低整体任务的成本。
sagaxu
198 天前
@kuanat #11

你预设了 Java 一定要改造 A 或者 B 去适配另一个类,实际上很少有人这么用。
现实场景经常更复杂,A 和 B 的方法签名可能不同,甚至 A 中的一个方法,在 B 中要分成多个方法调用,所以一般由使用者抽象定义接口。

// A 包
class A {
____void getFile(Arg a, Arg b, Arg c);
____void putFile(Arg a, Arg b);
}

// B 包
class B {
____void download(Arg a, Arg b);
____void upload(Arg a, Arg b, Arg c);
}


// main.java 调用的部分
interface Storage {
____default void get(...) {}
____default void put(...) {}
}

class StorageA implements Storage {...}
class StorageB implements Storage {...}

class main {
____void use(Storage a);
}

这跟 Go 有很大区别吗?

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

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

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

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

© 2021 V2EX