重学Go语言 | 一文详解Go反射编程

公众号:程序员读书,欢迎关注

在Go语言的标准库中,有很多的包功能很强大,比如 fmt包的函数可以把任何类型的数值输出到控制台,再比如 enconding/json可以将结构体、数组、切片等不同的数据类型序列化为一个 JSON字符串,这些包之所以如此强大,是因为使用了Go语言的反射机制。

反射是什么?反射能做什么?,在这篇文章中,我们来了解一下!

什么是反射

反射是编程语言的高级特性之一,运用反射机制可以在程序运行时动态检查变量的类型与具体的值,并且可以动态修改变量的值或者调用变量内在支持的方法。

为什么需要反射

上面对反射的描述可以让你觉得有点难以理解,我们都知道变量的类型在代码编译时就能确定的,比如下面的代码中变量 n在编译时就确定为 int:

go 复制代码
package main

import (  
    "fmt"
)

func main() {  
    n := 10
    fmt.Printf("%d %T", n, n)
}

既然编译时就已经知道变量的类型,为何还要去检测变量的类型呢?

这是因为有些时候我们要处理的类型是不确定的,比如 fmt包下的输出函数允许输出任何类型数据,fmt的输出函数大部分都接收类型为 interface{}参数,这就表示只有在程序运行时才能确定要输出的值是什么类型。

我们自己来实现一个可以格式化任何数据的函数,该函数的参数同样是 interface{},表示可以格式化任何数据,我们可以用 switch类型分支来判断数据类型:

go 复制代码
type stringer interface {
	String() string
}

func MyFormat(x interface{}) string {
	switch x := x.(type) {
	case stringer:
		return x.String()
	case string:
		return x
	case int:
		return strconv.Itoa(x)
	case bool:
		if x {
			return "true"
		}
		return "false"
	case User: //自定义的User类型
		return "My Name is:" + x.Name
	default:
		return "???"
	}
}

MyFormat()函数中,我们还判断了一个自定义的结构体 User,接下来我们可调用该函数进行数据格式化:

go 复制代码
package main

import (
	"fmt"
	"strconv"
)

type User struct {
	Name   string
	Gender uint8
}

func main() {
	fmt.Println(MyFormat(User{Name: "小明", Gender: 1}))
	fmt.Println(MyFormat(12))
    fmt.Println(MyFormat("str"))
}

但是,如果我们想格式化数组,切片,map呢?你会发现 switch类型分支无法判断数组、切片和 map类型的。

而对于自定义类型,比如 User,虽然可以判断,但却要一直添加 case分支来判断,要知道自定义类型是无穷无尽且未知的。

这时候反射机制就派上用场了!

Go反射编程

当我们把一个值或变量赋给一个 interface{}类型的变量时:

go 复制代码
var i interface{}

n := 10
i = n
s := "test"
i = s

上面的代码中,空接口变量 i由两个部分组成,当把 ns赋给 i,由类型与值变化如下图所示:

反射的作用就在于检测空接口类型的类型(type )和值(value )是什么,Go语言的反射能力由 reflect 包提供,这个包里有两个重要的类型:Type Value,分别表示检测到的空接口现在的具体类型和值。

reflect.Type

Type是一个接口,当我们把要检测的值传给 reflect.TypeOf()函数后,会返回一个实现了 Type接口实例,从 TypeOf()函数的签名可以看出该函数的参数就是一个空接口类型:

go 复制代码
func TypeOf(i interface{}) Type 

Type接口有许多的方法,比如我们可以调用 TypeKind()方法输出该接口当前的类型:

go 复制代码
i := 10
t := reflect.TypeOf(i)
fmt.Println(t.Kind())//int

reflect.Value

Value则代表变量的具体值,reflect包的 ValueOf()函数可以返回一个 Value结构体,ValueOf()函数的参数同样是一个 interface{}类型:

go 复制代码
func ValueOf(i interface{}) Value

Value 结构体同样有 Kind()用于获取变量的类型:

go 复制代码
i := 10
v := reflect.ValueOf(i)
fmt.Println(v.Kind()) //int

虽然类型可以无尽穷,但通过 Kind()方法返回类型却是固定的,Kind()返回以下面的取值范围:

go 复制代码
package reflect

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

实战

现在我们使用反射机制来完善我们的格式化函数 MyFormat:

