引子:Go 标准库上的 any
前段时间在查看 Go 标准库 encoding/json 的属性时,见到 decode.go 上 unmarshal 方法的参数类型是一个 any(GitHub Link),具体的代码片段如下:
func (d *decodeState) unmarshal(v any) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Pointer || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
d.scanWhile(scanSkipSpace)
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
err := d.value(rv)
if err != nil {
return d.addErrorContext(err)
}
return d.savedError
}
隔壁写 TS 刚过来的瞬间就触发了警报。为什么这里没有定义具体的参数类型?还写了显式的 any ?在 IDE 上继续往里面看一下这个 any,去到 builtin.go 文件下:
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
原来就是底层定义的对于空接口 interface{} 的一个类型别名。
写过 TS 的很自然地产生了一个疑问以及比较:Go 还有 TS 这两种语言都定义了 any,他们有什么异同吗?
TS 中的 any:不安全类型
TS 中的 any 我们都很熟悉了,一个 any 类型的变量可以接收 / 赋值给任意一个有着其他具体类型的变量。同时,对 any 类型的变量访问其任何属性或者调用任何方法都是被允许的,就算这些属性和方法在编译时并不存在也不会报错。
// 隐式类型推断为 any
let value;
value = 42; // OK
value = "hello"; // OK
// 显式声明为 any
let value: any;
value = 42; // OK
value = true; // OK
value.foo.bar = "world"; // OK
// any 类型的函数参数
function sum(a: any, b: any) {
return a + b;
}
sum(1, 2); // 返回 3
sum("a", "b"); // 返回 "ab"
很明显 any 的使用使得 TS 代码在编译时绕过了类型检查,但同时却也在代码中埋下了一颗潜藏的、在运行时会随时随地爆炸的雷。也因此,在 TS 代码中我们一直是强调要消除各种显式的还有隐式的 any,通过各种方式手段来保证类型的安全,譬如常见的几种:
- 使用 unknown 取代不明确类型的 any
let value: unknown;
value = 10;
value = 'height';
if (typeof value === 'string') {
const upperCaseValue = value.toUpperCase();
console.log(upperCaseValue);
} else if (typeof value === 'number') {
console.log(value.toFixed(2));
}
- 使用泛型约束不明确类型的 any
interface LengthType {
length: number;
}
// T 继承自 LengthType,通过泛型约束它必须具有 length 属性
function get<T extends LengthType>(arg: T): number {
return arg.length;
}
- 使用类型保护消除局部范围下的 any
interface Foo {
foo: number;
common: string;
}
function isFoo(arg: any): arg is Foo {
return (arg as Foo).foo !== undefined;
}
function funcFoo(arg: any) {
if (isFoo(arg)) {
console.log(arg.foo); // ok
console.log(arg.bar); // TS Error
}
}
Go 中的 any:空接口 interface{}
而到了 Go,在 Go 1.18 版本以后引入了 any 作为空接口 interfac{} 的别名,两者在语义上是完全等价的。也就是说在 Go 中,本质上是不存在 any 这么一个类型的,有的也只是一个没有实现任何一个方法的空接口。
那空接口在 Go 中有什么作用呢?
-
存储任意类型的值
很显然的,由于任何类型都实现了 至少零个 方法,所以任何类型的值都可以被存储到空接口变量上。
var x any x = 42 // 存储整数 x = "hello" // 存储字符串 x = []int{1, 2, 3} // 存储切片 -
实现泛型函数处理未知类型的参数数据
使用空接口作为函数参数类型,实现可以接受任意值的泛型函数
func printAny(x any) { fmt.Println(x) } -
实现动态类型转换
利用类型断言,从空接口值中获取到具体的值及其类型
var c string var d interface{} = 123 c, ok := d.(string) if ok != false { fmt.Println(c) // 输出: 10 }
从上面的例子可以看出,Go 中通过空接口提供了很大的灵活性,利用其特性在实现泛型编程以及处理一些未知类型的数据方面可以发挥极大作用。与此同时,不同于 TS,Go 不会让 any 成为在运行时随时会爆zha的雷,而是会在编译期对类型安全进行保证。
func main() {
type Val struct {
value string
}
var e Val
v := Val{"hello"}
var f any = v // ✅ OK
// 直接将一个空接口类型的变量 f 赋值给指定类型的变量 e 时,会在编译期报错:
// cannot use f (variable of type interface{}) as Val value in assignment: need type assertion
e := f // ❌ Error
// 要求通过类型断言,明确空接口的类型转换
e := f.(Val) // ✅ OK
fmt.Println(e)
// 并且也无法直接访问空接口类型变量下的其他属性,会在编译期报错:
// f.value undefined (type interface{} has no field or method value)
f.value = "world" // ❌ Error
// 只有类型断言后,明确了变量的具体类型之后才可安全地进行访问
e.value = "world" // ✅ OK
}
从上面的片段可以知道,空接口类型的变量尽管可以接受一个其他具体类型的值(如上例中 Val struct 类型的 v)但是却不能直接赋值给一个其他类型的变量(如上例中的 e)。这样的代码将无法正确编译通过,会报出编译时错误。如果仔细看报错的信息会发现,Go 编译器也在报错信息上给出了正确的做法:需要类型断言!开发者必须在代码上显式地通过类型断言写法,明确地对空接口进行指定类型的转换,才能通过编译器的类型检查并且得到一个转换为指定类型之后的值,这时才允许将其赋值给指定的具体类型变量。并且,如果当在代码中尝试访问一个空接口值的字段或者方法时,Go 编译器会抛出类似 x.(type interface {}) has no field or method value 这样的错误。必须要首先进行类型断言,将这个空接口转换为具体类型之后,才能访问该类型的字段或者方法。这一点上,是 Go 比之 TS 在类型上更为安全的做法。
在使用类型断言时还需要注意一点的是,类型断言是对接口值进行的强制类型转换。一旦转换失败,程序将会直接 Panic。所以在不能保证类型转换必定成功的情况下,一般还要用上类型断言的第二个结果变量 ok 加以判断,对转换失败的情况( ok!=true )进行处理。
不过有正就有反。使用空接口带来了代码上极大的灵活性,却也同时要付出在运行时的额外开销。因为如此一来,Go 就必须在运行时对变量进行类型检查和转换,而这对于使用具体的接口类型却是不需要的。
reflection:查出 interface{} 户口
从前文我们知道,Go 中 interface{} 类型变量可以直接存储各种类型的值。换句话讲,就是一个来者不拒。那既然谁都能存,我们能知道存的到底是个啥吗?
在 TS 上,或许这挺难做到:获取一个被声明为 any 类型的变量上所存储的值的真实类型。我们可以通过类型保护在运行时缩小变量的类型范围,但这做法终究比较迂回,不能直接获取到一个 any 值在运行时的真实类型。
function handleValue(value: any) {
// 通过类型保护收缩类型
if (typeof value === "string") {
// 这里 value 被识别为 string 类型
console.log(value.length);
} else if (typeof value === "number") {
// 这里 value 被识别为 number 类型
console.log(value.toFixed(2));
}
}
但是来到 Go 中,有一个强大的能力特性却可以做到:反射(reflection)
func main() {
var a int32 = 123
var b string = "test"
var c struct{ foo string } = struct{ foo string }{"bar"}
var d []int = []int{1, 2, 3}
var e map[string]int = map[string]int{"one": 1, "two": 2}
var f any
f = a
fmt.Println("type: ", reflect.TypeOf(f)) // int32
f = b
fmt.Println("type: ", reflect.TypeOf(f)) // string
f = c
fmt.Println("type: ", reflect.TypeOf(f)) // struct{foo string}
f = d
fmt.Println("type: ", reflect.TypeOf(f)) // []int
f = e
fmt.Println("type: ", reflect.TypeOf(f)) // map[string]int
}
由上面这个 case 可以看到,在 Go 中,一个 any 类型的变量,不管其上具体存储的是什么类型的值,是原生基础类型也好,是自定义类型也好,通过反射(reflect.TypeOf)我们总能准确地获取到变量当下所存储值的具体的真实类型。
事实上,Go 中反射的能力还不止于此,获取变量的类型什么的还属于基操,通过反射甚至还可以做到更多事情,譬如:
- 拿到反射类型对象后,将其转化为接口类型变量
- 通过遍历 reflect.Type 的 Field 获知变量的所有成员
- 通过反射修改反射类型对象状态以及其值
- 通过反射动态地调用其方法等
由于本文仅是借 any 一窥 Go 的类型系统,对 reflection 更多相关内容就不过多探讨,可以直接查看:pkg.go.dev/reflect