
课程笔记
课程笔记丨《手把手带你写一个 Web 框架》
极客时间《手把手带你写一个 Web 框架》课程笔记。
框架"零件"
context
- 框架级 Context:可以组合优秀框架(如 gin.Context)的基础上,扩展自己常用的功能函数。
- 业务级 Context:针对具体的业务,组合框架 Context,封装更多的业务工具函数,进一步提升效率。
- 如果有必要进步提升性能的话,可以使用 sync.Pool 对 Context 进行管理,避免 Context 频繁创建销毁带来的性能损耗。
- 灵活使用链路调用,有助于提升代码的清晰度和可扩展性。
路由匹配
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 框架)。选择的时候可以考虑以下几个点:
- 命令变动的频率(二者在这一点区别不大,不过如果变动频率比较低,那 CLI 的劣势就相对可以忽略了)
- 命令使用的复杂性(参数越多,则需要越详尽的帮助说明)
- 业务逻辑相关性(越相关,则逻辑越复杂,使用代码越好管控)
常见的思路有:
- 生成项目脚手架(init)
- 项目启动管理(build、start、stop、restart、update)
- 服务模版生成(provider list/new)—— 可以结合
survey做命令行渐进式输入,template生成模板代码 - 命令行系列生成(command list/new)
- 定时任务(cron list/run)
- swagger 生成(swagger gen)
来自开放网络的回应
引用、喜欢、转发和站外回复会通过 Webmention 回到这里。
接收与展示代码已经就绪,注册正式域名后即可开始收集回应。
评论
评论由 GitHub Discussions 保存和管理。