分类 Golang 下的文章

这是一个很经典的面试题,例如现在面试官让你写一个超时控制函数,你是否有注意到内存泄露?

package main

import (
    "context"
    "fmt"
    "time"
)

func job() error {
    ctx, _ := context.WithTimeout(context.Background(), time.Second)
    done := make(chan struct{})
    go func() {
        //耗时操作
        time.Sleep(time.Millisecond * 1200)
        done <- struct{}{}
    }()
    select {
    case <-ctx.Done():
        return fmt.Errorf("time out!")
    case <-done:
        return nil
    }
}

func main() {
    fmt.Println(job())
}

如果你的代码是这样写的,那么恭喜你,内存泄露了!

我们修改一下 main 函数:

func main() {
    for i := 0; i <= 50; i++ {
        go func() {
            job()
        }()
    }
    for {
        time.Sleep(time.Second * 2)
        fmt.Println(runtime.NumGoroutine())
    }

}

执行一下再看看,效果很神奇。

2023-04-10T21:31:47.png

它会一直打印 52,证明我们上面开的 50 个协程一直没有被释放。

2023-04-10T21:31:57.png

问题其实就出在我画的圈里面,因为每一个程序都超时了,那么就租到了 ←ctx.Done() 这个 case 中去,Job 就算是完成了,但是我们的 ←done 就无法取值了, done ← strcut{}{} 就会被阻塞在这里了。这个协程就永远结束不了。

解决办法就是给 done 一个长度:

done := make(chan struct{}, 1)

2023-04-10T21:32:11.png

只要协程能够往下走,job 中的协程就会被 gc。

这个系列的文章来自于,我们以前在团队内部的分享,用来做微服务体系架构设计的学习使用,一开始是放在 github 上的,近期有时间,就打算整理一下,顺便将这段时间的学习和沉淀融入进去,可能会很庞大,所以就先列出来一个目录。

Golang 基础篇

如果不喜欢看视频的话推荐看这个系列的文章 : https://www.liwenzhou.com/posts/Go/golang-menu/

作为入门还是非常不错的。

微服务概念篇

Web 框架篇

RPC 篇

  1. 网络编程
  2. RPC 的实现
  3. 注册中心
  4. etcd 实现注册中心
  5. etcd 实现配置中心

服务治理篇

  1. 可用性
  2. 可观测行

DevOPS 篇

Goridge 是 SpiralsCout 开源的高性能的 PHP 到 Golang 编解码器库。该库可以帮助你在 PHP 调用 Go 的服务方法。

Goridge 入门

使用方法:

我们先来编写简单的 Go 程序,目的就是提供一个 rpc 接口给 PHP 进行访问。

GO111MODULE=on go get github.com/roadrunner-server/goridge/v3

package main

import (
    "fmt"
    "net"
    "net/rpc"

    goridgeRpc "github.com/roadrunner-server/goridge/v3/pkg/rpc"
)

type App struct{}

func (s *App) Hi(name string, r *string) error {
    *r = fmt.Sprintf("Hello, %s!", name)
    return nil
}

func main() {
    ln, err := net.Listen("tcp", ":6001")
    if err != nil {
        panic(err)
    }

    _ = rpc.Register(new(App))

    for {
        conn, err := ln.Accept()
        if err != nil {
            continue
        }
        _ = conn
        go rpc.ServeCodec(goridgeRpc.NewCodec(conn))
    }
}

Hi 函数用来接受一个 name 入参。

# 进入工程陌路
cd project
# 安装 spiral/goridge sdk
composer install spiral/goridge
# 编写 index.php
vim index.php

<?php

use Spiral\Goridge\RPC\RPC;
use Spiral\Goridge\SocketRelay;

include 'vendor/autoload.php';

$rpc1 = new RPC(new SocketRelay("127.0.0.1", 6001));
echo $rpc1->call("App.Hi", "Goridge RPC");

2023-03-08T12:31:55.png

PHP 访问 Go 原生 RPC 的方法

