Gin实践 连载一 Golang介绍与环境安装
若有任何问题或建议,欢迎及时交流和碰撞。我的公众号是 【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy。
Golang介绍与环境安装
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance — up to 40 times faster. If you need smashing performance, get yourself some Gin.
Gin是用Golang开发的一个微框架,类似Martinier的API,重点是小巧、易用、性能好很多,也因为 httprouter 的性能提高了40倍。
准备环节
一、安装Golang
首先,根据对应的操作系统选择安装包下载,
在这里我使用的是Centos 64位系统
wget https://studygolang.com/dl/golang/go1.9.2.linux-amd64.tar.gz
tar -zxvf go1.9.2.linux-amd64.tar.gz
mv go/ /usr/local/
配置 /etc/profile
vi /etc/profile
添加环境变量GOROOT和将GOBIN添加到PATH中
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
添加环境变量GOPATH(这个可按实际情况设置目录位置)
export GOPATH=/usr/local/go/path
配置完毕后,执行命令令其生效
source /etc/profile
在控制台输入go version
,若输出版本号则安装成功
那么大家会有些疑问,纠结go
本身有什么东西,我们刚刚设置的环境变量是什么?
1、 go
本身有什么东西
首先,我们在解压的时候会得到一个名为go
的文件夹,其中包括了所有Go
语言相关的一些文件,在这下面又包含很多文件夹和文件,我们来简单说明其中主要文件夹的作为:
- api:用于存放依照
Go
版本顺序的API增量列表文件。这里所说的API包含公开的变量、常量、函数等。这些API增量列表文件用于Go
语言API检查 - bin:用于存放主要的标准命令文件(可执行文件),包含
go
、godoc
、gofmt
- blog:用于存放官方博客中的所有文章
- doc:用于存放标准库的HTML格式的程序文档。我们可以通过
godoc
命令启动一个Web程序展示这些文档 - lib:用于存放一些特殊的库文件
- misc:用于存放一些辅助类的说明和工具
- pkg:用于存放安装
Go
标准库后的所有归档文件(以.a
结尾的文件)。注意,你会发现其中有名称为linux_amd64
的文件夹,我们称为平台相关目录。这类文件夹的名称由对应的操作系统和计算架构的名称组合而成。通过go install
命令,Go
程序会被编译成平台相关的归档文件存放到其中 - src:用于存放
Go
自身、Go
标准工具以及标准库的所有源码文件 - test:存放用来测试和验证
Go
本身的所有相关文件
2、 刚刚设置的环境变量是什么
- GOROOT:
Go
的根目录 - GOPATH:用户工作区
- PATH下增加$GOROOT/bin:
Go
的bin
下会存放可执行文件,我们把他加入PATH中就可以直接在命令行使用
3、 工作区是什么?
这在Go
中是一个非常重要的概念,在一般情况下,Go
源码文件必须放在工作区中,也就是说,我们写的项目代码都必须放在我们所设定的工作区中,虽然对于命令源码文件来说,这不是必须的。但我们大多都是前一种情况。工作区其实就是一个对应特定工程的目录,它应包含3个子目录:src
目录、pkg
目录、bin
目录
- src:用于以代码包的形式组织并保存Go源码文件
- pkg:用于存放通过
go install
命令安装后的代码包的归档文件(.a 结尾的文件) - bin:与pkg目录类似,在通过
go install
命令完成安装后,保存由Go命令源码文件生成的可执行文件
4、 什么是命令源码文件?
如果一个源码文件被声明属于main
代码包,且该文件代码中包含无参数声明和结果声明的main
函数,则它就是命令源码文件。命令源码文件可通过go run
命令直接启动运行
二、安装Govendor
If using go1.5, ensure GO15VENDOREXPERIMENT=1 is set.
在命令行下执行安装
go get -u github.com/kardianos/govendor
等待一会,安装成功后。
我们cd /usr/local/go/path
(第三方依赖包,会默认安装在GOPATH的第一个目录下)目录,
执行ls
,可以在工作区中看到bin
、pkg
、src
三个目录。这就是我们上面一小节所说的工作区了!
那么,我们所安装的govendor去哪里了呢?
答案就在工作区内,所生成的代码包大概是这样。我们所需要的是编译好的可执行文件,在/usr/local/go/path/bin
中。
path/ ├── bin │ └── govendor ├── pkg │ └── linux_amd64 │ └── github.com │ └── kardianos │ └── govendor │ ├── ... └── src └── github.com └── kardianos └── govendor ├── ...
大家还记得我们先前在环境变量PATH
中设置了GOBIN,
我们现在要做的就是把工作区中bin
目录下的可执行文件govendor
给移动过去,或者你可以将$GOPATH的BIN目录给加入环境变量中
那样就可以直接在命令行直接执行govendor
了
mv /usr/local/go/path/bin/govendor /usr/local/go/bin/
移动成功后,在命令行执行govendor -version
,若出现版本号,则成功
#govendor -version
$ v1.0.9
在这里为什么单独挑出一节来讲govendor
呢?
大家可以想想,虽然我们在本地开发,利用$GOPATH
达到安装第三方依赖包,然后去使用他,似乎也没有什么问题。
但是在实际的多人协作及部署中是有问题的
- 每一个新来的人都要
go get
很多次 - 拉下来的版本还可能不一样
- 线上部署更麻烦了
因此我们在这简单的使用govendor
来解决这个问题,在这个项目完成的最后,你只需govendor init
再govendor add +external
就能美滋滋的把依赖包都放到项目的vendor
目录中,就能把它一同传上你的版本库里了,是不是很方便呢。
当然了,目前官方推荐的包管理工具就有十几种,大家可以适当考察一下,这个不在本篇的范围内。
三、安装Gin
在命令行下执行安装
go get -u github.com/gin-gonic/gin
检查/usr/local/go/path
中是否存在gin
的代码包
四、测试Gin是否安装成功
编写一个test.go
文件
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
执行test.go
go run test.go
访问$HOST:8080/ping,若返回{"message":"pong"}
则正确
curl 127.0.0.1:8080/ping
至此,我们的环境安装都基本完成了:)
具体gin
的介绍从连载二开始,会讲解Demo所涉及的知识点!
Gin实践 连载二 搭建Blog API’s(一)

