Gin框架中使用数据库——Go Gin框架(七)

安装MySQL数据库

MySQL官方链接:https://dev.mysql.com/downloads/mysql/
Debian:sudo apt install mysql-5.7
安装过程:略

Go 安装MySQL驱动

go get "github.com/go-sql-driver/mysql"

在项目中使用MySQL

在go mod模式下,还需要在项目中引用mysql模块

go.mod文件

module MonaGinWeb

go 1.15

require (
    github.com/gin-gonic/gin v1.6.3
    github.com/go-sql-driver/mysql v1.5.0
)

在go中连接MySQL

import (
    "database/sql"
    "log"
    "strconv"
)

var db *sql.DB

func init() {
    c := MySQLConnInfo{
        user:       "",
        password:   "",
        connMethod: "tcp",
        host:       "",
        port:       0,
        dbname:     "",
        otherArgs:  "",
    }
    //数据库连接字符串
    //"user:password@tcp(host:port)/dbname"
    connStr := c.user + ":" + c.password +
        "@" + c.connMethod + "(" + c.host + ":" + strconv.Itoa(c.port) + ")/" +
        c.dbname + c.otherArgs
    if temp, err := sql.Open("mysql", connStr); err == nil {
        db = temp
    } else { //连接出现错误
        log.Fatal(err.Error())
        return
    }
}
  • func Open(driverName string, dataSourceName string) (*DB, error):该函数用于打开程序与SQL数据库的连接。其会返回创建(连接成功)的一个DB指针,有两个参数:
    • driverName string:用于指定数据库引擎
    • dataSourceName string:用于传入数据库的连接字符串

MySQL的增删改查

(待补充)

Gin 中间件的编写和使用——Go Gin框架(六)

中间件

在web应用服务中,完整的一个业务处理在技术上包含客户端操作、服务器端处理、返回处理结果给客户端三个步骤。

在实际的业务开发和处理中,会有更负责的业务和需求场景。一个完整的系统可能要包含鉴权认证、权限管理、安全检查、日志记录等多维度的系统支持。

鉴权认证、权限管理、安全检查、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的所有业务都适用。

由此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效的将具体业务和系统功能进行解耦,并且,还可以达到灵活配置的目的。

这种通用业务独立开发并灵活配置使用的组件,一般称之为”中间件”,因为其位于服务器和实际业务处理程序之间。其含义就是相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。中间件的位置和角色示意图如下图所示:

Gin的中间件

定义:

// HandlerFunc defines(定义) the handler(处理器) used by gin middleware(中间件) as return value.
// HandleFunc函数 定义了一个返回值可以被用作gin中间件的处理器。
type HandlerFunc func(*Context)

从一开始创建Gin engine使用的函数gin.Default()源码如下:

// Default returns an Engine instance(实例) with the Logger and Recovery middleware(中间件) already attached(附加).
// Default函数 返回一个已经附加了Logger和Recovery的中间件引擎实例
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

其中第5行enging.Use(Logger(), Recovery())就是一个使用中间件的实例。

  • func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes:对引擎中所有的请求,附加中间件。
  • 参数 middleware ...HandlerFunc:需要被添加的多个中间件
// Use attaches(附加、附属品) a global(总体的,全局的) middleware to the router.
//ie. the middleware attached though Use() will be included in the handlers chain(链) for every single(单一的) request.
//【中间件将通过Use()被附加在每一个处理器请求中。】
//Even(即使、甚至) 404, 405, static files...
//For example, this is the right(正确的) place for a logger or error management(管理) middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}
  • Logger() 返回的是一个日志的中间件,它可以在控制台输出调试日志。
// Logger instances(实例) a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc {
    return LoggerWithConfig(LoggerConfig{})
}
  • Recovery() 返回的是一个用于捕获任何panic的中间件,并在捕获之后,如果只有一个panic的情况下,其向客户端返回一个500错误。
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc {
    return RecoveryWithWriter(DefaultErrorWriter)
}

如何自定义一个中间件

