gin-web开发

1 gin介绍

Gin 是一个用 Golang编写的 高性能的web 框架, 由于http路由的优化,速度提高了近 40 倍。 Gin的特点就是封装优雅、API友好。

Gin的一些特性:

  • 快速 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。

  • 支持中间件 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。

  • Crash 处理 Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!

  • JSON 验证 Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。

  • 路由组 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。

  • 错误管理 Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。

  • 内置渲染 Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。

  • 可扩展性 新建一个中间件非常简单。

安装十分简单:

go get -u github.com/gin-gonic/gin

2 创建一个简单的服务

package main
// 导入gin包
import "github.com/gin-gonic/gin"

// 入口函数
func main() {
    // 初始化一个http服务对象
    // 默认使用Logger and Recovery中间件
	r := gin.Default()
        
    // 设置一个get请求的路由,url为/ping, 处理函数(或者叫控制器函数)是一个闭包函数。
	r.GET("/ping", func(c *gin.Context) {
    	// 通过请求上下文对象Context, 直接往客户端返回一个json
    	// type H map[string]any
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

    // 不写具体address默认在0.0.0.0:8080启动
	r.Run(":8080") // 监听并在 0.0.0.0:8080 上启动服务
}

3 设置图标

import "github.com/thinkerou/favicon"
/*
Use函数是 Gin 框架中用于注册中间件的方法。依次按照顺序执行
  
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {  
    engine.RouterGroup.Use(middleware...)  
    engine.rebuild404Handlers()  
    engine.rebuild405Handlers()  
    return engine  
}

favicon.New是一个用于处理favicon请求的中间件,返回指定的图标

func New(path string) gin.HandlerFunc

*/ 
r.Use(favicon.New("./favicon/18.ico"))

4 创建路由

/*
目录结构
gin/
    static/
        css/
        js/
    template/
*/

    // 创建服务
	r := gin.Default()
	// 设置图标
	r.Use(favicon.New("./favicon/18.ico"))

	// 加载静态页面
	r.LoadHTMLGlob("template/*")
	/* 
	加载资源文件
    func (group *RouterGroup) Static(relativePath, root string) IRoutes
    将relativePath路径映射到文件系统中的root中
	*/
	r.Static("/static", "./static")

	// 创建一个请求
	r.GET("/json", func(context *gin.Context) {
		// 返回一个JSON数据
		context.JSON(http.StatusOK, gin.H{"JSON": "YES"})
		// 返回字符串
		context.String(http.StatusOK, "pong")
	})

	r.GET("/html", func(context *gin.Context) {
    	/* 
    	HTML方法可以把参数传递给前端

        只需要在前端写下:
        	获取后端的数据为:  
            {{.msg}}
    	*/
		context.HTML(http.StatusOK, "index.html", gin.H{
			"msg": "前端传参测试",
		})
	})

    // 重定向,如果地址不加http/https的话会在路由后面追加路径
    r.GET("/redirect", func(context *gin.Context) {
		context.Redirect(302, "http://www.baidu.com/")
	})

    // 404
    // func (engine *Engine) NoRoute(handlers ...HandlerFunc)
    // `NoRoute` 方法用于定义处理未匹配路由的处理器。当客户端请求的路径没有匹配到任何已定义的路由时,Gin 会调用 `NoRoute` 方法中定义的处理器。默认情况下,Gin 会返回一个 404 状态码。
	r.NoRoute(func(context *gin.Context) {
		context.HTML(http.StatusNotFound, "404.html", "")
	})

5 参数渲染

在后端中可以给前端传递各种类型的参数

// string渲染
func RenderStr(context *gin.Context) {
	context.HTML(http.StatusOK, "user/user.html", "user string")
}

/*
stuct渲染:
type H map[string]any
使用gin.H{}是一样的使用方法
*/
func RenderStruct(context *gin.Context) {
	userinfo := UserInfo{UserID: 1, UserName: "ky"}
	context.HTML(http.StatusOK, "user/struct.html", userinfo)
}

// 数组渲染
func RenderArray(context *gin.Context) {
	userarr := []int{1, 2, 3}
	context.HTML(http.StatusOK, "user/array.html", userarr)
}


// 前端文件
<!-- 字符串渲染 -->  
<H1>{{.}}</H1>

<!-- 结构体渲染 --> 
<h1>UserID:{{.UserID}}</h1>  
<h1>UserName:{{.UserName}}</h1>

<!-- 数组渲染 --> 
{{/*第一种方式*/}}  
{{range $i,$v := .}}  
  {{$i}}  
  {{$v}}  
<br>  
{{end}}  
  
<br>  
{{/*第二种方式*/}}  
{{range .}}  
    {{.}}  
{{end}}

6 获取请求参数

    // usl?userid=ks&username=k
    /*
        context.DefaultQuery("id","123")  

        ?id=1,2,3
        context.QueryArray("id")

        ?name[1]=ha&name[2]=he
        context.QueryMap("name") map[1:ha 2:he]
    */
	r.GET("/user/info", func(context *gin.Context) {
		uid := context.Query("userid")
		username := context.Query("username")
		context.JSON(http.StatusOK, gin.H{
			"uid":      uid,
			"username": username,
		})
	})

	//  /user/info/ks/k
	// 占位符可以使用*,这样可以在不完全匹配情况下请求到该路由
	r.GET("/users/info/:userid/:username", func(context *gin.Context) {
		userid := context.Param("userid")
		username := context.Param("username")
		context.JSON(http.StatusOK, gin.H{
			"uid":      userid,
			"username": username,
		})
	})

	// 前端给后端传递json
	r.POST("/post", func(context *gin.Context) {
		// request.body
		// func (c *Context) GetRawData() ([]byte, error)
		b, _ := context.GetRawData()
		var m map[string]interface{}
		
		// func Unmarshal(data []byte, v interface{}) error
		// 将 JSON 数据解码(反序列化)为 Go 数据结构
		_ = json.Unmarshal(b, &m)
		context.JSON(http.StatusOK, m)
	})

	// 处理表单
	/*
	context.DefaultPostForm("age","18")  
    context.PostFormArray("name")  // 多选框返回列表
    context.PostFormMap("password") // input name属性写成map形式即可
	*/
	/*
    <form action="/user/add" method="post">  
        <p>username: <input type="text" name="username"></p>  
        <p>userword1: <input type="text" name="password[1]"></p>  
        <p>userword2: <input type="text" name="password[2]"></p>  
        <button type="submit">提交</button>  
    </form>
	*/
	r.POST("/user/add", func(context *gin.Context) {
		username := context.PostForm("username")
		context.JSON(http.StatusOK, gin.H{
			"username": username,
		})
	})

7 数据绑定

Gin提供了两类绑定方法:

  • Must bind

    • Bind、BindJSON、BindXML、BindQuery、BindYAML

    • 这些方法属于MustBindWith的具体调用。如果发生绑定错误,则请求终止,并触发 c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)

    • 响应码被设置为400并且 Content-Type被设置为 text/plain;charset-utf-8

    • 只能把响应码设置为400-422之间

  • Should bind

    • ShouldBind、ShouldBindJSON、ShouldBindXML、ShouldBindQuery、ShouldBindYAML

    • 这些方法都属于 ShouldBindWith调用。如果发生绑定错误,Gin会返回错误并由开发者处理错误和请求。

  • 使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。 如果你明确知道要绑定什么,可以使用 MustBindWithShouldBindWith

  • 你也可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:"required",但绑定时是空值, Gin 会报错。

<form action="/user/add" method="post">  
    <p>用户名: <input type="text" name="username"></p>  
    <p>密码: <input type="text" name="password"></p>  
    <p>地址: <input type="text" name="addr"></p>  
    <button type="submit">提交</button>  
</form>

// 参数绑定
type Users struct {  
    Name string `form:"name" uri:"name" json:"name"`  
    Pass string `form:"password" uri:"password" json:"password"`  
    Addr string `form:"addr" uri:"addr" json:"addr"`  
}  
  
func BindHtml(context *gin.Context) {  
    context.HTML(http.StatusOK, "user/bind_form.html", "")  
}  
  
func BindForm(context *gin.Context) {  
    var users Users  
    // 绑定表单
    _ = context.ShouldBind(&users)  
    context.JSON(http.StatusOK, users)  
}  
  
func BindQuery(context *gin.Context) {  
    var users Users  
    // 绑定查询参数  
    // http://127.0.0.1:8080/bindquery?name=123&age=18&addr=asdasd  
    context.ShouldBindQuery(&users)  
    context.JSON(http.StatusOK, users)  
}  
  
func BindUri(context *gin.Context) {  
    var users Users  
    // http://127.0.0.1:8080/binduri/123/123/123  
    context.ShouldBindUri(&users)  
    context.JSON(http.StatusOK, users)  
}

// 绑定 JSON
type Login struct {
	User     string `form:"user" json:"user" xml:"user"  binding:"required"`
	Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
	router := gin.Default()

	// 绑定 JSON ({"user": "manu", "password": "123"})
	router.POST("/loginJSON", func(c *gin.Context) {
		var json Login
		if err := c.ShouldBindJSON(&json); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		
		if json.User != "manu" || json.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		} 
		
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// 绑定 XML (
	//	<?xml version="1.0" encoding="UTF-8"?>
	//	<root>
	//		<user>manu</user>
	//		<password>123</password>
	//	</root>)
	router.POST("/loginXML", func(c *gin.Context) {
		var xml Login
		if err := c.ShouldBindXML(&xml); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		
		if xml.User != "manu" || xml.Password != "123" {
			c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
			return
		} 
		
		c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
	})

	// 监听并在 0.0.0.0:8080 上启动服务
	router.Run(":8080")
}

/*
$ curl -v -X POST \
  http://localhost:8080/loginJSON \
  -H 'content-type: application/json' \
  -d '{ "user": "manu" }'
*/

7.1 数据验证

Gin使用 go-playground/validator/v10 进行验证。 查看标签用法的全部文档. 不满足校验会报错:Error:Field validation for 'Desc' failed on the 'required' tag

如果有多个验证字段,按顺序进行验证,如果前面验证不通过不会对后面的进行验证:

    • :忽略字段

  • required:不为空校验,binding:"required"

  • regexp:正则表达式校验

  • min:最小长度,binding:"min=1"

  • max:最大长度

  • |:或

  • structonly:如果有嵌套可以决定只验证结构体上的

  • Exists

  • omitempty:省略空,如果为空,则不会继续校验该字段其他的规则,只有不为空才会继续验证其他的

  • dive:嵌套验证

Name [][]int `binding:"min=10,dive,max=20,deive required"`
// min=10针对第一个[]
// max=20针对第二个[]
// required针对Name
  • len:长度

  • eq/ne/gte/ge/lt/lte:gt、gte、lt、lte等都可以用于时间的比较,后面可以不跟值,表示大于当前utc时间

  • eqfield:等于其他字段的值

  • nefield:不等于其他字段的值

  • eqcsfield:类似eqfield,它会验证相对于顶层结构提供的字段

eqcsfield = InnerStructField.Fiedl
  • nqcsfield

  • gtfield:大于其他字段的值

  • gtefield

  • gtcsfield

  • gtecsfield

  • alpha:字符串仅包含字母字符

  • alphanum:仅包含字母数字字符

  • numeric:字符串包含基本数字值。不包含指数等

  • hexadecimal:字符串包含有效的十六进程

  • hexcolor:字符串值包含有效的十六进程颜色,包括#

  • rgb:包含有效的rgb颜色

  • rgba

  • email:包含有效的电子邮件

  • url:包含有效的网址,必须包含http://等

  • uri:包含有效的uri。将接受golang请求uri接受的任何uri

  • contains:包含子字符串

  • excludes:排除

  • uuid

  • ip

7.2 自定义验证器

  1. 安装包

go   get github.com/go-playground/validator/v10
  1. 定义验证器

// 必须是validator.Func 类型  
var Len6Valid validator.Func = func(fl validator.FieldLevel) bool {  
    data := fl.Field().Interface().(string)  
    if len(data) > 6 {  
       fmt.Println("false")  
       return false  
    } else {  
       fmt.Println("true")  
       return true  
    }  
}
  1. 注册验证器

// 在路由匹配前,main中即可  
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {  
    v.RegisterValidation("len_valid", user.Len6Valid)  
}
  1. 使用验证器

type Article struct {  
    Id      int    `form:"Id" binding:"required"`  
    Title   string `form:"title" binding:"required,len_valid"`  
    Content string `form:"content" binding:"required"`  
    Desc    string `form:"desc" binding:"required"`  
}

7.3 beego验证器

  1. 下载包

go get github.com/astaxie/beego/validation
  1. 常用验证器

  2. 使用

/*
- 验证函数写在"valid"tag标签里
- 各个验证规则使用;分割
- 参数使用括号包裹,多个参数使用,分割
- 正则函数匹配模式用//括起来
- 各个函数的key值为字段名.验证函数名
*/
type Article struct {  
    Id      int    `valid:"Required"`  
}
// 初始化验证器
var article Article
valid := validation.Validation{}
b,err := valid.Valid(&article) // ->bool,err
if !b{
    for _,err1 := range valid.Errors{
        fmt.Println(err1.Key)
        fmt.Println(err1.Message)
    }
}
  1. 自定义错误信息

    1. #TODO:如何使用beego自定义验证器

var MessageTmpls = map[string]string{  
    "Required":     "Can not be empty",  
    "Min":          "Minimum is %d",  
    "Max":          "Maximum is %d",  
    "Range":        "Range is %d to %d",
    }
validation.SetDefaultMessage(MessageTmpls)

8 文件上传

  
// 方式一:表单单文件  
/*  
curl -X POST http://localhost:8080/upload \  
  -F "file=@/Users/appleboy/test.zip" \  -H "Content-Type: multipart/form-data"*/  
func FormSingleFile(context *gin.Context) {  
    file, _ := context.FormFile("file")  
    log.Println(file.Filename)  
  
    // 这个路径是从项目根目录开始  
    // 会自动创建目录  
    dst := "./files/" + file.Filename  
    // 上传文件至指定的完整文件路径  
    // 会覆盖同名文件  
    context.SaveUploadedFile(file, dst)  
  
    context.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))  
}  
  
