draveness / blog-comments

面向信仰编程
https://draveness.me
140 stars 6 forks source link

如何写出优雅的 Golang 代码 · /golang-101 #149

Closed draveness closed 2 years ago

draveness commented 5 years ago

https://draveness.me/golang-101

leafduo commented 5 years ago

很多都很赞同,尤其是纵向划分模块

依赖比较复杂的话可以试下用 wire 来注入

Go 里面搞 monkey patch 感觉很疯狂啊,看原理应该跟 fishhook 差不多

draveness commented 5 years ago

很多都很赞同,尤其是纵向划分模块

依赖比较复杂的话可以试下用 wire 来注入

Go 里面搞 monkey patch 感觉很疯狂啊,看原理应该跟 fishhook 差不多

我看了下 wire 感觉还挺有意思的,不过看起来是 alpha 版本

houjunchen commented 5 years ago

@draveness

很多都很赞同,尤其是纵向划分模块

依赖比较复杂的话可以试下用 wire 来注入

Go 里面搞 monkey patch 感觉很疯狂啊,看原理应该跟 fishhook 差不多

我看了下 wire 感觉还挺有意思的,不过看起来是 alpha 版本

我們是用 https://github.com/uber-go/dig 來解決依賴注入

carlsiry commented 5 years ago

代码配色的主题叫什么名字?

GiaoGiaoCat commented 5 years ago

「过私有的接口体」 应改为「私有的结构体」

GiaoGiaoCat commented 5 years ago

可以开通个打赏功能?感觉作者的文章都是用心的干货。

wujunze commented 5 years ago

好文章 对 Go 项目工程化很有意义 感谢博主分享

draveness commented 5 years ago

「过私有的接口体」 应改为「私有的结构体」

已经修复了

可以开通个打赏功能?感觉作者的文章都是用心的干货。

暂时不会在博客上挂这个东西..

draveness commented 5 years ago

代码配色的主题叫什么名字?

prism 具体什么名字不记得了

draveness commented 5 years ago

@draveness

很多都很赞同,尤其是纵向划分模块 依赖比较复杂的话可以试下用 wire 来注入 Go 里面搞 monkey patch 感觉很疯狂啊,看原理应该跟 fishhook 差不多

我看了下 wire 感觉还挺有意思的,不过看起来是 alpha 版本

我們是用 https://github.com/uber-go/dig 來解決依賴注入

依赖注入确实是一个比较有意思的话题,但是感觉在 Go 的社区中并不常见

yuanjize commented 5 years ago

干货十足啊,干过的公司没有写单元测试的,自己想过写但是很多依赖sql和http的不知道怎么搞。

bigwalle commented 5 years ago

赞,知识量很丰富!

draveness commented 5 years ago

干货十足啊,干过的公司没有写单元测试的,自己想过写但是很多依赖sql和http的不知道怎么搞。

单元测试还是非常重要的 🤣

yegle commented 5 years ago

面向接口部分,NewService返回一个Interface,这是一个很危险的设计。往简单的看,这样导致了返回的struct无法被copy。往复杂了看,这个时候一个interface就变成你的公开接口,这个interface即使只做了向后兼容的改动(例如新增一个method)都要break很多外部代码

draveness commented 5 years ago

面向接口部分,NewService返回一个Interface,这是一个很危险的设计。往简单的看,这样导致了返回的struct无法被copy。往复杂了看,这个时候一个interface就变成你的公开接口,这个interface即使只做了向后兼容的改动(例如新增一个method)都要break很多外部代码

~不返回 interface 难道返回一个 struct 么,那上层依赖的还是 struct,为什么还需要面向接口,NewService 的作用就是返回接口,隐藏内部的实现结构体~

相关资料

我又查了一下相关的资料,发现关于这个问题,很多人写过文章,大部分的文章都在提到了一句话『Accept interfaces return structs』,也就是接受接口并且返回结构体;而 Dave 曾经也发过一条 Twitter,我在这里就直接引用一下:

golang top tip: the consumer should define the interface. If you’re defining an interface and an implementation in the same package, you may be doing it wrong.

消费者应该负责定义接口,如果在一个包中同时定义了接口和实现,那么你可能就做错了。

思考

想了一下,这样做在某些情况下确实更好并且合理

  1. 如果我们上游只存在一个依赖,那么我们返回公开的 struct 就比较有价值,上游可以将返回的结构体方法通过 interface 进行隔离,去掉不会使用的方法,但是这就需要我们谨慎地定义当前结构体的公有方法以及变量;
  2. 如果上游存在多个依赖,为每一个 package 单独创建一个 interface 就非常麻烦,我们还是需要在新的 package 中创建 interface 来封装结构体的方式,但是在这种情况下让下游去返回一个 interface 相比之下就更加方便;

在一个常见的项目中,使用 NewService 的方式返回一个接口,作者觉得并没有什么问题,无论是 struct 无法被 copy 还是 interface 增加了方法会导致 break 外部的代码(还要有其他人实现这个接口)都不会有太大的影响,很多时候只有返回 interface 才能真正地让别人使用 interface.

