如何写出优雅的Golang代码(二)

 go教练   2019-07-29 16:10   75 人阅读  0 条评论

我们在上一节中介绍了一些能通过自动化工具发现的问题,这一节提到的最佳实践可能就没有办法通过自动化工具进行保证,这些最佳实践更像是Go语言社区内部发展过程中积累的一些工程经验和共识,遵循这些最佳实践能够帮助我们写出符合 Go 语言『味道』的代码,我们将在这一小节覆盖以下的几部分内容:

目录结构;模块拆分;显式调用;面向接口;

这四部分内容是在社区中相对来说比较常见的约定,如果我们学习并遵循了这些约定,同时在Go语言的项目中实践这几部分内容,相信一定会对我们如何写出优雅的Golang代码有所帮助。

目录结构

目录结构基本上就是一个项目的门面,很多时候我们从目录结构中就能够看出开发者对这门语言是否有足够的经验,所以在这里首先要介绍的最佳实践就是如何在Go语言的项目或者服务中组织代码。

官方并没有给出一个推荐的目录划分方式,很多项目对于目录结构的划分也非常随意,这其实也是没有什么问题的,但是社区中还是有一些比较常见的约定,例如:golang-standards/project-layout项目中就定义了一个比较标准的目录结构。

├── LICENSE.md ├── Makefile ├── README.md ├── api ├── assets ├── build ├── cmd ├── configs ├── deployments ├── docs ├── examples ├── githooks ├── init ├── internal ├── pkg ├── scripts ├── test ├── third_party ├── tools ├── vendor ├── web └── website

我们在这里就像简单介绍其中几个比较常见并且重要的目录和文件,帮助我们快速理解如何使用如上所示的目录结构,如果各位读者想要了解使用其他目录的原因,可以从golang-standards/project-layout项目中的README了解更详细的内容。

/pkg目录是Go语言项目中非常常见的目录,我们几乎能够在所有知名的开源项目(非框架)中找到它的身影,例如:

prometheus上报和存储指标的时序数据库istio服务网格 2.0 kubernetes容器调度管理系统grafana展示监控和指标的仪表盘

这个目录中存放的就是项目中可以被外部应用使用的代码库,其他的项目可以直接通过import引入这里的代码,所以当我们将代码放入pkg时一定要慎重,不过如果我们开发的是HTTP或者 RPC 的接口服务或者公司的内部服务,将私有和公有的代码都放到/pkg中也没有太多的不妥,因为作为最顶层的项目来说很少会被其他应用直接依赖,当然严格遵循公有和私有代码划分是非常好的做法,建议各位开发者对项目中公有和私有的代码进行妥善的划分。

私有代码

私有代码推荐放到/internal目录中,真正的项目代码应该写在/internal/app里,同时这些内部应用依赖的代码库应该在/internal/pkg子目录和/pkg中,下图展示了一个使用/internal目录的项目结构:

 微信截图_20190729095932.png

当我们在其他项目引入包含internal的依赖时,Go语言会在编译时报错:

An import of a path containing the element “internal” is disallowed if the importing code is outside the tree rooted at the parent of the "internal" directory.

这种错误只有在被引入的internal包不存在于当前项目树中才会发生,如果在同一个项目中引入该项目的internal包并不会出现这种错误。

Go语言的项目最不应该有的目录结构其实就是/src了,社区中的一些项目确实有/src文件夹,但是这些项目的开发者之前大多数都有Java的编程经验,这在 Java 和其他语言中其实是一个比较常见的代码组织方式,但是作为一个Go语言的开发者,我们不应该允许项目中存在/src目录。

最重要的原因其实是Go语言的项目在默认情况下都会被放置到$GOPATH/src目录下,这个目录中存储着我们开发和依赖的全部项目代码,如果我们在自己的项目中使用/src目录,该项目的PATH中就会出现两个src:

$GOPATH/src/github.com/draveness/project/src/code.go

上面的目录结构看起来非常奇怪,这也是我们在Go语言中不建议使用/src目录的最重要原因。

当然哪怕我们在Go语言的项目中使用/src目录也不会导致编译不通过或者其他问题,如果坚持这种做法对于项目的可用性也没有任何的影响,但是如果想让我们『看起来』更专业,还是遵循社区中既定的约定减少其他Go语言开发者的理解成本,这对于社区来说是一件好事。

另一种在Go语言中组织代码的方式就是项目的根目录下放项目的代码,这种方式在很多框架或者库中非常常见,如果想要引入一个使用pkg目录结构的框架时,我们往往需要使用github.com/draveness/project/pkg/somepkg,当代码都平铺在项目的根目录时只需要使用github.com/draveness/project,很明显地减少了引用依赖包语句的长度。

所以对于一个Go语言的框架或者库,将代码平铺在根目录下也很正常,但是在一个Go语言的服务中使用这种代码组织方法可能就没有那么合适了。

/cmd目录中存储的都是当前项目中的可执行文件,该目录下的每一个子目录都应该包含我们希望有的可执行文件,如果我们的项目是一个grpc服务的话,可能在/cmd/server/main.go中就包含了启动服务进程的代码,编译后生成的可执行文件就是server。

我们不应该在/cmd目录中放置太多的代码,我们应该将公有代码放置到/pkg中并将私有代码放置到/internal中并在/cmd中引入这些包,保证main函数中的代码尽可能简单和少。

/api目录中存放的就是当前项目对外提供的各种不同类型的 API 接口定义文件了,其中可能包含类似/api/protobuf-spec、/api/thrift-spec或者/api/http-spec的目录,这些目录中包含了当前项目对外提供的和依赖的所有API文件:

