gin 框架架构及源码解析

gin:gin是一个用golang编写的web框架,它采用了类Martini的api,有更好的性能,速度快40倍,如果你需要非常好的性能,就使用gin吧
功能列表:

  • 鉴权(auth)
  • 上下文(context)、参数绑定(binding)、渲染(render)
  • 中间件(middleware)
  • 路由(routergroup、tree)
  • gin engine(Engine)
    核心文件及目录结构:
gin/
  auth.go # 鉴权
  binding/ # 包含参数绑定的各种实现,比如form表单、header头、query参数,json、xml、toml、yaml、protobuf等格式的body数据解析格式
  context.go # gin自定义的context实现,
  gin.go # gin引擎的核心文件
  routergroup.go # 路由组的实现,不再使用httprouter中的router,自己实现了routergroup
  tree.go # 基于radix tree的数据结构,用于保存路由信息,基于httprouter的实现(https://github.com/julienschmidt/httprouter)
  render/ # 数据渲染相关,包含html、json、text、xml、yaml、protobuf等格式的数据渲染

其他文件及目录

gin/
  deprecated.go # 不推荐使用的内容放到这个文件下
  ginS # gin engine的单例实现,一般不用于线上,在编写脚本时可能用得上
  internal #  内部的一些实现,包含多个json库的封装,如jsoniter、sonic json等,bytes转string、string转bytes的函数封装。比较简单
  debug.go # debug相关
  errors.go # errors相关定义
  logger.go # 日志输出实现
  mode.go # 多种运行模式
  recover.go # 异常恢复的捕捉
  response_writer.go # 封装http ResponseWriter
  utils.go # 工具包

gin架构设计


image.png

接下来分别从RouterGroup、Context、Engine来分析gin的核心源码实现

RouterGroup.go

// 路由组、实现了IRouter接口
type RouterGroup struct {
    // Handlers 存储了调用Use函数的中间件
    Handlers HandlersChain
    // 存储路由组的url前缀
    basePath string
    // 存储了engine的地址,在路由组中可以直接调用engine的addRoute方法和noRoute、noMethod HandlerChain
    engine   *Engine
    root     bool
}
type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}

// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes
    Match([]string, string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    StaticFileFS(string, string, http.FileSystem) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}
// 当我们调用Group返回一个子路由组时,子路由组的Handlers继承了父路由组的所有handlers,basePath 也添加了父路由组的路径。所以父路由组的handlers都会作用于所有的子路由组中
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}
// 将middleware 追加到group的handlers中
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

// 通用方法、下面的Handle、GET、POST等都会调用handle
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 从group中计算basePath + relativePath,返回根路径
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 从group中返回所有middleware和传进来的handlers
    handlers = group.combineHandlers(handlers)
    // 调用engine的addRoute方法,将url和handlers写入httprouter的前缀树中
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

Context.go

type Context struct {
    // 对ResponseWriter的实现
    writermem responseWriter
    // 保留ServeHTTP的Request
    Request   *http.Request
    // 自定义的ResponseWriter接口、组合了http.ResponseWriter、http.Hijacker、http.Flusher接口,
    Writer    ResponseWriter
    // 存储url的restful风格的参数:/index/:name/:id
    Params   Params
    // 存储tree中当前url的handlers链(包含middlewares和业务处理handler)
    handlers HandlersChain
    // 记录Next函数处理Handlers链的位置
    index    int8
    // url的全路径
    fullPath string
    // 保存engine的指针
    engine       *Engine
    // 保存从tree中通过getValue返回的url参数,params的值会赋值给上面的Params
    params       *Params
    skippedNodes *[]skippedNode

    mu sync.RWMutex
    // 存储context上下文数据
    Keys map[string]any

    Errors errorMsgs

    Accepted []string
    // 缓存c.Request.URL.Query()中的值
    queryCache url.Values
    // 缓存c.Request.PostForm中的数据,包含POST、PATCH、PUT方法的body参数
    formCache url.Values

    sameSite http.SameSite
}