示例

Go 语言官方的 Context 包 就没有使用这种返回结构体的方式,它内部有 emptyCtxvalueCtx 等私有结构体,但是最后对外暴露的也只有 Context 接口。

总结

在生产者中返回结构体并消费者中定义接口是更加合理的,我们也应该这么去做,但是我也不认为在 NewService 这种『构造器』中返回接口就一定是有问题的。

PS:之后会对文章内容进行修改,非常感谢你提出的问题,让我对这件事情有了更清楚地理解。

yegle commented 5 years ago

面向inteface并不代表所有情况下都用接口。接受接口作为函数参数、返回符合某些接口的struct,也是面向接口。

xrlin commented 5 years ago

作者的文章都是干货满满,学习和毅力令人佩服,以后在个人项目中慢慢实践,只是这套用在我目前所在公司的项目中就很难了,公司风气过于浮躁。

lujin123 commented 5 years ago

看到的晚了😰,已经把项目按照mvc划分了,感觉再改一遍灰常麻烦

r6c commented 5 years ago

图是用什么画的?

draveness commented 5 years ago

接口的例子还有更好的写法:Accept interfaces, return structs

https://blog.chewxy.com/2018/03/18/golang-interfaces/

重复的问题,可以看上面的讨论 https://github.com/draveness/blog-comments/issues/149#issuecomment-497902619

pydr commented 5 years ago

醍醐灌顶,获益匪浅

fosmjo commented 5 years ago

纵向划分模块的情况下,以文中的 post,comment 模型为例,如果我想给 post定义一个 comments 方法(表示post的所有comments),给comment定义一个post方法(表示comment所属post),但是这不就造成循环引用了吗?有什么办法吗?

draveness commented 5 years ago

纵向划分模块的情况下,以文中的 post,comment 模型为例,如果我想给 post定义一个 comments 方法(表示post的所有comments),给comment定义一个post方法(表示comment所属post),但是这不就造成循环引用了吗?有什么办法吗?

文章举得例子可能不是特别恰当,想要说明的是我们要按照职责进行拆分,并不一定要分的这么细,我们还是要选择一个合适的粒度。

如果遇到了这种情况你说的这种情况,其实在设计上就对两个模块约定了较强的耦合,在这时如果:

  1. 各个模块并不需要进行拆分 — 还是将两个 package 合起来吧,blog 更像是一个合适的拆分粒度;
  2. 各个模块的访问量差异非常大,需要进行服务的拆分,也就不应该出现循环依赖的设计 — 需要重新设计模块(或服务)之间的依赖关系,一般情况下是上层(post)依赖下层(comment);
SmileEye commented 5 years ago

接触了一个go项目,一个源文件引入十几个包,很多包都有 init(), 看源文件代码的时候有些乱,一个变量没有初始化怎么使用的,最后发现是在 init() 中做的。 类似操作很多,导致看代码的时候不得不先关注引入包的init()执行了哪些操作,增加了理解代码的难度。

xrlin commented 5 years ago

@SmileEye 接触了一个go项目,一个源文件引入十几个包,很多包都有 init(), 看源文件代码的时候有些乱,一个变量没有初始化怎么使用的,最后发现是在 init() 中做的。 类似操作很多,导致看代码的时候不得不先关注引入包的init()执行了哪些操作,增加了理解代码的难度。

所以应该尽量避免这种使用方式,如果需要init初始化,最好每个包独立一个init文件用来存放init相关的操作。

noobcoderr commented 5 years ago

gomock部分,那么当我在不测试的情况下,要new一个service时的Blog接口参数如何而来呢?&jekyll或者 &wordpress 嘛?

draveness commented 5 years ago

gomock部分,那么当我在不测试的情况下,要new一个service时的Blog接口参数如何而来呢?&jekyll或者 &wordpress 嘛?

没明白你的问题,能说的详细点么?

noobcoderr commented 5 years ago

@draveness

gomock部分,那么当我在不测试的情况下,要new一个service时的Blog接口参数如何而来呢?&jekyll或者 &wordpress 嘛?

没明白你的问题,能说的详细点么?

我才刚学go没多久,基础不够扎实,有些问题比较没水平,请见谅。我上一个问题想要描述的是:在生产环境时,我们要实例化一个service出来,然后调用该实例的ListPosts()方法获取文章。问题就是实例化的时候NewService(Blog)里的Blog接口参数该怎么制造出来呢?我搜索了资料,接口变量存储的是实现者的值,所以我想确认的是 jekyll、wordpress结构体实现了Blog接口,那么他们的具体值(实例化后)即可当做形参Blog的值传输进去吧?

draveness commented 5 years ago

@draveness

gomock部分,那么当我在不测试的情况下,要new一个service时的Blog接口参数如何而来呢?&jekyll或者 &wordpress 嘛?

没明白你的问题,能说的详细点么?

