
2.3 公共组件
上在每个公司的项目中,都有一类组件,一般称其为基础组件,或公共组件。它们没有强业务属性,且串联着整个应用程序,一般由负责基建或第一批搭建该项目的程序员进行梳理和编写。如果没有这类组件,任由每个程序员各写一套,则是非常糟糕的,这会使得这个应用程序无法形成闭环。
本节我们将完成一个Web应用中最常用的一些基础组件,以保障应用程序的标准化。该基础组件一共分为五个板块,如图2-3所示。

图2-3
2.3.1 错误码标准化
在应用程序运行过程中,我们常常需要与客户端进行交互。交互一般分为两点:一个是返回正确响应下的结果集;另一个是返回错误响应下的错误码和消息体,以便告诉客户端,这一次请求发生了什么事,以及请求失败的原因。在错误码的处理上,又延伸出一个新的问题,那就是错误码的标准化处理。如果不对错误码进行提前预判,则会引发较大的麻烦,如图 2-4 所示。

图2-4
在图2-4中,可以看到客户端分别调用了三个不同的服务端,即服务端A、服务端B和服务端C。它们的响应结果的模式不同,如果不做修改,客户端就需要知道它调用的是哪个服务,然后对每一个服务写一种错误码处理规则,非常烦琐。如果后续添加了新的服务端,且响应结果的模式与之前的不同,则必须添加新的错误码处理规则。
因此,我们要尽可能地保证每个项目前后端的交互语言规则是一致的,也就是说,在搭建一个新项目之初,其中重要的一项预备工作就是将错误码格式标准化,以保证客户端可以“理解”错误码规则,不需要每次都写一套新的。
1.公共错误码
在项目目录pkg/errcode下新建common_code.go文件,预定义项目中的一些公共错误码,以便引导和规范大家的使用,代码如下:

2.错误处理
在项目目录pkg/errcode下新建errcode.go文件,在其中编写常用的一些错误处理公共方法,用以标准化我们的错误输出,代码如下:



首先,在编写错误处理公共方法过程中,声明了Error结构体,用于表示错误的响应结果。然后,把codes作为全局错误码的存储载体,以便查看当前的注册情况。最后,在调用NewError创建新的Error实例的同时,进行排重校验。
另外相对特殊的是StatusCode方法,它主要针对一些特定错误码进行状态码的转换。因为不同的内部错误码在HTTP状态码中表示不同的含义,所以我们需要将其区分开来,以便客户端及监控或报警等系统的识别和监听。
2.3.2 配置管理
在应用程序的运行生命周期中,应用的配置读取和更新可以直接改变应用程序,其包含的行为如图2-5所示。

图2-5
● 在启动时:可以进行一些初始化行为,如配置基础应用属性、连接第三方实例(MySQL、NoSQL)等。
● 在运行中:可以通过监听文件或变更其他存储载体来实现热更新配置效果。例如,一旦发现有变更,就对原有配置值进行修改,以此达到相关联的一个效果。另外,我们还可以通过配置热更新,达到功能灰度的效果,这也是一个比较常见的场景。
此外,配置组件可以根据实际情况去选型,一般来说,多为文件配置模式或配置中心模式。在本实例中(博客后端)中,我们的配置管理使用最常见的文件配置作为我们的选型。
1.安装viper
为了完成文件配置的读取,我们需要借助第三方开源库viper。在项目根目录下执行以下安装命令:

viper是目前Go语言中较为流行的文件配置解决方案,可适用于Go应用程序的完整配置,支持处理各种类型的配置需求和配置格式。
2.配置文件
在项目目录下的configs目录中新建config.yaml文件,写入如下配置:

在配置文件中,我们分别针对以下内容进行默认配置。
● Server:服务配置,设置gin的运行模式、默认的HTTP监听端口、允许读取和写入的最大持续时间。
● App:应用配置,设置默认每页数量、所允许的最大每页数量,以及默认的应用日志存储路径。
● Database:数据库配置,主要是连接实例所必需的基础参数。
3.编写组件
在编写完配置文件后,我们需要对读取配置的行为进行封装,以便应用程序的使用。在项目目录下的pkg/setting目录中新建setting.go文件,写入如下代码:

