Go语言入门21:反射

Keywords: #技术 #Golang #Go 入门笔记
Table of Contents

Go 语言入门基础学习笔记之 Go 语言的反射

golang

反射

变量的结构

image.png

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 包中提供下面几个函数:

  1. ValueOf:传入一个变量,获取这个变量的 value

image.png

  1. TypeOf:传入一个变量,获取这个变量的具体 typer

image.png

使用 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 结构体包含两个字段 Namesex,并且为 Name 字段添加了两个标签 infodoc。标签用于存储与字段相关的元信息。
  • 反射函数 findTag:
    • 接受一个空接口参数 str,可以传入任何类型。
    • 使用 reflect.TypeOfElem() 方法获取传入参数的类型信息。
    • 通过 NumField() 方法获取结构体字段的数量,并遍历这些字段。
    • 使用 Tag.Get 方法提取每个字段的 infodoc 标签,并打印出来。
  • 主函数 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 [周星驰 张柏芝]}

参考课程:

  1. 8小时转职Golang工程师
  2. Go语言教程 | 菜鸟教程