第68节 深入学习 gin 源码
❤️💕💕记录sealos开源项目的学习过程。k8s,docker和云原生的学习。Myblog:http://nsddd.top
[TOC]
Why
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。
- 可扩展性:新建一个中间件非常简单。
下载并安装 gin
go get -u github.com/gin-gonic/gin
项目结构
实际项目业务功能和模块会很多,我们不可能把所有代码都写在一个go文件里面或者写在一个main入口函数里面;我们需要对项目结构做一些规划,方便维护代码以及扩展。
Gin框没有对项目结构做出限制,我们可以根据自己项目需要自行设计。
MVC 架构设计:
├── conf #项目配置文件目录
│ └── config.toml #大家可以选择自己熟悉的配置文件管理工具包例如:toml、xml等等
├── controllers #控制器目录,按模块存放控制器(或者叫控制器函数),必要的时候可以继续划分子目录。
│ ├── food.go
│ └── user.go
├── main.go #项目入口,这里负责Gin框架的初始化,注册路由信息,关联控制器函数等。
├── models #模型目录,负责项目的数据存储部分,例如各个模块的Mysql表的读写模型。
│ ├── food.go
│ └── user.go
├── static #静态资源目录,包括Js,css,jpg等等,可以通过Gin框架配置,直接让用户访问。
│ ├── css
│ ├── images
│ └── js
├── logs #日志文件目录,主要保存项目运行过程中产生的日志。
└── views #视图模板目录,存放各个模块的视图模板,当然有些项目只有api,是不需要视图部分,可以忽略这个目录
└── index.html
Gin 框架运行模式
为方便调试,Gin 框架在运行的时候默认是debug模式,在控制台默认会打印出很多调试日志,上线的时候我们需要关闭debug模式,改为release模式。
export GIN_MODE=release
GIN_MODE
环境变量,可以设置为debug或者release
通过代码设置
在main函数,初始化gin框架的时候执行下面代码
// 设置 release模式
gin.SetMode(gin.ReleaseMode)
// 或者 设置debug模式
gin.SetMode(gin.DebugMode)
Gin 路由和控制器
路由是一个过程,指的是一个http
请求,如何找到对应的处理器函数(也可以叫控制器函数), Gin
框架的路由是基于httprouter
包实现的。
控制器函数主要负责执行 http请求-响应任务
。
💡简单的一个案例如下:
r := gin.Default()
// 路由定义post请求, url路径为:/user/login, 绑定doLogin控制器函数
r.POST("/user/login", doLogin)
// 控制器函数
func doLogin(c *gin.Context) {
// 获取post请求参数
username := c.PostForm("username")
password := c.PostForm("password")
// 通过请求上下文对象Context, 直接往客户端返回一个字符串
c.String(200, "username=%s,password=%s", username,password)
}
路由规则
一条路由规则由三部分组成:
- http请求方法
- url路径
- 控制器函数
http请求方法:
常用的http请求方法有下面4种:
- GET
- POST
- PUT
- DELETE
url路径:
echo框架,url路径有三种写法:
- 静态url路径
- 带路径参数的url路径
- 带星号(*)模糊匹配参数的url路径
💡简单的一个案例如下:
/ 例子1, 静态Url路径, 即不带任何参数的url路径 /users/center /user/111 /food/12 // 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。 // 路径参数值可以是数值,也可以是字符串 //定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径 /user/:id //定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径 /food/:id //定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径 /foods/:type/:page // 例子3. 带星号(*)模糊匹配参数的url路径 // 星号代表匹配任意路径的意思, 必须在*号后面指定一个参数名,后面可以通过这个参数获取*号匹配的内容。 //以/foods/ 开头的所有路径都匹配 //匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1 /foods/*path //可以通过path参数获取*号匹配的内容。
控制器函数:
控制器函数定义:
func HandlerFunc(c *gin.Context)
控制器函数接受一个上下文参数。
可以通过上下文参数,获取http请求参数,响应http请求。
💡简单的一个案例如下:
//实例化gin实例对象。
r := gin.Default()
//定义post请求, url路径为:/users, 绑定saveUser控制器函数
r.POST("/users", saveUser)
//定义get请求,url路径为:/users/:id (:id是参数,例如: /users/10, 会匹配这个url模式),绑定getUser控制器函数
r.GET("/users/:id", getUser)
//定义put请求
r.PUT("/users/:id", updateUser)
//定义delete请求
r.DELETE("/users/:id", deleteUser)
//控制器函数实现
func saveUser(c *gin.Context) {
...忽略实现...
}
func getUser(c *gin.Context) {
...忽略实现...
}
func updateUser(c *gin.Context) {
...忽略实现...
}
func deleteUser(c *gin.Context) {
...忽略实现...
}
分组路由
在我们做 API 的时候,如果要支持多个 API 的版本,我们可以通过 分组路由来实现
💡简单的一个案例如下:
func main() {
router := gin.Default()
// 创建v1组
v1 := router.Group("/v1")
{
// 在v1这个分组下,注册路由
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// 创建v2组
v2 := router.Group("/v2")
{
// 在v2这个分组下,注册路由
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
上面的例子将会注册下面的路由信息:
- /v1/login
- /v1/submit
- /v1/read
- /v2/login
- /v2/submit
- /v2/read
Note
一般还是推荐采用合适的目录结构进行分组
Gin如何处理请求参数
Get 请求
Get请求url例子:*/path?id=1234&name=Manu&value=*111
# 获取Get请求参数的常用函数:
func (c *Context) Query(key string) string
func (c *Context) DefaultQuery(key, defaultValue string) string
func (c *Context) GetQuery(key string) (string, bool)
💡简单的一个案例如下:
func Handler(c *gin.Context) {
//获取name参数, 通过Query获取的参数值是String类型。
name := c.Query("name")
//获取name参数, 跟Query函数的区别是,可以通过第二个参数设置默认值。
name := c.DefaultQuery("name", "tizi365")
//获取id参数, 通过GetQuery获取的参数值也是String类型,
// 区别是GetQuery返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
id, ok := c.GetQuery("id")
if !ok {
// 参数不存在
}
}
Post 请求
常用的函数:
func (c *Context) PostForm(key string) string
func (c *Context) DefaultPostForm(key, defaultValue string) string
func (c *Context) GetPostForm(key string) (string, bool)
💡简单的一个案例如下:
func Handler(c *gin.Context) {
//获取name参数, 通过PostForm获取的参数值是String类型。
name := c.PostForm("name")
// 跟PostForm的区别是可以通过第二个参数设置参数默认值
name := c.DefaultPostForm("name", "tizi365")
//获取id参数, 通过GetPostForm获取的参数值也是String类型,
// 区别是GetPostForm返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
id, ok := c.GetPostForm("id")
if !ok {
// 参数不存在
}
}
获取URL路径参数
获取URL路径参数,指的是获取 /user/:id
这类型路由绑定的参数,这个例子绑定了一个参数id。
获取url路径参数常用函数:
func (c *Context) Param(key string) string
💡简单的一个案例如下:
r := gin.Default()
r.GET("/user/:id", func(c *gin.Context) {
// 获取url参数id
id := c.Param("id")
})
将请求参数绑定到struct对象
前面获取参数的方式都是一个个参数的读取,比较麻烦,Gin框架支持将请求参数自动绑定到一个struct对象,这种方式支持Get/Post请求,也支持http请求body内容为json/xml格式的参数。
下面例子是将请求参数绑定到User struct对象。
// User 结构体定义
type User struct {
Name string `json:"name" form:"name"`
Email string `json:"email" form:"email"`
}
通过定义 struct
字段的标签,定义请求参数和 struct
字段的关系。 下面对 User
的 Name
字段的标签进行说明。
struct标签说明:
标签 | 说明 |
---|---|
json:"name" | 数据格式为json格式,并且json字段名为name |
form:"name" | 表单参数名为name |
下面是控制器代码:
r.POST("/user/:id", func(c *gin.Context) {
// 初始化user struct
u := User{}
// 通过ShouldBind函数,将请求参数绑定到struct对象, 处理json请求代码是一样的。
// 如果是post请求则根据Content-Type判断,接收的是json数据,还是普通的http请求参数
if c.ShouldBind(&u) == nil {
// 绑定成功, 打印请求参数
log.Println(u.Name)
log.Println(u.Email)
}
// http 请求返回一个字符串
c.String(200, "Success")
})
Gin如何获取客户ip
r := gin.Default()
r.GET("/ip", func(c *gin.Context) {
// 获取用户IP
ip := c.ClientIP()
})
Gin处理请求结果
gin.Context上下文对象支持多种返回处理结果,下面分别介绍不同的响应方式。
以字符串方式响应请求
通过String函数返回字符串。
函数定义:
func (c *Context) String(code int, format string, values ...interface{})
参数说明:
参数 | 说明 |
---|---|
code | http状态码 |
format | 返回结果,支持类似Sprintf函数一样的字符串格式定义,例如,%d 代表插入整数,%s代表插入字符串 |
values | 任意个format参数定义的字符串格式参数 |
💡简单的一个案例如下:
func Handler(c *gin.Context) {
// 例子1:
c.String(200, "欢迎访问nsddd.top!")
// 例子2: 这里定义了两个字符串参数(两个%s),后面传入的两个字符串参数将会替换对应的%s
c.String(200,"欢迎访问%s, 你是%s", "nsddd.top!","最靓的仔!")
}
以json格式响应请求
我们开发api接口的时候常用的格式就是json,下面是返回json格式数据的例子
// User 定义
type User struct {
Name string `json:"name"` // 通过json标签定义struct字段转换成json字段的名字。
Email string `json:"email"`
}
// Handler 控制器
func(c *gin.Context) {
//初始化user对象
u := &User{
Name: "Xinwei Xiong",
Email: "3293172751nss@gmail.com",
}
//返回json数据
//返回结果:{"name":"Xinwei Xiong", "email":"3293172751nss@gmail.com"}
c.JSON(200, u)
}
以xml格式响应请求
type User struct {
Name string `xml:"name"` // 通过xml标签定义struct字段转换成xml字段的名字。
Email string `xml:"email"`
}
c.XML(200, u)
以文件格式响应请求
下面介绍gin框架如何直接返回一个文件,可以用来做文件下载。
func(c *gin.Context) {
//通过File函数,直接返回本地文件,参数为本地文件地址。
//函数说明:c.File("文件路径")
c.File("/var/www/1.jpg")
}
例子2:
func(c *gin.Context) {
//通过FileAttachment函数,返回本地文件,类似File函数,区别是可以指定下载的文件名。
//函数说明: c.FileAttachment("文件路径", "下载的文件名")
c.FileAttachment("/var/www/1.jpg", "1.jpg")
}
html模板处理
Gin 框架默认封装了golang内置的 html/template
包用于处理html模版。
使用到了 Go语言 的 template 模板引擎的特性:
Go语言模板引擎
Go语言内置了 text/template 和 html/template两个模板库,专门用于处理网页html模板。 html/template 是在 text/template 模板库的基础上增加了对html输出的安全处理,主要目的是为了防止被攻击。
模版引擎使用流程:
- 编写模版代码
- 导入包
- 加载模版代码
- 根据模版参数渲染模版
💡简单的一个案例如下:
将下面模版代码保存至views/demo.tpl文件中, 文件后缀名随意。
{{define "demo"}}
昵称: {{.Name}},
{{- if .IsWin}}
恭喜,大吉大利,今晚吃鸡!
{{- else}}
遗憾,鸡被吃光了!
{{- end}}
{{- end}}
define "模板名" 用于定义子模板,后面渲染模板会用到这个名字。
然后我们导入包:
import "text/template"
加载模版代码:
// 加载模版代码,并且创建template对象t
// template.ParseGlob 函数加载views目录下的所有tpl为后缀的模版文件
// template.Must函数主要用于检测加载的模版有没有错误,有错误输出panic错误,并且结束程序。
t := template.Must(template.ParseGlob("./views/*.tpl"))
根据模版参数渲染模版
定义模版参数:
//这里定义一个struct类型的模版参数,实际上模版可以是任意类型数据
type GameStatus struct {
Name string
IsWin bool
}
因为只能传入一个模版参数,如果想传入多个模版参数,可以使用map或者struct类型。
初始化模板参数, 这里初始化一个参数数组,下面用于循环渲染模板。
var userStatus = []GameStatus{
{"大春", true},
{"NiuBee", false},
{"球球", true},
}
下面是根据userStatus 数组循环渲染模板
for _, u := range userStatus {
//根据参数u, 渲染命名为demo的模板,并且将渲染结果打印到标准输出
err := t.ExecuteTemplate(os.Stdout, "demo", u)
if err != nil {
log.Println("executing template:", err)
}
}
输出结果:
昵称: 大春,
恭喜,大吉大利,今晚吃鸡!
昵称: NiuBee,
遗憾,鸡被吃光了!
昵称: 球球,
恭喜,大吉大利,今晚吃鸡!
Gin 返回模板的案例
💡简单的一个案例如下:
func main() {
// 初始化gin对象
router := gin.Default()
// 首先加载templates目录下面的所有模版文件,模版文件扩展名随意
router.LoadHTMLGlob("templates/*")
// 绑定一个url路由 /index
router.GET("/index", func(c *gin.Context) {
// 通过HTML函数返回html代码
// 第二个参数是模版文件名字
// 第三个参数是map类型,代表模版参数
// gin.H 是map[string]interface{}类型的别名
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "Main website",
})
})
// 启动http服务,并且绑定在8080端口
router.Run(":8080")
}
模版代码
文件名:templates/index.html
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// 设置cookie
c.SetCookie("site_cookie", "cookievalue", 3600, "/", "localhost", false, true)
})
router.Run()/h1>
</html>
Gin框架如何处理cookie
cookie通常用于在浏览器中保存一些小数据,例如客户标识、用户非铭感数据。注意别使用cookie保存隐私数据。
gin框架主要通过上下文对象提供的SetCookie和Cookie两个函数操作cookie
设置cookie
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// 设置cookie
c.SetCookie("site_cookie", "cookievalue", 3600, "/", "localhost", false, true)
})
router.Run()
}
SetCookie函数定义:
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
参数说明:
参数名 | 类型 | 说明 |
---|---|---|
name | string | cookie名字 |
value | string | cookie值 |
maxAge | int | 有效时间,单位是秒,MaxAge=0 忽略MaxAge属性,MaxAge<0 相当于删除cookie, 通常可以设置-1代表删除,MaxAge>0 多少秒后cookie失效 |
path | string | cookie路径 |
domain | string | cookie作用域 |
secure | bool | Secure=true,那么这个cookie只能用https协议发送给服务器 |
httpOnly | bool | 设置HttpOnly=true的cookie不能被js获取到 |
读取 cookie
func Handler(c *gin.Context) {
// 根据cookie名字读取cookie值
data, err := c.Cookie("site_cookie")
if err != nil {
// 直接返回cookie值
c.String(200,data)
return
}
c.String(200,"not found!")
}
删除 cookie
通过将cookie的MaxAge设置为-1, 达到删除cookie的目的。
func Handler(c *gin.Context) {
// 设置cookie MaxAge设置为-1,表示删除cookie
c.SetCookie("site_cookie", "cookievalue", -1, "/", "localhost", false, true)
c.String(200,"删除cookie演示")
}
Go 和 Gin 开发 RESTful API
Gin 源码学习
Gin 的源码主要由以下几部分组成:
Engine
:代表一个 Gin 实例,维护中间件、路由信息等Router
:实现路由查找及分发请求的逻辑Context
:封装 Request 和 Response,提供一系列方便的方法访问 request/responseMiddleware
: 中间件相关代码Render
: 提供 JSON、XML、YAML、HTML 等响应渲染方法File
: 文件上传及服务相关方法ErrorHandler
: 错误处理中间件
这些部分组合在一起,构成了 Gin 这个轻量级 Web 框架。
END 链接
✴️版权声明 © :本书所有内容遵循CC-BY-SA 3.0协议(署名-相同方式共享)©