用Go语言实现的博客后端
2023-3-20
| 2023-5-4
0  |  阅读时长 0 分钟
Created time
Mar 20, 2023 02:00 PM
date
status
category
Origin
summary
tags
type
URL
icon
password
slug

编写公共组件

 

配置管理

这段代码定义了一个名为Setting的结构体,用于存储应用程序的配置信息。该结构体包含一个指向viper实例的指针vp,用于读取和处理配置信息。
NewSetting函数用于创建并初始化Setting实例。在函数中,首先创建一个viper实例,然后设置要读取的配置文件名、路径和类型,接着读取配置文件,并将配置信息存储在viper实例中。如果读取配置文件失败,则返回nil和错误信息。最后,返回一个包含viper实例的Setting实例。
ReadSection函数用于从配置文件中读取指定 section 的配置信息。k参数用于指定要读取的 section 名称,v 参数是一个指向结构体的指针,用于存储从配置文件中读取的值。在提供的代码块中,k作为UnmarshalKey函数的第一个参数,用于指定要从配置文件中读取的 section 名称。
因此,在调用 ReadSection函数时,应将要读取的 section 名称作为 k参数传递。
例如,如果您有一个包含以下内容的配置文件:
您可以像这样将 server 部分读入 ServerSettingS
结构体中:
您可以像这样将database部分读入DatabaseSettingS
结构体中:

数据库连接

我们在本项目中数据库相关的数据操作将使用第三方的开源库 gorm,它是目前 Go 语言中最流行的 ORM 库(从 Github Star 来看),同时它也是一个功能齐全且对开发人员友好的 ORM 库,目前在 Github 上相当的活跃,具有一定的保障,安装命令如下:
另外在社区中,也有其它的声音,例如有认为不使用 ORM 库更好的,这类的比较本文暂不探讨,但若是想了解的话可以看看像 sqlx 这类 database/sql 的扩展库,也是一个不错的选择。
编写组件NewDBEngine:我们打开项目目录 internal/model下的 model.go 文件,新增 NewDBEngine 方法,如下:
我们通过上述代码,编写了一个针对创建 DB 实例的 NewDBEngine 方法,同时增加了 gorm 开源库的引入和 MySQL 驱动库 github.com/jinzhu/gorm/dialects/mysql的初始化(不同类型的 DBType 需要引入不同的驱动库,否则会存在问题)。
 
包全局变量:我们在项目目录下的 global 目录,新增 db.go 文件,新增如下内容:
初始化:回到启动文件,也就是项目目录下的 main.go 文件,新增 setupDBEngine 方法初始化,如下:
这里需要注意,有一些人会把初始化语句不小心写成:global.DBEngine, err := model.NewDBEngine(global.DatabaseSetting),这是存在很大问题的,因为 := 会重新声明并创建了左侧的新局部变量,因此在其它包中调用 global.DBEngine 变量时,它仍然是 nil,仍然是达不到可用标准,因为根本就没有赋值到真正需要赋值的包全局变量 global.DBEngine 上。

日志写入

如果有心的读者会发现我们在上述应用代码中都是直接使用 Go 标准库 log 来进行的日志输出,这其实是有些问题的,因为在一个项目中,我们的日志需要标准化的记录一些的公共信息,例如:代码调用堆栈、请求链路 ID、公共的业务属性字段等等,而直接输出标准库的日志的话,并不具备这些数据,也不够灵活。
日志的信息的齐全与否在排查和调试问题中是非常重要的一环,因此在应用程序中我们也会有一个标准的日志组件会进行统一处理和输出。
安装:
我们先拉取日志组件内要使用到的第三方的开源库 lumberjack,它的核心功能是将日志写入滚动文件中,该库支持设置所允许单日志文件的最大占用空间、最大生存周期、允许保留的最多旧文件数,如果出现超出设置项的情况,就会对日志文件进行滚动处理。
而我们使用这个库,主要是为了减免一些文件操作类的代码编写,把核心逻辑摆在日志标准化处理上。
编写组件:首先在这一节中,实质上代码都是在同一个文件中的,但是为了便于理解,我们会在讲解上会将日志组件的代码切割为多块进行剖析。
日志分级:我们在项目目录下的 pkg/目录新建 logger目录,并创建 logger.go 文件,写入日志分级相关的代码:
我们先预定义了应用日志的 Level 和 Fields 的具体类型,并且分为了 Debug、Info、Warn、Error、Fatal、Panic 六个日志等级,便于在不同的使用场景中记录不同级别的日志。
日志标准化:我们完成了日志的分级方法后,开始编写具体的方法去进行日志的实例初始化和标准化参数绑定,继续写入如下代码:
  • WithLevel:设置日志等级。
  • WithFields:设置日志公共字段。
  • WithContext:设置日志上下文属性。
  • WithCaller:设置当前某一层调用栈的信息(程序计数器、文件信息、行号)。
  • WithCallersFrames:设置当前的整个调用栈信息。
日志格式化和输出:我们开始编写日志内容的格式化和日志输出动作的相关方法,继续写入如下代码:
日志分级输出:我们根据先前定义的日志分级,编写对应的日志输出的外部方法,继续写入如下代码:
上述代码中仅展示了 Info、Fatal 级别的日志方法,这里主要是根据 Debug、Info、Warn、Error、Fatal、Panic 六个日志等级编写对应的方法,大家可自行完善,除了方法名以及 WithLevel 设置的不一样,其他均为一致的代码。
包全局变量:在完成日志库的编写后,我们需要定义一个 Logger 对象便于我们的应用程序使用。因此我们打开项目目录下的 global/setting.go 文件,新增如下内容:
我们在包全局变量中新增了 Logger 对象,用于日志组件的初始化。
初始化:接下来我们需要修改启动文件,也就是项目目录下的 main.go 文件,新增对刚刚定义的 Logger 对象的初始化,如下:
通过这段程序,我们在 init 方法中新增了日志组件的流程,并在 setupLogger 方法内部对 global 的包全局变量 Logger 进行了初始化,需要注意的是我们使用了 lumberjack 作为日志库的 io.Writer,并且设置日志文件所允许的最大占用空间为 600MB、日志文件最大生存周期为 10 天,并且设置日志文件名的时间格式为本地时间。
在完成了上述的步骤后,日志组件已经正式的初始化完毕了,为了验证你是否操作正确,你可以在 main 方法中执行下述测试代码:
接着可以查看项目目录下的 storage/logs/app.log ,看看日志文件是否正常创建且写入了预期的日志记录,大致如下:

响应处理

在应用程序中,与客户端对接的常常是服务端的接口,那客户端是怎么知道这一次的接口调用结果是怎么样的呢?一般来讲,主要是通过对返回的 HTTP 状态码和接口返回的响应结果进行判断,而判断的依据则是事先按规范定义好的响应结果。
因此在这一小节,我们将编写统一处理接口返回的响应处理方法,它也正正与错误码标准化是相对应的。
类型转换:在项目目录下的 pkg/convert  目录下新建 convert.go 文件,如下:
分页处理:在项目目录下的 pkg/app  目录下新建 pagination.go 文件,如下:
响应处理:在项目目录下的 pkg/app  目录下新建 app.go 文件,如下:
我们可以找到其中一个接口方法,调用对应的方法,检查是否有误,如下:
验证响应结果,如下:
从响应结果上看,可以知道本次接口的调用结果的 HTTP 状态码为 500,响应消息体为约定的错误体,符合我们的要求。
在本文中,我们主要是针对项目的公共组件初始化,做了大量的规范制定、公共库编写、初始化注册等等行为,虽然比较繁琐,这这些公共组件在整个项目运行中至关重要,早期做的越标准化,后期越省心省事,因为大家直接使用就可以了,不需要过多的关心细节,也不会有人重新再造新的公共库轮子,导致要适配多套。

生成接口文档

我们在前面的章节中完成了针对业务需求的模块和路由的设计,并且完成了公共组件的处理,初步运行也没有问题,那么这一次是不是真的就可以开始编码了呢?
其实不然,虽然我们完成了路由的设计,但是接口的定义不是一个人的事,我们在提前设计好接口的入参、出参以及异常情况后,还需要其他同事一起进行接口设计评审,以便确认本次迭代的接口设计方案是尽可能正确和共同认可的,如下图:
notion image
那如何维护接口文档,是绝大部分开发人员都经历过的问题,因为前端、后端、测试开发等等人员都要看,每个人都给一份的话,怎么维护,这将是一个非常头大的问题。在很多年以前,也流行过用 Word 等等工具写接口文档,显然,这会有许许多多的问题,后端人员所耗费的精力、文档的时效性根本无法得到保障。
针对这类问题,市面上出现了大量的解决方案,Swagger 正是其中的佼佼者,它更加的全面和完善,具有相关联的生态圈。它是基于标准的 OpenAPI 规范进行设计的,只要照着这套规范去编写你的注解或通过扫描代码去生成注解,就能生成统一标准的接口文档和一系列 Swagger 工具。
在上文我们有提到 OpenAPI,你可能会对此产生疑惑,OpenAPI 和 Swagger 又是什么关系?
其实 OpenAPI 规范是在 2015 年由 OpenAPI Initiative 捐赠给 Linux 基金会的,并且 Swagger 对此更进一步的针对 OpenAPI 规范提供了大量与之相匹配的工具集,能够充分利用 OpenAPI 规范去映射生成所有与之关联的资源和操作去查看和调用 RESTful 接口,因此我们也常说 Swagger 不仅是一个“规范”,更是一个框架。
从功能使用上来讲,OpenAPI 规范能够帮助我们描述一个 API 的基本信息,比如:
  • 有关该 API 的描述。
  • 可用路径(/资源)。
  • 在每个路径上的可用操作(获取/提交…)。
  • 每个操作的输入/输出格式。
Swagger 相关的工具集会根据 OpenAPI 规范去生成各式各类的与接口相关联的内容,常见的流程是编写注解 =》调用生成库-》生成标准描述文件 =》生成/导入到对应的 Swagger 工具。
因此接下来第一步,我们要先安装 Go 对应的开源 Swagger 相关联的库,在项目 blog-service 根目录下执行安装命令,如下:
验证是否安装成功,如下:
如果命令行提示寻找不到 swag 文件,可以检查一下对应的 bin 目录是否已经加入到环境变量 PATH 中。
写入注解: 在完成了 Swagger 关联库的安装后,我们需要针对项目里的 API 接口进行注解的编写,以便于后续在进行生成时能够正确的运行,接下来我们将使用到如下注解:
注解
描述
@Summary
摘要
@Produce
API 可以产生的 MIME 类型的列表,MIME 类型你可以简单的理解为响应类型,例如:json、xml、html 等等
@Param
参数格式,从左到右分别为:参数名、入参类型、数据类型、是否必填、注释
@Success
响应成功,从左到右分别为:状态码、参数类型、数据类型、注释
@Failure
响应失败,从左到右分别为:状态码、参数类型、数据类型、注释
@Router
路由,从左到右分别为:路由地址,HTTP 方法
API : 我们切换到项目目录下的 internal/routers/api/v1 目录,打开 tag.go 文件,写入如下注解:
在这里我们只展示了标签模块的接口注解编写,接下来你应当按照注解的含义和参考上述接口注解,完成文章模块接口注解的编写。
Main: 那么接口方法本身有了注解,那针对这个项目,能不能写注解呢,万一有很多个项目,怎么知道它是谁?实际上是可以识别出来的,我们只要针对 main 方法写入如下注解:
生成: 在完成了所有的注解编写后,我们回到项目根目录下,执行如下命令:
在执行命令完毕后,会发现在 docs 文件夹生成 docs.go、swagger.json、swagger.yaml 三个文件。
路由: 那注解编写完,也通过 swag init 把 Swagger API 所需要的文件都生成了,那接下来我们怎么访问接口文档呢?其实很简单,我们只需要在 routers 中进行默认初始化和注册对应的路由就可以了,打开项目目录下的 internal/routers 目录中的 router.go 文件,新增代码如下:
从表面上来看,主要做了两件事,分别是初始化 docs 包和注册一个针对 swagger 的路由,而在初始化 docs 包后,其 swagger.json 将会默认指向当前应用所启动的域名下的 swagger/doc.json 路径,如果有额外需求,可进行手动指定,如下:
查看接口文档:
notion image
在完成了上述的设置后,我们重新启动服务端,在浏览器中访问 Swagger 的地址 http://127.0.0.1:8000/swagger/index.html,就可以看到上述图片中的 Swagger 文档展示,其主要分为三个部分,分别是项目主体信息、接口路由信息、模型信息,这三部分共同组成了我们主体内容。
 
可能会疑惑,我明明只是初始化了个 docs 包并注册了一个 Swagger 相关路由,Swagger 的文档是怎么关联上的呢,我在接口上写的注解又到哪里去了?
其实主体是与我们在章节 2.4.4 生成的文件有关的,分别是:
初始化 docs: 在第一步中,我们初始化了 docs 包,对应的其实就是 docs.go 文件,因为目录下仅有一个 go 源文件,其源码如下:
通过对源码的分析,我们可以得知实质上在初始化 docs 包时,会默认执行 init 方法,而在 init 方法中,会注册相关方法,主体逻辑是 swag 会在生成时去检索项目下的注解信息,然后将项目信息和接口路由信息按规范生成到包全局变量 doc 中去。
紧接着会在 ReadDoc 方法中做一些 template 的模板映射等工作,完善 doc 的输出。

注册路由

在上一步中,我们知道了生成的注解数据源在哪,但是它们两者又是怎么关联起来的呢,实际上与我们调用的 ginSwagger.WrapHandler(swaggerFiles.Handler) 有关,如下:
 
实际上在调用 WrapHandler 后,swag 内部会将其默认调用的 URL 设置为 doc.json,但你可能会纠结,明明我们生成的文件里没有 doc.json,这又是从哪里来的,我们接着往下看,如下:
在 CustomWrapHandler 方法中,我们可以发现一处比较经典 switch case 的逻辑。
在第一个 case 中,处理是的 index.html,这又是什么呢,其实你可以回顾一下,我们在先前是通过 http://127.0.0.1:8000/swagger/index.html 访问到 Swagger 文档的,对应的便是这里的逻辑。
在第二个 case 中,就可以大致解释我们所关注的 doc.json 到底是什么,它相当于一个内部标识,会去读取我们所生成的 Swagger 注解,你也可以发现我们先前在访问的 Swagger 文档时,它顶部的文本框中 Explore 默认的就是 doc.json(也可以填写外部地址,只要输出的是对应的 Swagger 注解)。
 
 
细心的读者可能会发现,我们先前在公共组件的章节已经定义好了一些基本类型的 Response 返回值,但我们在本章节编写成功响应时,是直接调用 model 作为其数据类型,如下:
这样写的话,就会有一个问题,如果有 model.Tag 以外的字段,例如分页,那就无法展示了。更接近实践来讲,大家在编码中常常会遇到某个对象内中的某一个字段是 interface,这个字段的类型它是不定的,也就是公共结构体,那注解又应该怎么写呢,如下情况:
可能会有的人会忽略它,采取口头说明,但这显然是不完备的。而 swag 目前在 v1.6.3 也没有特别好的新注解方式,官方在 issue 里也曾表示过通过注解来解决这个问题是不合理的,那我们要怎么做呢?
实际上,官方给出的建议很简单,就是定义一个针对 Swagger 的对象,专门用于 Swagger 接口文档展示,我们在 internal/model 的 tag.go 和 article.go 文件中,新增如下代码:
我们修改接口方法中对应的注解信息,如下:
接下来你只需要在项目根目录下再次执行 swag init,并在生成成功后再重新启动服务端,就可以查看到最新的效果了,如下:
notion image
在本章节中,我们简单介绍了 Swagger 和 Swagger 的相关生态圈组件,对所编写的 API 原型新增了响应的 Swagger 注解,在接下来中安装了针对 Go 语言的 Swagger 工具,用于后续的 Swagger 文档生成和使用。

