go的web框架gin学习笔记 | 青训营笔记

599 阅读9分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第10篇笔记。

gin资料

Github地址:gin

官方文档:gin官网

gin中文文档

一、常见请求案例

Gin 是一个 go 写的 web 框架,具有高性能的优点。

1.1、初始案例

package main
​
import (
    "github.com/gin-gonic/gin"
)
​
func main()  {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run()  //启动服务以及监听:127.0.0.1::8080
}

image-20220528093708388

1.2、get、post的常见请求处理方式

配套代码:go-ginLearn/demo1

简介

get:①获取路径参数,例如:/user/:name。②获取get参数,例如:/welcome?firstname=Jane&lastname=Doe。

post:①获取表单参数,form-data。

示例

package main
​
import (
    "github.com/gin-gonic/gin"
    "net/http"
)
​
func main()  {
    //使用默认中间件创建一个gin路由器
    r := gin.Default()
    //1、常见的方法
    //get
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    //get-1:获取路径中的参数【注意:无法匹配/user/,/user这类路径】 //示例:/hello/changlu
    r.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")  //获取url中的相应参数
        c.String(http.StatusOK, "hello %s", name)
    })
    //get-2:获取get参数  //示例:/welcome?firstname=Jane&lastname=Doe
    r.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest") //若是没有获取到,可获取到默认值。【底层还是走query方法】
        lastname := c.Query("lastname")
        c.String(http.StatusOK, "hello %s %s", firstname, lastname)
    })
    //post
    r.POST("/ping", commonResponse)
    //post-1:获取form表单
    r.POST("/form_post", func(c *gin.Context) {
        //获取表单,和get的query类似
        message := c.PostForm("message")
        nick := c.DefaultPostForm("nick", "anonymous")
        c.JSON(200, gin.H{
            "status": "posted",
            "message": message,
            "nick": nick,
        })
    })
    //put
    r.PUT("/ping", commonResponse)
    //其他方法:delete、patch、head、options
    //2、启动服务
    //r.Run()  //默认启动服务以及监听:127.0.0.1::8080
    r.Run(":3000")  //指定端口
}
​
func commonResponse(c *gin.Context)  {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

中间PostMan测试展示

post-1:建立表单

image-20220528095335716


1.3、POST处理JSON操作

配套代码:go-ginLearn/demo2

模拟请求示例

Postman请求:

image-20220528101046188

代码实操

两种方式来进行序列化:①使用JSON的工具类进行。②使用gin框架给我们自带的bind方法来进行解析。

package demo2
​
import (
    "encoding/json"
    "github.com/gin-gonic/gin"
)
​
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
​
func Main()  {
    r := gin.Default()
    //方式一:使用JSON工具来进行序列化
    r.POST("/user", func(c *gin.Context) {
        //1、定义一个Map来进行接收
        requestMap := make(map[string]string)
        //2、进行JSON序列化(使用JSON工具类)
        if err := json.NewDecoder(c.Request.Body).Decode(&requestMap); err != nil{
            panic(err)
        }
        c.JSON(200, gin.H{
            "code": 200,
            "result": requestMap,
        })
    })
    //方式二:使用gin自带的bind方法来进行(底层会根据对应的类型来判断进行序列化,建议用框架带的更方便)
    r.POST("/user2", func(c *gin.Context) {
        //1、定义一个user对象
        user := User{}
        //2、使用gin的bind来进行序列化
        if err := c.BindJSON(&user); err != nil {
            panic(err)
        }
        c.JSON(200, gin.H{
            "code": 200,
            "result": user,
        })
    })
    r.Run()
}

使用gin的bind处理的坑点gin 结构体json解析的坑

1、对应结构体的字段名一定要大写,否则解析不到,如下是可以的。【虽然这里字段开头是大写的不过不影响解析】

type User struct {
    Username string
    Password string
}
​
//若是你想要更加清晰明了的话可以如下使用`json`描述
type User struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

1.4、上传文件

配套代码:go-ginLearn/demo3

参考文章:UUID生成go获取文件名和后缀

Postman模拟请求

单个文件:/upload POST

image-20220528105504862

多个文件:/uploads POST

image-20220528105936477

运行效果:所有上传的图片名称都进行UUID处理

image-20220528110524030

代码

image-20220528105956006

common.go:封装的工具方法

package common
​
import (
    uuid "github.com/satori/go.uuid"
    "path"
)
​
func GenerateFileName(fileName string)string  {
    return uuid.NewV4().String() + path.Ext(fileName)
}

demo3.go:相应的文件上传demo

package demo3
​
import (
    "github.com/gin-gonic/gin"
    "go-ginLearn/common"
    "log"
    "mime/multipart"
)
​
var (
    dst = "C:\Users\93997\Desktop\upload\755b7cdd-ed6c-43c3-b62f-14a5c9e167a1.png"
)
​
func Main()  {
    r := gin.Default()
    //1、上传单个文件
    //表单限制上传大小(默认32MiB)
    //r.MaxMultipartMemory = 8 << 20  // 8Mib
    r.POST("/upload", func(c *gin.Context) {
        //单文件
        file, _ := c.FormFile("file") //直接取出key为file的文件
        log.Println(file.Filename)
        //保存文件
        saveFile(file, c)
        c.JSON(200, gin.H{
            "code": 200,
            "message": "上传成功",
        })
    })
    //2、上传多个文件
    r.POST("/uploads", func(c *gin.Context) {
        //1、取出文件数组
        form, _ := c.MultipartForm()
        files := form.File["file"]  //根据上传的key来取出对应的文件数组
        //2、遍历保存
        for _ , file := range files {
            saveFile(file, c)
        }
        c.JSON(200, gin.H{
            "code": 200,
            "message": "上传成功",
        })
    })
    r.Run()
}
​
//保存文件
func saveFile(file *multipart.FileHeader, c *gin.Context)  {
    //上传文件到制定目录
    targetPath := dst + common.GenerateFileName(file.Filename)
    if err := c.SaveUploadedFile(file, targetPath); err != nil {
        panic(err)
    }
}

二、路由分组

2.1、路由分组与无路由接口

配套代码:go-ginLearn/demo4/demo4.go

案例:①我们可以来使用gin的group来进行对url进行分组。②noroute:指的是无路由的url会走的相同方法。

代码

package demo4
​
import "github.com/gin-gonic/gin"func Main()  {
    r := gin.Default()
    //v1的API
    v1 := r.Group("/v1")
    {
        v1.POST("/login", func(c *gin.Context) {
            commonResponse(c, "/v1/login")
        })
        v1.POST("/register",func(c *gin.Context) {
            commonResponse(c, "/v1/register")
        })
    }
    
    //v2的API
    v2 := r.Group("v2")
    {
        v2.POST("/login", func(c *gin.Context) {
            commonResponse(c, "/v2/login")
        })
        v2.POST("/register", func(c *gin.Context) {
            commonResponse(c, "/v2/register")
        })
    }
    
    //其他无对应路由时走的接口
    r.NoRoute(func(c *gin.Context) {
        c.JSON(404, gin.H{
            "code": "404",
            "message": "page not found",
        })
    })
​
    r.Run()
}
​
func commonResponse(c *gin.Context, url string)  {
    c.JSON(200, gin.H{
        "code": 200,
        "url": url,
    })
}

测试结果

①/v1/login测试

image-20220528111246375

②/v2/login测试

image-20220528111257186

③随意一个不存在路由:此时就会走相应的一个无路由的接口

image-20220528134816770

三、插件

3.1、gin-jwt

3.1.1、gin-jwt描述

描述:gin的扩展插件,用于认证。

资料:gin-jwt的github地址

gin-jwt:

/469xxx/第25章 web层开发-用户接口开发/jwt
 jwt-go的一个实现
链接:https://pan.baidu.com/s/1eJRbmjipyTOoF1loF_5ucA 
提取码:b2rs 

gin-jwt 中间件是对 jwt-go 的封装以适应 gin 框架。gin-jwt 对不同的请求流程有不同的 handler:

  • 登录请求流程 是用 LoginHandler。
  • 需要 jwt 令牌的后续请求 是用 MiddlewareFunc。
  • 注销流程 是用 LogoutHandler。
  • 刷新请求流程 是用 RefreshHandler。
  • 登录失败、token不正确、权限不足 都会进入 Unauthorized 流程中。

以上每种处理句柄中有一个必须要实现的方法,还有其它可选的方法。


3.1.2、案例实现

配套代码:gin-jwt-Learn/gin-jwtLearn.go

demo描述

loginHandler:登录方法

1、Authorizator()方法:进行登录认证

2、PayloadFunc()处理载荷:也就是在token中进行携带的载荷信息。

MiddlewareFunc:需要认证的接口会使用到的插件

1、会对token进行认证处理

2、对于IdentityHandler是否提前实现:认证处理完成之后会进行解析token并存储到context中

①没有自己实现:"identity"

if mw.IdentityHandler == nil {
 mw.IdentityHandler = func(c *gin.Context) interface{} {
  claims := ExtractClaims(c)
  return claims[mw.IdentityKey]
 }
}

②若是自己实现了IdentityHandler的话

name就会取出自己提前定义好的IdentityKey

c.Set("JWT_PAYLOAD", claims)  //设置载荷信息
identity := mw.IdentityHandler(c)

if identity != nil {
    c.Set(mw.IdentityKey, identity)  //自定义存储的一个身份信息
}

3、若是实现了角色认证,就会走Authorizator()的方法。

代码实现:使用的是官方仓库中的demo示例

package gin_jwt_Learn

import (
	"log"
	"os"
	"time"

	jwt "github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
)

//登录实体类
type login struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

//token中存储相应信息的key名称
var identityKey = "id"

//测试需要认证接口
func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c) //取出claims
	user, _ := c.Get(identityKey) //获取自己提前定义好的身份信息
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

