gin源码学习-项目启动(1)
最近公司后台项目也在用golang的gin框架
进行重构,从gin官方给出的性能对比数据来看,性能十分优秀,而且干净整洁,少量代码就可以起一个web项目,简直不要太爽,目前github上的star数量已经65k(截至2022-12)
,十分亮眼。
已经有一定实践经验之后,这下来看看gin的具体实现,一来巩固,二来学习优秀开源框架的一些思想。
后面的源码学习与gin的使用,初步计划从这个方面入手:
1.项目quickstart
代码:
package main
import (
"gin-any/api" // 项目内的handlerfunc
"github.com/gin-gonic/gin"
)
func main() {
// 1.新建engine引擎
engine := gin.Default()
// 2.注册路由
engine.GET("/hello", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "hello gin!",
})
})
engine.POST("/login", api.Login)
// 注册路由,同时加入中间件
engine.GET("/user", api.AuthJWTMiddleware(), api.User)
// 3.启动项目
engine.Run(":8080")
}
我们看到gin项目的server仅需要简单几步,即可起来web server,接下来分别从代码中的3个步骤说明gin项目启动。
首先了解几个重要概念:
- engine:总的引擎,保存各个组件信息
- RouterGroup:路由组,保存路由信息
- engine.pool:sync.pool,复用context
- tree:radix tree或者认为是字典树,保存url与handler的映射关系
- context:请求与响应,用于request传递
1.gin.Default()
调用流程
源码
// gin-gonic/gin/gin.go
func Default() *Engine {
debugPrintWARNINGDefault() // 打印日志
engine := New() // 新建engine
engine.Use(Logger(), Recovery()) // 默认加入log/recovery中间件
return engine
}
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{ // 新建engine实例
RouterGroup: RouterGroup{ // RouterGroup
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.RouterGroup.engine = engine // 初始化RouterGroup
engine.pool.New = func() any { // 初始化pool
return engine.allocateContext()
}
return engine
}
// 加入中间件
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain // type HandlersChain []HandlerFunc
basePath string
engine *Engine
root bool
}
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...) // 加入到handlers的切片中, group.Handlers是组handlers或是全局的handlers
return group.returnObj()
}
从源码中可以看到,这一步的主要工作就是新建engine实例,初始化RouterGroup和pool,顺带把默认的log和recovery中间件注册进来。
在测试的时候,通过gin.Default()新建engine是没问题的,但如果你读过net/http的源码就会发现,这里会有问题?
什么问题呢?很多第三方的库其实也是基于net/http库构建的,其中很多也是用到了Servemux的默认设置,这里就是问题,所以这里就引出另外一个问题,gin.New()
。
gin.New()
同样先来看看demo:
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.New()
server := http.Server{
Addr: ":8080",
// called handler, if nil default http.DefaultServeMux
Handler: engine,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
engine.GET("/ping", pong)
err := server.ListenAndServe()
if err != nil {
log.Fatalln("server start failed:", err)
}
}
func pong(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"msg": "pong",
})
}
我们知道gin是基于net/http来构建自己的service,那就通过gin.New()构建自定义的gin server,中间件就是自己定义了,而且我们还可以设置handler,超时时间等,当然其他的和gin.Default()没啥不一样的了。
2.engine.Get() | engine.Post()等
调用流程
源码
// gin-gonic/gin/routerGroup.go
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { // 注册路由
return group.handle(http.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
// 根据传入的handlers,比如可能只是添加url的处理函数,或者添加中间件+url的处理函数,新建个handlerfuncs,
// 将原有的gorup.handlers copy过去,然后加添加的handlers加到尾部
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers) // 根据method/path/handlers加入到路由
return group.returnObj()
}
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)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
...
debugPrintRoute(method, path, handlers)
root := engine.trees.get(method) // 根据method尝试获取root节点
if root == nil { // root节点为空,新建root节点
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root}) // 加入到radix tree中
}
root.addRoute(path, handlers) // adds a node with the given handle to the path.
// Update maxParams
...
}
从源码中我们可以看到,基本上可以认为,结合method/path/handlers(加入了组内的handlers,即中间件),将这些信息加入到节点中,即完成路由的注册。
可以理解为,每添加一个路由,对应路由的handlers最终都是加入了组内的或者是全局的中间件的handlers,故后面request过来的时候递归调用,即中间件1--->中间件2--->视图函数--->中间件2--->中间件1。
3.engine.Run()
调用流程
源码
// go-gonic/gin/gin.go
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) { // Run方法
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()) // 最终调用http包的函数起server
return
}
// net/http/server.go
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr) // 监听服务
if err != nil {
return err
}
return srv.Serve(ln) //处理连接
}
// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error { // 每来一个新请求,起个go程处理请求
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
origListener := l
l = &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 failure
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
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 := ctx
if cc := srv.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
从这段源码中可以看到,其实gin的server也是简单封装了标准库中的net/http中的相关函数。
4.gin处理请求
调用流程
将请求流转串连起来的调用流程:
源码
// gin-gonic/gin/gin.go
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context) // 从pool中取出context
c.writermem.reset(w) // 重置
c.Request = req // req赋给context的Request
c.reset()
engine.handleHTTPRequest(c) // 处理具体的context,ctx中有request
engine.pool.Put(c) // 用完放回去
}
func (engine *Engine) handleHTTPRequest(c *Context) {
...
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root // 取出root
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil { // 处理请求
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next() // 实际请求处理
c.writermem.WriteHeaderNow()
return
}
...
}
...
}
// gin-gonic/gin/context.go
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) { // 遍历handlers,依次处理ctx
c.handlers[c.index](c)
c.index++
}
}
从源码中可以看到,每次处理请求时,从pool中取出context,重置后遍历handlers调用,处理完请求然后放回context,达到复用context的目的。
借用下简书-LinkinStar:
总结
基于gin的学习,在engine/context/pool/tree做以下总结:
-
engine
web server实例,包含复用器、中间件、配置,作为容器对象,是gin框架的基础。 -
context
gin将请求request和响应response都封装在context中,通过传递context指针,实现请求的处理,算是golang的特性。 -
pool
利用pool来重复利用对象,因为请求很多,所以会产生很多数量的context,利用sync.pool进行复用,从而减少内存的分配也提高了效率,值得学习。 -
tree
gin采用radix tree(字典树的扩展)的数据结构存储、查找相关信息,最早还以为只是使用map结构,后面随着经验增加,觉得map底层用的也是array,总有容量问题,就会有扩容问题,当然有没有可能用其他数据结构,使得其更加高效呢?
tree代码的结构图:
上述内容基于http的简单应用,https也都差不多,大体流程熟悉了,后面的适当修改即可。
源码解析不易,如果觉得有用欢迎转载。
本文参考文档: