反射是指在程序运行期间对程序本身进行访问和修改的能力。
一. 变量的内在机制
- 变量包含类型信息和值信息
- 类型信息:是静态的元信息,是预先定义好的
- 值信息:是程序运行过程中动态改变的
二. 反射的使用
- reflect包封装了反射相关的方法
- 获取类型信息:reflect.TypeOf,是静态的
- 获取值信息:reflect.ValueOf,是动态的
三. 空接口和反射
- 反射可以在运行时动态获取程序的各种详细信息
- 反射获取interface类型信息
- 反射获取interface值信息
- 反射修改值信息
四. 结构体与反射
- 查看类型,字段和方法
注意:结构体方法名和属性名必须大写,否则获取不到方法或报错。
package main
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Age int
}
func (u User) SayHello() {
fmt.Println("hello")
}
func Poni(x interface{}) {
//获取类型
t := reflect.TypeOf(x)
fmt.Println(t)
fmt.Println("字符串类型:", t.Name())
//获取值
v := reflect.ValueOf(x)
fmt.Println("值:", v)
//获取所有属性
//通过类型获取结构体字段个数
for i := 0; i < t.NumField(); i++ {
//通过类型取每一个字段,包括类型和字段变量名
f := t.Field(i)
fmt.Printf("%s, %v\n", f.Name, f.Type)
//通过值获取字段的值信息
//interface():获取字段对应的值
val := v.Field(i).Interface()
fmt.Println("val : ", val)
}
fmt.Println("---------方法------------")
//通过类型获取方法数
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(m.Name)
fmt.Println(m.Type)
}
}
func main() {
u := User{
1,
"wy",
20,
}
Poni(u)
}
- 查看匿名字段
- 修改结构体的值
- 调用结构体方法
- 获取字段的tag
标签保存在类型的reflect.StructField结构体里。
五.反射练习
- 任务:解析如下配置文件
- 序列化:将结构体序列化为配置文件数据并保存到硬盘
- 反序列化:将配置文件内容反序列化到程序的结构体
- 配置文件有server和mysql相关配置
#this is comment
;this a comment
;[]表示一个section
[server]
ip = 10.238.2.2
port = 8080
[mysql]
username = root
passwd = admin
database = test
host = 192.168.10.10
port = 8000
timeout = 1.2
- 思路
反序列化:将配置文件读上来,根据根据结构体的tag,赋值到对应结构体属性字段。利用反射(reflect)获取到tag和赋值
序列化:利用反射(reflect)获取到对应字段tag和值,编写为对应形式,再写道文件中。
main.go:主函数
package main
import (
"io/ioutil"
"log"
)
// 反序列化
func parseFile(infilename string, res interface{}) {
data, err := ioutil.ReadFile(infilename)
if err != nil {
log.Println("read file fail ", err)
return
}
//fmt.Println(string(data))
err = Unmarshal(data, res)
if err != nil {
log.Println("Unmarshal fail ", err)
return
}
}
// 序列化
func parseDataToFile(filename string, res interface{}) {
data, err := Marshal(res)
if err != nil {
log.Println("marshal fail ", err)
return
}
err = ioutil.WriteFile(filename, data, 0666)
if err != nil {
log.Println("write file fail ", err)
return
}
}
func main() {
var cfg Cofig
parseFile("./config.ini", &cfg)
log.Printf("反序列化成功%#v", cfg)
parseDataToFile("./my.ini", &cfg)
}
model.go:结构体
package main
//定义结构体
type ServerConfig struct {
IP string `ini:"ip"`
Port int `ini:"port"`
}
type MysqlConfig struct {
UserName string `ini:"username"`
Passwd string `ini:"passwd"`
Database string `ini:"database"`
Host string `ini:"host"`
Port int `ini:"port"`
Timeout float64 `ini:"timeout"`
}
type Cofig struct {
ServerConf ServerConfig `ini:"server"`
MysqlConf MysqlConfig `ini:"mysql"`
}
init_config.go:序列化和反序列化细节实现。
package main
import (
"errors"
"fmt"
"log"
"reflect"
"strconv"
"strings"
)
func myLable(str string, res_type reflect.Type) (fieldname string, err error) {
//去掉前后[]
lable_str := str[1 : len(str)-1]
//循环查找tag
for i := 0; i < res_type.NumField(); i++ {
field := res_type.Field(i)
lable_name := field.Tag.Get("ini")
if lable_name == lable_str {
//属性名,不是tag名
fieldname = field.Name
return
}
}
return
}
func myField(fieldName string, line string, res interface{}) (err error) {
//获取key和value
key_str := strings.TrimSpace(line[0:strings.Index(line, "=")])
val_str := strings.TrimSpace(line[strings.Index(line, "=")+1:])
//找到res中属性名为fieldName的field
res_val := reflect.ValueOf(res).Elem()
//fmt.Println(fieldName)
lable_name := res_val.FieldByName(fieldName)
//找到tag为key_str的属性
lable_type := lable_name.Type()
if lable_type.Kind() != reflect.Struct {
return errors.New(fieldName + " is not a struct")
}
var key_name string
for i := 0; i < lable_type.NumField(); i++ {
field := lable_type.Field(i)
tag_str := field.Tag.Get("ini")
if tag_str == key_str {
key_name = field.Name
break
}
}
//设置值
val_field := lable_name.FieldByName(key_name)
switch val_field.Type().Kind() {
case reflect.String:
val_field.SetString(val_str)
case reflect.Int:
tmp, err := strconv.ParseInt(val_str, 10, 32)
if err != nil {
log.Println(err)
return err
}
val_field.SetInt(tmp)
case reflect.Float64:
tmp, err := strconv.ParseFloat(val_str, 64)
if err != nil {
log.Println(err)
return err
}
val_field.SetFloat(tmp)
}
return
}
// 解析配置,序列化和反序列化
func Unmarshal(data []byte, res interface{}) (err error) {
//得到res类型
res_type := reflect.TypeOf(res)
//是输出型参数,需要是指针类型
if res_type.Kind() != reflect.Ptr {
return errors.New("args type error not pointer")
}
//获得实际类型
real_res_type := res_type.Elem()
if real_res_type.Kind() != reflect.Struct {
return errors.New("args real type error not struct")
}
//以'\n'分隔每一行
var myFieldName string
datalines := strings.Split(string(data), "\n")
for _, line := range datalines {
//去掉空格
line = strings.TrimSpace(line)
//处理有注释的情况
if len(line) == 0 || line[0] == '#' || line[0] == ';' {
continue
}
//按方括号判断大标签的情况
if line[0] == '[' {
//赋值大标签
myFieldName, err = myLable(line, real_res_type)
if err != nil {
log.Println("handle lable fail ", err)
return
}
continue
}
err = myField(myFieldName, line, res)
if err != nil {
continue
}
}
return
}
func Marshal(res interface{}) (data []byte, err error) {
res_type := reflect.TypeOf(res).Elem()
res_val := reflect.ValueOf(res).Elem()
if res_type.Kind() != reflect.Struct {
return nil, errors.New("type error")
}
var lines []string
for i := 0; i < res_type.NumField(); i++ {
//取类型字段
type_field := res_type.Field(i)
//取值
val_field := res_val.Field(i)
//获得标签
tag_str := type_field.Tag.Get("ini")
if len(tag_str) == 0 {
continue
}
//标签
line := fmt.Sprintf("[%s]\n", tag_str)
fmt.Print(line)
lines = append(lines, line)
if type_field.Type.Kind() != reflect.Struct {
continue
}
//遍历成员的属性
for j := 0; j < type_field.Type.NumField(); j++ {
sub_type_field := type_field.Type.Field(j)
//取标签
sub_tag := sub_type_field.Tag.Get("ini")
if len(sub_tag) == 0 {
continue
}
//取值
sub_val_field := val_field.Field(j)
//Interface为真正的值
line = fmt.Sprintf("%s = %v\n", sub_tag, sub_val_field.Interface())
fmt.Print(line)
lines = append(lines, line)
}
lines = append(lines, "\n")
}
for _, str := range lines {
d := []byte(str)
data = append(data, d...) //...将切片打散
}
return
}
写入到文件演示:
问题:
我直接编译main.go出现报错:
原因:
- go run和go build命令都会编译文件,go run不仅编译还会直接执行文件。由于上面只输入了main.go文件,在main.go文件中引用其它文件的函数,结构或变量会出现找不到的情况。
解决:
- 由于go run必须输入文件,此时需要输入所有引用文件
- go build可以不输入文件,默认自动查找引用文件并打开包。或者go build加上所有引用文件。