// User demo
type User struct {
	UserName  string
	FirstName string
	LastName  string
}

func Main() {
	//获取命令参数PORT,指定端口号
	port := os.Getenv("PORT")
	r := gin.Default()

	if port == "" {
		port = "8000"
	}

	//jwt插件
	// the jwt middleware
	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{  //相应jwt插件的接口,我们这里去进行实例化
		Realm:       "test zone",  //身份
		Key:         []byte("secret key"),  //秘钥
		Timeout:     time.Minute,  //超时时长
		MaxRefresh:  time.Minute,  //最大的一个刷新时间
		IdentityKey: identityKey,  //身份标识key
		PayloadFunc: func(data interface{}) jwt.MapClaims {  //载荷信息,实际上这个data就是自定义执行的Authenticator方法的返回对象
			if v, ok := data.(*User); ok {
				return jwt.MapClaims{
					identityKey: v.UserName,  //存放的是对应的用户名
				}
			}
			return jwt.MapClaims{}
		},
		IdentityHandler: func(c *gin.Context) interface{} {  //用于取出身份信息
			claims := jwt.ExtractClaims(c)
			return &User{
				UserName: claims[identityKey].(string),
			}
		},
		Authenticator: func(c *gin.Context) (interface{}, error) {  //login的身份认证
			var loginVals login
			if err := c.ShouldBind(&loginVals); err != nil {
				return "", jwt.ErrMissingLoginValues
			}
			userID := loginVals.Username
			password := loginVals.Password

			if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
				return &User{
					UserName:  userID,
					LastName:  "Bo-Yi",
					FirstName: "Wu",
				}, nil
			}

			return nil, jwt.ErrFailedAuthentication
		},
		Authorizator: func(data interface{}, c *gin.Context) bool {  //权限认证
			if v, ok := data.(*User); ok && v.UserName == "admin" {
				return true
			}

			return false
		},
		Unauthorized: func(c *gin.Context, code int, message string) {  //若是身份未认证成功。情况:①登录失败。②token不正确。③权限不足。
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
		},
		// TokenLookup is a string in the form of "<source>:<name>" that is used
		// to extract token from the request.
		// Optional. Default value "header:Authorization".
		// Possible values:
		// - "header:<name>"
		// - "query:<name>"
		// - "cookie:<name>"
		// - "param:<name>"
		TokenLookup: "header: Authorization, query: token, cookie: jwt",  //token查询方式:头部、query查询以及cookie携带
		// TokenLookup: "query:token",
		// TokenLookup: "cookie:token",

		// TokenHeadName is a string in the header. Default value is "Bearer"
		TokenHeadName: "Bearer",  //请求头的header的值

		// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
		TimeFunc: time.Now,
	})

	if err != nil {
		log.Fatal("JWT Error:" + err.Error())
	}

	// When you use jwt.New(), the function is already automatically called for checking,
	// which means you don't need to call it again.
	errInit := authMiddleware.MiddlewareInit()

	if errInit != nil {
		log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
	}

	//1、登录接口
	//测试接口:http://localhost:8080/login
	/**
		{
			"username": "admin",
			"password": "admin"
		}
	 */
	r.POST("/login", authMiddleware.LoginHandler)  //LoginHandler:登录逻辑

	//2、无路由接口,执行之前会进行认证
	//测试接口:http://localhost:8080/test
	//对于没有路由的会进行认证校验
	r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) {
		claims := jwt.ExtractClaims(c)
		log.Printf("NoRoute claims: %#v\n", claims)
		c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
	})

	//3、需要认证接口
	auth := r.Group("/auth")
	// 接口:http://localhost:8080/auth/refresh_token,刷新token接口
	// Refresh time can be longer than token timeout
	auth.GET("/refresh_token", authMiddleware.RefreshHandler)
	//使用jwt认证插件
	auth.Use(authMiddleware.MiddlewareFunc())
	{
		//接口:http://localhost:8080/auth/hello,用于查看当前的token信息
		auth.GET("/hello", helloHandler)
	}

	//指定端口运行
	//if err := http.ListenAndServe(":"+port, r); err != nil {
	//	log.Fatal(err)
	//}
	r.Run()
}

