Go猜想录
大道至简,悟者天成
Web框架 -- 概览

Beego

小案例

package main

import "github.com/beego/beego/v2/server/web"

func main() {
	web.BConfig.CopyRequestBody = true
	c := &UserController{}
	web.Router("/user", c, "get:GetUser")
	web.Router("/user", c, "post:CreateUser")
	web.Run(":8080")
}

type UserController struct {
	web.Controller
}

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

func (c *UserController) CreateUser() {
	u := &User{}
	err := c.Ctx.BindJSON(u)
	if err != nil {
		c.Ctx.WriteString(err.Error())
		return
	}

	c.Ctx.JSONResp(u)
}

type User struct {
	Name string
}

Controller 抽象

Beego 是基于 MVC (Model-View-Controller) 的,所以它定义了一个核心接口 ControllerInterface。ControllerInterface 定义了一个控制器必须要解决什么问题。

同时 ControllerInterface 的默认实现 Controller 提供了实现自定义控制器的各种辅助方法,所以在 Beego 里面, 一般都是组合 Controller 来实现自己的 Controller。

// 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()
}

注意到用户虽然被要求组合 Controller,但是路由注册和服务器启动是通过另外一套机制来完成的。

func main() {
	web.BConfig.CopyRequestBody = true
	c := &UserController{}
	web.Router("/user", c, "get:GetUser")
	web.Router("/user", c, "post:CreateUser")
	web.Run(":8081")
}

HttpServer 和 ControllerRegister

ControllerInterface 可以看做核心接口,因为它直接体现了 Beego 的设计初衷:MVC 模式。同时它也是用户核心接入点。

但是如果从功能特性上来说,HttpServer 和 ControllerRegister 才是核心。

  • HttpServer:代表一个服务器,大多数时候它就是一个进程。
  • ControllerRegister:真正干活的人。注册路由,路由匹配和执行业务代码都是透过它来完成的。
// HttpServer defines beego application with a new PatternServeMux.
type HttpServer struct {
	Handlers           *ControllerRegister
	Server             *http.Server
	Cfg                *Config
	LifeCycleCallbacks []LifeCycleCallback
}

// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
    ...
 }

Context 抽象

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

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

  • Input:定义了很多和处理请求有关的方法
  • Output:定义了很多和响应有关的方法
  • Response:对 http.ResponseWriter 的二次封装
// 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
}
func (c *UserController) CreateUser() {
	u := &User{}
	err := c.Ctx.BindJSON(u)
	if err != nil {
		c.Ctx.WriteString(err.Error())
		return
	}

	_ = c.Ctx.JSONResp(u)
}
// Controller defines some basic http request handler operations, such as
// http context, template and view, session and xsrf.
type Controller struct {
	// context data
	Ctx  *context.Context
	Data map[interface{}]interface{}
	...
}

路由树实现

Beego 的核心结构体是三个:

  • ControllerRegister:类似于容器,放着所有的路由树
    • 路由树是按照 HTTP method 来组织的, 例如 GET 方法会对应有一棵路由树
  • Tree:它代表的就是路由树,在 Beego 里面,一棵路由树被看做是由子树组成的
  • leafInfo:代表叶子节点
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
	routers      map[string]*Tree
	enablePolicy bool
	enableFilter bool
	...
}

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

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 式的定义,而是采用递归式的定义,即一棵树是由根节点+子树构成。

Untitled.png

抽象总结

ControllerRegister 最为基础,它解决了路由注册和路由匹配这个基础问题。

Context 和 Controller 为用户提供了丰富 API,用于辅助构建系统。

HttpServer 作为服务器抽象,用于管理应用生命周期和资源隔离单位。

  • ControllerRegister
    • Context
      • input
      • output
    • Controller
    • HttpServer

Gin

小案例

用户主动用的 MVC 模式

package main

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

func main() {
	g := gin.Default()
	ctrl := &UserController{}
	g.GET("/user", ctrl.GetUser)
	apiG := g.Group("/api")
	v1Api := apiG.Group("/v1")
	v1Api.GET("/test", func(context *gin.Context) {
		context.String(200, "hello, world, v1")
	})
	g.Run(":8080")
}

type UserController struct {
}

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

IRoutes 接口

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

Use 方法提供了用户接入自定义逻辑的能力,这个一般情况下也被看做是插件机制。

还额外提供了静态文件的接口。

Gin 没有 Controller 的抽象。所以在这方面我和 Gin 的倾向比较一致,即 MVC 应该是用户组织 Web 项目的模式, 而不是我们中间件设计者要考虑的。

// 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
	StaticFileFS(string, string, http.FileSystem) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

