Gin搭建Blog API’s (一)

项目地址:https://github.com/EDDYCJY/go-gin-example

思考

首先,在一个初始项目开始前,大家都要思考一下

  • 程序的文本配置写在代码中,好吗?

  • 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
  • conf:用于存储配置文件
  • middleware:应用中间件
  • models:应用数据库模型
  • pkg:第三方包
  • routers 路由逻辑处理
  • runtime:应用运行时数据

添加 Go Modules Replace

打开 go.mod 文件,新增 replace 配置项,如下:

  1. module github.com/EDDYCJY/go-gin-example
  2. go 1.13
  3. require (...)
  4. replace (
  5. github.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting
  6. github.com/EDDYCJY/go-gin-example/conf => ~/go-application/go-gin-example/pkg/conf
  7. github.com/EDDYCJY/go-gin-example/middleware => ~/go-application/go-gin-example/middleware
  8. github.com/EDDYCJY/go-gin-example/models => ~/go-application/go-gin-example/models
  9. github.com/EDDYCJY/go-gin-example/routers => ~/go-application/go-gin-example/routers
  10. )

可能你会不理解为什么要特意跑来加 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='文章标签管理';

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='文章管理';

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. INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');

编写项目配置包

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

接下来我们需要编写基础的应用配置文件,在 go-gin-exampleconf目录下新建app.ini文件,写入内容:

  1. #debug or release
  2. RUN_MODE = debug
  3. [app]
  4. PAGE_SIZE = 10
  5. JWT_SECRET = 23347$040412
  6. [server]
  7. HTTP_PORT = 8000
  8. READ_TIMEOUT = 60
  9. WRITE_TIMEOUT = 60
  10. [database]
  11. TYPE = mysql
  12. USER = 数据库账号
  13. PASSWORD = 数据库密码
  14. #127.0.0.1:3306
  15. HOST = 数据库IP:数据库端口号
  16. NAME = blog
  17. TABLE_PREFIX = blog_

建立调用配置的setting模块,在go-gin-examplepkg目录下新建setting目录(注意新增 replace 配置),新建 setting.go 文件,写入内容:

  1. package setting
  2. import (
  3. "log"
  4. "time"
  5. "github.com/go-ini/ini"
  6. )
  7. var (
  8. Cfg *ini.File
  9. RunMode string
  10. HTTPPort int
  11. ReadTimeout time.Duration
  12. WriteTimeout time.Duration
  13. PageSize int
  14. JwtSecret string
  15. )
  16. func init() {
  17. var err error
  18. Cfg, err = ini.Load("conf/app.ini")
  19. if err != nil {
  20. log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
  21. }
  22. LoadBase()
  23. LoadServer()
  24. LoadApp()
  25. }
  26. func LoadBase() {
  27. RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
  28. }
  29. func LoadServer() {
  30. sec, err := Cfg.GetSection("server")
  31. if err != nil {
  32. log.Fatalf("Fail to get section 'server': %v", err)
  33. }
  34. HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
  35. ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
  36. WriteTimeout = time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
  37. }
  38. func LoadApp() {
  39. sec, err := Cfg.GetSection("app")
  40. if err != nil {
  41. log.Fatalf("Fail to get section 'app': %v", err)
  42. }
  43. JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
  44. PageSize = sec.Key("PAGE_SIZE").MustInt(10)
  45. }

当前的目录结构:

  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

编写API错误码包

建立错误码的e模块,在go-gin-examplepkg目录下新建e目录(注意新增 replace 配置),新建code.gomsg.go文件,写入内容:

1、 code.go:

  1. package e
  2. const (
  3. SUCCESS = 200
  4. ERROR = 500
  5. INVALID_PARAMS = 400
  6. ERROR_EXIST_TAG = 10001
  7. ERROR_NOT_EXIST_TAG = 10002
  8. ERROR_NOT_EXIST_ARTICLE = 10003
  9. ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
  10. ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
  11. ERROR_AUTH_TOKEN = 20003
  12. ERROR_AUTH = 20004
  13. )

2、 msg.go:

  1. package e
  2. var MsgFlags = map[int]string {
  3. SUCCESS : "ok",
  4. ERROR : "fail",
  5. INVALID_PARAMS : "请求参数错误",
  6. ERROR_EXIST_TAG : "已存在该标签名称",
  7. ERROR_NOT_EXIST_TAG : "该标签不存在",
  8. ERROR_NOT_EXIST_ARTICLE : "该文章不存在",
  9. ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鉴权失败",
  10. ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超时",
  11. ERROR_AUTH_TOKEN : "Token生成失败",
  12. ERROR_AUTH : "Token错误",
  13. }
  14. func GetMsg(code int) string {
  15. msg, ok := MsgFlags[code]
  16. if ok {
  17. return msg
  18. }
  19. return MsgFlags[ERROR]
  20. }

编写工具包

go-gin-examplepkg目录下新建util目录(注意新增 replace 配置),并拉取com的依赖包,如下:

  1. go get -u github.com/unknwon/com

编写分页页码的获取方法