测试接口:

①登录接口:

image-20220528152147128

②需要认证校验的接口

image-20220528152240174

3.1.3、扩展gin-jwt

①支持form读取token

需求

接口文档在form-data中需要进行读取鉴权:

image-20220531001343339

解决方案

①由于gin-jwt不支持从form中读取token,那么就需要修改源码了,首先去到对应的项目地址进行克隆。

gin-jwt项目地址

②修改核心代码

image-20220531000719130

1、添加相应的读取form方法

image-20220531000805651

case "form":
    token, err = mw.jwtFromFormData(c, v)

2、添加方法

func (mw *GinJWTMiddleware) jwtFromFormData(c *gin.Context, key string) (string, error) {
	token := c.DefaultPostForm("token", "")
	if token == "" {
		return "", ErrEmptyParamToken
	}
	return token, nil
}

如何使用?

image-20220531001250610

新增即可:

, form: token

②自己调用方法生成token,而不是走loginhandler(调用原始方法)

这个方法在官方示例中没有使用,通过看源码发现给我们提供了这个方法

image-20220603185325815

var authMiddleware *jwt.GinJWTMiddleware
authMiddleware.TokenGenerator(jwt.MapClaims{
   //对应的id
   constants.IdentityKey: 用户id,
})

四、异常处理

