Post

Go常见设计模式

Go常见设计模式

设计模式是为了语言擦屁股。不同语言由于自身优劣不同,常见的设计模式及其实现有些小差别。

Go的思想是“组合优于继承”,不依托于复杂的类层次来实现多态,推荐使用Interface、高阶函数(行为参数化)、Goroutine/Channel 来实现各类设计模式。

策略模式

与大多数oop语言相同,go策略模式也依托于接口来实现。

在实际工程中,我们经常会搭配策略模式和简单工厂模式。对于不复杂的场景,简单工厂模式完全可以用一个简单的map实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package activitymgr

var ( //工厂Map
	globalActivityManager = container.ConcurrentMap[int32, GlobalActivityProcessor]{}
)

//策略接口
type GlobalActivityProcessor interface {
	Start(ctx context.Context, serverId int, activityId int32, startTime int64) error
	End(ctx context.Context, serverId int, activityId int32, startTime int64) error
	Preview(ctx context.Context, serverID int, activityId int32, startTime int64) error
}

//工厂操作:存
func RegisterGlobalActivity(activityID int32, processor GlobalActivityProcessor) {
	globalActivityManager.Store(activityID, processor)
}
//工厂操作:取
func GetGlobalActivityProcessor(activityID int32) (GlobalActivityProcessor, bool) {
	return globalActivityManager.LoadB(activityID)
}


//Base接口实现
type BaseGlobalActivityProcessor struct {
}

func (b *BaseGlobalActivityProcessor) Start(ctx context.Context, serverId int, activityId int32, startTime int64) error {
	return msgs.NotImplement(ctx)
}

func (b *BaseGlobalActivityProcessor) End(ctx context.Context, serverId int, activityId int32, startTime int64) error {
	return msgs.NotImplement(ctx)
}

func (b *BaseGlobalActivityProcessor) Preview(ctx context.Context, serverID int, activityId int32, startTime int64) error {
	return msgs.NotImplement(ctx)
}

我们观察上述代码很容易注意到,我们不仅仅定义了接口,定义工厂Map, 还额外定义了一个策略接口的默认实现BaseProcessor。后续所有想要实现该策略接口的“类”,只需组合/“继承”一下这个默认实现,再去重载一下自己需要实现的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package worldboss

////工厂操作:存 ————重载后的策略实现注册进Map
func init() {
  //init() 的执行时机就是 Go 程序启动阶段、main 函数运行之前——由 Go runtime 自动完成,与包导入顺序有关
	activitymgr.RegisterGlobalActivity(int32(consts.ActivityWorldBoss), &WorldBossActivityProcessor{})
}

//组合默认实现
type WorldBossActivityProcessor struct {
	activitymgr.BaseGlobalActivityProcessor   
}

//重载所需方法
func (b *WorldBossActivityProcessor) Start(ctx context.Context, serverId int, activityId int32, startTime int64) error {
	_, err := wrpcs.CallBattle[worldbosswp.OpenActivityResp](ctx, "worldbossp.OpenActivity", &worldbosswp.OpenActivityReq{
		ServerID:   serverId,
		ActivityId: activityId,
		StartTime:  startTime,
	})
	return err
}

func (b *WorldBossActivityProcessor) End(ctx context.Context, serverId int, activityId int32, startTime int64) error {
	return nil
}

最后的策略方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package activitymgr

//订阅cron服发来的活动变化事件
func SubscribeEventbus() {
	ctx := context.Background()
	err := topics.OnGlobalActivityChange.Subscribe(ctx, onGlobalActivityChange)
	if err != nil {
		panic(err)
	}
}

//消费事件,调用策略方法
func onGlobalActivityChange(ctx context.Context, event *gevent.GlobalActivityChangeEvent) {
	log := wlog.WithContext(ctx).WithFields(wlog.Fields{"event": event})
	conf, ok := aplsvc.GetApolloProvider(ctx).GetCSConfig(ctx).GetActivityCfgById(int(event.ActivityID))
	if !ok {
		log.Errorf("get activity config failed ")
		return
	}
  //工厂操作:取策略实现对象
	activityProcessor, ok := GetGlobalActivityProcessor(int32(conf.ActivityType))
	if !ok {
		log.Errorf("activity processor not found for activity %d", event.ActivityID)
		return
	}
	switch event.State {
	case int32(entrancep.ActivityState_ACTIVITY_STATE_OPEN):
		if err := activityProcessor.Start(ctx, event.ServerID, event.ActivityID, event.StartTime); err != nil {
			log.Errorf("start activity failed: %v", err)
			sms.DoSendLarkRobotMsg(consts.LarkServerNotify, event.String(fmt.Sprintf("错误原因: %v", err)))
		}
	case int32(entrancep.ActivityState_ACTIVITY_STATE_END):
		if err := activityProcessor.End(ctx, event.ServerID, event.ActivityID, event.StartTime); err != nil {
			log.Errorf("start activity failed: %v", err)
			sms.DoSendLarkRobotMsg(consts.LarkServerNotify, event.String(fmt.Sprintf("错误原因: %v", err)))
		}
	case int32(entrancep.ActivityState_ACTIVITY_STATE_PREVIEW):
		if err := activityProcessor.Preview(ctx, event.ServerID, event.ActivityID, event.StartTime); err != nil {
			log.Errorf("start activity failed: %v", err)
			sms.DoSendLarkRobotMsg(consts.LarkServerNotify, event.String(fmt.Sprintf("错误原因: %v", err)))
		}
	}
}

Option模式

结构体的默认属性,有时需要灵活的得到修改。我们无论是提供一大堆构造方法, 还是提供一些SetField方法在go中都不可取。于是Option模式应运而生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type Server struct { //结构体
    addr string
    tls  bool
}

type ServerOption  func(*Server) //Option函数

//返回的Option函数为闭包 , 自由变量是外层函数入参,本地变量是结构体指针
func WithAddr(a string) ServerOption{ 
  return  func(s *Server){
    s.addr = a
  }
}
func WithTls(t bool) ServerOption{
  return func(s *Server){
    s.tls = t
  }
}

//构造器
func NewServer(opts ...ServerOption) *Server{
  s := new(Server)        
  //也可以加默认值  s := &Server{addr: ":8080",tls:  false,}
  for _,opt := range opts{
    opt(s)
  }
  return s
}

//示例
server:= NewServer(WithAddr(":8080"),WithTls(true))

这类Option的运用最为典型,非常适合用于处理 初始值/可选值构造,构造时加上WithOption方法,将默认值改为自定义值。在后续方法处理逻辑中,就可以依托默认值==0值条件,使方法进入不同的逻辑分支。

但我们抛开上述经典场景,回归Option模式语法本身,发现它本质是对行为参数化的灵活运用。每一个WithOption函数所返回的是一个闭包。对于该闭包来说,自由变量是外层函数入参,本地变量是该闭包入参的那个结构体指针。

闭包:闭包是指一个多层嵌套函数的返回值(返回值也必须是一个函数)。被返回的这个闭包函数如果引用了外层函数的变量,就会捕获其引用(在go中会触发内存逃逸)携带该数据(称为自由变量)。而该闭包内部的局部变量或入参,称为该闭包的本地变量。

go实现行为参数化所采用的方案是 func函数可以作为参数来传递,同数据一样是一等公民,姑且将这种方案叫作高阶函数(实际上这种叫法来自于函数式编程语言,如Scheme)。高阶函数的使用在go各类框架、库函数中不胜枚举。比如我们订阅事件\dlq所注册的回调函数,都是行为参数化的体现。

虽然行为参数化这常用于 框架\库\底层组件设计,我们在实际业务开发中的某些场景也能使用行为参数化。

现在假设一个结构体,这个结构体中有大量的数据,一个业务流程中需要对这个结构体数据进行数据校验才可以继续推进流程,数据校验失败则抛出error。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

//某业务数据
type Item struct {
    Id int64
    a  int64
    b  int64
}

//构造方法
func NewItem(opts ...ItemOption) *Item {
    item := &Item{}
    for _, opt := range opts {
       opt(item)
    }
    return item
}

//检验方法
func (item *Item) CheckItem(opts ...CheckOption) error {
    for _, opt := range opts {
       if err := opt(item); err != nil {
          return err
       }
    }
    return nil
}


//数据填充Option
type ItemOption func(*Item)

func WithId(id int64) ItemOption {
	return func(o *Item) {
		o.Id = id
	}
}

func WithA(a int64) ItemOption {
	return func(o *Item) {
		o.a = a
	}
}

func WithB(b int64) ItemOption {
	return func(o *Item) {
		o.b = b
	}
}



//数据校验Option
type CheckOption func(*Item) error

func IdRight() CheckOption {
	return func(o *Item) error {
		if o.Id <= 0 {
			return errors.New("id must be greater than 0")
		}
		return nil
	}
}

func ARight() CheckOption {
	return func(o *Item) error {
		if o.a <= 0 {
			return errors.New("a must be greater than 0")
		}
		return nil
	}
}

func BRight() CheckOption {
	return func(o *Item) error {
		if o.b <= 0 {
			return errors.New("b must be greater than 0")
		}
		return nil
	}
}

我们在后续的业务流程中,就可以灵活的使用item.CheckItem,将自定义的XXRight按照需要加入传参,实现灵活的参数校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
    item := NewItem(WithId(-1), WithA(2), WithB(3))
  
  	//校验id,A,B 
    if err := item.CheckItem(IdRight(), ARight(), BRight()); err != nil {
       fmt.Println(err) //抛出Error "id must be greater than 0"
    }
  	// ... 操作Item数据,校验A
  	if err := item.CheckItem(ARight()); err != nil {
       fmt.Println(err)
    }
    // ... 操作Item数据,校验A,B 
  	if err := item.CheckItem(ARight(), BRight()); err != nil {
       fmt.Println(err)
    }
  
    fmt.Println(item)
}

当然大多数业务数据数据校验十分简单,校验参数、场合、时机都比较固定,一个函数就能搞定,就没必要搞这么麻烦。硬加高阶函数反而增加复杂度,画蛇添足。

装饰者模式

装饰器模式 (Decorator / Middleware),指不允许修改原函数的情况下,动态地给函数加功能。

如果你写过java,你会自然而然的想到AOP。让代理对象的代理方法被代理对象的被代理方法的上下填充自定义逻辑。

go不需要这么麻烦的写法,因为go可以方便的传递函数。

装饰器模式常用于Http\rpc中间件,其逻辑核心是:函数接受一个接口,并返回同一个接口

1
2
3
4
5
6
7
8
9
type Handler func(string)

func WithLogging(next Handler) Handler{
  return func(msg string){
        fmt.Println("[Log] Before execution")
        next(msg) // 执行原逻辑
    		fmt.Println("[Log] After execution")
  }
}

适配器模式

适配器模式(adapter pattern),又名包装器(Wrapper)模式。

如果我们借用OOP语言的术语,适配器模式的目标就是将外界给定的一个类,纳入系统的某一类层次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//接口
type Being interface{
  Eat()  //吃
  Move() //动
}

//鸟(吃,飞)
type Bird struct{ 
}

(b *Bird) Eat(){
  fmt.Println("bird eat")
}
(b *Bird) Fly(){
    fmt.Println("bird fly")
}

//机器人(充电,动)
type Robot struct{
}

(r *Robot) Charge(){
   fmt.Println("robot charge")
}
(r *Robot) Move(){
   fmt.Println("robot move")
}

我们可以观察到,BirdRobot两个结构体并没有实现Being,相当于没有纳入Being这个接口的“类层次”,我们需要想方设法地把两个结构体纳入“类层次”,即实现Being的两个接口,需要编写一个“包装类”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//鸟wrapper
type MyBird struct{
  Bird
}
(b *MyBird) Move(){
  b.Fly()
}

//机器人wrapper
type MyRobot struct{
  Robot
}
(r *MyRobot) Eat(){
  r.Charge()
}


//使用示例
var being Being = new(MyBird)
being.eat()  //"bird eat"
being.move() //"bird fly"

var being Being = new(MyRobot)
being.eat()  //"robot charge"
being.move() //"robot move"

单例模式

在Go中,实现单例模式的最佳实践不是“双重锁检查”,而是直接使用sync.Once

比如我们现在有一份全局配置,只用加载一次,采用懒汉式加载。

1
2
3
4
5
6
7
8
9
10
11
12
var(
	instance *Config
  once = sync.Once{}
)

func GetConfig() *Config{
  once.Do(func(){
    instance = &Config{}
  })
  return instance
}

This post is licensed under CC BY 4.0 by the author.

Trending Tags