Golang学习笔记(六):反射
反射(一)
反射是为了解决什么?(AI)(如有不适请自行跳过)
第一部分:Go 结构体与方法的设计哲学回顾 (我们已经建造的城市)
到目前为止,你已经掌握了 Go 语言构建程序的核心蓝图。这个蓝图可以用三个关键词来概括:组合、接口、并发。
- 【数据骨架】struct 是纯粹的数据容器
- 哲学:数据和行为分离。struct 的首要职责就是清晰地、静态地定义一个数据结构。它像建筑的钢筋骨架,结实、可预测。
- 对比 Python:它取代了 class 的数据部分(成员变量),但抛弃了继承体系。Go 选择了组合(Embedding),推崇“有一个 (has-a)”而不是“是一个 (is-a)”,这让结构更灵活、更不易出错。
- 【行为注入】方法 (Methods) 是绑定到任何类型的行为
- 哲学:行为可以被附加 (attach)到数据上。receiver 机制就是那个“胶水”,它将一个函数绑定到一个类型上,让这个类型“活”起来。
- 对比 Python:它取代了 class 的方法部分。但 Go 的方法更自由,它可以绑定到任何你自定义的类型上(struct, int 别名, map 别名...),而不仅仅是 struct。
- 【抽象契约】接口 (Interfaces) 是定义行为的“插座标准”
- 哲学:关心“能做什么”,而不是“是什么”。接口是 Go 实现多态和解耦的灵魂。它是静态的、编译时的“鸭子类型”。
- 对比 Python:它在思想上与 Protocol 相同,都是结构化子类型,但 Go 的接口是由编译器强制执行、并且是运行时的一个实体,远比 MyPy 的“建议”要强力。
到此为止,我们建造的 Go “城市”是这样的:
这是一个规划极其严谨的城市。所有建筑(struct)的蓝图(类型)都在施工前(编译时)被严格审查。建筑之间的所有管道、电缆接口(interface)都遵循着统一的、不可更改的【静态标准】。市民们(Goroutines)通过高效的、预设的交通网络(channel)进行交流。一切都井井有条,安全、高效、可预测。
第二部分:反射 (Reflection) 的登场 (我们需要一个“万能勘探车”)
现在,问题来了。在这个规划完美的城市里,我们突然接到一个前所未有的、在“蓝图”上没有预设过的任务。
反射旨在解决的问题是:
“当你在【运行时 (Runtime)】,面对一个你【在编译时完全未知】的、装着神秘货物的“集装箱”时,你该如何打开它、检查里面的货物是什么、甚至对货物进行操作?”
这个“集装箱”,就是 any (空接口)。
反射就是那台派来处理这个神秘集装箱的【万能勘探+操作车】。
它主要能做三件事:
- 识别 (Identification):
- 问题:我收到了一个 any 类型的变量,它里面装的到底是个 User 结构体,还是一个 int,还是一个 map?
- 反射:提供了 reflect.TypeOf(),像一台 X光机,可以扫描出“集装箱”里货物的类型蓝图。它会告诉你:“报告!里面是 main.User 类型,它是一个 struct,有 Name 和 Age 两个字段。”
- 读取 (Reading):
- 问题:好,我知道里面是 User 了,那这个 User 的 Name 字段现在具体的值是什么?
- 反射:提供了 reflect.ValueOf(),像一个机械臂,可以伸进“集装箱”,读取出货物具体的值。它会告诉你:“报告!Name 字段的值是字符串‘Alice’。”
- 修改 (Writing):
- 问题:我不仅想看,我还想把这个 User 的 Age 字段改成 30。
- 反射:它的机械臂还带写入功能。在满足一定安全条件(比如你操作的是一个指针)下,它可以修改“集装箱”里货物的值。
一句话总结:反射是在【运行时】对 Go 语言的【类型系统】进行的一次“逆向工程”。
给你的初步印象
- 什么时候会看到它? 在你日常的后端开发中,你几乎不会自己去驾驶这台“勘探车”。但你每天都在使用的核心基础设施,比如 encoding/json(JSON 编解码)、GORM(数据库 ORM),它们的内部就像是一个巨大的车库,停满了各式各样的反射车辆,为你处理各种你自定义的 struct。
- 它与接口的区别?
- 接口是在静态蓝图上,定义了一个已知的“插座标准”。
- 反射是在运行时,面对一个未知的物体,去动态地分析它的结构。
- 为什么要谨慎? 因为你一旦开动了这台“勘探车”,就等于放弃了 Go 编译器为你提供的一切静态安全保证。操作失误(比如试图把一个 string 字段设置成 int)不会在编译时被发现,而是会在运行时导致程序 panic(车毁人亡)。
结论:
在你接下来的学习中,请带着这样的印象:你即将学习的是 Go 语言的“终极武器”,一个主要由框架和库的作者使用的、用于实现“不可能的任务”的工具。掌握它的概念,能让你理解 Go 生态的底层逻辑;而掌握它的用法,则是在你成为 Go 高级开发者后的必经之路。
介绍反射
反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
| 语言 | 反射的核心动机 | 它的角色是什么? |
|---|---|---|
| Java | 在一个严格的 OOP 体系中,为框架提供运行时动态性和打破封装的能力。 | 一把打破常规的“万能钥匙”。 |
| Go | 在一个极简的静态类型体系中,为通用库提供运行时探知未知类型结构的能力。 | 一台能应对未知物体的“通用探测器”。 |
| Python | 语言的根基就是动态性,不需要额外的反射系统。 | 空气和水。它是语言与生俱来的、无处不在的本能。 |
反射在通用库中十分常见,例如encoding/json->Marshal->marshal就用到了反射:
func (e *encodeState) marshal(v any, opts encOpts) (err error) {
defer func() {
if r := recover(); r != nil {
if je, ok := r.(jsonError); ok {
err = je.error
} else {
panic(r)
}
}
}()
e.reflectValue(reflect.ValueOf(v), opts) // 在这里
return nil
}(encodedState的marshal方法)
ORM框架也使用了反射
Go语言的变量是分为两部分的:
- 类型信息:预先定义好的元信息
- 值信息:程序运行过程中可动态变化的
在Go的反射机制中,任何接口值都是由一个具体类型和具体类型的值两部分组成的
在Go语言中,reflect包提供了反射的相关功能。任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成;reflect包还提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value和Type
使用反射
func main() {
var x any = []int{31, 63, 127}
typeOfX := reflect.TypeOf(x) // 获取变量的类型
valueOfX := reflect.ValueOf(x) // 获取变量的值
fmt.Printf("For variable %#v, type is : %v, value is : %v\n", x, typeOfX, valueOfX)
// For variable []int{31, 63, 127}, type is : []int, value is : [31 63 127]
}深入反射
在反射中类型还分为两种:类型(Type) 和 种类(Kind) 。在Go中我们可以使用type关键字构造很多自定义类型,而种类则是指 底层的类型 。但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
在深入之前需要知道,对于反射得到的类型变量(比如这里的typeOfX,可以分别用.Name()和.Kind()获取类型名称和种类名称):
func main() {
x := person{ // 不一定是结构体,任意类型变量均可
name: "Tom",
age: 18,
}
typeOfX := reflect.TypeOf(x)
valueOfX := reflect.ValueOf(x)
fmt.Printf("For variable %#v, type is : %v, value is : %v\n", x, typeOfX, valueOfX)
// For variable []int{31, 63, 127}, type is : []int, value is : [31 63 127]
fmt.Printf("对于反射得到的类型%#v, 类型名称为: %v, 种类名称为: %v\n", typeOfX, typeOfX.Name(), typeOfX.Kind())
// 对于反射得到的类型&reflect.rtype{t:abi.Type{Size_:0x18, PtrBytes:0x8, Hash:0xa365fbec, TFlag:0x7, Align_:0x8, FieldAlign_:0x8, Kind_:0x19, Equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0xd9c5a0), GCData:(*uint8)(0xde7b10), Str:11378, PtrToThis:26848}},
// 类型名称为: person,
// 种类名称为: struct // 这里就拿到了`person`结构体的父级类型
}如果取的是结构体指针,则会得到下面的结果:
类型名称为: , 种类名称为: ptr.Name()方法返回值为空,显然person指针并非已定义类型
code: type ptrPerson *person
output: 类型名称为: ptrPerson, 种类名称为: ptr显式定义之后就可以了,记得再显式声明一个这个类型的变量来装结构体指针
不只是结构体指针打印不出来类型名称,切片、键值对乃至通道这种复合数据类型都无法获取类型名称,只有种类名称能够区分它们
func reflectPrint(x any) {
typeOfX := reflect.TypeOf(x)
fmt.Printf("对于反射得到的类型%#v, \n类型名称为: %v, 种类名称为: %v\n", typeOfX, typeOfX.Name(), typeOfX.Kind())
}
func main() {
x := person{
name: "Tom",
age: 18,
}
reflectPrint(x) // 类型名称为: person, 种类名称为: struct
y := []int{31, 63, 127}
reflectPrint(y) // 类型名称为: , 种类名称为: slice
z := map[string]string{
"username": "MemorySeer",
"age": "20",
}
reflectPrint(z) // 类型名称为: , 种类名称为: map
m := make(chan int, 3)
reflectPrint(m) // 类型名称为: , 种类名称为: chan
close(m)
}type包中定义的种类名称如下:
type Kind uint
const (
Invalid Kind = iota // 这是个iota常量计数器,所以这里其实是枚举, 从0-26
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Pointer
Slice
String
Struct
UnsafePointer)
// Ptr is the old name for the [Pointer] kind.
const Ptr = Pointer // 这个是22reflect.ValueOf VS 类型断言
reflect.ValueOf返回的是原始值(类型就叫Value),Go不支持隐式类型转换,所以一个ValueOf后的整型无法与一个原味整型进行运算
原始值的类型可以转回去:
_ = a.Int() + int64(b) // a.Int()返回int64类型的值reflect.Value类型还有下面这些针对不同类型的原始值获取方法:
Interface{} interface{}:返回空接口类型,可以使用类型断言转换为指定类型Int() int64Uint() uint64Float() float64Bool() boolBytes{} []bytesString() string
知道返回值类型是什么就可以了
反射与结构体 (转载)
反射查看结构体字段和方法
package main
import (
"fmt"
"reflect")
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// Hello 绑方法
func (u User) Hello() {
fmt.Println("Hello")
}
// Poni 传入interface{}
func Poni(o interface{}) {
t := reflect.TypeOf(o)
fmt.Println("类型:", t)
fmt.Println("字符串类型:", t.Name())
// 获取值
v := reflect.ValueOf(o)
fmt.Println(v)
// 可以获取所有属性
// 获取结构体字段个数:t.NumField()
for i := 0; i < t.NumField(); i++ {
// 取每个字段
f := t.Field(i)
fmt.Printf("%s : %v | ", 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, "zs", 20}
Poni(u)
}反射查看结构体匿名字段
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 匿名字段
type Boy struct {
User
Addr string
}
func main() {
m := Boy{User{1, "zs", 20}, "bj"}
t := reflect.TypeOf(m)
fmt.Println(t)
// Anonymous:匿名
fmt.Printf("%#v\n", t.Field(0))
// 值信息
fmt.Printf("%#v\n", reflect.ValueOf(m).Field(0))
}反射设置(结构体)变量的值
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
// 修改结构体值
func SetValue(o interface{}) {
v := reflect.ValueOf(o)
// 获取指针指向的元素
v = v.Elem()
// 取字段
f := v.FieldByName("Name")
if f.Kind() == reflect.String {
f.SetString("kuteng")
}
}
func main() {
u := User{1, "5lmh.com", 20}
SetValue(&u)
fmt.Println(u)
}反射调用方法
package main
import (
"fmt"
"reflect"
)
// 定义结构体
type User struct {
Id int
Name string
Age int
}
func (u User) Hello(name string) {
fmt.Println("Hello:", name)
}
func main() {
u := User{1, "5lmh.com", 20}
v := reflect.ValueOf(u)
// 获取方法
m := v.MethodByName("Hello")
// 构建一些参数
args := []reflect.Value{reflect.ValueOf("6666")}
// 没参数的情况下:var args2 []reflect.Value
// 调用方法,需要传入方法的参数
m.Call(args)
}反射获取字段tag
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string `json:"name1" db:"name2"`
}
func main() {
var s Student
v := reflect.ValueOf(&s)
// 类型
t := v.Type()
// 获取字段
f := t.Elem().Field(0)
fmt.Println(f.Tag.Get("json"))
fmt.Println(f.Tag.Get("db"))
}