Go语言从设计上就拒绝了泛型之前的联合类型概念,也没有类似TypeScript中 string | number 的语法。但在实际业务里,我们常常需要表示一个值可能是多种类型中的一种,例如解析JSON时某个字段有时是字符串有时是数字,或者状态机中的载荷随事件类型变化。要在Go中模拟这种行为,核心思路是用已有的复合类型与运行时机制来表达「多选一」的语义。

使用空接口与类型断言
最直观的模拟方式是利用 interface{}(Go 1.18后为 any)接收任意类型,再通过类型断言取出具体值。这种方式实现简单,调用方在运行时决定如何处理。
下面代码展示了一个可能持有 int 或 string 的容器:
package main
import "fmt"
func printValue(v interface{}) {
if s, ok := v.(string); ok {
fmt.Println("字符串:", s)
return
}
if i, ok := v.(int); ok {
fmt.Println("整数:", i)
return
}
fmt.Println("未知类型")
}
func main() {
printValue("hello")
printValue(42)
}
这种模式的优势是零额外结构、代码量少,适合一次性处理或原型阶段。但缺点同样明显:编译器无法帮你检查是否遗漏了某种类型,若传入未预期的类型,逻辑会静默走到默认分支;另外频繁的断言在热点路径上有轻微性能损耗。
当联合类型的候选种类固定且较少时,可以改用类型开关(type switch)让分支更清晰,但这仍属于运行时策略,没有编译期保障。
带标签的结构体模式
为了在编译期获得更多安全性,并让「当前是哪种类型」显式可查,常见做法是定义一个结构体,里面用一个枚举字段标记活跃类型,再为每种可能类型准备对应字段或用一个空接口存值。
以下示例用明确标签与独立字段避免误用:
package main
import "fmt"
type Kind int
const (
KindInt Kind = iota
KindString
)
type Union struct {
kind Kind
intVal string
strVal int
}
func NewInt(v int) Union {
return Union{kind: KindInt, intVal: fmt.Sprint(v)}
}
func NewString(v string) Union {
return Union{kind: KindString, strVal: len(v)}
}
func (u Union) Describe() string {
switch u.kind {
case KindInt:
return "int包装:" + u.intVal
case KindString:
return "string长度:" + fmt.Sprint(u.strVal)
default:
return "空"
}
}
func main() {
a := NewInt(10)
b := NewString("go")
fmt.Println(a.Describe())
fmt.Println(b.Describe())
}
这个模式把「类型信息」提升为一等公民,调用方必须先看 kind 再决定读哪部分数据,有效防止了错把字符串当数字用的低级bug。代价是每加一种类型就要改结构体并补充分支,扩展性一般。
如果候选类型很多,可以把值统一放到 interface{} 字段,仅用标签约束使用方式,这样既保留扩展性,又比纯空接口多了一层语义说明。
接口抽象与泛型约束
Go 1.18引入泛型后,我们可以用类型参数表达「多种类型但共享行为」。虽然不是传统联合类型,但能以约束方式模拟「满足某组方法的类型之一」。
示例用泛型函数处理不同数值类型:
package main
import "fmt"
type Number interface {
~int | ~float64
}
func add[T Number](a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(add(1.5, 2.5))
}
泛型适合「类型不同但操作一致」的场景,编译期就能确定类型集合,安全且高效。但它不能表达「字段结构不同」的联合,例如不能同时是用户结构和订单结构还共用一个函数逻辑,此时仍需回到标签结构体或接口。
综合来看,若只是临时承载多类型值,空接口加断言最快;若数据会在系统多处流转且类型错乱代价高,带标签结构体更稳;若行为统一,泛型是最优解。
模式选择建议
我们可以从三个维度判断该用哪种策略:类型数量是否固定、是否需要编译期检查、值是否共享操作方法。下面用表格归纳:
| 场景特征 | 推荐模式 | 主要风险 |
|---|---|---|
| 类型少、临时使用 | 空接口+类型断言 | 运行时遗漏分支 |
| 类型固定、需显式区分 | 带标签结构体 | 扩展时需改结构 |
| 类型多、行为一致 | 泛型约束 | 无法表达结构差异 |
在跨服务边界反序列化外源数据时,建议优先采用带标签结构体,并将解析逻辑集中在包内,避免联合类型的判断散落到业务各处。这样即使上游改动字段含义,也只需调整一个地方。
最后提醒,Go的 interface 本身就能承载多态,若你的联合类型其实只是「不同实现做同一件事」,直接用接口方法抽象比模拟联合更地道,也更符合Go的惯用法。
Gounion_typeinterface修改时间:2026-08-04 03:12:21