3.2 Gin搭建Blog API's (一)

思考

首先,在一个初始项目开始前,大家都要思考一下
    程序的文本配置写在代码中,好吗?
    API 的错误码硬编码在程序中,合适吗?
    db句柄谁都去Open,没有统一管理,好吗?
    获取分页等公共参数,谁都自己写一套逻辑,好吗?
显然在较正规的项目中,这些问题的答案都是不可以,为了解决这些问题,我们挑选一款读写配置文件的库,目前比较火的有 viper,有兴趣你未来可以简单了解一下,没兴趣的话等以后接触到再说。
但是本系列选用 go-ini/ini ,它的 中文文档。大家是必须需要要简单阅读它的文档,再接着完成后面的内容。

本文目标

    编写一个简单的API错误码包。
    完成一个 Demo 示例。
    讲解 Demo 所涉及的知识点。

介绍和初始化项目

初始化项目目录

在前一章节中,我们初始化了一个 go-gin-example 项目,接下来我们需要继续新增如下目录结构:
1
go-gin-example/
2
├── conf
3
├── middleware
4
├── models
5
├── pkg
6
├── routers
7
└── runtime
Copied!
    conf:用于存储配置文件
    middleware:应用中间件
    models:应用数据库模型
    pkg:第三方包
    routers 路由逻辑处理
    runtime:应用运行时数据

添加 Go Modules Replace

打开 go.mod 文件,新增 replace 配置项,如下:
1
module github.com/EDDYCJY/go-gin-example
2
3
go 1.13
4
5
require (...)
6
7
replace (
8
github.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting
9
github.com/EDDYCJY/go-gin-example/conf => ~/go-application/go-gin-example/pkg/conf
10
github.com/EDDYCJY/go-gin-example/middleware => ~/go-application/go-gin-example/middleware
11
github.com/EDDYCJY/go-gin-example/models => ~/go-application/go-gin-example/models
12
github.com/EDDYCJY/go-gin-example/routers => ~/go-application/go-gin-example/routers
13
)
Copied!
可能你会不理解为什么要特意跑来加 replace 配置项,首先你要看到我们使用的是完整的外部模块引用路径(github.com/EDDYCJY/go-gin-example/xxx),而这个模块还没推送到远程,是没有办法下载下来的,因此需要用 replace 将其指定读取本地的模块路径,这样子就可以解决本地模块读取的问题。
注:后续每新增一个本地应用目录,你都需要主动去 go.mod 文件里新增一条 replace(我不会提醒你),如果你漏了,那么编译时会出现报错,找不到那个模块。

初始项目数据库

新建 blog 数据库,编码为utf8_general_ci,在 blog 数据库下,新建以下表
1、 标签表
1
CREATE TABLE `blog_tag` (
2
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
3
`name` varchar(100) DEFAULT '' COMMENT '标签名称',
4
`created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
5
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
6
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
7
`modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
8
`deleted_on` int(10) unsigned DEFAULT '0',
9
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
10
PRIMARY KEY (`id`)
11
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';
Copied!
2、 文章表
1
CREATE TABLE `blog_article` (
2
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
3
`tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',
4
`title` varchar(100) DEFAULT '' COMMENT '文章标题',
5
`desc` varchar(255) DEFAULT '' COMMENT '简述',
6
`content` text,
7
`created_on` int(11) DEFAULT NULL,
8
`created_by` varchar(100) DEFAULT '' COMMENT '创建人',
9
`modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
10
`modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
11
`deleted_on` int(10) unsigned DEFAULT '0',
12
`state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',
13
PRIMARY KEY (`id`)
14
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
Copied!
3、 认证表
1
CREATE TABLE `blog_auth` (
2
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
3
`username` varchar(50) DEFAULT '' COMMENT '账号',
4
`password` varchar(50) DEFAULT '' COMMENT '密码',
5
PRIMARY KEY (`id`)
6
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
7
8
INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');
Copied!

编写项目配置包

go-gin-example 应用目录下,拉取 go-ini/ini 的依赖包,如下:
1
$ go get -u github.com/go-ini/ini
2
go: finding github.com/go-ini/ini v1.48.0
3
go: downloading github.com/go-ini/ini v1.48.0
4
go: extracting github.com/go-ini/ini v1.48.0
Copied!
接下来我们需要编写基础的应用配置文件,在 go-gin-exampleconf目录下新建app.ini文件,写入内容:
1
#debug or release
2
RUN_MODE = debug
3
4
[app]
5
PAGE_SIZE = 10
6
JWT_SECRET = 23347$040412
7
8
[server]
9
HTTP_PORT = 8000
10
READ_TIMEOUT = 60
11
WRITE_TIMEOUT = 60
12
13
[database]
14
TYPE = mysql
15
USER = 数据库账号
16
PASSWORD = 数据库密码
17
#127.0.0.1:3306
18
HOST = 数据库IP:数据库端口号
19
NAME = blog
20
TABLE_PREFIX = blog_
Copied!
建立调用配置的setting模块,在go-gin-examplepkg目录下新建setting目录(注意新增 replace 配置),新建 setting.go 文件,写入内容:
1
package setting
2
3
import (
4
"log"
5
"time"
6
7
"github.com/go-ini/ini"
8
)
9
10
var (
11
Cfg *ini.File
12
13
RunMode string
14
15
HTTPPort int
16
ReadTimeout time.Duration
17
WriteTimeout time.Duration
18
19
PageSize int
20
JwtSecret string
21
)
22
23
func init() {
24
var err error
25
Cfg, err = ini.Load("conf/app.ini")
26
if err != nil {
27
log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
28
}
29
30
LoadBase()
31
LoadServer()
32
LoadApp()
33
}
34
35
func LoadBase() {
36
RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
37
}
38
39
func LoadServer() {
40
sec, err := Cfg.GetSection("server")
41
if err != nil {
42
log.Fatalf("Fail to get section 'server': %v", err)
43
}
44
45
HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
46
ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
47
WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
48
}
49
50
func LoadApp() {
51
sec, err := Cfg.GetSection("app")
52
if err != nil {
53
log.Fatalf("Fail to get section 'app': %v", err)
54
}
55
56
JwtSecret = sec.Key("JWT_SECRET").MustString("[email protected])*#)[email protected]#@*[email protected]!)")
57
PageSize = sec.Key("PAGE_SIZE").MustInt(10)
58
}
Copied!
当前的目录结构:
1
go-gin-example
2
├── conf
3
│ └── app.ini
4
├── go.mod
5
├── go.sum
6
├── middleware
7
├── models
8
├── pkg
9
│ └── setting.go
10
├── routers
11
└── runtime
Copied!

编写API错误码包

建立错误码的e模块,在go-gin-examplepkg目录下新建e目录(注意新增 replace 配置),新建code.gomsg.go文件,写入内容:
1、 code.go:
1
package e
2
3
const (
4
SUCCESS = 200
5
ERROR = 500
6
INVALID_PARAMS = 400
7
8
ERROR_EXIST_TAG = 10001
9
ERROR_NOT_EXIST_TAG = 10002
10
ERROR_NOT_EXIST_ARTICLE = 10003
11
12
ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
13
ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
14
ERROR_AUTH_TOKEN = 20003
15
ERROR_AUTH = 20004
16
)
Copied!
2、 msg.go:
1
package e
2
3
var MsgFlags = map[int]string {
4
SUCCESS : "ok",
5
ERROR : "fail",
6
INVALID_PARAMS : "请求参数错误",
7
ERROR_EXIST_TAG : "已存在该标签名称",
8
ERROR_NOT_EXIST_TAG : "该标签不存在",
9
ERROR_NOT_EXIST_ARTICLE : "该文章不存在",
10
ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鉴权失败",
11
ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超时",
12
ERROR_AUTH_TOKEN : "Token生成失败",
13
ERROR_AUTH : "Token错误",
14
}
15
16
func GetMsg(code int) string {
17
msg, ok := MsgFlags[code]
18
if ok {
19
return msg
20
}
21
22
return MsgFlags[ERROR]
23
}
Copied!

编写工具包

go-gin-examplepkg目录下新建util目录(注意新增 replace 配置),并拉取com的依赖包,如下:
1
go get -u github.com/unknwon/com
Copied!

编写分页页码的获取方法

util目录下新建pagination.go,写入内容:
1
package util
2
3
import (
4
"github.com/gin-gonic/gin"
5
"github.com/unknwon/com"
6
7
"github.com/EDDYCJY/go-gin-example/pkg/setting"
8
)
9
10
func GetPage(c *gin.Context) int {
11
result := 0
12
page, _ := com.StrTo(c.Query("page")).Int()
13
if page > 0 {
14
result = (page - 1) * setting.PageSize
15
}
16
17
return result
18
}
Copied!

编写models init

拉取gorm的依赖包,如下:
1
go get -u github.com/jinzhu/gorm
Copied!
拉取mysql驱动的依赖包,如下:
1
go get -u github.com/go-sql-driver/mysql
Copied!
完成后,在go-gin-examplemodels目录下新建models.go,用于models的初始化使用
1
package models
2
3
import (
4
"log"
5
"fmt"
6
7
"github.com/jinzhu/gorm"
8
_ "github.com/jinzhu/gorm/dialects/mysql"
9
10
"github.com/EDDYCJY/go-gin-example/pkg/setting"
11
)
12
13
var db *gorm.DB
14
15
type Model struct {
16
ID int `gorm:"primary_key" json:"id"`
17
CreatedOn int `json:"created_on"`
18
ModifiedOn int `json:"modified_on"`
19
}
20
21
func init() {
22
var (
23
err error
24
dbType, dbName, user, password, host, tablePrefix string
25
)
26
27
sec, err := setting.Cfg.GetSection("database")
28
if err != nil {
29
log.Fatal(2, "Fail to get section 'database': %v", err)
30
}
31
32
dbType = sec.Key("TYPE").String()
33
dbName = sec.Key("NAME").String()
34
user = sec.Key("USER").String()
35
password = sec.Key("PASSWORD").String()
36
host = sec.Key("HOST").String()
37
tablePrefix = sec.Key("TABLE_PREFIX").String()
38
39
db, err = gorm.Open(dbType, fmt.Sprintf("%s:%[email protected](%s)/%s?charset=utf8&parseTime=True&loc=Local",
40
user,
41
password,
42
host,
43
dbName))
44
45
if err != nil {
46
log.Println(err)
47
}
48
49
gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
50
return tablePrefix + defaultTableName;
51
}
52
53
db.SingularTable(true)
54
db.LogMode(true)
55
db.DB().SetMaxIdleConns(10)
56
db.DB().SetMaxOpenConns(100)
57
}
58
59
func CloseDB() {
60
defer db.Close()
61
}
Copied!

编写项目启动、路由文件

最基础的准备工作完成啦,让我们开始编写Demo吧!

编写Demo

go-gin-example下建立main.go作为启动文件(也就是main包),我们先写个Demo,帮助大家理解,写入文件内容:
1
package main
2
3
import (
4
"fmt"
5
"net/http"
6
7
"github.com/gin-gonic/gin"
8
9
"github.com/EDDYCJY/go-gin-example/pkg/setting"
10
)
11
12
func main() {
13
router := gin.Default()
14
router.GET("/test", func(c *gin.Context) {
15
c.JSON(200, gin.H{
16
"message": "test",
17
})
18
})
19
20
s := &http.Server{
21
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
22
Handler: router,
23
ReadTimeout: setting.ReadTimeout,
24
WriteTimeout: setting.WriteTimeout,
25
MaxHeaderBytes: 1 << 20,
26
}
27
28
s.ListenAndServe()
29
}
Copied!
执行go run main.go,查看命令行是否显示
1
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
2
3
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
4
- using env: export GIN_MODE=release
5
- using code: gin.SetMode(gin.ReleaseMode)
6
7
[GIN-debug] GET /test --> main.main.func1 (3 handlers)
Copied!
在本机执行curl 127.0.0.1:8000/test,检查是否返回{"message":"test"}

知识点

那么,我们来延伸一下Demo所涉及的知识点!

标准库

    fmt:实现了类似C语言printf和scanf的格式化I/O。格式化动作('verb')源自C语言但更简单
    net/http:提供了HTTP客户端和服务端的实现

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.ContextContextgin中的上下文,它允许我们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin中包含大量Context的方法,例如我们常用的DefaultQueryQueryDefaultPostFormPostForm等等

&http.Server 和 ListenAndServe?

1、http.Server:
1
type Server struct {
2
Addr string
3
Handler Handler
4
TLSConfig *tls.Config
5
ReadTimeout time.Duration
6
ReadHeaderTimeout time.Duration
7
WriteTimeout time.Duration
8
IdleTimeout time.Duration
9
MaxHeaderBytes int
10
ConnState func(net.Conn, ConnState)
11
ErrorLog *log.Logger
12
}
Copied!
    Addr:监听的TCP地址,格式为:8000
    Handler:http句柄,实质为ServeHTTP,用于处理程序响应HTTP请求
    TLSConfig:安全传输层协议(TLS)的配置
    ReadTimeout:允许读取的最大时间
    ReadHeaderTimeout:允许读取请求头的最大时间
    WriteTimeout:允许写入的最大时间
    IdleTimeout:等待的最大时间
    MaxHeaderBytes:请求头的最大字节数
    ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
    ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为nil则默认以日志包的标准日志记录器完成(也就是在控制台输出)
2、 ListenAndServe:
1
func (srv *Server) ListenAndServe() error {
2
addr := srv.Addr
3
if addr == "" {
4
addr = ":http"
5
}
6
ln, err := net.Listen("tcp", addr)
7
if err != nil {
8
return err
9
}
10
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
11
}
Copied!
开始监听服务,监听TCP网络地址,Addr和调用应用程序处理连接上的请求。
我们在源码中看到Addr是调用我们在&http.Server中设置的参数,因此我们在设置时要用&,我们要改变参数的值,因为我们ListenAndServe和其他一些方法需要用到&http.Server中的参数,他们是相互影响的。
3、 http.ListenAndServe连载一r.Run()有区别吗?
我们看看r.Run的实现:
1
func (engine *Engine) Run(addr ...string) (err error) {
2
defer func() { debugPrintError(err) }()
3
4
address := resolveAddress(addr)
5
debugPrint("Listening and serving HTTP on %s\n", address)
6
err = http.ListenAndServe(address, engine)
7
return
8
}
Copied!
通过分析源码,得知本质上没有区别,同时也得知了启动gin时的监听debug信息在这里输出。
4、 为什么Demo里会有WARNING
首先我们可以看下Default()的实现
1
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
2
func Default() *Engine {
3
debugPrintWARNINGDefault()
4
engine := New()
5
engine.Use(Logger(), Recovery())
6
return engine
7
}
Copied!
大家可以看到默认情况下,已经附加了日志、恢复中间件的引擎实例。并且在开头调用了debugPrintWARNINGDefault(),而它的实现就是输出该行日志
1
func debugPrintWARNINGDefault() {
2
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
3
`)
4
}
Copied!
而另外一个Running in "debug" mode. Switch to "release" mode in production.,是运行模式原因,并不难理解,已在配置文件的管控下 :-),运维人员随时就可以修改它的配置。
5、 Demo的router.GET等路由规则可以不写在main包中吗?
我们发现router.GET等路由规则,在Demo中被编写在了main包中,感觉很奇怪,我们去抽离这部分逻辑!
go-gin-examplerouters目录新建router.go文件,写入内容:
1
package routers
2
3
import (
4
"github.com/gin-gonic/gin"
5
6
"github.com/EDDYCJY/go-gin-example/pkg/setting"
7
)
8
9
func InitRouter() *gin.Engine {
10
r := gin.New()
11
12
r.Use(gin.Logger())
13
14
r.Use(gin.Recovery())
15
16
gin.SetMode(setting.RunMode)
17
18
r.GET("/test", func(c *gin.Context) {
19
c.JSON(200, gin.H{
20
"message": "test",
21
})
22
})
23
24
return r
25
}
Copied!
修改main.go的文件内容:
1
package main
2
3
import (
4
"fmt"
5
"net/http"
6
7
"github.com/EDDYCJY/go-gin-example/routers"
8
"github.com/EDDYCJY/go-gin-example/pkg/setting"
9
)
10
11
func main() {
12
router := routers.InitRouter()
13
14
s := &http.Server{
15
Addr: fmt.Sprintf(":%d", setting.HTTPPort),
16
Handler: router,
17
ReadTimeout: setting.ReadTimeout,
18
WriteTimeout: setting.WriteTimeout,
19
MaxHeaderBytes: 1 << 20,
20
}
21
22
s.ListenAndServe()
23
}
Copied!
当前目录结构:
1
go-gin-example/
2
├── conf
3
│ └── app.ini
4
├── main.go
5
├── middleware
6
├── models
7
│ └── models.go
8
├── pkg
9
│ ├── e
10
│ │ ├── code.go
11
│ │ └── msg.go
12
│ ├── setting
13
│ │ └── setting.go
14
│ └── util
15
│ └── pagination.go
16
├── routers
17
│ └── router.go
18
├── runtime
Copied!
重启服务,执行 curl 127.0.0.1:8000/test查看是否正确返回。
下一节,我们将以我们的 Demo 为起点进行修改,开始编码!

参考

本系列示例代码

关于

修改记录

    第一版:2018年02月16日发布文章
    第二版:2019年10月01日修改文章

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

我的公众号

image
Last modified 2yr ago