前言

        在前面介绍接口的时候有提过,接口是方法的抽象,接口只注重方法的实现,而不在乎是谁调用的,那么当一个函数传入一个接口时,除了使用类型断言,还有什么方法获取该接口的具体类型信息呢,标准库中的reflect包为我们提供了此功能。

一、什么是反射?

        《GO语言圣经》声明:“GO语言提供了一种机制在运行时更新变量和检查它们的值、调用他们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制称为反射。”反射常用于判断一个接口变量的类型信息。

二、reflect包的使用

        reflect包中有两个十分重要的结构体:reflect.Type与reflect.Value,其中Type保存了一个接口变量的所有类型信息,包括类型名,底层类型,类型的字段等信息,Value则包含了接口变量的值信息,包括值的类型,值的底层类型,值的方法等。下面分别介绍这两种结构体的基本使用,更多使用方法请参考Golang标准库中文文档。


1.reflect.Type

        reflect.Type包含了一个接口变量的各种类型信息,我们可以通过reflect.TypeOf()函数获得一个reflect.Type结构体。

type person struct {
	Name string `heihei:haha`
	age  int
	sex  string
}

func (p person) Info() {
	fmt.Printf("name:%s,age:%d,sex:%s\n", p.Name, p.age, p.sex)
}

func (p *person) SetAge(age int) {
	p.age = age
}

func (p person) GetAge() int {
	return p.age
}

func main(){
    var p = person{"lulu", 22, "male"}
    getType := reflect.TypeOf(p)
}

1.1.获取类型名与底层类型

/*-----------------reflect.Type--------------------------------*/
	/*方法
	1.Name(),获取该接口的类型名,可以是自定义的别名
	2.Kind(),获取该接口的底层类型,如struct,ptr等
    */
fmt.Printf("type:%v,kind:%v\n", getType.Name(), getType.Kind()) // 输出:type:person,kind:struct

1.2.获取结构体的字段信息和标签

// 获取结构体字段信息,注意,这里获取不到字段对应的值
	for i := 0; i < getType.NumField(); i++ {
		field := getType.Field(i) // 通过index获取字段
		fmt.Printf("fieldName:%v,fieldTag:%v\n", field.Name, field.Tag)
	}

/*输出
fieldName:Name,fieldTag:heihei:haha
fieldName:age,fieldTag:
fieldName:sex,fieldTag:
*/


2.reflect.Value

        reflect.Value包含了接口变量的各种值信息,可以通过reflect.ValueOf()获得reflect.Value对象。

2.1.获取值的类型与底层类型

/*------------------reflect.Value-----------------------------*/
	/*方法
	1.Type()获取值的类型,相当于%T
	2.Kind()获取值的底层类型
	*/
	getValue := reflect.ValueOf(p)
	fmt.Printf("type:%v,kind:%v\n", getValue.Type(), getValue.Kind()) //输出:type:main.person,kind:struct

2.2.获取结构体字段的值信息

// 获取结构体每个字段的值信息
	for i := 0; i < getValue.NumField(); i++ {
		value := getValue.Field(i)
		fmt.Printf("valueType:%v,valueValue:%v\n", value.Type(), value)
	}

/*输出
valueType:string,valueValue:lulu
valueType:int,valueValue:22
valueType:string,valueValue:male
*/

2.3.获取并调用结构体的方法

// 获取结构体的方法并调用,值接收者只能获得值接收者的方法,方法必须对外可见,否则找不到
	for i := 0; i < getValue.NumMethod(); i++ {
		method := getValue.Method(i)
		fmt.Printf("methodType:%v,mathodKind:%v\n", method.Type(), method.Kind())
		if method.Kind() == reflect.Func {
			method.Call(nil)
		}
	}

/*输出
    methodType:func() int,mathodKind:func
    methodType:func(),mathodKind:func
    name:lulu,age:22,sex:male
*/

	// 指针接收者方法的调用
	pointer := reflect.ValueOf(&p)
	method := pointer.MethodByName("SetAge")
	method.Call([]reflect.Value{reflect.ValueOf(18)})
	p.Info() //name:lulu,age:18,sex:male,成功修改

2.4.修改结构体字段的值

// 通过reflect修改值
	// 必须传入地址
	// 字段必须对外导出,否则会panic
	nameFiled := pointer.Elem().FieldByName("Name")
	//fmt.Println(nameFiled)
	nameFiled.SetString("Allen")
	p.Info() //name:Allen,age:18,sex:male

        通过reflect包修改接口变量的值时,要注意以下四点:

  1. 传入的接口变量必须是指针类型,否则会引发panic
  2. 修改的变量必须对外导出,否则会引发panic
  3. 对传入的指针接口变量,要通过Elem()方法获取其值后再进行相关操作
  4. 修改变量的值时,一定要知道该变量的底层类型。
三、练习:json格式数据的序列化与反序列化

3.1.标准库中json包的marshal与unmarshal

        在进行web开发时,前后端交互数据中有一种重要的数据格式,称为json格式。标准库中的json包为我们提供了marshal与unmarshal方法,marshal用于讲一个结构体对象序列化成一个json字符串,而unmarshal则用于将json字符串中各个key-value对赋值给对应的结构体对象。使用方法如下:

type person struct {
	Name      string `json:"name"`
	Age       int    `json:"age"`
	Sex       string `json:"sex"`
	IsStudent bool   `json:"isStudent"`
}

func main(){
    p := person{"LuLu", 18, "male", true}
	msg, err := json.Marshal(p)
	if err != nil {
		fmt.Printf("json marshal failed,err:%v\n", err)
		return
	}
	fmt.Println(string(msg)) // 输出:{"name":"LuLu","age":18,"sex":"male","isStudent":true}

	var p2 person
	err = json.Unmarshal(msg, &p2)
	if err != nil {
		fmt.Printf("json unmarshal failed,err:%v\n", err)
		return
	}
	fmt.Println(p2) // 输出:{LuLu 18 male true}
}

3.2.通过reflect包自定义marshal与unmarshal

        本次的练习就是用reflect包的相关操作实现上面的marshal方法与unmarshal方法,该练习假设输入全是正确格式的输入(即无需判断输入是否合法),同时,限定结构体字段类型只有string,int与bool类型三种,有需要可自行扩展。

type person struct {
	Name      string `json:"name"`
	Age       int    `json:"age"`
	Sex       string `json:"sex"`
	IsStudent bool   `json:"isStudent"`
}

// 定义一个结构体来保存需要序列化的和结构体字段信息
type fieldInfo struct {
	fieldName string       // 保存字段名
	fieldType reflect.Kind // 保存字段类型
	fieldTag  string       // 保存字段标签
	hasTag    bool         // 记录该字段是否有标签,有标签的话结果要根据标签来
}

// fieldInfo 工厂函数
func newFieldInfo(fieldName string, fieldType reflect.Kind, fieldTag string, hasTag bool) *fieldInfo {
	return &fieldInfo{
		fieldName: fieldName,
		fieldType: fieldType,
		fieldTag:  fieldTag,
		hasTag:    hasTag,
	}
}

// 序列化函数
func marShal(v interface{}) (string, error) {
	// 对于进来的接口类型,先判断是不是结构体,不是则返回错误
	vType := reflect.TypeOf(v) // 获取reflect.Type,其中包含了字段的相关信息
	if vType.Kind() != reflect.Struct {
		return "", errors.New("Please input a struct\n")
	}
	vValue := reflect.ValueOf(v) // 获取reflect.Value,其中包含了字段的各种值信息

	fields := make([]*fieldInfo, 0, 5)   // 定义一个切片来获取结构体的所有字段信息,默认只有五个字段,不够的话会通过append扩容
	result := make(map[string]string, 5) // 存放结果
	// 遍历获取所有字段
	for i := 0; i < vType.NumField(); i++ { // 遍历每一个字段
		field := vType.Field(i)      // 获取字段
		tag := field.Tag.Get("json") //获取标签
		if tag != "" {               // 如果有json标签,设置hasTag属性为true
			info := newFieldInfo(field.Name, field.Type.Kind(), tag, true)
			fields = append(fields, info) // 保存当前字段信息
		} else { // 如果没有json标签,设置hasTag属性为false
			info := newFieldInfo(field.Name, field.Type.Kind(), tag, false)
			fields = append(fields, info) //保存当前字段信息
		}
	}

	// 遍历获取字段的值
	for _, info := range fields {
		value := vValue.FieldByName(info.fieldName) // 通过字段名获取字段Value
		switch info.fieldType {                     // 判断当前字段的类型
		case reflect.String:
			if info.hasTag { // 如果有tag,在结果中使用tag取代name
				result[info.fieldTag] = value.String()
			} else { // 没有tag则直接使用字段名name
				result[info.fieldName] = value.String()
			}
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			if info.hasTag { // 如果有tag,在结果中使用tag取代name
				result[info.fieldTag] = strconv.Itoa(int(value.Int()))
			} else { // 没有tag则直接使用字段名name
				result[info.fieldName] = strconv.Itoa(int(value.Int()))
			}
		case reflect.Bool:
			if info.hasTag { // 如果有tag,在结果中使用tag取代name
				result[info.fieldTag] = strconv.FormatBool(value.Bool())
			} else { // 没有tag则直接使用字段名name
				result[info.fieldName] = strconv.FormatBool(value.Bool())
			}
		default: // 还可以添加许多类型判断,此处示例只添加三种
			return "", errors.New("Invalid type\n")
		}

	}

	resultSlice := make([]string, 0, 5)
	for key, value := range result {
		// 拼接成json格式字符串
		resultSlice = append(resultSlice, fmt.Sprintf("\"%s\":\"%s\"", key, value))
	}

	return "{" + strings.Join(resultSlice, ",") + "}", nil
}