4.1、gin的全局异常处理捕捉并统一返回

配套代码:go-ginLearn/demo5/demo5.go

参考:golang(gin)的全局统一异常处理,并统一返回json

对于web项目中进行全局异常捕获:

package demo5

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

func Recover(c *gin.Context)  {
	defer func() {
		if r := recover(); r != nil{
			//打印错误堆栈信息
			log.Printf("panic: %v\n", r)
			debug.PrintStack()
			//封装通用JSON返回
			c.JSON(http.StatusOK, gin.H{
				"code": "1",
				"msg": errorToString(r),
			})
			//终止后续接口调用,不加的话recover到异常后,还会继续执行接口里后续代码
			c.Abort()
		}
	}()
	//加载完 defer recover,继续后续的插件及代码执行
	c.Next()
}
//错误转字符串
func errorToString(r interface{}) string {
	switch v := r.(type) {
		case error:
			return v.Error()
		default:
			return r.(string)
	}
}

func Main()  {
	r := gin.Default()
	//使用全局异常捕捉插件:Recover 要尽量放在第一个被加载
	r.Use(Recover)
	r.GET("/test", func(c *gin.Context) {
		// 无意抛出 panic
		var slice = []int{1, 2, 3, 4, 5}
		slice[6] = 6
	})

	r.Run()
}

测试接口:当进行panic时,就会向上抛出,之后被defer进行捕捉到

image-20220528154821093

五、请求参数校验

Gin请求参数校验

字符串:

max=32 # 最大值为10,即小于等于10

使用方式:

type Timeout struct {
	Connect int `json:"connect" binding:"required;max=32"`
	Read    int `json:"read" binding:"required"`
	Send    int `json:"send" binding:"required"`
}

对于shouldBindQuery对应字段的别名:form:xx

//登录
type UserLoginParam struct {
	UserName string `json:"username" form:"username" `
	PassWord string `json:"password" form:"password"`
}

扩展

1、JSON序列化与反序列化

import (
	"encoding/json"
	"fmt"
)

type userInfo struct {
	Name string
	Age int `json:"age"`   //json序列化时替换字段名称
	Hobby []string
}

func main() {
	a := userInfo{
		Name:  "cl",
		Age:   10,
		Hobby: []string{"golang","java"},
	}
	//1、对象 => JSON
	buf, err := json.Marshal(a)
	//若是出现异常
	if err != nil {
		panic(buf)
	}
	//打印序列化后的json
	fmt.Println(string(buf))

	//2、对象 => JSON,过程中进行处理
	buf, err = json.MarshalIndent(a, "", "\t")
	if err != nil {
		panic(buf)
	}
	fmt.Println(string(buf))

	//3、JSON => 对象
	var b userInfo
	err = json.Unmarshal(buf, &b)
	if err != nil {
		panic(err)  //作为报告致命错误的一种方式,当某些不应该发生的场景发生时,我们就应该调用panic。【替代try catch】
	}
	fmt.Printf("%#v\n", b)
	
}

image-20220508231042728


参考文章

[1]. gin 结构体json解析的坑

[2]. Gin中文文档