Go Web 编程之 Gin 框架


Gin 介绍与安装


简介

Gin 是一个基于 httprouter 开发的 Golang Web 框架,封装比较优雅,API 友好,源码注释比较明确,中文文档 齐全,具有快速灵活,容错方便等特点。


安装

下载并安装 Gin ,输入如下命令:

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

将其导入项目中(若要使用常量 http.StatusOK ,需导入 net/http 包), 使用如下的形式:

import "github.com/gin-gonic/gin"

第一个 Gin 示例:

package main

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

func main() {
		// 1.创建路由
		r := gin.Default()
		
		// 2.绑定路由规则(GET:请求方式;/:请求的路径),当客户端以GET 方法请求 / 路径时,会执行后面的匿名函数
		// gin.Context,封装了 request 和 response
		r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello CQUPT!")
		})

		// 3.监听端口,默认在 8080
		// Run("里面不指定端口号默认为 8080")
		r.Run(":8000")
}

执行程序访问指定的网址 http://127.0.0.1:8000/ ,浏览该页面会输出一串 string 字符串,具体内容如下:

Hello CQUPT!

该程序调用 String() 方法以字符串格式返回 HTTP 请求响应,其方法的声明如下:

func (c *Context) String(code int, format string, values ...any)

RESTful API

Gin 框架中采用的路由库是基于 httprouter ,支持开发 RESTful API 的开发。

REST 与技术无关,代表的是一种软件架构风格,REST 是Representational State Transfer 的简称,中文翻译为 “表征状态转移” 或“表现层状态转化” 。

REST 的含义就是客户端与 Web 服务器之间进行交互的时候,使用HTTP 协议中的 4 个请求方法代表不同的动作。

请求方法含义
GET获取资源
POST新建资源
PUT更新资源
DELETE删除资源

只要 API 程序遵循了 REST 风格,便可以称其为 RESTful API ,目前在前后端分离的架构中,前后端基本都是通过 RESTful API 来进行交互。

例如如下的程序:

请求方法URL含义
GET/get查询 xxx 信息
POST/put创建 xxx 记录
PUT/post更新 xxx 信息
DELETE/delete删除 xxx 信息
package main

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

func main() {
		r := gin.Default()
		
		r.GET("/get", func(c *gin.Context) {
		c.String(http.StatusOK, "查询 xxx 信息")
		})
		
		r.POST("/post",func(c *gin.Context) {
		c.String(http.StatusOK, "创建 xxx 记录")
		})
		
		r.PUT("/put",func(c *gin.Context) {
		c.String(http.StatusOK, "更新 xxx 信息")
		})

		r.DELETE("/delete",func(c *gin.Context) {
		c.String(http.StatusOK, "删除 xxx 信息")
		})

		//监听端口默认为8080
		r.Run(":8000")
}

开发 RESTful API 时通常使用 Postman 来作为客户端的测试工具。


文件的加载处理


模板文件的加载

在 Gin 框架中调用 LoadHTMLGlob() 方法或 LoadHTMLFiles() 方法可以加载模板文件,其方法的声明如下:

func (engine *Engine) LoadHTMLGlob(pattern string)
func (engine *Engine) LoadHTMLFiles(files ...string)

例如创建一个 template 目录,在该目录下创建一个名为 index.htmlHTML 模板文件,该文件的具体内容如下:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>{{.title}}</title>
  </head>
	<body>
		姓名:{{.name}}
	</body>
</html>

template 同目录下编写一个 Server HTTP 程序来加载解析渲染该模板文件,该程序的具体代码如下:

package main

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

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

		r.LoadHTMLGlob("template/*")
		r.GET("/index", func(c *gin.Context) {
				c.HTML(http.StatusOK, "index.html", gin.H{"title":"Go Web","name":"cqupthao"})
		})

		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/index ,浏览到页面的内容如下:

姓名:cqupthao

文件路径的获取

关于模板文件和静态文件的路径,可以调用用下面的函数获取当前执行程序的路径,程序部分代码如下:

func getCurrentPath() string {
		if ex, err := os.Executable(); err == nil {
				return filepath.Dir(ex)
		}
	return "./"
}

静态文件的加载

当项目文件中引用了 JSCSSJPG 等静态文件时,需要在渲染页面前调用 gin.Static() 方法或 gin.StaticFile() 方法,其方法的声明如下:

func (group *RouterGroup) Static(relativePath, root string) IRoutes
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

具体的使用方法参考如下程序代码部分:

func main() {
		r := gin.Default()
		r.Static("/static", "./static")
		// r.StaticFile("/static","./static")
		r.LoadHTMLGlob("templates/**/*")
   		// ...
		r.Run(":8000")
}

Gin 渲染


HTML 渲染

Gin 支持加载 HTML 模板文件, 然后调用 HTML() 方法根据模板参数进行配置并返回相应的数据(本质上是字符串替换),其方法的声明如下:

func (c *Context) HTML(code int, name string, obj any)

例如在 template 目录下创建 public 文件夹和 user 文件夹,在 user 目录下创建一个名为 index.htmlHTML 模板文件,该文件的具体内容如下:

{{ define "user/index.html" }}
{{template "public/header" .}}
学校:{{.school}}
{{template "public/footer" .}}
{{ end }}

public 目录下创建一个名为 header.htmlfooter.htmlHTML 模板文件,其文件的具体内容如下:

{{define "public/header"}}
<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>{{.title}}</title>
  </head>
 	<body>
{{end}} 
{{define "public/footer"}}
</body>
</html>
{{end}}