go 复制代码
func MyFormat(x interface{}) string {
	v := reflect.ValueOf(x)
	switch v.Kind() {
	case reflect.Invalid:
		return "invalid"
	case reflect.Array, reflect.Slice:
		str := "["
		for i := 0; i < v.Len(); i++ {
			str += MyFormat(v.Index(i).Interface())
			if i < v.Len()-1 {
				str += ","
			}
		}
		return str + "]"
	case reflect.Struct:
		str := "{"
		for i := 0; i < v.NumField(); i++ {
			str += MyFormat(v.Field(i).Interface())
			if i < v.NumField()-1 {
				str += ","
			}
		}
		return str + "}"
	case reflect.String:
		return v.String()
	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Int64:
		return fmt.Sprintf("%d", v.Int())
	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return fmt.Sprintf("%d", v.Uint())
	case reflect.Bool:
		if v.Bool() {
			return "true"
		}
		return "false"
	case reflect.Map:
		str := "{"
		for i, key := range v.MapKeys() {
			str += key.String() + ":" + MyFormat(v.MapIndex(key).Interface())
			if i < v.Len()-1 {
				str += ","
			}
		}
		str += "}"
		return str
	default:
		return "invalid"
	}
}

现在调用 MyFormat()函数,可以格式化结构体,Map,数组,切片等类型,对于未知的类型,可以通过Kind()方法判断其底层类型,因为最终都可以格式化:

go 复制代码
package main

import (
	"fmt"
	"reflect"
)

type Address struct {
	Detail string
}

func main() {
	user := User{
		Name:      "小明",
		Gender:    1,
		MyAddress: Address{Detail: "我的地址"},
        Label:     map[string]string{"爱好": "打球"},
	}
	fmt.Println(MyFormat(user))
	fmt.Println(MyFormat([]int{1, 2, 3}))
	fmt.Println(MyFormat(map[string]string{"11": "test"}))

}

type User struct {
	Name      string
	Gender    uint8
	MyAddress Address
	Label     map[string]string
}

MyFormat()中,我们可以看到对数组和切片、结构体、Map这些复杂类型的处理,分别调用不同的方法,比如当类型为Map时可以调用 MapKeys()方法返回Map的所有key,而是结构体,可以调用 NumField()方法返回所有字段。

反射除了检测变量的值,还可能动态设置变量的值,接下来我们来实现一个将 HTTP Query参数解析到结构体的功能。

ini 复制代码
id=1&name=小明

我们要实现的功能是将上面的类似 HTTP Query请求参数的字符串映射到一个结构体当中,首先实现一个将字符串 HTTP Query字符串解析为 Map的函数:

go 复制代码
func MockQueryParams(queryParamStr string) map[string]string {
	queryParamArr := strings.Split(queryParamStr, "&")
	Params := make(map[string]string)
	for _, v := range queryParamArr {
		s := strings.Split(v, "=")
		Params[s[0]] = s[1]
	}
	return Params
}

再接着,将解析得到的 Map反射到结构体当中,这里要调用 reflect.ValueElem()方法来获得结构体的指针,而要修改结构体的字段的值,则就需要判断字段的类型,并调用相应的方法,比如字符串,就调用 ValueSetString()方法:

go 复制代码
func ParseHttpQuery(queryParams map[string]string, params interface{}) error {

	v := reflect.ValueOf(params).Elem()

	fields := make(map[string]reflect.Value)
	//解析结构体
	for i := 0; i < v.NumField(); i++ {
		fieldInfo := v.Type().Field(i)
		tag := fieldInfo.Tag
		name := tag.Get("data")
		if name == "" {
			name = strings.ToLower(fieldInfo.Name)
		}
		fields[name] = v.Field(i)
	}

	//遍历参数
	for name, value := range queryParams {
		f := fields[name]
		if !f.IsValid() {
			continue
		}

		if f.Kind() == reflect.Slice {
			elem := reflect.New(f.Type().Elem()).Elem()
			if err := setValue(elem, value); err != nil {
				return fmt.Errorf("%s: %v", name, err)
			}
			f.Set(reflect.Append(f, elem))
		} else {
			if err := setValue(f, value); err != nil {
				return fmt.Errorf("%s: %v", name, err)
			}
		}
	}
	return nil
}

func setValue(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
}

功能实现后就在可以实现数据解析了:

go 复制代码
package main

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

type User struct {
	ID   int    `data:"id"`
	Name string `data:"name"`
}

func main() {
	//模拟HTTP Query参数
	queryParamStr := "id=1&name=小明"
	//将HTTP Query解析为Map
	Params := MockQueryParams(queryParamStr)
	u := &User{}
	ParseHttpQuery(Params, u)
    //输出:{1,"小明"}
    fmt.Println(u) 
}

使用反射的几点建议

尽量不要使用反射,主要原因有三点:

  • 反射代码运行效率低
  • 反射代码比较难懂
  • 反射代码容易出错

小结

反射是一种元编程能力,可以动态检测和调用变量的方法等,对于要写出能兼容不同类型且能处理未知道数据类型的代码,反射机制非常有用,但是反射机制的代码往往让开发人员能难理解,因此应该慎重使用反射机制。

相关推荐
尚学教辅学习资料1 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck6 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风7 小时前
详解K8S--声明式API
后端
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml48 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~8 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616888 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端