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

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

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 国际许可协议进行许可。