util目录下新建pagination.go,写入内容:

  1. package util
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/unknwon/com"
  5. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  6. )
  7. func GetPage(c *gin.Context) int {
  8. result := 0
  9. page, _ := com.StrTo(c.Query("page")).Int()
  10. if page > 0 {
  11. result = (page - 1) * setting.PageSize
  12. }
  13. return result
  14. }

编写models init

拉取gorm的依赖包,如下:

  1. go get -u github.com/jinzhu/gorm

拉取mysql驱动的依赖包,如下:

  1. go get -u github.com/go-sql-driver/mysql

完成后,在go-gin-examplemodels目录下新建models.go,用于models的初始化使用

  1. package models
  2. import (
  3. "log"
  4. "fmt"
  5. "github.com/jinzhu/gorm"
  6. _ "github.com/jinzhu/gorm/dialects/mysql"
  7. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  8. )
  9. var db *gorm.DB
  10. type Model struct {
  11. ID int `gorm:"primary_key" json:"id"`
  12. CreatedOn int `json:"created_on"`
  13. ModifiedOn int `json:"modified_on"`
  14. }
  15. func init() {
  16. var (
  17. err error
  18. dbType, dbName, user, password, host, tablePrefix string
  19. )
  20. sec, err := setting.Cfg.GetSection("database")
  21. if err != nil {
  22. log.Fatal(2, "Fail to get section 'database': %v", err)
  23. }
  24. dbType = sec.Key("TYPE").String()
  25. dbName = sec.Key("NAME").String()
  26. user = sec.Key("USER").String()
  27. password = sec.Key("PASSWORD").String()
  28. host = sec.Key("HOST").String()
  29. tablePrefix = sec.Key("TABLE_PREFIX").String()
  30. db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
  31. user,
  32. password,
  33. host,
  34. dbName))
  35. if err != nil {
  36. log.Println(err)
  37. }
  38. gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string {
  39. return tablePrefix + defaultTableName;
  40. }
  41. db.SingularTable(true)
  42. db.LogMode(true)
  43. db.DB().SetMaxIdleConns(10)
  44. db.DB().SetMaxOpenConns(100)
  45. }
  46. func CloseDB() {
  47. defer db.Close()
  48. }

编写项目启动、路由文件

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

编写Demo

go-gin-example下建立main.go作为启动文件(也就是main包),我们先写个Demo,帮助大家理解,写入文件内容:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "github.com/gin-gonic/gin"
  6. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  7. )
  8. func main() {
  9. router := gin.Default()
  10. router.GET("/test", func(c *gin.Context) {
  11. c.JSON(200, gin.H{
  12. "message": "test",
  13. })
  14. })
  15. s := &http.Server{
  16. Addr: fmt.Sprintf(":%d", setting.HTTPPort),
  17. Handler: router,
  18. ReadTimeout: setting.ReadTimeout,
  19. WriteTimeout: setting.WriteTimeout,
  20. MaxHeaderBytes: 1 << 20,
  21. }
  22. s.ListenAndServe()
  23. }

执行go run main.go,查看命令行是否显示

  1. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
  2. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
  3. - using env: export GIN_MODE=release
  4. - using code: gin.SetMode(gin.ReleaseMode)
  5. [GIN-debug] GET /test --> main.main.func1 (3 handlers)

在本机执行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. }
  • 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. }

开始监听服务,监听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. address := resolveAddress(addr)
  4. debugPrint("Listening and serving HTTP on %s\n", address)
  5. err = http.ListenAndServe(address, engine)
  6. return
  7. }

通过分析源码,得知本质上没有区别,同时也得知了启动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. }

大家可以看到默认情况下,已经附加了日志、恢复中间件的引擎实例。并且在开头调用了debugPrintWARNINGDefault(),而它的实现就是输出该行日志

  1. func debugPrintWARNINGDefault() {
  2. debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
  3. `)
  4. }

而另外一个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. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  5. )
  6. func InitRouter() *gin.Engine {
  7. r := gin.New()
  8. r.Use(gin.Logger())
  9. r.Use(gin.Recovery())
  10. gin.SetMode(setting.RunMode)
  11. r.GET("/test", func(c *gin.Context) {
  12. c.JSON(200, gin.H{
  13. "message": "test",
  14. })
  15. })
  16. return r
  17. }

修改main.go的文件内容:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "github.com/EDDYCJY/go-gin-example/routers"
  6. "github.com/EDDYCJY/go-gin-example/pkg/setting"
  7. )
  8. func main() {
  9. router := routers.InitRouter()
  10. s := &http.Server{
  11. Addr: fmt.Sprintf(":%d", setting.HTTPPort),
  12. Handler: router,
  13. ReadTimeout: setting.ReadTimeout,
  14. WriteTimeout: setting.WriteTimeout,
  15. MaxHeaderBytes: 1 << 20,
  16. }
  17. s.ListenAndServe()
  18. }

当前目录结构:

  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

重启服务,执行 curl 127.0.0.1:8000/test查看是否正确返回。

下一节,我们将以我们的 Demo 为起点进行修改,开始编码!

参考

本系列示例代码

关于

修改记录

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

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

我的公众号

image