在 2019 年,我们的团队开始引入 Golang,原因是想通过 Golang 良好的并发性来提升我们的应用访问速度,但是正常情况下公司是不会给研发团队大片的时间去进行重构,这个时候我们就需要 PHP 和 Go 进行通讯。

不过那个时候我们并没有使用 Goridge,而是直接用 PHP 访问 Go RPC。现在我们来模拟一下当时的场景,还是用 Goridge 的那个例子,其实实现思路大体上是一致的,不过我们并没有写自己的编解码,而是利用了 jsonrpc。

package main

import (
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
)

type App struct{}

func (s *App) Hi(name string, r *string) error {
    *r = fmt.Sprintf("Hello, %s!", name)
    return nil
}

func main() {
    rpc := rpc.NewServer()
    app := new(App)
    rpc.Register(app)
    ln, err := net.Listen("tcp", ":6001")
    defer ln.Close()
    if err != nil {
        panic(err)
    }
    for {
        conn, err := ln.Accept()
        if err != nil {
            panic(err)
        }
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

在 go 中我,我们使用 jsonrpc 作为解码器,这样一来就是可以通过 socket 传递 json 的方式进行访问 go 的 rpc 接口了。

2023-03-08T13:16:34.png

如果对 go rpc 不是特别了解的话,可以通过 https://colobu.com/2016/09/18/go-net-rpc-guide/ 这篇文章了解一下,该文的作者是 rpcx 的作者鸟窝,是 Golang 界的一位大牛。

Web 核心 Server

在 《Golang 主流 Web 框架路由实现分析》中我们注意到对于一个 Web 框架来说,至少提供了三个抽象:

  • 代表服务器的吃哦续爱那个,我们这里称之为 Server
  • 代表上下文的抽象,我们这里称之为 Context
  • 路由树

从前面框架对比来看,对于一个 Web 框架来说,我们首先要有一个整体代表服务器的抽象,也就是 Server。

Server 从特性上来说,至少提供了三个功能:

  1. 生命周期管理:即启动、关闭。如果在后期,我们还要考虑增加生命周期回调特性
  2. 路由注册接口:提供路由注册功能
  3. 作为http包到 web 框架的桥梁

http.Handler接口

http 暴露了一个接口,Handler,它是我们引入自定义 Web 框架的相关连接点,自定义 Server 有两种实现方案:

方案 1. 只组合 http.Handler。

type Server interface {
    http.Handler
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)
}

优点:

  1. 用户可在使用的饿时候只需要调用 Http.ListenAndServe 就可以
  2. 和 HTTPS 协议完全无缝衔接
  3. 极简设计

缺点

  1. 难以控制生命周期,并且在控制生命周期的时候增加回调支持
  2. 缺乏控制力:如果将来希望支持优雅退出的功能,将会难以支持

方案 2.组合 http.Handler 并增加 Start 方法

type Server interface {
    http.Handler
    Start(addr string) error
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)

  s.Start(":8881")
}

优点:

  1. Server 既可以当成普通的 http.Handler 来使用,又可以作为一个独立的实体,拥有自己的管理生命周期能力
  2. 完全的控制

缺点:

  1. 如果用户不希望使用 ListenAndServeTLS,那么 Server 需要提供 HTTPS 的支持

这两个实现方案都直接耦合了 Go 自带的 http 包,如果我们希望切换为 fasthttp 或者类似的 http 包,则会非常困难。

HTTPServer 实现

// 确保 HTTPServer 实现了 Server 接口
var _ Server = &HTTPServer {}

type HTTPServer struct {

}

