Go语言学习笔记之反射用法详解
本文实例讲述了go学习笔记之反射用法。分享给大家供大家参考,具体如下:
一、类型(type)
反射(reflect)让我们能在运行期探知对象的类型信息和内存结构,这从一定程度上弥(mi)补了静态语言在动态行为上的不足。同时,反射还是实现元编程的重要手段。
和 c 数据结构一样,go 对象头部并没有类型指针,通过其自身是无法在运行期获知任何类型相关信息的。反射操作所需要的全部信息都源自接口变量。接口变量除存储自身类型外,还会保存实际对象的类型数据。
func typeof(i interface{}) type
func valueof(i interface{}) value
这 两个 反射入口函数,会将任何传入的对象转换为接口类型。
在面对类型时,需要区分 type
和 kind
。前者表示真实类型(静态类型),后者表示其基础结构(底层类型)类别 -- 基类型。
func main() {
var a x = 100
t := reflect.typeof(a)
fmt.println(t)
fmt.println(t.name(), t.kind())
}
输出:
x int
所以在类型判断上,须选择正确的方式
type y int
func main() {
var a, b x = 100, 200
var c y = 300
ta, tb, tc := reflect.typeof(a), reflect.typeof(b), reflect.typeof(c)
fmt.println(ta == tb, ta == tc)
fmt.println(ta.kind() == tc.kind())
}
除通过实际对象获取类型外,也可直接构造一些基础复合类型。
a := reflect.arrayof(10, reflect.typeof(byte(0)))
m := reflect.mapof(reflect.typeof(""), reflect.typeof(0))
fmt.println(a, m)
}
输出:
[10]uint8 map[string]int
传入对象 应区分 基类型 和 指针类型,因为它们并不属于同一类型。
x := 100
tx, tp := reflect.typeof(x), reflect.typeof(&x)
fmt.println(tx, tp, tx == tp)
fmt.println(tx.kind(), tp.kind())
fmt.println(tx == tp.elem())
}
输出:
int *int false int ptr true
方法 elem() 返回 指针、数组、切片、字典(值)或 通道的 基类型。
fmt.println(reflect.typeof(map[string]int{}).elem())
fmt.println(reflect.typeof([]int32{}).elem())
}
输出:
int int32
只有在获取 结构体指针 的 基类型 后,才能遍历它的字段。
type user struct {
name string
age int
}
type manager struct {
user
title string
}
func main() {
var m manager
t := reflect.typeof(&m)
if t.kind() == reflect.ptr {
t = t.elem()
}
for i := 0; i < t.numfield(); i++ {
f := t.field(i)
fmt.println(f.name, f.type, f.offset)
if f.anonymous { // 输出匿名字段结构
for x := 0; x < f.type.numfield(); x++ {
af := f.type.field(x)
fmt.println(" ", af.name, af.type)
}
}
}
}
输出:
user main.user 0 name string age int title string 24
对于匿名字段,可用多级索引(按照定义顺序)直接访问。
name string
age int
}
type manager struct {
user
title string
}
func main() {
var m manager
t := reflect.typeof(m)
name, _ := t.fieldbyname("name") // 按名称查找
fmt.println(name.name, name.type)
age := t.fieldbyindex([]int{0, 1}) // 按多级索引查找
fmt.println(age.name, age.type)
}
输出:
name string age int
fieldbyname() 不支持多级名称,如有同名遮蔽,须通过匿名字段二次获取。
同样地,输出方法集时,一样区分 基类型 和 指针类型。
type b struct {
a
}
func (a) av() {}
func (*a) ap() {}
func (b) bv() {}
func (*b) bp() {}
func main() {
var b b
t := reflect.typeof(&b)
s := []reflect.type{t, t.elem()}
for _, t2 := range s {
fmt.println(t2, ":")
for i := 0; i < t2.nummethod(); i++ {
fmt.println(" ", t2.method(i))
}
}
}
输出:
*main.b : {ap main func(*main.b) <func(*main.b) value> 0} {av main func(*main.b) <func(*main.b) value> 1} {bp main func(*main.b) <func(*main.b) value> 2} {bv main func(*main.b) <func(*main.b) value> 3} main.b : {av main func(*main.b) <func(*main.b) value> 0} {bv main func(*main.b) <func(*main.b) value> 1}
有一点和想象的不同,反射能探知当前包或外包的非导出结构成员。
"net/http"
"reflect"
"fmt"
)
func main() {
var s http.server
t := reflect.typeof(s)
for i := 0; i < t.numfield(); i++ {
fmt.println(t.field(i).name)
}
}
输出:
addr handler readtimeout writetimeout tlsconfig maxheaderbytes tlsnextproto connstate errorlog disablekeepalives nextprotoonce nextprotoerr
相对 reflect 而言,当前包 和 外包 都是“外包”。
可用反射提取 struct tag,还能自动分解。其常用于 orm 映射,或数据格式验证。
type user struct {
name string `field:"name" type:"varchar(50)"`
age int `field:"age" type:"int"`
}
func main() {
var u user
t := reflect.typeof(u)
for i := 0; i < t.numfield(); i++ {
f := t.field(i)
fmt.printf("%s: %s %s\n", f.name, f.tag.get("field"), f.tag.get("type"))
}
}
输出:
name: name varchar(50) age: age int
辅助判断方法 implements()、convertibleto、assignableto() 都是运行期进行 动态调用 和 赋值 所必需的。
func (x) string() string {
return ""
}
func main() {
var a x
t := reflect.typeof(a)
// implements 不能直接使用类型作为参数,导致这种用法非常别扭
st := reflect.typeof((*fmt.stringer)(nil)).elem()
fmt.println(t.implements(st))
it := reflect.typeof(0)
fmt.println(t.convertibleto(it))
fmt.println(t.assignableto(st), t.assignableto(it))
}
输出:
true true true false
二、值(value)
和 type 获取类型信息不同,value 专注于对象实例数据读写。
在前面章节曾提到过,接口变量会复制对象,且是 unaddressable 的,所以要想修改目标对象,就必须使用指针。
a := 100
va, vp := reflect.valueof(a), reflect.valueof(&a).elem()
fmt.println(va.canaddr(), va.canset())
fmt.println(vp.canaddr(), vp.canset())
}
输出:
false false true true
就算传入指针,一样需要通过 elem()
获取目标对象。因为被接口存储的指针本身是不能寻址和进行设置操作的。
注意,不能对非导出字段直接进行设置操作,无论是当前包还是外包。
name string
code int
}
func main() {
p := new(user)
v := reflect.valueof(p).elem()
name := v.fieldbyname("name")
code := v.fieldbyname("code")
fmt.printf("name: canaddr = %v, canset = %v\n", name.canaddr(), name.canset())
fmt.printf("code: canaddr = %v, canset = %v\n", code.canaddr(), code.canset())
if name.canset() {
name.setstring("tom")
}
if code.canaddr() {
*(*int)(unsafe.pointer(code.unsafeaddr())) = 100
}
fmt.printf("%+v\n", *p)
}
输出:
name: canaddr = true, canset = true code: canaddr = true, canset = false {name:tom code:100}
value.pointer 和 value.int 等方法类型,将 value.data 存储的数据转换为指针,目标必须是指针类型。而 unsafeaddr 返回任何 canaddr value.data 地址(相当于 & 取地址操作),比如 elem() 后的 value,以及字段成员地址。
以结构体里的指针类型字段为例,pointer 返回该字段所保存的地址,而 unsafeaddr 返回该字段自身的地址(结构对象地址 + 偏移量)。
可通过 interface 方法进行类型 推荐 和 转换。
type user struct {
name string
age int
}
u := user{
"q.yuhen",
60,
}
v := reflect.valueof(&u)
if !v.caninterface() {
println("caninterface: fail.")
return
}
p, ok := v.interface().(*user)
if !ok {
println("interface: fail.")
return
}
p.age++
fmt.printf("%+v\n", u)
}
输出:
{name:q.yuhen age:61}
也可以直接使用 value.int、bool 等方法进行类型转换,但失败时会引发 pani,且不支持 ok-idiom。
复合类型对象设置示例:
c := make(chan int, 4)
v := reflect.valueof(c)
if v.trysend(reflect.valueof(100)) {
fmt.println(v.tryrecv())
}
}
输出:
100 true
接口有两种 nil 状态,这一直是个潜在麻烦。解决方法是用 isnil() 判断值是否为 nil。
var a interface{} = nil
var b interface{} = (*int)(nil)
fmt.println(a == nil)
fmt.println(b == nil, reflect.valueof(b).isnil())
}
输出:
true false true
也可用 unsafe 转换后直接判断 iface.data 是否为零值。
var b interface{} = (*int)(nil)
iface := (*[2]uintptr)(unsafe.pointer(&b))
fmt.println(iface, iface[1] == 0)
}
输出:
&[712160 0] true
让人很无奈的是,value 里的某些方法并未实现 ok-idom 或返回 error,所以得自行判断返回的是否为 zero value。
v := reflect.valueof(struct {name string}{})
println(v.fieldbyname("name").isvalid())
println(v.fieldbyname("xxx").isvalid())
}
输出:
true false
三、方法
动态调用方法,谈不上有多麻烦。只须按 in 列表准备好所需参数即可。
func (x) test(x, y int) (int, error) {
return x + y, fmt.errorf("err: %d", x + y)
}
func main() {
var a x
v := reflect.valueof(&a)
m := v.methodbyname("test")
in := []reflect.value{
reflect.valueof(1),
reflect.valueof(2),
}
out := m.call(in)
for _, v := range out {
fmt.println(v)
}
}
输出:
3 err: 3
对于变参来说,用 callslice() 要更方便一些。
func (x) format(s string, a ...interface{}) string {
return fmt.sprintf(s, a...)
}
func main() {
var a x
v := reflect.valueof(&a)
m := v.methodbyname("format")
out := m.call([]reflect.value{
reflect.valueof("%s = %d"), // 所有参数都须处理
reflect.valueof("x"),
reflect.valueof(100),
})
fmt.println(out)
out = m.callslice([]reflect.value{
reflect.valueof("%s = %d"),
reflect.valueof([]interface{}{"x", 100}),
})
fmt.println(out)
}
输出:
[x = 100] [x = 100]
无法调用非导出方法,甚至无法获取有效地址。
四、构建
反射库提供了内置函数 make()
和 new()
的对应操作,其中最有意思的就是 makefunc()
。可用它实现通用模板,适应不同数据类型。
func add(args []reflect.value) (results []reflect.value) {
if len(args) == 0 {
return nil
}
var ret reflect.value
switch args[0].kind() {
case reflect.int:
n := 0
for _, a := range args {
n += int(a.int())
}
ret = reflect.valueof(n)
case reflect.string:
ss := make([]string, 0, len(args))
for _, s := range args {
ss = append(ss, s.string())
}
ret = reflect.valueof(strings.join(ss, ""))
}
results = append(results, ret)
return
}
// 将函数指针参数指向通用算法函数
func makeadd(fptr interface{}) {
fn := reflect.valueof(fptr).elem()
v := reflect.makefunc(fn.type(), add) // 这是关键
fn.set(v) // 指向通用算法函数
}
func main() {
var intadd func(x, y int) int
var stradd func(a, b string) string
makeadd(&intadd)
makeadd(&stradd)
println(intadd(100, 200))
println(stradd("hello, ", "world!"))
}
输出:
300 hello, world!
如果语言支持泛型,自然不需要这么折腾
希望本文所述对大家go语言程序设计有所帮助。
上一篇: 合并table中值相同的列