Go基础:利用反射来修改变量的值

Go语言精选

共 10083字,需浏览 21分钟

 ·

2021-06-03 00:14

本文内容:

  • Go语言反射的可寻址概念

  • Go语言反射的可修改概念

  • Go语言反射的使用示例

可寻址

回想一下Go语言的表达式,比如xx.f[1]*p这样的表达式表示一个变量,而x+1f(2)之类的表达式则不表示变量。一个变量是一个可寻址的存储区域,包含了一个值,并且可以通过这个地址来更新值。

reflect.Value也有一个类似的区分,有些是可寻址的:

x := 2                  // 是否可寻址
a := reflect.ValueOf(2) // no
b := reflect.ValueOf(x) // no
c := reflect.ValueOf(&x) // no
d := c.Elem()   // yes(x)
  • a里面的值是不可寻址的,它包含的是整数2的一个副本。

  • b也是如此。

  • c里面的值也是不可寻址的,它包含的是指针&x的一个副本。

  • 通过reflect.ValueOf(x)返回的reflect.Value对象都是不可寻址的。

  • d是通过对c中的指针提领出来的,所以它是可寻址的。

  • 调用reflect.ValueOf(&x).Elem()可以获得任意变量x的可寻址的Value值。

可以通过变量的CanAddr()方法来询问reflect.Value变量是否可寻址:

fmt.Println(a.CanAddr())    // false
fmt.Println(b.CanAddr()) // false
fmt.Println(c.CanAddr()) // false
fmt.Println(d.CanAddr()) // true

我们可以通过一个指针来间接获取一个可寻址的reflect.Value对象,即使这个指针本身是不可寻址的。

可寻址的常见规则在反射包的CanAddr()方法的注释上有清晰的说明:

  1. 切片成员(an element of a slice)

  2. 可寻址数组成员(an element of an addressable array)

  3. 可寻址结构体的字段(a field of an addressable struct)

  4. 解析指针的内容(the result of dereferencing a pointer)

从一个可寻址的reflect.Value()获取变量需要三步:

  1. 调用Addr(),返回一个Value,其中包含一个指向变量的指针。

  2. 在这个Value上调用Interface(),得到一个包含这个指针的interface{}值。

  3. 最后,如果我们知道变量类型,使用类型断言将接口内容转换成一个普通指针

之后就可以通过这个指针来更新变量了。

Set()

除了通过指针来更新变量之外,还可以直接调用reflect.Value.Set()方法来更新变量。

d.Set(reflect.ValueOf(4))
fmt.Println(x) // "4"

一般由编译器在编译期检查可赋值性条件,在这种情况下则是在运行时Set()方法来检查。上面的变量和值都是int类型,但如果变量类型是int64,这个程序就会崩溃。

// 崩溃:int64不可被赋值给int
d.Set(reflect.ValueOf(int64(5)))

当然,在一个不可寻址的reflect.Value上调用Set()方法也会崩溃:

// 崩溃:在不可寻址的值上使用Set()
b.Set(reflect.ValueOf(3))

除了基本的Set()方法之外,还有一些衍生函数:SetInt()SetUint()SetString()SetFloat()

这些方法还有一定程度的容错性。只要变量类型是某种带符号的整数,比如SetInt(),甚至可以是底层类型是带符号整数的命名类型,都可以成功。如果值太大了还会无提示地截断它。但需要注意的是,在指向interface{}变量的reflect.Value上调用SetInt()会崩溃,调用Set()反而没有问题。

x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2) // OK,x = 2
rx.Set(reflect.ValueOf(3)) // OK,x = 3
rx.SetString("hello") // 崩溃:字符串不能赋给整数
rx.Set(reflect.ValueOf("hello")) // 崩溃:字符串不能赋给整数

var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2) // 崩溃:在指向接口的Value上调用SetInt
ry.Set(reflect.ValueOf(3)) // OK,y = int(3)
ry.SetString("hello") // 崩溃:在指向接口的Value上调用SetString
ry.Set(reflect.ValueOf("hello")) // OK,y = "hello"

可修改

反射可以读取未导出结构字段的值,但不能更新这些值。

一个可寻址的reflect.Value会记录它是否通过遍历一个未导出字段来获得,如果是通过未导出字段来获得的,则不允许被修改。所以在更新变量前用CanAddr()来检查并不能保证正确。CanSet()方法才能正确地报告一个reflect.Value是否可寻址且可更改。

使用示例

  • 访问结构体字段标签

  • 显示类型的方法

访问结构体字段标签

在学习了反射的基本使用之后,我们编写一个示例程序。

在一个Web服务器中,绝大部分HTTP处理函数的第一件事就是提取请求参数到局部变量中。我们将定义一个工具函数Unpack(),使用结构体字段标签来简化HTTP处理程序的编写。