根据上文实例,中间件有两个要点:
1. 中间件是一个func
2. 该func返回值的类型是HandleFunc

例:

方法一:使用 Use() 函数

该方法适用于为一整个engine中所有的请求添加中间件。

中间件调用函数

func main(){
    engine := gin.Default()
    engine.POST("/yyy",funcfunc(context *gin.Context){
        /*TODO 业务逻辑*/
    })//这个yyy的POST请求没有绑定中间件

    //第一种添加中间件的方法(为以下所有方法添加中间件)
    engine.Use(requestInfo())//从这行代码以下定义的请求才会绑定requestInfo()中间件

    engine.Get("/xxx", funcfunc(context *gin.Context){
        /*TODO 业务逻辑*/
    })//该xxx的GET请求已绑定了request中间件。
    engine.Run()
}

中间件函数

//用于向控制台输出请求信息的中间件
func requestInfo() gin.HandlerFunc {
    return func(context *gin.Context) {
        //执行具体业务之前运行
        fmt.Println("(middleware)request route: ", context.FullPath(), "     request method: ", context.Request.Method)
        fmt.Println("(middleware)wrong status code:", context.Writer.Status()) //这里获取到的状态码应该是错误的(永远都是200)

        //开始执行具体业务
        context.Next()

        //执行完具体业务之后
        fmt.Println("(middleware)right status code:", context.Writer.Status())
    }
}

方法二:单个Handle使用中间件的方式

该方法适用于对某一个Handle处理器使用特定的中间件

处理器函数

engine.GET("/M4SH", requestInfo(), func(context *gin.Context) {
    fmt.Println("↓---运行具体业务---↓")
    context.JSON(404, map[string]interface{}{
        "code": 0,
        "msg":  "咕噜灵波",
    })
    fmt.Println("↑---运行具体业务---↑")
})
  • func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes:第一种方法Use()函数,只为在该语句后面的处理器函数附加中间件。其使用了可变参数,意味着该函数可以同时添加多个中间件。
  • func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes:第二种方法,在定义处理器的同时,直接附加该处理器所需要的中间件。该函数也可以传入可变参数。一般情况下,会把真正处理业务逻辑的匿名函数放在最后一个参数位置上。
  • func (c *Context) Next()context.Next()只允许使用在中间件函数中。该函数可以把一个中间件函数分成三层。

有些中间件功能必须在第三层后续语句中运行,例如:

    fmt.Println("(middleware)wrong status code:", context.Writer.Status()) //这里获取到的状态码是错误的(是200)
    context.Next()      //开始执行具体业务,例如具体业务返回了404错误码。
    //执行完具体业务之后
    fmt.Println("(middleware)right status code:", context.Writer.Status())//获取到的状态码是404,正确

Gin使用路由组分类处理请求——Go Gin框架(五)

路由组功能

在实际项目开发中,均是模块化开发。同一模块的API接口一般会有相同的接口前缀。
例如,对于一个xx管理模块,会有“xx添加”、“xx删除”、“xx修改”、“xx查询”。
一个学生管理模块的API如下:

学生注册:/student/Add
学生删除:/student/Delete
学生修改:/student/Modify
学生查询:/student/Search

路由组

Gin框架为我们提供了路由组功能。

func group(engine *gin.Engine) {
    userGroup := engine.Group("/user")
    //visit /user/Add
    userGroup.POST("/Add", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //业务处理逻辑
    })

    //visit /user/Delete/:id
    userGroup.POST("/Delete/:id", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //业务处理逻辑
    })

    //visit /user/Modify/:id
    userGroup.POST("/Modify/:id", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //业务处理逻辑
    })

    //visit /user/Search?id=
    userGroup.GET("/Search", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //业务处理逻辑
    })
}
  • func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup:该函数用于定义一个group即一个公共的访问前缀。
    • relativePath string:相对路径,即公共前缀名。
    • handlers ...HandlerFunc:处理函数,由于是可变长度参数,故这里占时为空。

根据type RouterGroup struct的源码:

type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

