使用gin搭建HTTP服务
gin 是用Go
语言编写的一个高性能HTTP Web框架,通过它,我们可以快速搭建一个HTTP服务。
本文并不是一篇gin的入门教程,而是介绍如何基于gin搭建一个完善的web服务,包括代码结构的分层,接口输出的标准化,如何进行错误处理,以及使用时的注意事项。
先把服务跑起来
首先,我们先让服务可以运行起来。创建一个项目,编写main.go
,内容如下:
1 | func main() { |
紧接着,运行起来,访问http://localhost:8080/ping
,可以看到接口返回了{"message": "pong"}
。
定义项目结构
关于Go项目的标准化,可以参考Standard Go Project Layout
我们对项目结构做分层如下:
1 | project |
假如我们不希望代码被其他项目使用,可以将代码从pkg
移到internal
中。
- cmd:存放的是程序的入口,项目有时不止一个入口,那就可以在这里增加。
- configs:存放配置文件,比如数据库配置。
- doc:程序相关文档
- internal:项目代码(不对外公开)
- pkg:项目代码
- ctrl:存放控制器
- model:存放对象模型
- db:存放数据库连接
- router:存放路由器
- svc:service,存放业务逻辑
- vendor: 第三方库
更改程序入口
对应回我们前面的程序,我们进行改进。
首先,我们将目录
建立起来,然后在cmd
目录下建立server
目录,将开始时的main.go
改名server.go
放入其中:
1 | cmd |
以后的程序入口都由cmd包负责。
修改路由
在pkg/router
下面,我们创建文件engine.go
,内容如下:
1 | func Default() *gin.Engine { |
然后修改入口文件cmd/server/server.go
的内容:
1 | func main() { |
以后的接口定义,都在router
层下编写。
新增控制器
接下来,我们需要将控制器提取出来。
在pkg/ctrl
目录下,我们新增文件example.go
:
1 | type ExampleController struct{} |
编辑pkg/router/engine.go
,修改控制器指向:
1 | func Default() *gin.Engine { |
项目结构完成
按照上面的操作,我们已经定义好了项目的结构,业务代码只要按照上面的结构进行存放即可,如有需要,也可以灵活变动。
目前改造完的目录代码结构如下:
1 | project |
定制中间件
约定输出格式
后端给到前端的接口内容,我们希望格式是比较标准化的。
比如当接口返回200状态码却不带body时,我们默认输出{"message": "ok"}
,
当接口返回404却不带body时,我们默认输出{"message": "找不到资源"}
按照这样的思维,我们新建文件pkg/router/middleware.go
:
1 | func responseHandler(c *gin.Context) { |
我们定义了一个responseHandler
的gin
中间件,然后修改pkg/router/engine.go
,配置上中间件:
1 | func Default() *gin.Engine { |
为了测试效果,我们在pkg/ctrl/example.go
中加上控制器对应的内容:
1 | func (ec *ExampleController) NotFound(c *gin.Context) { |
并配置到路由中:
1 | engine.GET("/ping", ctrl.Example.Ping) |
程序执行起来访问对应的接口,你应该能够看到效果了。
标准化错误处理
当我们通过gin.Default()
得到一个引擎实例时,它默认带了错误处理功能(recovery)。
假设我们希望有自己的错误处理流程,比如程序出错时我们需要将错误栈输出到ELK
之类地方,方便我们进行错误的定位,那我们可以自定义一个错误处理中间件。
下面是一个简单的示例,在实际使用中,如果需要复杂的功能,可以在进行对应的改造。
新建文件pkg/error.go
,增加错误,并提供相应的panic
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30type Error struct {
Err error
Msg string
Code int
}
// 将错误直接抛出
func PanicIfErr(err error) {
if err != nil {
PanicError(http.StatusInternalServerError, err)
}
}
// 抛出错误时,携带状态码
func PanicError(code int, err error) {
panic(&Error{
Err: err,
Msg: "请求出错,请稍后尝试",
Code: code,
})
}
// 自定义状态码和错误信息的错误
func PanicErrorWithMsg(code int, msg string) {
panic(&Error{
Err: errors.New(msg),
Msg: msg,
Code: code,
})
}
在pkg/router/middleware.go
中,我们加上错误处理的中间件:
1 | func recovery(c *gin.Context) { |
最后,我们不使用gin
默认的中间件,将我们自己的中间件添加到路由中:
1 | func Default() *gin.Engine { |
经过这一番改造后,我们对错误处理的规则如下:
- 当程序发生意料之外的错误时,比如数据库访问出错,我们直接使用
pkg.PanicErr(err)
,将原始error抛出,中间件会处理成500错误,并提示“服务出错,请稍后尝试”,同时将信息记录到日志中; - 当程序发生在意料之中时:
- 通过
pkg.PanicErrorWithMsg(code, msg)
,自定义error的msg,这类适用于4xx的客户端错误,比如参数出错,账号不正确,没有权限等等; - 通过
pkg.PanicError(code, err)
,将原始错误抛出,同时自定义我们想要的HTTP code。
- 通过
至此,我们应该完成了一个比较简单而统一的web项目结构,这是我对于Go项目的一些应用思考,可能并不适用于所有人,希望能帮到你。
其他注意事项
路由冲突
现在的API
设计中,RESTful
架构大家怕是耳闻能熟了,应用HTTP方法和地址的巧妙设计,使用者能直接猜出接口的作用(语义化)。
当我们在使用gin
路由的时候,可能会遇到路由设计上的冲突问题(更深层的原因希望以后抽出时间来专门写一篇介绍),这里说明什么情况下会产生路由冲突。
当我们使用wildcard
参数时,就可能产生冲突,比如:
1 | GET /api/users/:id |
这两条配置实际上是冲突的,因为后者已经被前者所包括。这里的冲突有两个条件:
- 一样的HTTP方法
- 某路由通配符覆盖了其他路由
这种冲突在其他语言和框架可能并不会出现,这个时候,我们只能对API进行调整。
示例代码
关于上述的项目结构,我存放在了gin-exam,可以前往查看。
- 本文链接:https://keepmoving.ren/golang/building-webservice-with-gin/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!