在上述代码中,我们编写了NewSetting方法,用于初始化本项目配置的基础属性,即设定配置文件的名称为config、配置类型为yaml,并且设置其配置路径为相对路径configs/,以确保在项目目录下能够成功启动编写组件。
另外,viper是允许设置多个配置路径的,这样可以尽可能地尝试解决路径查找问题,也就是说,可以不断地调用AddConfigPath方法。在本书后面会深入介绍这部分内容。
下面新建section.go文件,用于声明配置属性的结构体,并编写读取区段配置的配置方法,代码如下:


4.包全局变量
仅读取文件的配置信息是不够的,我们还需将配置信息和应用程序关联起来,这样才能使用它。下面在项目目录的global目录下新建setting.go文件,写入如下代码:

这里对最初预估的三个区段进行了配置并声明了全局变量,以便在接下来的步骤中将其关联起来,提供给应用程序内部调用。
另外,全局变量的初始化是会随着应用程序的不断演进而不断改变的,也就是说,这里展示的并不一定是最终结果。
5.初始化配置读取
在完成所有的预备行为后,回到项目根目录下的main.go文件,修改代码如下:


这里新增了一个init方法。在Go语言中,init方法常用于应用程序内的一些初始化操作,它在main方法之前自动执行。在Go语言中,程序的执行顺序是:全局变量初始化→init方法→main 方法……注意,不要滥用 init 方法,如果 init方法过多,则很容易迷失在各个库的 init方法中。
在上面的应用程序中,init方法的主要作用是控制应用程序的初始化流程。在整个应用代码中只有一个 init 方法,因此在这里调用了初始化配置的方法,起到把配置文件内容映射到应用配置结构体中的作用。
6.修改服务端配置
在启动文件main.go中设置已经映射好的配置和gin的运行模式,这样在程序重新启动之后即可生效,代码如下:


7.验证
在完成了配置相关的初始化后,我们需要校验配置是否真正地映射到了配置结构体上。一般来说,可以通过断点或简单打日志的方式进行查看。最终配置的包全局变量的值如下所示:

2.3.3 数据库连接
1.安装
在本项目中,与数据库相关的数据操作将使用第三方开源库gorm。它是目前Go语言中最流行的ORM库(从GitHub Star来看),功能十分齐全,且对开发人员非常友好。安装gorm的命令如下:

另外,在社区中,也有其他的声音,例如有人认为不使用 ORM 库更好,这类的比较本书做不探讨,若想了解,可以看看database/sql扩展库。
2.编写组件
打开项目目录internal/model下的model.go文件,新增NewDBEngine方法,代码如下:


在上述代码中,编写了一个针对创建DB实例的NewDBEngine方法,同时增加了gorm开源库的引入和 MySQL 驱动库 github.com/jinzhu/gorm/dialects/mysql 的初始化(不同类型的DBType需要引入不同的驱动库,否则会存在问题)。
3.包全局变量
在项目目录下的global目录中新增db.go文件,内容如下:

4.初始化
回到启动文件,即项目目录下的main.go文件,在其中新增setupDBEngine方法的初始化,代码如下:


需要注意的是,有些人会把初始化语句不小心写成:global.DBEngine,err:=model.NewDBEngine(global.DatabaseSetting),这是存在很大问题的。由于:=会重新声明并创建左侧的新局部变量,因此在其他包中调用global.DBEngine变量时,它仍然是nil,达不到可用标准,因为在赋值时并没有赋值到真正需要赋值的包全局变量global.DBEngine上。
2.3.4 日志写入
有心的读者可能会发现,在上述应用代码中,都是直接使用Go标准库log来进行日志输出的。这其实是存在问题的,因为在一个项目中,日志需要标准化地记录一些公共信息,如代码调用堆栈、请求链路ID、公共的业务属性字段等,如果直接输出标准库的日志,则并不包含这些公共信息,使得日志不够完整。
一份完整的日志在排查和调试问题时非常重要,因此在应用程序中,是有一个标准的日志组件进行统一处理和输出日志的。
1.安装