Engine 实现

Engine 可以看做是 Beego 中 HttpServer 和 ControllerRegister 的合体。

  • 实现了路由树功能,提供了注册和匹配路由的功能
  • 它本身可以作为一个 Handler 传递到 http 包,用于启动服务器
// 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) {
	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
}

Engine 的路由树功能本质上是依赖于 methodTree 的。

methodTrees 和 methodTree

methodTree 才是真实的路由树。

Gin 定义了 methodTrees,它实际上代表的是森林,即每一个 HTTP 方法都对应到一棵树。

type Engine struct {
    ...
    noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	// Engine 字段
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
    ...
}
type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree

HandlerFunc 和 HandlersChain

HandlerFunc 定义了核心抽象 —— 处理逻辑。

在默认情况下,它代表了注册路由的业务代码。

HandlersChain 则是构造了责任链模式。

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc

Untitled 2.png

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

Context 抽象

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

  • 处理请求的 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 3.png

路由树实现

Gin 的关键结构体更加直观:

  • methodTrees:也就是路由树也是按照 HTTP 方法组织的,例如 GET 会有一棵路由树
  • methodTree:定义了单棵树。树在 Gin 里面采用的是 children 的定义方式,即树由节点构成(注意对比 Beego)
  • node:代表树上的一个节点,里面维持住了 children,即子节点。同时有 nodeType 和 wildChild 来标记一些特殊节点

Untitled 1.png

抽象总结

  • methodTree & node
    • HandlerFunc
    • Context
    • Engine

Iris

小案例

package main

import (
	"github.com/kataras/iris/v12"
)

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

Application

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

它提供了:

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

路由相关

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

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

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

总结:设计过于复杂,职责不清晰,不符合一般人的直觉,新人学习和维护门槛高。

// repository passed to all parties(subrouters), it's the object witch keeps
// all the routes.
type repository struct {
	routes []*Route
	pos    map[string]int
}
// Route contains the information about a registered Route.
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
	Name       string         `json:"name"`   // "userRoute"
	Method     string         `json:"method"` // "GET"

Context 抽象

Context 也是代表上下文。

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

基本上和 Beego 和 Gin 的 Context 没啥区别。

比较有特色的是它的 Context 支持请求级别的添加 Handler,即 AddHandler 方法。

// AddHandler can add handler(s)
// to the current request in serve-time,
// these handlers are not persistenced to the router.
//
// Router is calling this function to add the route's handler.
// If AddHandler called then the handlers will be inserted
// to the end of the already-defined route's handler.
func (ctx *Context) AddHandler(handlers ...Handler) {
	ctx.handlers = append(ctx.handlers, handlers...)
}

抽象总结

  • Route Party APIBuilder
    • Handler
    • Context
    • Application

Echo

小案例

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	// Echo instance
	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routes
	e.GET("/", hello)

	// Start server
	e.Logger.Fatal(e.Start(":8080"))
}

// Handle
func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}

Echo

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

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

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

  • Route:这其实就是代表路由树
  • Routers:这代表的是根据 Host 来进行分组组织,可以看做是近似于 namespace 之类的概念,既是一种组织方式,也是一种隔离机制
        maxParam         *int
		router           *Router
		routers          map[string]*Router
		pool             sync.Pool

Route 和 node

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

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

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

    // Router is the registry of all registered routes for an `Echo` instance for
    // request matching and URL path parameter parsing.
	Router struct {
		tree   *node
		routes map[string]*Route
		echo   *Echo
	}
    node struct {
		kind           kind
		label          byte
		prefix         string
		parent         *node
		staticChildren children
		originalPath   string
		methods        *routeMethods
		paramChild     *node
		anyChild       *node
		paramsCount    int
		// isLeaf indicates that node does not have child routes

Context

一个大而全的接口,定义了处理请求和响应的各种方法。

和 Beego、Gin、Iris 的 Context 没有什么区别。

核心抽象

  • Route & node
    • HandlerFunc
    • Context
    • Echo

框架对比

Untitled 10.png

所以实际上我们要造一个 Web 框架,就是要建立我们自己的这几个抽象。

Web 框架小结

  • Web 框架拿来做什么?处理 HTTP 请求,为用户提供便捷 API,为用户提供无侵入式的插件机制,提供如上传下载等默认功能
  • 为什么都已经有了 http 包,还要开发 Web 框架?高级路由功能、封装 HTTP 上下文以提供简单 API、封装 Server 以提供生命周期控制、设计插件机制以提供无侵入式解决方案
  • Web 框架的核心?路由树、上下文 Context、 Server(按照我理解的重要性排序)

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。