这几天重新学习了一遍gin框架:收获颇多
Gin框架的初始化
有些项目中 初始化gin框架写的是: r := gin.New()
r.Use(logger.GinLogger(), logger.GinRecovery(true))
而不是r := gin.Default()
为什么呢?
点击进入Default源码发现其实他也是new+两个中间件,(Logger,Recovery)
default源码
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
但是中间件已经固定了,对于一些精细化开发来说换成new+自定义中间件则更有性价比;
自定义的:
r := gin.New()
r.Use(logger.GinLogger(), logger.GinRecovery(true)) //中间件
更精细化,这两个区别是一个用的是基础log,一个用的是gin框架特殊处理的log
项目结构
后端处理路线通常是先写router层:各种各样的路由信息,
再写controller层:
1 获取参数和参数校验 :定义参数结构,接收参数并存放
2 业务处理 :写到逻辑层
3 返回响应 :可以写中间件方便多次处理
然后logic层:(举登录的例子)
1.一系列登录逻辑判断:
用户是否存在 sql写在dao层,供logic层调用
账号密码是否符合格式 可以不使用sql语句的,直接逻辑判断
判断完成生成对应uid 调用一系列外置方法
对数据进行加密 调用一系列外置方法
保存进数据库 调用dao层sql方法
然后是dao层,写各种数据库操作封装成的方法以供调用
在我看来,controller层最大,logic以实现controller层的业务而出现,dao层最小,以实现logic层中的数据库操作为己任;
所以每次写小需求,先写router层,再写controller层,再写logic层,然后是dao层
这样理解起来比盲目学习要好很多;
Query和Param的区别
虽然已经学了不知道多少遍了,但这两个老是弄反,再写一遍吧
Query:
Query
用于获取 URL 查询参数。它从 URL 中解析参数,并返回一个字符串值。
如:
name := c.Query("name")
https://example.com/search?query=golang&page=1&limit=10
Param:
Param
用于获取路径参数。路径参数是包含在 URL 中的参数,例如/users/:id
中的:id
就是路径参数。
如:
https://example.com/users/123
userID := c.Param("id") // Returns "123"
数据校验
而shouldbindjson是什么呢?
ShouldBindJSON:
ShouldBindJSON
用于将 JSON 数据绑定到结构体。它从请求的 JSON 主体中提取数据,并将其解析到指定的结构体中。
var user User
if err := c.ShouldBindJSON(&user); err == nil {
// 处理 user 结构体中的数据
}
将 HTTP 请求体中的 JSON 数据绑定到变量 user
。
注意点:ShouldBindJSON只能识别是不是json格式,并不能保证数据是否完全符合结构体格式,例如
这样写都会直接通过,
只有
会报错,所以需要手动去对数据进行业务判断(数据校验),例如数据不为空,数据类型不对,数据不符合格式等等,因此就引入了第三方的validator库进行爽歪歪!(^ ▽ ^),ps:之后说这个库
binding就是gin框架参数校验的tag:required就是不能为空,不然直接结束
常用tag
- form:
form:"fieldName"
表示该字段的值将从 HTTP 请求的表单数据中获取,其中"fieldName"
是表单字段的名称。
- query:
query:"paramName"
表示该字段的值将从 URL 查询参数中获取,其中"paramName"
是查询参数的名称。
- json:
json:"fieldName"
表示该字段的值将从 JSON 数据中获取,其中"fieldName"
是 JSON 对象中的字段名。
- uri:
uri:"paramName"
表示该字段的值将从 URL 的路径参数中获取,其中"paramName"
是路径参数的名称。
- binding:
binding:"required"
表示该字段是必需的,如果请求中缺少该字段,将返回错误。
- xml:
xml:"fieldName"
表示该字段的值将从 XML 数据中获取,其中"fieldName"
是 XML 对象中的字段名。
- header:
header:"Header-Name"
表示该字段的值将从 HTTP 请求头中获取,其中"Header-Name"
是请求头的名称。
- time:
time_format:"2006-01-02"
表示该时间字段的格式,用于将字符串转换为时间类型。
这些标签值用于告诉 Gin 框架在处理请求时如何从不同的数据源(如表单、查询参数、JSON 数据等)中提取和绑定数据。在使用 Gin 进行参数绑定时,可以根据请求中的数据类型和来源选择适当的标签。
validator库
接下来是重头戏validator库的说明,validator使用起来还是非常方便的,就是配置麻烦了一点(只是亿点点…)
validator
提供了许多内置的验证规则,如 required
、min
、max
、email
、url
等,同时也支持使用正则表达式、自定义函数等进行验证。使用 validator
可以有效地提高应用程序的数据完整性和安全性。
而当不符合validator的tag规则时,返回的错误也是由validator库内置的,比如
我们需要将其改成中文并符合我们自己的代码习惯(这种配置配一次就行了,以后项目框架基本不会变,validator的代码也几乎不需要变化);
例如说自动翻译成中文,将特定字段格式转为json供前端使用,去掉结构体名称,处理复杂逻辑时候的结构体转换,咳咳,想要完美道阻且艰,错误信息完美到这里已经差不多了(还有很多优化空间阿伟!)
然后是固定代码格式:(包含了初始化翻译器,可以自动翻译中文,把结构体字段转为json,去掉包名,给用户看)
最后还是只有三个功能,可以直接使用哦~~
package controller
import (
"fmt"
"reflect"
"strings"
"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"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
)
// 这种初始化代码,知道意思就行,因为每次的项目这个都是提前写好的;
// 定义一个全局翻译器T
var trans ut.Translator
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() // 中文翻译器
enT := en.New() // 英文翻译器
// 第一个参数是备用(fallback)的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
var ok bool
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = zhTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
// removeTopStruct 去除提示信息中的结构体名称,使用时直接包裹就行了
func removeTopStruct(field map[string]string) map[string]string {
res := map[string]string{}
for field, err := range field {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
使用的时候举例子:
controller/user的post登陆请求
func SignUpHandler(c *gin.Context) {
//1 获取参数和参数校验
p := new(models.ParamSignUp) //声明参数p是接收账号密码时的类型
err := c.ShouldBindJSON(&p) //获取参数传给p
if err != nil {
zap.L().Error("SignUp with invalid param", zap.Error(err)) //用日志输出
//判断err是不是validator.ValidationErrors 类型,不是的话就不翻译了
errs, ok := err.(validator.ValidationErrors)
if !ok { //不是validator类型,正常响应
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
}
//是validator类型的,进行翻译操作
c.JSON(http.StatusOK, gin.H{
"msg": removeTopStruct(errs.Translate(trans)),
})
return
}
只用判断是不是validator类型,是的话返回err.Translate(trans)就好了
至于为什么不做其他的处理直到完美,首先代码复杂了,其次对效率也有不小的影响,有这三步已经差不多了;
对状态码的定义
为什么要引入自定义状态码呢?首先我们看自己写的响应
我们会发现很大一部分代码都用来表示重复的响应代码段了,有什么统一格式的写法么?
当然有!蹡蹡
我们可以定义成这种格式,为什么呢,因为统一格式有利于前端进行工作,包括看错误提示信息和接受数据都方便一些
把经常会用到的响应单独拉出来写进一个文件,例如:
package controller
type MyCode int64 //自定义MyCode类型
const ( //使用 const 关键字定义了一组错误码,每个错误码都是 MyCode 类型的常量。
CodeSuccess MyCode = 1000
CodeInvalidParams MyCode = 1001
CodeUserExist MyCode = 1002
CodeUserNotExist MyCode = 1003
CodeInvalidPassword MyCode = 1004
CodeServerBusy MyCode = 1005
CodeInvalidToken MyCode = 1006
CodeInvalidAuthFormat MyCode = 1007
CodeNotLogin MyCode = 1008
)
var msgFlags = map[MyCode]string{ //map类型,映射错误码和信息
CodeSuccess: "success",
CodeInvalidParams: "请求参数错误",
CodeUserExist: "用户名重复",
CodeUserNotExist: "用户不存在",
CodeInvalidPassword: "用户名或密码错误",
CodeServerBusy: "服务繁忙",
CodeInvalidToken: "无效的Token",
CodeInvalidAuthFormat: "认证格式有误",
CodeNotLogin: "未登录",
}
func (c MyCode) Msg() string { //把上边两个连起来,传入code,传出code对应的信息
//当调用 Msg 方法时,会查找 msgFlags 中是否存在对应的错误信息,
//如果存在则返回,否则返回默认的错误信息,例如 CodeServerBusy 对应的信息。
msg, ok := msgFlags[c]
if ok {
return msg
}
return msgFlags[CodeServerBusy]
}
再写几个方法把这些响应给包装起来:
例如
type ResponseData struct {
Code MyCode `json:"code"` //把错误码都拉过来
Message string `json:"message"`
Data interface{} `json:"data"`
}
func ResponseError(c *gin.Context, code MyCode) { //错误的,传入状态码,直接用写好的错误信息
rd := &ResponseData{
Code: code,
Message: code.Msg(),
Data: nil,
}
c.JSON(http.StatusOK, rd)
}
在代码中调用时前后比较
if err != nil {
zap.L().Error("SignUp with invalid param", zap.Error(err)) //用日志输出
//判断err是不是validator.ValidationErrors 类型,不是的话就不翻译了
errs, ok := err.(validator.ValidationErrors)
if !ok { //不是validator类型,正常响应
//这里原本是写响应状态码和msg,但现在就可以直接调用定义好的了
ResponseError(c, CodeUserExist)
}
然后是登录
用户认证
cookie和session方式
我们平时访问网站,当不登录时,权限是非常低的,因为http是无状态的协议
需要每次访问接口时将cookie和session也访问一下是否带cookie的值
问题:
所以token也就应运而生了
token方式
看这个图的话会觉得和cookie,session方式基本差不多
其实差距还是很大的
而对应的实践就是JWT了
-
签名
三部分组成:
-
Header(头部):
- 头部通常由两部分组成:令牌的类型(JWT)和所使用的签名算法,例如 HMAC SHA256 或 RSA。
- 头部是Base64Url编码的JSON字符串。
示例:
code{ "alg": "HS256", "typ": "JWT" }
-
Payload(负载):
- 负载包含有关声明(claims)的信息,可以包括用户的身份信息、权限等。
- 负载也是Base64Url编码的JSON字符串。
示例:
code{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
-
Signature(签名):
- 签名是通过将编码后的头部、编码后的负载和秘钥进行签名生成的。
- 签名用于验证消息的完整性和认证发送方。
示例:
codeHMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
这三部分通过点号连接在一起,形成完整的JWT,如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIkpvaG4gRG9lIiwgI
需要知道这串jwt并没有进行过加密,若需要保证数据安全性,可以用加密算法处理
总结一下这几天学习的心得:
其实ginweb并不难,难的是没有web基础就进入学习,刚学时,并不清楚日志,配置文件,数据验证等等web中要用到的各种库,其实把理论学好分开来看,这些初始化以及只需后期调用的代码,是可以一直重复利用的,只要是web项目,那么只用一套完整的初始化流程代码,就可以只用专注于写接口就行了,只需要会改配置,知道什么意思就行,而接口书写上边已经说过了,只有router层,controller层,logic层,然后是dao层,甚至很多接口还是高度重复的,。
所以才有基础web开发最简单这种说法,现在我也越发的认同了~