golang组合、接口以及反序列化

Sat, Sep 4, 2021 阅读时间 6 分钟

现在,我想用Go实现一个Event类,有如下的要求:

  1. 有多种Event类型,不同的Event类型拥有不同的参数;
  2. 这些Event会被持久化到存储中,在需要时反序列化回来;
  3. 每个Event都有Process方法,具体的方法实现逻辑存放在不同的Event具体类型中。

ok,看需求并不复杂,不就是面向对象的继承、重载那一套吗,so easy,直接开搞! 但是真正实现时,才发现没有想象中的那么简单…

1. 结构体实现

首先我们使用go来实现结构体Event,然后利用组合的方式实现两个Event:

type Event struct {
	EventType string `json:"event_type"`
}

type ClickEvent struct {
	Event `json:",inline"`
	OnPosX int64 `json:"on_pos_x"`
	OnPosY int64 `json:"on_pos_y"`
}

type DragEvent struct {
	Event `json:",inline"`
	OnPosX int64 `json:"on_pos_x"`
	OnPosY int64 `json:"on_pos_y"`
	ToPosX int64 `json:"to_pos_x"`
	ToPosY int64 `json:"to_pos_y"`
}

很简单,没有问题。

2. 序列化与反序列化

让我们尝试将其序列化到json,以便持久化存储:

func main() {
	event1 := ClickEvent{
		Event:  Event{EventType: "click"},
		OnPosX: 0,
		OnPosY: 0,
	}
	b1, _ := json.Marshal(&event1)

	event2 := DragEvent{
		Event:  Event{EventType: "drag"},
		OnPosX: 1,
		OnPosY: 2,
		ToPosX: 3,
		ToPosY: 4,
	}
	b2, _ := json.Marshal(&event2)

	fmt.Println(string(b1))
	fmt.Println(string(b2))
}

看上去也不难。对每个具体的Event直接json反序列化即可。以上程序会输出如下的序列化之后的字符串:

{"event_type":"click","on_pos_x":0,"on_pos_y":0}
{"event_type":"drag","on_pos_x":1,"on_pos_y":2,"to_pos_x":3,"to_pos_y":4}

那如果反序列化呢?

为了使程序更加通用,我们假设并不清楚读取得到的json究竟是哪一种事件类型。一般会有一个函数能反序列化所有的事件类型,根据其中的EventType类型不同反序列化成不同的事件类型的实例。

让我们先试试直接反序列化成组合的根结构体会怎么样?

func main() {
	jsonStr := `{"event_type":"click","on_pos_x":10,"on_pos_y":20}`
	event, err := unmarshalEvent(jsonStr)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", event)

	fmt.Println(event.(*ClickEvent).OnPosX) //备注2
}

func unmarshalEvent(jStr string) (*Event, error) {
	var tmpEvent Event // //备注1
	err := json.Unmarshal([]byte(jStr), &tmpEvent)
	return &tmpEvent, err
}

备注2的位置我们遇到了麻烦。这里json从类型上看是ClickEvent类型,但是使用组合结构体Event进行反序列化之后并没有得到想要的类型。该Event无法强制转换为ClickEvent类型,就算ClickEvent中组合了Event也不行。我们需要另想办法。

需要注意的是,以上程序报的错误是

invalid type assertion: tmpEvent.(*ClickEvent) (non-interface type Event on left)

这里似乎说的是备注1的位置没有使用interface{}导致的该问题。但是就算改成了var tmpEvent interface{}, 也依旧会报错,变成如下错误:

panic: interface conversion: interface {} is map[string]interface {}, not *main.ClickEvent

也就是说json直接被反序列化成了map[string]interface {},似乎更糟了。

这里有一种解决方案,就是在unmarshalEvent函数中先反序列化为Event,然后再根据其类型反序列化为其他类型。代码如下:

func main() {
	jsonStr := `{"event_type":"click","on_pos_x":10,"on_pos_y":20}`
	event, err := unmarshalEvent(jsonStr)
	if err != nil {
		panic(err)
	}
	
	fmt.Println(reflect.TypeOf(event)) //备注2
	switch event.(type) { //备注3
	case *ClickEvent:
		fmt.Println(event.(*ClickEvent).OnPosX)
	case *DragEvent:
		fmt.Println(event.(*DragEvent).ToPosX)
	default:
		// todo more judge
	}
}

