• gin编写后端API的使用技巧


    前言

    之前在写练手的go项目的时候, 一方面确实觉得使用go来作为开发语言觉得顺手且速度快, 另一方面也感觉到了一些令人头疼的地方, 比如在编写某些接口时, 有一些复合查询的条件, 例如招聘网站的按 省市/地铁/商圈/工种/薪资/工龄/学历 等条件查询, 该查询是复合的, 你不知道每次用户选择的是哪些类型等等类似的问题, 本篇总结一下我是怎样去整理代码的, 我也没写多久 go 的项目, 可能总结的不到位, 欢迎纠正

    本文不使用 ORM, 只使用 Gin 和 Sqlx

    正文

    总是需要定义好多结构体

    我们知道, 使用Gin返回数据时, Gin会自己将结构体转换成 json 返回给前端, 但是因为每个接口的返回值总是不尽相同的, 这样就会造成几乎每个接口都需要定义一个结构体作为这个接口专用的返回值结构, 对于只用一次的结构体, 不应该单独放置, 故我们使用匿名结构体来作为这个处理函数专用的结构体

    func test(c *gin.Context) {
    	// init struct
    	res := struct {
    		Users *[]struct {
    			UserID int `json:"user_id" db:"id"`
    		} `json:"users"`
    		Count int `json:"count"`
    	}{}
    	// select
    	sqlStr := "SELECT id FROM user"
    	if err := db.DB.Select(&res.Users, sqlStr); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	// return res
    	// ...
    }
    

    我认为应尽早的将返回结构体定义出来, 这样可减少函数内部的变量定义, 比如上面的代码, res.Users 可直接作为查询语句结果的扫描结构体使用, 避免出现先定义一个变量 users, 在赋值给 res.Users 的情况, 这是一个好习惯

    当然, 对于某些通用的结构体同时适用多个接口的情况, 我们还是使用定义一个结构体复用的方式为佳, 如果是多个接口有大部分是相同的结构, 我们也可以写一个通用的结构体, 然后每个接口独立出来的单独定义匿名结构体即可, 例如

    type user struct {
    	ID int `json:"user_id" db:"id"`
    }
    
    func test(c *gin.Context) {
    	// init struct
    	res := struct {
    		Users *[]user `json:"users"`
    		Count int     `json:"count"`
    	}{}
    	// select
    	sqlStr := "SELECT id FROM user"
    	if err := db.DB.Select(&res.Users, sqlStr); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	// return res
    	// ...
    }
    
    func test1(c *gin.Context) {
    	// init struct
    	res := struct {
    		Users *[]user `json:"users"`
    		Page  int     `json:"page"`
    		Count int     `json:"count"`
    	}{}
    	// select
    	sqlStr := "SELECT id FROM user"
    	if err := db.DB.Select(&res.Users, sqlStr); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	// return res
    	// ...
    }
    
    

    组合查询

    如前言所说, 我们经常会需要对数据进行组合查询, 会导致代码变得混乱, 这里提供一个思路可以较好的保持代码的整洁性和可读性

    func test(c *gin.Context) {
    	args := []interface{}{}
    	search := " "
    	j := "WHERE"
    	// get data
    	if name, ok := c.GetQuery("user_name"); !ok {
    		search += fmt.Sprintf("%v name LIKE ? ", j)
    		args = append(args, "%"+name+"%")
    		j = "AND"
    	}
    	if groupID, ok := c.GetQuery("group_id"); !ok {
    		search += fmt.Sprintf("%v group_id=? ", j)
    		args = append(args, groupID)
    		j = "AND"
    	}
    	search += "ORDER BY id DESC "
    	// init struct
    	res := struct {
    		Users *[]struct {
    			UserID int `json:"user_id" db:"id"`
    		} `json:"users"`
    		Count int `json:"count"`
    	}{}
    	// select
    	sqlStr := "SELECT id FROM user"
    	if err := db.DB.Select(&res.Users, sqlStr+search, args...); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	// return res
    	// ...
    }
    
    

    如上面所写, 在先定义两个变量, args为 interface 类型, 存放我们传入的参数, search为 string 类型, 存放我们拼接的查询sql语句, search为一个空格的字符串目的是保持SQL语句不报错, 而后, 我们并不知道哪个参数为第一个参数, 所以我们定义一个连接的string, 默认为 WHERE , 然后我们获取有可能存在的参数, 如果其存在则代表用户选择了这个筛选条件, 我们将其语句加在 search 之后, 同时将参数放置在 args 后, 假设找到的这个参数为第一个参数, 则使用 WHERE 连接, 同时将连接设置为 AND 保证格式合法, 注意拼接的SQL语句最后面都有一个空格目的是符合格式

    如果想排序, 最后在后面加上 order by 来排序即可

    带分页的组合查询

    实际的开发中, 往往接口都是需要带分页的, 那么带分页的组合查询, 一般也需要在返回值中加入一个字段标示总条数来方便排序, 有人会使用 FOUND_ROWS() 来查询上一次查询的总条数, 但是这个函数 mysql 官方并不推荐使用, 并且在以后打算替代, 官方文档, 其推荐使用 COUNT 来查询, mysql对 COUNT(*) 进行了特别的优化, 使用该函数速度会很快(SELECT 从一个表查询的时候) 官方文档

    首先我们编写一个通用的函数来处理URL中的分页值

    // LimitVerify limit middleware
    // Receive page and page_size from url
    // page default 1, page_size default 20
    func LimitVerify(c *gin.Context) {
    	page, err := strconv.Atoi(c.DefaultQuery("page", "1"))
    	if err != nil {
    		page = 1
    	}
    	pageSize, err := strconv.Atoi(c.DefaultQuery("page_size", "20"))
    	if err != nil {
    		pageSize = 20
    	}
    	c.Set("page", page)
    	c.Set("pageSize", pageSize)
    	c.Next()
    }
    

    将其加在需要分页的接口里

    groupGroup.GET("/:groupID/user", middleware.LimitVerify, test)
    

    然后在具体的逻辑中, 即可使用 c.Get() 来获取分页数据

    func test(c *gin.Context) {
    	args := []interface{}{}
    	countArgs := []interface{}{}
    	search := " "
    	countSearch := " "
    	j := "WHERE"
    	// get page
    	page, _ := c.Get("page")
    	pageSize, _ := c.Get("pageSize")
    	// get data
    	if name, ok := c.GetQuery("user_name"); !ok {
    		search += fmt.Sprintf("%v name LIKE ? ", j)
    		countSearch += fmt.Sprintf("%v name LIKE ? ", j)
    		args = append(args, "%"+name+"%")
    		countArgs = append(countArgs, "%"+name+"%")
    		j = "AND"
    	}
    	if groupID, ok := c.GetQuery("group_id"); !ok {
    		search += fmt.Sprintf("%v group_id=? ", j)
    		countSearch += fmt.Sprintf("%v group_id=? ", j)
    		args = append(args, groupID)
    		countArgs = append(countArgs, groupID)
    		j = "AND"
    	}
    	search += "ORDER BY id DESC "
    	if page != 0 {
    		// limit
    		search = search + " LIMIT ?,?"
    		args = append(args, pageSize.(int)*(page.(int)-1), pageSize.(int))
    	}
    	// init struct
    	res := struct {
    		Users *[]struct {
    			UserID int `json:"user_id" db:"id"`
    		} `json:"users"`
    		Count int `json:"count"`
    	}{}
    	// select
    	sqlStr := "SELECT id FROM user"
    	if err := db.DB.Select(&res.Users, sqlStr+search, args...); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	sqlStr = "SELECT COUNT(id) FROM user"
    	if err := db.DB.Get(&res.Count, sqlStr+countSearch, countArgs...); err != nil {
    		fmt.Println("error")
    		// return error
    		// ...
    	}
    	// return res
    	// ...
    }
    
    

    为了加入 count, 我们又新增一组参数和sql, 名为 countArgs 和 countSearch, 为了接口兼容性, 我们和前段商议当 page 参数为 0 时不进行分页, 所以仅仅在 page 不等于 0 时加入分页

    通用的JSON返回函数

    一般接口返回的数据都是JSON, 但是每次又要写 c.JSON 于是我将其按照使用场景写了几个通用的函数

    responseFormat.go

    package tools
    
    import (
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    )
    
    // FormatOk ok
    func FormatOk(c *gin.Context) {
    	c.JSON(http.StatusOK, gin.H{
    		"code": 200,
    		"data": "success",
    	})
    	// Return directly
    	c.Abort()
    }
    
    // FormatError err
    func FormatError(c *gin.Context, errorCode int, message string) {
    	c.JSON(http.StatusOK, gin.H{
    		"code": errorCode,
    		"data": message,
    	})
    	// Return directly
    	c.Abort()
    }
    
    // FormatData data
    func FormatData(c *gin.Context, data interface{}) {
    	c.JSON(http.StatusOK, gin.H{
    		"code": 200,
    		"data": data,
    	})
    	// Return directly
    	c.Abort()
    }
    
    

    通用的JWT函数

    JWT作为一种HTTP鉴权方式已经有非常多的人员使用, 这里提供自用的签发和解密啊函数供参考

    jwt.go

    package tools
    
    import (
    	"time"
    
    	"github.com/dgrijalva/jwt-go"
    )
    
    // UserData jwt user info
    type UserData struct {
    	ID       int    `json:"id" db:"user_id"`
    	Name     string `json:"name" db:"user_name"`
    	RoleName string `json:"role_name" db:"role_name"`
    	GroupID  *int   `json:"group_id" db:"group_id"`
    }
    
    type myCustomClaims struct {
    	Data UserData `json:"data"`
    	jwt.StandardClaims
    }
    
    // JWTIssue issue jwt
    func JWTIssue(d UserData) (string, error) {
    	// set key
    	mySigningKey := []byte(EnvConfig.JWT.Key)
    
    	// Calculate expiration time
    	nowTime := time.Now()
    	expireTime := nowTime.Add(time.Duration(EnvConfig.JWT.Expiration) * time.Second)
    
    	// Create the Claims
    	claims := myCustomClaims{
    		d,
    		jwt.StandardClaims{
    			ExpiresAt: expireTime.Unix(),
    			Issuer:    "remoteAdmin",
    		},
    	}
    
    	// issue
    	t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    	st, err := t.SignedString(mySigningKey)
    	if err != nil {
    		return "", err
    	}
    	return st, nil
    }
    
    // JWTDecrypt string token to data
    func JWTDecrypt(st string) (*UserData, error) {
    	token, err := jwt.ParseWithClaims(st, &myCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
    		return []byte(EnvConfig.JWT.Key), nil
    	})
    
    	if err != nil {
    		return nil, err
    	}
    
    	if claims, ok := token.Claims.(*myCustomClaims); ok && token.Valid {
    		// success
    		return &claims.Data, nil
    	}
    	return nil, err
    }
    
    

    userVerify.go

    package middleware
    
    import (
    	"fmt"
    	"remoteAdmin/db"
    	"remoteAdmin/tools"
    	"strconv"
    
    	"github.com/gin-gonic/gin"
    	"go.uber.org/zap"
    )
    
    // TokenVerify get user info from jwt
    func TokenVerify(c *gin.Context) {
    	t := c.Request.Header.Get("token")
    	if t == "" {
    		tools.FormatError(c, 2003, "token expired or invalid")
    		tools.Log.Warn(fmt.Sprintf("token invalid: %v", t))
    		return
    	}
    	u, err := tools.JWTDecrypt(t)
    	if err != nil {
    		tools.FormatError(c, 2003, "token expired or invalid")
    		tools.Log.Warn(fmt.Sprintf("token invalid: %v", t), zap.Error(err))
    		return
    	}
    	// get RDB token
    	if val, err := db.RDB.Get(db.RDB.Context(), strconv.Itoa(u.ID)).Result(); err != nil || val != t {
    		tools.FormatError(c, 2003, "token expired or invalid")
    		tools.Log.Info(fmt.Sprintf("token expired: %v", t), zap.Error(err))
    		return
    	}
    	// set userData to gin.Context
    	c.Set("userID", u.ID)
    	c.Set("userRoleName", u.RoleName)
    	if u.GroupID != nil {
    		c.Set("userGroupID", *u.GroupID)
    	}
    	// Next
    	c.Next()
    }
    
    

    其中结构体 userData 为存放的信息结构, 可按需修改

    开启Gin的跨域

    一般前后端分离的项目后端都需要设置同意跨域, gin设置跨域代码如下

    CrossDomain.go

    package middleware
    
    import (
    	"fmt"
    	"net/http"
    	"strings"
    
    	"github.com/gin-gonic/gin"
    )
    
    // CorsHandler consent cross-domain middleware
    func CorsHandler() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		method := c.Request.Method               // method
    		origin := c.Request.Header.Get("Origin") // header
    		var headerKeys []string                  // keys
    		for k := range c.Request.Header {
    			headerKeys = append(headerKeys, k)
    		}
    		headerStr := strings.Join(headerKeys, ", ")
    		if headerStr != "" {
    			headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
    		} else {
    			headerStr = "access-control-allow-origin, access-control-allow-headers"
    		}
    		if origin != "" {
    			c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
    			c.Header("Access-Control-Allow-Origin", "*")                                       // This is to allow access to all domains
    			c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") // All cross-domain request methods supported by the server, in order to avoid multiple'pre-check' requests for browsing requests
    			//  header
    			c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
    			c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar")
    			c.Header("Access-Control-Max-Age", "172800")
    			c.Header("Access-Control-Allow-Credentials", "false")
    			c.Set("content-type", "application/json")
    		}
    
    		// Release all OPTIONS methods
    		if method == "OPTIONS" {
    			c.JSON(http.StatusOK, "Options Request!")
    		}
    		// Processing request
    		c.Next()
    	}
    }
    
    

    使用方法: 将其注册到总路由 router 的中间件中即可, 例如

    // InitApp init gshop app
    func InitApp() *gin.Engine {
    	// gin.Default uses Use by default. Two global middlewares are added, Logger(), Recovery(), Logger is to print logs, Recovery is panic and returns 500
    	router := gin.Default()
    	// Add consent cross-domain middleware
    	router.Use(middleware.CorsHandler())
    
    	// init app router
    	user.Router(router)
    
    	return router
    }
    
    

    Gin日志

    我通常使用 Zap 模块来记录日志, 将日志写入进文件中, 但是 Gin 自己携带了日志, 尤其是设置 debug 关闭时无法完美的将其兼容到一起, 于是我找到了 李文周的博客 大佬的博客, 抄袭了一下, 达到了Gin日志与自己记录的日志合并到同一个文件的效果, 并且共用日志分割功能

    log.go

    package tools
    
    import (
    	"net"
    	"net/http"
    	"net/http/httputil"
    	"os"
    	"runtime/debug"
    	"strings"
    	"time"
    
    	"github.com/gin-gonic/gin"
    	"github.com/natefinch/lumberjack"
    	"go.uber.org/zap"
    	"go.uber.org/zap/zapcore"
    )
    
    // Log zapLog
    var Log *zap.Logger
    
    // LumberJackLogger log io
    var LumberJackLogger *lumberjack.Logger
    
    // Log cutting settings
    func getLogWriter() zapcore.WriteSyncer {
    	LumberJackLogger = &lumberjack.Logger{
    		Filename:   "api.log", // Log file location
    		MaxSize:    10,        // Maximum log file size(MB)
    		MaxBackups: 5,         // Keep the maximum number of old files
    		MaxAge:     30,        // Maximum number of days to keep old files
    		Compress:   false,     // Whether to compress old files
    	}
    	return zapcore.AddSync(LumberJackLogger)
    }
    
    // log encoder
    func getEncoder() zapcore.Encoder {
    	// Use the default JSON encoding
    	encoderConfig := zap.NewProductionEncoderConfig()
    	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
    	return zapcore.NewJSONEncoder(encoderConfig)
    }
    
    // InitLogger init log
    func InitLogger() {
    	writeSyncer := getLogWriter()
    	encoder := getEncoder()
    	core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
    	Log = zap.New(core, zap.AddCaller())
    }
    
    // GinLogger Receive the default log of the gin framework
    func GinLogger() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		start := time.Now()
    		path := c.Request.URL.Path
    		query := c.Request.URL.RawQuery
    		c.Next()
    
    		cost := time.Since(start)
    		Log.Info("[GIN]",
    			zap.Int("status", c.Writer.Status()),
    			zap.String("method", c.Request.Method),
    			zap.String("path", path),
    			zap.String("query", query),
    			zap.String("ip", c.ClientIP()),
    			zap.String("user-agent", c.Request.UserAgent()),
    			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
    			zap.Duration("cost", cost),
    		)
    	}
    }
    
    // GinRecovery Recover the panic that may appear in the project, and use zap to record related logs
    func GinRecovery(stack bool) gin.HandlerFunc {
    	return func(c *gin.Context) {
    		defer func() {
    			if err := recover(); err != nil {
    				// Check for a broken connection, as it is not really a
    				// condition that warrants a panic stack trace.
    				var brokenPipe bool
    				if ne, ok := err.(*net.OpError); ok {
    					if se, ok := ne.Err.(*os.SyscallError); ok {
    						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
    							brokenPipe = true
    						}
    					}
    				}
    
    				httpRequest, _ := httputil.DumpRequest(c.Request, false)
    				if brokenPipe {
    					Log.Error(c.Request.URL.Path,
    						zap.Any("error", err),
    						zap.String("request", string(httpRequest)),
    					)
    					// If the connection is dead, we can't write a status to it.
    					c.Error(err.(error)) // nolint: errcheck
    					c.Abort()
    					return
    				}
    
    				if stack {
    					Log.Error("[Recovery from panic]",
    						zap.Any("error", err),
    						zap.String("request", string(httpRequest)),
    						zap.String("stack", string(debug.Stack())),
    					)
    				} else {
    					Log.Error("[Recovery from panic]",
    						zap.Any("error", err),
    						zap.String("request", string(httpRequest)),
    					)
    				}
    				c.AbortWithStatus(http.StatusInternalServerError)
    			}
    		}()
    		c.Next()
    	}
    }
    
    

    我们在自己写日志时, 调用全局变量 Log 即可

    tools.Log.Warn("DB error", zap.Error(err))
    

    记录Gin的日志, 将其注册进全局的 router 中间件即可

    // InitApp init gshop app
    func InitApp() *gin.Engine {
    	// gin.Default uses Use by default. Two global middlewares are added, Logger(), Recovery(), Logger is to print logs, Recovery is panic and returns 500
    	// gin.New not use Logger and Recovery
    	router := gin.Default()
    	// gin log
    	router.Use(tools.GinLogger(), tools.GinRecovery(true))
    	// Add consent cross-domain middleware
    	router.Use(middleware.CorsHandler())
    
    	// init app router
    	user.Router(router)
    	group.Router(router)
    	device.Router(router)
    	dynamic.Router(router)
    	control.Router(router)
    
    	return router
    }
    

        作者:ChnMig

        出处:http://www.cnblogs.com/chnmig/

        本文版权归作者和博客园所有,欢迎转载。转载请在留言板处留言给我,且在文章标明原文链接,谢谢!

        如果您觉得本篇博文对您有所收获,觉得我还算用心,请点击左下角的 [推荐],谢谢!

  • 相关阅读:
    C++编程入门题目--No.5
    C++编程入门题目--No.4
    C++编程入门题目--No.3
    C++编程入门题目--No.2
    C++入门编程题目 NO.1
    深度使用魅族16T后的评价(本人魅友,绝对客观公正,不要盲目的为手机厂商辩护,想想从当初到现在,魅族正在一步步背离自己的信仰,有问题,解决问题才能有更好的发展)
    ACM及各类程序竞赛专业术语
    python刷LeetCode:3.无重复字符的最长子串
    python刷LeetCode:2.两数相加
    python刷LeetCode:1.两数之和
  • 原文地址:https://www.cnblogs.com/chnmig/p/14296356.html
Copyright © 2020-2023  润新知