从sync.Pool中取出context后,做一些初始化的工作

func (c *Context) reset() {
    c.Writer = &c.writermem
    c.Params = c.Params[:0]
    c.handlers = nil
    c.index = -1

    c.fullPath = ""
    c.Keys = nil
    c.Errors = c.Errors[:0]
    c.Accepted = nil
    c.queryCache = nil
    c.formCache = nil
    c.sameSite = 0
    *c.params = (*c.params)[:0]
    *c.skippedNodes = (*c.skippedNodes)[:0]
}

由于context不是线程安全的,如果要在多个协程中使用context,可以调用封装好的Copy函数

func (c *Context) Copy() *Context {}

Next函数用于递归调用Middleware,先执行所有Middleware函数调用Next()之前的部分,再执行业务逻辑的Handler,最后采用先进后出的方式,依次执行所有Middleware函数调用Next()之后的内容。

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++ // 这儿的index++,主要是为了解决,如果在Middleware中没有调用Next()函数,能够保证程序能够继续执行,而不是死循环
    }
}
// 将index设置为abortIndex,终止后续middleware和业务handler的执行
func (c *Context) Abort() {
    c.index = abortIndex
}

Key相关的函数

// 设置key,keys用于存储上下文数据。如果调用ShouldBindBodyWith方法,也会把body整个内容缓存为一个key/value存储到keys中
func (c *Context) Set(key string, value any) {}
func (c *Context) Get(key string) (value any, exists bool) {}
// 从Key中获取对应的string类型的值,其他类型的实现类似,不额外介绍
func (c *Context) GetString(key string) (s string) {
    if val, ok := c.Get(key); ok && val != nil {
        s, _ = val.(string)
    }
    return
}
func (c *Context) GetBool(key string) (b bool) {}
func (c *Context) GetInt(key string) (i int) {}
func (c *Context) GetInt64(key string) (i64 int64) {}
func (c *Context) GetUint(key string) (ui uint) {}
func (c *Context) GetUint64(key string) (ui64 uint64) {}
func (c *Context) GetFloat64(key string) (f64 float64) {}
func (c *Context) GetTime(key string) (t time.Time) {}
func (c *Context) GetDuration(key string) (d time.Duration) {}
func (c *Context) GetStringSlice(key string) (ss []string) {}
func (c *Context) GetStringMap(key string) (sm map[string]any) {}
func (c *Context) GetStringMapString(key string) (sms map[string]string) {}
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {}

Param 相关函数

// 获取Param 参数,Param参数从路由tree中解析出来的,如果路由参数中有多个相同key的数据,只会返回第一个值
func (c *Context) Param(key string) string {
    return c.Params.ByName(key)
}
// 也可以手动设置param参数的值
func (c *Context) AddParam(key, value string) {
    c.Params = append(c.Params, Param{Key: key, Value: value})
}

Query参数相关函数

// 如果存在,从url query中返回值,否则返回"",是c.Request.URL.Query().Get(key)的简写
// 
//      GET /path?id=1234&name=Manu&value=
//         c.Query("id") == "1234"
//         c.Query("name") == "Manu"
//         c.Query("value") == ""
//         c.Query("wtf") == ""
func (c *Context) Query(key string) (value string) {
    value, _ = c.GetQuery(key)
    return
}

// 带默认值的Query
func (c *Context) DefaultQuery(key, defaultValue string) string {
    if value, ok := c.GetQuery(key); ok {
        return value
    }
    return defaultValue
}

// GetQuery 和 Query()类似, 多一个bool返回值,是Query()函数的底层调用
// 
//  GET /?name=Manu&lastname=
//  ("Manu", true) == c.GetQuery("name")
//  ("", false) == c.GetQuery("id")
//  ("", true) == c.GetQuery("lastname")
func (c *Context) GetQuery(key string) (string, bool) {
    if values, ok := c.GetQueryArray(key); ok {
        return values[0], ok
    }
    return "", false
}