template 同目录下编写一个 Server HTTP 程序来加载解析渲染模板文件,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
		r.LoadHTMLGlob("template/**/*")
		r.GET("/index", func(c *gin.Context) {
				c.HTML(http.StatusOK, "user/index.html", gin.H{"title": "Go Web",
"school": "CQUPT"})
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/index ,浏览到页面的内容如下:

学校:CQUPT

又例如在 template 目录下创建 post 文件夹和 user 文件夹,在 posts 目录下创建一个名为 index.htmlHTML 模板文件,该文件的具体内容如下:

{{define "posts/index.html"}}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>posts/index</title>
  </head>
	<body>
    	成功加载 {{.title}}.html 模板文件!
	</body>
</html>
{{end}}

users 目录下创建一个名为 index.htmlHTML 模板文件,该文件的具体内容如下:

{{define "users/index.html"}}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Go Web</title>
  </head>
	<body>
   	 	成功加载 {{.title}}.html 模板文件!
	</body>
</html>
{{end}}

template 同目录下编写一个 Server HTTP 程序来加载解析渲染模板文件,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
	
		// 加载 template 文件夹下面的所有模板文件
		r.LoadHTMLGlob("template/**/*")
	
		//等价于 r.LoadHTMLFiles("template/posts/index.html", "templates/users/index.html")
		r.GET("/posts/index", func(c *gin.Context) {
				c.HTML(http.StatusOK, "posts/index.html", gin.H{
						"title": "posts/index",
				})
		})

		r.GET("users/index", func(c *gin.Context) {
				c.HTML(http.StatusOK, "users/index.html", gin.H{
					"title": "users/index",
				})
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/users/index ,浏览到页面的内容如下:

成功加载 users/index.html 模板文件!

执行程序访问指定网址 http:127.0.0.1:8000/posts/index ,浏览到页面的内容如下:

成功加载 posts/index.html 模板文件!

自定义模板函数

注册自定义的模板函数使用 SetFuncMap() 方法,其方法的声明如下:

func (engine *Engine) SetFuncMap(funcMap template.FuncMap)

例如定义一个不转义相应内容的 safe 模板函数,程序关键代码部分如下:

package main

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

func main() {
		r := gin.Default()
		r.SetFuncMap(template.FuncMap{
				"safe": func(str string) template.HTML{
						return template.HTML(str)
				},
			})
		r.LoadHTMLFiles("./index.tmpl")

		r.GET("/index", func(c *gin.Context) {
				c.HTML(http.StatusOK, "index.tmpl", "<a href='https://cqupthao.gitee.io'>博客</a>")
		})

		r.Run(":8000")
}

创建一个 index.tmpl 模板文件,在文件中使用定义好的 safe 模板函数,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>修改模板引擎的标识符</title>
  </head>
	<body>
		<div>{{ . | safe }}</div>
	</body>
</html>

模板的继承

在 Gin 框架中默认使用单模板,若需使用 block template 功能,可以通过 "github.com/gin-contrib/multitemplate" 库来实现。

例如项目目录下的 template 文件夹下有以下模板文件,其中includes 目录下的 home.tmplindex.tmpl 模板文件继承了 layouts 目录下的 base.tmpl 模板文件,然后定义一个 loadTemplates 函数加载模板文件,该函数具体代码如下:

func loadTemplates(templatesDir string) multitemplate.Renderer {
		r := multitemplate.NewRenderer()
		layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
		if err != nil {
				panic(err.Error())
		}
		includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
		if err != nil {
				panic(err.Error())
		}
		// 为layouts/和includes/目录生成 templates map
		for _, include := range includes {
				layoutCopy := make([]string, len(layouts))
				copy(layoutCopy, layouts)
				files := append(layoutCopy, include)
				r.AddFromFiles(filepath.Base(include), files...)
		}
		return r
}

main() 函数中调用该函数,关键代码部分如下:

func indexFunc(c *gin.Context){
		c.HTML(http.StatusOK, "index.tmpl", nil)
}

func homeFunc(c *gin.Context){
		c.HTML(http.StatusOK, "home.tmpl", nil)
}

func main(){
		r := gin.Default()
		r.HTMLRender = loadTemplates("./template")
		r.GET("/index", indexFunc)
		r.GET("/home", homeFunc)
		r.Run()
}

JSON 渲染

在 Gin 框架中通过调用 JSON() 方法来实现以 JSON 格式生成 HTTP 请求响应,其方法的声明如下:

func (c *Context) JSON(code int, obj any)

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

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

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

		// gin.H 是map[string]interface{}的缩写
		r.GET("/someJSON", func(c *gin.Context) {
				// 方式一:自己拼接JSON
				c.JSON(http.StatusOK, gin.H{"name":"cqupthao","message": "CQUPT"})
		})
		r.GET("/moreJSON", func(c *gin.Context) {
				// 方法二:使用结构体
				var msg struct {
						Name    string `json:"user"`
						Message string						
				}
				msg.Name = "cqupthao"
				msg.Message = "CQUPT"			
				c.JSON(http.StatusOK, msg)
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/someJSON ,浏览到页面的内容如下:

{"message":"CQUPT","name":"cqupthao"}

执行程序访问指定网址 http:127.0.0.1:8000/moreJSON ,浏览到页面的内容如下:

{"user":"cqupthao","Message":"CQUPT"}

XML 渲染

在 Gin 框架中通过调用 XML() 方法来实现以 XML 格式生成 HTTP 请求响应(需要使用具名的结构体类型),其方法的声明如下:

func (c *Context) XML(code int, obj any)

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
		// gin.H 是 map[string]interface{} 的缩写
		r.GET("/someXML", func(c *gin.Context) {
				// 方式一:自己拼接 JSON
				c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
		})
		r.GET("/moreXML", func(c *gin.Context) {
				// 方法二:使用结构体
				type MessageRecord struct {
						Name    string
						Message string
				}
				var msg MessageRecord
				msg.Name = "cqupthao"
				msg.Message = "CQUPT"
				c.XML(http.StatusOK, msg)
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/someXML ,浏览到页面的内容如下:

<map>
	<message>Hello world!</message>
</map>

执行程序访问指定网址 http:127.0.0.1:8000/moreXML ,浏览到页面的内容如下:

<MessageRecord>
	<Name>cqupthao</Name>
	<Message>CQUPT</Message>
</MessageRecord>

YMAL 渲染

在 Gin 框架中通过调用 YMAL() 方法来实现以 YMAL 格式生成 HTTP 请求响应,其方法的声明如下:

func (c *Context) YAML(code int, obj any)

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
		r.GET("/someYAML", func(c *gin.Context) {
				c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/someYAML 会生成名为 someYAML 的文件,该文件的具体内容如下:

message: ok
status: 200

ProtoBuf 渲染

在 Gin 框架中通过调用 ProtoBuf() 方法来实现以 ProtoBuf 格式生成 HTTP 请求响应,其方法的声明如下:

func (c *Context) ProtoBuf(code int, obj any)

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

import (
		"github.com/gin-gonic/gin"
		"github.com/gin-gonic/gin/testdata/protoexample"
		"net/http"
)

func main() {
		r := gin.Default()
		r.GET("/someProtoBuf", func(c *gin.Context) {
				reps := []int64{int64(1), int64(2)}
				label := "test"
				// protobuf 的具体定义写在 testdata/protoexample 文件中
				data := &protoexample.Test{
						Label: &label,
						Reps:  reps,
				}
				// 数据在响应中变为二进制数据
				// 将输出被 protoexample.Test protobuf 序列化了的数据
				c.ProtoBuf(http.StatusOK, data)
		})
		r.Run(":8000")
}

执行程序访问指定网址 http:127.0.0.1:8000/someProtoBuf 会生成名为 someProtoBuf 的文件。


综合示例

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

import (
		"github.com/gin-gonic/gin"
		"github.com/gin-gonic/gin/testdata/protoexample"
		"net/http"
)

// 多种响应方式
func main() {
		// 1.创建路由
		// 默认使用了2个中间件Logger(), Recovery()
		r := gin.Default()
		
		// 1.json
		r.GET("/someJSON", func(c *gin.Context) {
				c.JSON(200, gin.H{"message": "someJSON", "status": 200})
		})

		// 2. 结构体响应
		r.GET("/someStruct", func(c *gin.Context) {
				var msg struct {
						Name		string
						Message		string
				}

				msg.Name = "cqupthao"
				msg.Message = "CQUPT"

				c.JSON(200, msg)
		})

		// 3.XML
		r.GET("/someXML", func(c *gin.Context) {
				c.XML(200, gin.H{"message": "CQUPT"})
		})

		// 4.YAML响应
		r.GET("/someYAML", func(c *gin.Context) {
				c.YAML(200, gin.H{"name": "cqupthao"})
		})

		// 5.protobuf格式,谷歌开发的高效存储读取的工具
		// 数组?切片?如果自己构建一个传输格式,应该是什么格式?
		r.GET("/someProtoBuf", func(c *gin.Context) {
				reps := []int64{int64(1), int64(2)}
				// 定义数据
				label := "label"
				// 传protobuf格式数据
				data := &protoexample.Test{
						Label:&label,
						Reps:reps,
				}

		c.ProtoBuf(200, data)
		})

		r.Run(":8000")
}

获取参数


URL 参数

URL 参数指的是 URL? 后面携带的参数(如:/user/search?username=cqupthao&address=CQUPT ,key-value 格式,多个 key-value 用 & 连接),获取 URL 参数可以通过调用 DefaultQuery() 方法或 Query() 方法,其方法的声明如下:

func (c *Context) DefaultQuery(key, defaultValue string) string
func (c *Context) Query(key string) (value string)

DefaultQuery() 方法若参数不存在,返回默认值;Query() 方法若参数不存在,返回空串。

例如编写一个 Server HTTP 程序,该程序的具体代码如下:

package main

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

func main() {
		// Default 返回一个默认的路由引擎
		r := gin.Default()
		r.GET("/user/search", func(c *gin.Context) {
				// 取不到则使用指定的默认值
				username := c.DefaultQuery("username", "cqupthao")
				
				// username := c.Query("username")
				address := c.Query("address")
				/*
				// 取到返回(值,true);取不到返回("",false)
				username,ok = c.GetQuery("username")
				if !ok {
						username = "somebody"
				}
				*/
				// 输出 JSON 格式的结果给调用方
				c.JSON(http.StatusOK, gin.H{
						"message":  "ok",
						"username": username,
						"address":  address,
				})
		})
		
		r.Run(":8000")
}

执行程序访问指定网址 http://127.0.0.1:8000/user/search 不传递参数),浏览到页面的内容如下:

{"address":"","message":"ok","username":"cqupthao"}

执行程序访问指定网址 http://127.0.0.1:8000/user/search?username=cqupthao&address=CQUPT(传递参数),浏览到页面的内容如下:

{"address":"CQUPT","message":"ok","username":"cqupthao"}

表单参数

表单传输为 post 请求,HTTP 常见的传输格式有如下四种:

  • application/json

  • application/x-www-form-urlencoded

  • application/xml

  • multipart/form-data

获取表单参数可以通过调用 PostForm() DefaultPostForm() 方法获取,该方法默认解析的是 x-www-form-urlencodedfrom-data 格式的参数,其方法的声明分别如下:

func (c *Context) PostForm(key string) (value string)
func (c *Context) DefaultPostForm(key, defaultValue string) string

例如创建一个 form.html 表单文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>Document</title>
  </head>
 	<body>
		<form action="http://localhost:8000/login" method="post"
		action="application/x-www-form-urlencoded">
		用户名:<input type="text" name="username" placeholder="请输入你的用户名">
<br>&nbsp;&nbsp;&nbsp;码:<input type="password" name="userpassword"
placeholder="请输入你的密码">  <br>
		<input type="submit" value="提交">
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来获取该表单参数,该程序的具体代码如下:

package main

import (
		"fmt"
		"net/http"
		"github.com/gin-gonic/gin"
)

func main() {
		r := gin.Default()
		r.LoadHTMLFiles("./form.html")
		r.GET("/login",func(c *gin.Context){
				c.HTML(http.StatusOK,"form.html",nil)
		})
		
		r.POST("/login", func(c *gin.Context) {
		types := c.DefaultPostForm("type", "post")
		username := c.PostForm("username")
		password := c.PostForm("userpassword")		
		
		/*
		// 设置默认值方式
		username := c.DefaultPostForm("username","somebody")
		password := c.DefaultPostForm("userpassword","***")
		*/

		/*
		username,ok := c.GetPostForm("username")
		if !ok {
				username = "sb"
		}
		userpassword,ok := c.GetPostForm("userpassword")
		if !ok {
				userpassword = "ssb"
		}	
		*/	
		// c.String(http.StatusOK,fmt.Sprintf("username:%s,password:%s,type:%s",username, password, types))
		c.String(http.StatusOK, fmt.Sprintf("username:%s,password:%s,type:%s",username, password, types))
		})
		
		r.Run(":8000")
}

执行程序访问指定网址,在框中输入用户名和密码,点击提交按钮,浏览到页面的内容如下:

username:cqupthao,password:123456,type:post

JSON 参数

当前端请求的数据通过 JSON 格式提交时(如向 /json 发送一个 POST 请求),则获取请求参数的方式如下:

r.POST("/json", func(c *gin.Context) {
		// 注意:下面为了举例子方便,暂时忽略了错误处理
		b, _ := c.GetRawData()  // 从 c.Request.Body 读取请求数据
		// 定义 map 或结构体
		var m map[string]interface{}
		// 反序列化
		_ = json.Unmarshal(b, &m)

		c.JSON(http.StatusOK, m)
})

PATH 参数

请求的参数通过 URL 路径传递(如:http://localhost/xxx/cqupthao ),可以通过调用 Param() 方法来获取 PATH 参数,其方法的声明如下:

func (c *Context) Param(key string) string

编写一个 Server HTTP 程序来获取网址 http://localhost/xxx/cqupthaoPATH 参数,该程序的具体代码如下:

package main

import (
		"net/http"
		"strings"
		"github.com/gin-gonic/gin"
)

func main() {
		r := gin.Default()
		r.GET("/user/:name/*action", func(c *gin.Context) {
				name := c.Param("name")
				action := c.Param("action")

				// 截取
				action = strings.Trim(action, "/")
				c.String(http.StatusOK, name+" "+action)
		})

		// 默认为监听 8080 端口
		r.Run(":8000")
}

执行程序访问网址 http://127.0.0.1:8000/user/hello/cqupthao ,浏览到页面的内容如下:

hello cqupthao

又例如编写一个 Server HTTP 程序获取网址 http://localhost:8000/user/search/cqupthao/CQUPT 的请求 URL 路径中的参数,该程序的具体代码如下:

package main

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

func main() {
		// Default返回一个默认的路由引擎
		r := gin.Default()
		r.GET("/user/search/:username/:address", func(c *gin.Context) {
				username := c.Param("username")
				address := c.Param("address")
				//输出 json 格式结果给调用方
				c.JSON(http.StatusOK, gin.H{
						"message":  "ok",
						"username": username,
						"address":  address,
				})
		})

		r.Run(":8000")
}

执行程序访问网址 http://localhost:8000/user/search/cqupthao/CQUPT](http://localhost:8000/user/search/cqupthao/CQUPT) ,浏览到页面的内容如下:

{"address":"CQUPT","message":"ok","username":"cqupthao"}

参数绑定


为了能够更方便的获取请求相关参数,提高开发效率,可以基于请求的 Content-Type 来识别请求数据类型并利用反射机制自动提取请求中 QueryStringformJSONXML 等参数到结构体中。

通过调用 ShouldBind() 方法能够基于请求自动提取 JSONformQueryString 类型的数据并把值绑定到指定的结构体对象,其方法的声明如下:

func (c *Context) ShouldBind(obj any) error

例如编写一个 Server HTTP 程序来绑定和解析各种类型的参数,该程序的具体代码如下:

package main

import (
        "fmt"
        "net/http"
        "github.com/gin-gonic/gin"
)

// Binding from JSON
type Login struct {
	User     string `form:"user" json:"user" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func main() {
		r := gin.Default()
		// 绑定JSON的示例 ({"user": "xxx", "password": "123456"})
		r.POST("/loginJSON", func(c *gin.Context) {
				var login Login

				if err := c.ShouldBind(&login); err == nil {
						fmt.Printf("login info:%#v\n", login)
						c.JSON(http.StatusOK, gin.H{
								"user":     login.User,
								"password": login.Password,
						})
				} else {
						c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				}
		})

		// 绑定 form 表单示例 (user=xxx&password=123456)
		r.POST("/loginForm", func(c *gin.Context) {
				var login Login
				// ShouldBind() 会根据请求的 Content-Type 自行选择绑定器
				if err := c.ShouldBind(&login); err == nil {
						c.JSON(http.StatusOK, gin.H{
								"user":     login.User,
								"password": login.Password,
						})
				} else {
						c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				}
		})

		// 绑定QueryString示例 (/loginQuery?user=xxx&password=123456)
		r.GET("/loginForm", func(c *gin.Context) {
				var login Login
				// ShouldBind() 会根据请求的 Content-Type 自行选择绑定器
				if err := c.ShouldBind(&login); err == nil {
						c.JSON(http.StatusOK, gin.H{
								"user":     login.User,
								"password": login.Password,
						})
				} else {
						c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				}
		})

	r.Run(":8000")
}

ShouldBind()会按照下面的顺序解析请求中的数据完成绑定:

  • 如果是 GET 请求,只使用 Form 绑定引擎(query)。

  • 如果是 POST 请求,检查 content-type 是否为 JSONXML 后再使用 Formform-data)。


JSON 数据解析和绑定

在 Gin 框架中绑定 JSON 数据可通过调用 ShouldBindJSON() 方法,其方法的声明如下:

func (c *Context) ShouldBindJSON(obj any) error

编写一个 Server HTTP 程序来绑定和解析 JSON 参数,该程序的具体代码如下:

package main

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

// 定义接收数据的结构体
type Login struct {
		// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
		User	string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`

		Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
		// 1.创建路由
		// 默认使用了2个中间件Logger(), Recovery()
		r := gin.Default()
		// JSON绑定
		r.POST("/loginJSON", func(c *gin.Context) {
				// 声明接收的变量
				var json Login
				// 将 request 的 body 中的数据,自动按照json格式解析到结构体
				if err := c.ShouldBindJSON(&json); err != nil {
						// 返回错误信息
						// gin.H封装了生成json数据的工具
						c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
						return
				}
				// 判断用户名密码是否正确
				if json.User != "root" || json.Pssword != "admin" {
						c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
						return
				}
				c.JSON(http.StatusOK, gin.H{"status": "200"})
		})
		r.Run(":8000")
}

表单数据解析和绑定

在 Gin 框架中绑定表单数据通过调用 Bind() 方法,其方法的声明如下:

func (c *Context) Bind(obj any) error

例如创建一个 form.html 表单文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>Document</title>
  </head>
	<body>
		<form action="http://localhost:8000/loginForm" method="post"
	enctype="application/x-www-form-urlencoded">
			<br>用户名:<input type="text" name="username"></br>
			<br>密码:<input type="password" name="password"></br>
			<br><input type="submit" value="提交"></br>
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来绑定和解析该表单参数,该程序的具体代码如下:

package main

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

		// 定义接收数据的结构体
type Login struct {
		// binding:"required" 修饰的字段,若接收为空值,则报错,是必须字段
		User	string `form:"username" json:"user" uri:"user" xml:"user"

binding:"required"`
		Pssword string `form:"password" json:"password" uri:"password"
xml:"password" binding:"required"`
}

func main() {
		r := gin.Default()
		// JSON 绑定
		r.POST("/loginForm", func(c *gin.Context) {
				// 声明接收的变量
				var form Login
				// Bind() 默认解析并绑定 form 格式
				// 根据请求头中 content-type 自动推断
				if err := c.Bind(&form); err != nil {
						c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
						return

				}
				// 判断用户名密码是否正确
				if form.User != "root" || form.Pssword != "admin" {
						c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
						return
				}
		c.JSON(http.StatusOK, gin.H{"status": "200"})
		})
		r.Run(":8000")
}

执行程序后,用浏览器打开该 HTML 文件,在框中输入错误的用户名和密码,点击提交按钮,浏览到页面的内容如下:

{"status":"304"}

执行程序后,用浏览器打开该 HTML 文件,在框中输入正确的用户名和密码,点击提交按钮,浏览到页面的内容如下:

{"status":"200"}

URI 数据解析和绑定

在 Gin 框架中绑定 URI 数据可通过调用 ShouldBindUri() 方法,其方法的声明如下:

func (c *Context) ShouldBindUri(obj any) error

编写一个 Server HTTP 程序来绑定和解析 URI 参数,该程序的具体代码如下:

package main

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

		// 定义接收数据的结构体
type Login struct {
		// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
		User 	string `form:"username" json:"user" uri:"user" xml:"user"
binding:"required"`
		Pssword string `form:"password" json:"password" uri:"password"
xml:"password" binding:"required"`
}

func main() {
		r := gin.Default()
		// JSON 绑定
		r.GET("/:user/:password", func(c *gin.Context) {
		// 声明接收的变量
		var login Login
		// Bind() 默认解析并绑定 form 格式
		// 根据请求头中 content-type 自动推断
		if err := c.ShouldBindUri(&login); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
		}
		// 判断用户名密码是否正确
		if login.User != "root" || login.Pssword != "admin" {
				c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
				return
		}
		c.JSON(http.StatusOK, gin.H{"status": "200"})
		})
		r.Run(":8000")
}

执行程序,在终端输入命令 curl http://127.0.0.1:8000/root/admin(错误的用户名和密码),输出的结果如下:

{"status":"304"}

执行程序,在终端输入命令 curl http://127.0.0.1:8000/root/admin(正确的用户名和密码),输出的结果如下:

{"status":"200"}

文件上传


在 Gin 框架中调用 FormFile() 方法获取文件,调用 SaveUploadedFile() 方法来实现文件的上传,其方法的声明如下:

func (c *Context) FormFile(name string) (*multipart.FileHeader, error)
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error

单个文件的上传

例如创建一个名为 index.htmlHTML 文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>上传文件示例</title>
  </head>
	<body>
		<form action="http://localhost:8000/upload" method="post"
		enctype="multipart/form-data">
				<br>上传文件:<input type="file" name="file" ></br>
				<br><input type="submit" value="提交"></br>
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来实现单个文件的上传,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
		//限制上传最大尺寸
		r.MaxMultipartMemory = 8 << 20
		r.POST("/upload", func(c *gin.Context) {
				file, err := c.FormFile("file")
				if err != nil {
						c.String(500, "上传图片出错")
				}
				// c.JSON(200, gin.H{"message": file.Header.Context})
				c.SaveUploadedFile(file, file.Filename)
				c.String(http.StatusOK, file.Filename)
		})
		r.Run(":8000")
}

上传文件到指定位置

例如创建一个名为 index.htmlHTML 文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <title>上传单个文件示例</title>
  </head>
	<body>
		<form action="/upload" method="post" enctype="multipart/form-data">
   		 <input type="file" name="f1">
   	 	<input type="submit" value="上传">
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来实现单个文件的上传,该程序的具体代码如下:

func main() {
		r := gin.Default()
		// 处理multipart forms提交文件时默认的内存限制是32 MiB
		// 可以通过下面的方式修改
		// router.MaxMultipartMemory = 8 << 20  // 8 MiB
		r.POST("/upload", func(c *gin.Context) {
				// 单个文件
				file, err := c.FormFile("f1")
				if err != nil {
						c.JSON(http.StatusInternalServerError, gin.H{
						"message": err.Error(),
					})
					return
				}

				log.Println(file.Filename)
				dst := fmt.Sprintf("/root/home/%s_%d", file.Filename)
				// 上传文件到指定的目录
				c.SaveUploadedFile(file, dst)
				c.JSON(http.StatusOK, gin.H{
						"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
				})
		})
		r.Run()
}

特定文件的上传

有的用户上传文件需要限制上传文件的类型以及上传文件的大小,因此基于原生的函数写法写了一个可以限制大小以及文件类型的上传函数。

例如创建一个名为 index.htmlHTML 文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>上传特定文件示例</title>
  </head>
	<body>
		<form action="http://localhost:8000/upload" method="post"
		enctype="multipart/form-data">
				<br>上传文件:<input type="file" name="file" ></br>
				<br><input type="submit" value="提交"></br>
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来实现单个文件的上传,该程序的具体代码如下:

package main

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

func main() {
		r := gin.Default()
		r.POST("/upload", func(c *gin.Context) {
				_, headers, err := c.Request.FormFile("file")
				if err != nil {
						log.Printf("Error when try to get file: %v", err)
				}
				//headers.Size 获取文件大小
				if headers.Size > 1024*1024*2 {
						fmt.Println("文件太大了")
						return
				}
				//headers.Header.Get("Content-Type")获取上传文件的类型
				if headers.Header.Get("Content-Type") != "image/png" {
						fmt.Println("只允许上传png图片")
						return
				}
				c.SaveUploadedFile(headers, "./video/"+headers.Filename)
				c.String(http.StatusOK, headers.Filename)
		})
		r.Run()
}

多个文件的上传

例如创建一个名为 index.htmlHTML 文件,该文件的具体内容如下:

<!DOCTYPE html>
<html lang="en">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>上传多个文件示例</title>
  </head>
	<body>
		<form action="http://localhost:8000/upload" method="post"
		enctype="multipart/form-data">
				<br>上传文件:<input type="file" name="files" multiple></br>
				<br><input type="submit" value="提交"></br>
		</form>
	</body>
</html>

编写一个 Server HTTP 程序来实现单个文件的上传,该程序的具体代码如下:

package main

import (
		"github.com/gin-gonic/gin"
		"net/http"
		"fmt"
)

// gin的helloWorld
func main() {
		r := gin.Default()
		// 限制表单上传大小 8MB,默认为32MB
		r.MaxMultipartMemory = 8 << 20
		r.POST("/upload", func(c *gin.Context) {
				form, err := c.MultipartForm()
				if err != nil {
						c.String(http.StatusBadRequest, fmt.Sprintf("get err %s",err.Error()))
				}
				// 获取所有图片
				files := form.File["files"]
				// 遍历所有图片
				for _, file := range files {
						// 逐个存
						if err := c.SaveUploadedFile(file, file.Filename); err != nil { 
								c.String(http.StatusBadRequest,
fmt.Sprintf("upload err %s",err.Error()))
								return
						}
				}
				c.String(200, fmt.Sprintf("upload ok %d files", len(files)))
		})
		//默认端口号是8080
		r.Run(":8000")
}

重定向


HTTP 重定向

HTTP 重定向的实现是调用 Redirect() 方法, 内部、外部重定向均支持,其方法的声明如下:

func (c *Context) Redirect(code int, location string)

使用方法参考如下关键的程序代码部分:

r.GET("/test", func(c *gin.Context) {
		c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})

路由重定向

路由重定向通过调用 HandleContext() 方法来实现,其方法的声明如下:

func (engine *Engine) HandleContext(c *Context)

使用方法参考如下关键的程序代码部分:

r.GET("/test", func(c *gin.Context) {
    	// 指定重定向的 URL
    	c.Request.URL.Path = "/test2"
    	r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
    	c.JSON(http.StatusOK, gin.H{"hello": "world"})
})

同步与异步


goroutine 机制可以方便地实现异步处理,在启动新的 goroutine 时,不应该使用原始上下文,必须使用它的只读副本。

package main

import (
		"log"
		"time"
		"github.com/gin-gonic/gin"
)

func main() {
		r := gin.Default()
		// 1.异步
		r.GET("/long_async", func(c *gin.Context) {
				// 需要一个副本
				copyContext := c.Copy()
				// 异步处理
				go func() {
						time.Sleep(3 * time.Second)
						log.Println("异步执行:" + copyContext.Request.URL.Path)
				}()
		})
		// 2.同步
		r.GET("/long_sync", func(c *gin.Context) {
				time.Sleep(3 * time.Second)
				log.Println("同步执行:" + c.Request.URL.Path)
		})
		r.Run(":8000")
}

路由拆分与注册


基本的路由注册

下面最基础的 gin 路由注册方式,适用于路由条目比较少的简单项目或者项目demo。

r.GET("/index", func(c *gin.Context) {...})
r.GET("/login", func(c *gin.Context) {...})
r.POST("/login", func(c *gin.Context) {...})
package main

import (
		"net/http"

		"github.com/gin-gonic/gin"
)

func helloHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello q1mi!",
	})
}

func main() {
		r := gin.Default()
		r.GET("/hello", helloHandler)
		if err := r.Run(); err != nil {
				fmt.Println("startup service failed, err:%v\n", err)
		}
}

此外,还有一个可以匹配所有请求方法的 Any() 方法如下:

r.Any("/test", func(c *gin.Context) {...})

为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。

r.NoRoute(func(c *gin.Context) {
		c.HTML(http.StatusNotFound, "views/404.html", nil)
})

路由组

可以将拥有共同 URL 前缀的路由划分为一个路由组,习惯性一对 {} 包裹同组的路由,这只是为了规范。

func main() {
		r := gin.Default()
		userGroup := r.Group("/user")
		{
				userGroup.GET("/index", func(c *gin.Context) {...})
				userGroup.GET("/login", func(c *gin.Context) {...})
				userGroup.POST("/login", func(c *gin.Context) {...})

		}
		shopGroup := r.Group("/shop")
		{
				shopGroup.GET("/index", func(c *gin.Context) {...})
				shopGroup.GET("/cart", func(c *gin.Context) {...})
				shopGroup.POST("/checkout", func(c *gin.Context) {...})
		}
		r.Run()
}
package main

import (
		"github.com/gin-gonic/gin"
		"fmt"
)

// gin的helloWorld
func main() {
		r := gin.Default()
		// 路由组1 ,处理GET请求
		v1 := r.Group("/v1")
		// {} 是书写规范
		{
				v1.GET("/login", login)
				v1.GET("submit", submit)
		}
		v2 := r.Group("/v2")
		{
				v2.POST("/login", login)
				v2.POST("/submit", submit)
		}
		r.Run(":8000")
}

func login(c *gin.Context) {
		name := c.DefaultQuery("name", "jack")
		c.String(200, fmt.Sprintf("hello %s\n", name))
}

func submit(c *gin.Context) {
		name := c.DefaultQuery("name", "lily")
		c.String(200, fmt.Sprintf("hello %s\n", name))
}

路由组也是支持嵌套的,例如:

shopGroup := r.Group("/shop")
		{
				shopGroup.GET("/index", func(c *gin.Context) {...})
				shopGroup.GET("/cart", func(c *gin.Context) {...})
				shopGroup.POST("/checkout", func(c *gin.Context) {...})
				// 嵌套路由组
				xx := shopGroup.Group("xx")
				xx.GET("/oo", func(c *gin.Context) {...})
		}

通常将路由分组用在划分业务逻辑或划分 API 版本时。


路由拆分成单独文件或包

当项目的规模增大后就不太适合继续在项目的main.go文件中去实现路由注册相关逻辑了,我们会倾向于把路由部分的代码都拆分出来,形成一个单独的文件或包:

我们在routers.go文件中定义并注册路由信息:

package main

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

func helloHandler(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
				"message": "Hello q1mi!",
		})
}

func setupRouter() *gin.Engine {
		r := gin.Default()
		r.GET("/hello", helloHandler)
				return r
}

此时main.go中调用上面定义好的setupRouter函数:

func main() {
		r := setupRouter()
		if err := r.Run(); err != nil {
				fmt.Println("startup service failed, err:%v\n", err)
		}
}

此时的目录结构:

gin_demo
├── go.mod
├── go.sum
├── main.go
└── routers.go

把路由部分的代码单独拆分成包的话也是可以的,拆分后的目录结构如下:

gin_demo
├── go.mod
├── go.sum
├── main.go
└── routers
└── routers.go

routers/routers.go需要注意此时setupRouter需要改成首字母大写:

package routers

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

func helloHandler(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
				"message": "Hello q1mi!",
		})
}

// SetupRouter 配置路由信息
func SetupRouter() *gin.Engine {
		r := gin.Default()
		r.GET("/hello", helloHandler)
		return r
}

main.go文件内容如下:

package main

import (
	"fmt"
	"gin_demo/routers"
)

func main() {
		r := routers.SetupRouter()
		if err := r.Run(); err != nil {
				fmt.Println("startup service failed, err:%v\n", err)
		}
}

路由拆分成多个文件

当我们的业务规模继续膨胀,单独的一个routers文件或包已经满足不了我们的需求了,

func SetupRouter() *gin.Engine {
		r := gin.Default()
		r.GET("/hello", helloHandler)
 	 	r.GET("/xx1", xxHandler1)
  		...
  		...
  		r.GET("/xx30", xxHandler30)
		return r
}

因为我们把所有的路由注册都写在一个SetupRouter函数中的话就会太复杂了。

我们可以分开定义多个路由文件,例如:

gin_demo
├── go.mod
├── go.sum
├── main.go
└── routers
├── blog.go
└── shop.go
routers/shop.go中添加一个LoadShop的函数,将shop相关的路由注册到指定的路由器:

func LoadShop(e *gin.Engine)  {
		e.GET("/hello", helloHandler)
  		e.GET("/goods", goodsHandler)
  		e.GET("/checkout", checkoutHandler)
		...
		...
}

routers/blog.go中添加一个`LoadBlog的函数,将blog相关的路由注册到指定的路由器:

func LoadBlog(e *gin.Engine) {
		e.GET("/post", postHandler)
  		e.GET("/comment", commentHandler)
  		...
  		...
}

在main函数中实现最终的注册逻辑如下:

func main() {
		r := gin.Default()
		routers.LoadBlog(r)
		routers.LoadShop(r)
		if err := r.Run(); err != nil {
				fmt.Println("startup service failed, err:%v\n", err)
		}
}

路由拆分到不同的 APP

有时候项目规模实在太大,那么我们就更倾向于把业务拆分的更详细一些,例如把不同的业务代码拆分成不同的APP。

因此我们在项目目录下单独定义一个app目录,用来存放我们不同业务线的代码文件,这样就很容易进行横向扩展。大致目录结构如下:

gin_demo
├── app
│ ├── blog
│ │ ├── handler.go
│ │ └── router.go
│ └── shop
│ ├── handler.go
│ └── router.go
├── go.mod
├── go.sum
├── main.go
└── routers
└── routers.go
其中app/blog/router.go用来定义blog相关的路由信息,具体内容如下:

func Routers(e *gin.Engine) {
		e.GET("/post", postHandler)
		e.GET("/comment", commentHandler)
}

app/shop/router.go用来定义shop相关路由信息,具体内容如下:

func Routers(e *gin.Engine) {
		e.GET("/goods", goodsHandler)
		e.GET("/checkout", checkoutHandler)
}

routers/routers.go中根据需要定义Include函数用来注册子app中定义的路由,Init函数用来进行路由的初始化操作:

type Option func(*gin.Engine)

var options = []Option{}

// 注册app的路由配置
func Include(opts ...Option) {
		options = append(options, opts...)
}

// 初始化
func Init() *gin.Engine {
		r := gin.Default()
		for _, opt := range options {
				opt(r)
		}
		return r
}

main.go中按如下方式先注册子app中的路由,然后再进行路由的初始化:

func main() {
		// 加载多个APP的路由配置
		routers.Include(shop.Routers, blog.Routers)
		// 初始化路由
		r := routers.Init()
		if err := r.Run(); err != nil {
				fmt.Println("startup service failed, err:%v\n", err)
		}
}

路由原理

Gin框架中的路由使用的是httprouter这个库。httproter会将所有路由规则构造一颗前缀树

例如有 root and as at cn com


中间件


Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。


定义中间件

在 Gin 框架中的中间件必须是一个 gin.HandlerFunc 类型。

记录接口耗时的中间件

例如我们像下面的代码一样定义一个统计请求耗时的中间件。

// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
		return func(c *gin.Context) {
				start := time.Now()
				c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
				// 调用该请求的剩余处理程序
				c.Next()
				// 不调用该请求的剩余处理程序
				// c.Abort()
				// 计算耗时
				cost := time.Since(start)
				log.Println(cost)
		}
}

记录响应体的中间件

我们有时候可能会想要记录下某些情况下返回给客户端的响应数据,这个时候就可以编写一个中间件来搞定。


type bodyLogWriter struct {
		gin.ResponseWriter               // 嵌入gin框架ResponseWriter
		body               *bytes.Buffer // 我们记录用的response
}

// Write 写入响应体数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
		w.body.Write(b)                  // 我们记录一份
		return w.ResponseWriter.Write(b) // 真正写入响应
}

// ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func ginBodyLogMiddleware(c *gin.Context) {
		blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}
		c.Writer = blw // 使用我们自定义的类型替换默认的

		c.Next() // 执行业务逻辑

		fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应
}

跨域中间件 cors

推荐使用社区的https://github.com/gin-contrib/cors 库,一行代码解决前后端分离架构下的跨域问题。

注意: 该中间件需要注册在业务处理函数前面。

这个库支持各种常用的配置项,具体使用方法如下。

package main

import (
  		"time"
  		"github.com/gin-contrib/cors"
  		"github.com/gin-gonic/gin"
)

func main() {
  		r := gin.Default()
  		// CORS for https://foo.com and https://github.com origins, allowing:
 	 	// - PUT and PATCH methods
  		// - Origin header
  		// - Credentials share
  		// - Preflight requests cached for 12 hours
  		r.Use(cors.New(cors.Config{
    			AllowOrigins:     []string{"https://foo.com"},  // 允许跨域发来请求的网站
    			AllowMethods:     []string{"GET", "POST", "PUT", "DELETE",  "OPTIONS"},  // 允许的请求方法
    			AllowHeaders:     []string{"Origin", "Authorization", "Content-Type"},
    			ExposeHeaders:    []string{"Content-Length"},
    			AllowCredentials: true,
    			AllowOriginFunc: func(origin string) bool {  // 自定义过滤源站的方法
      			return origin == "https://github.com"
    			},MaxAge: 12 * time.Hour,
  		}))
  		r.Run()
}

当然可以简单的像下面的示例代码那样使用默认配置,允许所有的跨域请求。

func main() {
  		r := gin.Default()
  		// same as
  		// config := cors.DefaultConfig()
  		// config.AllowAllOrigins = true
  		// router.Use(cors.New(config))
  		r.Use(cors.Default())
  		r.Run()
}

注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

全局路由注册

所有请求都经过此中间件

func main() {
		// 新建一个没有任何默认中间件的路由
		r := gin.New()
		// 注册一个全局中间件
		r.Use(StatCost())
	
		r.GET("/test", func(c *gin.Context) {
				name := c.MustGet("name").(string) // 从上下文取值
				log.Println(name)
				c.JSON(http.StatusOK, gin.H{
						"message": "Hello world!",
				})
		})
		r.Run()
}
package main

import (
		"fmt"
		"time"
		"github.com/gin-gonic/gin"
)

// 定义中间
func MiddleWare() gin.HandlerFunc {
		return func(c *gin.Context) {
				t := time.Now()
				fmt.Println("中间件开始执行了")
				// 设置变量到Context的key中,可以通过Get()取
				c.Set("request", "中间件")
				status := c.Writer.Status()
				fmt.Println("中间件执行完毕", status)
				t2 := time.Since(t)
				fmt.Println("time:", t2)
		}
}

func main() {
		r := gin.Default()
		// 注册中间件
		r.Use(MiddleWare())
		// {}为了代码规范
		{
				r.GET("/ce", func(c *gin.Context) {
						// 取值
						req, _ := c.Get("request")
						fmt.Println("request:", req)
						// 页面接收
						c.JSON(200, gin.H{"request": req})
				})

		}
		r.Run()
}

注:黑色的数据里面有一步算时间差没有执行。

单个路由注册

func main(){
// 给/test2路由单独注册中间件(可注册多个)
		r.GET("/test2", StatCost(), func(c *gin.Context) {
				name := c.MustGet("name").(string) // 从上下文取值
				log.Println(name)
				c.JSON(http.StatusOK, gin.H{
			"message": "Hello world!",
				})
		})
}
package main

import (
		"fmt"
		"time"
		"github.com/gin-gonic/gin"
)

// 定义中间
func MiddleWare() gin.HandlerFunc {
		return func(c *gin.Context) {
				t := time.Now()
				fmt.Println("中间件开始执行了")
				// 设置变量到Context的key中,可以通过Get()取
				c.Set("request", "中间件")
				// 执行函数
				c.Next()
				// 中间件执行完后续的一些事情
				status := c.Writer.Status()
				fmt.Println("中间件执行完毕", status)
				t2 := time.Since(t)
				fmt.Println("time:", t2)
		}
}
func main() {
		r := gin.Default()
		//局部中间键使用
		r.GET("/ce", MiddleWare(), func(c *gin.Context) {
				// 取值
				req, _ := c.Get("request")
				fmt.Println("request:", req)
				// 页面接收
				c.JSON(200, gin.H{"request": req})
		})
		r.Run()
}

路由组注册

为路由组注册中间件有以下两种写法。

写法1:

shopGroup := r.Group("/shop", StatCost())
{
    	shopGroup.GET("/index", func(c *gin.Context) {...})
    	...
    	...
}

写法 2 :

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    	shopGroup.GET("/index", func(c *gin.Context) {...})
    	...
    	...
}

Next() 方法

package main

import (
		"fmt"
		"time"
		"github.com/gin-gonic/gin"
)

// 定义中间
func MiddleWare() gin.HandlerFunc {
		return func(c *gin.Context) {
				t := time.Now()
				fmt.Println("中间件开始执行了")
				// 设置变量到Context的key中,可以通过Get()取
				c.Set("request", "中间件")
				// 执行函数
				c.Next()
				// 中间件执行完后续的一些事情
				status := c.Writer.Status()
				fmt.Println("中间件执行完毕", status)
				t2 := time.Since(t)
				fmt.Println("time:", t2)
		}
}

func main() {
		r := gin.Default()
		// 注册中间件
		r.Use(MiddleWare())
		// {}为了代码规范
		{
				r.GET("/ce", func(c *gin.Context) {
				// 取值
				req, _ := c.Get("request")
				fmt.Println("request:", req)
				// 页面接收

				c.JSON(200, gin.H{"request": req})
		})

		}
		r.Run()
}

执行该程序,访问指定网址输出如下的结果:

中间件开始执行了
中间件执行完毕 404
time: 1.14367ms

中间件注意事项

  • gin默认中间件

gin.Default() 默认使用了 LoggerRecovery 中间件,其中:

  • Logger 中间件将日志写入 gin.DefaultWriter ,即使配置了GIN_MODE=release

  • Recovery 中间件会 recover 任何 panic,如果有 panic 的话,会写入500 响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

  • gin 中间件中使用 goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

  • 运行多个服务

可以在多个端口启动服务,例如:

package main

import (
		"log"
		"net/http"
		"time"
		"github.com/gin-gonic/gin"
		"golang.org/x/sync/errgroup"
)

var (
		g errgroup.Group
)

func router01() http.Handler {
		e := gin.New()
		e.Use(gin.Recovery())
		e.GET("/", func(c *gin.Context) {
				c.JSON(http.StatusOK,gin.H{
						"code":  http.StatusOK,
						"error": "Welcome server 01",
				},
			)
		})

		return e
}

func router02() http.Handler {
		e := gin.New()
		e.Use(gin.Recovery())
		e.GET("/", func(c *gin.Context) {
				c.JSON(http.StatusOK,gin.H{
						"code":  http.StatusOK,
						"error": "Welcome server 02",
				},
			)
		})

		return e
}

func main() {
		server01 := &http.Server{
				Addr:         ":8080",
				Handler:      router01(),
				ReadTimeout:  5 * time.Second,
				WriteTimeout: 10 * time.Second,
		}

		server02 := &http.Server{
				Addr:         ":8081",
				Handler:      router02(),
				ReadTimeout:  5 * time.Second,
				WriteTimeout: 10 * time.Second,
		}
   		// 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
		g.Go(func() error {
				return server01.ListenAndServe()
		})

		g.Go(func() error {
				return server02.ListenAndServe()
		})

		if err := g.Wait(); err != nil {
				log.Fatal(err)
		}
}

中间件练习

例如定义一个程序计时中间件,然后定义 2 个路由,执行函数后应该打印统计的执行时间,如下:

package main

import (
		"fmt"
		"time"
		"github.com/gin-gonic/gin"
)

// 定义中间
func myTime(c *gin.Context) {
		start := time.Now()
		c.Next()
		// 统计时间
		since := time.Since(start)
		fmt.Println("程序用时:", since)
}

func main() {
		r := gin.Default()
		// 注册中间件
		r.Use(myTime)
		// {}为了代码规范
		shoppingGroup := r.Group("/shopping")
		{
				shoppingGroup.GET("/index", shopIndexHandler)
				shoppingGroup.GET("/home", shopHomeHandler)
		}
		r.Run(":8000")
}

func shopIndexHandler(c *gin.Context) {
		time.Sleep(5 * time.Second)
}

func shopHomeHandler(c *gin.Context){
		time.Sleep(3*time.Second)
}

运行程序,访问指定网址后输出如下的结果:

程序用时: 5.001643926s
[GIN] 2023/03/04 - 20:57:24 | 200 |  5.001982483s |       127.0.0.1 | GET      "/shopping/index"
程序用时: 3.001534013s
[GIN] 2023/03/04 - 20:57:30 | 200 |  3.001646264s |       127.0.0.1 | GET      "/shopping/home"

参数验证


结构体验证

用 gin 框架的数据验证,可以不用解析数据,减少 if else 语句,会简洁许多。

package main

import (
		"fmt"
		"time"
		"github.com/gin-gonic/gin"
)

//Person ..
type Person struct {
		//不能为空并且大于10
		Age	int	`form:"age" binding:"required,gt=10"`
		Name	string	`form:"name" binding:"required"`
		Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func main() {
		r := gin.Default()
		r.GET("/5lmh", func(c *gin.Context) {
				var person Person
				if err := c.ShouldBind(&person); err != nil {
						c.String(500, fmt.Sprint(err))
						return
				}
				c.String(200, fmt.Sprintf("%#v", person))
		})
		r.Run()
}

(1)自定义验证

package main

import (
		"net/http"
		"reflect"
		"github.com/gin-gonic/gin"
		"github.com/gin-gonic/gin/binding"
		"gopkg.in/go-playground/validator.v8"
)

/*
对绑定解析到结构体上的参数,自定义验证功能
比如我们要对 name 字段做校验,要不能为空,并且不等于 admin ,类似这种需求,就无法
binding 现成的方法
需要我们自己验证方法才能实现 官网示例(https://godoc.org/gopkg.in/go-
playground/validator.v8#hdr-Custom_Functions)
这里需要下载引入下 gopkg.in/go-playground/validator.v8
*/
type Person struct {
		Age int `form:"age" binding:"required,gt=10"`
		// 2、在参数 binding 上使用自定义的校验方法函数注册时候的名称
		Name	string `form:"name" binding:"NotNullAndAdmin"`
		Address string `form:"address" binding:"required"`
}
// 1、自定义的校验方法
func nameNotNullAndAdmin(v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string) bool {
		if value, ok := field.Interface().(string); ok {
		// 字段不能为空,并且不等于   admin
		return value != "" && !("5lmh" == value)
		}
		return true
}

func main() {
		r := gin.Default()
		// 3、将我们自定义的校验方法注册到 validator中
		if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
				// 这里的 key 和 fn 可以不一样最终在 struct 使用的是 key
				v.RegisterValidation("NotNullAndAdmin", nameNotNullAndAdmin)
}

/*

curl -X GET "http://127.0.0.1:8080/testing?name=&age=12&address=beijing"

curl -X GET "http://127.0.0.1:8080/testing?
name=lmh&age=12&address=beijing"
curl -X GET "http://127.0.0.1:8080/testing?
name=adz&age=12&address=beijing"
*/
		r.GET("/5lmh", func(c *gin.Context) {
				var person Person
				if e := c.ShouldBind(&person); e == nil {
						c.String(http.StatusOK, "%v", person)
				} else {
						c.String(http.StatusOK, "person bind err:%v", e.Error())
				}
		})
		r.Run()
}

多语言翻译验证

当业务系统对验证信息有特殊需求时,例如:返回信息需要自定义,手机端返回的信息需要是中文而pc端发挥返回的信息需要时英文,如何做到请求一个接口满足上述三种情况。

package main
import (
		"fmt"
		"github.com/gin-gonic/gin"
		"github.com/go-playground/locales/en"
		"github.com/go-playground/locales/zh"
		"github.com/go-playground/locales/zh_Hant_TW"
		ut "github.com/go-playground/universal-translator"
		"gopkg.in/go-playground/validator.v9"
		en_translations "gopkg.in/go-playground/validator.v9/translations/en"
		zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
		zh_tw_translations "gopkg.in/go-playground/validator.v9/translations/zh_tw"
)

var (
		Uni*ut.UniversalTranslator
		Validate *validator.Validate
)

type User struct {
		Username string `form:"user_name" validate:"required"`
		Tagline  string `form:"tag_line" validate:"required,lt=10"`
		Tagline2 string `form:"tag_line2" validate:"required,gt=1"`
}

func main() {
		en := en.New()
		zh := zh.New()
		zh_tw := zh_Hant_TW.New()
		Uni = ut.New(en, zh, zh_tw)
		Validate = validator.New()
		route := gin.Default()
		route.GET("/5lmh", startPage)
		route.POST("/5lmh", startPage)
		route.Run(":8080")
}

func startPage(c *gin.Context) {
		//这部分应放到中间件中
		locale := c.DefaultQuery("locale", "zh")
		trans, _ := Uni.GetTranslator(locale)
		switch locale {
			case "zh":
					zh_translations.RegisterDefaultTranslations(Validate, trans)
					break
			case "en":
					en_translations.RegisterDefaultTranslations(Validate, trans)
					break
			case "zh_tw":
					zh_tw_translations.RegisterDefaultTranslations(Validate, trans)
					break
			default:
					zh_translations.RegisterDefaultTranslations(Validate, trans)
					break
}

		//自定义错误内容

		Validate.RegisterTranslation("required", trans, func(ut ut.Translator)
error {
				return ut.Add("required", "{0} must have a value!", true) // see
				universal-translator for details
				}, func(ut ut.Translator, fe validator.FieldError) string {
				t, _ := ut.T("required", fe.Field())
				return t
		})

		//这块应该放到公共验证方法中
		user := User{}
		c.ShouldBind(&user)
		fmt.Println(user)
		err := Validate.Struct(user)
		if err != nil {
				errs := err.(validator.ValidationErrors)
				sliceErrs := []string{}
				for _, e := range errs {
						sliceErrs = append(sliceErrs, e.Translate(trans))
				}
				c.String(200, fmt.Sprintf("%#v", sliceErrs))
		}
		c.String(200, fmt.Sprintf("%#v", "user"))
}

会话控制


Cookie

  • 介绍

  • HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出

  • Cookie就是解决HTTP协议无状态的方案之一,中文是小甜饼的意思

  • Cookie实际上就是服务器保存在浏览器上的一段信息。浏览器有了Cookie之后,每次向服务器发送请求时都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求

  • Cookie由服务器创建,并发送给浏览器,最终由浏览器保存

  • Cookie的用途

测试服务端发送cookie给客户端,客户端请求时携带cookie

  • Cookie的缺点

  • 不安全,明文

  • 增加带宽消耗

  • 可以被禁用

  • cookie有上限

  • Cookie的使用

测试服务端发送cookie给客户端,客户端请求时携带cookie

package main

import (
    	"fmt"
    	"github.com/gin-gonic/gin"
)

func main() {
    	r := gin.Default()
  		// 服务端要给客户端cookie
    	r.GET("/cookie", func(c *gin.Context) {
        		cookie, err := c.Cookie("gin_cookie") // 获取 Cookie
        		if err != nil {
            			cookie = "NotSet"
        // 给客户端设置cookie
		//  maxAge int, 单位为秒
		// path,cookie所在目录
		// domain string,域名
		//   secure 是否智能通过https访问
		// httpOnly bool  是否允许别人通过js获取自己的cookie
            			c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        		}
        		fmt.Printf("Cookie value: %s \n", cookie)
    	})

    	r.Run()
}

Cookie 练习

模拟实现权限验证中间件

有2个路由,login和home

login用于设置cookie

home是访问查看信息的请求

在请求home之前,先跑中间件代码,检验是否存在cookie

访问home,会显示错误,因为权限校验未通过

package main

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

func AuthMiddleWare() gin.HandlerFunc {
		return func(c *gin.Context) {
				// 获取客户端cookie并校验
				if cookie, err := c.Cookie("abc"); err == nil {
						if cookie == "123" {
								c.Next()
.								return
						}
				}
				// 返回错误
				c.JSON(http.StatusUnauthorized, gin.H{"error": "err"})
				// 若验证不通过,不再调用后续的函数处理
				c.Abort()
				return
		}
}

func main() {
		r := gin.Default()
		r.GET("/login", func(c *gin.Context) {
				// 设置cookie
				c.SetCookie("abc", "123", 60, "/",
"localhost", false, true)
				// 返回信息
				c.String(200, "Login success!")
		})
		r.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
				c.JSON(200, gin.H{"data": "home"})
		})
		r.Run(":8000")
}

Session

gorilla/sessions为自定义session后端提供cookie和文件系统session以及基础结构。

主要功能是:

  • 简单的API:将其用作设置签名(以及可选的加密)cookie的简便方法。

  • 内置的后端可将session存储在cookie或文件系统中。

  • Flash消息:一直持续读取的session值。

  • 切换session持久性(又称“记住我”)和设置其他属性的便捷方法。

  • 旋转身份验证和加密密钥的机制。

  • 每个请求有多个session,即使使用不同的后端也是如此。

  • 自定义session后端的接口和基础结构:可以使用通用API检索并批量保存来自不同商店的 session。

package main

import (
		"fmt"
		"net/http"
		"github.com/gorilla/sessions"
)

// 初始化一个cookie存储对象
// something-very-secret应该是一个你自己的密匙,只要不被别人知道就行
var store = sessions.NewCookieStore([]byte("something-very-secret"))

func main() {
		http.HandleFunc("/save", SaveSession)
		http.HandleFunc("/get", GetSession)
		err := http.ListenAndServe(":8080", nil)
				if err != nil {
						fmt.Println("HTTP server failed,err:", err)
						return
				}
		}

func SaveSession(w http.ResponseWriter, r *http.Request) {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
//  获取一个session对象,session-name是session的名字
		session, err := store.Get(r, "session-name")
		if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
		}

		// 在session中存储值
		session.Values["foo"] = "bar"
		session.Values[42] = 43
		// 保存更改
		session.Save(r, w)
		}
func GetSession(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
		if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
		}
		foo := session.Values["foo"]
		fmt.Println(foo)
}

删除 session 的值:

// 删除
// 将session的最大存储时间设置为小于零的数即为删除
session.Options.MaxAge = -1
session.Save(r, w)
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值