为接口做参数校验

接下来我们将正式进行编码,在进行对应的业务模块开发时,第一步要考虑到的问题的就是如何进行入参校验,我们需要将整个项目,甚至整个团队的组件给定下来,形成一个通用规范,在今天本章节将核心介绍这一块,并完成标签模块的接口的入参校验。

validator 介绍

在本项目中我们将使用开源项目 go-playground/validator 作为我们的本项目的基础库,它是一个基于标签来对结构体和字段进行值验证的一个验证器。
那么,我们要单独引入这个库吗,其实不然,因为我们使用的 gin 框架,其内部的模型绑定和验证默认使用的是 go-playground/validator 来进行参数绑定和校验,使用起来非常方便。
在项目根目录下执行命令,进行安装:

业务接口校验

接下来我们将正式开始对接口的入参进行校验规则的编写,也就是将校验规则写在对应的结构体的字段标签上,常见的标签含义如下:
标签
含义
required
必填
gt
大于
gte
大于等于
lt
小于
lte
小于等于
min
最小值
max
最大值
oneof
参数集内的其中之一
len
长度要求与 len 给定的一致

标签接口

我们回到项目的 internal/service目录下的 tag.go 文件,针对入参校验增加绑定/验证结构体,在路由方法前写入如下代码:
在上述代码中,我们主要针对业务接口中定义的的增删改查和统计行为进行了 Request 结构体编写,而在结构体中,应用到了两个 tag 标签,分别是 form 和 binding,它们分别代表着表单的映射字段名和入参校验的规则内容,其主要功能是实现参数绑定和参数检验。

文章接口

接下来到项目的 internal/service  目录下的 article.go 文件,针对入参校验增加绑定/验证结构体。这块与标签模块的验证规则差不多,主要是必填,长度最小、最大的限制,以及要求参数值必须在某个集合内的其中之一,因此不再赘述。

国际化处理

编写中间件

go-playground/validator 默认的错误信息是英文,但我们的错误信息不一定是用的英文,有可能要简体中文,做国际化的又有其它的需求,这可怎么办,在通用需求的情况下,有没有简单又省事的办法解决呢?
如果是简单的国际化需求,我们可以通过中间件配合语言包的方式去实现这个功能,接下来我们在项目的 internal/middleware目录下新建 translations.go 文件,用于编写针对 validator 的语言包翻译的相关功能,新增如下代码:
在自定义中间件 Translations 中,我们针对 i18n 利用了第三方开源库去实现这块功能,分别如下:
  • go-playground/locales:多语言包,是从 CLDR 项目(Unicode 通用语言环境数据存储库)生成的一组多语言环境,主要在 i18n 软件包中使用,该库是与 universal-translator 配套使用的。
  • go-playground/universal-translator:通用翻译器,是一个使用 CLDR 数据 + 复数规则的 Go 语言 i18n 转换器。
  • go-playground/validator/v10/translations:validator 的翻译器。
而在识别当前请求的语言类别上,我们通过 GetHeader 方法去获取约定的 header 参数 locale,用于判别当前请求的语言类别是 en 又或是 zh,如果有其它语言环境要求,也可以继续引入其它语言类别,因为 go-playground/locales 基本上都支持。
在后续的注册步骤,我们调用 RegisterDefaultTranslations 方法将验证器和对应语言类型的 Translator 注册进来,实现验证器的多语言支持。同时将 Translator 存储到全局上下文中,便于后续翻译时的使用。

注册中间件

回到项目的 internal/routers  目录下的 router.go 文件,新增中间件 Translations 的注册,新增代码如下:
至此,我们就完成了在项目中的自定义验证器注册、验证器初始化、错误提示多语言的功能支持了。