// QueryArray 返回[]string
func (c *Context) QueryArray(key string) (values []string) {
    values, _ = c.GetQueryArray(key)
    return
}
// 初始化query缓存
func (c *Context) initQueryCache() {
    if c.queryCache == nil {
        if c.Request != nil {
            c.queryCache = c.Request.URL.Query()
        } else {
            c.queryCache = url.Values{}
        }
    }
}

// GetQueryArray 返回值带[]string和bool
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
    c.initQueryCache() // 初始化query cache
    values, ok = c.queryCache[key]
    return
}

// QueryMap 返回字典值
func (c *Context) QueryMap(key string) (dicts map[string]string) {
    dicts, _ = c.GetQueryMap(key)
    return
}

// GetQueryMap 返回bool和字段值
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
    c.initQueryCache() // 初始化query cache
    return c.get(c.queryCache, key) // 返回map格式数据
}

// PostForm 从post中返回key
func (c *Context) PostForm(key string) (value string) {
    value, _ = c.GetPostForm(key)
    return
}

// DefaultPostForm 返回带默认值的key
func (c *Context) DefaultPostForm(key, defaultValue string) string {
    if value, ok := c.GetPostForm(key); ok {
        return value
    }
    return defaultValue
}

// GetPostForm 返回key值和bool
func (c *Context) GetPostForm(key string) (string, bool) {
    if values, ok := c.GetPostFormArray(key); ok {
        return values[0], ok
    }
    return "", false
}

// PostFormArray 返回切片类型的值
func (c *Context) PostFormArray(key string) (values []string) {
    values, _ = c.GetPostFormArray(key)
    return
}

// 初始化form 缓存
func (c *Context) initFormCache() {
    if c.formCache == nil {
        c.formCache = make(url.Values)
        req := c.Request
        if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            if !errors.Is(err, http.ErrNotMultipart) {
                debugPrint("error on parse multipart form array: %v", err)
            }
        }
        c.formCache = req.PostForm
    }
}

// GetPostFormArray 返回切片类型值和bool
func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
    c.initFormCache()
    values, ok = c.formCache[key]
    return
}

// PostFormMap 返回字典类型的值
func (c *Context) PostFormMap(key string) (dicts map[string]string) {
    dicts, _ = c.GetPostFormMap(key)
    return
}

// GetPostFormMap 返回字典类型的值和bool
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
    c.initFormCache()
    return c.get(c.formCache, key)
}

// 
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
    dicts := make(map[string]string)
    exist := false
    for k, v := range m {
        if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
            if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
                exist = true
                dicts[k[i+1:][:j]] = v[0]
            }
        }
    }
    return dicts, exist
}

Query参数Binding

MustBind在绑定出错时返回400错误
Bind在绑定出错时不会返回错误

// Bind 会通过请求方法和content-type来判断使用哪个类型的bind
func (c *Context) Bind(obj any) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.MustBindWith(obj, b)
}

// BindJSON 使用json解析body数据
func (c *Context) BindJSON(obj any) error {
    return c.MustBindWith(obj, binding.JSON)
}
// 同上
func (c *Context) BindXML(obj any) error {}
func (c *Context) BindQuery(obj any) error {}
func (c *Context) BindYAML(obj any) error {}
func (c *Context) BindTOML(obj any) error {}

// BindHeader 绑定header数据到obj对象
func (c *Context) BindHeader(obj any) error {
    return c.MustBindWith(obj, binding.Header)
}

// BindUri 将uri参数绑定到obj
func (c *Context) BindUri(obj any) error {
    if err := c.ShouldBindUri(obj); err != nil {
        c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
        return err
    }
    return nil
}

// MustBindWith 使用提供的binding将数据绑定到obj,如果绑定出错,返回400错误
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    if err := c.ShouldBindWith(obj, b); err != nil {
        c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
        return err
    }
    return nil
}