首先,我们展示如何使用这个方法。下面的search()函数就是一个HTTP处理函数,它定义一个变量data,data的类型是一个字段与HTTP请求参数对应的匿名结构。结构体的字段标签指定参数名称,这些名称一般比较短,含义也比较模糊,毕竟URL长度有限,不能浪费。Unpack函数从请求中提取数据来填充这个结构体,这样不仅可以方便地访问,还避免了手动转换类型。

func search(resp http.ResponseWriter, req *http.Request) {
    var data struct{
        Labels []string `http:"1"`
        MaxResults int `http:"max"`
        Exact bool `http:"x"`
    }
    data.MaxResults = 10
    if err := Unpack(req, &data); err != nil {
        http.Error(resp, err.Error(), http.StatusBadRequest) // 400
        return
    }
}

我们将重点放在Unpack()函数的实现上:

func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }

    // 创建字段映射表,键为有效名称,值为字段名称
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem()
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i)
        tag := fieldInfo.Tag
        name := tag.Get("http")
        if name == "" {
            name = strings.ToLower(fieldInfo.Name)
        }
        fields[name] = v.Field(i)
    }

    // 对请求中每个参数更新结构体中对应的字段
    for name, values := range req.Form {
        f := fields[name]
        if !f.IsValid() {
            continue // 忽略不能识别的HTTP参数
        }
        for _, value := range values {
            if f.Kind() == reflect.Slice {
                elem := reflect.New(f.Type().Elem()).Elem()
                if err := populate(elem, value); err != nil {
                    return fmt.Errorf("%s : %v", name, err)
                }
                f.Set(reflect.Append(f, elem))
            } else {
                if err := populate(f, value); err != nil {
                    return fmt.Errorf("%s : %v", name, err)
                }
            }
        }
    }
    return nil
}

下面的Unpack()函数做了三件事情:

  1. 首先,调用req.ParseForm()来解析请求。在这之后,req.Form就有了所有的请求参数,这个方法对HTTPGETPOST请求都适用。

  2. 然后,Unpack()函数构造了一个从每个有效字段名到对应字段变量的映射。在字段有标签时,有效字段名与实际字段名可能会有差别。reflect.TypeField()方法会返回一个reflect.StructField类型,这个类型提供了每个字段的名称、类型以及一个可选的标签。它的Tag字段类型为reflect.StructTag,底层类型为字符串,提供了一个Get()方法用于解析和提取对于一个特定键的子串。

  3. 最后,Unpack()遍历HTTP参数中的键值对,并且更新对应的结构体字段。注意,同一个参数可能会出现多次。如果有这种情况并且字段是slice类型,则这个参数的所有值都会追加到slice里。如果不是,则这个字段会被多次覆盖,仅有最后一个值才是有效的。

populate()函数负责从单个HTTP请求参数值填充单个字段v(或者slice字段中的每个元素)。现在,它仅支持字符串、有符号整数和布尔值。对于其他类型的支持,可以自己练习。

func populate(v reflect.Value, value string) error {
    switch v.Kind() {
    case reflect.String:
        v.SetString(value)
    case reflect.Int:
        i, err := strconv.ParseInt(value, 10, 64)
        if err != nil {
            return err
        }
        v.SetInt(i)
    case reflect.Bool:
        b, err := strconv.ParseBool(value)
        if err != nil {
            return err
        }
        v.SetBool(b)
    default:
        return fmt.Errorf("unsupported kind %s", v.Type())
    }
    return nil
}

显示类型的方法

使用reflect.Type来显示一个任意值的类型并枚举它的方法:

func Print(x interface{}) {
    v := reflect.ValueOf(x)
    t := v.Type()
    fmt.Printf("type %s\n", t)
    for i := 0; i < v.NumMethod(); i++ {
        methodType := v.Method(i).Type()
        fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,                     strings.TrimPrefix(methodType.String(), "func"))
    }
}

reflect.Typereflect.Value都有一个叫做Method()的方法,每个t.Method(i)都会返回一个描述了这个方法的名称和类型reflect.Method类型实例。而每个v.Method()都会返回一个已绑定接收者方法reflect.Method类型实例。

使用reflect.Value.Call()方法可以调用Func类型的Value,但本示例程序只需要它的类型。

下面就是两个类型time.Duration和*strings.Replacer的方法列表:

Print(time.Hour)
// type time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Microseconds() int64
// func (time.Duration) Milliseconds() int64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Round(time.Duration) time.Duration
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string
// func (time.Duration) Truncate(time.Duration) time.Duration

Print(new(strings.Replacer))
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)

推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报