反射基础
反射非常灵活,反射能够在运行时,操作不同类型的对象,主要包含了两个类型reflect.Type
和reflect.Value
reflect.Type
是接口类型,里面提供了一些方法如下,通过reflect.Type
可以获取到这个类型的方法,名字等
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Name() string
PkgPath() string
Size() uintptr
String() string
Kind() Kind
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
...
}
reflect.Value
是结构体,但是里面所有的成员变量都是非导出的,要操作需要通过Setxxx()
来完成。
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
所以在写反射代码时实际上主要就是和这两个类型打交道。
反射法则
- 从
interface{}
变量可以反射出反射对象; - 从反射对象可以获取
interface{}
变量; - 要修改反射对象,其值必须可设置;
第一法则
从interface{}
可以转换到反射对象,这就意味着所有的类型都可以获取到其反射对象,这之间的桥梁就是reflect.TypeOf
和reflect.ValueOf
,获取了反射对象就能进行操作
第二法则
我们可以从反射对象可以获取到interface{}
,这两个法则听起来好像就是废话,但是有这两点的保证,就能够让我们,在反射对象上的操作能够回归到结构体上,例如:
package main
import (
"fmt"
"reflect"
)
func main() {
str := "str"
t := reflect.TypeOf(str)
v := reflect.ValueOf(str)
fmt.Println(t, v)
itfv := v.Interface()
fmt.Println(itfv.(string))
}
➜ reflect go run main.go
string str
str
上面的代码进行了两次转换:
string
->any
->reflect.Type
orreflect.Value
reflect.Value
->any
->string
第三法则
要修改反射对象,其值必须可设置,听着有点理所应当,但是实际上,因为Golang是值传递的,如果在反射对象时进行更改,就会出现panic,因为原始值不会有影响,这个时候就需要指针,假设有如下代码:
package main
import "reflect"
func main() {
i := 1
v := reflect.ValueOf(i)
v.SetInt(3)
}
➜ priciple go run p3.go
panic: reflect: reflect.Value.SetInt using unaddressable value
goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x60?)
/root/sdk/go1.20.5/src/reflect/value.go:262 +0x85
reflect.flag.mustBeAssignable(...)
/root/sdk/go1.20.5/src/reflect/value.go:249
reflect.Value.SetInt({0x464d40?, 0x4d7468?, 0x0?}, 0x3)
/root/sdk/go1.20.5/src/reflect/value.go:2309 +0x48
main.main()
/root/code/go-study/reflect/priciple/p3.go:8 +0xaf
exit status 2
这就是违反了第三法则,反射无法修改这种值传递,要想修改,就需要将指针作为值进行传递,再进行修改,如下:
package main
import (
"fmt"
"reflect"
)
func main() {
i := 1
v := reflect.ValueOf(&i)
v.Elem().SetInt(2)
fmt.Println(i)
}
➜ priciple go run p3.go
2
具体流程为:
- 首先获取变量的指针
reflect.ValueOf
- 获取到指针指向的值
reflect.ValueOf.Elem
- 修改具体值
reflect.Value.SetInt()
方法调用
通过反射来调用方法也是个很有意思的逻辑,比如有以下场景,我们需要调用Hello("lvliao")
方法,
package main
import (
"fmt"
"reflect"
)
func Hello(name string) string {
fmt.Println("hello", name)
return name
}
func main() {
v := reflect.ValueOf(Hello)
t := v.Type()
fmt.Println(t.NumIn(), t.NumOut())
result := v.Call([]reflect.Value{
reflect.ValueOf("lvliao"),
})
fmt.Println(result[0].String())
}
➜ poc go run reflect/method/hello/hello.go
1 1
hello lvliao
lvliao
通过reflect.Value
可以获取到方法的Value,对于方法的入参则是在Type中进行存储的
- 使用
reflect.Value.Call
调用方法,返回的结果也是[]Value
- 使用
reflect.Type.NumIn/NumOut
获取方法的出参和入参个数
反射+Tag实现初始化默认结构体
背景
在写基础库的时,一般会有很多配置,但是并不是这么多都是需要做配置的,需要配置的可能只是一小部分,例如日志Config的初始化:
type Config struct {
Level string `default:"debug"`
CallerSkip int `default:"3"`
// ZapCore config
Structured bool `default:"true"`
CoreLevel string `default:"debug"'`
ErrorFile bool `default:"false"`
Prod bool
Filename string
MaxSize int `default:"500"`
MaxAge int `default:"7"`
MaxBackups int `default:"5"`
LocalTime bool
Compress bool `default:"true"`
}
其中很多的变量都可以直接使用默认值,做到开箱即用,这只是一个基础的场景,在实际场景中,往往是多个结构体组合成一个最终Config,如:
type Config struct {
RunMode string `yaml:"RunMode" default:"debug"`
Addr string `yaml:"Addr" default:":8080"`
Log *log.Config `yaml:"Log"`
Database *models.DatabaseConfig `yaml:"Database"`
}
最好这种场景也能直接通过default: Tag
来完成默认的配置生成
单结构体通过Tag默认初始化
以上面的第一种场景,只通过tag初始化默认值,这个时候就需要使用到反射。分析一下逻辑应该是:
- 根据第三法则,反射要修改反射对象,其值必须修改,所以首先初始化这个结构体,然后把指针传过去
reflect.ValueOf()
- 需要遍历这个结构体的所有变量,
reflect.Value.NumFiled
和reflect.Value.Filed(i)
- 获取到tag中
default
对应的默认值,reflect.StructField.Tag.Lookup
- 设置到Value中
reflect.Value.SetXxx()
package main
import (
"fmt"
"reflect"
"strconv"
)
const tagKey = "default"
type Config struct {
Level string `default:"debug"`
CallerSkip int `default:"3"`
Structured bool `default:"true"`
CoreLevel string `default:"debug"'`
ErrorFile bool `default:"false"`
Prod bool
Filename string
MaxSize int `default:"500"`
MaxAge int `default:"7"`
MaxBackups int `default:"5"`
LocalTime bool
Compress bool `default:"true"`
}
func main() {
c := &Config{}
v := reflect.ValueOf(c).Elem()
for i := 0; i < v.NumField(); i++ {
vf := v.Field(i)
vt := v.Type().Field(i)
fmt.Println("name:", vt.Name, ", kind:", vf.Kind())
tag, _ := vt.Tag.Lookup(tagKey)
switch vf.Kind() {
case reflect.Bool:
fmt.Println("set bool", tag)
v, _ := strconv.ParseBool(tag)
vf.SetBool(v)
case reflect.String:
fmt.Println("set string", tag)
vf.SetString(tag)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Println("set int", tag)
v, _ := strconv.ParseInt(tag, 10, 64)
vf.SetInt(v)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fmt.Println("set uint", tag)
v, _ := strconv.ParseUint(tag, 10, 64)
vf.SetUint(v)
case reflect.Float32, reflect.Float64:
fmt.Println("set float", tag)
v, _ := strconv.ParseFloat(tag, 64)
vf.SetFloat(v)
}
}
fmt.Println("==============")
fmt.Println("level:", c.Level, "callerSkip:", c.CallerSkip, "structured:", c.Structured)
}
➜ poc go run reflect/config/simple_config/simple_config.go
name: Level , kind: string
set string debug
name: CallerSkip , kind: int
set int 3
name: Structured , kind: bool
set bool true
name: CoreLevel , kind: string
set string debug
name: ErrorFile , kind: bool
set bool false
name: Prod , kind: bool
set bool
name: Filename , kind: string
set string
name: MaxSize , kind: int
set int 500
name: MaxAge , kind: int
set int 7
name: MaxBackups , kind: int
set int 5
name: LocalTime , kind: bool
set bool
name: Compress , kind: bool
set bool true
==============
level: debug callerSkip: 3 structured: true
以上已经可以通过tag初始化单个结构体
通过Tag初始化嵌套结构体
上面只是通过反射初始化了最简单的结构体类型,但是当结构体出现嵌套时,逻辑和上面大体相同,只不过在代码是线上会有些差别,主要体现在递归去处理。
针对上面的第二种场景,设计了如下结构:
type Config struct {
Mode string `default:"debug"`
Addr string `default:"127.0.0.1"`
Port int `default:"8080"`
Log *Log
AccessLog Log
}
type Log struct {
Level string `default:"debug"`
CallerSkip int `default:"3"`
// ZapCore config
Structured bool `default:"true"`
CoreLevel string `default:"debug"'`
ErrorFile bool `default:"false"`
Prod bool
Filename string
MaxSize int `default:"500"`
MaxAge int `default:"7"`
MaxBackups int `default:"5"`
LocalTime bool
Compress bool `default:"true"`
}
在Config
里面包含了两个结构体,这两个结构体一个是引用的类型,一个是非引用的类型,在处理的逻辑上应当是:
- 通过
reflect.Value.Elem
获取到需要修改默认值的反射对象 - 通过
reflect.Value.NumFiled
对反射对象进行遍历- 如果是基础对象,直接赋值
- 如果是引用或者结构体,需要再次通过
reflect.Value.Elem
获取到需要修改的对象,然后重复上面的步骤
- 总体是个递归的过程
package main
import (
"container/list"
"fmt"
"reflect"
"strconv"
)
const tagKey = "default"
func main() {
c := &Config{}
v := reflect.ValueOf(c).Elem()
l := list.New()
l.PushBack(v)
for ele := l.Front(); ele != nil; ele = ele.Next() {
e := ele.Value.(reflect.Value)
if e.Type().Kind() != reflect.Struct {
continue
}
for i := 0; i < e.NumField(); i++ {
vf := e.Field(i)
if vf.Kind() == reflect.Pointer && vf.IsNil() {
vf = reflect.New(vf.Type().Elem())
e.Field(i).Set(vf)
}
t := typeElem(e.Field(i).Type())
tag, _ := e.Type().Field(i).Tag.Lookup(tagKey)
switch t.Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64, reflect.String:
//setFieldDefaultByTag(ctx, f, tf)
setDefaultValue(vf, tag)
case reflect.Struct:
l.PushBack(valueElem(vf))
}
}
}
fmt.Println(c.Log.Level, c.AccessLog.Level, c.Log.CallerSkip, c.AccessLog.CallerSkip)
}
func typeElem(t reflect.Type) (ret reflect.Type) {
ret = t
if ret.Kind() == reflect.Ptr || ret.Kind() == reflect.Uintptr {
ret = ret.Elem()
}
return
}
func valueElem(v reflect.Value) (ret reflect.Value) {
ret = v
if ret.Kind() == reflect.Ptr {
ret = ret.Elem()
}
return
}
func setDefaultValue(vf reflect.Value, tag string) {
switch vf.Kind() {
case reflect.Bool:
v, _ := strconv.ParseBool(tag)
vf.SetBool(v)
case reflect.String:
vf.SetString(tag)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, _ := strconv.ParseInt(tag, 10, 64)
vf.SetInt(v)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, _ := strconv.ParseUint(tag, 10, 64)
vf.SetUint(v)
case reflect.Float32, reflect.Float64:
v, _ := strconv.ParseFloat(tag, 64)
vf.SetFloat(v)
}
}
➜ poc go run reflect/config/struct_config/struct_config.go
2023-12-27T14:17:42.471+0800 INFO struct_config/struct_config.go:66 after reflect config {"config": {"Mode":"debug","Addr":"127.0.0.1","Port":8080,"Log":{"Level":"debug","CallerSkip":3,"Structured":true,"CoreLevel":"debug","ErrorFile":false,"Prod":false,"Filename":"","MaxSize":500,"MaxAge":7,"MaxBackups":5,"LocalTime":false,"Compress":true},"AccessLog":{"Level":"debug","CallerSkip":3,"Structured":true,"CoreLevel":"debug","ErrorFile":false,"Prod":false,"Filename":"","MaxSize":500,"MaxAge":7,"MaxBackups":5,"LocalTime":false,"Compress":true}}}
通过上面可以看出可以使引用和非引用的Config都已经被初始化到里面。
其他
上面这种初始化默认值的方式可以写到基础库里,这样在其他基础库写配置时也可以更加简单。
本文只是写了一些比较浅显的东西,对于更深的,比如反射的实现是怎样的,可以阅读下面的官方文档和源码orz
Golang官方文档-反射: https://go.dev/blog/laws-of-reflection
反射基础-博客: https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-reflect/
扩展阅读:
- 接口的实现判定
- 通过Call实现方法调用的原理