// 假设输入的字符串是满足json格式的字符串
func dealJsonStr(s string) map[string]string {
	result := make(map[string]string)
	temp1 := s[1 : len(s)-1]           // 去掉json字符串前后"{"与"}"
	temp2 := strings.Split(temp1, ",") // 获取每个key-value对
	for _, v := range temp2 {
		temp := strings.Split(v, ":") // 分割key与value
		key := temp[0]
		value := temp[1]
		result[key[1:len(key)-1]] = value[1 : len(value)-1] // 去掉引号
	}

	return result
}

// 假设输入的字符串是满足json格式的字符串
func unMarshal(s string, v interface{}) error {

	vType := reflect.TypeOf(v)       // 获取reflect.Type,其中包含了字段的相关信息
	vValue := reflect.ValueOf(v)     // 获取reflect.Value,其中包含了字段的各种值信息
	if vType.Kind() != reflect.Ptr { // 需要修改结构体,因此必须是指针类型
		return errors.New("input must ptr\n")
	}

	jsonMap := dealJsonStr(s) // 解析json字符串,获取key-value对
	// 对每个字段信息开始遍历
	for i := 0; i < vType.Elem().NumField(); i++ {
		field := vType.Elem().Field(i) // 获取当前字段
		tag := field.Tag.Get("json")   // 获取当前字段tag
		fieldType := field.Type.Kind() // 获取当前字段类型
		if value, ok := jsonMap[field.Name]; ok {
			switch fieldType { // 根据类型设置字段的值
			case reflect.String:
				vValue.Elem().FieldByName(field.Name).SetString(value)
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				setValue, err := strconv.Atoi(value)
				if err != nil {
					return err
				}
				vValue.Elem().FieldByName(field.Name).SetInt(int64(setValue))
			case reflect.Bool:
				setValue, err := strconv.ParseBool(value)
				if err != nil {
					return err
				}
				vValue.Elem().FieldByName(field.Name).SetBool(setValue)
			default:
				return errors.New("unmarshal invalid type\n")
			}
		} else if value1, ok1 := jsonMap[tag]; ok1 {
			switch fieldType {
			case reflect.String:
				vValue.Elem().FieldByName(field.Name).SetString(value1)
			case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
				setValue, err := strconv.Atoi(value1)
				if err != nil {
					return err
				}
				vValue.Elem().FieldByName(field.Name).SetInt(int64(setValue))
			case reflect.Bool:
				setValue, err := strconv.ParseBool(value1)
				if err != nil {
					return err
				}
				vValue.Elem().FieldByName(field.Name).SetBool(setValue)
			default:
				return errors.New("unmarshal invalid type\n")
			}
		} else {
			return errors.New("can not parse the struct\n")
		}
	}

	return nil

}

func main() {
	p := person{"LuLu", 18, "male", true}
	
	j, err := marShal(p)
	if err != nil {
		fmt.Printf("marshal failed,err:%v\n", err)
		return
	}
	fmt.Println(j) // 输出:{"name":"LuLu","age":"18","sex":"male","isStudent":"true"}

	var p2 person
	err = unMarshal(j, &p2)
	if err != nil {
		fmt.Printf("unmarshal failed,err:%v\n", err)
		return
	}
	fmt.Println(p2) // 输出:{LuLu 18 male true}

}

总结

        我们可以通过reflect包动态获取一个接口变量的类型信息与值信息,并调用相关的方法或修改值。但是要注意,reflect包十分复杂,且效率十分低下,比正常代码运行速度慢两个数量级,非必要情况谨慎使用reflect包!