接口校验

我们在项目下的 pkg/app  目录新建 form.go 文件,写入如下代码:
在上述代码中,我们主要是针对入参校验的方法进行了二次封装,在 BindAndValid 方法中,通过 ShouldBind 进行参数绑定和入参校验,当发生错误后,再通过上一步在中间件 Translations 设置的 Translator 来对错误消息体进行具体的翻译行为。
另外我们声明了 ValidError 相关的结构体和类型,对这块不熟悉的读者可能会疑惑为什么要实现其对应的 Error 方法呢,我们简单来看看标准库中 errors 的相关代码,如下:
标准库 errors 的 New 方法实现非常简单,errorString 是一个结构体,内含一个 s 字符串,也只有一个 Error 方法,就可以认定为 error 类型,这是为什么呢?这一切的关键都在于 error 接口的定义,如下:
在 Go 语言中,如果一个类型实现了某个 interface 中的所有方法,那么编译器就会认为该类型实现了此 interface,它们是”一样“的。

验证

我们回到项目的 internal/routers/api/v1  下的 tag.go 文件,修改获取多个标签的 List 接口,用于验证 validator 是否正常,修改代码如下:
在命令行中利用 CURL 请求该接口,查看验证结果,如下:
另外你还需要注意到 TagListRequest 的校验规则里其实并没有 required,因此它的校验规则应该是有才校验,没有该入参的话,是默认无校验的,也就是没有 state 参数,也应该可以正常请求,如下:
在 Response 中我们调用的是 gin.H  作为返回结果集,因此该输出结果正确。
 
在本章节中,我们介绍了在 gin 框架中如何通过 validator 来进行参数校验,而在一些定制化场景中,我们常常需要自定义验证器,这个时候我们可以通过实现 binding.Validator  接口的方式,来替换其自身的 validator::
也就是说如果你有定制化需求,也完全可以自己实现一个验证器,效仿我们前面的模式,就可以完全替代 gin 框架原本的 validator 使用了。
而在章节的后半段,我们对业务接口进行了入参校验规则的编写,并且针对错误提示的多语言化问题(也可以理解为一个简单的国际化需求),通过中间件和多语言包的方式进行了实现,在未来如果你有更细致的国际化需求,也可以进一步的拓展。

模块开发:标签管理

在初步完成了业务接口的入参校验的逻辑处理后,接下来我们正式的进入业务模块的业务逻辑开发,在本章节将完成标签模块的接口代码编写,涉及的接口如下:
功能
HTTP 方法
路径
新增标签
POST
/tags
删除指定标签
DELETE
/tags/:id
更新指定标签
PUT
/tags/:id
获取标签列表
GET
/tags

新建 model 方法

首先我们需要针对标签表进行处理,并在项目的 internal/model  目录下新建 tag.go 文件,针对标签模块的模型操作进行封装,并且只与实体产生关系,代码如下:
  • Model:指定运行 DB 操作的模型实例,默认解析该结构体的名字为表名,格式为大写驼峰转小写下划线驼峰。若情况特殊,也可以编写该结构体的 TableName 方法用于指定其对应返回的表名。
  • Where:设置筛选条件,接受 map,struct 或 string 作为条件。
  • Offset:偏移量,用于指定开始返回记录之前要跳过的记录数。
  • Limit:限制检索的记录数。
  • Find:查找符合筛选条件的记录。
  • Updates:更新所选字段。
  • Delete:删除数据。
  • Count:统计行为,用于统计模型的记录数。
需要注意的是,在上述代码中,我们采取的是将 db *gorm.DB  作为函数首参数传入的方式,而在业界中也有另外一种方式,是基于结构体传入的,两者本质上都可以实现目的,读者根据实际情况(使用习惯、项目规范等)进行选用即可,其各有利弊。

处理 model 回调

单点登录身份鉴权系统 casdoor 源码阅读7天用Go从零实现Web框架Gee教程
Loading...