gin框架练习笔记

本文主要记录一下自己学习gin框架过程中亲自写的一些练习与笔记,温故而知新。

全部是参考这篇博客:https://www.liwenzhou.com/posts/Go/Gin_framework/#autoid-0-8-3

另外,所有的代码均使用testing实现,需要将go文件的名称命名为xxx_test.go。

使用gin.H发送嵌套字典的响应

有时候需要发送字典嵌套的响应,像这样:

实际上可以这样写返回的响应:

// home路由
func homeHandler(c *gin.Context){
    // 获取参数
    username := c.MustGet("username").(string)

    // 返回响应
    c.JSON(http.StatusOK, gin.H{
        "code": 2000,
        "msg": "success",
        "data": gin.H{"username":username},
    })
}

基本请求与响应的处理以及启动服务

package t_ginProjets

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/testdata/protoexample"
    "net/http"
    "testing"
)

// 视图函数 GET请求
func hello(c *gin.Context) {
    // c.JSON,返回json格式的数据
    // TODO 返回JSON的方式一:自己拼接JSON
    c.JSON(http.StatusOK, gin.H{
        "name":    "whw",
        "message": "hello world!",
    })
}

// POST请求
func postMethod(c *gin.Context) {
    // TODO 返回JSON的方式二:使用结构体
    var msg struct {
        Name    string `json:"post_name"` // 注意最终json的变量名是后面的!
        Age     int    `json:"post_age"`
        Message string `json:"post_message"`
    }
    // 创建一个结构体对象
    msg.Name = "whw"
    msg.Age = 18
    msg.Message = "666666"
    // TODO 直接将结构体对象传入参数中即可
    c.JSON(200, msg)
}

// PUT请求
func putMethod(c *gin.Context) {
    c.JSON(200, gin.H{
        "name": "putMethod",
        "msg":  "hello put",
    })
}

// DELETE请求
func deleteMethod(c *gin.Context) {
    c.JSON(200, gin.H{
        "name": "deleteMethod",
        "msg":  "hello delete",
    })
}

// TODO 简单的请求与响应处理 + json渲染 + protobuf渲染 + 启动服务
func TestGinReq(t *testing.T) {
    // 路由引擎
    r := gin.Default()
    // GET
    r.GET("/hello", hello)
    // POST
    r.POST("/post", postMethod)
    // PUT 传入匿名函数的方式
    r.PUT("/put", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "name": "myPutMethod",
            "msg":  "HELLO PUT",
        })
    })
    // DELETE
    r.DELETE("/delete", deleteMethod)

    // protobuf渲染
    r.GET("/someProtoBuf", func(c *gin.Context) {
        reps := []int64{int64(1), int64(2)}
        label := "test"
        // protobuf 的具体定义写在 testdata/protoexample 文件中。
        data := &protoexample.Test{
            Label: &label,
            Reps:  reps,
        }
        // 请注意,数据在响应中变为二进制数据
        // 将输出被 protoexample.Test protobuf 序列化了的数据
        c.ProtoBuf(http.StatusOK, data)
    })

    // 启动HTTP服务  什么都不写默认是 0.0.0.0:8080
    r.Run("127.0.0.1:9000")

}
View Code

获取参数

获取querystring、form、path中的参数

package t_ginProjets

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "testing"
)

// TODO 1、获取querystring参数
// /user/search?username=whw&address=China
func queryString(c *gin.Context){
    username := c.DefaultQuery("username","")
    address := c.DefaultQuery("address","")
    // 将结果返回给调用方
    c.JSON(http.StatusOK,gin.H{
        "message": "OK",
        "username": username,
        "address": address,
    })
}
func TestQueryString(t *testing.T){
    // Default返回一个默认的路由引擎
    r := gin.Default()

    r.GET("/user/search",queryString)

    r.Run("127.0.0.1:9000")

}

// TODO 2、获取form参数
// 请求的数据通过form表单来提交,例如向/user/search发送一个POST请求,获取请求数据的方式如下
func getFormData(c *gin.Context){
    // DefaultPostForm取不到值时会返回指定的默认值
    username := c.DefaultPostForm("username","")
    password := c.DefaultPostForm("password","")
    // 返回结果给调用方
    c.JSON(http.StatusOK,gin.H{
        "message": "OK",
        "username": username,
        "password": password,
    })

}
func TestFormData(t *testing.T){
    // 默认引擎
    r := gin.Default()

    r.POST("/user/search",getFormData)

    r.Run("127.0.0.1:9000")

}

// TODO 3、获取path参数
// 请求的参数通过URL路径传递,例如:/user/search/whw/China: 获取请求URL路径中的参数的方式如下。
func GetPathParams(c *gin.Context){
    username := c.Param("username")
    address := c.Param("address")
    // 输出json
    c.JSON(http.StatusOK,gin.H{
        "msg":"OK",
        "username":username,
        "address":address,
    })
}
func TestGetParams(xxx *testing.T){
    // 引擎
    r := gin.Default()

    r.GET("/user/search/:username/:address",GetPathParams)

    r.Run("127.0.0.1:9000")

}
ginParams_test.go

参数绑定 

为了能够更方便的获取请求相关参数,提高开发效率。
我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryString、form表单、JSON、XML等参数到结构体中。
下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,
并把值绑定到指定的结构体对象。
shouldBind会按照下面的顺序解析请求中的数据完成绑定:
1、如果是 GET 请求,只使用 Form 绑定引擎(query)。
2、如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。
package t_ginProjets

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

// TODO 参数绑定: ShouldBind
type Login struct {
    User string `form:"user" json:"user" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

// 1 绑定JSON的示例:{"user": "whw", "password": "123456"}
func bindJsonTest(c *gin.Context){
    var login Login

    if err := c.ShouldBind(&login); err == nil{
        // 打印
        fmt.Printf("login info:%#v \n", login)
        // 返回给调用者
        c.JSON(http.StatusOK,gin.H{
            "user": login.User,
            "password": login.Password,
        })
    }else{
        c.JSON(http.StatusBadRequest,gin.H{
            "error": err.Error(),
        })
    }
}

// 2 绑定form表单的示例:user=whw&password=123456
func bindFormTest(c *gin.Context){
    var login Login

    // ShouldBind()会根据请求的Content-Type自行选择绑定器
    if err := c.ShouldBind(&login); err == nil{
        c.JSON(http.StatusOK,gin.H{
            "user": login.User,
            "password": login.Password,
        })
    }else{
        c.JSON(http.StatusBadRequest,gin.H{
            "error": err.Error(),
        })
    }
}

// 3、绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
func bindQueryStringTest(c *gin.Context){
    var login Login

    // ShouldBind()会根据请求的Content-Type自行选择绑定器
    if err := c.ShouldBind(&login); err == nil{
        c.JSON(http.StatusOK, gin.H{
            "user": login.User,
            "password": login.Password,
        })
    }else{
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
    }
}

func TestParams(t *testing.T){
    // 默认引擎
    r := gin.Default()

    // JSON的示例
    r.POST("/loginJson", bindJsonTest)

    // form表单示例
    r.POST("/loginForm", bindFormTest)

    // QueryString示例
    r.GET("/loginQueryString",bindQueryStringTest)

    r.Run("127.0.0.1:9090")
}
xxxx_test.go

文件上传

package t_ginProjets

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "testing"
)

// TODO 文件上传

// TODO 单个文件上传

// 前端代码
/*
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="f1">
    <input type="submit" value="上传">
</form>
</body>
</html>
*/

func TestUploadSingleFile(t *testing.T) {
    router := gin.Default()
    // 处理multipart forms提交文件时默认的内存限制是32 MiB
    // 可以通过下面的方式修改
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // 单个文件
        file, err := c.FormFile("f1")
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "message": err.Error(),
            })
            return
        }

        log.Println(file.Filename)
        dst := fmt.Sprintf("~/Downloads/%s", file.Filename)
        // 上传文件到指定的目录
        c.SaveUploadedFile(file, dst)
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("'%s' uploaded!", file.Filename),
        })
    })
    router.Run("127.0.0.1:9800")
}


// TODO 多个文件上传
func TestUploadmutiFiles(t *testing.T) {
    router := gin.Default()
    // 处理multipart forms提交文件时默认的内存限制是32 MiB
    // 可以通过下面的方式修改
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // Multipart form
        form, _ := c.MultipartForm()
        files := form.File["file"]

        for index, file := range files {
            log.Println(file.Filename)
            dst := fmt.Sprintf("~/Downloads/%s_%d", file.Filename, index)
            // 上传文件到指定的目录
            c.SaveUploadedFile(file, dst)
        }
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("%d files uploaded!", len(files)),
        })
    })
    router.Run()
}
View Code

重定向

package t_ginProjets

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "testing"
)

// TODO 1、HTTP重定向
// HTTP 重定向很容易。 内部、外部重定向均支持。

func TestHttpRedirect(t *testing.T){
    r := gin.Default()

    r.GET("/baidu",func(c *gin.Context){
        c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com")
    })

    r.Run("127.0.0.1:9000")
}

// TODO 2、路由重定向
// 路由重定向,使用HandleContext
func TestRouterRedirect(t *testing.T){
    // 默认引擎
    r := gin.Default()
    // test1的路由
    r.GET("/test1",func(c *gin.Context){
        c.JSON(http.StatusOK,gin.H{
            "msg":"i am test1!",
        })
    })
    // test2的路由 重定向到test1
    r.GET("/test2",func(c *gin.Context){
        // 指定重定向的URL
        c.Request.URL.Path = "/test2"
        r.HandleContext(c)
    })
    // 启动服务
    r.Run("127.0.0.1:9001")
}
View Code

gin路由相关的方法

package t_ginProjets

import (
    "github.com/gin-gonic/gin"
    "net/http"
    "testing"
)

// Gin的路由
/*
Gin框架中的路由使用的是httprouter(https://github.com/julienschmidt/httprouter)这个库。
其基本原理就是构造一个路由地址的前缀树。
*/

// TODO 1、普通路由
/*
    r.GET("/index", func(c *gin.Context) {...})
    r.GET("/login", func(c *gin.Context) {...})
    r.POST("/login", func(c *gin.Context) {...})
*/

// TODO 2、匹配所有请求的方法
/*
    r.Any("/test", func(c *gin.Context) {...})
*/

// TODO 3、为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。
/*
    r.NoRoute(func(c *gin.Context) {
        c.HTML(http.StatusNotFound, "views/404.html", nil)
    })
*/

// TODO *** 4、路由组: 通常我们将路由分组用在 "划分业务逻辑" 或 "划分API版本" 时。
/*
我们可以将拥有共同URL前缀的路由划分为一个路由组。
习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。
*/
func TestRouterGroup(t *testing.T){
    // 定义默认引擎
    r := gin.Default()

    // 路由组 userGroup
    userGroup := r.Group("/user")
    {
        // /user/index
        userGroup.GET("/index",func(c *gin.Context){
            c.JSON(http.StatusOK,gin.H{
                "msg":"user.index",
            })
        })
        // /user/login
        userGroup.GET("/login",func(c *gin.Context){
            c.JSON(http.StatusOK,gin.H{
                "msg":"user.login",
            })
        })
    }

    // 路由组 shopGroup
    shopGroup := r.Group("/shop")
    {
        // shop/index
        shopGroup.GET("/index",func(c *gin.Context){
            c.JSON(http.StatusOK,gin.H{
                "msg":"shop.index",
            })
        })
        // shop/login
        shopGroup.GET("/login",func(c *gin.Context){
            c.JSON(http.StatusOK,gin.H{
                "msg":"shop.login",
            })
        })
    }

    // 启动服务
    r.Run("127.0.0.1:9900")
}

// TODO 5、嵌套路由组
/*
shopGroup := r.Group("/shop")
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        shopGroup.GET("/cart", func(c *gin.Context) {...})
        shopGroup.POST("/checkout", func(c *gin.Context) {...})
        // TODO 嵌套路由组
        xx := shopGroup.Group("xx")
        xx.GET("/oo", func(c *gin.Context) {...})
    }
*/
router_test.go

gin的中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。
这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

中间件注意事项

1、默认中间件

gin.Default()默认使用了Logger和Recovery中间件,其中:
- Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
- Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

2、gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,
不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

代码

package t_ginProjets

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "testing"
    "time"
)

// Gin 中间件


// TODO 1、定义中间件
// Gin中的中间件必须是一个gin.HandlerFunc类型。
// 例如我们像下面的代码一样定义一个统计请求耗时的中间件。
func StatCost() gin.HandlerFunc{
    return func(c *gin.Context){
        start := time.Now()
        // // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
        c.Set("name","naruto")
        // 调用该请求的剩余处理程序
        c.Next()

        // 不调用该请求的剩余处理程序!
        // 注意:加上的话,该中间件后的请求逻辑不会处理!
        // c.Abort()

        // 计算耗时
        cost := time.Since(start)
        log.Println("耗时: ",cost)
    }
}

// TODO 2、注册中间件
// 在gin框架中,我们可以为每个路由添加任意数量的中间件。
// TODO 2.1、为全局路由注册
func TestRegisterGlobalMiddleWare(t *testing.T){
    // 新建一个没有任何中间件的路由
    r := gin.New()
    // 注册一个全局中间件
    r.Use(StatCost())

    r.GET("/middleware_test1", func(c *gin.Context){
        // 从上下文取值
        name := c.MustGet("name").(string)
        log.Println("上下文的name: ",name)

        // sleep 1秒
        time.Sleep(time.Second)

        c.JSON(http.StatusOK, gin.H{
            "message": "Hello World!",
        })
    })

    r.Run("127.0.0.1:9002")
}

// TODO 2.2、为某个路由单独注册中间件(当然可以注册多个)
// 再定义一个只打印的中间件
func JustPrint() gin.HandlerFunc {
    return func(c *gin.Context){
        // 设置另外一个值
        c.Set("middleware2",666)
        log.Println("自定义中间件 justPrint...")
    }
}
func TestRegisterRouterMiddleWare(t *testing.T){
    r := gin.New()

    // 给单独的路由注册中间件
    r.GET("/middleware_test2",StatCost(), JustPrint(),func(c *gin.Context){
        // 从上下文取值
        midd2 := c.MustGet("middleware2").(int)
        name := c.MustGet("name").(string)
        log.Println("midd2>>> ",midd2)

        // 返回响应
        c.JSON(http.StatusOK,gin.H{
            "middle2": midd2,
            "name": name,
        })
    })

    r.Run("127.0.0.1:9003")
}

// TODO 3、为路由组注册中间件
// TODO 3.1、写法1:
/*
    shopGroup := r.Group("/shop", StatCost())
    {
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
    }
 */
// TODO 3.2、写法2(在路由组中注册中间件):
/*
    shopGroup := r.Group("/shop")
    shopGroup.Use(StatCost())
    {
        shopGroup.GET("/index", func(c *gin.Context) {...})
        ...
    }
*/
middleware_test.go

运行多个服务器

package t_ginProjets

import (
    "log"
    "net/http"
    "testing"
    "time"
    "github.com/gin-gonic/gin"
    "golang.org/x/sync/errgroup"
)

// TODO 运行多个服务
//我们可以在多个端口启动服务


var (
    g errgroup.Group
)

func router01() http.Handler {
    e := gin.New()
    e.Use(gin.Recovery())
    e.GET("/", func(c *gin.Context) {
        c.JSON(
            http.StatusOK,
            gin.H{
                "code":  http.StatusOK,
                "error": "Welcome server 01",
            },
        )
    })

    return e
}

func router02() http.Handler {
    e := gin.New()
    e.Use(gin.Recovery())
    e.GET("/", func(c *gin.Context) {
        c.JSON(
            http.StatusOK,
            gin.H{
                "code":  http.StatusOK,
                "error": "Welcome server 02",
            },
        )
    })

    return e
}

func TestRunServers(t *testing.T) {
    server01 := &http.Server{
        Addr:         "127.0.0.1:8080",
        Handler:      router01(),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    server02 := &http.Server{
        Addr:         "127.0.0.1:8081",
        Handler:      router02(),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }
    // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
    g.Go(func() error {
        return server01.ListenAndServe()
    })

    g.Go(func() error {
        return server02.ListenAndServe()
    })

    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}
runServers_test.go

~~~

posted on 2020-12-08 15:18  江湖乄夜雨  阅读(372)  评论(0编辑  收藏  举报