Golang如何使用reflect操作struct

444 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Golang的Reflect包是做什么用的?

Reflection in computing is the ability of a program to examine its own structure, particularly through types;it's a form of metaprogramming 反射是程序检查其自身结构的能力,尤其是通过类型;它是元编程的一种形式。 From The Go Blog - The Laws of Reflection

上面是反射的定义,也就是golang Reflect包设计的目的。

元编程是什么?

元编程是一种编程技术,其中计算机程序具有将其他程序视为其数据的能力。这意味着可以设计一个程序来读取,生成,分析或转换其他程序,甚至在运行时对其进行修改。在某些情况下,这使程序员可以最大程度地减少表达解决方案的代码行数,从而减少开发时间。它还使程序具有更大的灵活性,可以有效地处理新情况而无需重新编译。 From: wiki - Metaprogramming

Golang是静态类型语言,每个变量都有自己的类型,在编译时就会有已知的变量类型。但是,有一个重要的类型interface

在Golng中,interface和reflection是互相依赖的特性,了解什么事反射前,需要了解好interface。

Interface是指一些列方法和变量的集合,只定义,并不实现具体,一个典型的例子就是io.Readerio.Wirter

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

只要实现了Read方法的,就是一个Reader,interface在很多其他语言也是有的。例如实现了Read方法的struct value,都可以作为参数传到func Printer(r io.Reader) {}

在Golang中,空的interface{}是代表可以是任意类型。

现在就可以一起看看golang中的反射机制了

Reflect.TypeOf和Reflect.ValueOf

从简单的来看,反射只是操作interface类型变量中的一堆key-value的一些方法,reflection包里面对应有reflect.Typereflet.Value两个类型。

下面针对golang已有的变量类型去看看如何通过reflect去操作变量的例子。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x)) 
    // =================
    // type: float64
   
    var x float64 = 3.4
	fmt.Println("value:", reflect.ValueOf(x).String())
	// =================
	// value: <float64 Value>
}

reflect.Typereflect.Value本身就有很多方法去查看和操作变量。例如:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns a uint64.

使用reflect.ValueInterface()方法,可以将refect.Value又转化为interface类型进行操作。

y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

使用反射操作变量

去修改反射对象的值,必须确保反射对象的值是可以修改的,reflect.ValueCanSet()方法

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
// ==============
// settability of v: false

这里不能进行修改的原因是,变量v只是变量x的一个copy,而不是变量x本身

而正确的方式应该是,先取变量地址的值,然后通过指针获取变量的值

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: 取x的地址.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
// ================
// type of p: *float64
// settability of p: false

v := p.Elem() // Note: 取地址指向的具体值
fmt.Println("settability of v:", v.CanSet())
// ==============
settability of v: true

使用reflect操作struct

上面说了这么多,现在我们再回到主题,这才是这边文章想要说的东西。

interface{}可以是任意类型,也就是说可以是一个struct

上面的简单例子,是用反射修改变量的值,如果变量类型是struct,其实同样可以也是修改struct field的值的

先来看看将struct类型变成reflect.Value,可以拿到什么信息

type T struct {
    A int
    B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

// ===================
// 0: A int = 23
// 1: B string = skidoo

例子中可以看到,对于struct的field,我们可以拿到字段名,字段类型,还有字段的值,另外其实我们也可以拿到字段的Tags(以后再介绍Tags)。

而修改struct字段,可以这样操作

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
// ===============
// t is now {77 Sunset Strip}