func (s * HttpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {

}

func (s * HttpServer) Start (addr string) error {
        return http.ListenAndServe(addr, s)
}

func Serve(l net.Listener, handler Handler) error {
        srv := &Server{Handler: handler}
        return srv.Serve(l)
}

该实现直接使用 http.ListenAndServe 来启动,后续可以根据需要替换为:

  • 内部创建 http.Server 来启动
  • 使用 http.Serve 来启动,环球聚更大得我灵活性,比如讲端口监听和服务器启动分离等呢个

ServeHTTP 则是我们整个 Web 框架的核心入口,我们将在整个方法内部完成:

  • Context 构建
  • 路由匹配
  • 执行业务逻辑

注册路由

暂时我们可以考虑站在用户的角度,考虑如何注册路由,在《Golang 主流 Web 框架路由实现分析》文章中,详细介绍了Beego、Gin 等框架的路由注册实现,我们可以参考一下。

大致上分为两类方法:

  • 针对任意的方法的:如 Gin 和 Iris 的 Handle 方法,Echo 的 Add 方法。
  • 针对不同 HTTP 方法的:如 Get、Post、Delete,这类方法基本很伤都是委托给前一类方法

所以实际上,核心方法只需要一个,例如 Handle。其他的方法都建立在这上面,在这里我们使用 AddRoute 作为核心方法,其函数只要接受三个参数。

  • method:请求方法例如 Get、Put、Post
  • path:请求路径
  • handleFunc: 业务逻辑

type HandleFunc func()

type Server interface {
    http.Handler
    Start(addr string) error

    AddRouter(method string, path string, handleFunc HandleFunc)
}

这里我们需要思考一下:

  • AddRoute 方法只接受一个 HandleFunc,因为我希望它只注册业务逻辑。即便真有多个场景,用户可以自己组合成一个。
  • 如果允许注册多个,那么在实现的时候需要考虑,其中一个失败了,是否还允许继续执行下去;反过来,如果其中一个 HandleFunc 中药终端执行,怎么中断?
  • 这里我们采用新的名字 AddRoute,我认为更加贴合方法本意,Handle 看上去是处理什么东西,而是实质上这里只是注册路由。

其他框架都是允许注册多个的,但是其实用起来体验不会很好

  • Gin 和 Iris 最后一个是不定参数,那么完全可以一个都不传,如 PUT(“path”),这个在编译器无法发现
  • Echo 则是存在我希望 h 传入 nil 的可能。实际上 Echo 是讲中间件真粗额逻辑和路由逻辑合并在一起了。

回到我们设计的 AddRoute 的设计上,这种设计思路很常用,针对不同的 HTTP 方法的注册 API,我们都可以委托给 Handle 方法,也就是我们的 AddRoute。

func (s *HttpServer) Post(path string, handler handleFunc) {
    s.AddRoute(http.MethodPost, path, handler);
}

Context 设计

ServeHTTP 方法作为 http 包与 web 框架的关联点,需要在 ServeHTTP 内部,执行:

  • 构建起Web 框架的杀好难过i行啊问
  • 查找路由树,并执行命中路由的代码

Beego 路由实现分析

Beego 的设计初衷是为了提供一个简单易用的 web 框架,同时也保证了高性能和可扩展性。它的设计理念是基于 MVC 模式,在 Go 最早期的时候算是最流行的框架之一。

下面的代码是 Beego 最简单的使用方法。

type UserController struct {
    web.Controller
}

func (c* UserController) GetUser() {
    c.Ctx.WriteString("hello maksim")
}

func main() {
    web.BConfig.CopyReqeustBody = true
    web.Router("/user", c, "get:GetUser")
    web.Run(":8080")
}

这是一段非常经典的基于 MVC 的代码结构。

web.Router 用来注册路由,当我们访问 /user 路径的时候,就去访问 UserController 中的 GetUser 方法,其中 ”get:User“中的 get 就是 http方法,如果是 Get 就执行,如果是 Post 就无法响应请求。

Controller 抽象

Beego 是基于 MVC(Model-View-Controller)的,所以它定义了一个核心接口ControllerInterface,在这个接口中我们可以看到很多和 MVC 相关的概念。

// ControllerInterface is an interface to uniform all controller handler.
type ControllerInterface interface {
    Init(ct *context.Context, controllerName, actionName string, app interface{})
    Prepare()
    Get()
    Post()
    Delete()
    Put()
    Head()
    Patch()
    Options()
    Trace()
    Finish()
    Render() error
    XSRFToken() string
    CheckXSRFCookie() bool
    HandlerFunc(fn string) bool
    URLMapping()
}

ControllerInterface定义了一个控制器必须要解决什么问题。同时 ContollerInterface 的默认实现 Controller 提供了实现自定义控制器的各种辅助方法,所以在 Beego里面,一般都是组合 Controller 来实现自己的Controller,例如案例中的代码那样:

type UserController struct {
    web.Controller
}

在 Beego 中路由注册和服务启动时通过另外一套机制实现的(HTTPServer + ControllerRegister),Controller 理解为只是帮助我们组织代码。

HTTPServer

ControllerInterface 可以看做是核心接口,因为它直接提现了 Beego 的设计初衷:MVC 模式。同时它也是用户核心切入点。但是如果从功能特性上来说 HTTPServer 和 ControllerRegister 才是核心。

HttpServer:代表一个”服务器“,大多数时候它就是一个进程,其结构体定义如下:

// HttpServer defines beego application with a new PatternServeMux.
type HttpServer struct {
    Handlers           *ControllerRegister
    Server             *http.Server
    Cfg                *Config
    LifeCycleCallbacks []LifeCycleCallback
}

我们可以看到 HttpServer 中其实就包含了我们的 http.Server ,这个结构体相信你不会陌生,在这里就不做赘述了,其实这里的 HttpServer 也起到了资源隔离的作用。

路由树

Beego 的核心结构体是三个: ControllerRegister、Tree,leafinfo

ControllerRegister:真正干活的人,注册路由,路由匹配和执行业务代码都是通过它来完成的,它类似容器,放着所有的路由树,路由树是按照 HTTP mthod来组织的,例如 Get 方法会对应有一颗路由树。

// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
    routers      map[string]*Tree
    enablePolicy bool
    enableFilter bool
    policies     map[string]*Tree
    filters      [FinishRouter + 1][]*FilterRouter
    pool         sync.Pool

    // the filter created by FilterChain
    chainRoot *FilterRouter

    // keep registered chain and build it when serve http
    filterChains []filterChainConfig

    cfg *Config
}