若有任何问题或建议,欢迎及时交流和碰撞。我的公众号是 【脑子进煎鱼了】,GitHub 地址:https://github.com/eddycjy。
Gin搭建Blog API’s (一)
项目地址:https://github.com/EDDYCJY/go…
思考
首先,在一个初始项目开始前,大家都要思考一下
- 各种的程序配置写在代码中,好吗
- API的错误码硬编在程序中,合适吗
- db句柄谁都去
Open
,好吗 - 获取分页等公共参数,不统一管理起来,好吗
显然在较正规的项目中,这些问题的答案都是不可以
为了解决这些问题,我们挑选一款读写配置文件的库,本系列中选用go-ini/ini ,它的中文文档。大家需要先简单阅读它的文档,再接着完成后面的内容。
我们还会编写一个简单的API错误码包,并且完成一个Demo示例和讲解知识点,便于后面的学习。
介绍和初始化项目
初始工作区
首先,我们需要增加一个工作区(GOPATH)路径用于我们的Blog
项目。
将你新的工作区加入到/etc/profile
中的GOPATH
环境变量中, 并在新工作区中,建立bin
、pkg
、src
三个目录。
在src
目录下创建gin-blog
目录,初始的目录结构:
$GOPATH
├── bin
├── pkg
└── src
└── gin-blog
初始化项目目录
gin-blog/ ├── conf ├── middleware ├── models ├── pkg ├── routers └── runtime
- conf:用于存储配置文件
- middleware:应用中间件
- models:应用数据库模型
- pkg:第三方包
- routers 路由逻辑处理
- runtime 应用运行时数据
初始项目数据库
新建blog
数据库,编码为utf8_general_ci
在blog
数据库下,新建以下表
1、 标签表
CREATE TABLE `blog_tag` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT '' COMMENT '标签名称',
`created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
`modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
`deleted_on` int(10) unsigned DEFAULT '0',
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';
2、 文章表
CREATE TABLE `blog_article` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',
`title` varchar(100) DEFAULT '' COMMENT '文章标题',
`desc` varchar(255) DEFAULT '' COMMENT '简述',
`content` text,
`created_on` int(11) DEFAULT NULL,
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
`modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
`deleted_on` int(10) unsigned DEFAULT '0',
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
3、 认证表
CREATE TABLE `blog_auth` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT '' COMMENT '账号',
`password` varchar(50) DEFAULT '' COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');
编写项目配置包
拉取go-ini/ini
的依赖包
go get -u github.com/go-ini/ini
我们需要编写基础的应用配置文件,在gin-blog
的conf
目录下新建app.ini
文件,写入内容:
#debug or release
RUN_MODE = debug
[app]
PAGE_SIZE = 10
JWT_SECRET = 23347$040412
[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60
[database]
TYPE = mysql
USER = 数据库账号
PASSWORD = 数据库密码
#127.0.0.1:3306
HOST = 数据库IP:数据库端口号
NAME = blog
TABLE_PREFIX = blog_
建立调用配置的setting
模块,在gin-blog
的pkg
目录下新建setting
目录,新建setting.go
文件,写入内容:
package setting
import (
"log"
"time"
"github.com/go-ini/ini"
)
var (
Cfg *ini.File
RunMode string
HTTPPort int
ReadTimeout time.Duration
WriteTimeout time.Duration
PageSize int
JwtSecret string
)
func init() {
var err error
Cfg, err = ini.Load("conf/app.ini")
if err != nil {
log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
}
LoadBase()
LoadServer()
LoadApp()
}
func LoadBase() {
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
}
func LoadServer() {
sec, err := Cfg.GetSection("server")
if err != nil {
log.Fatalf("Fail to get section 'server': %v", err)
}
HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
}
func LoadApp() {
sec, err := Cfg.GetSection("app")
if err != nil {
log.Fatalf("Fail to get section 'app': %v", err)
}
JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
PageSize = sec.Key("PAGE_SIZE").MustInt(10)
}
当前的目录结构:
gin-blog/
├── conf
│ └── app.ini
├── middleware
├── models
├── pkg
│ └── setting
│ └── setting.go
├── routers
├── runtime
编写API错误码包
建立错误码的e
模块,在gin-blog
的pkg
目录下新建e
目录,新建code.go
和msg.go
文件,写入内容:
1、 code.go:
package e
const (
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
ERROR_EXIST_TAG = 10001
ERROR_NOT_EXIST_TAG = 10002
ERROR_NOT_EXIST_ARTICLE = 10003
ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
ERROR_AUTH_TOKEN = 20003
ERROR_AUTH = 20004
)
2、 msg.go:
package e
var MsgFlags = map[int]string {
SUCCESS : "ok",
ERROR : "fail",
INVALID_PARAMS : "请求参数错误",
ERROR_EXIST_TAG : "已存在该标签名称",
ERROR_NOT_EXIST_TAG : "该标签不存在",
ERROR_NOT_EXIST_ARTICLE : "该文章不存在",
ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鉴权失败",
ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超时",
ERROR_AUTH_TOKEN : "Token生成失败",
ERROR_AUTH : "Token错误",
}
func GetMsg(code int) string {
msg, ok := MsgFlags[code]
if ok {
return msg
}
return MsgFlags[ERROR]
}
编写工具包
在gin-blog
的pkg
目录下新建util
目录,
拉取com
的依赖包
go get -u github.com/Unknwon/com
编写分页页码的获取方法
在util
目录下新建pagination.go
,写入内容:
package util
import (
"github.com/gin-gonic/gin"
"github.com/Unknwon/com"
"gin-blog/pkg/setting"
)
func GetPage(c *gin.Context) int {
result := 0
page, _ := com.StrTo(c.Query("page")).Int()
if page > 0 {
result = (page - 1) * setting.PageSize
}
return result
}
编写models init
拉取gorm
的依赖包
go get -u github.com/jinzhu/gorm
拉取mysql
驱动的依赖包
go get -u github.com/go-sql-driver/mysql
完成后,在gin-blog
的models
目录下新建models.go
,用于models
的初始化使用
package models
import (
"log"
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
"gin-blog/pkg/setting"
)
var db *gorm.DB
type Model struct {
ID int `gorm:"primary_key" json:"id"`
CreatedOn int `json:"created_on"`
ModifiedOn int `json:"modified_on"`
}
func init() {
var (
err error
dbType, dbName, user, password, host, tablePrefix string
)
sec, err := setting.Cfg.GetSection("database")
if err != nil {
log.Fatal(2, "Fail to get section 'database': %v", err)
}
dbType = sec.Key("TYPE").String()
dbName = sec.Key("NAME").String()
user = sec.Key("USER").String()
password = sec.Key("PASSWORD").String()
host = sec.Key("HOST").String()
tablePrefix = sec.Key("TABLE_PREFIX").String()
db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
user,
password,
host,
dbName))
if err != nil {
log.Println(err)
}
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
return tablePrefix + defaultTableName;
}
db.SingularTable(true)
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
}
func CloseDB() {
defer db.Close()
}
编写项目启动、路由文件
最基础的准备工作完成啦,让我们开始编写Demo吧!
编写Demo
在gin-blog
下建立main.go
作为启动文件(也就是main
包),
我们先写个Demo,帮助大家理解,写入文件内容:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"gin-blog/pkg/setting"
)
func main() {
router := gin.Default()
router.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
})
})
s := &http.Server{
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
Handler: router,
ReadTimeout: setting.ReadTimeout,
WriteTimeout: setting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
执行go run main.go
,查看命令行是否显示
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /test --> main.main.func1 (3 handlers)
在本机执行curl 127.0.0.1:8000/test
,检查是否返回{"message":"test"}
。
知识点
那么,我们来延伸一下Demo所涉及的知识点!
1、 标准库:
2、 Gin:
- gin.Default():返回Gin的
type Engine struct{...}
,里面包含RouterGroup
,相当于创建一个路由Handlers
,可以后期绑定各类的路由规则和函数、中间件等 - router.GET(…){…}:创建不同的HTTP方法绑定到
Handlers
中,也支持POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等常用的Restful方法 - gin.H{…}:就是一个
map[string]interface{}
- gin.Context:
Context
是gin
中的上下文,它允许我们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin
中包含大量Context
的方法,例如我们常用的DefaultQuery
、Query
、DefaultPostForm
、PostForm
等等
3、 &http.Server
和ListenAndServe
?
http.Server:
type Server struct {
Addr string
Handler Handler
TLSConfig *tls.Config
ReadTimeout time.Duration
ReadHeaderTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
}
- Addr:监听的TCP地址,格式为
:8000
- Handler:http句柄,实质为
ServeHTTP
,用于处理程序响应HTTP请求 - TLSConfig:安全传输层协议(TLS)的配置
- ReadTimeout:允许读取的最大时间
- ReadHeaderTimeout:允许读取请求头的最大时间
- WriteTimeout:允许写入的最大时间
- IdleTimeout:等待的最大时间
- MaxHeaderBytes:请求头的最大字节数
- ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
- ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为
nil
则默认以日志包的标准日志记录器完成(也就是在控制台输出)
ListenAndServe:
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
开始监听服务,监听TCP网络地址,Addr和调用应用程序处理连接上的请求。
我们在源码中看到Addr
是调用我们在&http.Server
中设置的参数,因此我们在设置时要用&
,我们要改变参数的值,因为我们ListenAndServe
和其他一些方法需要用到&http.Server
中的参数,他们是相互影响的。
4、 http.ListenAndServe
和连载一的r.Run()
有区别吗?
我们看看r.Run
的实现:
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
通过分析源码,得知本质上没有区别,同时也得知了启动gin
时的监听debug信息在这里输出。
5、 为什么Demo里会有WARNING
?
首先我们可以看下Default()
的实现
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
大家可以看到默认情况下,已经附加了日志、恢复中间件的引擎实例。并且在开头调用了debugPrintWARNINGDefault()
,而它的实现就是输出该行日志
func debugPrintWARNINGDefault() {
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)
}
而另外一个Running in "debug" mode. Switch to "release" mode in production.
,是运行模式原因,并不难理解,已在配置文件的管控下 :-),运维人员随时就可以修改它的配置。
6、 Demo的router.GET
等路由规则可以不写在main
包中吗?
我们发现router.GET
等路由规则,在Demo中被编写在了main
包中,感觉很奇怪,我们去抽离这部分逻辑!
在gin-blog
下routers
目录新建router.go
文件,写入内容:
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/pkg/setting"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "test",
})
})
return r
}
修改main.go
的文件内容:
package main
import (
"fmt"
"net/http"
"gin-blog/routers"
"gin-blog/pkg/setting"
)
func main() {
router := routers.InitRouter()
s := &http.Server{
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
Handler: router,
ReadTimeout: setting.ReadTimeout,
WriteTimeout: setting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ └── models.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ └── router.go
├── runtime
重启服务,执行curl 127.0.0.1:8000/test
查看是否正确返回。
下一节,我们将以我们的Demo为起点进行修改,开始编码!
Gin实践 连载三 搭建Blog API’s(二)

编写Tag的API’s、Models
项目地址:https://github.com/EDDYCJY/go…
本大节将会涉及到以下知识点:
- gin:Golang的一个微框架,性能极佳
- beego-validation:本节采用的beego的表单验证库,中文文档
- gorm,对开发人员友好的ORM框架,英文文档
- com,工具包
- 业务逻辑的编写
我们开始编写业务代码,博客文章会有标签的概念,
定义接口
本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!
- 获取标签列表:GET(“/tags”)
- 新建标签:POST(“/tags”)
- 更新指定标签:PUT(“/tags/:id”)
- 删除指定标签:DELETE(“/tags/:id”)
编写路由空壳
开始编写路由文件逻辑,在routers
下新建api
目录,我们当前是第一个API大版本,因此在api
下新建v1
目录,再新建tag.go
文件,写入内容:
package v1
import (
"github.com/gin-gonic/gin"
)
//获取多个文章标签
func GetTags(c *gin.Context) {
}
//新增文章标签
func AddTag(c *gin.Context) {
}
//修改文章标签
func EditTag(c *gin.Context) {
}
//删除文章标签
func DeleteTag(c *gin.Context) {
}
注册路由
我们打开routers
下的router.go
文件,修改文件内容为:
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
apiv1 := r.Group("/api/v1")
{
//获取标签列表
apiv1.GET("/tags", v1.GetTags)
//新建标签
apiv1.POST("/tags", v1.AddTag)
//更新指定标签
apiv1.PUT("/tags/:id", v1.EditTag)
//删除指定标签
apiv1.DELETE("/tags/:id", v1.DeleteTag)
}
return r
}
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ └── models.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ ├── api
│ │ └── v1
│ │ └── tag.go
│ └── router.go
├── runtime
检验路由是否注册成功
回到命令行,执行go run main.go
,检查路由规则是否注册成功。
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
运行成功,那么我们愉快的开始编写我们的接口吧!
下载依赖包
首先我们要拉取validation
的依赖包,在后面的接口里会使用到表单验证
go get -u github.com/astaxie/beego/validation
编写标签列表的models逻辑
创建models
目录下的tag.go
,写入文件内容:
package models
type Tag struct {
Model
Name string `json:"name"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}
func GetTags(pageNum int, pageSize int, maps interface {}) (tags []Tag) {
db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags)
return
}
func GetTagTotal(maps interface {}) (count int){
db.Model(&Tag{}).Where(maps).Count(&count)
return
}
- 我们创建了一个
Tag struct{}
,用于Gorm
的使用。并给予了附属属性json
,这样子在c.JSON
的时候就会自动转换格式,非常的便利 - 可能会有的初学者看到
return
,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了 - 有人会疑惑
db
是哪里来的;因为在同个models
包下,因此db *gorm.DB
是可以直接使用的
编写标签列表的路由逻辑
打开routers
目录下v1版本的tag.go
,第一我们先编写获取标签列表的接口
修改文件内容:
package v1
import (
"net/http"
"github.com/gin-gonic/gin"
//"github.com/astaxie/beego/validation"
"github.com/Unknwon/com"
"gin-blog/pkg/e"
"gin-blog/models"
"gin-blog/pkg/util"
"gin-blog/pkg/setting"
)
//获取多个文章标签
func GetTags(c *gin.Context) {
name := c.Query("name")
maps := make(map[string]interface{})
data := make(map[string]interface{})
if name != "" {
maps["name"] = name
}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
maps["state"] = state
}
code := e.SUCCESS
data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)
data["total"] = models.GetTagTotal(maps)
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
//新增文章标签
func AddTag(c *gin.Context) {
}
//修改文章标签
func EditTag(c *gin.Context) {
}
//删除文章标签
func DeleteTag(c *gin.Context) {
}
c.Query
可用于获取?name=test&state=1
这类URL参数,而c.DefaultQuery
则支持设置一个默认值code
变量使用了e
模块的错误编码,这正是先前规划好的错误码,方便排错和识别记录util.GetPage
保证了各接口的page
处理是一致的c *gin.Context
是Gin
很重要的组成部分,可以理解为上下文,它允许我们在中间件之间传递变量、管理流、验证请求的JSON和呈现JSON响应
在本机执行curl 127.0.0.1:8000/api/v1/tags
,正确的返回值为{"code":200,"data":{"lists":[],"total":0},"msg":"ok"}
,若存在问题请结合gin结果进行拍错。
在获取标签列表接口中,我们可以根据name
、state
、page
来筛选查询条件,分页的步长可通过app.ini
进行配置,以lists
、total
的组合返回达到分页效果。
编写新增标签的models逻辑
接下来我们编写新增标签的接口
打开models
目录下v1版本的tag.go
,修改文件(增加2个方法):
...
func ExistTagByName(name string) bool {
var tag Tag
db.Select("id").Where("name = ?", name).First(&tag)
if tag.ID > 0 {
return true
}
return false
}
func AddTag(name string, state int, createdBy string) bool{
db.Create(&Tag {
Name : name,
State : state,
CreatedBy : createdBy,
})
return true
}
...
编写新增标签的路由逻辑
打开routers
目录下的tag.go
,修改文件(变动AddTag方法):
package v1
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"github.com/Unknwon/com"
"gin-blog/pkg/e"
"gin-blog/models"
"gin-blog/pkg/util"
"gin-blog/pkg/setting"
)
...
//新增文章标签
func AddTag(c *gin.Context) {
name := c.Query("name")
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
createdBy := c.Query("created_by")
valid := validation.Validation{}
valid.Required(name, "name").Message("名称不能为空")
valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if ! models.ExistTagByName(name) {
code = e.SUCCESS
models.AddTag(name, state, createdBy)
} else {
code = e.ERROR_EXIST_TAG
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}
...
用Postman
用POST访问http://127.0.0.1:8000/api/v1/tags?name=1&state=1&created_by=test
,查看code
是否返回200
及blog_tag
表中是否有值,有值则正确。
编写models callbacks
但是这个时候大家会发现,我明明新增了标签,但created_on
居然没有值,那做修改标签的时候modified_on
会不会也存在这个问题?
为了解决这个问题,我们需要打开models
目录下的tag.go
文件,修改文件内容(修改包引用和增加2个方法):
package models
import (
"time"
"github.com/jinzhu/gorm"
)
...
func (tag *Tag) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())
return nil
}
func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())
return nil
}
重启服务,再在用Postman
用POST访问http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test
,发现created_on
已经有值了!
在这几段代码中,涉及到知识点:
这属于gorm
的Callbacks
,可以将回调方法定义为模型结构的指针,在创建、更新、查询、删除时将被调用,如果任何回调返回错误,gorm将停止未来操作并回滚所有更改。
gorm
所支持的回调方法:
- 创建:BeforeSave、BeforeCreate、AfterCreate、AfterSave
- 更新:BeforeSave、BeforeUpdate、AfterUpdate、AfterSave
- 删除:BeforeDelete、AfterDelete
- 查询:AfterFind
编写其余接口的路由逻辑
接下来,我们一口气把剩余的两个接口(EditTag、DeleteTag)完成吧
打开routers
目录下v1版本的tag.go
文件,修改内容:
...
//修改文章标签
func EditTag(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
name := c.Query("name")
modifiedBy := c.Query("modified_by")
valid := validation.Validation{}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
valid.Required(id, "id").Message("ID不能为空")
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
code = e.SUCCESS
if models.ExistTagByID(id) {
data := make(map[string]interface{})
data["modified_by"] = modifiedBy
if name != "" {
data["name"] = name
}
if state != -1 {
data["state"] = state
}
models.EditTag(id, data)
} else {
code = e.ERROR_NOT_EXIST_TAG
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}
//删除文章标签
func DeleteTag(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
code = e.SUCCESS
if models.ExistTagByID(id) {
models.DeleteTag(id)
} else {
code = e.ERROR_NOT_EXIST_TAG
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}
编写其余接口的models逻辑
打开models
下的tag.go
,修改文件内容:
...
func ExistTagByID(id int) bool {
var tag Tag
db.Select("id").Where("id = ?", id).First(&tag)
if tag.ID > 0 {
return true
}
return false
}
func DeleteTag(id int) bool {
db.Where("id = ?", id).Delete(&Tag{})
return true
}
func EditTag(id int, data interface {}) bool {
db.Model(&Tag{}).Where("id = ?", id).Updates(data)
return true
}
...
验证功能
重启服务,用Postman
- PUT访问http://127.0.0.1:8000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1,查看code是否返回200
- DELETE访问http://127.0.0.1:8000/api/v1/tags/1,查看code是否返回200
至此,Tag的API’s完成,下一节我们将开始Article的API’s编写!
Gin实践 连载四 搭建Blog API’s(三)

编写Article的API’s、Models
项目地址:https://github.com/EDDYCJY/go…
定义接口
本节编写文章的逻辑,我们定义一下接口吧!
- 获取文章列表:GET(“/articles”)
- 获取指定文章:POST(“/articles/:id”)
- 新建文章:POST(“/articles”)
- 更新指定文章:PUT(“/articles/:id”)
- 删除指定文章:DELETE(“/articles/:id”)
编写路由逻辑
在routers
的v1版本下,新建article.go
文件,写入内容:
package v1
import (
"github.com/gin-gonic/gin"
)
//获取单个文章
func GetArticle(c *gin.Context) {
}
//获取多个文章
func GetArticles(c *gin.Context) {
}
//新增文章
func AddArticle(c *gin.Context) {
}
//修改文章
func EditArticle(c *gin.Context) {
}
//删除文章
func DeleteArticle(c *gin.Context) {
}
我们打开routers
下的router.go
文件,修改文件内容为:
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
)
func InitRouter() *gin.Engine {
...
apiv1 := r.Group("/api/v1")
{
...
//获取文章列表
apiv1.GET("/articles", v1.GetArticles)
//获取指定文章
apiv1.GET("/articles/:id", v1.GetArticle)
//新建文章
apiv1.POST("/articles", v1.AddArticle)
//更新指定文章
apiv1.PUT("/articles/:id", v1.EditArticle)
//删除指定文章
apiv1.DELETE("/articles/:id", v1.DeleteArticle)
}
return r
}
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ ├── api
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
在基础的路由规则配置结束后,我们开始编写我们的接口吧!
编写models逻辑
创建models
目录下的article.go
,写入文件内容:
package models
import (
"github.com/jinzhu/gorm"
"time"
)
type Article struct {
Model
TagID int `json:"tag_id" gorm:"index"`
Tag Tag `json:"tag"`
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())
return nil
}
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())
return nil
}
我们创建了一个Article struct {}
,与Tag
不同的是,Article
多了几项
gorm:index
,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响Tag
字段,实际是一个嵌套的struct
,它利用TagID
与Tag
模型相互关联,在执行查询的时候,能够达到Article
、Tag
关联查询的功能time.Now().Unix()
返回当前的时间戳
接下来,请确保已对上一章节的内容通读且了解,由于逻辑偏差不会太远,我们本节直接编写这五个接口
打开models
目录下的article.go
,修改文件内容:
package models
import (
"time"
"github.com/jinzhu/gorm"
)
type Article struct {
Model
TagID int `json:"tag_id" gorm:"index"`
Tag Tag `json:"tag"`
Title string `json:"title"`
Desc string `json:"desc"`
Content string `json:"content"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
State int `json:"state"`
}
func ExistArticleByID(id int) bool {
var article Article
db.Select("id").Where("id = ?", id).First(&article)
if article.ID > 0 {
return true
}
return false
}
func GetArticleTotal(maps interface {}) (count int){
db.Model(&Article{}).Where(maps).Count(&count)
return
}
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
return
}
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)
return
}
func EditArticle(id int, data interface {}) bool {
db.Model(&Article{}).Where("id = ?", id).Updates(data)
return true
}
func AddArticle(data map[string]interface {}) bool {
db.Create(&Article {
TagID : data["tag_id"].(int),
Title : data["title"].(string),
Desc : data["desc"].(string),
Content : data["content"].(string),
CreatedBy : data["created_by"].(string),
State : data["state"].(int),
})
return true
}
func DeleteArticle(id int) bool {
db.Where("id = ?", id).Delete(Article{})
return true
}
func (article *Article) BeforeCreate(scope *gorm.Scope) error {
scope.SetColumn("CreatedOn", time.Now().Unix())
return nil
}
func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
scope.SetColumn("ModifiedOn", time.Now().Unix())
return nil
}
在这里,我们拿出三点不同来讲
1、 我们的Article
是如何关联到Tag
???
func GetArticle(id int) (article Article) {
db.Where("id = ?", id).First(&article)
db.Model(&article).Related(&article.Tag)
return
}
能够达到关联,首先是gorm
本身做了大量的约定俗成
Article
有一个结构体成员是TagID
,就是外键。gorm
会通过类名+ID的方式去找到这两个类之间的关联关系Article
有一个结构体成员是Tag
,就是我们嵌套在Article
里的Tag
结构体,我们可以通过Related
进行关联查询
2、 Preload
是什么东西,为什么查询可以得出每一项的关联Tag
?
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)
return
}
Preload
就是一个预加载器,它会执行两条SQL,分别是SELECT * FROM blog_articles;
和SELECT * FROM blog_tag WHERE id IN (1,2,3,4);
,那么在查询出结构后,gorm
内部处理对应的映射逻辑,将其填充到Article
的Tag
中,会特别方便,并且避免了循环查询
那么有没有别的办法呢,大致是两种
gorm
的Join
- 循环
Related
综合之下,还是Preload
更好,如果你有更优的方案,欢迎说一下 🙂
3、 v.(I)
是什么?
v
表示一个接口值,I
表示接口类型。这个实际就是Golang中的类型断言,用于判断一个接口值的实际类型是否为某个类型,或一个非接口值的类型是否实现了某个接口类型
打开routers
目录下v1版本的article.go
文件,修改文件内容:
package v1
import (
"net/http"
"log"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"github.com/Unknwon/com"
"gin-blog/models"
"gin-blog/pkg/e"
"gin-blog/pkg/setting"
"gin-blog/pkg/util"
)
//获取单个文章
func GetArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
code := e.INVALID_PARAMS
var data interface {}
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
data = models.GetArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
//获取多个文章
func GetArticles(c *gin.Context) {
data := make(map[string]interface{})
maps := make(map[string]interface{})
valid := validation.Validation{}
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
maps["state"] = state
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
var tagId int = -1
if arg := c.Query("tag_id"); arg != "" {
tagId = com.StrTo(arg).MustInt()
maps["tag_id"] = tagId
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
}
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
code = e.SUCCESS
data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
data["total"] = models.GetArticleTotal(maps)
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
//新增文章
func AddArticle(c *gin.Context) {
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
createdBy := c.Query("created_by")
state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
valid := validation.Validation{}
valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
valid.Required(title, "title").Message("标题不能为空")
valid.Required(desc, "desc").Message("简述不能为空")
valid.Required(content, "content").Message("内容不能为空")
valid.Required(createdBy, "created_by").Message("创建人不能为空")
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistTagByID(tagId) {
data := make(map[string]interface {})
data["tag_id"] = tagId
data["title"] = title
data["desc"] = desc
data["content"] = content
data["created_by"] = createdBy
data["state"] = state
models.AddArticle(data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]interface{}),
})
}
//修改文章
func EditArticle(c *gin.Context) {
valid := validation.Validation{}
id := com.StrTo(c.Param("id")).MustInt()
tagId := com.StrTo(c.Query("tag_id")).MustInt()
title := c.Query("title")
desc := c.Query("desc")
content := c.Query("content")
modifiedBy := c.Query("modified_by")
var state int = -1
if arg := c.Query("state"); arg != "" {
state = com.StrTo(arg).MustInt()
valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
}
valid.Min(id, 1, "id").Message("ID必须大于0")
valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
if models.ExistTagByID(tagId) {
data := make(map[string]interface {})
if tagId > 0 {
data["tag_id"] = tagId
}
if title != "" {
data["title"] = title
}
if desc != "" {
data["desc"] = desc
}
if content != "" {
data["content"] = content
}
data["modified_by"] = modifiedBy
models.EditArticle(id, data)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_TAG
}
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}
//删除文章
func DeleteArticle(c *gin.Context) {
id := com.StrTo(c.Param("id")).MustInt()
valid := validation.Validation{}
valid.Min(id, 1, "id").Message("ID必须大于0")
code := e.INVALID_PARAMS
if ! valid.HasErrors() {
if models.ExistArticleByID(id) {
models.DeleteArticle(id)
code = e.SUCCESS
} else {
code = e.ERROR_NOT_EXIST_ARTICLE
}
} else {
for _, err := range valid.Errors {
log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : make(map[string]string),
})
}
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
├── models
│ ├── article.go
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ └── pagination.go
├── routers
│ ├── api
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
验证功能
我们重启服务,执行go run main.go
,检查控制台输出结果
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /api/v1/tags --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST /api/v1/tags --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT /api/v1/tags/:id --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
[GIN-debug] GET /api/v1/articles --> gin-blog/routers/api/v1.GetArticles (3 handlers)
[GIN-debug] GET /api/v1/articles/:id --> gin-blog/routers/api/v1.GetArticle (3 handlers)
[GIN-debug] POST /api/v1/articles --> gin-blog/routers/api/v1.AddArticle (3 handlers)
[GIN-debug] PUT /api/v1/articles/:id --> gin-blog/routers/api/v1.EditArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)
使用Postman
检验接口是否正常(大家可以选用合适的参数传递方式,此处为了方便展示我选用了URL传参),
- POST:http://127.0.0.1:8000/api/v1/articles?tag_id=1&title=test1&desc=test-desc&content=test-content&created_by=test-created&state=1
- GET:http://127.0.0.1:8000/api/v1/articles
- GET:http://127.0.0.1:8000/api/v1/articles/1
- PUT:http://127.0.0.1:8000/api/v1/articles/1?tag_id=1&title=test-edit1&desc=test-desc-edit&content=test-content-edit&modified_by=test-created-edit&state=0
- DELETE:http://127.0.0.1:8000/api/v1/articles/1
至此,我们的API’s编写就到这里,下一节我们将介绍另外的一些技巧!
Gin实践 连载五 使用JWT进行身份校验

原文地址:使用JWT进行身份校验
在前面几节中,我们已经基本的完成了API’s的编写
但是,还存在一些非常严重的问题,例如,我们现在的API是可以随意调用的,这显然还不够完美,是有问题的
那么我们采用 jwt-go (GoDoc)的方式来简单解决这个问题
项目地址:https://github.com/EDDYCJY/go…
下载依赖包
首先,我们下载jwt-go的依赖包
go get -u github.com/dgrijalva/jwt-go
编写jwt
工具包
我们需要编写一个jwt
的工具包,我们在pkg
下的util
目录新建jwt.go
,写入文件内容:
package util
import (
"time"
jwt "github.com/dgrijalva/jwt-go"
"gin-blog/pkg/setting"
)
var jwtSecret = []byte(setting.JwtSecret)
type Claims struct {
Username string `json:"username"`
Password string `json:"password"`
jwt.StandardClaims
}
func GenerateToken(username, password string) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(3 * time.Hour)
claims := Claims{
username,
password,
jwt.StandardClaims {
ExpiresAt : expireTime.Unix(),
Issuer : "gin-blog",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)
return token, err
}
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
在这个工具包,我们涉及到
NewWithClaims(method SigningMethod, claims Claims)
,method
对应着SigningMethodHMAC struct{}
,其包含SigningMethodHS256
、SigningMethodHS384
、SigningMethodHS512
三种crypto.Hash
方案func (t *Token) SignedString(key interface{})
该方法内部生成签名字符串,再用于获取完整、已签名的token
func (p *Parser) ParseWithClaims
用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
func (m MapClaims) Valid()
验证基于时间的声明exp, iat, nbf
,注意如果没有任何声明在令牌中,仍然会被认为是有效的。并且对于时区偏差没有计算方法
有了jwt
工具包,接下来我们要编写要用于Gin
的中间件,我们在middleware
下新建jwt
目录,新建jwt.go
文件,写入内容:
package jwt
import (
"time"
"net/http"
"github.com/gin-gonic/gin"
"gin-blog/pkg/util"
"gin-blog/pkg/e"
)
func JWT() gin.HandlerFunc {
return func(c *gin.Context) {
var code int
var data interface{}
code = e.SUCCESS
token := c.Query("token")
if token == "" {
code = e.INVALID_PARAMS
} else {
claims, err := util.ParseToken(token)
if err != nil {
code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
} else if time.Now().Unix() > claims.ExpiresAt {
code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
}
}
if code != e.SUCCESS {
c.JSON(http.StatusUnauthorized, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
c.Abort()
return
}
c.Next()
}
}
如何获取Token
那么我们如何调用它呢,我们还要获取Token
呢?
1、 我们要新增一个获取Token
的API
在models
下新建auth.go
文件,写入内容:
package models
type Auth struct {
ID int `gorm:"primary_key" json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
func CheckAuth(username, password string) bool {
var auth Auth
db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
if auth.ID > 0 {
return true
}
return false
}
在routers
下的api
目录新建auth.go
文件,写入内容:
package api
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"gin-blog/pkg/e"
"gin-blog/pkg/util"
"gin-blog/models"
)
type auth struct {
Username string `valid:"Required; MaxSize(50)"`
Password string `valid:"Required; MaxSize(50)"`
}
func GetAuth(c *gin.Context) {
username := c.Query("username")
password := c.Query("password")
valid := validation.Validation{}
a := auth{Username: username, Password: password}
ok, _ := valid.Valid(&a)
data := make(map[string]interface{})
code := e.INVALID_PARAMS
if ok {
isExist := models.CheckAuth(username, password)
if isExist {
token, err := util.GenerateToken(username, password)
if err != nil {
code = e.ERROR_AUTH_TOKEN
} else {
data["token"] = token
code = e.SUCCESS
}
} else {
code = e.ERROR_AUTH
}
} else {
for _, err := range valid.Errors {
log.Println(err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
我们打开routers
目录下的router.go
文件,修改文件内容(新增获取token的方法):
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/auth", api.GetAuth)
apiv1 := r.Group("/api/v1")
{
...
}
return r
}
验证Token
获取token
的API方法就到这里啦,让我们来测试下是否可以正常使用吧!
重启服务后,用GET
方式访问http://127.0.0.1:8000/auth?username=test&password=test123456
,查看返回值是否正确
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjAwMzcsImlzcyI6Imdpbi1ibG9nIn0.-kK0V9E06qTHOzupQM_gHXAGDB3EJtJS4H5TTCyWwW8"
},
"msg": "ok"
}
我们有了token
的API,也调用成功了
将中间件接入Gin
2、 接下来我们将中间件接入到Gin
的访问流程中
我们打开routers
目录下的router.go
文件,修改文件内容(新增引用包和中间件引用)
package routers
import (
"github.com/gin-gonic/gin"
"gin-blog/routers/api"
"gin-blog/routers/api/v1"
"gin-blog/pkg/setting"
"gin-blog/middleware/jwt"
)
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
gin.SetMode(setting.RunMode)
r.GET("/auth", api.GetAuth)
apiv1 := r.Group("/api/v1")
apiv1.Use(jwt.JWT())
{
...
}
return r
}
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
│ └── jwt
│ └── jwt.go
├── models
│ ├── article.go
│ ├── auth.go
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ ├── jwt.go
│ └── pagination.go
├── routers
│ ├── api
│ │ ├── auth.go
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
到这里,我们的JWT
编写就完成啦!
验证功能
我们来测试一下,再次访问
- http://127.0.0.1:8000/api/v1/articles
- http://127.0.0.1:8000/api/v1/articles?token=23131
正确的反馈应该是
{
"code": 400,
"data": null,
"msg": "请求参数错误"
}
{
"code": 20001,
"data": null,
"msg": "Token鉴权失败"
}
我们需要访问http://127.0.0.1:8000/auth?username=test&password=test123456
,得到token
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjQ2OTMsImlzcyI6Imdpbi1ibG9nIn0.KSBY6TeavV_30kfmP7HWLRYKP5TPEDgHtABe9HCsic4"
},
"msg": "ok"
}
再用包含token
的URL参数去访问我们的应用API,
访问http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...
,检查接口返回值
{
"code": 200,
"data": {
"lists": [
{
"id": 2,
"created_on": 1518700920,
"modified_on": 0,
"tag_id": 1,
"tag": {
"id": 1,
"created_on": 1518684200,
"modified_on": 0,
"name": "tag1",
"created_by": "",
"modified_by": "",
"state": 0
},
"content": "test-content",
"created_by": "test-created",
"modified_by": "",
"state": 0
}
],
"total": 1
},
"msg": "ok"
}
返回正确,至此我们的jwt-go
在Gin
中的验证就完成了!
Gin实践 连载六 编写一个简单的文件日志

