Go 语言简单易学,对于大部分开发者来说,编写可运行的代码并不是一件难事,但如果想真正成为 Go 编程高手,你需要花很多精力去研究 Go 的编程哲学。
在我的 Go 开发生涯中,我见过各种各样的代码问题,例如:代码不规范、难以阅读;函数共享性差、代码重复率高;不是面向接口编程、代码扩展性差、代码不可测;代码质量低下。究其原因,是因为这些代码的开发者很少花时间去认真研究如何开发一个优雅的 Go 项目,更多时间是埋头在需求开发中。
如果你想提高自己的 Go 研发能力,很有必要花点时间,研究下如何开发一个优秀的 Go 项目。本节课我们就来看看,开发一个优秀 Go 应用的一些方法。
如何写出优雅的 Go 项目?
那么,如何写出一个优雅的 Go 项目呢?在回答这个问题之前,我们先来看另外两个问题:
- 为什么是 Go 项目,而不是 Go 应用?
- 一个优雅的 Go 项目具有哪些特点?
先来看第一个问题。Go 项目是一个偏工程化的概念,不仅包含了 Go 应用,还包含了项目管理和项目文档:
一个优雅的 Go 项目,不仅要求我们的 Go 应用是优雅的,还要确保我们的项目管理和文档也是优雅的。在我看来一个优雅的 Go 项目应该具备以下特点:
- 符合Go编码规范和最佳实践;
- 易阅读、易理解,易维护;
- 易测试、易扩展;
- 代码质量高。
要开发一个优秀的 Go 项目,在我看来就是用“最佳实践”的方式去实现 Go 项目中的 Go 应用、项目管理和项目文档。具体来说,就是编写高质量的 Go 应用、高效管理项目、编写高质量的项目文档。
为了协助你理解,我将这些逻辑绘制成了下面一张图。
编写高质量的 Go 应用
基于我的研发经验,要编写一个高质量的 Go 应用,其实可以归纳为 5 个方面:代码结构、代码规范、代码质量、编程哲学和软件设计方法,见下图。
接下来,我们详细说说这些内容。
代码结构
我们可以通过以下两个手段来组织代码结构:
- 组织一个好的目录结构。
- 选择一个好的模块拆分方法。做好模块拆分,可以使项目内模块职责分明,做到低耦合高内聚。
目前业界有两种拆分方法,分别是按层拆分和按功能拆分。
首先,我们看下按层拆分,最典型的是 MVC 架构中的模块拆分方式。在 MVC 架构中,我们将服务中的不同组件按访问顺序,拆分成了 Model、View 和 Controller 三层。每层完成不同的功能:
- View(视图),是提供给用户的操作界面,用来处理数据的显示。
- Controller(控制器),负责根据用户从 View 层输入的指令,选取 Model 层中的数据,然后对其进行相应的操作,产生最终结果。
- Model(模型),是应用程序中用于处理数据逻辑的部分。
在 Go 项目中,按层拆分会带来很多问题。最大的问题是循环引用:相同功能可能在不同层被使用到,而这些功能又分散在不同的层中,很容易造成循环引用。
那什么是按功能拆分呢?我给你看一个例子你就明白了。比如,一个订单系统,我们可以根据不同功能将其拆分成用户(user)、订单(order)和计费(billing)3 个模块,每一个模块提供独立的功能,功能更单一。下面是该订单系统的代码目录结构:
$ tree pkg
$ tree --noreport -L 2 pkg
pkg
├── billing
├── order
│ └── order.go
└── user
相较于按层拆分,按功能拆分模块带来的好处也很好理解:
- 不同模块,功能单一,可以实现高内聚低耦合的设计哲学。
- 因为所有的功能只需要实现一次,引用逻辑清晰,会大大减少出现循环引用的概率。
所以,有很多优秀的 Go 项目采用的都是按功能拆分的模块拆分方式,例如 Kubernetes、Docker、Helm、Prometheus 等。
除了组织合理的代码结构这种方式外,编写高质量 Go 应用的另外一个行之有效的方法,是遵循 Go 语言代码规范来编写代码。在我看来,这也是最容易出效果的方式。
代码规范
通常我们可以遵循 2 类代码规范:编码规范和最佳实践。
首先,我们的代码要符合 Go 编码规范,这是最容易实现的途径。Go 社区有很多这类规范可供参考,其中,比较受欢迎的是 Uber Go 语言编码规范。
除了遵循编码规范,要想成为 Go 编程高手,你还得学习并遵循一些最佳实践。“最佳实践”是社区经过多年探索沉淀下来的、符合 Go 语言特色的经验和共识,它可以帮助你开发出一个高质量的代码。
这里我给你推荐几篇介绍 Go 语言最佳实践的文章,供你参考:
- Effective Go:高效 Go 编程,由 Golang 官方编写,里面包含了编写 Go 代码的一些建议,也可以理解为最佳实践。
- Go Code Review Comments:Golang 官方编写的 Go 最佳实践,作为 Effective Go 的补充。
- Style guideline for Go packages:包含了如何组织 Go 包、如何命名 Go 包、如何写 Go 包文档的一些建议。
代码质量
有了组织合理的代码结构、符合 Go 语言代码规范的 Go 应用代码之后,我们还需要通过一些手段来确保我们开发出的是一个高质量的代码,这可以通过单元测试来实现。
单元测试非常重要。 我们开发完一段代码后,第一个执行的测试就是单元测试。它可以保证我们的代码是符合预期的,一些异常变动能够被及时感知到。进行单元测试,不仅需要编写单元测试用例,还需要我们确保代码是可测试的,以及具有一个高的单元测试覆盖率。
接下来,我就来介绍下如何编写一个可测试的代码。
如果我们要对函数 A 进行测试,并且 A 中的所有代码均能够在单元测试环境下按预期被执行,那么函数 A 的代码块就是可测试的。我们来看下一般的单元测试环境有什么特点:
- 可能无法连接数据库。
- 可能无法访问第三方服务。
如果函数 A 依赖数据库连接、第三方服务,那么在单元测试环境下执行单元测试就会失败,函数就没法测试,函数是不可测的。
解决方法也很简单:将依赖的数据库、第三方服务等抽象成接口,在被测代码中调用接口的方法,在测试时传入 mock 类型,从而将数据库、第三方服务等依赖从具体的被测函数中解耦出去。如下图所示:
为了提高代码的可测性,降低单元测试的复杂度,对 function 和 mock 的要求是:
- 要尽可能减少 function 中的依赖,让 function 只依赖必要的模块。编写一个功能单一、职责分明的函数,会有利于减少依赖。
- 依赖模块应该是易 Mock 的。
当我们的代码可测之后,就可以借助一些工具来 Mock 需要的接口了。常用的 Mock 工具,有这么几个:golang/mock、sqlmock、httpmock、bouk/monkey。
接下来,我们再一起看看如何提高我们的单元测试覆盖率。
当我们编写了可测试的代码之后,接下来就需要编写足够的测试用例,用来提高项目的单元测试覆盖率。这里我有以下两个建议供你参考:
- 使用 gotests 工具自动生成单元测试代码,减少编写单元测试用例的工作量,将你从重复的劳动中解放出来。
- 定期检查单元测试覆盖率。你可以通过以下方法来检查:
$ go test -race -cover -coverprofile=./coverage.out -timeout=10m -short -v ./...
$ go tool cover -func ./coverage.out
编程哲学
那编程哲学是什么意思呢?在我看来,编程哲学,其实就是要编写符合 Go 语言设计哲学的代码。Go 语言有很多设计哲学,对代码质量影响比较大的,我认为有两个:面向接口编程和面向”对象”编程。
面向接口编程
我们先来看下面向接口编程。
Go 接口是一组方法的集合。任何类型,只要实现了该接口中的方法集,那么就属于这个类型,也称为实现了该接口。
接口的作用,其实就是为不同层级的模块提供一个定义好的中间层。这样,上游不再需要依赖下游的具体实现,充分地对上下游进行了解耦。很多流行的 Go 设计模式,就是通过面向接口编程的思想来实现的。
使用接口可以带来以下好处:
- 代码扩展性更强;
- 可以解耦上下游的实现;
- 提高了代码的可测性;
- 代码更健壮、更稳定。
面向“对象”编程
接下来,我们再来看下面向“对象”编程。
面向对象编程(OOP)有很多优点,例如可以使我们的代码变得易维护、易扩展,并能提高开发效率等,所以一个高质量的 Go 应用在需要时,也应该采用面向对象的方法去编程。那什么叫“在需要时”呢?就是我们在开发代码时,如果一个功能可以通过接近于日常生活和自然的思考方式来实现,这时候就应该考虑使用面向对象的编程方法。
Go 语言不支持面向对象编程,但是却可以通过一些语言级的特性来实现类似的效果。面向对象编程中,有几个核心特性:类、实例、抽象,封装、继承、多态、构造函数、析构函数、方法重载、this 指针。在 Go 中可以通过以下几个方式来实现类似的效果:
- 类、抽象、封装通过结构体来实现。
- 实例通过结构体变量来实现。
- 继承通过组合来实现。这里解释下什么叫组合:一个结构体嵌到另一个结构体,称作组合。例如一个结构体包含了一个匿名结构体,就说这个结构体组合了该匿名结构体。
- 多态通过接口来实现。
至于构造函数、析构函数、方法重载和 this 指针等,Go 为了保持语言的简洁性去掉了这些特性。
软件设计方法
在你编写 Go 代码的时候,还可以遵循一些业界沉淀下来的,优秀的软件设计方法。优秀的软件设计方法有很多,其中有两类方法对我们代码质量的提升特别有帮助,分别是设计模式(Design pattern)和 SOLID 原则。
在我看来,设计模式可以理解为业界针对一些特定的场景总结出来的最佳实现方式。它的特点是解决的场景比较具体,实施起来会比较简单;而 SOLID 原则更侧重设计原则,需要我们彻底理解,并在编写代码时多思考和落地。
设计模式
我们先了解下有哪些设计模式。
在软件领域,沉淀了一些比较优秀的设计模式,其中最受欢迎的是 GOF 设计模式。GOF 设计模式中包含了 3 大类(创建型模式、结构型模式、行为型模式),共 25 种经典的、可以解决常见软件设计问题的设计方案。这 25 种设计方案同样也适用于 Go 语言开发的项目,设计模式列表如下图所示:
SOLID 原则
如果说设计模式解决的是具体的场景,那么 SOLID 原则就是我们设计应用代码时的指导方针。
SOLID 原则,是由罗伯特·C·马丁在 21 世纪早期引入的,包括了面向对象编程和面向对象设计的五个基本原则:
表格 还在加载中,请等待加载完成后再尝试复制
遵循 SOLID 原则可以确保我们设计的代码是易维护、易扩展、易阅读的。SOLID 原则同样也适用于 Go 程序设计。
如果你需要更详细地了解 SOLID 原则,可以参考下 SOLID原则介绍 这篇文章。
高效管理项目
一个优雅的 Go 项目,还需要具备高效的项目管理特性。那么如何高效管理我们的项目呢?
不同团队、不同项目会采用不同的方法来管理项目,在我看来比较重要的有3点,分别是制定一个高效的开发流程、使用 Makefile 管理项目和将项目管理自动化。我们可以通过自动生成代码、借助工具、对接 CI/CD 系统等方法来将项目管理自动化。具体见下图:
高效的开发流程
高效管理项目的第一步,就是要有一个高效的开发流程,这可以提高开发效率、减少软件维护成本。
使用 Makefile 管理项目
为了更好地管理项目,除了一个高效的开发流程之外,使用 Makefile 也很重要。Makefile 可以将项目管理的工作通过 Makefile 依赖的方式实现自动化,除了可以提高管理效率之外,还能够减少人为操作带来的失误,并统一操作方式,使项目更加规范。
自动生成代码
低代码的理念现在越来越流行。虽然低代码有很多缺点,但确实有很多优点,例如:
- 自动化生成代码,减少工作量,提高工作效率。
- 代码有既定的生成规则,相比人工编写代码,准确性更高、更规范。
目前来看,自动生成代码现在已经成为趋势,比如 Kubernetes 项目有很多代码都是自动生成的。我认为,想写出一个优雅的 Go 项目,你也应该认真思考哪些地方的代码可以自动生成。
善于借助工具
在开发 Go 项目的过程中,我们也要善于借助工具,来帮助我们完成一部分工作。利用工具可以带来很多好处:
- 解放双手,提高工作效率;
- 利用工具的确定性,可以确保执行结果的一致性。例如,使用 golangci-lint 对代码进行检查,可以确保不同开发者开发的代码至少都遵循 golangci-lint 的代码检查规范;
- 有利于实现自动化,可以将工具集成到 CI/CD 流程中,触发流水线自动执行。
对接 CI/CD
代码在合并入主干时,应该有一套 CI/CD 流程来自动化地对代码进行检查、编译、单元测试等,只有通过后的代码才可以并入主干。通过 CI/CD 流程来保证代码的质量。当前比较流行的 CI/CD 工具有 Jenkins、GitLab、Argo、Github Actions、JenkinsX 等。
编写高质量的项目文档
最后,一个优雅的项目,还应该有完善的文档。例如 README.md、安装文档、开发文档、使用文档、API 接口文档、设计文档等等。
小结
使用 Go 语言做项目开发,核心目的其实就是开发一个优雅的 Go 项目。那么如何开发一个优雅的 Go 项目呢?Go 项目包含三大内容,即 Go 应用、项目管理、项目文档,因此开发一个优雅的 Go 项目,其实就是编写高质量的 Go 应用、高效管理项目和编写高质量的项目文档。