Tree:它代表 的就是路由树,在 Beego 里面,一颗路由树倍看做是由子树组成的。

// Tree has three elements: FixRouter/wildcard/leaves
// fixRouter stores Fixed Router
// wildcard stores params
// leaves store the endpoint information
type Tree struct {
    // prefix set for static router
    prefix string
    // search fix route first
    fixrouters []*Tree
    // if set, failure to match fixrouters search then search wildcard
    wildcard *Tree
    // if set, failure to match wildcard search
    leaves []*leafInfo
}

leafinfo:代表叶子结点的,结构如下

type leafInfo struct {
    // names of wildcards that lead to this leaf. eg, ["id" "name"] for the wildcard ":id" and ":name"
    wildcards []string

    // if the leaf is regexp
    regexps *regexp.Regexp

    runObject interface{}
}

Beego 的树定义,并没有采用 children 式的定义,而是采用递归式的定义,即一棵树是由根节点 + 子树构成。

2023-04-06T07:26:24.png

Context 抽象

用户操作请求和响应都是通过 Ctx 来达成的。它代表的是整个请求执行过程的上下文。

// Context Http request context struct including BeegoInput, BeegoOutput, http.Request and http.ResponseWriter.
// BeegoInput and BeegoOutput provides an api to operate request and response more easily.
type Context struct {
    Input          *BeegoInput
    Output         *BeegoOutput
    Request        *http.Request
    ResponseWriter *Response
    _xsrfToken     string
}

进一步,Beego 将 Context 细分了几个部分:

  • Input: 定义了很多和处理请求有关的方法
  • Outpu:定义了很多和响应有关的方法
  • Response:对 http.RequestWriter的二次封装

核心抽象总结

  • ControllerRegister 最为基础,它解决了路由注册和路由匹这个基础问题。
  • Context 和 Controller 位用户提供了丰富的 API,用于辅助构建系统。
  • HttpServer 作为服务器抽象,用于管理应用生命周期和资源隔离单位。

Gin 路由实现分析

Gin 框架可以说是目前国内最流行的 API 框架了,Gin没有Controller的抽象,对于如何组织我们的代码结构,和编码方式全都交给用户去决定。