首先拉取日志组件内需要用到的第三方开源库 lumberjack,它的核心功能是把日志写入滚动文件中,该库允许我们设置单日志文件的最大占用空间、最大生存周期、可保留的最多旧文件数等。如果有出现超出设置项的情况,则对日志文件进行滚动处理。
这个库可以减免一些文件操作类的代码编写,把核心逻辑摆在日志标准化处理上。
2.编写组件
实际上,本节中的代码均在同一个文件中,但为了便于理解,在讲解时我们会将日志组件的代码切割为多块进行剖析。
(1)日志分级。
在项目目录下的pkg/目录中新建logger目录,并创建logger.go文件,写入日志分级相关的代码:


在上述代码中,预定义了应用日志的Level和Fields的具体类型,并且把日志分为debug、info、warn、error、fatal和panic六个等级,以便在不同的使用场景中记录不同级别的日志。
(2)日志标准化。
在完成了日志的分级方法后,开始编写具体的方法,用来对日志的实例初始化和标准化参数进行绑定,继续写入如下代码:


● WithLevel:设置日志等级。
● WithFields:设置日志公共字段。
● WithContext:设置日志上下文属性。
● WithCaller:设置当前某一层调用栈的信息(程序计数器、文件信息和行号)。
● WithCallersFrames:设置当前的整个调用栈信息。
(3)日志格式化和输出。
下面开始编写日志内容的格式化和日志输出动作的相关方法,继续写入如下代码:

(4)日志分级输出。
根据先前定义的日志分级,编写对应的日志输出的外部方法,继续写入如下代码:

这里主要是根据日志的六个等级编写对应的方法,读者可自行完善,除方法名和WithLevel设置得不同外,其他代码均相同。
3.包全局变量
在编写完日志库后,还需要定义一个Logger对象,以便应用程序使用。打开项目目录下的global/setting.go文件,新增如下代码:

从上述代码中可以看出,在包全局变量中新增了Logger对象,用于日志组件的初始化。
4.初始化
修改启动文件,即项目目录下的main.go文件,对刚刚定义的Logger对象进行初始化,代码如下:


在上述代码中,在init方法中新增了日志组件的流程,并在setupLogger方法内部对global的包全局变量 Logger 进行了初始化。需要注意的是,这里使用了 lumberjack 作为日志库的io.Writer,并且将日志文件所允许的最大占用空间设置为600MB、日志文件最大生存周期为10天、日志文件名的时间格式为本地时间。
5.验证
在完成前面的步骤后,日志组件就初始化完毕了。下面开始验证,在main方法中执行下述测试代码:

查看项目目录下的storage/logs/app.log文件,看看日志文件是否正常创建且写入了预期的日志记录,内容大致如下:

2.3.5 响应处理
在应用程序中,与客户端对接的通常是服务端的接口,那么客户端是如何知道这一次的接口调用的结果是怎样的呢?一般来说,主要是通过对返回的HTTP状态码和接口返回的响应结果进行判断的,而判断的依据则是事先按规范定义好响应结果。
本节将编写统一处理接口返回的响应处理方法,该方法与错误码标准化相对应。
1.类型转换
在pkg/convert目录中新建convert.go文件,代码如下:

2.分页处理
在pkg/app目录中新建pagination.go文件,代码如下:


3.响应处理
在pkg/app目录中新建app.go文件,代码如下:


4.验证
找到其中一个接口方法,调用对应的方法,检查是否有误,代码如下:

验证响应结果,代码如下:

从响应结果可以看出,本次接口的调用结果的 HTTP 状态码为 500,响应消息体为约定的错误体,符合要求。
2.3.6 小结
在本节中,我们主要对项目的公共组件做了初始化,包括大量的规范制定、公共库编写、初始化注册等行为,虽然比较烦琐,但这这些公共组件在整个项目运行中至关重要。早期做得越标准,后期越省事,因为大家直接使用就可以了,不需要过多地关注细节,也不需要再造新的公共库轮子,做多套适配。