空接口、any 与类型 switch
有一个特别的接口:它一个方法都不要求。既然「有对的方法就满足」,那零个方法的接口,任何类型都满足 —— 这就是空接口 interface{},现在有个更短的名字 any。它能装下一切,代价是你拿到手时完全不知道里面是什么。这一章讲怎么把它安全地摊开:类型断言和类型 switch,以及它和 Kotlin 的 when (x) { is Int -> } 差在哪。
零个方法的接口,谁都满足
顺着上一章的逻辑推:一个接口要求越少方法,能满足它的类型越多。那么,一个要求零个方法的接口呢?
interface{} // 空接口:零个方法要求
any // Go 1.18 起,这是 interface{} 的官方别名,完全等价
任何类型都有「至少零个方法」,所以任何类型都满足空接口。any 因此成了 Go 版的「万能容器」—— 它能装下 int、string、你的 struct、切片、任何东西:
var x any = 42
x = "hello"
x = []int{1, 2, 3}
x = User{Name: "harry"} // 都行,any 来者不拒
你会在这些地方碰到它:fmt.Println(a ...any) 能打印任意东西、JSON 解析出的 map[string]any、泛型出现之前的通用容器。它相当于 Java 的 Object、Kotlin 的 Any。
装进 any 的那一刻,你把类型信息丢了。编译器只知道「这是个 any」,不知道里面是 int 还是 string,你没法对它做任何具体操作(不能 x + 1、不能 x.Name)。要用它,你必须先「问出」它的真实类型,再把它取回来 —— 这一步是有运行时开销、也有出错可能的。
所以 Go 社区对 any 的态度是:它是逃生舱,不是日常工具。能用具体类型就用具体类型,能用小接口就用小接口,能用泛型(卷 VI)就用泛型 —— 只有在真的「什么类型都可能」时(比如通用序列化、打印)才用 any。滥用它,等于放弃了 Go 静态类型的全部好处。
取回类型:类型断言 x.(T)
从 any 里把具体类型「问」回来,第一把工具是类型断言:
var x any = "hello" s := x.(string) // 断言 x 是 string,取回来。s 现在是普通 string fmt.Println(len(s)) // 5,能当字符串用了 n := x.(int) // 断言错了!x 不是 int → 运行时 panic
直接 x.(int) 断言错了会 panic,很危险。所以正确姿势永远是comma-ok 形式 —— 还记得第 7 章 map 那个 v, ok 吗,同一个惯用法:
if s, ok := x.(string); ok {
// 断言成功,s 是 string,用它
fmt.Println(len(s))
} else {
// x 不是 string,ok 是 false,不会 panic
}
s, ok := x.(string):成功则 ok 为 true、s 是取回的值;失败则 ok 为 false、s 是零值、不 panic。永远用带 ok 的形式,除非你 100% 确定类型(那种直接断言,错了就该 panic)。
问「它到底是哪种类型」:类型 switch
当一个 any 可能是好几种类型、你要分情况处理时,一个个 if x.(T) 太笨。Go 给了个专门的形态 —— 类型 switch。下面这台 Demo 就是它,选不同的值,看它命中哪个 case:
语法是 switch v := x.(type),注意那个特殊的 .(type) 只能用在 switch 里:
switch v := x.(type) {
case nil:
fmt.Println("是 nil")
case int:
fmt.Println("是 int,能算", v*2) // 在这个 case 里,v 就是 int
case string:
fmt.Println("是 string,长度", len(v)) // 这里 v 是 string
case []int:
fmt.Println("是 int 切片")
default:
fmt.Printf("别的类型: %T\n", v) // v 保持 any
}
妙处在 v:在每个 case 分支里,v 自动就是那个 case 的具体类型,直接能用(在 case int 里 v 是 int,能做乘法;在 case string 里 v 是 string,能取 len)。这就是把 any 安全摊开的标准方式:一次 switch,把「不知道是什么」变成「知道是什么、且已经取回来了」。
类型 switch 长得像 Kotlin 的 when (x) { is Int -> ... },但有个你必须记住的区别:Go 的类型 switch 匹配的是「任意类型」,编译器不会、也没法帮你检查有没有漏掉某种情况。
Kotlin 里对一个 sealed class 做 when,编译器能穷举它所有子类,你漏了一个它会警告甚至报错 —— 因为 sealed 的子类是封闭、已知的。而 Go 的 any 能是宇宙中任何类型,是开放的,编译器不可能知道「所有可能」,也就没法帮你穷举检查。所以你的类型 switch 必须自己想周全,并且几乎总要带一个 default 兜底。这是下一节的重点。
那 Go 怎么表达「封闭的一组类型」
你在 Kotlin 里爱用 sealed class + when 表达「这个东西只可能是这几种之一,且我处理全了」。Go 没有 sealed class(第 1 章账本里的一员)。它的替代方案是「接口 + 类型 switch」的组合,但要接受一个哲学差异:
// 用一个接口代表「一个表达式节点」
type Expr interface {
isExpr() // 一个私有方法,只有本包类型能实现 —— 变相「封闭」
}
type Num struct{ Val float64 }
type Add struct{ L, R Expr }
func (Num) isExpr() {} // 让 Num 满足 Expr
func (Add) isExpr() {}
// 用类型 switch 分派
func eval(e Expr) float64 {
switch e := e.(type) {
case Num: return e.Val
case Add: return eval(e.L) + eval(e.R)
default: panic("未知的 Expr 类型") // 兜底:新增类型忘了处理时炸出来
}
}
那个私有方法 isExpr() 是个惯用小技巧:因为只有本包的类型才能实现它(方法名小写,包外碰不到),你就变相地把「谁能是 Expr」限制在了本包内 —— 这是 Go 版的「半封闭」。但注意,即便如此,编译器仍然不会强制你的 switch 处理完所有情况,你还是得靠那个 default: panic 在运行时兜底。Kotlin 用编译期穷举保证你不漏,Go 用运行时 panic 让你漏了当场知道 —— 又一次「编译期保证 vs 运行时暴露」的取舍。
Kotlin 对照
Any → any:几乎一一对应,都是万能顶层类型,都建议少用。
智能转换 if (x is String) { x.length } → 类型断言 if s, ok := x.(string); ok:都是「检查类型后安全使用」,但 Kotlin 的智能转换是隐式的(检查后 x 自动变 String),Go 是显式的(你得用 s 这个新变量接住取回的值)。又是 Go 「不玩隐式魔法」的一贯风格。
sealed class + 穷尽 when → 接口 + isXxx() + 类型 switch + default:这是最大的能力差。Kotlin 的 sealed + when 给你编译期穷尽检查,是它一个很强的安全网。Go 给不了这个 —— 它的类型 switch 面对的是开放世界,只能靠 default: panic 在运行时兜底。迁移时你会明确怀念这一点:Go 里没有编译器帮你盯着「加了新类型、有没有漏改某个 when」。你只能靠测试和那句 panic。这是 Go 为了「结构化、开放的接口」付出的实打实的代价,诚实地告诉你,别指望它。
这一章的一句话
空接口 any(interface{})零方法要求,谁都满足,能装下一切但丢掉类型信息,是逃生舱不是日常工具;用类型断言 x.(T)(永远带 comma-ok)或类型 switch 把它安全摊开;而它和 Kotlin 最大的区别是——面对开放世界,Go 的类型 switch 不做穷尽检查,你得自己用 default 兜底。
下一章我们回答那个从第 1 章就欠着的大问题:Go 没有继承,那「复用一个类型的行为」怎么办?答案是嵌入 —— 组合优于继承的 Go 式实现。