17

Go Gin 系列二:搭建Blog API's (一)

 4 years ago
source link: https://studygolang.com/articles/25584
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大家好,我是煎鱼,这是本项目的地址: github.com/eddycjy/go-… ,如果有什么问题欢迎随时交流和沟通。

思考

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

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

  • API 的错误码硬编码在程序中,合适吗?

  • db句柄谁都去 Open ,没有统一管理,好吗?

  • 获取分页等公共参数,谁都自己写一套逻辑,好吗?

显然在较正规的项目中,这些问题的答案都是 不可以 ,为了解决这些问题,我们挑选一款读写配置文件的库,目前比较火的有 viper ,有兴趣你未来可以简单了解一下,没兴趣的话等以后接触到再说。

但是本系列选用 go-ini/ini ,它的 中文文档 。大家是必须需要要简单阅读它的文档,再接着完成后面的内容。

本文目标

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

介绍和初始化项目

初始化项目目录

在前一章节中,我们初始化了一个 go-gin-example 项目,接下来我们需要继续新增如下目录结构:

go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
复制代码
  • conf:用于存储配置文件
  • middleware:应用中间件
  • models:应用数据库模型
  • pkg:第三方包
  • routers 路由逻辑处理
  • runtime:应用运行时数据

添加 Go Modules Replace

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

module github.com/EDDYCJY/go-gin-example

go 1.13

require (...)

replace (
		github.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting
		github.com/EDDYCJY/go-gin-example/conf    	  => ~/go-application/go-gin-example/pkg/conf
		github.com/EDDYCJY/go-gin-example/middleware  => ~/go-application/go-gin-example/middleware
		github.com/EDDYCJY/go-gin-example/models 	  => ~/go-application/go-gin-example/models
		github.com/EDDYCJY/go-gin-example/routers 	  => ~/go-application/go-gin-example/routers
)
复制代码

可能你会不理解为什么要特意跑来加 replace 配置项,首先你要看到我们使用的是完整的外部模块引用路径( github.com/EDDYCJY/go-gin-example/xxx ),而这个模块还没推送到远程,是没有办法下载下来的,因此需要用 replace 将其指定读取本地的模块路径,这样子就可以解决本地模块读取的问题。

注:后续每新增一个本地应用目录,你都需要主动去 go.mod 文件里新增一条 replace(我不会提醒你),如果你漏了,那么编译时会出现报错,找不到那个模块。

初始项目数据库

新建 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-gin-example 应用目录下,拉取 go-ini/ini 的依赖包,如下:

$ go get -u github.com/go-ini/ini
go: finding github.com/go-ini/ini v1.48.0
go: downloading github.com/go-ini/ini v1.48.0
go: extracting github.com/go-ini/ini v1.48.0
复制代码

接下来我们需要编写基础的应用配置文件,在 go-gin-exampleconf 目录下新建 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 模块,在 go-gin-examplepkg 目录下新建 setting 目录(注意新增 replace 配置),新建 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)
}
复制代码

当前的目录结构:

go-gin-example
├── conf
│   └── app.ini
├── go.mod
├── go.sum
├── middleware
├── models
├── pkg
│   └── setting.go
├── routers
└── runtime
复制代码

编写API错误码包

建立错误码的 e 模块,在 go-gin-examplepkg 目录下新建 e 目录(注意新增 replace 配置),新建 code.gomsg.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]
}
复制代码

编写工具包

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

go get -u github.com/unknwon/com
复制代码

编写分页页码的获取方法

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

package util

import (
	"github.com/gin-gonic/gin"
	"github.com/unknwon/com"

	"github.com/EDDYCJY/go-gin-example/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
复制代码

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

package models

import (
	"log"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/EDDYCJY/go-gin-example/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.LogMode(true)
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(100)
}

func CloseDB() {
	defer db.Close()
}
复制代码

编写项目启动、路由文件

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

编写Demo

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

package main

import (
    "fmt"
	  "net/http"

    "github.com/gin-gonic/gin"

	  "github.com/EDDYCJY/go-gin-example/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所涉及的知识点!

标准库

  • 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:

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
}
复制代码
:8000
ServeHTTP
nil

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

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

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

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

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

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

package routers

import (
    "github.com/gin-gonic/gin"
    
    "github.com/EDDYCJY/go-gin-example/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"

	"github.com/EDDYCJY/go-gin-example/routers"
	"github.com/EDDYCJY/go-gin-example/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()
}
复制代码

当前目录结构:

go-gin-example/
├── 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 为起点进行修改,开始编码!

参考

本系列示例代码

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK