Go语言中AST的结构体表示与序列化:缺sum类型的困境
我太懂这种困扰了——Go没有Haskell那样原生的求和类型(sum types),这对AST这类需要“多种互斥类型节点”的场景来说,确实是个不小的阻碍。先拿你给出的布尔AST例子来拆解问题:
先看Haskell里的清晰定义
data BoolAST = Or BoolAST BoolAST | And BoolAST BoolAST | Lit Bool
这种求和类型直接把AST的三种节点类型(或运算、与运算、布尔字面量)统一在BoolAST下,编解码时天然就能识别不同节点类型,非常直观。
你的尝试方案:用接口模拟求和类型
你尝试用Go的接口来模拟这种结构,思路是对的:
type BoolStatement interface { ComputeBool() bool Serialize() []byte } type Or struct { Left BoolStatement Right BoolStatement } type And struct { Left BoolStatement Right BoolStatement } type Lit struct { // 你之前写的BoolFunc应该对应Haskell的Lit,这里调整命名更贴合场景 Value bool }
这个方案在序列化时确实能正常工作:每个节点类型实现自己的Serialize()方法,递归处理子节点就行。但问题出在反序列化上——这也是Go接口模拟求和类型的核心痛点:
方案1的核心问题
当你拿到一段序列化后的[]byte时,你根本不知道它对应的是Or、And还是Lit类型。Go的序列化机制(比如标准库的encoding/json)无法直接将字节流反序列化为BoolStatement接口类型,因为它需要明确的具体类型信息。你总不能每次都挨个做类型断言试错吧?这在复杂AST场景下完全不可维护。
可行的解决思路
针对这个问题,我在实际项目里常用几种方案,分享给你:
1. 给每个节点添加类型标识字段
这是最直接的方案:给每个实现BoolStatement的结构体加一个明确的类型标记,序列化时先写入标记,反序列化时先读取标记,再决定实例化哪个具体类型。
修改后的示例代码:
type NodeType string const ( NodeTypeOr NodeType = "or" NodeTypeAnd NodeType = "and" NodeTypeLit NodeType = "lit" ) type BoolStatement interface { ComputeBool() bool Serialize() ([]byte, error) GetType() NodeType } type Or struct { Type NodeType `json:"type"` Left BoolStatement `json:"left"` Right BoolStatement `json:"right"` } func (o *Or) GetType() NodeType { return NodeTypeOr } // 实现ComputeBool和Serialize逻辑... type And struct { Type NodeType `json:"type"` Left BoolStatement `json:"left"` Right BoolStatement `json:"right"` } func (a *And) GetType() NodeType { return NodeTypeAnd } // 实现ComputeBool和Serialize逻辑... type Lit struct { Type NodeType `json:"type"` Value bool `json:"value"` } func (l *Lit) GetType() NodeType { return NodeTypeLit } // 实现ComputeBool和Serialize逻辑...
反序列化时,你可以先把字节流反序列化为一个包含Type字段的临时结构体,根据Type值创建对应的具体节点实例,再把字节流反序列化为该实例,完美解决类型识别问题。
2. 利用接口注册机制
如果不想给每个结构体加类型字段,还可以在程序启动时把节点类型和对应的标识符注册到一个全局映射里,序列化时写入标识符,反序列化时通过标识符从映射中获取类型,再进行反序列化。比如:
var nodeTypeMap = make(map[NodeType]reflect.Type) func init() { nodeTypeMap[NodeTypeOr] = reflect.TypeOf(&Or{}) nodeTypeMap[NodeTypeAnd] = reflect.TypeOf(&And{}) nodeTypeMap[NodeTypeLit] = reflect.TypeOf(&Lit{}) }
这种方式更灵活,适合节点类型较多的场景,但需要用到反射,对性能有一点点影响(一般AST场景可以忽略)。
3. 用泛型辅助封装(Go 1.18+)
Go 1.18之后的泛型可以帮你封装一些重复的编解码逻辑,但本质上还是绕不开类型标识的问题,只是能减少一些重复代码。比如写一个通用的序列化函数,自动带上类型标记,反序列化时自动根据标记选择类型。
总结
Go虽然没有原生求和类型,但用“接口+类型标识”的方式完全可以模拟出类似的效果,核心就是解决反序列化时的类型识别问题。你最初的方案已经走对了第一步,补上类型标识的逻辑就能解决编解码的完整流程了。
内容的提问来源于stack exchange,提问作者DantheMan