var _ IRouter = &RouterGroup{}

可以看到其实现了一个IRouter接口,该接口包含了GETPOST等方法,通过这些方法可以实现该路由组中的具体实现方法。

附:IRouter接口的声明

type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

Gin多数据返回请求结果——Go Gin框架(四)

完整的请求

一个完整的请求应该包含请求处理请求返回结果三个步骤,在服务器端对请求处理完成之后,应该将结果返回给客户端。

Gin框架中提供了一下返回请求的数据格式

[]byte 和 string

之前的实例中,我们都用[]byte来返回数据。

  • func (ResponseWriter) Write([]byte) (int, error):用于向接收端输出[]byte切片。该方法被定义于官方的http包中
  • func (ResponseWriter) WriteString(string) (int, error):用于向接收端输出string该方法被定义在context包中

JSON

在项目开发中,JSON格式规范使用更为普遍。Gin框架直接支持将返回数据组装成JSON格式返回给客户端。

将map格式转换成JSON格式

func mapToJSON(engine *gin.Engine) {
    engine.GET("/mapToJSON", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //向前端传回JSON数据
        context.JSON(200, map[string]interface{}{
            "code":    0,                                      //在开发过程中,自己定义的一个状态码。这里用0代表成功
            "message": "OK",                                   //返回的一个状态码
            "data":    "request route: " + context.FullPath(), //返回的内容
        })
    })
}
  • func (c *Context) JSON(code int, obj interface{}):这个函数可以将map[string]interface{}类型的变量自动地转换成json数据,并返回给前端。其有两个参数,
    • 第一个code int表示返回的http状态码,可选200、300等状态码,或者直接使用golang提供的net/http包中的http.StatusOK(代表200状态码)等已经定义好的常量。
    • 第二个obj interface{}表示任意obj,这里可以是map[string]interface{}类型或者是struct类型(下一个示例)。

将struct格式转换为JSON格式

type Response struct {
    Code    int
    Message string
    Data    interface{}
}

func structToJSON(engine *gin.Engine) {
    engine.GET("/structToJSON", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        resp := Response{
            Code:    0,
            Message: "OK",
            Data:    "request route: " + context.FullPath(),
        }
        context.JSON(200, &resp)
    })
}

HTML

除了JSON格式以外,gin框架还支持返回HTML格式的数据。可以直接渲染HTML页面。

HTML模板传入

func returnHTML(engine *gin.Engine) {
    engine.GET("/returnHTML", func(context *gin.Context) {
        fmt.Println("request route: ", context.FullPath())
        //设置html文件目录的位置。设置过以后,这些html文件才能被engine访问
        engine.LoadHTMLGlob("./returnResultFormat/HTML/*")

        //设置静态文件目录(映射)
        engine.Static("/img","./returnResultFormat/image")

        //错误示范:context.HTML(http.StatusOK, "./returnResultFormat/HTML/index.html", nil) //直接这样加载会报错。
        context.HTML(http.StatusOK, "index.html", gin.H{
            "something":"这句话是由后端传入的",
        })
    })
}
  • func (c *Context) HTML(code int, name string, obj interface{}):这个函数用于向前端返回一个HTML文档。被返回的HTML文档必须先由func (engine *Engine) LoadHTMLGlob(pattern string)函数定义html文件目录的位置,只有被定义过的内容才能被gin引擎访问。
      1. code int:返回的状态码
      1. name string:要加载的HTML模板名
      1. obj interface{}:要传入的模板信息,一般使用gin提供的结构type H map[string]interface{}(gin.H)传入。在前端的默认定义为{{.something}}。如果不需要传入信息,可以传入nil