// 方式二:表单多文件  
/*  
curl -X POST http://localhost:8080/upload \  
  -F "upload[]=@/Users/appleboy/test1.zip" \  -F "upload[]=@/Users/appleboy/test2.zip" \  -H "Content-Type: multipart/form-data"*/  
func FormManyFile(context *gin.Context) {  
    file, _ := context.MultipartForm()  
    files := file.File["file"]  
  
    dst := "./files/"  
    for _, f := range files {  
       log.Println(f.Filename)  
       context.SaveUploadedFile(f, dst+f.Filename)  
    }  
    context.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))  
}

9 响应数据类型

func OutJson(context *gin.Context) {  
    context.JSON(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
// 生成具有转义的非ASCLL字符的ASCLL-only JSON  
func OutAscJson(context *gin.Context) {  
    context.AsciiJSON(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
// 使用JSONP向不同域的服务器请求数据。如果查询参数存在回调,则将回调添加到响应体中  
func OutJsonp(context *gin.Context) {  
    context.JSONP(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
/*  
c.JSON():默认会转义 HTML 敏感字符,以确保安全性。  
c.PureJSON():直接输出原始字符,保持 JSON 数据的原貌。  
*/  
func OutPureJson(context *gin.Context) {  
    context.PureJSON(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
// 使用SecureJSON防止json劫持。如果给定的结构是数组值,则默认预置“while(1)”到响应体  
// json劫持:利用网站的cookie未过期,如何访问攻击者的虚假页面,该页面就可以拿到json形式的用户敏感信息  
func OutSecureJson(context *gin.Context) {  
    context.SecureJSON(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
func OutXml(context *gin.Context) {  
    context.XML(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}  
  
func OutYaml(context *gin.Context) {  
    context.YAML(http.StatusOK, gin.H{  
       "code": 200,  
       "tag":  "<br>",  
       "msg":  "提交成功",  
       "html": "<b>Hello,world!</b>",  
    })  
}

10 路由组

    // 路由组
    //func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
    // 第一层路由/user,可以在路由组中指定中间件,这样路由组中的所有路由都会使用这个中间件
	userGroup := r.Group("/user")
	{
    	// 第二层路由/user/add
		userGroup.GET("/add")
	}
	// 子路由组/user/yes
	yes := userGroup.Group("/yes")
	yes.GET("/1", myHandler(), func(context *gin.Context) {
		handler := context.MustGet("Handler").(string)
		context.JSON(http.StatusOK, gin.H{
			"子路由组":    "/user/yes/1",
			"handler": handler,
		})
	})

11 中间件

  • 什么是中间件

    • 开发者自定义的钩子函数

  • 中间件的作用

    • 中间件适合处理一些公共的业务逻辑,比如登陆认证、权限校验、数据分页、记录日志、耗时统计等

    • 需要对某一类的函数进行通用的前置或后置处理

  • 中间件的回调要先于用户定义的路由处理函数

  • Use是追加中间件,会按照顺序调用中间件

  • 常用中间件

11.1 自定义中间件

  
// 自定义中间件第一种方式
func MyHandler() gin.HandlerFunc {  
    return func(context *gin.Context) {  
       // 后续所有处理都能拿到这里的参数  
       // 设置上下文中的键值对
       context.Set("Handler", "myHandler")  

       //  MustGet用于从上下文中获取存储的键值对
       // func (c *Context) MustGet(key string) interface{}
       if context.MustGet("Handler").(string) == "myHandlers" {  
          context.Next() // 放行  
       } else {  
          context.Abort() // 阻止  
       }  
    }  
}

// 自定义中间件第二种方式
func MiddleWarel(context *gin.Context) {  
    fmt.Println("这是中间件定义的第二种方式")  
}

func main(){
    h := gin.New()

    // 注册自定义中间件
    // 如果在路由组之后再使用中间件,会对前面的路由组无效
	h.Use(MyHandler())

	h.GET("/test", func(context *gin.Context) {
		context.String(200, "yes")
	})

	// 运行
	h.Run()
}

11.2 Next和Abort

  • Next

    • 在定义的众多中间件会形成一条中间件链,而通过Next函数来对后面的中间件进行执行

    • 当遇到c.Netx()函数时,它取出所有的没有被执行过的注册函数都执行一边,然后再回到本函数中

    • Next函数是在请求前执行,而Next函数后是在请求后执行

      • c.Next() 之前的代码会在请求处理前(Pre-Request)​ 执行。

      • ​**c.Next() 之后的代码会在请求处理后(Post-Request)​** 执行(即响应已生成,但尚未发送给客户端)。

    • 可以用在token校验,把用户id存起来给功能性函数使用

  • Abort

    • 终止调用整个链条,但只是不会调用此函数所有中间件的后面中间件,本中间件和之前的中间件会调用完毕

    • 比如token认证没有通过,不能直接使用return返回,而是使用Abort来终止

func MiddleWare1(context *gin.Context) {  
    fmt.Println("中间件一开始")  
    context.Next()  
    fmt.Println("中间件一结束")  
}  
  
func MiddleWare2() gin.HandlerFunc {  
    return func(context *gin.Context) {  
       fmt.Println("中间件二开始")  
       context.Next()  
       fmt.Println("中间件二结束")  
    }  
}

/*
中间件一开始
中间件二开始
中间件二结束
中间件一结束
*/

11.3 利用Next计算请求时间

func MiddleWare1(context *gin.Context) {  
    start_time := time.Now()  
  
    fmt.Println("中间件一开始")  
    context.Next()  
  
    fmt.Println(time.Since(start_time))  
    fmt.Println("中间件一结束")  
}  
  
func MiddleWare2() gin.HandlerFunc {  
    return func(context *gin.Context) {  
       fmt.Println("中间件二开始")  
       time.Sleep(time.Second * 3)  
       fmt.Println("中间件二结束")  
    }  
}  
func MiddleWare3(context *gin.Context) {  
    fmt.Println("中间件三开始")  
    context.Next()  
    time.Sleep(time.Second * 3)  
    fmt.Println("中间件三结束")  
}
// 请注意设置的HTTP超时时间

11.4 中间件作用域

  • 全局中间件:直接在创建出来的路由引擎中使用

router := gin.Default()  
  
router.Use(middle.MiddleWare1, middle.MiddleWare2(), middle.MiddleWare3)
  • 路由组中间件:在创建出来的路由组直接使用,该路由组下的所有路由都将使用该中间件

userGroup := r.Group("/user")
userGroup.Use(middle.MiddleWare1, middle.MiddleWare2(), middle.MiddleWare3)
  • 局部中间件:直接在路由上使用

yes.GET("/1", myHandler(), func(context *gin.Context) {
		handler := context.MustGet("Handler").(string)
		context.JSON(http.StatusOK, gin.H{
			"子路由组":    "/user/yes/1",
			"handler": handler,
		})
	})

11.5 gin.BasicAuth()中间件

router.Use(gin.BasicAuth(gin.Accounts{  
    "zs": "123",  
}))

11.6 WrapH和WrapF

func WrapFTest(w http.ResponseWriter, r *http.Request) {  
  
}
router.GET("/middle", gin.WrapF(middle.WrapFTest), middle.MiddleAuth)

/*
WrapH和WrapF区别:
- 需要自己去定义struct实现这个Handler接口
*/
type Test struct {  
}  
  
func (test *Test) TestH(w http.ResponseWriter,r *http.Request){  
      
} 

12 模板语法

统一使用{{和}}作为左右标签,在该左右标签中的内容会被解析为Go代码逻辑(如变量、条件、循环等),在左右标签之外的内容原样输出。

// 后端给前端传递的数据
func UserTemplate(contest *gin.Context) {  
  
    arr := []int{1, 2, 3, 4}  
    maps := map[string]interface{}{  
       "one":   "one",  
       "tow":   "2",  
       "three": 3,  
    }  
    names := Person{  
       Name: "k",  
       Age:  18,  
       Addr: "beijin",  
    }  
  
    contest.HTML(http.StatusOK, "user/user_template.html", gin.H{  
       "string": "string message",  
       "arr":    arr,  
       "map":    maps,  
       "struct": names,  
       "funcname": SubStr,
    })  
}

func SubStr(str string, l int) string {  
    ret := str[0:l]  
    return (ret + "...")  
}

12.1 上下文

  • .:访问当前位置的上下文

  • $:引用当前模板根级的上下文

  • $.:引用模板中的根级上下文

12.2 去除空格

  • {{- :去除前空格

  • -}}:去除后空格

12.3 支持go语言的符号

  1. 字符串:{{"zhaoli"}}

  2. 原始字符串:{{`yes`}}

  3. 字节类型:{{'a'}} ->97字符编码号

  4. nil类型:{{print nil}}

    • {{nil}}只有nil会报错:nil is not a command

12.4 定义变量

  1. 定义:

{{$username := "xxxx"}}
  1. 使用

{{$username}}

注意:只能在当前模板使用

12.5 pipline

可以是上下文的变量输出,也可以是函数通过管道传递的返回值

  • {{.Name}}是上下文的变量输出,是个pipline

  • {{"h"|len}}是函数通过管道传递的返回值,是个pipline

12.6 流程控制-if

  1. if...else...

{{ if .string1 }}  
    {{.string}}  
{{ else }}  
    {{ print "else" }}  
{{end}}
  1. if嵌套

{{ if .string1 }}  
    {{.string}}  
{{ else if .string1 }}  
    {{ print "no" }}  
{{ else }}  
    {{ print "else" }}  
{{end}}

12.7 循环range

{{ range.map }}  
    {{.}} 
    <br>  
{{end}}

{{ range $v := .map }}   
    {{$v}}  
    <br>  
{{end}}

{{ range $i,$v := .map }}  
    {{$i}}  
    {{$v}}  
    <br>  
{{end}}

// 支持else,长度为0的时候执行else
{{ range $i,$v := .map1 }}  
    {{$i}}  
    {{$v}}  
    <br>  
{{else}}  
    {{ 0 }}  
{{end}}

12.8 with伴随

  • 作用:用于重定向pipline

// 不用每个字段前都加struct了
{{with .struct}}  
    {{.Name}}  
    {{.Age}}  
    {{.Addr}}  
{{else}} // 逻辑跟range一样
    {{0}}
{{end}}

12.9 template

  • 作用:导入其他的模板

{{/*调用子结构传递参数*/}}
{{template "user/base.html" .}}
  • 导入的模板文件也要使用define包含

  • 如果想在引用的模板中获取动态数据,必须使用.访问当前位置的上下文

  • 引入的作用位置跟 template使用的位置有关

12.10 模板函数

  • print

    • print:fmt.Sprint

    • printf:fmt.Sprintf

    • println:fmt.Sprintln

格  式
描  述

%v

按值的本来值输出

%+v

在 %v 基础上,对结构体字段名和值进行展开

%#v

输出 Go 语言语法格式的值

%T

输出 Go 语言语法格式的类型和值

%%

输出 % 本体

%b

整型以二进制方式显示

%o

整型以八进制方式显示

%d

整型以十进制方式显示

%x

整型以十六进制方式显示

%X

整型以十六进制、字母大写方式显示

%U

Unicode 字符

%f

浮点数

%p

指针,十六进制方式显示

{{print "hello" "world"}}  
{{printf "name:%s name:%s" "k" "s"}}
  • 括号:设置优先级

{{printf "name:%s name:%s" "k" (printf "%s-%s" "1" "2")}}
  • and:只要有一个为空,则整体为空,如果都不为空,则返回最后一个

{{and .arr $.name}}
  • or:只要有有一个不为空,则返回第一个不为空的,否则返回空

  • call:是一个动态调用其他函数或方法的工具,主要用于在模板中灵活执行传入的函数或方法。

{{$funcname := .funcname }}  
{{ call $funcname "yes" 2}}
  • index:读取指定类型对于下标的值

    • 支持map/array/slice/string

{{index .arr 1}}
  • len:返回指定类型的长度

{{len .arr}}
  • not:返回输入参数的否定值,布尔类型

  • urlquery: 有些符号在URL中是不能直接传递的,如果要在URL中传递这些特殊符号,那么就要使用该符号的编码

{{urlquery "http://www.baidu.com"}}
{{/*
http%3A%2F%2Fwww.baidu.com
%后面的就是自负的16进制字符码
*/}}
 
  • 判断符,返回布尔值

    • eq:等于,可以用于字符串判断

      • 支持多个参数,只要有一个与第一个值相等就返回true

    • ne:不等于

    • lt:大于

    • le:大于等于

    • gt:小于

    • ge:小于等于

  • Format:实现时间的格式化,返回字符串。也可以在后端转换在前端使用。

    • {{.time_data.Format "2006/01/02 15:04:05"}}

  • html:转义文本中的html标签

  • js:返回用JavaScript的escape处理后的文本

    • escape函数可以对字符串进行编码,这样就可以在所有的计算机上读取该字符串,可以使用unescape解码

  • slice:对string/slice/array进行切片

{{ slice "hello" 1 3 }} → "el"
{{ slice .Items 0 2 }}  → 获取列表前两项

12.10.1 自定义模板函数

  1. 定义函数

func SubStr(str string, l int) string {  
    ret := str[0:l]  
    return (ret + "...")  
}
  1. 注册函数:注册函数要在控制器加载静态文件之前

router.SetFuncMap(template.FuncMap{  
    "SubStr": user.SubStr, // 字符串为前端使用的名称  
})
  1. 使用函数

{{SubStr "yes" 2}}

13 日志

13.1 基于gin的日志中间件

  • 使用日志文件

// 创建日志文件  
f, _ := os.Create("gin.log")  
  
// 重新赋值DefaultWriter  
gin.DefaultWriter = io.MultiWriter(f)  
  
// 同时在控制台打印信息  
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

13.2 logrus

#TODO:logrus

  • 下载包

go get github.com/sirupsen/logrus
  • 定义logrus配置文件

{  
  "log_dir": "logs/gin_project.log",  
  "log_level": "info"  
}
  • 设置logrus

type Conf struct {  
    LogDir   string `json:"log_dir"`  
    LogLevel string `json:"log_level"`  
}  
  
func Load_conf() *Conf {  
    var config = new(Conf)  
    f, _ := os.Open("conf/log_conf.json")  
    defer f.Close()  
    conf, _ := io.ReadAll(f)  
    json.Unmarshal(conf, config)  
    fmt.Println(config)  
    return config  
}

// 创建一个日志实例  
var Log = logrus.New()  
  
func init() {  
    logConf := Load_conf()  
  
    // 设置日志输出文件  
    f, err := os.OpenFile(logConf.LogDir, os.O_APPEND|os.O_WRONLY, os.ModeAppend)  
    if err != nil {  
       panic(err)  
    }  
  
    Log.Out = f  
  
    // 设置日志级别  
    level_map := map[string]logrus.Level{  
       "error": logrus.ErrorLevel,  
       "info":  logrus.InfoLevel,  
       "debug": logrus.DebugLevel,  
    }  
  
    Log.SetLevel(level_map[logConf.LogLevel])  
  
    // 日志格式化  
    // JSONFormatter  
    Log.SetFormatter(&logrus.TextFormatter{})  
}
  • logrus使用

r.GET("/json", func(context *gin.Context) {  
    // 返回一个JSON数据  
    f := logrus.Fields{  
       "name": "k",  
       "age":  12,  
    }  
    // WithFields在日志输出中增加字段
    logs_source.Log.WithFields(f).Info("这是info级别")  
    context.JSON(http.StatusOK, gin.H{"JSON": "YES"})  
})

/*
time="2025-03-20T19:57:07+08:00" level=info msg="这是info级别"  
time="2025-03-20T20:07:03+08:00" level=info msg="这是info级别" age=12 name=k
*/
  • 什么是session

    • Session是在无状态的http协议下,服务端记录用户状态时用于标识具体用户的机制

    • 它是在服务端保存的用来跟踪用户状态的数据结构,可以保存在文件、数据库或者集群中

    • 在浏览器关闭后这次的Session就消失了,下次打开就不再拥有这个Session。其实并不是Session消失了,而是Session ID变了。

  • 什么是Cookie

    • Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息

    • 每次HTTP请求时,客户端都会发送相应的Cookie信息到服务端。它的过期时间可以任意设置,如果不主动清除它,很长一段时间都可以保留

  • session和cookie

    • Cookie在客户端,Session在服务器端

    • Cookie安全性一般,他人可以通过分析存放在本地的Cookie并进行Cookie欺骗。在安全性第一的前提下,选择Session更优。重要的交互信息比如权限等放在Session中,一般的信息记录放在Cookie

    • 单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie

    • Session可以放在文件、数据库或内存中

    • 用户验证这种场合一般会用到Session。因此维持一个会话的核心就是客户端的唯一标识,即Session ID

    • Session的运行依赖Session ID,而Session ID是存在Cookie中的,也就是说,如果浏览器禁用了Cookie,Session也会失效(但是可以通过其他方式实现,比如在url中传递SessionID)

#TODO :怎么发送session并认证

  • 下载包

go get github.com/gin-contrib/sessions
  • 基于Cookie存储引擎使用

// 加密的盐  
store := cookie.NewStore([]byte("kelly"))  
  
// 使用session中间件  
r.Use(sessions.Sessions("gin_session", store))

r.GET("/json", func(context *gin.Context) {  
  
    // 初始化session对象  
    session := sessions.Default(context)  
  
    // 设置session  
    session.Set("name", "kelly")  
  
    // 获取session  
    name := session.Get("name")  
    fmt.Println("==========_____++++++++")  
    fmt.Println(name) // kelly  
  
    // 删除指定session  
    session.Delete("name")  
  
    // 清除所有的session  
    session.Clear()  
  
    // 保存session  
    session.Save()  
  
    // 返回一个JSON数据  
    context.JSON(http.StatusOK, gin.H{"JSON": "YES"})  
})
  • 基于redis存储引擎

// 下载
go get github.com/gin-contrib/sessions/redis

//参数从左到右依次为:redis最大空闲连接数,通信协议,redis地址,redis密码,加密密钥  
store, err := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))  
fmt.Println(err)  
r.Use(sessions.Sessions("session_test", store))

最后更新于