// ShouldBind 
func (c *Context) ShouldBind(obj any) error {
    b := binding.Default(c.Request.Method, c.ContentType())
    return c.ShouldBindWith(obj, b)
}

// ShouldBindJSON 
func (c *Context) ShouldBindJSON(obj any) error {
    return c.ShouldBindWith(obj, binding.JSON)
}
func (c *Context) ShouldBindXML(obj any) error {}
func (c *Context) ShouldBindQuery(obj any) error {}
func (c *Context) ShouldBindYAML(obj any) error {}
func (c *Context) ShouldBindTOML(obj any) error {}
func (c *Context) ShouldBindHeader(obj any) error {}
func (c *Context) ShouldBindUri(obj any) error {}

// ShouldBindWith 使用提供的binding将request数据绑定到obj,出现错误不会返回400
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
    return b.Bind(c.Request, obj)
}

// ShouldBindBodyWith 与 ShouldBindWith 类似, 但会body中的数据缓存到context的Keys中,供下次调用重用
// 注意: 该方法在绑定之前读取body数据。所以如果只调用一次的话,使用ShouldBindWith会有更好的性能,
func (c *Context) ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error) {
    var body []byte
    if cb, ok := c.Get(BodyBytesKey); ok {
        if cbb, ok := cb.([]byte); ok {
            body = cbb
        }
    }
    if body == nil {
        body, err = io.ReadAll(c.Request.Body)
        if err != nil {
            return err
        }
        c.Set(BodyBytesKey, body)
    }
    return bb.BindBody(body, obj)
}

Cookie相关函数

// 读写Cookie
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {}
func (c *Context) Cookie(name string) (string, error) {}

Render渲染相关函数

// Render 写入响应头和调用Render来渲染数据
func (c *Context) Render(code int, r render.Render) {
    c.Status(code)

    if !bodyAllowedForStatus(code) {
        r.WriteContentType(c.Writer)
        c.Writer.WriteHeaderNow()
        return
    }

    if err := r.Render(c.Writer); err != nil {
        // Pushing error to c.Errors
        _ = c.Error(err)
        c.Abort()
    }
}

// 使用http模板渲染数据,内部统一调用Render函数来渲染数据
func (c *Context) HTML(code int, name string, obj any) {} 
func (c *Context) IndentedJSON(code int, obj any) {} // 使用智能json
func (c *Context) SecureJSON(code int, obj any) {}
func (c *Context) JSONP(code int, obj any) {}
func (c *Context) JSON(code int, obj any) {}
func (c *Context) AsciiJSON(code int, obj any) {}
func (c *Context) PureJSON(code int, obj any) {}
func (c *Context) XML(code int, obj any) {}
func (c *Context) YAML(code int, obj any) {}
func (c *Context) TOML(code int, obj any) {}
func (c *Context) ProtoBuf(code int, obj any) {}
func (c *Context) String(code int, format string, values ...any) {}
// 重定向
func (c *Context) Redirect(code int, location string) {}

Negotiate

Negotiate主要用于根据客户端提供的格式,协商返回对应格式的数据

Engine.go

// HandlerFunc 定义了中间件和业务handler的函数
type HandlerFunc func(*Context)

// HandlersChain Handlers链,底层为handler的切片
type HandlersChain []HandlerFunc