其最简单的使用方法如下:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

如果我们想用 MVC 架构去组织我们的代码结构我们可以这样:

package main

import "github.com/gin-gonic/gin"

type UserController struct {

}

func (c * UserController) GetUser(ctx *gin.Context) {
    ctx.String(200, "hello world")
}

func main() {
    r := gin.Default()
    ctrl := &UserController{}
    g.Get("/user", ctrl.GetUser)
    r.Run() 

IRoutes 接口

核心接口 IRoutes:提供的是注册路由的抽象。它的实现 Engine类似于ControllerRegister。

// 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

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}
  • Use方法提供了用户接入自定义逻辑的能力,这个一般情况下也被看做是插件机制。
  • StaticFile,Static,StaticFcs 额外提供了静态文件的接口。

Engine 实现

type Engine struct {
    ...
    
    trees    methodTrees
}

Engine 可以看做是 Beego 中 HttpServerControllerRegister 的合体。

  • 实现了路由树功能,提供了注册和匹配路由的功能
  • 它本身可以作为一个Handler传递到http包,用于启动服务器

路由树

Gin 的路由树的关键结构体设计的更加直观:methodTrees,methodTree,Node。

methodTreeEngine 中的一个结构体,Engine的路由树功能本质上是依赖于 methodTree 实现路由树的功能,methodTree 定义了单棵树,数在 Gin 里面采用的是 children 定义方式,即树由节点构成,定义如下:

type methodTree struct {
    method string
    root   *node
}

其中,method 代表 HTTP 方法,root 则是存储路由的根节点。

methodTrees 它实际上代表的是森林,即每一个 HTTP 方法都对应到一树上,例如 GET 会有一颗路由树,这一点和 Beego 的设计很类似。

type methodTrees []methodTree

node:代表树上的一个节点,里面维持了 children,即子节点。同时有 ndoeType 和 wildChild 来标记一些特殊节点。

type nodeType uint8

const (
    static nodeType = iota // default
    root
    param
    catchAll
)

type node struct {
    path      string
    indices   string
    children  []*node
    handlers  HandlersChain
    priority  uint32
    nType     nodeType
    maxParams uint8
    wildChild bool
    fullPath  string
}

Gin 是利用路由的公共前缀来构造路由树的。

2023-04-06T07:59:04.png

HandlerFunc 和 HandlersChain

HandlerFunc 定义了核心抽象——处理逻辑,它是一个函数类型,它接受一个 Context 参数并返回 void。这个函数的目的是处理请求并生成响应。

HandlersChain 是一个由 HandlerFunc 组成的切片类型,它的目的是让我们能够组合多个处理程序函数,这样的数据结构构造了一个责任链模式。

Gin 中,一个 HandlerFunc 可以调用 context.Next() 来转移控制到链中的下一个处理程序。这使得我们可以在多个处理程序之间共享状态,以及在处理程序之间传递控制。

Untitled

最后一个则是封装了业务逻辑的 HandlerFunc。

Context 抽象

Context 也是代表了执行的上下文,提供了丰富的 API:

  • 处理请求的 API,代表以 Get 和 Bind 为前缀的方法
  • 处理响应的 API,代表返回 Json 或者 XML 相应的方法
  • 渲染界面,如 HTML 方法
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter

    Params   Params
    handlers HandlersChain
    index    int8
    fullPath string
}

Untitled

Iris是一款Go语言编写的高性能Web框架,相对于 gin 在国内用的要稍微少一点,它的路由实现是基于httprouter的。Iris的路由功能强大,支持多种请求方法、正则表达式匹配和中间件等特性。

func main() {
    app := iris.New()

    app.Get("/", func (ctx iris.Context){
            _, _ = ctx.HTML("Hello <strong> %s </strong>", "World!")
    }) 

    _ = app.Listen(":8083")
}

Iris 路由实现分析

Application

Application 是 Iris 的核心抽象,它代表的是“应用”。实际上这个语义更接近 Beego 的 HttpServer 和 Gin 的 Engine。