原文地址:编写一个简单的文件日志
在上一节中,我们解决了API’s可以任意访问的问题,那么我们现在还有一个问题。
就是我们的日志,都是输出到控制台上的,这显然对于一个项目来说是不合理的,因此我们这一节简单封装log
库,使其支持简单的文件日志!
项目地址:https://github.com/EDDYCJY/go…
新建logging
包
我们在pkg
下新建logging
目录,新建file.go
和log.go
文件,写入内容:
编写file
文件
1、 file.go:
package logging
import (
"os"
"time"
"fmt"
"log"
)
var (
LogSavePath = "runtime/logs/"
LogSaveName = "log"
LogFileExt = "log"
TimeFormat = "20060102"
)
func getLogFilePath() string {
return fmt.Sprintf("%s", LogSavePath)
}
func getLogFileFullPath() string {
prefixPath := getLogFilePath()
suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)
return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}
func openLogFile(filePath string) *os.File {
_, err := os.Stat(filePath)
switch {
case os.IsNotExist(err):
mkDir()
case os.IsPermission(err):
log.Fatalf("Permission :%v", err)
}
handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("Fail to OpenFile :%v", err)
}
return handle
}
func mkDir() {
dir, _ := os.Getwd()
err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
if err != nil {
panic(err)
}
}
os.Stat
:返回文件信息结构描述文件。如果出现错误,会返回*PathError
type PathError struct {
Op string
Path string
Err error
}
os.IsNotExist
:能够接受ErrNotExist
、syscall
的一些错误,它会返回一个布尔值,能够得知文件不存在或目录不存在os.IsPermission
:能够接受ErrPermission
、syscall
的一些错误,它会返回一个布尔值,能够得知权限是否满足os.OpenFile
:调用文件,支持传入文件名称、指定的模式调用文件、文件权限,返回的文件的方法可以用于I/O。如果出现错误,则为*PathError
。
const (
// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
O_RDONLY int = syscall.O_RDONLY // 以只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 以只写模式打开文件
O_RDWR int = syscall.O_RDWR // 以读写模式打开文件
// The remaining values may be or'ed in to control behavior.
O_APPEND int = syscall.O_APPEND // 在写入时将数据追加到文件中
O_CREATE int = syscall.O_CREAT // 如果不存在,则创建一个新文件
O_EXCL int = syscall.O_EXCL // 使用O_CREATE时,文件必须不存在
O_SYNC int = syscall.O_SYNC // 同步IO
O_TRUNC int = syscall.O_TRUNC // 如果可以,打开时
)
os.Getwd
:返回与当前目录对应的根路径名os.MkdirAll
:创建对应的目录以及所需的子目录,若成功则返回nil
,否则返回error
os.ModePerm
:const
定义ModePerm FileMode = 0777
编写log
文件
2、 log.go
package logging
import (
"log"
"os"
"runtime"
"path/filepath"
"fmt"
)
type Level int
var (
F *os.File
DefaultPrefix = ""
DefaultCallerDepth = 2
logger *log.Logger
logPrefix = ""
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
)
const (
DEBUG Level = iota
INFO
WARNING
ERROR
FATAL
)
func init() {
filePath := getLogFileFullPath()
F = openLogFile(filePath)
logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
func Debug(v ...interface{}) {
setPrefix(DEBUG)
logger.Println(v)
}
func Info(v ...interface{}) {
setPrefix(INFO)
logger.Println(v)
}
func Warn(v ...interface{}) {
setPrefix(WARNING)
logger.Println(v)
}
func Error(v ...interface{}) {
setPrefix(ERROR)
logger.Println(v)
}
func Fatal(v ...interface{}) {
setPrefix(FATAL)
logger.Fatalln(v)
}
func setPrefix(level Level) {
_, file, line, ok := runtime.Caller(DefaultCallerDepth)
if ok {
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
} else {
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
}
logger.SetPrefix(logPrefix)
}
log.New
:创建一个新的日志记录器。out
定义要写入日志数据的IO
句柄。prefix
定义每个生成的日志行的开头。flag
定义了日志记录属性
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
log.LstdFlags
:日志记录的格式属性之一,其余的选项如下
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23
Ltime // the time in the local time zone: 01:23:23
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
Llongfile // full file name and line number: /a/b/c/d.go:23
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
LstdFlags = Ldate | Ltime // initial values for the standard logger
)
当前目录结构:
gin-blog/
├── conf
│ └── app.ini
├── main.go
├── middleware
│ └── jwt
│ └── jwt.go
├── models
│ ├── article.go
│ ├── auth.go
│ ├── models.go
│ └── tag.go
├── pkg
│ ├── e
│ │ ├── code.go
│ │ └── msg.go
│ ├── logging
│ │ ├── file.go
│ │ └── log.go
│ ├── setting
│ │ └── setting.go
│ └── util
│ ├── jwt.go
│ └── pagination.go
├── routers
│ ├── api
│ │ ├── auth.go
│ │ └── v1
│ │ ├── article.go
│ │ └── tag.go
│ └── router.go
├── runtime
我们自定义的logging
包,已经基本完成了,接下来让它接入到我们的项目之中吧!
我们打开先前包含log
包的代码,
- 打开
routers
目录下的article.go
、tag.go
、auth.go
- 将
log
包的引用删除,修改引用我们自己的日志包为gin-blog/pkg/logging
- 将原本的
log.Println(...)
改为log.Info(...)
例如auth.go
文件的修改内容:
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/astaxie/beego/validation"
"gin-blog/pkg/e"
"gin-blog/pkg/util"
"gin-blog/models"
"gin-blog/pkg/logging"
)
...
func GetAuth(c *gin.Context) {
...
code := e.INVALID_PARAMS
if ok {
...
} else {
for _, err := range valid.Errors {
logging.Info(err.Key, err.Message)
}
}
c.JSON(http.StatusOK, gin.H{
"code" : code,
"msg" : e.GetMsg(code),
"data" : data,
})
}
验证功能
修改文件后,重启服务,我们来试试吧!
获取到API的Token后,我们故意传错误URL参数给接口,如:http://127.0.0.1:8000/api/v1/articles?tag_id=0&state=9999999&token=eyJhbG..
然后我们到$GOPATH/gin-blog/runtime/logs
查看日志:
$ tail -f log20180216.log
[INFO][article.go:79]2018/02/16 18:33:12 [state 状态只允许0或1]
[INFO][article.go:79]2018/02/16 18:33:42 [state 状态只允许0或1]
[INFO][article.go:79]2018/02/16 18:33:42 [tag_id 标签ID必须大于0]
[INFO][article.go:79]2018/02/16 18:38:39 [state 状态只允许0或1]
[INFO][article.go:79]2018/02/16 18:38:39 [tag_id 标签ID必须大于0]
日志结构一切正常,我们的记录模式都为Info
,因此前缀是对的,并且我们是入参有问题,也把错误记录下来了,这样排错就很方便了!
至此,本节就完成了,这只是一个简单的扩展,实际上我们线上项目要使用的文件日志,是更复杂一些,开动你的大脑 举一反三吧!
Gin实践 连载七 Golang优雅重启HTTP服务

