前言

应云而生的go语言,给我们带来了很多的不错的特性,但是如何让go代码更规范,更优雅,期望可以给你些许帮助。

作者对于golang也是在不断学习中,文章章节主要基于golang官方博客、社区的收集及思考,如有不妥可以文末留言,期望您的斧正, 此文也会不断完善。

目录

1. 代码规范

2. 如何写

3. 如何测

1. 代码规范

1.1 规范说明

Go语言比较常见并且使用广泛的代码规范就是官方提供的 Go Code Review Comments,无论你是短期还是长期使用 Go 语言编程,都应该至少完整地阅读一遍这个官方的代码规范指南,它既是我们在写代码时应该遵守的规则,也是在代码审查时需要注意的规范

作者从Go Code Review Coments收录部分,方便大家参考。

包如何命名

  1. 包名只允许包含小写字母,不要包含下划线和大小写字母混合。
  2. 尽量短,但是要有代表性的名字, 避免使用像’common’,‘util’ 这样的包名。
  3. 可以使用缩写,但是有歧义的话,就不要使用了; 另外在客户端引用包时也要尽量合理的变量名,比如: buffered I/O称为bufio, 而不要称其为 buf, 因为buf是一个buffer很合理的名字。
  4. 尽量避免不必要的包名冲突,这样可以减少混乱,并减少客户端代码中本地重命名的需要
  5. 包名一般建议使用单数形式。查看相关讨论

包内容(结构体/函数等)

  1. 避免包内容与包名重复, 比如HTTP包提供Server, 而不是HTTPServer
  2. 不同的包内支持有相同的名称, 比如标准库包含几种名为Reader的类型, 包括jpeg.Readerbufio.Readercsv.Reader.

学习 Go 语言相关的代码规范是一件非常重要的事情,也是让我们的项目遵循统一规范的第一步,虽然阅读代码规范相关的文档非常重要,但是在实际操作时我们并不能靠工程师自觉地遵守以及经常被当做形式的代码审查,而是需要借助工具来辅助执行。

1.2 辅助工具

作者在这一节中就会介绍三种非常切实有效的办法帮助我们在项目中自动化地进行一些代码规范检查和静态检查保证项目的质量,如果想了解更多的辅助工具请至vscode-go的wiki页面Go tools that the Go extension depends on

goimports / gofmt

goimports有两个作用,管理导入的包(添加缺少的包/删除未使用的包)

$ go get golang.org/x/tools/cmd/goimports

除了修复导入之外,goimport还会以与gofmt相同的样式来格式化代码. 所以goimports 就是等于 gofmt 加上依赖包管理。

golint / golangci-lint

golint 是官方的静态代码检查工具,主要基于 Effective GOCodeReviewComments作为依据,显示相应的样式错误信息,但它在可定制化上有着非常差的支持。

另外官方提醒大家,golint并不完善提出的是建议,它有可能误报。不要将其输出视为黄金标准。

: The suggestions made by golint are exactly that: suggestions. Golint is not perfect, and has both false positives and false negatives. Do not treat its output as a gold standard.

golangci-lint却有着很好的定制性,但主要维护者宣布近期将停止维护

goLanguageServer - 最终的选择

之前Go开发总是需要引出很多的Go tools(正如作者在1,2段落介绍的)来提供不同的特性,但是现在Go module的开发模式已经稳定运行几个Go版本了,如果当前你也已经在用Go modules, 基于更好的性能强烈推荐使用Go language server, 你也可以通过gopls - User guide基于自己的编辑器开启使用gopls的旅程。

: If you have chosen to use the Go language server, then most of the below tools are no longer needed as the corresponding features will be provided by the language server. Eventually, once the language server is stable, we will move to using it and deprecate the use of individual tools below.

2. 如何写

2.1 项目结构

