Gin
Gin 是一个 Go 语言写的 Web 框架。
1 安装
go get -u github.com/gin-gonic/gin
2 HelloWorld
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func helloWorld(context *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "world",
})
}
func main() {
router := gin.Default()
router.GET("/hello", helloWorld)
router.Run("localhost:8000")
}
3 路由分组
假设现在有这些请求路径:/goods/list
、goods/add
、goods/del
,在不使用路由分组的情况下,通常会这样写:
router := gin.Default()
router.GET("/goods/list", goodsList)
router.POST("/goods/add", addGoods)
router.POST("/goods/del", delGoods)
这些路径都包含 /goods
,可以通过路由分组进行管理:
router := gin.Default()
goodsGroup := router.Group("/goods")
goodsGroup.GET("/list", goodsList)
goodsGroup.POST("/add", addGoods)
goodsGroup.POST("/del", delGoods)
4 获取 URL 上的变量
假设现在需要通过 /goods/[商品ID]
访问对应商品的详情,通过如下方法可以识别 URL 中的 ID 变量:
router := gin.Default()
router.GET("/goods/:id", func(context *gin.Context) {
id := context.Param("id")
context.JSON(http.StatusOK, gin.H{
"id": id,
})
})
假设现在想通过 /goods/[商品ID]/[动作]
来对商品进行操作,可以这样做:
router.GET("/goods/:id/:action", func(context *gin.Context) {
id := context.Param("id")
action := context.Param("action")
context.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
})
### 请求
GET http://localhost:8000/goods/123/delete
### 响应
{
"action": "delete",
"id": "123"
}
通过 /goods/:id/*action
也能实现,但是和 /:action
有些区别:
router.GET("/goods/:id/*action", func(context *gin.Context) {
id := context.Param("id")
action := context.Param("action")
context.JSON(http.StatusOK, gin.H{
"id": id,
"action": action,
})
})
### 请求
GET http://localhost:8000/goods/123/delete
### 响应
{
"action": "/delete",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123/delete/test
### 响应
{
"action": "/delete/test",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123
### 响应
{
"action": "/",
"id": "123"
}
### 请求
GET http://localhost:8000/goods/123/
### 响应
{
"action": "/",
"id": "123"
}
还可以直接绑定 Uri 到一个结构体:
type Goods struct {
Id int `uri:"id" binding:"required"`
Name string `uri:"name" binding:"required"`
}
func main() {
router := gin.Default()
router.GET("/goods/:id/:name", func(context *gin.Context) {
var goods Goods
if err := context.ShouldBindUri(&goods); err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{
"id": goods.Id,
"name": goods.Name,
})
})
router.Run("localhost:8000")
}
GET http://localhost:8000/goods/123/abc
# 响应
{
"id": 123,
"name": "abc"
}
5 获取表单信息
5.1 Get 请求
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
// 如果参数没取到,则使用空字符串""
lang := context.Query("lang")
// 如果参数没取到,则使用默认值"Gin"
framework := context.DefaultQuery("framework", "Gin")
context.JSON(http.StatusOK, gin.H{
"lang": lang,
"framework": framework,
})
}
测试:
GET http://localhost:8000/hello
# 响应
{
"framework": "Gin",
"lang": ""
}
GET http://localhost:8000/hello?lang=Java&framework=Spring
# 响应
{
"framework": "Spring",
"lang": "Java"
}
5.2 Post 请求
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.POST("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
lang := context.Query("lang")
lang := context.PostForm("lang")
framework := context.DefaultQuery("framework", "Gin")
framework := context.DefaultPostForm("framework", "Gin")
context.JSON(http.StatusOK, gin.H{
"lang": lang,
"framework": framework,
})
}
测试:
### 请求
POST http://localhost:8000/hello
Content-Type: application/x-www-form-urlencoded
lang = Go &
framework = Gin
# 响应
{
"framework": "Gin",
"lang": "Go"
}
6 Protobuf 渲染
定义 Protobuf 消息:
syntax = "proto3";
option go_package = '.;proto';
message Teacher {
string name = 1;
repeated string courses = 2;
}
生成 Go 包,在程序中导入使用:
func main() {
router := gin.Default()
router.GET("/hello", hello)
router.Run("localhost:8000")
}
func hello(context *gin.Context) {
user := &proto.Teacher{
Name: "zhang",
Courses: []string{"Gin", "GoLang"},
}
context.ProtoBuf(http.StatusOK, user)
}
7 表单验证
type SignUpInfo struct {
Username string `json:"username" binding:"required,min=3,max=20"` // 必传字段,要求 3 <= length <= 20
Password string `json:"password" binding:"required,min=8,max=20"` // 必传字段,要求 3 <= length <= 20
RePassword string `json:"rePassword" binding:"required,eqfield=Password"` // 必传字段,要求和 password 字段一致
Email string `json:"email" binding:"required,email"` // 必传字段,要求符合邮箱格式
Age uint8 `json:"age" binding:"gte=0,lte=120"` // 要求 0 <= age <= 120
}
func main() {
router := gin.Default()
router.POST("/signUp", signUp)
router.Run("localhost:8000")
}
func signUp(context *gin.Context) {
var info SignUpInfo
if err := context.ShouldBindJSON(&info); err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusOK, gin.H{"msg": "注册成功"})
}
验证不通过时,返回的信息是英文的,如下所示:
{
"error": "
Key: 'SignUpInfo.Username' Error:Field validation for 'Username' failed on the 'min' tag\n
Key: 'SignUpInfo.RePassword' Error:Field validation for 'RePassword' failed on the 'eqfield' tag\n
Key: 'SignUpInfo.Email' Error:Field validation for 'Email' failed on the 'email' tag\n
Key: 'SignUpInfo.Age' Error:Field validation for 'Age' failed on the 'lte' tag
"
}
可将错误信息转成中文:
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"net/http"
)
func InitTrans(locale string) (err error) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New()
enT := en.New()
uni := ut.New(zhT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return errors.New("translator not found")
}
switch locale {
case "zh":
if err := zh_translations.RegisterDefaultTranslations(v, trans); err != nil {
return err
}
case "en":
if err := en_translations.RegisterDefaultTranslations(v, trans); err != nil {
return err
}
}
}
return nil
}
var trans ut.Translator
func main() {
if err := InitTrans("zh"); err != nil {
fmt.Println(err)
}
router := gin.Default()
router.POST("/signUp", signUp)
router.Run("localhost:8000")
}
func signUp(context *gin.Context) {
var info SignUpInfo
if err := context.ShouldBindJSON(&info); err != nil {
errs, _ := err.(validator.ValidationErrors)
context.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
return
}
context.JSON(http.StatusOK, gin.H{"msg": "注册成功"})
}
此时,错误信息将转为中文:
{
"error": {
"SignUpInfo.Age": "Age必须小于或等于120",
"SignUpInfo.Email": "Email必须是一个有效的邮箱",
"SignUpInfo.RePassword": "RePassword必须等于Password",
"SignUpInfo.Username": "Username长度必须至少为3个字符"
}
}
8 中间件
func MyLogger() gin.HandlerFunc {
return func(context *gin.Context) {
t := time.Now()
// context.Abort() 终止请求
context.Next() // 处理请求
latency := time.Since(t)
log.Print(latency)
}
}
func main() {
router := gin.New()
router.Use(gin.Logger()) // 全局中间件
router.GET("/signUp", MyLogger(), signUp) // 路由中间件
router.Run("localhost:8000")
}
中间件后续逻辑的执行终止,必须使用 context.Abort()
,直接 return
无法阻止执行:
func MyLogger() gin.HandlerFunc {
return func(context *gin.Context) {
return // 后续逻辑依旧会被执行
context.Next()
}
}
从添加中间件的 Use
函数源码上看:
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
本质上是把中间件追加到了 group.Handlers
切片的后面。
而在 GET/POST
函数内部执行了一个 combineHandlers
函数:
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
这里将我们处理请求的函数和之前的 Handlers
拼在了一起。
而 context.Next()
的调用过程如下:
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
如果在自定义中间件里直接 return
了,只代表当前中间件的逻辑结束了,Handlers
中后续的函数仍然会依次执行。
从 context.Next()
的源码逻辑可以看出,真正决定 Handlers
中的函数调用的是 c.index
。而 context.Abort()
的作用正是修改这个变量:
const abortIndex int8 = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = abortIndex
}
9 优雅退出
关闭程序的时候可能有请求还没有处理完,此时处理过程就会被迫中断。优雅退出其实就是在程序关闭时,不暴力关闭,而是要等待进程中的逻辑处理完成后,才关闭。
import (
"fmt"
"net/http"
"os"
"os/signal"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
go func() {
_ = router.Run(":8000")
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
fmt.Println("关闭服务")
}