优雅的重启服务
在前面编写案例代码时,我相信你会想到
每次更新完代码,更新完配置文件后
就直接这么 ctrl+c
真的没问题吗,ctrl+c
到底做了些什么事情呢?
在这一节中我们简单讲述 ctrl+c
背后的信号以及如何在Gin
中优雅的重启服务,也就是对 HTTP
服务进行热更新
原文地址:Golang优雅重启HTTP服务
项目地址:https://github.com/EDDYCJY/go…
ctrl + c
内核在某些情况下发送信号,比如在进程往一个已经关闭的管道写数据时会产生
SIGPIPE
信号
在终端执行特定的组合键可以使系统发送特定的信号给此进程,完成一系列的动作
命令 | 信号 | 含义 |
---|---|---|
ctrl + c | SIGINT | 强制进程结束 |
ctrl + z | SIGTSTP | 任务中断,进程挂起 |
ctrl + \ | SIGQUIT | 进程结束 和 dump core |
ctrl + d | EOF | |
SIGHUP | 终止收到该信号的进程。若程序中没有捕捉该信号,当收到该信号时,进程就会退出(常用于 重启、重新加载进程) |
因此在我们执行ctrl + c
关闭gin
服务端时,会强制进程结束,导致正在访问的用户等出现问题
常见的 kill -9 pid
会发送 SIGKILL
信号给进程,也是类似的结果
信号
本段中反复出现信号是什么呢?
信号是 Unix
、类 Unix
以及其他 POSIX
兼容的操作系统中进程间通讯的一种有限制的方式
它是一种异步的通知机制,用来提醒进程一个事件(硬件异常、程序执行异常、外部发出信号)已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程。此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数
所有信号
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
怎样算优雅
目的
- 不关闭现有连接(正在运行中的程序)
- 新的进程启动并替代旧进程
- 新的进程接管新的连接
- 连接要随时响应用户的请求,当用户仍在请求旧进程时要保持连接,新用户应请求新进程,不可以出现拒绝请求的情况
流程
1、替换可执行文件或修改配置文件
2、发送信号量 SIGHUP
3、拒绝新连接请求旧进程,但要保证已有连接正常
4、启动新的子进程
5、新的子进程开始 Accet
6、系统将新的请求转交新的子进程
7、旧进程处理完所有旧连接后正常结束
实现优雅重启
endless
Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+)
我们借助 fvbock/endless 来实现 Golang HTTP/HTTPS
服务重新启动的零停机
endless server
监听以下几种信号量:
- syscall.SIGHUP:触发
fork
子进程和重新启动 - syscall.SIGUSR1/syscall.SIGTSTP:被监听,但不会触发任何动作
- syscall.SIGUSR2:触发
hammerTime
- syscall.SIGINT/syscall.SIGTERM:触发服务器关闭(会完成正在运行的请求)
endless
正正是依靠监听这些信号量,完成管控的一系列动作
安装
go get -u github.com/fvbock/endless
编写
打开 gin-blog 的 main.go
文件,修改文件:
package main
import (
"fmt"
"log"
"syscall"
"github.com/fvbock/endless"
"gin-blog/routers"
"gin-blog/pkg/setting"
)
func main() {
endless.DefaultReadTimeOut = setting.ReadTimeout
endless.DefaultWriteTimeOut = setting.WriteTimeout
endless.DefaultMaxHeaderBytes = 1 << 20
endPoint := fmt.Sprintf(":%d", setting.HTTPPort)
server := endless.NewServer(endPoint, routers.InitRouter())
server.BeforeBegin = func(add string) {
log.Printf("Actual pid is %d", syscall.Getpid())
}
err := server.ListenAndServe()
if err != nil {
log.Printf("Server err: %v", err)
}
}
endless.NewServer
返回一个初始化的 endlessServer
对象,在 BeforeBegin
时输出当前进程的 pid
,调用 ListenAndServe
将实际“启动”服务
验证
编译
$ go build main.go
执行
$ ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
...
Actual pid is 48601
启动成功后,输出了pid
为 48601;在另外一个终端执行 kill -1 48601
,检验先前服务的终端效果
[root@localhost go-gin-example]# ./main
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /auth --> ...
[GIN-debug] GET /api/v1/tags --> ...
...
Actual pid is 48601
...
Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection
可以看到该命令已经挂起,并且 fork
了新的子进程 pid
为 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection
大致意思为主进程(pid
为48601)接受到 SIGTERM
信号量,关闭主进程的监听并且等待正在执行的请求完成;这与我们先前的描述一致
唤醒
这时候在 postman
上再次访问我们的接口,你可以惊喜的发现,他“复活”了!
Actual pid is 48755
48601 Received SIGTERM.
48601 [::]:8000 Listener closed.
48601 Waiting for connections to finish...
48601 Serve() returning...
Server err: accept tcp [::]:8000: use of closed network connection
$ [GIN] 2018/03/15 - 13:00:16 | 200 | 188.096µs | 192.168.111.1 | GET /api/v1/tags...
这就完成了一次正向的流转了
你想想,每次更新发布、或者修改配置文件等,只需要给该进程发送SIGTERM信号,而不需要强制结束应用,是多么便捷又安全的事!
问题
endless
热更新是采取创建子进程后,将原进程退出的方式,这点不符合守护进程的要求
http.Server – Shutdown()
如果你的Golang >= 1.8
,也可以考虑使用 http.Server
的 Shutdown 方法
package main
import (
"fmt"
"net/http"
"context"
"log"
"os"
"os/signal"
"time"
"gin-blog/routers"
"gin-blog/pkg/setting"
)
func main() {
router := routers.InitRouter()
s := &http.Server{
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
Handler: router,
ReadTimeout: setting.ReadTimeout,
WriteTimeout: setting.WriteTimeout,
MaxHeaderBytes: 1 << 20,
}
go func() {
if err := s.ListenAndServe(); err != nil {
log.Printf("Listen: %s\n", err)
}
}()
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<- quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}
小结
在日常的服务中,优雅的重启(热更新)是非常重要的一环。而 Golang
在 HTTP
服务方面的热更新也有不少方案了,我们应该根据实际应用场景挑选最合适的
Gin实践 连载八 为它加上Swagger

