课程笔记丨《手把手带你写一个 Web 框架》 永久链接
课程笔记丨《手把手带你写一个 Web 框架》 的文章封面

课程笔记

课程笔记丨《手把手带你写一个 Web 框架》

极客时间《手把手带你写一个 Web 框架》课程笔记。

框架"零件"

context

  1. 框架级 Context:可以组合优秀框架(如 gin.Context)的基础上,扩展自己常用的功能函数。
  2. 业务级 Context:针对具体的业务,组合框架 Context,封装更多的业务工具函数,进一步提升效率。
  3. 如果有必要进步提升性能的话,可以使用 sync.Pool 对 Context 进行管理,避免 Context 频繁创建销毁带来的性能损耗。
  4. 灵活使用链路调用,有助于提升代码的清晰度和可扩展性。

路由匹配

Gin 使用 radix tree,尽可能压缩路由的公共前缀,同时使用 indices 加速路由的检索。

中间件

使用洋葱型中间件,可以很方便地进行 AOP 编程,有很大的扩展性。

如 Gin 框架中:

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

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

同时为 IRouter 接口也定义 Group 函数,这样可以进一步提升聚合类的逻辑复用:

// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
	IRoutes
	Group(string, ...HandlerFunc) *RouterGroup
}

在执行过程中,使用 c.Next()c.Abort() 来进行处理器调用或提前退出等逻辑控制:

const abortIndex int8 = math.MaxInt8 >> 1

func (c *Context) Next() {
	c.index++   // 初始化 c.index = -1
	for c.index < int8(len(c.handlers)) {
		if c.handlers[c.index] != nil {
			c.handlers[c.index](c)
		}
		c.index++
	}
}

func (c *Context) Abort() {
	c.index = abortIndex  // 设置为最大值,后面的 next 就会直接返回
}

扩展现有框架

相比于自己从零开始写一个 Web 框架,完全可以站在前人的肩膀上,将流行的、好用的、开源协议允许的框架代码拷贝到自己的仓库中,进行改造升级,从而快速搭建一个功能完善、经过验证、贴合团队需要的强悍 Web 框架。

一切皆服务

按照面向接口编程的理念,将每个模块看成是一个服务,服务的具体实现我们其实并不关心,我们关心的是服务提供的能力,即接口协议。那么框架主体真正要做的事情是什么呢?其实是:定义好每个模块服务的接口协议,规范服务与服务之间的调用,并且管理每个服务的具体实现

所有的服务都去框架主体中注册自身的模块接口协议,其他的服务调用功能模块的时候,并不是直接去这个服务获取实例,而是从框架主体中获取有这个接口协议的服务实例。

容器 container

服务提供接口定义可参考:

// NewInstance is a function that creates a new instance of a service
type NewInstance func(...any) (any, error)

// ServiceProvider is an interface that defines a service provider
type ServiceProvider interface {
	// Register a service provider into the container,
	// whether to initialize the service or not determined by the IsDefer method
	Register(Container) NewInstance
	// Boot the service provider, this method will be called after the container is initialized.
	// Is id recommend to do some initialization work in this method.
	// If returns error, the service initialization will be failed.
	Boot(Container) error
	// IsDefer determines whether the service provider should be deferred.
	// If true, the service provider will be deferred until the first time the service is used.
	IsDefer() bool
	// Params are the parameters which would be passed to the NewInstance function.
	Params(Container) []any
	// Name is a method that returns the unique name of the service provider.
	Name() string
}

容器接口定义可参考:

// Container is a service provider container, provides methods to register and resolve service providers.
type Container interface {
	// Bind binds a service provider into the container,
	// if the service provider is already bound, it would panic.
	Bind(provider ServiceProvider) error
	// IsBind checks if a service provider is bound into the container
	IsBind(key string) bool
	// Make resolves a service provider from the container
	Make(key string) (any, error)
	// MustMake resolves a service provider from the container, if not found, it will panic
	MustMake(key string) any
	// MakeNew creates a new instance of a service provider,
	// it is useful when you need to create a new instance of a service provider
	// and pass some different parameters to the service provider's constructor.
	MakeNew(key string, params ...any) (any, error)
}

接口实现可参考:

// Helps to check if the Container interface is implemented
var _ Container = (*HdwebContainer)(nil)

// HdwebContainer is the default implementation of the Container interface
type HdwebContainer struct {
	// providers is a map of service providers, key is the name of the service provider
	providers map[string]ServiceProvider
	// instances is a map of service instances, key is the name of the service
	instances map[string]any
	// lock is used to protect the container from concurrent access
	lock sync.RWMutex
}

// NewHdwebContainer creates a new HdwebContainer
func NewHdwebContainer() *HdwebContainer {
	return &HdwebContainer{
		providers: make(map[string]ServiceProvider),
		instances: make(map[string]any),
		lock:      sync.RWMutex{},
	}
}

// Bind binds a service provider into the container,
// if the service provider is already bound, '
// it will replace the existing one and return an error.
func (h *HdwebContainer) Bind(provider ServiceProvider) error {
	h.lock.Lock()
	key := provider.Name()
	if _, ok := h.providers[key]; ok {
		h.lock.Unlock()
		panic("service provider already bound: " + key)
	}
	h.providers[key] = provider
	h.lock.Unlock()

	if provider.IsDefer() {
		return nil
	}

	if err := provider.Boot(h); err != nil {
		return err
	}

	params := provider.Params(h)
	method := provider.Register(h)

	instance, err := method(params...)
	if err != nil {
		return err
	}

	h.lock.Lock()
	defer h.lock.Unlock()
	if _, ok := h.instances[key]; ok {
		panic("service provider already resolved: " + key)
	}
	h.instances[key] = instance
	return nil
}

// IsBind checks if a service provider is bound into the container
func (h *HdwebContainer) IsBind(key string) bool {
	return h.findServiceProvider(key) != nil
}

// Make resolves a service provider from the container
func (h *HdwebContainer) Make(key string) (any, error) {
	return h.make(key, nil, false)
}

// MakeNew creates a new instance of a service provider,
// it is useful when you need to create a new instance of a service provider
// and pass some different parameters to the service provider's constructor.
func (h *HdwebContainer) MakeNew(key string, params ...any) (any, error) {
	return h.make(key, params, true)
}

// MustMake resolves a service provider from the container, if not found, it will panic
func (h *HdwebContainer) MustMake(key string) any {
	ins, err := h.make(key, nil, false)
	if err != nil {
		panic(err)
	}
	return ins
}

func (h *HdwebContainer) make(key string, params []any, forceNew bool) (any, error) {
	sp := h.findServiceProvider(key)
	if sp == nil {
		return nil, errors.New("service provider not found: " + key)
	}
	if forceNew {
		return h.newInstance(sp, params)
	}

	if ins := h.getInstance(key); ins != nil {
		return ins, nil
	}

	h.lock.Lock()
	defer h.lock.Unlock()
	if ins, ok := h.instances[key]; ok {
		return ins, nil
	}
	ins, err := h.newInstance(sp, params)
	if err != nil {
		return nil, err
	}
	h.instances[key] = ins
	return ins, nil
}

func (h *HdwebContainer) getInstance(key string) any {
	h.lock.RLock()
	defer h.lock.RUnlock()
	return h.instances[key]
}

func (h *HdwebContainer) newInstance(sp ServiceProvider, params []any) (any, error) {
	if err := sp.Boot(h); err != nil {
		return nil, err
	}
	if params == nil {
		params = sp.Params(h)
	}
	method := sp.Register(h)
	return method(params...)
}

func (h *HdwebContainer) findServiceProvider(key string) ServiceProvider {
	h.lock.RLock()
	defer h.lock.RUnlock()
	return h.providers[key]
}

服务 service provider

  • contract:服务功能接口定义
  • provider:为服务实现 ServiceProvider 接口
  • service:实现服务 contract 功能接口

框架级服务

- contract  # 服务接口定义
	- app.go
	- kernel.go
	- env.go
	- config.go
- provider  # 服务实现
	- app # app 服务
		- provider.go # 实现 Service Provider
		- service.go  # 实现服务接口
	- kernel
		- provider.go
		- service.go

业务级服务

- {root}  # 根目录
	- app # app 目录
	- provider # 通用业务服务
		- user  # user 服务
			- contract.go # user 服务接口定义
			- service.go  # user 服务接口实现
			- provider.go # 为 user 服务实现 Service Provider
		- mail
			- contract.go
			- service.go
			- provider.go

自动化 DRY

在业务开发过程中,对于那些重复性的类模板劳动,可以使用 CLI 命令行工具,或其他自动化工具,来简化这些劳动输出。

这里有 2 个思路,一个是使用 Makefile,一个是使用 CLI(Go 里面可以使用 cobra 框架)。选择的时候可以考虑以下几个点:

  1. 命令变动的频率(二者在这一点区别不大,不过如果变动频率比较低,那 CLI 的劣势就相对可以忽略了)
  2. 命令使用的复杂性(参数越多,则需要越详尽的帮助说明)
  3. 业务逻辑相关性(越相关,则逻辑越复杂,使用代码越好管控)

常见的思路有:

  • 生成项目脚手架(init)
  • 项目启动管理(build、start、stop、restart、update)
  • 服务模版生成(provider list/new)—— 可以结合 survey 做命令行渐进式输入,template 生成模板代码
  • 命令行系列生成(command list/new)
  • 定时任务(cron list/run)
  • swagger 生成(swagger gen)

来自开放网络的回应

引用、喜欢、转发和站外回复会通过 Webmention 回到这里。

接收与展示代码已经就绪,注册正式域名后即可开始收集回应。

评论

评论由 GitHub Discussions 保存和管理。