我才刚学go没多久,基础不够扎实,有些问题比较没水平,请见谅。我上一个问题想要描述的是:在生产环境时,我们要实例化一个service出来,然后调用该实例的ListPosts()方法获取文章。问题就是实例化的时候NewService(Blog)里的Blog接口参数该怎么制造出来呢?我搜索了资料,接口变量存储的是实现者的值,所以我想确认的是 jekyll、wordpress结构体实现了Blog接口,那么他们的具体值(实例化后)即可当做形参Blog的值传输进去吧?

我们其实有两种选择:

  1. 直接返回接口
  2. 返回结构体

返回接口

一般情况可以这样,在 jekyllwordpress 各自的 package 里面创建一个 NewBlog 方法返回 Blog 接口

// pkg/jekyll/blog.go
package jekyll

type jekyll struct{}

func NewBlog(xxx) blog.Blog {
  return jekyll{}
}

func (j *jekyll) ListPosts() {}

// pkg/wordpress/blog.go
package wordpress

type wordpress struct{} 

func NewBlog(xxx) blog.Blog {
  return &wordpress{}
}

func (j *wordpress) ListPosts() {}

返回结构体

也可以返回结构体,就像在 这里 讨论的,这些结构体需要实现 ListPosts 方法,只要实现了 ListPosts 方法就意味着当前结构体可以作为参数传入 NewService(blog.Blog)

// pkg/jekyll/blog.go
package jekyll

type Blog struct {
}

func NewBlog(xxx) Blog {
  return Blog{}
}

func (*Blog) ListPosts() {}

小结

这两种方式怎么选就看你自己了,我一般在普通的业务服务中会用第一种方式,在框架和库中会用第二种

noobcoderr commented 5 years ago

非常感谢楼主,解答了我的疑惑的同时还提供了新的思路,由于上条回复过长就不再上条回复进行回复了,以后会多拜读楼主的博客的

NadiaSama commented 4 years ago

你好看了你的文章很受启发。我最近想参考一些go语言开源项目学习go语言使用方法,有什么好的项目推荐么?

chinaran commented 4 years ago

文章写得真棒👍

arithmeticsx commented 4 years ago

学到很多东西,受启发了,谢谢

liyebing commented 4 years ago

我觉得自己这活到狗身上去了。。。佩服,向作者学习

liyebing commented 4 years ago

"每一个单元测试都表示一个可能发生的情况,单元测试就是业务逻辑。" 这句话说到我心坎里去了

draveness commented 4 years ago

"每一个单元测试都表示一个可能发生的情况,单元测试就是业务逻辑。" 这句话说到我心坎里去了

还行是吧,哈哈哈

mvbn6789 commented 4 years ago

题主 你好 有没有哪个项目比较符合你这篇的最佳实践 我刚在写第一个go项目 刚好可以借鉴下~

draveness commented 4 years ago

题主 你好 有没有哪个项目比较符合你这篇的最佳实践 我刚在写第一个go项目 刚好可以借鉴下~

你可以看看 prometheus、kubernetes 这些开源项目,从目录结构开始,多用工具找找感觉

smartfly commented 4 years ago

单元测试使用这个gomonkey使用感觉还不错 https://github.com/agiledragon/gomonkey

draveness commented 4 years ago

单元测试使用这个gomonkey使用感觉还不错 https://github.com/agiledragon/gomonkey

文档看起来不是特别全,例子都在单元测试里

shenguanjiejie commented 4 years ago

请问移动端开发单元测试也是必要的么? 另外移动端单元测试的话, 还需要覆盖到接口的测试么?

draveness commented 4 years ago

请问移动端开发单元测试也是必要的么? 另外移动端单元测试的话, 还需要覆盖到接口的测试么?

那还是看看投入产出比吧,如果是界面单元测试,个人觉得没有必要,不过框架或者库还是需要写的,

shenguanjiejie commented 4 years ago

@draveness

请问移动端开发单元测试也是必要的么? 另外移动端单元测试的话, 还需要覆盖到接口的测试么?

那还是看看投入产出比吧,如果是界面单元测试,个人觉得没有必要,不过框架或者库还是需要写的,

感谢感谢, 真的解惑了 :)

To-echo commented 4 years ago

enhay commented 4 years ago

谢谢.

DustOak commented 3 years ago

您好 我有个问题关于拆分接口哪里想请问一下 您说的当一New的时候返回一个接口类型 那么如果我这个结构体只有这么一个且不会扩展的情况下是否还需要返回接口类型呢

draveness commented 3 years ago

您好 我有个问题关于拆分接口哪里想请问一下 您说的当一New的时候返回一个接口类型 那么如果我这个结构体只有这么一个且不会扩展的情况下是否还需要返回接口类型呢

这种情况都可以,在项目中统一就行

Gongzq5 commented 3 years ago

纵向切分真的是醍醐灌顶啊,最近刚开始从头写一个玩具后端,用MVC最后代码里有好多同名的包,写的好难受~谢谢博主

khsiufu commented 2 years ago

感谢分享,很棒文章

hisfei commented 2 years ago

复杂业务service 层与repo层是否也需要用接口拆分,如果垂直拆分对于大一些的项目,颗粒度还是有点粗了