Reflection in Golang

Static Typed Go

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

1
2
3
4
5
6
7
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)即可得到目标类型的变量。但在实际开发过程中,我们需要的不仅仅是基本类型的转换,譬如对于给定的接口类型:

1
2
3
4
5
Type I interface {
Read(b Buffer) bool
Write(b Buffer) bool
Close()
}

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

1
2
3
4
5
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{},若想正确访问数据我们就需要对返回结果进行类型断言:

1
2
3
4
5
6
7
8
9
10
11
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

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

1
2
3
4
5
type Config struct {
Port int64
Path string
Debug bool
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
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中,提供了一系列方法让我们“动态”地去判断变量的类型:

1
2
3
4
5
6
7
8
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动态判断该进行何种类型转换并存储到那个属性中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中反射的细节与需要注意的地方,值得一读。