Gin搭建Blog API’s (一)

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

    1. 各种的程序配置写在代码中,好吗
    2. API的错误码硬编在程序中,合适吗
    3. db句柄谁都去,好吗
    4. 获取分页等公共参数,不统一管理起来,好吗

    显然在较正规的项目中,这些问题的答案都是不可以

    为了解决这些问题,我们挑选一款读写配置文件的库,本系列中选用 ,它的中文文档。大家需要先简单阅读它的文档,再接着完成后面的内容。

    我们还会编写一个简单的API错误码包,并且完成一个Demo示例和讲解知识点,便于后面的学习。

    介绍和初始化项目

    首先,我们需要增加一个工作区(GOPATH)路径用于我们的Blog项目。

    将你新的工作区加入到/etc/profile中的GOPATH环境变量中, 并在新工作区中,建立binpkgsrc三个目录。

    src目录下创建gin-blog目录,初始的目录结构:

    初始化项目目录

    1. gin-blog/
    2. ├── conf
    3. ├── middleware
    4. ├── models
    5. ├── pkg
    6. ├── routers
    7. └── runtime
    • conf:用于存储配置文件
    • middleware:应用中间件
    • models:应用数据库模型
    • pkg:第三方包
    • routers 路由逻辑处理
    • runtime 应用运行时数据

    新建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. `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
    9. PRIMARY KEY (`id`)
    10. ) 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. `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',
    12. PRIMARY KEY (`id`)
    13. ) 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-ini/ini的依赖包

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

    我们需要编写基础的应用配置文件,在gin-blogconf目录下新建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模块,在gin-blogpkg目录下新建setting目录,新建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. if err != nil {
    31. log.Fatalf("Fail to get section 'server': %v", err)
    32. }
    33. RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
    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. }

    当前的目录结构:

    编写API错误码包

    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. }

    gin-blogpkg目录下新建util目录,

    拉取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. "gin-blog/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

    完成后,在gin-blogmodels目录下新建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. "gin-blog/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.DB().SetMaxIdleConns(10)
    43. db.DB().SetMaxOpenConns(100)
    44. }
    45. func CloseDB() {
    46. }

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

    gin-blog下建立main.go作为启动文件(也就是main包),

    我们先写个Demo,帮助大家理解,写入文件内容:

    执行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所涉及的知识点!

    1、 标准库:

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

    2、 Gin:

    • :返回Gin的type Engine struct{...},里面包含RouterGroup,相当于创建一个路由Handlers,可以后期绑定各类的路由规则和函数、中间件等
    • router.GET(…){…}:创建不同的HTTP方法绑定到Handlers中,也支持POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等常用的Restful方法
    • :就是一个map[string]interface{}
    • gin.ContextContextgin中的上下文,它允许我们在中间件之间传递变量、管理流、验证JSON请求、响应JSON请求等,在gin中包含大量Context的方法,例如我们常用的DefaultQueryQueryDefaultPostFormPostForm等等

    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则默认以日志包的标准日志记录器完成(也就是在控制台输出)

    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中的参数,他们是相互影响的。

    4、 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信息在这里输出。

    5、 为什么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.,是运行模式原因,并不难理解,已在配置文件的管控下 :-),运维人员随时就可以修改它的配置。

    6、 Demo的router.GET等路由规则可以不写在main包中吗?

    我们发现router.GET等路由规则,在Demo中被编写在了main包中,感觉很奇怪,我们去抽离这部分逻辑!

    gin-blogrouters目录新建router.go文件,写入内容:

    1. package routers
    2. import (
    3. "github.com/gin-gonic/gin"
    4. "gin-blog/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. gin-blog/
    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

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

    参考