反射基础

反射非常灵活,反射能够在运行时,操作不同类型的对象,主要包含了两个类型reflect.Typereflect.Value

  1. 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
    ...
}
  1. reflect.Value是结构体,但是里面所有的成员变量都是非导出的,要操作需要通过Setxxx()来完成。
type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

所以在写反射代码时实际上主要就是和这两个类型打交道。

反射法则

  1. 从 interface{} 变量可以反射出反射对象;
  2. 从反射对象可以获取 interface{} 变量;
  3. 要修改反射对象,其值必须可设置;

第一法则

interface{}可以转换到反射对象,这就意味着所有的类型都可以获取到其反射对象,这之间的桥梁就是reflect.TypeOfreflect.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

上面的代码进行了两次转换:

  1. string -> any -> reflect.Type or reflect.Value
  2. 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

具体流程为:

  1. 首先获取变量的指针reflect.ValueOf
  2. 获取到指针指向的值reflect.ValueOf.Elem
  3. 修改具体值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中进行存储的

  1. 使用reflect.Value.Call调用方法,返回的结果也是[]Value
  2. 使用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初始化默认值,这个时候就需要使用到反射。分析一下逻辑应该是:

  1. 根据第三法则,反射要修改反射对象,其值必须修改,所以首先初始化这个结构体,然后把指针传过去reflect.ValueOf()
  2. 需要遍历这个结构体的所有变量,reflect.Value.NumFiledreflect.Value.Filed(i)
  3. 获取到tag中default对应的默认值,reflect.StructField.Tag.Lookup
  4. 设置到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里面包含了两个结构体,这两个结构体一个是引用的类型,一个是非引用的类型,在处理的逻辑上应当是:

  1. 通过reflect.Value.Elem获取到需要修改默认值的反射对象
  2. 通过reflect.Value.NumFiled对反射对象进行遍历
    1. 如果是基础对象,直接赋值
    2. 如果是引用或者结构体,需要再次通过reflect.Value.Elem获取到需要修改的对象,然后重复上面的步骤
  3. 总体是个递归的过程
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/
扩展阅读:

  1. 接口的实现判定
  2. 通过Call实现方法调用的原理