本篇博客主要介绍了如何从零开始,使用Go Module作为依赖管理,基于Gin来一步一步搭建Go的Web服务器。并使用Endless来使服务器平滑重启,使用Swagger来自动生成Api文档。
源码在此处:项目源码
大家可以先查看源码,然后再根据本篇文章,来了解搭建过程中服务器的一些细节。
搭建环境
以下所有的步骤都基于MacOS。
安装go
在这里推荐使用homebrew进行安装。当然你也可以使用源码安装。1
brew install go
跑完命令之后,在命令行输入go
。如果在命令行看到如下输出,则代表安装成功。1
2
3
4
5
6Go is a tool for managing Go source code.
Usage:
go <command> [arguments]
The commands are:
...
...
需要注意的是,go的版本需要在1.11
之上,否则无法使用go module。以下是我的go的版本。1
2go version
# go version go1.12.5 darwin/amd64
IDE
推荐使用GoLand
配置GOPATH
打开GoLand,在GoLand的设置中找到Global GOPATH,将其设置为$HOME/go
。$HOME
目录就是你的电脑的用户目录,如果该目录下没有go
目录的话,也不需要新建,当我们在后面的操作中初始化模块的时候,会自动的在用户目录下新建go目录。
启用GO Module
同样,在GoLand中设置中找到Go Modules (vgo)。勾选Enable Go Modules (vgo) integration前的选择框来启用Go Moudle
搭建项目框架
新建目录
在你常用的工作区新建一个目录,如果你有github的项目,可以直接clone下来。
初始化go module
1 | go mod init $MODULE_NAME |
在刚刚新建的项目的根目录下,使用上述命令来初始化go module。该命令会在项目根目录下新建一个go.mod的文件。
如果你的项目是从github上clone下来的,$MODULE_NAME
这个参数就不需要了。它会默认为github.com/$GITHUB_USER_NAME/$PROJECT_NAME
。
例如本项目就是github.com/detectiveHLH/go-backend-starter
;如果是在本地新建的项目,则必须要加上最后一个参数。否则就会遇到如下的错误。
1 | go: cannot determine module path for source directory /Users/hulunhao/Projects/go/test/src (outside GOPATH, no import comments) |
初始化完成之后的go.mod
文件内容如下。
1 | module github.com/detectiveHLH/go-backend-starter |
新建main.go
在项目的根目录下新建main.go。代码如下。
1 | package main |
运行main.go
在根目录下使用go run main.go
,如果看到命令行中输出This works
则代表基础的框架已经搭建完成。接下来我们开始将Gin引入框架。
引入Gin
Gin是一个用Go实现的HTTP Web框架,我们使用Gin来作为starter的Base Framework。
安装Gin
直接通过go get命令来安装
1 | go get github.com/gin-gonic/gin |
安装成功之后,我们可以看到go.mod文件中的内容发生了变化。
并且,我们在设定的GOPATH下,并没有看到刚刚安装的依赖。实际上,依赖安装到了$GOPATH/pkg/mod下。
1 | module github.com/detectiveHLH/go-backend-starter |
同时,也生成了一个go.sum文件。内容如下。
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
用过Node的人都知道,在安装完依赖之后会生成一个package-lock.json文件,来锁定依赖的版本。以防止后面重新安装依赖时,安装了新的版本,但是与现有的代码不兼容,这会带来一些不必要的BUG。
但是这个go.sum文件并不是这个作用。我们可以看到go.mod中只记录了一个Gin的依赖,而go.sum中则有非常多。是因为go.mod中只记录了最顶层,就是我们直接使用命令行安装的依赖。但是要知道,一个开源的包通常都会依赖很多其他的依赖包。
而go.sum就是记录所有顶层和其中间接依赖的依赖包的特定版本的文件,为每一个依赖版本生成一个特定的哈希值,从而在一个新环境启用该项目时,可以做到对项目依赖的100%还原。go.sum还会保留一些过去使用过的版本的信息。
在go module下,不需要vendor目录来保证可重现的构建,而是通过go.mod文件来对项目中的每一个依赖进行精确的版本管理。
如果之前的项目用的是vendor,那么重新用go.mod重新编写不太现实。我们可以使用go mod vendor
命令将之前项目所有的依赖拷贝到vendor目录下,为了保证兼容性,在vendor目录下的依赖并不像go.mod一样。拷贝之后的目录不包含版本号。
而且通过上面安装gin可以看出,通常情况下,go.mod文件是不需要我们手动编辑的,当我们执行完命令之后,go.mod也会自动的更新相应的依赖和版本号。
下面我们来了解一下go mod的相关命令。
- init 初始化go module
- download 下载go.mod中的依赖到本地的缓存目录中($GOPATH/pkg/mod)下
- edit 编辑go.mod,通过命令行手动升级和获取依赖
- vendor 将项目依赖拷贝到vendor下
- tidy 安装缺少的依赖,舍弃无用的依赖
- graph 打印模块依赖图
- verify 验证依赖是否正确
还有一个命令值得提一下,go list -m all
可以列出当前项目的构建列表。
修改main.go
修改main.go的代码如下。
1 | package main |
上述的代码引入了路由,熟悉Node的应该可以看出,这个与koa-router的用法十分相似。
启动服务器
照着上述运行main.go的步骤,运行main.go。就可以在控制台看到如下的输出。
1 | This works. |
此时,服务器已经在8080端口启动了。然后在浏览器中访问http://localhost:8080/hello,就可以看到服务器的正常返回。同时,服务器这边也会打印相应的日志。
1 | [GIN] 2019/06/08 - 17:41:34 | 200 | 214.213µs | ::1 | GET /hello |
构建路由
新建路由模块
在根目录下新建router目录。在router下,新建router.go文件,代码如下。
1 | package router |
在这个文件中,导出了一个InitRouter函数,该函数返回gin.Engine类型。该函数还定义了一个路由为/api/v1/hello的GET请求。
在main函数中引入路由
将main.go的代码改为如下。
1 | package main |
然后运行main.go,启动之后,访问http://localhost:8080/api/v1/hello,可以看到,与之前访问/hello路由的结果是一样的。
到此为止,我们已经拥有了一个拥有简单功能的Web服务器。那么问题来了,这样的一个开放的服务器,只要知道了地址,你的服务器就知道暴露给其他人了。这样会带来一些安全隐患。所以我们需要给接口加上鉴权,只有通过认证的调用方,才有权限调用服务器接口。所以接下来,我们需要引入JWT。
引入JWT鉴权
使用go get命令安装jwt-go依赖。
1 | go get github.com/dgrijalva/jwt-go |
新建jwt鉴权文件
在根目录下新建middleware/jwt目录,在jwt目录下新建jwt.go文件,代码如下。
1 | package jwt |
引入常量
此时,代码中会有错误,是因为我们没有声明consts这个包,其中的变量SUCCESS、INVALID_PARAMS和ERROR_AUTH_CHECK_TOKEN_FAIL是未定义的。根据code获取服务器返回信息的函数GetMsg也没定义。同样没有定义的还有util.ParseToken(token)和claims.ExpiresAt。所以我们要新建consts包。我们在根目录下新建consts目录,并且在consts目录下新建code.go,将定义好的一些常量引进去,代码如下。
新建const文件
1 | const ( |
新建message文件
再新建message.go文件,代码如下。
1 | var MsgFlags = map[int]string{ |
新建util
在根目录下新建util,并且在util下新建jwt.go,代码如下。
1 | package util |
新建setting包
在上面的util中,setting包并没有定义,所以在这个步骤中我们需要定义setting包。
使用go get命令安装依赖。
1 | go get gopkg.in/ini.v1 |
在项目根目录下新建setting目录,并在setting目录下新建setting.go文件,代码如下。
1 | package setting |
新建配置文件
在项目根目录下新建config目录,并新建app.ini文件,内容如下。
1 | [app] |
实现登录接口
新增登录接口
到此为止,通过jwt token进行鉴权的逻辑已经全部完成,剩下的就需要实现登录接口来将token在用户登录成功之后返回给用户。
使用go get命令安装依赖。
1 | go get github.com/astaxie/beego/validation |
在router下新建login.go,代码如下。
1 | package router |
新增返回类
在util包下新增response.go文件,代码如下。
1 | package util |
新增鉴权逻辑
除了返回类,login.go中还有关键的鉴权逻辑还没有实现。在根目录下新建service/authentication目录,在该目录下新建auth.go文件,代码如下。
1 | package authentication |
在此处,需要自己真正的根据业务去实现对用户调用接口的合法性校验。例如,可以根据用户的用户名和密码去数据库做验证。
修改router.go
修改router.go中的代码如下。
1 | package router |
可以看到,我们在路由文件中加入了/login接口,并使用了我们自定义的jwt鉴权的中间件。只要是在v1下的路由,请求之前都会先进入jwt中进行鉴权,鉴权通过之后才能继续往下执行。
运行main.go
到此,我们使用go run main.go
启动服务器,访问http://localhost:8080/api/v1/hello会遇到如下错误。
1 | { |
这是因为我们加入了鉴权,凡是需要鉴权的接口,都需要带上参数token。而要获取token则必须要先要登录,假设我们的用户名是Tom,密码是123。以此来调用登录接口。
1 | http://localhost:8080/login?username=Tom&password=123 |
在浏览器中访问如上的url之后,可以看到返回如下。
1 | { |
有了token之后,我们再调用hello接口,可以看到数据正常的返回了。
1 | http://localhost:8080/api/v1/hello?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlRvbSIsInBhc3N3b3JkIjoiMTIzIiwiZXhwIjoxNTYwMTM5MTE3LCJpc3MiOiJnby1iYWNrZW5kLXN0YXJ0ZXIifQ.I-RSi-xVV1Tk_2iBWolF1u94Y7oVBQXnHh6OI2YKJ6U |
一般的处理方法是,前端拿到这个token,利用持久化存储存下来,然后之后的每次请求都将token写在header中发给后端。后端先通过header中的token来校验调用接口的合法性,验证通过之后才进行真正的接口调用。
而在这我将token写在了request param中,只是为了做一个例子来展示。
引入swagger
完成了基本的框架之后,我们就开始为接口引入swagger文档。写过java的同学应该对swagger不陌生。往常写API文档,都是手写。即每个接口的每一个参数,都需要手打。
而swagger不一样,swagger只需要你在接口上打上几个注解(Java中的操作),就可以自动为你生成swagger文档。而在go中,我们是通过注释的方式来实现的,接下来我们安装gin-swagger。
安装依赖
1 | go get github.com/swaggo/gin-swagger |
在router中注入swagger
引入依赖之后,我们需要在router/router.go中注入swagger。在import中加入_ "github.com/detectiveHLH/go-backend-starter/docs"
。
并在router := gin.New()
之后加入如下代码。
1 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) |
为接口编写swagger注释
在router/login.go中的Login函数上方加上如下注释。
1 | // @Summary 登录 |
初始化swagger
在项目根目录下使用swag init
命令来初始化swagger文档。该命令将会在项目根目录生成docs目,内容如下。
1 | . |
查看swagger文档
运行main.go,然后在浏览器访问http://localhost:8080/swagger/index.html就可以看到swagger根据注释自动生成的API文档了。
引入Endless
安装Endless
1 | go get github.com/fvbock/endless |
修改main.go
1 | package main |
写在后面
对比起没有go module的依赖管理,现在的go module更像是Node.js中的package.json,也像是Java中的pom.xml,唯一不同的是pom.xml需要手动更新。
当我们拿到有go module项目的时候,不用担心下来依赖时,因为版本问题可能导致的一些兼容问题。直接使用go mod中的命令就可以将制定了版本的依赖全部安装,其效果类似于Node.js中的npm install
。
go module定位module的方式,与Node.js寻找依赖的逻辑一样,Node会从当前命令执行的目录开始,依次向上查找node_modules中是否有这个依赖,直到找到。go则是依次向上查找go.mod文件,来定位一个模块。
相信之后go之后的依赖管理,会越来越好。
Happy hacking.
参考:
往期文章:
相关:
- 个人网站: Lunhao Hu
- 微信公众号: SH的全栈笔记(或直接在添加公众号界面搜索微信号LunhaoHu)