目录结构基本上就是一个项目的门面,很多时候我们从目录结构中就能够看出开发者对这门语言是否有足够的经验,有些项目对于目录结构的划分非常随意,虽然对于功能性而言没有什么问题,但是社区基于golang-standards/project-layout 给大家提供一个相对比较推荐的目录划分方式。

如下:

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

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

/pkg

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

  • kubernetes 容器调度管理系统
  • grafana 展示监控和指标的仪表盘

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

/internal 私有代码

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

当我们在其他项目引入包含 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 包并不会出现这种错误。

/cmd

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

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

/api

/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语言项目如何正确使用Makefile

小结

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

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

3. 如何测

3.1 单元测试

单元测试的缺失不仅会意味着较低的工程质量,而且意味着重构的难以进行,一个有单元测试的项目尚且不能够保证重构前后的逻辑完全相同,一个没有单元测试的项目很可能本身的项目质量就堪忧,更不用说如何在不丢失业务逻辑的情况下进行重构了。

可测试

写代码并不是一件多困难的事情,不过想要在项目中写出可以测试的代码并不容易,而优雅的代码一定是可以测试的,我们在这一节中需要讨论的就是什么样的代码是可以测试的。

如果想要想清楚什么样的才是可测试的,我们首先要知道测试是什么?作者对于测试的理解就是控制变量,在我们隔离了待测试方法中一些依赖之后,当函数的入参确定时,就应该得到期望的返回值。

黑/白盒测试

如果只是简单做黑盒测试是没有意义的,因为测试意义就在于发现可能出现问题的地方,也通过白盒测试,测试不同的参数合并、条件边界和其他可能出现问题的地方。

注意: 我们所说的白盒测试是使用模块内部如何实现的方式测试模块,但仅通过公共接口进行测试; 而不是通过读取私有数据或拦截私有消息来检查模块的内部行为,从而测试模块是否根据其(当前)设计工作。

常见错误

1. Pointers Everywhere

前言

通过值传变量将会创建一个此变量的拷贝,然而通过指针的形式传递仅仅是拷贝此变量的内存地址,所以,用指针的形式传值通常更快吗?

所以你认同这一个点,请看下这个例子

附录

如何发布package

使用go一段时间后,你可能会想做发布一些package,让大家均可以使用,作者分享些注意点,期望对于你意义:

  1. module的名称不一定要和package 的名称一致:

an module是由一棵Go源文件树定义的,树的根目录中有一个go.mod文件。在go.mod 的文件内通过module 指令定义模块的路径,也是模板块中所有软件包的公共前缀,

比如 你为仓库github.com/user/mymod创建一个模块,这个仓库内包含两个包github.com/user/mymod/foogithub.com/user/mymod/bar, 然后,go.mod文件中的第一行通常会将模块路径声明为模块github.com/user/mymod,相应的文件的目录结构如下:

  mymod
  |-- bar
  |   `-- bar.go
  |-- foo
  |   `-- foo.go
  `-- go.mod

这样你就可以使用包含模块路径的全路径导入包,如

  import "github.com/user/mymod/bar"
  1. 注意package的命名规范,不要使用下划线、短横线,详细查看1.代码规范章节。

  2. 关于版本 vMAJOR.MINOR.PATCH

    • Increment the MAJOR version when you make a backwards incompatible change to the public API of your module. This should only be done when absolutely necessary.

    • Increment the MINOR version when you make a backwards compatible change to the API, like changing dependencies or adding a new function, method, struct field, or type.

    • Increment the PATCH version after making minor changes that don’t affect your module’s public API or dependencies, like fixing a bug.

: Do not delete version tags from your repo. If you find a bug or a security issue with a version, release a new version. If people depend on a version that you have deleted, their builds may fail. Similarly, once you release a version, do not change or overwrite it.

  1. Major version strategies

The examples in this post will follow the major version subdirectory strategy, since it provides the most compatibility. We recommend that module authors follow this strategy as long as they have users developing in GOPATH mode.

参考

推荐阅读

  1. Go语言项目如何正确使用Makefile
  2. Effective Go》中英双语版