Gin - 高性能 Golang Web 框架的介绍和使用 (yoytang.com)
源码分析1
路由2
请求报文3
获取参数4
参数绑定5
验证6
响应7
中间件8
上传与返回文件9
日志10
http 重定向和路由重定向11
cookie12
记录日志13
响应输出14
gin 常见的坑(错误)15
源码分析
gin 初始化
func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine }
调用顺序
gin.Default()
-gin.New()
-Engine.Use()
-RouterGroup.Use()
gin 如何注册中间件 处理函数
无论是单个路由和路由组,都会使用
RouterGroup.Handle
函数。单个路由在
Engine.RouterGroup
实例上加处理函数Engine 有 RouterGroup 有 Engine
路由组会实例化新的
RouterGroup
实例上加处理函数RouterGroup 有 Engine
不过最终规则 处理函数和路径都会作为节点添加到路由方法树的适当位置。这样,当请求到达时,Gin 就可以根据请求的路径和方法在路由方法树中进行匹配,找到对应的处理函数来处理该请求。
调用顺序
RouterGroup.Handle
-Engine.addRoute(httpMethod, absolutePath, handlers)
-root.addRoute(path, handlers)
gin 的启动
调用顺序
r.Run()
-http.ListenAndServe(address, engine)
-server.ListenAndServe()
gin 请求和响应过程
server.ListenAndServe()
-Server.Serve()
-conn.serve()
-serverHandler{c.server}.ServeHTTP(w, w.req)
-
Engine.ServeHTTP()
-Engine.handleHTTPRequest()
-c.Next()
-c.writermem.WriteHeaderNow()
-
server.ListenAndServe()
- 设置监听:
net.Listen("tcp", addr)
负责设置监听地址; - 接受并处理网络请求:
srv.Serve(ln)
负责在监听位置上接受网络请求,建立连接并做出响应。
- 设置监听:
-
Server.Serve()
使用循环不断
for Accept
请求,使用go c.serve(connCtx)
去服务一个新的连接。 -
go c.serve(connCtx)
serverHandler{c.server}.ServeHTTP(w, w.req)
这里
c.server
的Handler
字段的值是*gin.Engine
类型,所以调用的是Engine.ServeHTTP()
方法。 -
Engine.ServeHTTP()
- 建立连接上下文:从缓存池中提取上下文对象,填入当前连接的
http.ResponseWriter
实例与http.Request
实例; - 处理连接:以上下文对象的形式将连接交给函数处理,由
engine.handleHTTPRequest(c)
封装实现了; - 回收连接上下文:处理完毕后,将上下文对象回收进缓存池中。
- 建立连接上下文:从缓存池中提取上下文对象,填入当前连接的
-
handleHTTPRequest()
- 遍历查找
engine.trees
以找出当前请求的 HTTP Method 对应的处理树; - 从该处理树中,根据当前请求的路径与参数查询出对应的处理函数
value
; - 将查询出的处理函数链(
gin.HandlerChain
)写入当前连接上下文的c.handlers
中; - 执行
c.Next()
,调用 handlers 链上的下一个函数(中间件/业务处理函数),开始形成 LIFO 的函数调用栈; - 待函数调用栈全部返回后,
c.writermem.WriteHeaderNow()
根据上下文信息,将 HTTP 状态码写入响应头。 ↩
- 遍历查找
-
路由
-
什么是路由
路由(Routing)是根据 HTTP 请求行的 URL 路径(或者携带的参数)匹配到相对应的处理程序或处理函数的过程(handler)
-
静态路径路由
根据请求行的固定请求 URL 路径路由
func main() { r := gin.Default() r.GET("/get", func(c *gin.Context) {}) r.GET("/login", func(c *gin.Context) {}) r.POST("/login", func(c *gin.Context) {}) // 默认绑定 :8080 r.Run() }
r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusNotFound, "templates/404.html", nil) })
-
动态路径路由
根据请求行的动态请求 URL 路径路由
/users/123 /users/456 /users/23456
以上等等,我们有很多用户,如果我们都一个个为这些用户注册这些静态路径路由,那么我们是很难注册完的,而且我们还会有新注册的用户,可见这种办法不行。
我们观察这些(路径 URL),发现它们具备一定的规则:前面都是
users
,后面是users
的id
。这样我们就可以把这些路由归纳为:/users/id
这样我们就知道只有
id
这部分是可以变的,前面的users
是不变的。可变的id
可以作为成我们 API 服务输入的参数,这样我们就可以通过这个id
参数,获取对应的用户信息,这种 URL 匹配的模式,我们称之为动态路径路由。func main() { r := gin.Default() r.GET("/users/:id", func(c *gin.Context) { id := c.Param("id") c.String(200, "The user id is %s", id) }) r.Run(":8080") }
在动态路径路由时可以通过 Param 来获取路径(参数)
-
路由组
我们可以将 有共同前缀路径的请求的路由划分为一个路由组。
package main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() user := r.Group("/user") user.GET("/index", func(c *gin.Context) {}) user.POST("/login", func(c *gin.Context) {}) v1 := r.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) v1.POST("/read", readEndpoint) } r.Run() }
路由组支持嵌套的
func main() { r := gin.Default() user := r.Group("/user") user.GET("/index", func(c *gin.Context) {}) user.POST("/login", func(c *gin.Context) {}) pwd := user.Group("/pwd") pwd.GET("/pwd", func(c *gin.Context) {}) r.Run() }
-
文件路径
↩
-
请求报文
HTTP 服务端获取 URL 碰到的大坑 – 兰陵美酒郁金香的个人博客 (xhyonline.com)
-
如何获取起始行
-
如何获得首部
c.Request.Header
c.GetHeader()
参数是首部字段名
c.Request.Header.Get()
参数是首部字段名 -
如何获得主体
c.Request.Body
↩
-
获取参数
-
URL 键值对参数(Query 参数、Get 请求参数)
-
URL 键值对参数的例子
/path?id=1234&name=Manu&sex=male
-
什么时候会有 URL 键值对参数
根据 HTML 规范,表单的
method
属性设置为GET
时,浏览器会忽略enctype
属性的值,并将表单数据编码为查询参数附加在 URI 中。这意味着无论使用multipart/form-data
还是application/x-www-form-urlencoded
,表单数据都将以查询参数的形式添加到 URI 中。 -
在 gin 中如何获得:使用
Query
类
-
-
URL 路径参数
/delete/${inboundid} /delete/1 /delete/2/nihao // 对应gin的动态路由 /users/:id /user/:id/:name
r := gin.Default() r.GET("/user/:id/:name", func(c *gin.Context) { // 获取url的绑定参数 id := c.Param("id") name := c.Param("name") })
-
Post 请求参数
-
什么时候会有
- 表单的
method
属性设置为POST
,表单的enctype
属性为multipart/form-data
时,表单数据以多部分(multipart)的形式发送到服务器,每个部分都有自己的头部和内容。 - 表单的
method
属性设置为POST
,表单的enctype
属性为application/x-www-form-urlencoded
时,表单数据以键值对的形式编码,使用特定的 URL 编码规则。
提交的数据不会作为键值对的形式添加到请求行 URI 参数中。相反,数据将被包含在 HTTP 请求的消息体(body)中进行传输。
- 表单的
-
在 gin 中如何获得:使用
PostForm
类
-
-
HTML 表单
当表单的
method
属性设置为GET
时,表单提交的数据会作为键对的形式(foo=bar&boo=baz)添加到请求行 URI 参数中。
当表单的方法
method
属性设置为POST
,并且表单的enctype
属性设置为multipart/form-data
时。提交的数据不会作为键值对的形式添加到请求行 URI 参数中。相反,数据将被包含在 HTTP 请求的消息体(body)中进行传输。
当表单的
enctype
属性设置为application/x-www-form-urlencoded
时,浏览器会将表单数据编码为键值对,并将其添加到请求行 URI 的查询参数中,无论请求方法是 GET 还是 POST。 -
如何获取表单请求中数据
参数请求行 uri 中时,使用
Query
类/welcome?firstname=Jane&lastname=Doe func main() { router := gin.Default() // 匹配的url格式: /welcome?firstname=Jane&lastname=Doe router.GET("/welcome", func(c *gin.Context) { firstname := c.DefaultQuery("firstname", "Guest") lastname := c.Query("lastname") // 是 c.Request.URL.Query().Get("lastname") 的简写 c.String(http.StatusOK, "Hello %s %s", firstname, lastname) }) router.Run(":8080") }
数据在请求 body ,使用
PostForm
类func main() { router := gin.Default() router.POST("/form_post", func(c *gin.Context) { message := c.PostForm("message") nick := c.DefaultPostForm("nick", "anonymous") // 此方法可以设置默认值 c.JSON(200, gin.H{ "status": "posted", "message": message, "nick": nick, }) }) router.Run(":8080") }
数据在请求行 也在 body
↩示例: POST /post?id=1234&page=1 HTTP/1.1 Content-Type: application/x-www-form-urlencoded name=manu&message=this_is_great func main() { router := gin.Default() router.POST("/post", func(c *gin.Context) { id := c.Query("id") page := c.DefaultQuery("page", "0") name := c.PostForm("name") message := c.PostForm("message") fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) }) router.Run(":8080") }
-
参数绑定
-
什么是参数绑定
将 GET 参数、POST 参数、路径参数、JSON、XML、YAML 等绑定到指定的结构体实例上。
-
如何绑定
-
第一类绑定 Must bind
这些方法底层使用
MustBindWith
,如果存在绑定错误,请求将被以下指令中止c.AbortWithError(400,err).SetType(ErrorTypeBind)
,响应状态代码会被设置为 400,请求头Content-Type
被设置为text/plain; charset=utf-8
。注意,如果你试图在此之后设置响应代码,将会发出一个警告。
-
第二类绑定 Should bind (可控 常用
ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindYAML, ShouldBindHeader, ShouldBindQuery, ShouldBindUri,
这些方法底层使用
ShouldBindWith
,如果存在绑定错误,则返回错误,开发人员可以正确处理请求和错误。
当我们使用绑定方法时,Gin 会根据 Content-Type 推断出使用哪种绑定器
例如:
type Login struct { User string `form:"user" json:"user" xml:"user" ` Password string `form:"password" json:"password" xml:"password" ` }
这个结构体类型 可以绑定
表单jsonxml
具体绑定那个,在绑定时 Gin 会根据 Content-Type 推断出使用哪种绑定器 -
↩
-
验证
-
自定义验证器
如何在参数绑定时候使用自定义验证器
-
给结构体字段添加 标签
type Booking struct { CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"` }
-
实现自定义验证器
var bookableDate validator.Func = func(fl validator.FieldLevel) bool { date, ok := fl.Field().Interface().(time.Time) if ok { today := time.Now() if today.After(date) { return false } } return true }
-
注册自定义验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("bookabledate", bookableDate) }
-
使用 绑定时
-
↩
-
响应
- 响应字符串
- 响应 json
- 响应 xml
- 响应文件
- 设置响应的头 ↩
中间件
-
中间件什么时候执行
在请求到达相应的路由处理函数之前 之后被执行
在中间件内部为什么使用
c.Next
方法可以进入下一个中间件呢? -
单个路由使用中间件
在注册路由的时候指定要执行的中间件即可
func main() { r := gin.Default() // 注册一个路由,使用了 middleware1,middleware2 两个中间件 r.GET("/someGet", middleware1, middleware2, handler) // 默认绑定 :8080 r.Run() } func handler(c *gin.Context) { log.Println("exec handler") }
-
路由组使用中间件
路由组使用中间件和单个路由类似,只不过是要把中间件放到
Group
上.// 省略的代码 ... func main() { router := gin.Default() // 定义一个组前缀, 并使用 middleware1 中间件 // 访问 /v2/login 就会执行 middleware1 函数 v2 := router.Group("/v2", middleware1) v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) v2.POST("/read", readEndpoint) router.Run(":8080") } // 省略的代码 ...
-
在中间件中使用
go func
当在中间件或
handler
中启动新的Goroutine
时,不能使用原始的上下文,必须使用只读副本。
↩func main() { r := gin.Default() r.GET("/long_async", func(c *gin.Context) { // 创建在 goroutine 中使用的副本 tmp := c.Copy() go func() { // 用 time.Sleep() 模拟一个长任务。 time.Sleep(5 * time.Second) // 请注意您使用的是复制的上下文 "tmp",这一点很重要 log.Println("Done! in path " + tmp.Request.URL.Path) }() }) r.GET("/long_sync", func(c *gin.Context) { // 用 time.Sleep() 模拟一个长任务。 time.Sleep(5 * time.Second) // 因为没有使用 goroutine,不需要拷贝上下文 log.Println("Done! in path " + c.Request.URL.Path) }) r.Run() }
-
上传与返回文件
-
如何从请求读取单个文件
对于单个文件 使用
FormFile
方法,读取文件。func main() { r := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // r.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("C:/tmp/%s", file.Filename) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("'%s' uploaded!", file.Filename), }) }) r.Run()
-
如何将文件保存在本地
-
SaveUploadedFile
方法 -
os.create
-
-
如何从请求读取多个文件
对于多个文件使用
MultipartForm()
方法 是解析的多部分表单,包括文件上传。返回值类型
type Form struct { Value map[string][]string File map[string][]*FileHeader }
func main() { r := gin.Default() // 处理multipart forms提交文件时默认的内存限制是32 MiB // 可以通过下面的方式修改 // r.MaxMultipartMemory = 8 << 20 // 8 MiB r.POST("/upload", func(c *gin.Context) { // Multipart form form, _ := c.MultipartForm() files := form.File["file"] for index, file := range files { log.Println(file.Filename) dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index) // 上传文件到指定的目录 c.SaveUploadedFile(file, dst) } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("%d files uploaded!", len(files)), }) }) r.Run()
-
请求中是有文件有参数混合怎么办
使用
PostForm
获取到参数,使用FormFile
读取文件。 -
如何返回文件
返回只能返回一种数据类型
c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", file.Filename)) c.File("./" + file.Filename)
↩
-
日志
gin 框架自带的 日志
gin 自带的日志是一个中间件形式,我们可以设置日志的输出目标、日志的格式、日志的颜色
-
日志的颜色
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。 gin.DisableConsoleColor() // 强制日志颜色化 gin.ForceConsoleColor()
-
日志的输出目标
f, _ := os.Create("gin.log") gin.DefaultWriter = io.MultiWriter(f)
-
日志的格式
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers) }
第三方日志包
日志分割包 ↩
-
http 重定向和路由重定向
-
http 重定向
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/test", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "https://www.jiyik.com/") }) r.Run() }
-
路由重定向
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() r.GET("/test1", 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"}) }) r.Run() }
↩
-
cookie
↩
记录日志
↩package main import ( "github.com/gin-gonic/gin" "io" "os" ) func main() { // 禁用控制台颜色,将日志写入文件时不需要控制台颜色。 gin.DisableConsoleColor() // 记录到文件。 f, _ := os.Create("gin.log") gin.DefaultWriter = io.MultiWriter(f) // 如果需要同时将日志写入文件和控制台,请使用以下代码。 // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) r.Run() }
响应输出
↩
gin 常见的坑(错误)
这些方法底层使用
MustBindWith
,如果存在绑定错误,请求将被以下指令中止c.AbortWithError(400,err).SetType(ErrorTypeBind)
,响应状态代码会被设置为 400,请求头Content-Type
被设置为text/plain; charset=utf-8
。-
跨域问题(天坑)
Go Gin 跨域访问 CORS 解决 - 知乎 (zhihu.com)
负责解决跨域的中间件,要使用
Use
方法,作为全局中间件才能生效。type Engine struct { RouterGroup // 其他字段 pool sync.Pool trees methodTrees // 其他字段 } type RouterGroup struct { Handlers HandlersChain basePath string engine *Engine root bool }
gin 有只有唯一的
Engine
类实例,实例有RouterGroup
字段,RouterGroup
字段还有*Engine
字段(指向唯一的Engine
类实例的指针);除了唯一的
Engine
实例,还有多个RouterGroup
实例;其他的
RouterGroup
都会包含 唯一的Engine
实例的RouterGroup
的Handlers HandlersChain
。func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: group.combineHandlers(handlers), // 新的RouterGroup实例Handlers字段包含主RouterGroup实例Handlers字段的值 basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } } func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) // 以group.Handlers为基础 和 handlers 构建新的 HandlersChain类型实例 并返回值。 copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers }
-
参数绑定问题
gin 框架-参数绑定方法 ShouldBind 和 BindJSON 的区别 - ilovetesting - 博客园 (cnblogs.com)
错误:
[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 200
复现场景:使用
Must bind
类方法绑定参数,设置响应头状态码为 200,但客户端收到的 httpcode 为 400,无论开发者如何强制返回 httpcode,code 就是 400。原因: ↩
-
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于