gin编写后端API的使用技巧

摘要:
=Nil{fmt.Println//returnerror//…}//returnres//…}}我认为应该尽快定义返回结构,以便减少函数内部变量的定义。例如,上面的代码res.Users可以直接用作查询语句结果的扫描结构,以避免先定义变量user,然后将值分配给res.user,这是一个好习惯。当然,对于同时适用于多个接口的一些常见结构,最好定义一个结构以供重用。如果许多接口具有相同的结构,我们也可以编写一个公共结构,然后为每个接口独立定义匿名结构。例如,typeuserstruct{IDint`json:“user_id”db:“id”`}functionest{//initstructres:=struct{Users*[]user`json:”Users“`Count`json::“Count”`}{}//selectsqlStr:=“SELECTIDFROMuser”ifer:=db.db.Select;犯错误
前言

之前在写练手的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
}

免责声明:文章转载自《gin编写后端API的使用技巧》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Neo4J图库的基础介绍(二)-图库开发应用array_walk() 函数下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

1、结构体

1、为什么需要结构体   为了表示一些复杂的事物,而普通的基本类型无法满足实际要。 2、求什么叫结构体  把一些基本类型数据组合在一起形成的一个新的复合数据类型,这个叫做结构体。 3、如何定义结构体 /*---------------------------------------------- 结构体的定义方式 ---------------------...

002输入子系统驱动

输入子系统概念介绍(第十三课/第一节) 回顾第三个驱动程序(中断方式的按键驱动程序)和测试程序,发现有一些缺点:这个驱动程序没办法用在别人写的现成的应用程序上(比如:QT),因为别人写的应用程序肯定不会来打开你这个"/dev/third_chrdev"。别人打开的是一些现成的设备(比如:/dev/tty),甚至别人都不打开设备,而是直接调用 scanf()...

(转)C#调用C函数(DLL)传递参数问题

备忘: 1.C函数参数为字符串char*。如果是入参,对应C#中string或StringBuilder;如果是出参对应C#中StringBuider; 2.C函数参数为结构体指针,需在C#中对应定义结构体。如果是入参,C#中可为myfunction(MyStruct mystruct)或myfunction(ref MyStruct mystruct);...

标准C程序设计七---32

Linux应用 编程深入 语言编程标准C程序设计七---经典C11程序设计以下内容为阅读:《标准C程序设计》(第7版) 作者:E. Balagurusamy(印), 李周芳译 清华大学出版社 2017.7《21天学通C语言》(第7版) 作者:Bradley Jones Peter Aitken Dean Miller(美), 姜佑译 人民邮电出版社 201...

源码剖析——深入Windows句柄本质

参考资料: 1. http://www.codeforge.cn/read/146318/WinDef.h__html windef.h头文件 2. http://www.codeforge.cn/read/146318/WinNT.h__html winnt.h头文件 3. https://msdn.microsoft.com/en-us/library...

内表、结构赋值转换规则

内表转换规则... 57 C语言中的结构对齐... 57 ABAP结构体对齐... 58 结构体相互赋值转换规则... 59 MOVE-CORRESPONDING(结构体赋值)... 62 内表转换规则 内表只能被转换成其他内表,而不能转换成结构或基本类型。 一个内表能否转换成其他内表与内表中的现有数据行没有关系,而是看两个内表的行结构是否可转换...