// Application is responsible to manage the state of the application.
// It contains and handles all the necessary parts to create a fast web server.
type Application struct {

    *router.APIBuilder
    *router.Router
    router.HTTPErrorHandler 
    ContextPool             *context.Pool
    
    contextErrorHandler context.ErrorHandler

    config *Configuration

    // the golog logger instance, defaults to "Info" level messages (all except "Debug")
    logger *golog.Logger

    I18n *i18n.I18n

    // Validator is the request body validator, defaults to nil.
    Validator context.Validator
    minifier *minify.M

    view *view.View

    builded     bool
    defaultMode bool

    OnBuild func() error

    mu sync.Mutex

    name string

    Hosts             []*host.Supervisor
    hostConfigurators []host.Configurator
}

它提供了:

  • 生命周期控制功能,如 Shutdown 等方法
  • 注册路由的 API

路由相关

Iris 的设计非常复杂。在 Beego 和 Gin 里面能够明显看到路由树的审计,但是在 Iris 里面就很很难看出来。

在Iris中,路由的注册是通过HandleHandleFunc方法实现的。Handle方法接收一个iris.Handler类型的参数,而HandleFunc方法接收一个func(c iris.Context)类型的参数。这两个方法内部都会调用Party方法和addRoute方法完成路由的注册。

Party方法用于创建一个子路由分组,并返回一个新的Party对象。addRoute方法则将路由信息保存到路由表中,其中包括请求方法、路径、处理函数和中间件等信息。Iris的路由表是基于httprouter实现的,使用了Trie树和Radix树结构进行快速匹配。

处理路由相关的三个抽象:

  • Route:直接代表了已注册的路由。在 Beego 和 Gin 里面,对应的是路由树的节点。
  • APIBuilder:创建 Route 的 Builder 模式,Party 也是它创建的。
  • repository:存储了所有的 Routes 了,有点接近 Gin 的 methodTrees 的概念

    type repository struct {
        routes []*Route
        paths  map[string]*Route // only the fullname path part, required at CreateRoutes for registering index page.
    }

Context 抽象

Context 也是代表上下文。

Context 本身也是提供了各种处理请求和响应的方法。

基本上和 Beego 和 Gin 的 Context 没啥区别,比较有特色的是它的 Context 支持请求级别的添加 Handler,即 AddHandler 方法。

Echo 路由分析

Echo

Echo 是它内部的一个结构体,类似与 Beego 的 HttpServer 和 Gin 的 Engine

  • 暴露了注册路由的方法,但是它并不是路由树的载体
  • 生命周期管理:如 Shutdown 和 Start 等方法

在 Echo 里面有两个相似的字段:

  • router: 这其实就是代表路由树
  • routers:这代表的是根据 Host 进行分组组织,可以看到是近似于namespace 类似的概念,既是一种组织方式,也是一种隔离机制。

Route 和 node

Router代表的就是路由树,node 代表的是路由树上的节点。

node 里面有一个很有意思的设计:staticChildern、paramChild 和 anyChild。利用这个设计可以轻松实现路由优先级和路由冲突检测。

它里面还有一个字段叫做 echo,维护的事使用 Route 的 Echo 实例。这种设计形态在别的地方也能见到,比如说在 sql.Tx里维护了一个 sql.DB 的实例。

Context

一个大而全的接口,定义类处理请求和响应的各种方法。和 Beego、Gin、Iris 的 Contex 没有什么区别。

框架的对比

BeegoGinIrisEcho
代表服务器HttpServerEngineApplicationEcho
代表路由ControllerRegisterMethodThreeRouteRoute
上下文ContextContextContextContext
处理逻辑HanderFuncHanderFuncHanderFuncHanderFunc

路由树的总结

  • 归根结底就是设计一颗多叉树
  • 同时我们按照 HTTP 方式来组织路由树,每个 HTTP 方法一棵树
  • 节点维护住自己的子节点

参考资料

  1. 极客时间 - Golnag 实战训练营
  2. 慕课网 - Golang 高级工程师
  3. Gin 官网