API's
,必然离不开一个好的API
文档
要开发纯手写 API
文档,不存在的 :=)
原文地址:为它加上Swagger
项目地址:https://github.com/EDDYCJY/go…
安装 swag
1、go get
$ go get -u github.com/swaggo/swag/cmd/swag
若 $GOPATH/bin
没有加入$PATH
中,你需要执行将其可执行文件移动到$GOBIN
下
mv $GOPATH/bin/swag /usr/local/go/bin
2、gopm get
该包有引用golang.org
上的包,若无科学上网,你可以使用 gopm 进行安装
gopm get -g -v github.com/swaggo/swag/cmd/swag
cd $GOPATH/src/github.com/swaggo/swag/cmd/swag
go install
同理将其可执行文件移动到$GOBIN
下
验证是否安装成功
$ swag -v
swag version v1.1.1
安装 gin-swagger
$ go get -u github.com/swaggo/gin-swagger
$ go get -u github.com/swaggo/gin-swagger/swaggerFiles
注:三个包都有一定大小,安装需要等一会或要科学上网
初始化
编写API注释
Swagger
中需要将相应的注释或注解编写到方法上,再利用生成器自动生成说明文件
gin-swagger
给出的范例:
// @Summary Add a new pet to the store
// @Description get string by ID
// @Accept json
// @Produce json
// @Param some_id path int true "Some ID"
// @Success 200 {string} string "ok"
// @Failure 400 {object} web.APIError "We need ID!!"
// @Failure 404 {object} web.APIError "Can not find ID"
// @Router /testapi/get-string-by-int/{some_id} [get]
我们可以参照 Swagger
的注解规范和范例去编写
// @Summary 新增文章标签
// @Produce json
// @Param name query string true "Name"
// @Param state query int false "State"
// @Param created_by query int false "CreatedBy"
// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
// @Router /api/v1/tags [post]
func AddTag(c *gin.Context) {
// @Summary 修改文章标签
// @Produce json
// @Param id path int true "ID"
// @Param name query string true "ID"
// @Param state query int false "State"
// @Param modified_by query string true "ModifiedBy"
// @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
// @Router /api/v1/tags/{id} [put]
func EditTag(c *gin.Context) {
参考的注解请参见 go-gin-example。以确保获取最新的 swag 语法
生成
我们进入到gin-blog
的项目根目录中,执行初始化命令
[$ gin-blog]# swag init
2018/03/13 23:32:10 Generate swagger docs....
2018/03/13 23:32:10 Generate general API Info
2018/03/13 23:32:10 create docs.go at docs/docs.go
完毕后会在项目根目录下生成docs
docs/
├── docs.go
└── swagger
├── swagger.json
└── swagger.yaml
我们可以检查 docs.go
文件中的 doc
变量,详细记载中我们文件中所编写的注解和说明
验证
大功告成,访问一下 http://127.0.0.1:8000/swagger/index.html
, 查看 API
文档生成是否正确
参考
Gin实践 连载九 将Golang应用部署到Docker

原文地址:将Golang应用部署到Docker
注:
- 开始前你需要安装好
docker
,配好镜像源 - 本章节源码在
f-20180324-docker
分支上 - 从本章节开始 项目目录都以
go-gin-example
为基准(请配合自己本地项目灵活变动)
介绍
在这里简单介绍下Docker,建议深入学习
Docker 是一个开源的轻量级容器技术,让开发者可以打包他们的应用以及应用运行的上下文环境到一个可移植的镜像中,然后发布到任何支持Docker的系统上运行。 通过容器技术,在几乎没有性能开销的情况下,Docker 为应用提供了一个隔离运行环境
- 简化配置
- 代码流水线管理
- 提高开发效率
- 隔离应用
- 快速、持续部署
接下来我们正式开始对项目进行 docker
的所需处理和编写,每一个大标题为步骤大纲
Golang
一、编写Dockerfile
在 go-gin-example
项目根目录创建 Dockerfile 文件,写入内容
FROM golang:latest
WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
RUN go build .
EXPOSE 8000
ENTRYPOINT ["./go-gin-example"]
作用
golang:latest
镜像为基础镜像,将工作目录设置为 $GOPATH/src/go-gin-example
,并将当前上下文目录的内容复制到 $GOPATH/src/go-gin-example
中
在进行 go build
编译完毕后,将容器启动程序设置为 ./go-gin-example
,也就是我们所编译的可执行文件
注意 go-gin-example
在 docker
容器里编译,并没有在宿主机现场编译
说明
Dockerfile 文件是用于定义 Docker 镜像生成流程的配置文件,文件内容是一条条指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建;这些指令应用于基础镜像并最终创建一个新的镜像
你可以认为用于快速创建自定义的 Docker 镜像
1、 FROM
指定基础镜像(必须有的指令,并且必须是第一条指令)
2、 WORKDIR
格式为 WORKDIR
<工作目录路径>
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如果目录不存在,WORKDIR
会帮你建立目录
3、COPY
格式:
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
COPY
指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置
4、RUN
用于执行命令行命令
格式:RUN
<命令>
5、EXPOSE
格式为 EXPOSE
<端口1> [<端口2>…]
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
在 Dockerfile 中写入这样的声明有两个好处
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射
- 运行时使用随机端口映射时,也就是
docker run -P
时,会自动随机映射EXPOSE
的端口
6、ENTRYPOINT
ENTRYPOINT
的格式和 RUN
指令格式一样,分为两种格式
exec
格式:
<ENTRYPOINT> "<CMD>"
shell
格式:
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]
ENTRYPOINT
指令是指定容器启动程序及参数
二、构建镜像
go-gin-example
的项目根目录下执行 docker build -t gin-blog-docker .
该命令作用是创建/构建镜像,-t
指定名称为 gin-blog-docker
,.
构建内容为当前上下文目录
$ docker build -t gin-blog-docker .
Sending build context to Docker daemon 96.39 MB
Step 1/6 : FROM golang:latest
---> d632bbfe5767
Step 2/6 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
---> 56294f978c5d
Removing intermediate container e112997b995d
Step 3/6 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
---> 3b60960120cf
Removing intermediate container 63e310b3f60c
Step 4/6 : RUN go build .
---> Running in 52648a431450
---> 7bfbeb301fea
Removing intermediate container 52648a431450
Step 5/6 : EXPOSE 8000
---> Running in 98f5b387d1bb
---> b65bd4076c65
Removing intermediate container 98f5b387d1bb
Step 6/6 : ENTRYPOINT ./go-gin-example
---> Running in c4f6cdeb667b
---> d8a109c7697c
Removing intermediate container c4f6cdeb667b
Successfully built d8a109c7697c
三、验证镜像
查看所有的镜像,确定刚刚构建的 gin-blog-docker
镜像是否存在
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
gin-blog-docker latest d8a109c7697c About a minute ago 946 MB
docker.io/golang latest d632bbfe5767 8 days ago 779 MB
...
四、创建并运行一个新容器
执行命令 docker run -p 8000:8000 gin-blog-docker
$ docker run -p 8000:8000 gin-blog-docker
dial tcp 127.0.0.1:3306: connect: connection refused
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
...
Actual pid is 1
运行成功,你以为大功告成了吗?
你想太多了,仔细看看控制台的输出了一条错误 dial tcp 127.0.0.1:3306: connect: connection refused
我们研判一下,发现是 Mysql
的问题,接下来第二项我们将解决这个问题
Mysql
一、拉取镜像
从 Docker
的公共仓库 Dockerhub
下载 MySQL
镜像(国内建议配个镜像)
$ docker pull mysql
二、创建并运行一个新容器
运行 Mysql
容器,并设置执行成功后返回容器ID
$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -d mysql
8c86ac986da4922492934b6fe074254c9165b8ee3e184d29865921b0fef29e64
连接 Mysql
初始化的 Mysql
应该如图
Golang + Mysql
一、删除镜像
由于原本的镜像存在问题,我们需要删除它,此处有几种做法
- 删除原本有问题的镜像,重新构建一个新镜像
- 重新构建一个不同
name
、tag
的新镜像
删除原本的有问题的镜像,-f
是强制删除及其关联状态
若不执行 -f
,你需要执行 docker ps -a
查到所关联的容器,将其 rm
解除两者依赖关系
$ docker rmi -f gin-blog-docker
Untagged: gin-blog-docker:latest
Deleted: sha256:d8a109c7697c3c2d9b4de7dbb49669d10106902122817b6467a031706bc52ab4
Deleted: sha256:b65bd4076c65a3c24029ca4def3b3f37001ff7c9eca09e2590c4d29e1e23dce5
Deleted: sha256:7bfbeb301fea9d8912a4b7c43e4bb8b69bdc57f0b416b372bfb6510e476a7dee
Deleted: sha256:3b60960120cf619181c1762cdc1b8ce318b8c815e056659809252dd321bcb642
Deleted: sha256:56294f978c5dfcfa4afa8ad033fd76b755b7ecb5237c6829550741a4d2ce10bc
二、修改配置文件
将项目的配置文件 conf/app.ini
,内容修改为
#debug or release
RUN_MODE = debug
[app]
PAGE_SIZE = 10
JWT_SECRET = 233
[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60
[database]
TYPE = mysql
USER = root
PASSWORD = rootroot
HOST = mysql:3306
NAME = blog
TABLE_PREFIX = blog_
三、重新构建镜像
重复先前的步骤,回到 gin-blog
的项目根目录下执行 docker build -t gin-blog-docker .
四、创建并运行一个新容器
关联
Q:我们需要将 Golang
容器和 Mysql
容器关联起来,那么我们需要怎么做呢?
A:增加命令 --link mysql:mysql
让 Golang
容器与 Mysql
容器互联;通过 --link
,可以在容器内直接使用其关联的容器别名进行访问,而不通过IP,但是--link
只能解决单机容器间的关联,在分布式多机的情况下,需要通过别的方式进行连接
运行
执行命令 docker run --link mysql:mysql -p 8000:8000 gin-blog-docker
$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
...
Actual pid is 1
结果
检查启动输出、接口测试、数据库内数据,均正常;我们的 Golang
容器和 Mysql
容器成功关联运行,大功告成 🙂
Review
思考
虽然应用已经能够跑起来了
但如果对 Golang
和 Docker
有一定的了解,我希望你能够想到至少2个问题
- 为什么
gin-blog-docker
占用空间这么大?(可用docker ps -as | grep gin-blog-docker
查看) Mysql
容器直接这么使用,数据存储到哪里去了?
创建超小的Golang镜像
Q:第一个问题,为什么这么镜像体积这么大?
A:FROM golang:latest
拉取的是官方 golang
镜像,包含Golang的编译和运行环境,外加一堆GCC、build工具,相当齐全
这是有问题的,我们可以不在Golang容器中现场编译的,压根用不到那些东西,我们只需要一个能够运行可执行文件的环境即可
构建Scratch镜像
Scratch镜像,简洁、小巧,基本是个空镜像
一、修改Dockerfile
FROM scratch
WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
EXPOSE 8000
CMD ["./go-gin-example"]
二、编译可执行文件
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-gin-example .
编译所生成的可执行文件会依赖一些库,并且是动态链接。在这里因为使用的是 scratch
镜像,它是空镜像,因此我们需要将生成的可执行文件静态链接所依赖的库
三、构建镜像
$ docker build -t gin-blog-docker-scratch .
Sending build context to Docker daemon 133.1 MB
Step 1/5 : FROM scratch
--->
Step 2/5 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example
---> Using cache
---> ee07e166a638
Step 3/5 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example
---> 1489a0693d51
Removing intermediate container e3e9efc0fe4d
Step 4/5 : EXPOSE 8000
---> Running in b5630de5544a
---> 6993e9f8c944
Removing intermediate container b5630de5544a
Step 5/5 : CMD ./go-gin-example
---> Running in eebc0d8628ae
---> 5310bebeb86a
Removing intermediate container eebc0d8628ae
Successfully built 5310bebeb86a
注意,假设你的Golang应用没有依赖任何的配置等文件,是可以直接把可执行文件给拷贝进去即可,其他都不必关心
这里可以有好几种解决方案
- 依赖文件统一管理挂载
- go-bindata 一下
…
因此这里如果解决了文件依赖的问题后,就不需要把目录给 COPY
进去了
四、运行
$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker-scratch
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /auth --> github.com/EDDYCJY/go-gin-example/routers/api.GetAuth (3 handlers)
...
成功运行,程序也正常接收请求
接下来我们再看看占用大小,执行 docker ps -as
命令
$ docker ps -as
CONTAINER ID IMAGE COMMAND ... SIZE
9ebdba5a8445 gin-blog-docker-scratch "./go-gin-example" ... 0 B (virtual 132 MB)
427ee79e6857 gin-blog-docker "./go-gin-example" ... 0 B (virtual 946 MB)
从结果而言,占用大小以Scratch
镜像为基础的容器完胜,完成目标
Mysql挂载数据卷
倘若不做任何干涉,在每次启动一个 Mysql
容器时,数据库都是空的。另外容器删除之后,数据就丢失了(还有各类意外情况),非常糟糕!
数据卷
数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v
这个命令
数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
- 数据卷 可以在容器之间共享和重用
- 对 数据卷 的修改会立马生效
- 对 数据卷 的更新,不会影响镜像
- 数据卷 默认会一直存在,即使容器被删除
注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的 数据卷。
如何挂载
首先创建一个目录用于存放数据卷;示例目录 /data/docker-mysql
,注意 --name
原本名称为 mysql
的容器,需要将其删除 docker rm
$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -v /data/docker-mysql:/var/lib/mysql -d mysql
54611dbcd62eca33fb320f3f624c7941f15697d998f40b24ee535a1acf93ae72
创建成功,检查目录 /data/docker-mysql
,下面多了不少数据库文件
验证
接下来交由你进行验证,目标是创建一些测试表和数据,然后删除当前容器,重新创建的容器,数据库数据也依然存在(当然了数据卷指向要一致)
我已验证完毕,你呢?
Gin实践 连载十 定制 GORM Callbacks

GORM itself is powered by Callbacks, so you could fully customize GORM as you want
原文地址:定制 GORM Callbacks
项目地址:https://github.com/EDDYCJY/go…
GORM 本身是由回调驱动的,所以我们可以根据需要完全定制 GORM,以此达到我们的目的
- 注册一个新的回调
- 删除现有的回调
- 替换现有的回调
- 注册回调的顺序
在 GORM 中包含以上四类 Callbacks,我们结合项目选用 “替换现有的回调” 来解决一个小痛点
问题
在 models 目录下,我们包含 tag.go 和 article.go 两个文件,他们有一个问题,就是 BeforeCreate、BeforeUpdate 重复出现了,那难道 100 个文件,就要写一百次吗?
1、tag.go
2、article.go
显然这是不可能的,如果先前你已经意识到这个问题,那挺OK,但没有的话,现在开始就要改
解决
在这里我们通过 Callbacks 来实现功能,不需要一个个文件去编写
实现Callbacks
打开 models 目录下的 models.go 文件,实现以下两个方法:
1、updateTimeStampForCreateCallback
// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
if !scope.HasError() {
nowTime := time.Now().Unix()
if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
if createTimeField.IsBlank {
createTimeField.Set(nowTime)
}
}
if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
if modifyTimeField.IsBlank {
modifyTimeField.Set(nowTime)
}
}
}
}
在这段方法中,会完成以下功能
- 检查是否有含有错误(db.Error)
scope.FieldByName
通过scope.Fields()
获取所有字段,判断当前是否包含所需字段
for _, field := range scope.Fields() {
if field.Name == name || field.DBName == name {
return field, true
}
if field.DBName == dbName {
mostMatchedField = field
}
}
field.IsBlank
可判断该字段的值是否为空
func isBlank(value reflect.Value) bool {
switch value.Kind() {
case reflect.String:
return value.Len() == 0
case reflect.Bool:
return !value.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return value.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return value.Uint() == 0
case reflect.Float32, reflect.Float64:
return value.Float() == 0
case reflect.Interface, reflect.Ptr:
return value.IsNil()
}
return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}
- 若为空则
field.Set
用于给该字段设置值,参数为interface{}
2、updateTimeStampForUpdateCallback
// updateTimeStampForUpdateCallback will set `ModifyTime` when updating
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
if _, ok := scope.Get("gorm:update_column"); !ok {
scope.SetColumn("ModifiedOn", time.Now().Unix())
}
}
scope.Get(...)
根据入参获取设置了字面值的参数,例如本文中是gorm:update_column
,它会去查找含这个字面值的字段属性scope.SetColumn(...)
假设没有指定update_column
的字段,我们默认在更新回调设置ModifiedOn
的值
注册Callbacks
在上面小节我已经把回调方法编写好了,接下来需要将其注册进 GORM 的钩子里,但其本身自带 Create 和 Update 回调,因此调用替换即可
在 models.go 的 init 函数中,增加以下语句
db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)
验证
访问 AddTag 接口,成功后检查数据库,可发现 created_on
和 modified_on
字段都为当前执行时间
访问 EditTag 接口,可发现 modified_on
为最后一次执行更新的时间
拓展
我们想到,在实际项目中硬删除是较少存在的,那么是否可以通过 Callbacks 来完成这个功能呢?
答案是可以的,我们在先前 Model struct
增加 DeletedOn
变量
type Model struct {
ID int `gorm:"primary_key" json:"id"`
CreatedOn int `json:"created_on"`
ModifiedOn int `json:"modified_on"`
DeletedOn int `json:"deleted_on"`
}
实现Callbacks
打开 models 目录下的 models.go 文件,实现以下方法:
func deleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")
if !scope.Search.Unscoped && hasDeletedOnField {
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedOnField.DBName),
scope.AddToVars(time.Now().Unix()),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
} else {
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
}
func addExtraSpaceIfExist(str string) string {
if str != "" {
return " " + str
}
return ""
}
scope.Get("gorm:delete_option")
检查是否手动指定了delete_optionscope.FieldByName("DeletedOn")
获取我们约定的删除字段,若存在则UPDATE
软删除,若不存在则DELETE
硬删除scope.QuotedTableName()
返回引用的表名,这个方法 GORM 会根据自身逻辑对表名进行一些处理scope.CombinedConditionSql()
返回组合好的条件SQL,看一下方法原型很明了
func (scope *Scope) CombinedConditionSql() string {
joinSQL := scope.joinsSQL()
whereSQL := scope.whereSQL()
if scope.Search.raw {
whereSQL = strings.TrimSuffix(strings.TrimPrefix(whereSQL, "WHERE ("), ")")
}
return joinSQL + whereSQL + scope.groupSQL() +
scope.havingSQL() + scope.orderSQL() + scope.limitAndOffsetSQL()
}
scope.AddToVars
该方法可以添加值作为SQL的参数,也可用于防范SQL注入
func (scope *Scope) AddToVars(value interface{}) string {
_, skipBindVar := scope.InstanceGet("skip_bindvar")
if expr, ok := value.(*expr); ok {
exp := expr.expr
for _, arg := range expr.args {
if skipBindVar {
scope.AddToVars(arg)
} else {
exp = strings.Replace(exp, "?", scope.AddToVars(arg), 1)
}
}
return exp
}
scope.SQLVars = append(scope.SQLVars, value)
if skipBindVar {
return "?"
}
return scope.Dialect().BindVar(len(scope.SQLVars))
}
注册Callbacks
在 models.go 的 init 函数中,增加以下删除的回调
db.Callback().Delete().Replace("gorm:delete", deleteCallback)
验证
重启服务,访问 DeleteTag 接口,成功后即可发现 deleted_on 字段有值
小结
在这一章节中,我们结合 GORM 完成了新增、更新、查询的 Callbacks,在实际项目中常常也是这么使用
毕竟,一个钩子的事,就没有必要自己手写过多不必要的代码了
(注意,增加了软删除后,先前的代码需要增加 deleted_on
的判断)
参考
本系列示例代码
本系列目录
- 连载一 Golang介绍与环境安装
- 连载二 搭建Blog API’s(一)
- 连载三 搭建Blog API’s(二)
- 连载四 搭建Blog API’s(三)
- 连载五 使用JWT进行身份校验
- 连载六 编写一个简单的文件日志
- 连载七 Golang优雅重启HTTP服务
- 连载八 为它加上Swagger
- 连载九 将Golang应用部署到Docker
- 连载十 定制 GORM Callbacks
- 连载十一 Cron定时任务
- 连载十二 优化配置结构及实现图片上传
- 连载十三 优化你的应用结构和实现Redis缓存
- 连载十四 实现导出、导入 Excel
- 连载十五 生成二维码、合并海报
- 连载十六 在图片上绘制文字
- 连载十七 用 Nginx 部署 Go 应用
- 番外 Golang交叉编译
- 番外 请入门 Makefile
相关文档
- Gin
- Gin Web Framework
- Go并发编程实战
- govendor
