Go语言入门21:反射
Table of Contents
Go 语言入门基础学习笔记之 Go 语言的反射
反射
变量的结构
Go 语言中的一个变量时包含它的类型 type 和值 value ,称为一对 pair。所谓反射,就是可以通过变量来得到 static type 或者 concrete type。
package main
import "fmt"
func main() {
var a string
//a: pair<type:string, value:"abcd">
a = "abcd"
//allType: pair<type:string, value:"abcd">
var allType interface{}
allType = a
str, ok := allType.(string)
fmt.Println(str, ok)
}
// 输出结果
abcd true
a 是一个 string 类型,拥有 type 和 value 两个“属性”。当它赋值给一个空接口类型时,接口的两个指针分布指向了 a 这个变量的 type 和 value。因此可以通过类型断言来判断它的数据类型,也可以直接将它的值打印出来。
package main
import (
"fmt"
"io"
"os"
)
func main() {
//tty: pair<type:*os.File, value:"/dev/tty"文件描述符>
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
fmt.Println("open file error", err)
return
}
//r: pair<type:, value:>
var r io.Reader
//r: pair<type:*os.File, value:"/dev/tty"文件描述符>
r = tty
//w: pair<type:, value:>
var w io.Writer
//w: pair<type:*os.Writer, value:"/dev/tty"文件描述符>
w = r.(io.Writer)
w.Write([]byte("HELLO THIS is A TEST!!!\n"))
}
OpenFile()
函数用于打开一个文件,返回文件指针和错误。/dev/tty
是当前 Linux 的终端。O_RDWR
可读可写权限。于是 tty
的 type 为一个文件类型的指针,value 为打开终端文件的文件描述符(指针)。
io.Reader
是一个接口类型,因此 r = tty
赋值后,r
的 type 和 value 与 tty
一样。io.Writer
也是一个接口类型,把 r
类型转换并赋值给 w
时,所传递的 pair,也就是 type 和 value 是不会发生变换的。
再一个例子:
package main
import "fmt"
type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
// 具体类型
type Book struct {
}
// 实现接口的全部方法
func (b *Book) ReadBook() {
fmt.Println("Read a book")
}
func (b *Book) WriteBook() {
fmt.Println("Write a book")
}
func main() {
// b: pair<type:Book, value:Book{}地址>
b := &Book{}
// r: pair<type:, value:>
var r Reader
// r: pair<type:Book, value:Book{}地址>
r = b
r.ReadBook()
var w Writer
// r: pair<type:Book, value:Book{}地址>
w = r.(Writer) // 此处断言成功,w r具体类型相同
w.WriteBook()
}
// 输出结果
Read a book
Write a book
以上这几个例子,告诉我们,在编写 Go 语言代码时,心中需要牢记变量中有一对 type 和 value,它们会随着赋值和类型转换等操作一同被拷贝转移。
reflect 包
reflect 包提供一种名叫反射的机制,所谓反射就是允许我们将一个变量作为一个输入的形参来得到当前变量的类型 type 和值 value。
这种反射的意义实际上与其他语言反射的意义是一样的,可以简单理解为在日常编写程序时,可以通过 value 来动态推断出变量的 type。这是因为一个变量始终会带有这样一个对,即 type 和 value。反射的意义就是可以获取到变量真实的类型。
reflect 包中提供下面几个函数:
ValueOf
:传入一个变量,获取这个变量的 value
TypeOf
:传入一个变量,获取这个变量的具体 typer
使用 reflect 包反射基本类型:
package main
import (
"fmt"
"reflect"
)
func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}
func main() {
var num float64 = 1.2345
reflectNum(num)
}
// 输出结果
type: float64
value: 1.2345
使用 reflect 动态反射复杂类型:
package main
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Age int
}
func (u User) Call() {
fmt.Println("User is called...")
fmt.Printf("%v\n", u) // 打印结构体自身
}
func main() {
user := User{1, "Tom", 20}
DoFiledAndMethod(user) // 传入结构体实例
}
func DoFiledAndMethod(input interface{}) {
// 获取input的类型
inputType := reflect.TypeOf(input)
fmt.Println("inputType is:", inputType.Name())
// 获取input的值
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is:", inputValue)
// 通过type获取里面的字段
// 1. 获取interface的reflect.Type,通过Type的NumField进行遍历
// 2. 通过reflect.Type的Field获取其Field
// 3. 通过Field的Interface()获取对应的value
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
// 通过type获取里面的方法
// 1. 获取interface的reflect.Type,通过Type的NumMethod进行遍历
// 2. 通过reflect.Type的Method获取其Method
// 3. 通过Method的Interface()获取对应的func
for i := 0; i < inputType.NumMethod(); i++ {
method := inputType.Method(i)
fmt.Printf("%s: %v\n", method.Name, method.Type)
}
}
// 输出结果
inputType is: User
inputValue is: {1 Tom 20}
Id: int = 1
Name: string = Tom
Age: int = 20
Call: func(main.User)
以上只是对于 reflect 包的反射机制的非常简单的讲解,需要更深入的理解 reflect,可以去看 Go语言标准库文档中文版的源码文档。
结构体标签
结构体标签定义
结构体标签的概念也需要使用反射进行解释。结构体标签的定义是通过反引号来定义,通过反射机制来取出结构体标签中的值。
package main
import (
"fmt"
"reflect" // 引入 reflect 包,用于反射
)
// 定义一个结构体 resume,包含两个字段:Name 和 sex
type resume struct {
// Name 字段,带有两个标签:info 和 doc
Name string `info:"name" doc:"我的名字"`
// sex 字段,带有一个标签:info
sex string `info:"sex"`
}
// findTag 函数接受一个空接口类型的参数 str
// 该函数通过反射获取参数的字段标签信息,并打印出来
func findTag(str interface{}) {
// 使用 reflect.TypeOf 获取 str 的类型,并通过 Elem() 获取指向的值的类型
t := reflect.TypeOf(str).Elem()
// 遍历结构体的每个字段
for i := 0; i < t.NumField(); i++ {
// 获取当前字段的 "info" 标签
taginfo := t.Field(i).Tag.Get("info")
// 获取当前字段的 "doc" 标签
tagdoc := t.Field(i).Tag.Get("doc")
// 打印标签信息
fmt.Println("info:", taginfo, "doc:", tagdoc)
}
}
// main 函数是程序的入口点
func main() {
// 创建一个 resume 结构体的实例 re
var re resume
// 调用 findTag 函数,传入 re 的指针
findTag(&re)
}
// 输出结果
info: name doc: 我的名字
info: sex doc:
- 结构体定义:
resume
结构体包含两个字段Name
和sex
,并且为Name
字段添加了两个标签info
和doc
。标签用于存储与字段相关的元信息。
- 反射函数
findTag
:- 接受一个空接口参数
str
,可以传入任何类型。 - 使用
reflect.TypeOf
和Elem()
方法获取传入参数的类型信息。 - 通过
NumField()
方法获取结构体字段的数量,并遍历这些字段。 - 使用
Tag.Get
方法提取每个字段的info
和doc
标签,并打印出来。
- 接受一个空接口参数
- 主函数
main
:- 创建一个
resume
结构体的实例re
。 - 调用
findTag
函数并传入re
的指针,以便在函数中访问其字段标签。
- 创建一个
在 Json 中的应用
结构体标题有两个常见的应用:json 编解码和orm 映射关系。下面是一个 Json 的应用:
package main
import (
"encoding/json"
"fmt"
)
type Movie struct {
// 结构体字段的tag是用来指定json编码的时候的字段名
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"price"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{
Title: "喜剧之王",
Year: 2000,
Price: 10,
Actors: []string{"周星驰", "张柏芝"},
}
// 或者
// movie := Move{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}
// 序列化 编码过程 结构体 -> json字符串
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("jsonStr: %s\n", jsonStr)
// 反序列化 解码过程 json字符串 -> 结构体
myMovie := Move{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal failed")
return
}
fmt.Printf("myMovie: %v\n", myMovie)
}
// 输出结果
jsonStr: {"title":"喜剧之王","year":2000,"price":10,"actors":["周星驰","张柏芝"]}
myMovie: {喜剧之王 2000 10 [周星驰 张柏芝]}
参考课程: