《Go语言圣经》——反射和泛型

501 阅读5分钟

反射是什么?

反射其实是一种动态的获取对象的类型和方法。有的时候我们希望通过抽象出一个能够接受多个类型的函数/方法来减少我们的代码量。就好比c++中的泛型,例如实现一个add函数,这个函数可以接受不同的类型,并实现相加,功能如下:

template <typename T>
auto add(T a, T b) -> decltype(a + b) {
    return a + b;
}

这个函数就基本满足要求,对于不同类型的代码都可以实现相加。那么go语言是如何实现类似的需求呢?其中一种方法就是通过反射。
go语言的变量包括(type, value)两部分,类型又分为静态类型和动态类型。拿c++举例子:

Animal* p = new Dog();

变量p的静态类型是Animal类,动态类型则是Dog。而在go语言中,我们知道空接口可以类型变量可以接收任何类型的变量。形如

//静态类型都是any,即空接口
//动态类型分别是string, int, slice
var i any = "abc"
var i any = 123
var i any = []int{1, 2, 3}

any在go语言中是一个特定的类型:type any interface{},也就是空接口。上面3个变量的type部分分别记录它们的动态类型,value则记录具体的值。正因为如此,在go语言中,很多时候nil != nil,因为变量的值虽然相同,但是类型不同,如下:

var a, b, c any = "abc", 123, "a" + "b" + "c"
fmt.Println(a == b)    // false
fmt.Println(a == c)    // true

var x *int = nil
var y *string = nil
//fmt.Println(x == y)  // 不可比较,无法编译
var ix, iy any = x, y
var i any = nil
fmt.Println(ix == iy)  // false 
fmt.Println(ix == i)   // false
fmt.Println(iy == i)   // false

我们希望实现一个通用的函数,那我们就需要通过any类型去接收变量,然后通过反射去获取它的类型,然后针对每个类型做不同的处理。好在reflect包已经帮我们实现了相应的功能。

relfect

var i any = "hello"
fmt.Printf("type: %s\n", reflect.TypeOf(i))     // type: string
fmt.Printf("value: %v\n", reflect.ValueOf(i))   // value: hello
i = 123
fmt.Printf("type: %s\n", reflect.TypeOf(i))     // type: int
fmt.Printf("value: %v\n", reflect.ValueOf(i))   // value: 123
i = []int{1, 2, 3}
fmt.Printf("type: %s\n", reflect.TypeOf(i))     // type: []int
fmt.Printf("value: %v\n", reflect.ValueOf(i))   // value: [1, 2, 3]

fmt.Printf("type: %T\n", i) //%T效果和reflect.TypeOf()类似

通过reflect.TypeOf()reflect.ValueOf()我们可以获取某个变量的类型和值。实际使用的时候不用这么麻烦,我们可以通过类型断言来判断某个变量的类型:

  • 如果成功,则返回该类型的值
var i any = "123"
v, ok := i.(string)
if !ok {
    fmt.Println("type is not string")
}
fmt.Println(v) // “123”

我们也可以通过type switch类型断言来获取类型,然后对不同的类型做不同的处理:

package main

import (
    "fmt"
    "strconv"
)

//接收任意类型,转换为string
func toString(i interface{}) string {
    switch v := i.(type) {
    case string:
       return v
    case int:
       return strconv.Itoa(v)
    case float64:
       return strconv.FormatFloat(v, 'f', -1, 64)
    case bool:
       if v {
          return "true"
       }
       return "false"
    default:
       return ""
    }
}

func main() {
    var s = toString(123)
    fmt.Println(s)              // 123
    s = toString(true)
    fmt.Println(s)              // true
    s = toString(1.234)
    fmt.Println(s)              // 1.234
}

类型断言可以帮助我们处理不同的类型的情况,但我们不可能把所有的类型都罗列出来。如果传进来的是复合类型呢。我们可以使用反射,它功能更加强大:

package main

import (
    "fmt"
    "reflect"
    "strconv"
)

// 接收任意类型,转换为string
func toString(i interface{}) string {
    v := reflect.ValueOf(i)
    switch v.Kind() {
    case reflect.Invalid:
       return "<invalid>"
    case reflect.Bool:
       return strconv.FormatBool(v.Bool())
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
       return strconv.FormatInt(v.Int(), 10)
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
    case reflect.Float32, reflect.Float64:
       return strconv.FormatFloat(v.Float(), 'g', -1, 64)
    case reflect.String:
       return v.String()
    }
    return ""
}

func main() {
    var s = toString(123)
    fmt.Println(s) // 123
    s = toString(true)
    fmt.Println(s) // true
    s = toString(1.234)
    fmt.Println(s) // 1.234
}

泛型

泛型和用反射都能去实现多态的效果,但它们实现思路不一样。泛型是在编译期间确定类型,是编译期多态;而interface{}则类似于c语言中的void*,通过变量的元数据在运行期间获取类型进而根据类型做出不同的动作。
函数要支持这种泛型行为,需要有2个前提条件

  1. 对于函数而言,需要一种方式来声明这个函数到底支持哪些类型的参数
  2. 对于函数调用方而言,需要一种方式来指定传给函数的到底是int类型的map还是float类型的map

为了满足以上前提条件:

  1. 在声明函数的时候,除了需要像普通函数一样添加函数参数之外,还要声明类型参数(type parameters)。这些类型参数让函数能够实现泛型行为,让函数可以处理不同类型的参数。
  2. 在函数调用的时候,除了需要像普通函数调用一样传递实参之外,还需要指定泛型函数的类型参数对应的类型实参(type arguments)。

每个类型参数都有一个类型限制(type constraint),类型限制就好比类型参数的meta类型,每个类型限制会指明函数调用时该类型参数允许的类型实参。

尽管一个类型参数的类型限制是一系列类型的集合,但是在编译期,类型参数只会表示一种具体的类型,也就是函数调用方实际使用的类型实参。如果类型实参的类型不满足类型参数的类型限制,编译就会失败。例子如下:

package main

import "fmt"

// 接收int和float32类型的切片,并求和
func sumSlice[T int | float32](s []T) T {
    var sum T
    for _, i := range s {
       sum += i
    }
    return sum
}

func main() {
    var ints = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    var floats = []float32{1.334, 2.34, 5.234}
    fmt.Println(sumSlice(ints))
    fmt.Println(sumSlice(floats))
}

如果把上述代码中的类型约束改为T any则会有问题,因为它无法保证所有传进来的类型都支持+这个运算符。C++中也是类似,使用者调用模板函数必须确保自定义类型支持相应的运算符,否则运行也会报错。只不过看来go语言更加保守。

#include <iostream>

using namespace std;

struct Point {
    int x;
    int y;
};

template <typename T>
auto add(T a, T b) -> decltype(a + b) {
    return a + b;
};

int main()
{
    cout << add(3, 4) << endl;    // 7
    Point p1(2, 3), p2(3, 4);
    cout << add(p1, p2) << endl;  //报错,需要重载运算符+
    return 0;
}