gin框架源码实战day1
Radix树
这个路由信息:
r := gin.Default()r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)
得到的路由树解析:
Priority Path Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>
解读:
这个图只是把结点的值写在了变上,不要误会,其实值是在点中。
节点:每个节点代表路由路径的一部分。例如,s、earch\ 和 support\ 节点分别代表 /search/ 和 /support/ 路径的组成部分。
分支:节点之间的连接线表示路径的分支。例如,├ 和 └ 符号表示从父节点分出的不同子路径。
叶节点:代表完整的路由路径,通常在叶节点处关联具体的处理函数(如 <1>、<2> 等)。这些处理函数用于响应对应路径的 HTTP 请求。
nil 节点:表示该路径节点是参数路径(如 :post)的一部分,或者是路径的中间部分,而不直接关联处理函数。
路由树详细解读
根节点:\ 代表根节点,即路由的起点。它关联了处理根路径 / 请求的函数 *<1>。
s 分支:表示以 s 开头的路径。它进一步分为两个子路径:earch\(对应 /search/,处理函数 *<2>)和 upport\(对应 /support/,处理函数 *<3>)。
blog\ 分支:代表 /blog/ 路径,关联处理函数 *<4>。它有一个参数子路径 :post\,用于匹配如 /blog/:post/ 形式的路径,最终关联的处理函数是 *<5>。
about-us\ 分支:代表 /about-us/ 路径,关联处理函数 *<6>。它有一个静态子路径 team\,对应 /about-us/team/ 路径,处理函数为 *<7>。
contact\ 分支:代表 /contact/ 路径,关联处理函数 *<8>。
符号说明
├ 和 └:分支符号,├ 用于表示除最后一个分支外的节点,└ 用于表示最后一个分支节点。
nil:表示该节点不直接关联处理函数,可能是因为它是一个参数化的路径部分,或者是未到达叶节点的中间节点。
*<数字>:代表与节点关联的处理函数。数字仅为示例,实际上会是指向处理函数的指针。
源码解读:
从这个最简单的例子,来逐步分析源码
package mainimport ("github.com/gin-gonic/gin""log""net/http"
)func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "ok")})err := r.Run(":8080")if err != nil {log.Fatalln(err)}
}
先看启动的过程,也就是run函数
err := r.Run(“:8080”)这个我们只知道是用来启动的,来看看内部是如何启动的。
run函数的源码:
看源码的时候养成一个习惯,关注主要流程,忽略一些不必要的因素。
func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()if engine.isUnsafeTrustedProxies() {debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return
}
该方法是用来启动 Gin 应用并监听 HTTP 请求的。
具体分析:
方法签名 :
func (engine *Engine) Run(addr ...string) (err error) {
engine *Engine:Engine 是 Gin 框架的核心结构体,代表整个 Gin 应用。这里的 engine 是一个指向 Engine 实例的指针,表示 Run 方法是 Engine 的一个方法。
addr …string:这是一个可变参数,表示 Run 方法可以接受一个或多个字符串参数,这些字符串代表监听地址(包括端口号)。如果没有提供参数,默认监听地址是 :8080。
(err error):这是方法的返回值,表示方法执行结束后可能返回的错误。
Engine结构体解读:
type Engine struct {RouterGroup// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a// handler for the path with (without) the trailing slash exists.// For example if /foo/ is requested but a route only exists for /foo, the// client is redirected to /foo with http status code 301 for GET requests// and 307 for all other request methods.RedirectTrailingSlash bool// RedirectFixedPath if enabled, the router tries to fix the current request path, if no// handle is registered for it.// First superfluous path elements like ../ or // are removed.// Afterwards the router does a case-insensitive lookup of the cleaned path.// If a handle can be found for this route, the router makes a redirection// to the corrected path with status code 301 for GET requests and 307 for// all other request methods.// For example /FOO and /..//Foo could be redirected to /foo.// RedirectTrailingSlash is independent of this option.RedirectFixedPath bool// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the// current route, if the current request can not be routed.// If this is the case, the request is answered with 'Method Not Allowed'// and HTTP status code 405.// If no other Method is allowed, the request is delegated to the NotFound// handler.HandleMethodNotAllowed bool// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was// fetched, it falls back to the IP obtained from// `(*gin.Context).Request.RemoteAddr`.ForwardedByClientIP bool// AppEngine was deprecated.// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD// #726 #755 If enabled, it will trust some headers starting with// 'X-AppEngine...' for better integration with that PaaS.AppEngine bool// UseRawPath if enabled, the url.RawPath will be used to find parameters.UseRawPath bool// UnescapePathValues if true, the path value will be unescaped.// If UseRawPath is false (by default), the UnescapePathValues effectively is true,// as url.Path gonna be used, which is already unescaped.UnescapePathValues bool// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.// See the PR #1817 and issue #1644RemoveExtraSlash bool// RemoteIPHeaders list of headers used to obtain the client IP when// `(*gin.Engine).ForwardedByClientIP` is `true` and// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.RemoteIPHeaders []string// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by// that platform, for example to determine the client IPTrustedPlatform string// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm// method call.MaxMultipartMemory int64// UseH2C enable h2c support.UseH2C bool// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.ContextWithFallback booldelims render.DelimssecureJSONPrefix stringHTMLRender render.HTMLRenderFuncMap template.FuncMapallNoRoute HandlersChainallNoMethod HandlersChainnoRoute HandlersChainnoMethod HandlersChainpool sync.Pooltrees methodTreesmaxParams uint16maxSections uint16trustedProxies []stringtrustedCIDRs []*net.IPNet
}
Engine 结构体是 Gin Web 框架的核心,它继承了 RouterGroup,提供了路由和中间件的管理,同时也包含了很多与 HTTP 服务运行相关的配置。
解读:
1.RouterGroup:
type RouterGroup struct {Handlers HandlersChainbasePath stringengine *Engineroot bool
}
RouterGroup 是 Gin 框架中用于组织路由和中间件的结构体,它允许开发者将具有共同前缀的路由组织在一起,并且可以共享中间件。这样做有助于代码的组织和复用,使得路由和中间件的管理更加方便和高效。下面是对 RouterGroup 结构体字段的解读:
Handlers HandlersChain: HandlersChain 类型,代表一系列的中间件处理函数。这些处理函数会按照添加的顺序被执行,并且会被绑定到 RouterGroup 下的所有路由上。这意味着,所有在此 RouterGroup 中注册的路由都会先通过这些中间件的处理。
这个地方我当初看我还是没懂,这里我做进一步解读:
HandlersChain 解释
HandlersChain 是一个中间件处理函数的切片(slice),在 Gin 中用于存储一组中间件。中间件是在处理 HTTP 请求之前或之后运行的函数,通常用于执行一些预处理操作(如日志记录、身份验证、数据验证等)或后处理操作(如设置响应头等)。在 RouterGroup 中注册的中间件会自动应用到该组下的所有路由上。
中间件执行流程
当一个请求到达 Gin 服务器时,Gin 会根据请求的 URL 和方法匹配路由。如果匹配成功,Gin 会按照 HandlersChain 中的顺序执行相应的中间件,最后执行路由的主处理函数。每个中间件可以选择:
1.直接返回响应,不再调用后续的中间件或主处理函数。
2.修改请求或响应的内容,然后调用下一个中间件或主处理函数。
3.执行一些不依赖请求内容的操作,如日志记录。
例子:
假设我们有一个 Web 应用,我们想为 /api 路径下的所有路由添加日志和身份验证两个中间件。
package mainimport ("github.com/gin-gonic/gin""net/http"
)// 日志中间件
func LoggerMiddleware() gin.HandlerFunc {return func(c *gin.Context) {// 记录请求开始的日志// 注意:这里只是示例,实际使用时可能需要记录更多详细信息println("Starting request:", c.Request.URL.Path)c.Next() // 调用下一个中间件或主处理函数// 请求处理完毕后,记录日志println("Request finished")}
}// 身份验证中间件
func AuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {// 检查用户是否已认证// 这里只是简单示例,实际应用中应该是更复杂的逻辑token := c.GetHeader("Authorization")if token != "valid-token" {// 如果用户未认证,返回 401 状态码并终止请求c.AbortWithStatus(http.StatusUnauthorized)return}c.Next() // 用户已认证,继续处理请求}
}func main() {r := gin.Default()apiGroup := r.Group("/api")// 为/api路由组添加日志和身份验证中间件apiGroup.Use(LoggerMiddleware(), AuthMiddleware())// 注册一个路由apiGroup.GET("/users", func(c *gin.Context) {// 假设这是获取用户列表的处理函数c.JSON(http.StatusOK, gin.H{"message": "获取用户列表"})})r.Run(":8080")
}
在解读之前先补充gin.Context
type Context struct {writermem responseWriterRequest *http.RequestWriter ResponseWriterParams Paramshandlers HandlersChainindex int8fullPath stringengine *Engineparams *ParamsskippedNodes *[]skippedNode// This mutex protects Keys map.mu sync.RWMutex// Keys is a key/value pair exclusively for the context of each request.Keys map[string]any// Errors is a list of errors attached to all the handlers/middlewares who used this context.Errors errorMsgs// Accepted defines a list of manually accepted formats for content negotiation.Accepted []string// queryCache caches the query result from c.Request.URL.Query().queryCache url.Values// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,// or PUT body parameters.formCache url.Values// SameSite allows a server to define a cookie attribute making it impossible for// the browser to send this cookie along with cross-site requests.sameSite http.SameSite
}
gin.Context 是 Gin 框架中一个非常核心的结构体,它在 Gin 处理 HTTP 请求的过程中被广泛使用。gin.Context 封装了 Go 原生的 http.Request 和 http.ResponseWriter,提供了丰富的方法和属性,方便开发者对请求和响应进行操作。通过 gin.Context,开发者可以访问请求的数据、设置响应的数据、管理中间件、执行特定的路由函数等。
字段解读:
HTTP 请求和响应
writermem: 内部使用的 responseWriter,用于缓存响应写入器的状态。
Request: 指向原始的 http.Request 对象,包含了 HTTP 请求的所有信息,如头部、URL、请求体等。
Writer: ResponseWriter 接口的实例,用于构造 HTTP 响应。它封装了底层的 http.ResponseWriter,提供了更多的功能,如状态码的设置、写入响应头和体。
路由和处理
Params: 路由参数集合,每个参数包含键和值。例如,对于路由 /user/:name,Params 会包含一个键为 “name” 的参数。
handlers: HandlersChain 类型,存储了当前路由匹配到的所有中间件和处理函数。
index: 当前执行到的中间件或处理函数在 handlers 中的索引。
fullPath: 匹配到的完整路由路径。
应用和路由上下文
engine: 指向 Engine 的指针,即当前 Context 所属的 Gin 应用实例。
params: 一个指向路由参数 Params 的指针,用于内部处理。
skippedNodes: 内部使用,用于优化路由匹配过程。
并发控制和请求数据
mu: sync.RWMutex,保护 Keys 字段的读写操作。
Keys: 为每个请求独有的键值对存储空间,可以在中间件和处理函数间共享数据。
Errors: 存储在处理请求过程中产生的错误信息列表。
内容协商和缓存
Accepted: 手动指定的用于内容协商的格式列表。
queryCache: 缓存了从 Request.URL.Query() 解析出的查询字符串参数。
formCache: 缓存了从 POST、PATCH 或 PUT 请求体解析出的表单数据。
Cookie 和安全
sameSite: http.SameSite 类型,用于设置 SameSite Cookie 属性,这有助于防止 CSRF 攻击。
gin.Context 的主要功能和用法包括:
请求数据处理:gin.Context 提供了多种方法来获取请求的参数,包括 URL 路径参数(:param 和 *param),查询字符串参数(Query),表单值(PostForm),以及 JSON、XML 等格式的请求体数据(BindJSON、BindXML 等)。
响应设置:开发者可以通过 gin.Context 设置 HTTP 响应的状态码、头部(Headers)、以及响应体。gin.Context 支持直接返回 JSON、XML、HTML 等格式的响应体,通过方法如 JSON、XML、HTML 等实现。
中间件管理:gin.Context 允许在处理函数中动态地添加或跳过后续的中间件执行,通过 Next、Abort 或 AbortWithStatus 等方法控制请求的处理流程。
错误处理:gin.Context 提供了 Error 方法,允许在请求处理过程中记录错误信息。这些错误信息可以在后续的中间件或请求处理函数中被检索和处理。
请求/响应上下文:gin.Context 在整个请求处理流程中被传递,作为请求上下文存在。它允许在中间件和处理函数之间共享数据,通过 Set、Get 方法存取上下文中的值。
HandlersChain例子解读:
在这个例子中,我们为 /api 路由组添加了两个中间件:LoggerMiddleware 和 AuthMiddleware。这意味着,对于 /api/users 的所有请求,首先会执行日志中间件记录请求开始,然后执行身份验证中间件检查用户是否已认证。如果用户未认证,请求将被终止,并返回 401 Unauthorized。如果用户已认证,请求最终会到达主处理函数,返回用户列表。
basePath string: 字符串类型,表示该 RouterGroup 的基础路径(Base Path)。所有在该组中注册的路由都会以这个路径为前缀。例如,如果 basePath 是 /api,那么在此组中注册的一个 /users 路径实际上会被解析为 /api/users。
*engine Engine: 指向 Engine 的指针,Engine 是 Gin 应用的核心结构体。这个字段表明了 RouterGroup 与一个 Engine 实例是关联的,通过这种方式,RouterGroup 可以访问 Engine 提供的功能和配置,如注册新的路由、添加中间件等。
root bool: 布尔类型,标识该 RouterGroup 是否是根路由组。在 Gin 中,根路由组是直接与 Engine 实例关联的路由组,而非根路由组则是通过调用根路由组的 Group 方法创建的子路由组。这个字段通常被内部使用,以区分根路由组和其他子路由组。
到这里就是engine第一个字段的解读
继续解读engine的字段
基本HTTP服务配置
RedirectTrailingSlash: 如果为 true,当路径匹配不成功但是去掉或添加尾部斜线后能匹配到时,会自动重定向到正确的路径。
RedirectFixedPath: 如果为 true,当请求的路径没有直接的处理函数时,Gin 会尝试修正路径(比如去掉多余的斜线、进行大小写不敏感的匹配)并重定向到修正后的路径。
HandleMethodNotAllowed: 如果为 true,当请求的方法(GET、POST等)不被允许时,会返回 405 Method Not Allowed 错误。
ForwardedByClientIP: 如果为 true,会尝试从请求头中解析出客户端的真实 IP 地址。
与请求路径和参数解析相关
UseRawPath: 如果为 true,Gin 会使用 url.RawPath 来查找参数。
UnescapePathValues: 如果为 true,路径中的参数值将被解码。
RemoveExtraSlash: 如果为 true,即使 URL 中包含额外的斜线也可以解析参数。
关于请求体的配置
MaxMultipartMemory: 设置解析 multipart/form-data 类型的请求体时允许的最大内存占用量。
高级配置
UseH2C: 如果为 true,启用 h2c 支持。
ContextWithFallback: 如果为 true,在某些情况下允许 Context 使用备用的方法来处理 Deadline、Done、Err 和 Value。
其他重要字段
RemoteIPHeaders: 定义了一组头部字段,用于在启用 ForwardedByClientIP 时解析客户端 IP 地址。
TrustedPlatform: 设置信任的平台,用于处理特定平台下的头部字段。
delims, secureJSONPrefix, HTMLRender, FuncMap: 分别用于模板渲染的定界符、安全 JSON 前缀、HTML 渲染器以及模板函数映射。
allNoRoute, allNoMethod, noRoute, noMethod: 分别用于处理未找到路由和不被允许的方法的处理函数链。
pool: 用于存储 Context 对象的池,优化内存使用。
trees: 存储所有路由的前缀树,用于快速匹配路由。
maxParams, maxSections: 分别用于限制路由参数和路径段的最大数量,提高路由匹配的效率。
trustedProxies, trustedCIDRs: 分别用于存储信任的代理服务器列表和 CIDR,用于解析客户端真实 IP 地址。
现在终于可以看刚开始的例子了:
方法体
defer func() { debugPrintError(err) }()
这行代码使用了 defer 关键字来确保在 Run 方法结束前执行 debugPrintError(err)。这是一个错误处理的模式,用于在方法退出时打印出现的任何错误。
if engine.isUnsafeTrustedProxies() {...
}
这段代码检查是否信任了所有代理,如果是,将打印一个安全警告。这是因为过度信任代理可能导致安全问题,尤其是在解析客户端 IP 地址时.
address := resolveAddress(addr)
调用 resolveAddress 函数解析提供的地址参数 addr。如果 addr 为空,则函数返回默认的监听地址(例如 :8080)。
这个函数也可以做一个解读:
这个函数实际上是gin框架里面的函数,一开始我还以为是net/http里的。
这个函数只是一个辅助函数,用于处理启动服务器时提供的地址参数。如果没有提供参数,这个函数会返回一个默认值比如(:8080),这意味着服务器将监听网络接口上的8080端口。
func resolveAddress(addr []string) string {switch len(addr) {case 0:if port := os.Getenv("PORT"); port != "" {debugPrint("Environment variable PORT=\"%s\"", port)return ":" + port}debugPrint("Environment variable PORT is undefined. Using port :8080 by default")return ":8080"case 1:return addr[0]default:panic("too many parameters")}
}
它的作用是根据提供的参数来解析服务器应当监听的地址。这个函数处理了几种不同的情况,以决定最终的监听地址。以下是对这个函数行为的逐条解读:
我这里一开始有一个疑问的,哪里来的切片,问题出现在这里:
Run(addr …string),看看里面的参数处理
addr …string 在 Go 语言中使用的是一种称为“变参函数”(Variadic Function)的特性,它允许你传递零个或多个 string 类型的参数给函数。这种参数在函数内部被处理为一个 string 类型的切片(slice)。
解释
… 符号:这个符号放在类型之前,表示该函数接受任意数量的该类型的参数。在这个例子中,addr …string 表示 Run 函数可以接受任意数量的 string 参数。
在函数内部
在 Run 函数的内部,addr 会被当作一个 []string 切片来处理。这意味着你可以对它执行所有切片操作,如 len(addr) 来获取传入参数的数量,或通过索引访问各个参数等。
直接举个例子:
不传递任何参数:Run(),这时 addr 作为一个空的 string 切片。
传递一个参数:Run(“:8080”),这时 addr 包含一个元素 “:8080”。
传递多个参数:Run(“:8080”, “:8081”),这时 addr 包含两个元素 “:8080” 和 “:8081”。
总的来说就是会把字符串转切片。它并不是像我之前理解的那样一个一个的加,而是针对的传递多个参数这种情况。
参数
addr []string: 一个字符串切片,包含了可能被用来指定监听地址的参数。
函数逻辑
1.没有提供参数 (len(addr) == 0):
首先,函数会检查环境变量 PORT 是否被设置。如果设置了,函数将使用这个环境变量的值作为端口号,并返回一个地址字符串,格式为 “:” + port,意味着监听所有网络接口上的这个端口。
如果环境变量 PORT 没有被设置,函数会通过打印一条调试信息来通知使用默认的 :8080 端口,并返回 “:8080” 作为监听地址。
提供了一个参数 (len(addr) == 1):
如果 addr 切片中只有一个元素,函数直接返回这个元素作为监听地址。这允许直接通过参数来指定完整的监听地址,例如 “127.0.0.1:8080” 或 “:8080”。
提供了多于一个参数 (len(addr) > 1):
如果 addr 切片中包含多于一个元素,函数将通过 panic 抛出一个错误,提示“too many parameters”。这是为了避免在启动服务器时出现参数上的混淆,确保启动行为的明确性。
debugPrint("Listening and serving HTTP on %s\n", address)
打印一条消息,告知正在监听的 HTTP 地址。
err = http.ListenAndServe(address, engine.Handler())这个函数的内部实现:
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}
使用标准库 net/http 的 ListenAndServe 函数来监听解析出的地址并启动 HTTP 服务器。engine.Handler() 返回一个处理 HTTP 请求的处理器,它将被用于处理所有到达监听地址的 HTTP 请求。
继续解读:
addr: 一个字符串,指定服务器监听的地址和端口。例如,“:8080” 表示监听本机的 8080 端口。
handler: 实现了 http.Handler 接口的对象。http.Handler 是一个接口,要求实现一个方法 ServeHTTP(ResponseWriter, *Request)。这个方法用于处理所有的 HTTP 请求。
*ServeHTTP(ResponseWriter, Request)
是 Go 语言 net/http 包中定义的 http.Handler 接口的唯一方法。任何想要处理 HTTP 请求的对象都需要实现这个接口。这个方法提供了处理 HTTP 请求并生成响应的基础设施。
w http.ResponseWriter: http.ResponseWriter 是一个接口,提供了向客户端发送响应的方法。通过这个接口,你可以写入响应体(Write 方法),设置响应状态码(WriteHeader 方法),以及添加或修改响应头部(Header 方法返回一个可以修改的 http.Header 对象)。
*r http.Request: *http.Request 是一个指向 http.Request 结构体的指针,它包含了这个 HTTP 请求的所有信息,比如 URL、头部、查询参数、表单数据、请求体等。通过这个参数,你可以读取和分析客户端发送的请求。
这个方法的工作流程:
当 HTTP 服务器接收到一个请求时,它会构造一个 http.Request 对象,并找到合适的处理器来处理这个请求。然后,它调用这个处理器的 ServeHTTP 方法,传入一个 http.ResponseWriter 和 *http.Request 作为参数:
读取请求: *使用 r http.Request 来获取请求的详细信息,比如请求的路径、方法、头部、请求体等。
处理请求: 根据请求的内容,执行相应的逻辑,可能会涉及读取数据库、执行计算、调用其他服务等操作。
发送响应: 使用 w http.ResponseWriter 向客户端发送响应。这包括设置响应状态码、响应头部以及写入响应体。
http.ListenAndServe内部实现:
内部首先会创建一个 http.Server 结构体实例,然后调用这个实例的 ListenAndServe 方法来启动服务器。
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
创建 http.Server 实例: 创建一个 http.Server 结构体实例,其中 Addr 字段设置为函数参数提供的地址,Handler 字段设置为处理请求的处理器。
调用 ListenAndServe: 接下来,调用这个 http.Server 实例的 ListenAndServe 方法。这个方法会让服务器开始监听指定的地址,当有 HTTP 请求到达时,使用提供的处理器来处理这些请求。
再次深挖这个函数
func (srv *Server) ListenAndServe() error {if srv.shuttingDown() {return ErrServerClosed}addr := srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp", addr)if err != nil {return err}return srv.Serve(ln)
}
这个函数用于启动一个 HTTP 服务器并监听指定的地址。
func (srv *Server) ListenAndServe() error {
关于函数:
*srv Server: *Server 表示这是 Server 结构体的一个方法,其中 srv 是对当前 Server 实例的引用。
返回类型是 error,如果服务器启动成功并运行,则正常情况下不会返回(因为它会一直运行直到被关闭)。如果启动过程中遇到错误,将返回相应的错误。
函数体
if srv.shuttingDown() {return ErrServerClosed
}
这段代码检查服务器是否已经在关闭过程中。如果是,则返回 ErrServerClosed 错误。这是为了防止在服务器关闭后再次尝试启动它。
addr := srv.Addr
if addr == "" {addr = ":http"
}
这里设置要监听的地址。如果 Server 实例的 Addr 字段为空,将默认使用 “:http”。“:http” 是一个特殊的地址,表示使用 HTTP 默认的端口号(80)监听所有网络接口。
ln, err := net.Listen("tcp", addr)
if err != nil {return err
}
使用 net.Listen 函数尝试监听上面确定的地址。这个函数第一个参数是网络类型 “tcp”,第二个参数是地址。如果监听成功,net.Listen 返回一个 net.Listener 接口的实例 ln,用于接受来自客户端的连接。如果监听失败(例如,地址已被占用),将返回错误。
return srv.Serve(ln)
调用 Server 实例的 Serve 方法,并将之前创建的监听器 ln 作为参数。Serve 方法会启动一个循环,接受客户端的连接请求,并为每个请求启动一个 goroutine 来处理。这个方法通常会一直运行,直到服务器被关闭。如果 Serve 方法因为某些原因返回(通常是监听器遇到错误),那么 ListenAndServe 也会返回相应的错误。
func (srv *Server) Serve(l net.Listener) error {if fn := testHookServerServe; fn != nil {fn(srv, l) // call hook with unwrapped listener}origListener := ll = &onceCloseListener{Listener: l}defer l.Close()if err := srv.setupHTTP2_Serve(); err != nil {return err}if !srv.trackListener(&l, true) {return ErrServerClosed}defer srv.trackListener(&l, false)baseCtx := context.Background()if srv.BaseContext != nil {baseCtx = srv.BaseContext(origListener)if baseCtx == nil {panic("BaseContext returned a nil context")}}var tempDelay time.Duration // how long to sleep on accept failurectx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, err := l.Accept()if err != nil {if srv.shuttingDown() {return ErrServerClosed}if ne, ok := err.(net.Error); ok && ne.Temporary() {if tempDelay == 0 {tempDelay = 5 * time.Millisecond} else {tempDelay *= 2}if max := 1 * time.Second; tempDelay > max {tempDelay = max}srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)time.Sleep(tempDelay)continue}return err}connCtx := ctxif cc := srv.ConnContext; cc != nil {connCtx = cc(connCtx, rw)if connCtx == nil {panic("ConnContext returned nil")}}tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew, runHooks) // before Serve can returngo c.serve(connCtx)}
}
我直接解读:
初始化部分:
if fn := testHookServerServe; fn != nil {fn(srv, l) // call hook with unwrapped listener
}
这部分代码检查是否存在一个名为 testHookServerServe 的测试钩子(一个可能在测试中使用的全局变量)。如果这个钩子被设置了(不为 nil),则会使用当前的服务器实例 (srv) 和监听器 (l) 调用它。这主要用于内部测试,允许在实际处理连接之前拦截和修改服务器的行为。
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
这段代码首先保存原始监听器的引用,然后用一个 onceCloseListener 包装原始监听器,确保它只能被关闭一次。通过 defer 语句确保在 Serve 方法结束时关闭监听器,释放相关资源。
if err := srv.setupHTTP2_Serve(); err != nil {return err
}
这里尝试为服务器设置 HTTP/2 支持。如果设置失败,例如因为环境不支持 HTTP/2,方法会返回错误
if !srv.trackListener(&l, true) {return ErrServerClosed
}
defer srv.trackListener(&l, false)
这段代码在服务器的内部跟踪结构中注册当前的监听器。如果服务器已经关闭,trackListener 会返回 false,并且方法会返回 ErrServerClosed 错误。使用 defer 确保在方法退出时取消对监听器的跟踪。
上下文准备
baseCtx := context.Background()
if srv.BaseContext != nil {baseCtx = srv.BaseContext(origListener)if baseCtx == nil {panic("BaseContext returned a nil context")}
}
这段代码创建了一个基础上下文 baseCtx。如果服务器的 BaseContext 函数被设置了,它会被调用来生成一个针对当前监听器的自定义上下文。如果 BaseContext 返回 nil,则触发 panic,因为预期 BaseContext 应总是返回有效的上下文。
接受连接的循环
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {rw, err := l.Accept()if err != nil {...continue}connCtx := ctxif cc := srv.ConnContext; cc != nil {connCtx = cc(connCtx, rw)if connCtx == nil {panic("ConnContext returned nil")}}tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew, runHooks)go c.serve(connCtx)
}
这是主要的循环,服务器在这里不断接受新的连接。每次尝试接受连接时可能遇到错误,比如因为网络问题导致的临时错误。如果发生了临时错误,服务器会等待一个延迟后再次尝试接受连接。这个延迟会在每次失败后增加,直到达到最大值。
如果接受连接成功,将创建一个新的连接上下文 connCtx,可能通过调用 ConnContext 函数进行定制。然后为每个接受的连接创建一个新的连接对象 (srv.newConn) 并调用其 serve 方法在新的 goroutine 中处理连接。
这个循环是无限的,直到服务器关闭或遇到非临时错误为止。。
返回值是 error 类型,如果服务器正常启动,则返回 nil。如果启动过程中出现错误,如端口被占用,将返回一个错误对象。
最后,方法返回。如果在监听过程中发生错误(例如,地址已被占用),err 将被赋值,并且通过之前 defer 的调用打印出来。
关于处理函数
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}
这个Handler是接口,这个接口实现了一个唯一方法:
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)engine.pool.Put(c)
}
首先解释engine.pool,这个是sync.Pool类型的字段,用于高效的重用对象。以减少垃圾回收(GC)的压力。sync.Pool 是 Go 语言标准库中提供的一种用于存储和重用临时对象的机制,它可以显著减少内存分配和回收的开销,特别是在高并发环境下。
它主要提供两个方法:Get() 和 Put(obj interface{})。sync.Pool 的使用不需要关心内部如何存储对象,只需要知道这两个方法的用途:
Get() interface{}: 从 Pool 中获取一个对象。如果 Pool 中没有可用对象,则会调用 Pool 在初始化时指定的 New 函数来创建一个新对象。
Put(obj interface{}): 将一个对象放回 Pool 中,使其可以被后续的 Get() 调用重用。放入 Pool 的对象应该是可以安全重用的。
Gin 中的用途
在 Gin 框架中,engine.pool 通常用于重用 Context 对象。每个 HTTP 请求都会创建一个 Context 对象,该对象包含了处理该请求所需的所有信息和方法。由于 HTTP 请求非常频繁,不断地创建和销毁 Context 对象会给垃圾回收带来压力,影响性能。
通过使用 sync.Pool 来重用 Context 对象,Gin 可以显著减少内存分配的次数,提高性能。具体做法是:
当 Gin 处理一个新的 HTTP 请求时,会通过 engine.pool.Get() 获取一个 Context 对象。如果 pool 中没有可用的对象,则会创建一个新的 Context 对象。
请求处理完成后,Gin 会在发送响应之前调用 engine.pool.Put(context) 将这个 Context 对象放回 pool 中,使其可以被后续的请求重用。
这种模式是高性能 HTTP 服务器常用的优化技巧之一。
小提示,在gin框架中,上下文Context结构体就是爹。它封装了每个 HTTP 请求的所有相关信息(字段)和处理过程所需的方法。通过 Context 对象,你可以访问请求数据(如参数、头部、体)、控制响应(设置状态码、发送数据)以及调用中间件或路由处理函数。
Request: 指向 http.Request 的指针,包含了原始的 HTTP 请求信息,如 URL、头部、查询参数、表单数据等。Writer: ResponseWriter 类型,用于构造和发送 HTTP 响应。它提供了设置响应状态码、写入响应头部和正文的方法。Params: 路由参数的集合,允许你通过名称获取动态路由参数的值。handlers: 处理当前请求的 HandlersChain,即一系列的处理函数。Gin 通过它来实现中间件和最终的路由处理函数。index: 当前正在执行的处理函数在 handlers 中的索引,控制着处理函数链的执行过程。Keys: 一个 map[string]interface{} 类型,用于在中间件和处理函数之间传递数据。Errors: 存储在处理请求过程中发生的错误。请求数据处理
Context 提供了多种方法来获取请求的数据:Query: 获取 URL 的查询参数。
DefaultQuery: 获取查询参数,如果指定的参数不存在,则返回默认值。
PostForm: 获取表单数据。
Param: 获取动态路由参数。
BindJSON: 将请求体中的 JSON 数据绑定到一个 Go struct。
响应设置
通过 Context,你可以轻松地设置响应数据和状态:JSON: 发送 JSON 格式的响应。
HTML: 发送 HTML 格式的响应。
Status: 设置响应的 HTTP 状态码。
Header: 设置响应头部。
中间件和路由处理
Next: 调用此方法会继续执行下一个处理函数。
Abort: 停止调用链中剩余的处理函数,通常用于中间件中条件不满足时提前终止请求处理。
现在看这个源码就是砍瓜切菜。
然后继续往下走,最重要的就是下面这个请求处理函数,用于处理每个HTTP请求:
engine.handleHTTPRequest©
追一下源码:
func (engine *Engine) handleHTTPRequest(c *Context) {httpMethod := c.Request.MethodrPath := c.Request.URL.Pathunescape := falseif engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues}if engine.RemoveExtraSlash {rPath = cleanPath(rPath)}// Find root of the tree for the given HTTP methodt := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}if httpMethod != http.MethodConnect && rPath != "/" {if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}}break}if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method == httpMethod {continue}if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {c.handlers = engine.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)
}
下面逐段解读:
请求路径和解码设置
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {rPath = c.Request.URL.RawPathunescape = engine.UnescapePathValues
}if engine.RemoveExtraSlash {rPath = cleanPath(rPath)
}
获取请求方法和路径:首先,代码获取了请求的 HTTP 方法和路径(.Method 和 .Path)。
选择使用 RawPath:如果 engine.UseRawPath 被设置为 true 且 RawPath 不为空,那么将使用 RawPath 作为请求路径,这可能是因为开发者希望处理特定的编码情况。
unescape:作用是指示在路由匹配和参数提取过程中是否需要对 URL 路径进行解码。尽管在提供的代码段中没有直接看到 unescape 的使用,但它在实际的路由匹配和参数处理逻辑中起到了关键作用,特别是在处理那些需要根据配置决定是否解码路径参数的场景中。
路径解码设置:如果选择使用 RawPath,根据 engine.UnescapePathValues 决定是否对路径参数进行解码。
移除多余的斜杠:如果 engine.RemoveExtraSlash 被设置为 true,那么将调用 cleanPath(rPath) 函数来移除路径中的多余斜杠。
如果看的不舒服,说明这些字段要学习一下:
c.Request.Method
c.Request 是一个 *http.Request 对象,它是 Go 标准库中定义的,表示一个 HTTP 请求。
.Method 字段包含了 HTTP 请求的方法(如 “GET”、“POST” 等)。
c.Request.URL.Path
c.Request.URL 是一个 *url.URL 对象,代表解析后的 URL。
.Path 字段包含了 URL 的路径部分,这是经过解码的路径,比如 /user/john。
c**.Request.URL.RawPath**
.RawPath 也是 *url.URL 对象的字段,它包含未经解码的原始路径。这在处理某些特殊字符时很有用,因为解码后的路径可能与原始路径不完全相同。
engine.UseRawPath
UseRawPath 是 Engine 结构体的一个字段,表示是否应该使用 RawPath 而不是 Path 来获取请求的路径。这个字段允许开发者根据需要选择使用原始路径还是解码后的路径。
engine.UnescapePathValues
UnescapePathValues 是 Engine 结构体的另一个字段,当设置为 true 时,表示在路由匹配和参数提取过程中,应该对路径中的百分号编码(URL 编码)的值进行解码。这对于需要在路径参数中使用特殊字符的情况很有用。
engine.RemoveExtraSlash
RemoveExtraSlash 是 Engine 结构体的字段,指示 Gin 在处理请求路径时是否应该移除多余的斜杠。例如,将 //user//john/ 处理为 /user/john。
路由匹配
//通过engine.trees拿到所有的路由树,这个切片装了所有的路由树。
t := engine.trees
//然后开始遍历所有的路由树
for i, tl := 0, len(t); i < tl; i++ {//先匹配路由树的方法,因为路由树是按不同方法就是不同的路由树if t[i].method != httpMethod {//不对的话就跳过,因为方法都对不上那就显然不是这颗树。就可以跳过了。continue}//这里就是匹配成功了,然后把这颗路由树的根节点取出来。root := t[i].root//这个方法就是实现在当前路由树种查找与请求rpath匹配的路由。这个方法返回了一个结构体。包含与匹配路由相关的参数: (params)、处理函数 (handlers) 和完整路径 (fullPath)。//关于这个函数的参数://第一个就是要进行查找的路由//c.params 用于存放从路径中解析出的参数。//c.skippedNodes 是用于内部路由匹配优化的。//unescape 指示是否需要对路径参数进行解码。value := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {//如果找到路由包含的参数,则将这些参数赋值给当前请求的Context。c.Params = *value.params}//如果找到匹配的路由并且存在对应的处理函数,则更新当前Context的处理函数链,设置当前请求的完整路径,然后再调用c.Next执行处理函数链。再调用c.writermem.WriteHeaderNow() 立即写入响应头部,然后返回以结束处理流程。if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}...
}
遍历所有的路由树 (engine.trees),查找与当前请求方法匹配的树。
使用请求路径 rPath 来在找到的路由树中查找匹配的路由节点。
如果找到匹配的路由,更新上下文 c 的参数、处理函数链 (handlers) 和完整路径 (fullPath),然后执行处理函数链,并立即写入响应头部。
看不懂就看这个:
engine.trees :
engine.trees 是一个存储了不同 HTTP 方法(如 GET、POST)对应的路由树的切片。每个路由树负责管理一个特定 HTTP 方法的所有路由规则。这种数据结构使得基于请求方法和路径的路由查找变得非常高效。
在 Gin 中,路由树是一种优化的数据结构,用于快速匹配 URL 路径到相应的处理函数。每个树节点可能代表 URL 路径的一部分,并且树中的路径可能包含参数(如 /user/:name 中的 :name)。
它的源码还有,为什么会是这样:
type methodTree struct {method stringroot *node
}type methodTrees []methodTree
methodTree 和 methodTrees 是 Gin 框架用于构造和存储路由信息的数据结构。
methodTree
methodTree 结构体代表了一个特定 HTTP 方法(如 GET、POST)的路由树。这个结构体包含两个字段:
method: 字符串类型,表示 HTTP 方法。这告诉 Gin 这棵树包含的所有路由都是为哪种 HTTP 方法定义的,例如 “GET” 或 “POST”。
root: 指向 node 类型的指针。这个 node 是当前方法路由树的根节点。在这棵树中,每个节点可能代表路由路径的一部分,并且树中的路径可能包含参数(如 /user/:name 中的 :name)。
理解总结:由于不同的方法那就对应着不同的路由树,那么这个切片中每一个元素就是代表了一棵路由树,然后从内部结构体来看,里面两个字段,存了这棵路由树的方法名和路由树的根结点
这种结构使得基于请求方法和路径的高效路由匹配成为可能。每当一个新路由被添加到 Gin 应用中时,它会被插入到相应 HTTP 方法的 methodTree 的路由树中。
然后就是工作流程:
当 Gin 收到一个 HTTP 请求时,它首先检查请求的方法。然后,在 methodTrees 中查找与该方法相匹配的 methodTree。找到后,Gin 使用请求的路径在对应的 methodTree 的路由树中进行匹配,以找到最合适的处理函数。
如果请求的路径与路由树中的某个路由模式匹配,对应的处理函数就会被执行。如果没有找到匹配的路由,Gin 将返回 404 Not Found 错误响应(或者如果配置了处理 405 Method Not Allowed 的逻辑,也可能返回 405)。
里面的c.Next()我也要说说怎么实现的:
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}
}
这个方法的主要作用是控制中间件和处理函数的执行流程。c.Next() 控制着当前请求的处理函数链 (c.handlers) 的执行。这个链就相当于是以恶个数组,然后数组里面存的是函数,在 Gin 中,每个请求都有一个与之关联的处理函数链,这个链表可能包含多个中间件和一个最终的路由处理函数。
递增 c.index:c.index 是一个 int8 类型的字段,表示当前正在执行的处理函数在 c.handlers 中的索引。在调用 Next() 方法时,首先将 c.index 增加 1,以便从下一个处理函数开始执行。
执行处理函数链:for 循环遍历 c.handlers,只要 c.index 的值小于 c.handlers 的长度,就执行当前索引对应的处理函数 c.handlersc.index。每执行完一个处理函数后,c.index 再次增加 1,移动到下一个处理函数。
执行流程控制
当在某个中间件或处理函数中调用 c.Next() 时,Gin 会继续执行当前请求的处理函数链中的下一个处理函数。
与此同时,还有两个函数非常的常用:
判断是否已经终止处理器调用。
func (c *Context) IsAborted() bool {return c.index >= abortIndex
}
这个函数用来终止处理器调用。
func (c *Context) Abort() {c.index = abortIndex
}
可以看到就是检查索引,终止操作时通过把索引直接改成上限。由于到了上限根据c.Next()的代码,到了上限就不会往后执行了。
还有一些相关的方法是终止处理器调用,并设置响应体。
func (c *Context) AbortWithStatus(code int) {c.Status(code)c.Writer.WriteHeaderNow()c.Abort()
}
其实看了内部的终止操作,还是基于C.Abort()来实现终止,这些只不过附加了一些可能用到的功能。
重定向和固定路径
if httpMethod != http.MethodConnect && rPath != "/" {if value.tsr && engine.RedirectTrailingSlash {redirectTrailingSlash(c)return}if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {return}
}
对于非 CONNECT 方法的请求,如果找到的路由节点建议进行尾部斜杠重定向 (value.tsr 为 true) 并且 engine.RedirectTrailingSlash 为 true,则执行重定向。
如果启用了固定路径重定向 (engine.RedirectFixedPath),且存在可以通过修正路径得到的匹配路由,则进行重定向。
处理 405 Method Not Allowed
if engine.HandleMethodNotAllowed {for _, tree := range engine.trees {if tree.method == httpMethod {continue}if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {c.handlers = engine.allNoMethodserveError(c, http.StatusMethodNotAllowed, default405Body)return}}
}
默认处理和 404 Not Found
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
如果没有匹配到任何路由,设置当前上下文的处理函数为全局的未找到路由的处理函数 (engine.allNoRoute),并返回 404 Not Found 错误。
总结,handleHTTPRequest 方法是 Gin 框架用于路由分发和请求处理的核心逻辑。它根据请求的路径和方法查找注册的路由,执行匹配的处理函数,或者根据配置执行重定向、返回 404 Not Found 或 405 Method Not Allowed 错误。
以上就是今天所看的源码,至少了解了这些内容,我个人感觉也没那么难受了。一开始看简直是眼花缭乱。