七叶笔记 » golang编程 » OOP 思想在 TCC/APIX/GORM 源码中的应用

OOP 思想在 TCC/APIX/GORM 源码中的应用

名词解释

OOP

面向对象程序 设计(Object Oriented Programming,OOP)是一种计算机编程架构。OOP 的一条基本原则是计算机程序由单个能够起到子程序作用的单元或对象组合而成。OOP 达到了软件工程的三个主要目标:重用性、灵活性和扩展性。面向对象编程的三大特点:封装性、继承性和多态性。

TCC

动态配置中心 TCC(ToutiaoConfigCenter)是提供给业务方的一套平台+SDK 的配置管理解决方案,提供的功能有权限管理、配置管理、版本管理、 灰度发布 、多地区多环境支持等。与 百度 开源的“百度分布式配置中心 BRCC”功能类似。

APIX

Golang 实现的 web 框架,可参考开源项目 Gin。

GORM

Golang 编写的热门数据库 ORM 框架。

背景

大力智能学习灯 于 2019 年 10 月份上线,截止 2021 年底,台灯出货量已超过 100w 台,完成了从 0 到 1 的探索。在成立之初,很多方向的产品为了尽早拿到用户反馈,要求快速迭代,研发在代码实现上相对快糙猛,早期阶段这无可厚非,但慢慢地,自习室、系统工具、知识宇宙等应用已经变成灯上核心基建,如果还按之前的野蛮生长的方式将会为台灯的成长埋下隐患。

在这样的背景下,大力智能服务端推动 OOP 技术专项的落地,希望能够:提升团队成员自身的 编码 水平;统一团队内部编程风格;支撑业务快速迭代。

TCC、APIX、GORM 都是日常项目中经常会依赖到的外部包,本文从这些项目的源码出发,在学习的过程中,解读良好的代码设计在其中的应用,希望能帮忙大家更好的理解和应用 OOP 思想,写出更优秀的代码。

OOP 原则

单一职责原则 ( SRP )

一个类只负责一个职责(功能模块)。

开放封闭原则 (OCP)

一个类、方法或模块的扩展性要保持开放,可扩展但不影响源代码(封闭式更改)

替换原则(LSP)

子类可以替换父类,并且不会导致程序错误。

接口隔离原则(ISP)

一个类对另一个类的依赖应该建立在最小的接口上。

依赖倒置原则 (DIP)

高层次的模块不应该依赖于低层次的模块,它们应该依赖于抽象。

参数可选,开箱即用—函数式选项模式

解决问题 :在设计一个函数时,当存在配置参数较多,同时参数可选时,函数式选项模式是一个很好的选择,它既有为不熟悉的调用者准备好的默认配置,还有为需要定制的调用者提供自由修改配置的能力,且支持未来灵活扩展属性。

TCC 在创建 B config Client 对象时使用了该模式。 BConfigClient 是用于发送 http 请求获取后端服务中 key 对应的 value 值,其中 getoptions 结构体是 BConfigClient 的配置类,包含请求的 cluster 、addr、auth 等信息,小写开头,属于内部结构体,不允许外部直接创建和修改,但同时对外提供了 GetOption 的方法去修改 getoptions 中的属性,其中 With Cluster WithAddr WithAuth 是快捷生成 GetOption 的函数。

这样的方式很好地控制了哪些属性能被外部修改,哪些是不行的。当 getoptions 需要增加新属性时,给定一个默认值,对应增加一个新 GetOption 方法即可,对于历史调用方来说无感,能向前兼容式的升级,符合 OOP 中的对修改关闭,对扩展开放的开闭设计原则。

 type getoptions  struct  {
   cluster string
   addr    string
   auth     bool 
}

// GetOption represents option of get op
type GetOption func(o *getoptions)

// WithCluster sets cluster of get context
func WithCluster(cluster string) GetOption {
   return func(o *getoptions) {
      o.cluster = cluster
   }
}

// WithAddr sets addr for http  request  instead get from consul
func WithAddr(addr string) GetOption {
   return func(o *getoptions) {
      o.addr = addr
   }
}
// WithAuth Set the GDPR Certify On.
func WithAuth(auth bool) GetOption {
   return func(o *getoptions) {
      o.auth = auth
   }
}
  

NewBConfigClient 方法接受一个可变长度的 GetOption ,意味着调用者可以不用传任何参数,开箱即用,也可以根据自己的需要灵活添加。函数内部首先初始化一个默认配置,然后循环执行 GetOption 方法,将用户定义的操作赋值给默认配置。

 // NewBConfigClient creates instance of BConfigClient
func NewBConfigClient(opts ...GetOption) *BConfigClient {
   oo := getoptions{cluster: defaultCluster}
   for _, op := range opts {
      op(&oo)
   }
   c := &BConfigClient{oo: oo}
   ......
   return c
}
  

通过组合扩展功能—装饰模式

解决问题 :当已有类功能不够便捷时,通过组合的方式实现对已有类的功能扩展,实现了对已有代码的黑盒复用。

TCC 使用了装饰模式扩展了原来已有的 Client V2 的能力。

在下面的 DemotionClient 结构体中组合了 ClientV2 的引用,对外提供了 GetInt GetBool 两个方法,包掉了对原始 string 类型的转换,对外提供了更为便捷的方法。

 // Get 获取key对应的value.
func (c *ClientV2) Get(ctx context.Context, key string) (string, error)
  
 type DemotionClient struct {
   *ClientV2
}

func NewDemotionClient(serviceName string, config *ConfigV2) (*DemotionClient, error) {
   clientV2, err := NewClientV2(serviceName, config)
   if err != nil {
      return nil, err
   }
   client := &DemotionClient{clientV2}
   return client,  nil 
}

// GetInt parse value to int
func (d *DemotionClient) GetInt(ctx context.Context, key string) (int, error) {
   value, err := d.Get(ctx, key)
   if err != nil {
      return 0, err
   }
   ret, err := strconv.Atoi(value)
   if err != nil {
      return 0, fmt.Errorf("GetInt Error: Key = %s; value = %s is not int", key, value)
   }
   return ret, nil
}

// GetBool parse value to bool:
//     if value=="0" return false;
//     if value=="1" return true;
//     if value!="0" && value!="1" return error;
func (d *DemotionClient) GetBool(ctx context.Context, key string) (bool, error) {
   ......
   // 类似GetInt方法
}
  

由于 Golang 语言对嵌入类型的支持, DemotionClient 在扩展能力的同时, ClientV2 的原本方法也能正常调用,这样 语法糖 的设计让组合操作达到了继承的效果,且符合 OOP 中替换原则。

Java 语言对比,如下面的例子,类 A 和类 B 实现了 IHi 的接口,类 C 组合了接口 IHi , 如果需要暴露 IHi 的方法,则类 C 需要添加一个代理方法,这样 java 语言的组合在代码量上会多于继承方式,而 Golang 中无需额外代码即可提供支持。

 public interface IHi {
        public  void  hi();
    }

    public class A implements IHi {
        @Override
        public void hi() {
            System.out.println("Hi, I am A.");
        }
    }

    public class B implements IHi {
        @Override
        public void hi() {
            System.out.println("Hi, I am B.");
        }
    }

    public class C {
        IHello h;

        public void hi() {
            h.hi();
        }
    }

    public  static  void main(String args[]) {
        C c = new C();
        c.h = new A();
        c.hi();
        c.h = new B();
        c.hi();
    }
  

隐藏复杂对象构造过程—工厂模式

解决问题 :将对象复杂的构造逻辑隐藏在内部,调用者不用关心细节,同时集中变化。

TCC 创建 LogCounnter 时使用了工厂模式,该类作用是根据错误日志出现的频率判断是否需要打印日志,如果在指定的时间里,错误日志的触发超过指定次数,则需要记录日志。

NewLogCounter 方法通过入参 LogMode 枚举类型即可生成不同规格配置的 LogCounter ,可以无需再去理解 TriggerLogCount、 Trigger LogDuration、Enable 的含义。

 type LogMode string

 const  (
   LowMode       LogMode = "low"
   MediumMode    LogMode = "medium"
   HighMode      LogMode = "high"
   AlwaysMode    LogMode = "always"
   ForbiddenMode LogMode = "forbidden"
)

// In TriggerLogDuration, if error times < TriggerLogCount pass, else print error log.
type LogCounter struct {
   FirstLogTime       time.Time
   LogCount           int
   mu                 sync.RWMutex
   TriggerLogCount    int
   TriggerLogDuration time.Duration
   Enable             bool // If Enable is true, start the rule.
}

func NewLogCounter(logMode LogMode, triggerLogCount int, triggerLogDuration time.Duration) *LogCounter {
   logCounter := &LogCounter{}
   switch logMode {
   case AlwaysMode:
      logCounter.Enable = false
   case LowMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 5
      logCounter.TriggerLogDuration = 60 * time.Second
   case MediumMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 5
      logCounter.TriggerLogDuration = 300 * time.Second
   case HighMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 3
      logCounter.TriggerLogDuration = 300 * time.Second
   case ForbiddenMode:
      logCounter.Enable = true
      logCounter.TriggerLogCount = 0
   }
   if triggerLogCount > 0 {
      logCounter.Enable = true
      logCounter.TriggerLogCount = triggerLogCount
      logCounter.TriggerLogDuration = triggerLogDuration
   }
   return logCounter
}

func (r *LogCounter) CheckPrintLog() bool
func (r *LogCounter) CheckDiffTime(lastErrorTime, newErrorTime time.Time) bool
  

一步步构建复杂对象—建造者模式

解决问题 :使用多个简单的对象一步一步构建成一个复杂的对象。

APIX 在创建请求的匹配函数 Matcher 时使用了建造者模式。

APIX 中提供了指定对哪些 request 生效的中间件,定义和使用方式如下, CondHandlersChain 结构体中定义了匹配函数 Matcher 和命中后执行的处理函数 HandlersChain

以“对路径前缀为`/wechat` 的请求开启微信认证中间件”为例子,Matcher 函数不用开发者从头实现一个,只需要初始化 SimpleMatcherBuilder 对象,设置请求前缀后,直接 Build 出来即可,它将复杂的匹配逻辑隐藏在内部,非常好用。

 // Conditional handlers chain
type CondHandlersChain struct {
   // 匹配函数
   Matcher func(method, path string) bool
   // 命中匹配后,执行的处理函数
   Chain   HandlersChain
}

// 对路径前缀为 `/wechat` 的请求开启微信认证中间件
mw1 := apix.CondHandlersChain{
   Matcher: new(apix.SimpleMatcherBuilder).PrefixPath("/wechat").Build(),
   Chain:   apix.HandlersChain{wxsession.New Middleware ()},
}

// 注册中间件
e.CondUse(mw1)
  

SimpleMatcherBuilder 是一个建造者,它实现了 MatcherBuilder 接口,该类支持 method、pathPrefix 和 paths 三种匹配方式,业务方通过 Method() PrefixPath() FullPath() 三个方法的组合调用即可构造出期望的匹配函数。

 type MatcherBuilder interface {
   Build() func(method, path string) bool
}

var _ MatcherBuilder = (*SimpleMatcherBuilder)(nil)

// SimpleMatcherBuilder build a matcher for CondHandlersChain.
// An `AND` logic will be applied to all fields(if provided).
type SimpleMatcherBuilder struct {
   method     string
   pathPrefix string
   paths      []string
}

func (m *SimpleMatcherBuilder) Method(method string) *SimpleMatcherBuilder {
   m.method = method
   return m
}

func (m *SimpleMatcherBuilder) PrefixPath(path string) *SimpleMatcherBuilder {
   m.pathPrefix = path
   return m
}

func (m *SimpleMatcherBuilder) FullPath(path ...string) *SimpleMatcherBuilder {
   m.paths = append(m.paths, path...)
   return m
}

func (m *SimpleMatcherBuilder) Build() func(method, path string) bool {
   method, prefix := m.method, m.pathPrefix
   paths := make(map[string]struct{}, len(m.paths))
   for _, p := range m.paths {
      paths[p] = struct{}{}
   }

   return func(m, p string) bool {
       if  method != "" && m != method {
         return false
      }
      if prefix != "" && !strings.HasPrefix(p, prefix) {
         return false
      }

      if len(paths) == 0 {
         return true
      }

      _, ok := paths[p]
      return ok
   }
}

var _ MatcherBuilder = (AndMBuilder)(nil)
var _ MatcherBuilder = (OrMBuilder)(nil)
var _ MatcherBuilder = (*NotMBuilder)(nil)
var _ MatcherBuilder = (*ExcludePathBuilder)(nil)
......
  

除此之外, ExcludePathBuilder AndMBuilder OrMBuilder *NotMBuilder 也实现了 MatcherBuilder 接口,某些对象内部又嵌套了对 MatcherBuilder 的调用,达到了多条件组合起来匹配的目的,非常灵活。

 // 路径以 `/api/v2` 开头的请求中,除了 `/api/v2/legacy` 外,都开启中间件
mb1 := new(apix.SimpleMatcherBuilder).PrefixPath("/api/v2")
mb2 := new(apix.ExcludePathBuilder).FullPath("/api/v2/legacy")
mw3 := apix.CondHandlersChain{
    Matcher: apix.AndMBuilder{mb1, mb2}.Build(),
    Chain: apix.HandlersChain{...},
}
e.CondUse(mw1, mw2)
  

工厂方法模式 VS 建造者模式

工厂方法模式 注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。

举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。[引用自

Web 中间件— 责任链模式

解决问题 :当业务处理流程很长时,可将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到没有对象处理它为止。

APIX 应用了责任链模式来实现中间件的功能,类似的逻辑可参考文章“Gin 中间件的编写和使用”。

首先要定义 中间件 接口,即下文中的 HandlerFunc ,然后定义 HandlersChain 将一组处理函数组合成一个处理链条,最后将 Handler sChain 插入 Context 中。

开始执行时,是调用 Context Next 函数,遍历每个 HandlerFunc ,然后将 Context 自身的引用传入, index 是记录当前执行到第几个中间件,当过程中出现不满足继续进行的条件时,可以调用 Abort() 来终止流程。

 // 定义中间件的接口
type HandlerFunc func(*Context)

// 将一组处理函数组合成一个处理链条
type HandlersChain []HandlerFunc

// 处理的上下文
type Context struct {
    // ...

    // handlers 是一个包含执行函数的数组
    // type HandlersChain []HandlerFunc
        handlers HandlersChain
    // index 表示当前执行到哪个位置了
        index    int8

    // ...
}

// Next 会按照顺序将一个个中间件执行完毕
// 并且 Next 也可以在中间件中进行调用,达到请求前以及请求后的处理
func (c *Context) Next() {
   c.index++
   for c.index < int8(len(c.handlers)) {
      if handler := c.handlers[c.index]; handler != nil {
         handler(c)
      }
      c.index++
   }
}

// 停止中间件的循环, 通过将索引后移到abortIndex实现。
func (c *Context) Abort() {
   if c.IsDebugging() && c.index < int8(len(c.handlers)) {
      handler := c.handlers[c.index]
      handlerName := nameOfFunction(handler)
      c.SetHeader("X-APIX-Aborted", handlerName)
   }

   c.index = abortIndex
}
  

下面是一个检查用户是否登录的中间件实现,业务方也可以实现自己的中间件插入到请求处理中,非常灵活。

 // RequireLogin 检查用户是否登陆成功。如果不是,终止请求。
func RequireLogin(c *apix.Context) {
   if c.Header(agwconsts.Key_LoaderSessionError) == "1" {
      hsuite.AbortWithBizCode(c, bizstat.APIErrRPCFailed)
      return
   }

   if c.UserId() == 0 {
      hsuite.AbortWithBizCode(c, bizstat.APIErrSessionExpired)
      return
   }
}
  

在服务启动时,注册中间件。

 func main() {
   e := apiservice.Default(
      hsuite.WithBizCodeErrs(consts.BizCodeErrs...), // user-defined error code
   )
   // 可通过 e.Use(), e.CondUse() 注册中间件
   e.Use(devicesession.AGWSessionSuccess, devicesession.NewHWSessionMiddleware(), middleware.Tracing)
   ......
   apiservice.Run()
}
  

什么是洋葱模型

请求进来时,一层一层的通过中间件执行Next函数进入到你设置的下一个中间件中,并且可以通过Context对象一直向下传递下去,当到达最后一个中间件的时候,又向上返回到最初的地方。

该模型常用于记录请求耗时、埋点等场景。

在“Go 语言动手写 Web 框架”[这篇文章中为我们举了一个浅显易懂的例子。

假设我们应用了中间件 A 和 B,和路由映射的 Handler。c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),接下来的流程是这样的:

 func A(c *Context) {
    part1
    c.Next()
    part2
}
func B(c *Context) {
    part3
    c.Next()
    part4
}
  
 - c.index++,c.index 变为 0
- 0 < 3,调用 c.handlers[0],即 A
- 执行 part1,调用 c.Next()
- c.index++,c.index 变为 1
- 1 < 3,调用 c.handlers[1],即 B
- 执行 part3,调用 c.Next()
- c.index++,c.index 变为 2
- 2 < 3,调用 c.handlers[2],即Handler
- Handler 调用完毕,返回到 B 中的 part4,执行 part4
- part4 执行完毕,返回到 A 中的 part2,执行 part2
- part2 执行完毕,结束。
  

最终的调用顺序是part1 -> part3 -> Handler -> part 4 -> part2。

依赖注入,控制反转—观察者模式

解决问题 :解耦观察者和被观察者,尤其是存在多个观察者的场景。

TCC 使用了观察者模式实现了当某 key 的 value 发生变更时执行回调的逻辑。

TccClient 对外提供 AddListener 方法,允许业务注册对某 key 变更的监听,同时开启定时轮询,如果 key 的值与上次不同就回调业务的 callback 方法。

这里的观察者是调用 AddListener 的发起者,被观察者是 TCC 的 key。 Callback 可以看作只有一个函数的接口, TccClient 的通知回调不依赖于具体的实现,而是依赖于抽象,同时 Callback 对象不是在内部构建的,而是在运行时传入的,让被观察者不再依赖观察者,通过依赖注入达到控制反转的目的。

 // Callback for listener,外部监听者需要实现该方法传入,用于回调
type Callback func(value string, err error)

// 一个监听者实体
type listener struct {
   key             string
   callback        Callback
   lastVersionCode string
   lastValue       string
   lastErr         error
}

// 检测监听的key是否有发生变化,如果有,则回调callback函数
func (l *listener) update(value, versionCode string, err error) {
   if versionCode == l.lastVersionCode && err == l.lastErr {
      return
   }
   if value == l.lastValue && err == l.lastErr {
      // version_code updated, but value not updated
      l.lastVersionCode = versionCode
      return
   }
   defer func() {
      if r := recover(); r != nil {
         logs.Errorf("[TCC] listener callback panic, key: %s, %v", l.key, r)
      }
   }()
   l.callback(value, err)
   l.lastVersionCode = versionCode
   l.lastValue = value
   l.lastErr = err
}

// AddListener add listener of key, if key's value updated, callback will be called
func (c *ClientV2) AddListener(key string, callback Callback, opts ...ListenOption) error {
   listenOps := listenOptions{}
   for _, op := range opts {
      op(&listenOps)
   }

   listener := listener{
      key:      key,
      callback: callback,
   }
   if listenOps.curValue == nil {
      listener.update(c.getWithCache(context.Background(), key))
   } else {
      listener.lastValue = *listenOps.curValue
   }

   c.listenerMu.Lock()
   defer c.listenerMu.Unlock()
   if _, ok := c.listeners[key]; ok {
      return fmt.Errorf("[TCC] listener already exist, key: %s", key)
   }
   c.listeners[key] = &listener
   // 一个client启动一个监听者
   if !c.listening {
      go c.listen()
      c.listening = true
   }
   return nil
}

// 轮询监听
func (c *ClientV2) listen() {
   for {
      time.Sleep(c.listenInterval)
      listeners := c.getListeners()
      for key := range listeners {
         listeners[key].update(c.getWithCache(context.Background(), key))
      }
   }
}

// 加锁防止多线程同时修改listeners,同时拷贝一份map在循环监听时使用。
func (c *ClientV2) getListeners() map[string]*listener {
   c.listenerMu.Lock()
   defer c.listenerMu.Unlock()
   listeners := make(map[string]*listener, len(c.listeners))
   for key := range c.listeners {
      listeners[key] = c.listeners[key]
   }
   return listeners
}
  

什么是控制反转(Ioc—Inversion of Control)

控制反转不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序,传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

[引用自

什么是依赖注入(DI—Dependency Injection)

组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

[引用自

控制反转和依赖注入是同一个概念的不同角度描述 。简而言之,当依赖的外部组件时,不要直接从内部 new,而是从外部传入。

替代 IF—策略模式

解决场景 :支持不同策略的灵活切换,避免多层控制语句的不优雅实现,避免出现如下场景:

 if xxx {
  // do something
} else if xxx {
  // do something
} else if xxx {
  // do something
} else if xxx {
  // do something
} else {
}  

通常的做法是定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法。

在 GORM 的 clause/clause.go 中使用到策略模式实现 SQL 的拼装。

现实业务中 SQL 语句千变万化,GORM 将 SQL 的拼接过程,拆分成了一个个小的子句,这些子句统一实现 clause.Interface 这个接口,然后各自在 Build 方法中实现自己的构造逻辑。

以最简单的分页查询为例,在使用 db 链式调用构建 SQL 时,对 Limit Offset Order 的函数调用最终转化成了 Limit 子句和 OrderBy 子句,两者都实现了 clause.Interface 接口。

 db.WithContext(ctx).
    Model(&Course{}).
    Order("course_id DESC").
    Limit(0).
    Offset(100)  
 // Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
   tx = db.getInstance()
   tx.Statement.AddClause(clause.Limit{Limit: limit})
   return
}

// Offset specify the number of records to skip before starting to return the records
func (db *DB) Offset(offset int) (tx *DB) {
   tx = db.getInstance()
   tx.Statement.AddClause(clause.Limit{Offset: offset})
   return
}


// Order specify order when retrieve records from database
//     db.Order("name DESC")
//     db.Order(clause.OrderByColumn{Column: clause.Column{Name: "name"}, Desc: true})
func (db *DB) Order(value interface{}) (tx *DB) {
   tx = db.getInstance()

   switch v := value.(type) {
   case clause.OrderByColumn:
      tx.Statement.AddClause(clause.OrderBy{
         Columns: []clause.OrderByColumn{v},
      })
   case string:
      if v != "" {
         tx.Statement.AddClause(clause.OrderBy{
            Columns: []clause.OrderByColumn{{
               Column: clause.Column{Name: v, Raw: true},
            }},
         })
      }
   }
   return
}  

Clause 的接口定义:

 // Interface clause interface
type Interface interface {
   Name() string
   Build(Builder)
   MergeClause(*Clause)
}  

Limit Clause 的定义:

 
// Limit limit clause
type Limit struct {
   Limit  int
   Offset int
}

// Build build where clause
func (limit Limit) Build(builder Builder) {
   if limit.Limit > 0 {
      builder.WriteString("LIMIT ")
      builder.WriteString(strconv.Itoa(limit.Limit))
   }
   if limit.Offset > 0 {
      if limit.Limit > 0 {
         builder.WriteString(" ")
      }
      builder.WriteString("OFFSET ")
      builder.WriteString(strconv.Itoa(limit.Offset))
   }
}

// Name where clause name
func (limit Limit) Name() string {......}
// MergeClause merge limit by clause
func (limit Limit) MergeClause(clause *Clause) {......}  

OrderBy Clause 的定义:

 type OrderByColumn struct {
   Column  Column
   Desc    bool
   Reorder bool
}

type OrderBy struct {
   Columns    []OrderByColumn
   Expression Expression
}

// Build build where clause
func (orderBy OrderBy) Build(builder Builder) {
   if orderBy.Expression != nil {
      orderBy.Expression.Build(builder)
   } else {
      for idx, column := range orderBy.Columns {
         if idx > 0 {
            builder.WriteByte(',')
         }

         builder.WriteQuoted(column.Column)
         if column.Desc {
            builder.WriteString(" DESC")
         }
      }
   }
}

// Name where clause name
func (limit Limit) Name() string {......}
// MergeClause merge order by clause
func (limit Limit) MergeClause(clause *Clause) {......}  

下面的截图中列举了实现 clause.Interface 接口的所有类,以后 SQL 支持新子句时,创建一个类实现 clause.Interface 接口,并在函数调用的地方实例化该类,其余执行的代码皆可不变,符合 OOP 中的开闭原则和依赖倒置原则。

Lazy 加载,线程安全—单例模式

解决场景 :变量只想初始化一次。

APIX 在埋点中间件中通过单例模式实现了对变量延迟且线程安全地赋值。

Metrics() 用来生成 Metric 埋点中间件,在加载的过程,由于 APIX 的路由表还未注册完毕,所以需要把两个变量 metricMap 和 pathMap 的初始化放在中间件的执行过程中,但服务器启动后,这两个变量的值是固定的,没必要反复初始化,其次大量请求过来时,中间件的逻辑会并发执行,存在线程不安全的问题。

故在实现的过程中用到了 sync.Once 对象,只要声明类型的 once 变量,就可以直接使用它的 Do 方法,Do 方法的参数是一个无参数,无返回的函数。

 func Metrics() HandlerFunc {
   ......
   metricMap := make(map[string]m3.Metric) // key: handler name
   pathMap := make(map[string][]string)    // key: handler name, value: paths
   once := sync.Once{}                     // protect maps init

   return func(c *Context) {
      // why init in handler chain not earlier: routes haven't been registered into the engine when the middleware func created.
      once.Do(func() {
         for _, r := range c.engine.Routes() {
            metricMap[r.Handler] = cli.NewMetric(r.Handler+".calledby", tagMethod, tagURI, tagErrCode, tagFromCluster, tagToCluster, tagFrom, tagEnv)
            pathMap[r.Handler] = append(pathMap[r.Handler], r.Path)
         }
      })

      c.Next()
      ......
   }
}  

Sync.Once

sync.Once 的源码很短,它通过对一个标识值,原子性的修改和加载,来减少锁竞争的。

 type Once struct {
        done uint32
        m    Mutex
}

func (o *Once) Do(f func()) {
    // 加载标识值,判断是否已被执行过
    if atomic.LoadUint32(&o.done) == 0 {
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) { // 还没执行过函数
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 { // double check 是否已被执行过函数
        defer atomic.StoreUint32(&o.done, 1) // 修改标识值
        f() // 执行函数
    }
}  

它有两个特性,一是不管调用 Do 方法多少次,里面的函数只会执行一次;二是如果开始有两个并发调用,可以保证第二个调用不会立即返回,会在获取锁的时候阻塞,等第一个调用执行完毕之后,第二个调用进行二次校验之后就直接返回了。

Sync.Once 有个问题,Do 的过程并不关注 f 函数执行的结果是成功还是失败,当 f()执行失败时,由于本身的机制,没有机会再次初始化了。如果你需要二次初始化,可以看看下面传送门中关于“sync.Once 重试”的文章。

传送门

  • 百度分布式配置中心 BRCC:
  • Gin github 地址:
  • GORM 官网:
  • Gin 中间件的编写和使用:
  • Go 语言动手写 Web 框架:
  • sync.Once 重试:

参考和引用文献

相关文章