Go:gin

时间:March 8, 2020 分类:

目录:

gin简介

gin是一个go语言编写的web框架

安装方式

go get github.com/gin-gonic/gin

使用

gin示例

package main

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

func main() {
    // 创建一个默认的路由引擎
    r := gin.Default()
    // GET:请求方式;/hello:请求的路径
    // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
    r.GET("/hello", func(c *gin.Context) {
        // c.JSON:返回JSON格式的数据
        c.JSON(200, gin.H{
            "message": "Hello world!",
        })
    })
    // 启动HTTP服务,默认在0.0.0.0:8080启动服务
    r.Run(":22222")
}

请求对应接口

$ curl 127.0.0.1:22222/hello
{"message":"Hello world!"}

获取参数

获取querystring参数

package main

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

func main() {
    r := gin.Default()
    r.GET("/user/search", func(c *gin.Context) {
        username := c.DefaultQuery("username", "why")
        address := c.Query("address")
        c.JSON(200, gin.H{
            "message": "ok",
            "username": username,
            "address": address,
        })
    })
    // 启动HTTP服务,默认在0.0.0.0:8080启动服务
    r.Run(":22222")
}

请求对应接口

$ curl '127.0.0.1:22222/user/search?username=wanghongyu&address=chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}

获取form参数

package main

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

func main() {
    r := gin.Default()
    r.POST("/user/search", func(c *gin.Context) {
        username := c.PostForm("username")
        address := c.PostForm("address")
        c.JSON(http.StatusOK, gin.H{
            "message": "ok",
            "username": username,
            "address": address,
        })
    })
    // 启动HTTP服务,默认在0.0.0.0:8080启动服务
    r.Run(":22222")
}

请求对应接口

$ curl '127.0.0.1:22222/user/search' -d 'username=wanghongyu&address=chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}

获取path参数

package main

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

func main() {
    r := gin.Default()
    r.GET("/user/search/:username/:address", func(c *gin.Context) {
        username := c.Param("username")
        address := c.Param("address")
        c.JSON(http.StatusOK, gin.H{
            "message": "ok",
            "username": username,
            "address": address,
        })
    })
    // 启动HTTP服务,默认在0.0.0.0:8080启动服务
    r.Run(":22222")
}

请求对应接口

$ curl '127.0.0.1:22222/user/search/wanghongyu/chengde'
{"address":"chengde","message":"ok","username":"wanghongyu"}

参数绑定

请求通过基于Content-Type识别数据类型,利用反射机制提取请求中的QueryStringform表单、json数据等

package main

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