$tree ./api api └── protobuf-spec └── oceanbookpb ├── oceanbook.pb.go └── oceanbook.proto

二级目录的主要作用就是在一个项目同时提供了多种不同的访问方式时,用这种办法避免可能存在的潜在冲突问题,也可以让项目结构的组织更加清晰。

Makefile

最后要介绍的Makefile文件也非常值得被关注,在任何一个项目中都会存在一些需要运行的脚本,这些脚本文件应该被放到/scripts目录中并由Makefile触发,将这些经常需要运行的命令固化成脚本减少『祖传命令』的出现。

总的来说,每一个项目都应该按照固定的组织方式进行实现,这种约定虽然并不是强制的,但是无论是组内、公司内还是整个Go语言社区中,只要达成了一致,对于其他工程师快速梳理和理解项目都是很有帮助的。

这一节介绍的Go语言项目的组织方式也并不是强制要求的,这只是Go语言社区中经常出现的项目组织方式,一个大型项目在使用这种目录结构时也会对其进行微调,不过这种组织方式确实更为常见并且合理。

模块拆分

我们既然已经介绍过了如何从顶层对项目的结构进行组织,接下来就会深入到项目的内部介绍Go语言对模块的一些拆分方法。

Go语言的一些顶层设计最终导致了它在划分模块上与其他的编程语言有着非常明显的不同,很多其他语言的Web框架都采用 MVC的架构模式,例如Rails和Spring MVC,Go语言对模块划分的方法就与Ruby和Java完全不同。

按层拆分

无论是Java还是Ruby,它们最著名的框架都深受MVC架构模式 的影响,我们从Spring MVC的名字中就能体会到MVC对它的影响,而Ruby社区的Rails框架也与MVC的关系非常紧密,这是一种Web框架的最常见架构方式,将服务中的不同组件分成了 Model、View和Controller三层。

 微信截图_20190729095941.png

这种模块拆分的方式其实就是按照层级进行拆分,Rails脚手架默认生成的代码其实就是将这三层不同的源文件放在对应的目录下:models、views和controllers,我们通rails new example生成一个新的Rails项目后可以看到其中的目录结构:

$tree -L 2 app app ├── controllers │ ├── application_controller.rb │ └── concerns ├── models │ ├── application_record.rb │ └── concerns └── views └── layouts

而很多Spring MVC的项目中也会出现类似model、dao、view的目录,这种按层拆分模块的设计其实有以下的几方面原因:

MVC架构模式 - MVC本身就强调了按层划分职责的设计,所以遵循该模式设计的框架自然有着一脉相承的思路;扁平的命名空间 - 无论是Spring MVC还是Rails,同一个项目中命名空间非常扁平,跨文件夹使用其他文件夹中定义的类或者方法不需要引入新的包,使用其他文件定义的类时也不需要增加额外的前缀,多个文件定义的类被『合并』到了同一个命名空间中;单体服务的场景 - Spring MVC和Rails刚出现时,SOA和微服务架构还不像今天这么普遍,绝大多数的场景也不需要通过拆分服务;

上面的几个原因共同决定了Spring MVC和Rails会出现models、views和controllers的目录并按照层级的方式对模块进行拆分。

按职责拆分

Go语言在拆分模块时就使用了完全不同的思路,虽然MVC架构模式是在我们写Web服务时无法避开的,但是相比于横向地切分不同的层级,Go语言的项目往往都按照职责对模块进行拆分:

 微信截图_20190729095954.png

对于一个比较常见的博客系统,使用Go语言的项目会按照不同的职责将其纵向拆分成post、user、comment三个模块,每一个模块都对外提供相应的功能,post模块中就包含相关的模型和视图定义以及用于处理API请求的控制器(或者服务):

$tree pkg pkg ├── comment ├── post │ ├── handler.go │ └── post.go └── user

Go语言项目中的每一个文件目录都代表着一个独立的命名空间,也就是一个单独的包,当我们想要引用其他文件夹的目录时,首先需要使用import关键字引入相应的文件目录,再通过pkg.xxx的形式引用其他目录定义的结构体、函数或者常量,如果我们在 Go语言中使用model、view和controller来划分层级,你会在其他的模块中看到非常多的model.Post、model.Comment和view.PostView。

这种划分层级的方法在Go语言中会显得非常冗余,并且如果对项目依赖包的管理不够谨慎时,很容易发生引用循环,出现这些问题的最根本原因其实也非常简单:

package

项目是按照层级还是按照职责对模块进行拆分其实并没有绝对的好与不好,语言和框架层面的设计最终决定了我们应该采用哪种方式对项目和代码进行组织。

Java和Ruby这些语言在框架中往往采用水平拆分的方式划分不同层级的职责,而Go语言项目的最佳实践就是按照职责对模块进行垂直拆分,将代码按照功能的方式分到多个package中,这并不是说Go语言中不存在模块的水平拆分,只是因为package作为一个Go语言访问控制的最小粒度,所以我们应该遵循顶层的设计使用这种方式构建高内聚的模块。

以上就是今天给大家介绍的如何写出优雅的Golang代码(二),如果你想了解更多关于golang的知识技巧,可以继续关注我们http://www.fastgolang.com

本文地址:http://fastgolang.com/114.html
版权声明:本文为原创文章,版权归 go教练 所有,欢迎分享本文,转载请保留出处!

 发表评论


表情

还没有留言,还不快点抢沙发?