Reflection in Golang

Static Typed Go

Go作为一门静态类型的语言,所有变量都定义了其所属于的类型,不同类型的变量间不能随意赋值,例如:

    var a int
    var b string

    a = 1
    b = "codeb2cc"

    a = b

a和b不是同一类型的变量,若尝试直接赋值将会报错cannot use b (type string) as type int in assignment,不同类型变量间的赋值需要进行类型转换(Conversion),这点与C/C++里是一致的。在Go里,对于变量x与目标类型T,类型转换需要满足以下其中一种情况:

  • x可以赋值为类型T的变量
  • x与T有着一致的实现类型(underlying types)
  • x与T都是匿名指针类型并且具有相同的实现类型
  • x与T都是整数/浮点数类型
  • x与T都是复数类型
  • x是整数/bytes片段/runes,T是string
  • x是string,T是bytes片段或runes

对于可以进行类型转换的变量,通过var c = float(100)即可得到目标类型的变量。但在实际开发过程中,我们需要的不仅仅是基本类型的转换,譬如对于给定的接口类型:

    Type I interface {
        Read(b Buffer) bool
        Write(b Buffer) bool
        Close()
    }

只要实现了这三种方法,就可以认为该类型是合法的、可操作的,但由于无法确定最终传入变量的类型,我们需要在使用前将其转化为我们已知的类型。这种情况下由于类型转化(Conversion)只关心数据,我们需要类型断言(Type Assertion)来帮助我们:

    var x I
    var y File

    x = y.(I)
    z, ok := y.(I)

类型断言判断变量是否为nil以及是否实现了断言所需要的接口,若断言通过将得到一个目标类型的变量,否则将出现run-time panic。若不希望在断言失败时程序可以继续执行,可以通过z, ok := y.(I)的形式判断变量断言是否成功,若失败z为目标类型的零值。

类型断言应用的常见例子是对JSON数据的解析。Go标准库中对JSON类与数组的解析返回的结果分别是map[string]interface{}[]interface{},若想正确访问数据我们就需要对返回结果进行类型断言:

    b := []byte(`{"Foo":"Bar", "Hello": ["W", "o", "r", "l", "d"]}`)

    var f interface{}
    err := json.Unmarshal(b, &f)

    // f类型为interface{}, 需要转换为map[string]interface{}来访问数据
    m := f.(map[string]interface{})
    fmt.Println(m["Foo"])

    // 同样的, m["Hello"]的类型为interface{}, 在正常访问前我们需要转换为对应的类型
    n := m["Hello"].([]byte)

Reflection

通过类型转换和类型断言,我们基本能够解决大多数问题,但单是可以解决是不够的,我们还需要更加方便(优雅)的解决方案。考虑这样一个场景,我们定义一个配置结构用来保存服务的配置信息,结构中用了多种类型:

    type Config struct {
      Port int64
      Path string
      Debug bool
    }

同时我们将服务的配置保存在文本中,在服务加载时读取进来:

Port=8000
Path=/tmp/go_reflection
Debug=true

对配置文件解析我们基本可以得到(string, []byte)这样的元组,我们需要将数据存储到Config中,由于结构中已经定义了对应的类型,因此我们在存储时需要进行类型转换:

    data = map[string][]byte{
      "Port": []byte("8000"),
      "Path": []byte("/tmp/go_reflection"),
      "Debug": []byte("true"),
    }

    config := Config{}
    i, err := strconv.ParseInt(string(data["Port"]), 10, 64)
    if err == nil {
      config.Port = i
    }

对于int64类型我们需要strconv.ParseInt,对于string我们需要string(),对于bool我们需要strconv.ParseBool,在上面这个例子中我们可以通过switch来判断每个配置项需要进行的转换,但在实际的应用中配置可能多达数十项,继续使用这种原始的方法简直就是一个噩梦。这时我们需要通过Go的Reflect模块来帮助我们更优雅地解决这个问题。

反射(Reflection)作为元编程的一种形式,赋予了我们在运行时判断变量类型的能力。Go的reflect模块通过将数据封装在reflect.Typereflect.Value中,提供了一系列方法让我们“动态”地去判断变量的类型:

    var x = 2013

    switch reflect.ValueOf(x).Kind() {
    case reflect.Int:
      fmt.Println("It's an int!")
    case reflect.String:
      fmt.Println("It's a string!")
    }

在我们配置文件的例子中,我们可以根据每个配置的Key动态判断该进行何种类型转换并存储到那个属性中:

    func (s *Config) Set(key string, value []byte) error {
        reflectValue := reflect.ValueOf(s).Elem()
        reflectField := reflectValue.FieldByName(key)
        switch reflectField.Kind() {
        case reflect.Int64:
            i, err := strconv.ParseUint(string(value), 10, 64)
            if err != nil {
                return err
            }
            reflectField.SetInt(i)
        case reflect.String:
            reflectField.SetString(string(value))
        case reflect.Bool
            i, err := strconv.ParseBool(string(value))
            if err != nil {
                return err
            }
            reflectField.SetBool(i)
        }
        return nil

这样,我们只需要通过config.Set("Port", []byte("8000"))一个方法就可以正确地存储配置信息,就算结构定义有变更也不需要重写解析方法。关于反射Golang Blog上有一篇详细的文章The Laws of Reflection,可以更好地帮助我们Go中反射的细节与需要注意的地方,值得一读。