// gin Engine
type Engine struct {
    // 组合了RouterGroup,所以可在engine中可以直接调用Group()、GET()、POST()等方法
    RouterGroup

    // 启用自动重定向,如果当前路由无法匹配,但存在带(不带)尾部斜杠的路径处理程序。
    // 例如,如果请求 /foo/ 但仅存在 /foo 的路由,则对于 GET 请求,客户端将重定向到 /foo,HTTP 状态代码为 301
    // 和 307 用于所有其他请求方法。
    RedirectTrailingSlash bool

    // 启用路径修复,如果没有已为其注册的handler,则路由器尝试修复当前请求路径。
    // 首先删除多余的路径元素,例如 ../ 或 // 。
    // 然后路由器对清理后的路径进行不区分大小写的查找。
    // 如果可以找到该路由的句柄,则路由器进行重定向到正确的路径,
    // GET 请求的状态代码为 301, 所有其他请求方法返回的状态代码为 307 。
    // 例如 /FOO 和 /..//Foo 可以重定向到 /foo。
    // RedirectTrailingSlash 与此选项没有关系。
    RedirectFixedPath bool

    // 如果启用,如果当前请求无法路由,路由器将检查是否允许使用其他方法
    // 如果是这种情况,则请求将得到“不允许的方法”的响应和 HTTP 405 状态码。
    // 如果没有其他方法可路由,则返回 NotFound Handler
    HandleMethodNotAllowed bool
    UseRawPath bool
    UnescapePathValues bool
    // 移出额外的斜杠
    RemoveExtraSlash bool
    // 限制http.Request的 ParseMultipartForm 的maxMemory参数值
    MaxMultipartMemory int64
    // UseH2C 开启http2协议,使用http2client.
    UseH2C bool
    delims           render.Delims
    secureJSONPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    // 存储context的pool
    pool             sync.Pool
    // 使用压缩前缀树(radix tree) 存储url和handler的对应关系
    trees            methodTrees
    maxParams        uint16
    maxSections      uint16
    trustedProxies   []string
    trustedCIDRs     []*net.IPNet
}
// 验证Engine实现了IRouter
var _ IRouter = (*Engine)(nil)
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        // ...
    }
    engine.RouterGroup.engine = engine
    // 配置pool 生成临时对象的New函数
    engine.pool.New = func() any {
        return engine.allocateContext(engine.maxParams)
    }
    return engine
}

// Default 调用New函数初始化Engine,添加了Logger和Recover中间件
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}
// context临时对象的生成方法,在New函数中调用
func (engine *Engine) allocateContext(maxParams uint16) *Context {
    v := make(Params, 0, maxParams)
    skippedNodes := make([]skippedNode, 0, engine.maxSections)
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}

// 将中间件添加到RouterGroup的Handler链中
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

// 通过method、path和handler,将路由信息添加到tree中
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")

    debugPrintRoute(method, path, handlers)

    root := engine.trees.get(method)
    // root为空,就生成新的root节点
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    // 添加路由到tree中、tree中保存了多个压缩前缀树结构(每个请求方法一个)
    root.addRoute(path, handlers)

    // Update maxParams
    if paramsCount := countParams(path); paramsCount > engine.maxParams {
        engine.maxParams = paramsCount
    }

    if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
        engine.maxSections = sectionsCount
    }
}

// 调用http.ListenAndServe 启动服务,使用不同的模式启动服务
func (engine *Engine) Run(addr ...string) (err error) {}
func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {}
func (engine *Engine) RunUnix(file string) (err error) {}
func (engine *Engine) RunFd(fd int) (err error) {}
func (engine *Engine) RunListener(listener net.Listener) (err error) {}

// 实现了http.Handler接口,请求到达服务端后,会将请求交给ServeHTTP函数来处理
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从pool中获取一个context
    c := engine.pool.Get().(*Context)
    // 初始化ResponseWriter、Requet
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    // handleHTTPRequest处理实际的请求
    engine.handleHTTPRequest(c)

    // 将context放回pool
    engine.pool.Put(c)
}

// 处理具体的请求
func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // Find root of the tree for the given HTTP method
    t := engine.trees
    // 从trees中根据请求方法类型获取对应的tree
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // 从tree中获取handlers,并返回handlers和params
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        // handlers不为空,调用Next()方法递归调用middleware和业处理handler
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.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
    }
    // 如果开启了HandleMethodNotAllowed,就继续遍历tree,在其他tree中去匹配路由,如果匹配到后,就返回405错误
    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.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    // 返回404错误
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

到这里,gin的核心模块context、routergroup、engine的实现就分析完了,radix tree的实现可以参考httprouter。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容