type Login struct {
    User     string `form:"user" json:"user" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}


func main() {
    r := gin.Default()

        // 绑定JSON的示例 ({"user": "why", "password": "123456"})
    r.POST("/loginJSON", func(c *gin.Context) {
        var login Login

        if err := c.ShouldBindJSON(&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()})
        }
    })


        // 绑定form表单示例 (user=why&password=123456)
    r.POST("/loginForm", func(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()})
        }
    })

    // 绑定QueryString示例 (/loginQuery?user=why&password=123456)
    r.GET("/loginForm", func(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()})
        }
    })

    // 启动HTTP服务,默认在0.0.0.0:8080启动服务
    r.Run(":22222")
}

curl -X POST -H "content-type/application/json" 127.0.0.1:22222/loginJSON -d '{"user": "why", "password": "123456"}'
{"password":"123456","user":"why"}
curl -X POST -H "content-type: application/x-www-form-urlencoded" 127.0.0.1:22222/loginForm -d 'user=why&password=123456'
{"password":"123456","user":"why"}
curl -X GET '127.0.0.1:22222/loginForm?user=why&password=123456'
{"password":"123456","user":"why"}

ShouldBind会按照下面的顺序解析请求中的数据完成绑定

  • 如果是GET请求,只使用Form绑定引擎query。
  • 如果是POST请求,首先检查content-type是否为JSON或XML,然后再使用Form(form-data)。

文件上传

单个文件上传

前端代码

<!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>

后端代码

package main

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

func main() {
    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("/tmp/%s", file.Filename)
        // 上传文件到指定的目录
        c.SaveUploadedFile(file, dst)
        c.JSON(http.StatusOK, gin.H{
            "message": fmt.Sprintf("'%s' uploaded!", file.Filename),
        })
    })
    router.Run()
}

多个文件上传

package main

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

func main() {
    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("C:/tmp/%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()
}

重定向

HTTP重定向

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

路由重定向

r.GET("/test", func(c *gin.Context) {
    // 指定重定向的URL
    c.Request.URL.Path = "/test2"
    r.HandleContext(c)
})

r.GET("/test2", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"hello": "world"})
})

路由

普通路由

r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})

方法不同可以进行不同的路由,还有一个匹配所有请求方法的Any方法

r.Any("/test", func(c *gin.Context) {...})

对于没有路由到的也可以添加处理程序

r.NoRoute(func(c *gin.Context) {
        c.HTML(http.StatusNotFound, "views/404.html", nil)
    })

路由组

将相同的URL前缀划分为一个路由组

func main() {
    r := gin.Default()
    userGroup := r.Group("/user")
    {
        userGroup.GET("/index", func(c *gin.Context) {...})
        userGroup.GET("/login", func(c *gin.Context) {...})
        userGroup.POST("/login", func(c *gin.Context) {...})

    }
    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) {...})
    }
    r.Run()
}

路由组支持嵌套

路由原理

使用了httprouter库

Gin中间件

在请求中添加钩子,可以做一些公共的逻辑,例如登录认证,权限校验,数据分页,记录耗时等等

定义中间件

// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Set("name", "why") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
        // 调用该请求的剩余处理程序
        c.Next()
        // 不调用该请求的剩余处理程序
        // c.Abort()
        // 计算耗时
        cost := time.Since(start)
        log.Println(cost)
    }
}

注册中间件

全局注册

func main() {
    // 新建一个没有任何默认中间件的路由
    r := gin.New()
    // 注册一个全局中间件
    r.Use(StatCost())

    r.GET("/test", func(c *gin.Context) {
        name := c.MustGet("name").(string) // 从上下文取值
        log.Println(name)
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello world!",
        })
    })
    r.Run()
}

单个路由注册

// 给/test2路由单独注册中间件(可注册多个)
    r.GET("/test2", StatCost(), func(c *gin.Context) {
        name := c.MustGet("name").(string) // 从上下文取值
        log.Println(name)
        c.JSON(http.StatusOK, gin.H{
            "message": "Hello world!",
        })
    })

路由组注册

有两种写法

第一种:

shopGroup := r.Group("/shop", StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

第二种

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}

注意事项

默认中间件

gin.Default()使用了Logger和Recovery中间件

  • Logger中间件会将日志写入gin.DefaultWriter
  • Recover中间件会recover任何panic,并将panic写入500的响应码

如果不想使用可以用gin.New()新建一个没有任何中间件的路由

中间件中使用goroutine

在handler或者中间件启动新的goroutine的时候,不能使用原始的上下文(c *gin.Context),需要使用其只读副本c.Copy()

启动多个服务

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "golang.org/x/sync/errgroup"
)

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 main() {
    server01 := &http.Server{
        Addr:         ":8080",
        Handler:      router01(),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    server02 := &http.Server{
        Addr:         ":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)
    }
}

HTML渲染

对于HTML渲染等不实用的不做介绍

使用静态文件

r.Static("/static", "./static")

Cookies

import (
    "fmt"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/cookie", func(c *gin.Context) {
        cookie, err := c.Cookie("gin_cookie") // 获取Cookie
        if err != nil {
            cookie = "NotSet"
            // 设置Cookie
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }
        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

websocket

原生http+ws

package main

import (
    "flag"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

var upgrader = websocket.Upgrader{
    // 解决跨域问题
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
} // use default options

func ws(w http.ResponseWriter, r *http.Request) {
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

func main() {
    flag.Parse()
    log.SetFlags(0)
    http.HandleFunc("/ws", ws)
    fmt.Println(*addr)
    log.Fatal(http.ListenAndServe(*addr, nil))
}

使用gin

package control

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    // 解决跨域问题
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
} // use default options

func cmdWebSocket(c *gin.Context) {
    ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer ws.Close()
    for {
        mt, message, err := ws.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        err = ws.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}