设置HTML文件

  • func (engine *Engine) LoadHTMLGlob(pattern string):这个函数用于指定HTML模板的位置。只有设置过以后,这些html文件才能被engine访问。
    • pattern string HTML模板文件位置,这个位置可以相对于文件根目录指定。若想添加一个文件夹下所有的HTML文档,可以使用./path/*来传入。

设置静态资源的目录

  • func (group *RouterGroup) Static(relativePath string, root string) IRoutes:这个函数用于指定静态文件(img等)的位置。只有设置过以后,才能被engine访问。通过这个函数指定的静态文件目录是映射关系
    • relativePath string 相对位置,指从前端页面访问该静态资源的位置。
    • root string 根目录位置,指engine引擎要从硬盘中的哪个位置找到该静态资源。注:这两个参数接受的都是文件夹路径,而非设置HTML文件处所代表的“文件”。所以无需在路径结尾添加*号。

附:前端HTML页面代码

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>Mona's Gin Test</title>
</head>
<body>
    <p>HTML页面已加载</p>
    <br>
    <p>{{.something}}</p>
    <p>上面的话是由模板系统传入</p>
    <p>下面这张图片是静态资源的用例</p>
    <img src="./img/74020281_p0.png">
</body>
</html>

附:项目路径树

$> tree /t ./
MONAGINWEB
    │
    └─returnResultFormat
        │  byteAndString.go
        │  cmd.go
        │  html.go
        │  json.go
        │
        ├─HTML
        │      index.html
        │
        └─image
                74020281_p0.png

Gin请求参数绑定与多数据格式处理——Go Gin框架(三)

请求参数绑定

在开发中,我们总会写username := context.PostForm("username")//解析POST类似的功能,每次只能实现对一个数据操作的方法。
Gin框架提供了实体绑定的功能,可以将表单数据与结构体绑定,从而达到简化上诉过程的方法。

用用户注册功能来实践表单实体绑定的操作:

type UserRegister struct{
    Username string `form:"username" binding:"required"`
    Password string `form:"password" binding:"required"`
    Phone    string `form:"phone" binding:"required"`
}

其中:
form表示要与哪一个字段进行绑定
binding表示该字段是否是一个必须字段(required代表必须)

GET、POST、POST JSON 数据绑定

将GET、POST、POST JSON数据绑定到服务器端的struct中。
以POST JSON为例:

//记住,struct里面必须是已导出的变量(其他位置可获取,否则err会提示成功,但无法成功绑定)
type jsonInfo struct {
    Name string `form:"Name"`
    Age  int    `form:"Age"`
    Sex  string `form:"Sex"`
}

func jsonBinder(engine *gin.Engine) {
    engine.POST("/JSONbinder", func(context *gin.Context) {
        fmt.Println("visit route: ", context.FullPath())
        var info jsonInfo//先创建一个变量以存储从前端获取到的数据
        //解析前端通过POST提交的JSON数据
        if err := context.BindJSON(&info); err != nil {//这里必须是引用上面创建的变量。
            log.Fatal(err.Error())
            return
        }
        fmt.Println("json bind successful, info: ", info.Name, info.Age, info.Sex)
        _, _ = context.Writer.Write([]byte("json bind successful, info:" + info.Name))
    })
}
  • func (c *Context) BindJSON(obj interface{}) error:用于将获取到的JSON数据绑定到一个struct中。其中,obj interface{}是我们定义的object,该函数返回一个error。
  • func (c *Context) ShouldBind(obj interface{}) error:用于绑定获取到的POST中的数据。
  • func (c *Context) ShouldBindQuery(obj interface{}) error:用于绑定获取到的GET中的数据
  • obj的定义参考上文定义的方法。

POST JSON使用的json用例:

{
"name":"The name",
"sex":"Girl",
"age":16
}

附:HTTP接口调试插件

平常开发者可能会使用一款叫做PostMan的软件,这个软件界面时全英文的,对国内用户可能不是很友好,而且还得下载软件。我接下来介绍的这款“插件”是一款Chrome插件,它可以实现API接口的调试。
CRAP-API 官网
Chrome 插件下载
以下是使用截图:

Gin网络请求与路由处理——Go Gin框架(二)

创建Engine(引擎)

Engine代表Gin框架的一个结构体定义。
其中包括了路由组、中间件、页面渲染接口、框架配置设置等相关内容

Engine 有以下两种创建方式:

engine1 = gin.Default() //通常使用,会默认使用Logger和Recovery中间件
engine2 = gin.New()

Recovery中间件的作用是:如果程序执行过程中遇到了panic中断了服务,Recovery会恢复程序的执行,并返回服务器500内部错误。
Logger负责打印并输出日志的中间件,方便我们开发调试。

实际上 gin.Default() 内部也是调用 gin.New() 来实现的,但是前者添加了上述两个中间件。

用Engine处理HTTP请求

在实例engine中,包含很多种方法可以直接处理不同类型的HTTP请求。

HTTP协议中义工定义了八种操作方式,分别是:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE、CONNECT。
实际上开发上常用的也只有GET、POST、DELETE等几种操作方式。

通用处理请求

engine中可以直接进行HTTP请求的处理,在engine中,可以使用Handle方法进行HTTP请求的处理。Handle方法包含三个参数,如下所示:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handles ...HandlerFunc) IRoutes
  • httpMethod string:第一个参数表示要处理的HTTP的请求类型,是"GET""POST""DELETE"等八种操作方式的其中一种
  • relativePath string:第二个参数表示要解析的接口,由开发者定义。
  • handlers Handlerfunc:第三个参数是处理对应的请求的代码的定义。

分类处理请求

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
//等几个常用的请求处理方法

每个参数的具体含义,请参考通用处理请求中的参数说明。

实例

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func main() {
    engine := gin.Default()
    //第一种处理http请求的方法,使用handle
    //访问 /hello?name=xxxxx
    engine.Handle("GET", "/hello", func(context *gin.Context) {
        //context里面封装了一个上下文环境变量,在这里面当中有我们经常操作的属性或者方法。
        path := context.FullPath() //获取我们本次请求的接口
        fmt.Println(path)
        //获取GET中的参数
        name := context.DefaultQuery("name", "null") //获取具体参数,1.哪个字段 2.若获取不到的默认值
        fmt.Println(name)
        //输出
        _, _ = context.Writer.Write([]byte("hello, " + name))
    })
    //第二种处理http请求的方法(分类处理)
    //访问 /login
    engine.POST("/login", func(context *gin.Context) {
        fmt.Println(context.FullPath())
        //解析POST
        username := context.PostForm("username")
        //这种方法会返回是否成功获取Form里的值,以进行判断
        password, 获取成功 := context.GetPostForm("password")
        if 获取成功 {
            fmt.Println(username, "n", password)
            _, _ = context.Writer.Write([]byte(username + "登录,密码为:" + password))
        }
    })

    //delete
    //在访问路径中,指定id的方法
    //即:/delete/user/123或/delete/user/456(最后一项是一个变量)
    engine.DELETE("/delete/user/:id", func(context *gin.Context) {
        //获取路径中的变量值
        userID := context.Param("id")
        fmt.Println("删除用户:" + userID)
        context.Writer.Write([]byte("删除用户:"+userID))
    })
    //运行这个引擎
    _ = engine.Run(":8088")
}

上述示例代码中,包含了几个新的函数:

  • func (c *Context) FullPath() string: 该函数返回被访问路由的全路径。对于没有找到的路径,返回空
  • func (c *Context) DefaultQuery(key string, defaultValue string) string: 该函数用于GET请求中,用于获取GET请求中的参数。
    • key string:要获取GET请求中的哪个字段
    • defaultValue string:若获取不到则返回的默认值
  • func (c *Context) PostForm(key string) string
  • func (c *Context) GetPostForm(key string) (string, bool): 这两个函数用于获取POST请求中的参数。
    • key string:要获取POST请求中的哪个字段
    • GETPostForm 的第二个bool返回值,用于返回是否成功获取到该字段
  • func (c *Context) Param(key string) string: 用于获取访问路径中的参数值。
    • key string:要获取访问路径请求中的哪个字段,该字段在路径中的表示方法为/:key
  • func (ResponseWriter) Write([]byte) (int, error):用于向浏览器端输出。(返回值占时不知道含义)