func unmarshalEvent(jStr string) (interface{}, error) {
	var tmpEvent Event
	if err := json.Unmarshal([]byte(jStr), &tmpEvent); err != nil {
		return nil, err
	}

	switch tmpEvent.EventType { // 备注1
	case "click":
		var clickEvent ClickEvent
		err := json.Unmarshal([]byte(jStr), &clickEvent)
		return &clickEvent, err
	case "drag":
		var dragEvent DragEvent
		err := json.Unmarshal([]byte(jStr), &dragEvent)
		return &dragEvent, err
	default:
		//todo another type
	}

	return nil, errors.New("invalid type:" + tmpEvent.EventType)
}

代码可以正常运行,我们成功地识别出了json中的Event类型,并将其反序列化成了想要的具体类型。 由备注2的输出可以看到,我们得到的类型的确是*ClickEvent,而不是*Event

这里有两个问题:

  1. 我们需要再反序列化时枚举所有可能的Event类型。也就是说每当我需要新增一个Event类型,都需要修改备注1的代码,不符合开闭原则;
  2. 由于go无法在组合结构体之间相互赋值,unmarshalEvent的返回类型是一个interface{},这样就无可避免地需要在备注3处进行类型判断和类型强转,导致代码冗长。

第一个问题我们暂时先放一放;对于第二个问题,我们能否使用接口来解决?

3. 使用接口实现扩展

我们修改一下第1节的结构体定义,改成使用接口而非组合的方式进行Event的扩展:

type Event interface {
	GetEventType() string
	Process()
}

type ClickEvent struct {
	OnPosX int64 `json:"on_pos_x"`
	OnPosY int64 `json:"on_pos_y"`
}

func (e *ClickEvent) GetEventType() string {
	return "click"
}

func (e *ClickEvent) Process() {
	fmt.Println("process ClickEvent with on pos: ", e.OnPosX, ", ", e.OnPosY)
}

func (e *ClickEvent) MarshalJSON() ([]byte, error) {
	type copyType ClickEvent
	return json.Marshal(struct {
		copyType
		EventType string `json:"event_type"`
	}{
		copyType:  copyType(*e),
		EventType: "click",
	})
}

type DragEvent struct {
	OnPosX int64 `json:"on_pos_x"`
	OnPosY int64 `json:"on_pos_y"`
	ToPosX int64 `json:"to_pos_x"`
	ToPosY int64 `json:"to_pos_y"`
}

func (e *DragEvent) GetEventType() string {
	return "drag"
}

func (e *DragEvent) Process() {
	fmt.Println("process DragEvent with to pos: ", e.ToPosX, ", ", e.ToPosY)
}

func (e *DragEvent) MarshalJSON() ([]byte, error) {
	type copyType DragEvent
	return json.Marshal(struct {
		copyType
		EventType string `json:"event_type"`
	}{
		copyType:  copyType(*e),
		EventType: "drag",
	})
}

在这里,Event变成了一个接口而非结构体,除了提供GetEventType方法之外,也定义了必须实现的Process方法。注意其中为了保证序列化结果中包含event_type,这里重新定义了ClickEventDragEventMarshalJSON方法。

使用接口的方式实现的各个事件定义的序列化的使用方式和之前没有什么不同,这里直接看反序列化:


func main() {
	jsonStr := `{"event_type":"click","on_pos_x":10,"on_pos_y":20}`
	event, err := unmarshalEvent(jsonStr)
	if err != nil {
		panic(err)
	}

	fmt.Println(reflect.TypeOf(event)) 
	switch event.GetEventType() { //备注3
	case "click":
		fmt.Println(event.(*ClickEvent).OnPosX)
	case "drag":
		fmt.Println(event.(*DragEvent).ToPosX)
	default:
		// todo more judge
	}

    event.Process() //备注4
}

func unmarshalEvent(jStr string) (Event, error) {
	var tmpEvent map[string]interface{} // 备注1
	if err := json.Unmarshal([]byte(jStr), &tmpEvent); err != nil {
		panic(err)
	}

	switch tmpEvent["event_type"].(string) { // 备注2
	case "click":
		var clickEvent ClickEvent
		err := json.Unmarshal([]byte(jStr), &clickEvent)
		return &clickEvent, err
	case "drag":
		var dragEvent DragEvent
		err := json.Unmarshal([]byte(jStr), &dragEvent)
		return &dragEvent, err
	default:
		//todo another type
	}

	return nil, errors.New("invalid type:" + tmpEvent["event_type"].(string))
}

这里将unmarshalEvent方法的返回值类型由interface变更为了Event。代价就是在判断当前事件类型时不能使用Event接收反序列化结果了,备注1处和备注2处必须使用map[string]interface{},稍微复杂了些。

备注3处,为了获取原始的事件,还是需要进行类型的探测以及强转,不过这里好一点的是无需进行类型检测,只需要检测event.GetEventType()即可。另外在备注4处,我们可以无需进行类型探测和强转即可去调用各个实现事件的Process函数,这个就比之前使用interface{}的方式好很多。这真是接口"抽象行为"上的优势体现。但是使用接口的方式问题在于无法提取共性的参数进行复用,可以结合使用"组合"和"接口"两种方式来获取二者的优势。

4. 支持自定义的事件

到目前为止看来问题几乎已经完全得到了解决,除了上一章提到的一个问题:

我们需要再反序列化时枚举所有可能的Event类型。也就是说每当我需要新增一个Event类型,都需要修改备注1的代码,不符合开闭原则。

在我们需要支持用户自定义事件时,每次新增都需要修改一次unmarshalEvent的函数代码。这个该怎么改进呢?

退一步想一下,如果我自定义了一个事件,然后在unmarshalEvent中反序列化一个json成为这个自定义事件的类型实例,需要哪些必要的信息?

首先很明确的一点,就是当前提供的json具体是哪个类型,这个信息通过字段event_type已经可以知道了;其次,如果是自定义的扩展类型,还需要的数据就是“当前扩展类型的结构体定义”,或者说有一种机制能够让unmarshalEvent在读取到event_type之后还能够知道这种event_type对应的类型就行了。

这样看的话,我们可以为unmarshalEvent函数维护一个可以扩展的映射,维护event_type到具体结构体类型的映射关系。在我们新增一个event_type及其定义时,将其注册到这个扩展映射中即可。这样的话在事件类型扩展时unmarshalEvent本身不用做任何改动。

var mutex sync.RWMutex
var extendEventType map[string]reflect.Type

func RegisterExtendEvent(eventType string, p reflect.Type) {
	mutex.Lock()
	defer mutex.Unlock()

	if extendEventType == nil {
		extendEventType = make(map[string]reflect.Type)
	}

	extendEventType[eventType] = p
}

func GetEventByType(eventType string) (reflect.Type, error) {
	mutex.RLock()
	defer mutex.RUnlock()

	if extendEventType == nil || extendEventType[eventType] == nil {
		return nil, errors.New("Unregistered extendEventType:" + eventType)
	}
	return extendEventType[eventType], nil
}

func unmarshalEvent(jStr string) (Event, error) {
	var tmpEvent map[string]interface{}
	if err := json.Unmarshal([]byte(jStr), &tmpEvent); err != nil {
		panic(err)
	}

	eventType := tmpEvent["event_type"].(string)
	t, err := GetEventByType(eventType)
	if err != nil {
		return nil, err
	}
	extendEvent := reflect.New(t).Interface()
	err = json.Unmarshal([]byte(jStr), &extendEvent)
	return extendEvent.(Event), err
}

然后再需要扩展Event类型时进行注册即可:

RegisterExtendEvent("click", reflect.TypeOf(ClickEvent{}))
RegisterExtendEvent("drag", reflect.TypeOf(DragEvent{}))

到目前为止,我们似乎完成了所有的需求。看上去不错,但这里有一个限制就是unmarshalEvent()结果获取的是Event接口,也就是说我们可以调用其中的接口方法,但是无法调用实际类型的方法(强制转换后是可以的,但是还是需要switch判断实际类型后进行转换,不是我所需要的)。

假如我们需要的通用操作不只是Process(),需要扩展一个新的操作Cancel(),这个时候只有两种办法: 要么修改Event接口定义然后实现:

type Event interface {
	GetEventType() string
	Process()
	Cancel()
}

要么我重新定义一个Canceler接口:

type Canceler interface {
	Cancel()
}

重新定义一个Canceler接口是不合适的,unmarshalEvent()无法返回多个接口的变量,毕竟没有泛型;而如果我修改Event接口定义,那么我需要修改所有的Event实现,这工作量也太大了些。

参考接口类型的注册模式,我们也可以定义一个不同操作的注册机制。Event实现类只需要在需要的时候调用操作的注册方法即可;而在相应方法未注册时进行查找调用的话会忽略或者抛出异常:

var handleMutex sync.RWMutex
var handlerMap map[string]map[string]func(e Event)

func RegisterEventHandler(eventType string, handlerName string, f func(e Event)) {
	handleMutex.Lock()
	defer handleMutex.Unlock()

	if handlerMap == nil {
		handlerMap = make(map[string]map[string]func(e Event))
	}

	if handlerMap[eventType] == nil {
		handlerMap[eventType] = make(map[string]func(e Event))
	}

	handlerMap[eventType][handlerName] = f
}

func GetEventHandler(eventType string, handlerName string) (f func(e Event), error) {
	handleMutex.RLock()
	defer handleMutex.RUnlock()

	if handlerMap == nil || handlerMap[eventType] == nil {
		return nil, errors.New("Unregistered handler for eventType:" + eventType + " and handlerName:" + handlerName)
	}
	return handlerMap[eventType][handlerName], nil
}

然后记得注册和使用即可:

func ProcessClickEvent(e Event) {
	ce := e.(*ClickEvent)
	fmt.Println("process ClickEvent with on pos: ", ce.OnPosX, ", ", ce.OnPosY)
}

func CancelClickEvent(e Event) {
	ce := e.(*ClickEvent)
	fmt.Println("cancel ClickEvent with on pos: ", ce.OnPosX, ", ", ce.OnPosY)
}

func ProcessDragEvent(e Event) {
	de := e.(*DragEvent)
	fmt.Println("process DragEvent with to pos: ", de.ToPosX, ", ", de.ToPosY)
}

func CancelDragEvent(e Event) {
	de := e.(*DragEvent)
	fmt.Println("cancel DragEvent with to pos: ", de.ToPosX, ", ", de.ToPosY)
}

func main() {

	RegisterExtendEvent("click", reflect.TypeOf(ClickEvent{}))
	RegisterExtendEvent("drag", reflect.TypeOf(DragEvent{}))

	RegisterEventHandler("click", "process", ProcessClickEvent)
	RegisterEventHandler("click", "cancel", CancelClickEvent)
	RegisterEventHandler("drag", "process", ProcessDragEvent)
	RegisterEventHandler("drag", "cancel", CancelDragEvent)

	jsonStr := `{"event_type":"click","on_pos_x":10,"on_pos_y":20}`
	event, err := unmarshalEvent(jsonStr)
	if err != nil {
		panic(err)
	}

	processFunc, err := GetEventHandler(event.GetEventType(), "process")
	if err != nil {
		panic(err)
	}
	if processFunc != nil {
		processFunc(event)
	}
}

5. 自定义反序列化

前面几节使用了unmarshalEvent进行了事件的反序列化。我们的Event是独立的实体还好说,如果是某个结构体的一部分怎么办?这时候就必须自定义json的反序列化方法了。 比如我们有如下的结构体,然后尝试反序列化:

type Panel struct {
	Height int64 `json:"height"`
	Row    int64 `json:"row"`
	Event  Event `json:"event"`
}

func main() {
	var panel Panel
	panelJson := `{"height": 200, "row": 100, "event": {"event_type":"click","on_pos_x":10,"on_pos_y":20}}`
	if err := json.Unmarshal([]byte(panelJson), &panel); err != nil {
		panic(err)
	}

	fmt.Println(reflect.TypeOf(panel.Event))
}

很可惜,上面的程序报错了:

panic: json: cannot unmarshal object into Go struct field Panel.event of type main.Event

也就是说,PanelEvent无法反序列化。这是就需要自定义反序列化函数。为了Event的自定义反序列化不影响Panel的其他正常字段,这里可以使用一个小技巧:将Event包装起来:

type Panel struct {
	Height int64        `json:"height"`
	Row    int64        `json:"row"`
	Event  EventWrapper `json:"event"`
}

type EventWrapper struct {
	Event Event
}

func (w EventWrapper) MarshalJSON() ([]byte, error) {
	return json.Marshal(w.Event)
}

func (w *EventWrapper) UnmarshalJSON(data []byte) error {
	var tmpEvent map[string]interface{}
	if err := json.Unmarshal(data, &tmpEvent); err != nil {
		return err
	}

	eventType := tmpEvent["event_type"].(string)
	t, err := GetEventByType(eventType)
	if err != nil {
		return err
	}
	extendEvent := reflect.New(t).Interface()
	err = json.Unmarshal(data, &extendEvent)
	if err != nil {
		return err
	}

	w.Event = extendEvent.(Event)
	return nil
}

func main() {
	RegisterExtendEvent("click", reflect.TypeOf(ClickEvent{}))
	RegisterExtendEvent("drag", reflect.TypeOf(DragEvent{}))

	var panel Panel
	panelJson := `{"height": 200, "row": 100, "event": {"event_type":"click","on_pos_x":10,"on_pos_y":20}}`
	if err := json.Unmarshal([]byte(panelJson), &panel); err != nil {
		panic(err)
	}

	fmt.Println(reflect.TypeOf(panel.Event))

	b, err := json.Marshal(panel)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

回过头来,如果我们从一开始不是使用接口而是直接使用结构体来定义Event,事情又会变得不一样。我们只需要处理一件事情,那就是结构体之间的相互转换。

type Event struct {
	EventType  string      `json:"event_type"`
	EventParam interface{} `json:"event_param"`
}

func (e *Event) UnmarshalJSON(data []byte) error {

	// 使用新类型,避免递归调用
	type cloneType Event

	rawMsg := json.RawMessage{}
	e.EventParam = &rawMsg //将Param强制赋值为json.RawMessage,避免使用默认的map[string]interface{}
	if err := json.Unmarshal(data, (*cloneType)(e)); err != nil {
		return err
	}

	t, err := GetEventByType(e.EventType)
	if err != nil {
		return err
	}
	extendEvent := reflect.New(t).Interface()
	if len(rawMsg) != 0 {
		if err := json.Unmarshal(rawMsg, &extendEvent); err != nil { //使用rawMsg进行序列化,rawMsg由前面的反序列化得来
			return err
		}
	}
	e.EventParam = extendEvent
	return nil
}

func main() {
	RegisterExtendEvent("click", reflect.TypeOf(ClickEvent{}))
	RegisterExtendEvent("drag", reflect.TypeOf(DragEvent{}))

	jsonStr := `{"event_type":"click", "event_param": {"on_pos_x":10,"on_pos_y":20}}`
	var event Event
	err := json.Unmarshal([]byte(jsonStr), &event)
	if err != nil {
		panic(err)
	}
	fmt.Println(reflect.TypeOf(event))
	fmt.Println(event.EventParam.(*ClickEvent).OnPosX)
}

这个时候无需包装结构体,在反序列化的时候使用RawMessage进行延迟处理即可。

6. 总结

由于go语言没有完整的面向对象的类型支持和泛型支持,所以在处理时需要自己考虑持久化的数据结构如何以及如何反序列化回来,还有就是怎么进行灵活的类型和方法绑定。最后的实现方式似乎越来越接近面向对象支持的底层实现,说不定有一天看到c++的面向对象实现原理,会有一种“啊,这一块的实现方式我之前也有写过”的奇妙感觉。