Go 深入学习
文章目录
参考 《Go 语言设计与实现》 《Go 专家编程》 等资料的简单总结
因为之前记录在飞书云文档上,在尝试转换为md的过程中可能出现部分格式问题(转换工具),提供云文档链接便于查看*
编译过程
概念
- 抽象语法树(AST)-
一种用来表示编译语言的语法结构的树形结构,用于辅助编译器进行语法分析。
- 静态单赋值(SSA)
是一种中间代码的特性,即每个变量只赋值一次。
- 指令集架构(CPU 中用来计算和控制计算机系统的一套指令的集合)
分为复杂指令集体系(CISC)和精简指令集体系(RISC)
- 复杂指令集:
- 特点:指令数量多长度不等,有额外损失性能。
- 常用的是 AMD64(x86_64/x64) 指令集
- 精简指令集:
- 特点:指令精简长度相等
- 常用有 ARM
编译四阶段
词法分析 + 语法分析
- 词法分析(词法分析器)作用:将源文件转换为一个不包含空格,回车,换行等的 Token 序列。
通过 cmd/compile/internal/syntax/scanner.go
扫描数据源文件来匹配对应的字符,跳过空格和换行等空白字符。
- 语法分析(语法分析器)作用:将 Token 序列转为具有意义的结构体所组成的抽象语法树。
使用 LALR(1)
文法解析 Token,
类型检查
按顺序检查语法树中定义和使用的类型,确保不存在类型匹配问题。(包括结构体对接口的实现等),同时也会展开和改写一些内置函数如 make
改写为 makechan
,makeslice
,makemap
等。
拓展:
- 强弱类型
- 强类型:类型错误在编译期间会被指出。
Go,Java
- 弱类型:在运行时将类型错误进行隐式转换。
Js,PHP
- 静态类型检查和动态类型检查
- 静态类型检查:对源代码的分析来确定程序类型安全的过程,可以减少运行时的类型检查。
- 动态类型检查:编译时为所有对象添加类型标签之类的信息。运行时根据这些类型信息进行动态派发,向下转型,反射等特性。
Go Java
等都是两者相结合。比如接口像具体类型的转换等。。。- 执行过程
- 切片
OTARRAY
先对元素类型进行检查,再根据操作类型([]int,[...]int,[3]int
)的不同更新节点类型。
- 哈希表
OTMAP
创建 TMAP
结构,存储哈希表的键值类型并检查是否存在类型不匹配的错误。
- 关键字
OMAKE
根据 make
的第一个参数的类型进入不同的分支,然后更改当前节点的 Op 属性
- 切片:
长度参数必须被传入,长度必须小于等于切片的容量。
- 哈希表:
检查哈希表的可选初始容量大小
- Channel
检查可选 Channel 初识缓冲大小
中间代码生成
经过类型检查后,编译器并发编译所有 go 项目的函数生成中间代码,中间会对 AST 做一些替换工作。
go 编译器中间代码使用 SSA
特性,会对无用的变量和片段进行优化。
细节:
生成中间代码前编译器会替换一些抽象语法树中的元素。在遍历语法树时会将一些关键字和内置函数转化为函数调用。
机器码生成
Go 语言将 SSA 中间代码生成对应的目标机器码。
GOOS=linux GOARCH=amd64 go build main.go
- GOOS : 目标平台
- mac 对应 darwin
- linux 对应 linux
- windows 对应 windows
GOARCH
:目标平台的体系架构【386,amd64,arm】, 目前市面上的个人电脑一般都是 amd64 架构的- 386 也称 x86 对应 32 位操作系统
- amd64 也称 x64 对应 64 位操作系统
- arm 这种架构一般用于嵌入式开发。比如 Android , IOS , Win mobile , TIZEN 等
go 语言支持的架构:AMD64,ARM,ARM64,MIPS,MIPS64,ppc64,s390x,x86,Wasm
类型系统
分类
go 语言数据类型分为 命名类型
和 未命名类型
- 命名类型:
预声明的简单类型
和自定义类型
- 未命名类型(类型字面量):
array,chan,slice,map,pointer,struct,interface,func
注意:未命名类型==类型字面两==复合类型
底层类型
预声明类型
和类型字面量
的底层类型是自身自定义类型
的底层类型需要逐层向下查找
type new old // new 的底层类型和old的底层类型相同
仔细研究 Go(golang) 类型系统 - 知乎 (zhihu.com)
类型相同
- 两个
命名类型
:两个类型声明语句相同
var a int
var b int
//a 和 b类型相同
var c A
var d A
//c 和 d类型相同
- 两个
未命名
类型:声明时的类型字面量
相同且内部元素类型
相同 - 别名:永远相同
type myInt2 = int//起别名--myInt1和int完全相同
命名类型
和未命名类型
:永远不同
类型赋值
var a T1 //a的类型是T1
var b T2 //b的类型是T2
b = a //如果成功说明a可以直接赋值b
可以赋值条件:
- T1 和 T2 类型相同
- T1 和 T2 具有相同的底层类型,且其中至少有一个是
未命名类型
type mySlice []int
var list1 mySlice //mySlice 命名类型
var list2 []int //[]int 未命名类型
list1 = list2 //可以直接赋值
- 接口类型看方法集,只要实现了就能赋值。
- T1 和 T2 的底层类型都是 chan 类型,且 T1 和 T2 至少有一个是
未命名类型
type T chan int // 相同元素类型
var t1 chan int // 未命名类型
var t2 T // 命名类型
t2 = t1 // 成功赋值
nil
可以赋值给pointer,func,slice,map,chan,interface
a
是可以表示类型T1
的常量值
类型强制转换
Go 是强类型语言,如果不满足自动类型转换的条件,则必须强制类型转换.
语法:var a T = (T)(x)
将 x 强制类型转换为 T
非常量类型的变量 x 可以强制转化并传递给类型 T,需要满足如下任一条件:
- 可以
直接赋值
相同底层类型
.- x 的类型和 T 都是
未命名的指针类型
,并且指针指向的类型具有相同的底层类型
。
type T1 int
type T2 T1
var p1 *T2 // *T2
var p2 *int // *int
p2 = (*int)(p1) // 指针指向的底层类型都是int
- x 的类型和 T
都是整型,或者都是浮点型
。 - x 的类型和 T
都是复数类型
。 - x 是
整数值
或[]byte
类型的值,T 是string
类型。
s := string(123)
fmt.Println([]byte(s)) // [123]
- x 是一个
字符串
,T 是[]byte或[]rune
。 浮点型,整型
之间可以强制类型转换(可能会损失数据精度)
类型方法
只有命名类型才有方法,且只能给当前包下的类型添加方法
自定义类型
struct {
//这是一个未命名结构体类型
name string
age int
}
type Student struct{
//Student是一个命名结构体类型
name string
age int
}
interface{
//未命名接口类型
eat()
}
type name interface {
//name是一个命名接口类型
eat()
}
方法
Go 语言类型方法是对类型行为的封装,GO 语言的方法其实是特殊的函数,其将方法接收者作为函数第一个参数
//类型接收者是值类型
func (t typeName)methodName(paramList)(ReturnList){
//method body
}
//类型接收者是指针类型
func (t *typeName)methodName(paramList)(ReturnList){
//method body
}
方法调用
- 一般调用:
实例.方法名(参数)
s := Student{
name: "张三", age: 19}//Student类型对象的创建和初始化
s.eat()//调用方法
- 类型字面量调用:
类型.方法(实例,参数)
Student.eat(s)//因为方法其实就是特殊的函数
func eat(s Student){
// eat()方法转为函数
fmt.Println(s.name, "正在吃饭")
}
方法调用时的类型转换
一般调用
会根据接受者类型
自动转换。值->指针,指针->值
type T struct {
a int
}
func (t T) VSet(n int) {
t.a = n
}
func (t *T) PointSet(n int) {
t.a = n
}
func method() {
t1 := T{
a: 0}
t2 := T{
a: 0}
(&t1).VSet(1)
fmt.Println(t1.a) // 0
t2.PointSet(1)
fmt.Println(t2.a) // 1
}
类型字面量
调用不自动转换
pointer := &Data{
"张三"}
value := Data{
"张三"}
pointer := &Data{
"张三"}
value := Data{
"张三"}
(*Data).testPointer(pointer, 3) // 类型字面量 显式调用
(*Data).testValue(pointer, 3) // 正常
Data.testValue(value, 3)
// Data.testPointer(pointer, 3) // 类型检查错误
// Data.testPointer(value, 3) // 类型检查错误
// Data.testPointer(pointer, 3) // 类型检查错误
类型断言
i.(TypeName)
i必须是接口变量,TypeName
可以是 具体类型名
或者 接口类型名
- 若
TypeName
是具体类型名:判断i
所绑定的实例类型是否就是具体类型TypeName
- 若
TypeName
是接口类型名:判断 i 所绑定的实例对象是否同时实现了TypeName
接口
具体:
o := i.(TypeName) //不安全, 会panic()
会进行值拷贝,保存的是副本
o, ok := i.(TypeName) //安全
如果上述两个都不满足,则 ok 为 false(满足一个就是 true), 变量 o 是 TypeName 类型的“零值”,此种条件分支下程序逻辑不应该再去引用 o,因为此时的 o 没有意义。
接口类型查询
i
必须是接口类型,如果 case 后面是一个接口类型名,且接口变量 i
绑定的实例类型实现了该接口类型的方法,则匹配成功,v 的类型是接口类型,v底层绑定的实例是i绑定具体类型实例的副本.
switch v := i.(type){
case type1:
...
case type2:
...
...
}
数据结构
数组
初始化
[5]int{
1,2,3} //显式指定大小
[...]int{
1,2,3}//隐式推导
- 上限推导:编译器在编译时就会会确定元素个数来确定类型,所以两者在运行时没有区别。
- 语句转换:由字面量组成的数组根据元素个数编译器在类型检查期间会做出两种优化(不考虑逃逸分析)
- 元素个数
n<=4
:直接在栈上赋值初始化
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
- 元素个数
n>4
:先在静态存储区初始化数组元素,并将临时变量赋值给数组(栈)。
var arr [5]int
statictmp_0[0] = 1
statictmp_0[1] = 2
statictmp_0[2] = 3
statictmp_0[3] = 4
statictmp_0[4] = 5
arr = statictmp_0
访问和赋值
使用常量或整数直接访问数组会在类型检查期间进行数组越界分析,使用变量会在运行时检查。
切片
动态数组,长度不固定,可以追加元素,它会在容量不足的情况下自动扩容。
数据结构
type SliceHeader struct {
Data uintptr //指向底层数组
Len int //切片长度
Cap int //切片容量,Data数组长度
}
初始化
arr[0:3] or slice[0:3] //使用下标
slice := []int{
1, 2, 3} //字面量
slice := make([]int, 10) //关键字
- 使用下标
创建一个指向底层数组的切片结构体。修改数据会影响底层数组。
- 字面量
创建数组进行赋值然后通过下标进行初始化
var vstat [3]int //先创建数组
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat
slice := vauto[:] //最后使用下标创建切片
- 关键字
- 切片很小且不会发生逃逸,直接通过下标在
栈
或静态存储区
创建,然后通过下标进行初始化。
// make([]int,3,4)
var arr [4]int
n := arr[:3]
- 切片较大或逃逸:在堆上初始化切片再初始化。
new
相当于nil
a := *new([]int)
// var a []int
追加和扩容
append
会在编译时期被当成一个 TOKEN
直接编译成汇编代码,因此 append
并不是在运行时调用的一个函数,如果发生扩容则会调用 growslice()
函数。
// expand append(l1, l2...) to
// init {
// s := l1
// n := len(s) + len(l2)
// // Compare as uint so growslice can panic on overflow.
// if uint(n) > uint(cap(s)) {
// s = growslice(s, n)
// }
// s = s[:n]
// memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
// }
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap // old_cap
doublecap := newcap + newcap // 2*old_cap
if cap > doublecap {
//大于2*old_cap,直接分配 new_zap
newcap = cap
} else {
if old.cap < 1024 {
// <1024 直接2*old_cap
newcap = doublecap
} else {
// 1.25增长
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
}
new_cap <= old_cap
直接向后覆盖- 超过则扩容:为切片重新分配新的空间并复制原数组内容。
- 期望容量
new_cap > 2*old_cap
: 直接使用new_cap
进行分配 old_cap<1024
:直接分配2*old_cap
old_cap>=1024
:每次增加old_cap*1.25
直到大于为止
然后根据切片中的元素大小对齐内存。如果元素所占字节大小为 1,2或8
的倍数时会根据 class_to_size数组
向上取整来提高内存分配效率减少碎片。
var class_to_size = [_NumSizeClasses]uint16{
0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176,...]
var arr []int64 //元素占8字节
arr = append(arr, 1, 2, 3, 4, 5) //期望cap为5 期望分配5*8=40字节
fmt.Println(len(arr), cap(arr)) // 经过对齐分配48字节, cap为48/8=6
//5 6
复制切片
copy(a,b)
将 b 切片内容复制到 a 切片
使用 memmove()
进行内存复制
拓展表达式
arr2 := arr1[start:end:max]
指定 arr2
的容量为 max-start
所以 max
不能超过 cap(arr1)
arr := make([]int, 0, 5)//len=0 cap=5
arr1 := arr[2:3] //len=1 cap=3 默认max=5
arr2 := arr[2:3:4] //len=1 cap=2
arr3 := arr[5:5:5] //len=0 cap=0
Map
设计原理
- 哈希函数
输出范围大于输入范围且结果需较为均匀
- 处理哈希冲突
- 开放寻址法:退化为
O(N)
依次探测和比较数组中的元素来判断目标是否存在于哈希表中,冲突了就继续往后找位置
- 拉链法
1. 找到键相同的键值对 — 更新键对应的值;
1. 没有找到键相同的键值对 — 在链表的末尾追加新的键值对;
在一般情况下使用拉链法的哈希表装载因子都不会超过 1
装载因子:元素个数 / 桶个数
数据结构
// map数据结构 runtime/map.go/hmap
type hmap struct {
count int // 存储节点数
flags uint8 // 并发
B uint8 // buckets桶个数为2^B次方
noverflow uint16 // 溢出桶数
hash0 uint32 // hash seed
buckets unsafe.Pointer // bucket数组指针
oldbuckets unsafe.Pointer // 扩容时旧桶
nevacuate // 迁移进度
extra // 原有 buckets 满载后,会发生扩容动作,在 Go 的机制中使用了增量扩容,如下为细项:
/*
overflow 为 hmap.buckets (当前)溢出桶的指针地址
oldoverflow 为 hmap.oldbuckets (旧)溢出桶的指针地址
nextOverflow 为空闲溢出桶的指针地址
*/
}
//bucket数据结构 runtime/map.go/bmap 运行时
type bmap struct {
topbits [8]uint8 //存储key hash高8位,用于快速查找到目标
keys [8]keytype
values [8]valuetype
overflow uintptr //溢出桶
}
//每个bucket可以存储8个kv
tophash
:用于快速查找
<5
:存储状态empetyRest
:判空emptyOne
:当前为空ex,ey
:扩容相关时新的位置- evacuatedEmpty:迁移完毕
>5
:存储hash值高8位
当计算的哈希值小于 minTopHash 时,会直接在原有哈希值基础上加上 minTopHash,确保哈希值一定大于 minTopHash。
初始化
- 字面量
//hash := map[string]int{
// "1": 1,
// "2": 2,
// "3": 3,
//}
hash := make(map[string]int,3)
hash["1"] = 1
...
- 元素个数
n<=25
:先make
再挨个赋值 - 元素个数
n>25
:先make
再创建两个数组保存k,v
然后使用 for 循环进行赋值 - 运行时
make
- 当
hash被分配在栈上
且容量n<8
则使用快速初始化
hash 表
func makemap_small() *hmap {
h := new(hmap)
h.hash0 = fastrand()
return h
}
- 否则由传参元素个数
n
确定B
1. 如果桶数`x = 2^B < 24` 不创建溢出桶
1. 否则创建`2^(B-4)`个溢出桶
读写操作
- 查找
- 判断 map 是否为空,空则直接返回零值;判断当前是否并发读写 map,若是则抛出异常
- 根据 key 值算出 hash 值,取hash 值低 8 位与 hmap.B 取模确定 bucket 位置
- 判断是否正在发生扩容(
h.oldbuckets
是否为 nil),若正在扩容,则到老的 buckets 中查找(因为 buckets 中可能还没有值,搬迁未完成),若该 bucket 已经搬迁完毕。则到 buckets 中继续查找 - 取hash 值高 8 位在 tophash 数组中查询
- 如果 tophash 匹配成功,则计算 key 的所在位置,正式对比两个 key 是否一致
- 当前 bucket 没有找到,则继续从下个 overflow 的 bucket 中查找。
注:如果查找不到,也不会返回空值,而是返回相应类型的 0 值。
- 插入
- 判断
hmap
是否已经初始化(是否为nil
),根据 key 值算出哈希值;判断是否并发读写map
,若是则抛出异常,标记并发标记位。 - 取
hash值低8位
与hmap.B
取模确定bucket
位置,并判断是否正在扩容,若正在扩容中则先迁移再接着处理 - 迭代
buckets
中的每一个bucket
(共 8 个),对比bucket.tophash
与 top(高八位)是否一致。 - 若不一致,判断是否为空槽。若是空槽(有两种情况,第一种是没有插入过。第二种是插入后被删除),则把该位置标识为可插入 tophash 位置。注意,这里就是第一个可以插入数据的地方。
- 若 key 与当前 k 不匹配则跳过。但若是匹配(也就是原本已经存在),则进行更新。最后跳出并返回 value 的内存地址。
- 判断是否迭代完毕,若是则结束迭代 buckets 并更新当前桶位置
- 若满足三个条件:触发最大负载因子 、存在过多溢出桶
overflow buckets
、没有正在进行扩容。就会进行扩容动作(以确保后续的动作)
如果当前 bucket 已满则使用预先创建的溢出桶或者新创建一个溢出桶来保存数据,溢出桶不仅会被追加到已有桶的末尾,还会增加 noverflow
的数量
最后返回内存地址。这是因为隐藏的最后一步写入动作(将值拷贝到指定内存区域)是通过底层汇编配合来完成的,在 runtime 中只完成了绝大部分的动作
扩容
- 装载因子
n > 6.5
引发增量扩容
预分配 2
倍原 bucket
大小的 newbucket
放到 bucket
上,原 bucket
放到 oldbucket
上。
- 溢出桶数量
n > 2^15
引发等量扩容
和增量扩容的区别就是创建和原 bucket
等大小的新桶,最后清空旧桶和旧的溢出桶
如果处于扩容状态,每次写操作时,就先搬迁 bmap
数据到新桶(增量扩容分到两个桶,等量扩容分到一个桶)再继续,读会优先从旧桶读。
为什么字符串不可修改
- string 通常指向字符串字面量存储在只读段,不可修改
- map 中可以使用 string 作为 key,如果 key 可变则其实现会变得复杂
为什么 map 随机遍历?
- hash 随机写入
- 成倍扩容迫使元素顺序变化(分流到两个桶)
- 设计者不希望开发者依赖
map
的遍历顺序进行编程,所以每次初始化一个随机数作为起始点。
所以可以说「Go 的 Map 是无序的」。
字符串
概念
type string string
string
是8byte
字节的集合,通常但并不一定是 UTF-8 编码的文本。string
可以为空(长度为 0),但不会是nil
string
对象不可以修改。
数据结构
type StringHeader struct {
Data uintptr //指向底层数组的指针
Len int //数组大小
}
字符串分配到只读内存,所有的修改操作都是复制到切片然后修改
拼接
拼接会先获取长度,然后开辟空间最后复制数据
类型转换
一般两者之间直接转换会复制一遍,但 []byte 转为 string
在某些情况下不会复制
- 作为
map
的 key 进行临时查找 - 字符串临时拼接时
- 字符串比较时
反射转换
使用反射不需要开辟新空间(使用有风险)
// String to Bytes
func UnsafeStringToBytes(str string) []byte {
p := *(*reflect.StringHeader)(unsafe.Pointer(&str))
b := reflect.SliceHeader{
Data: p.Data,
Len: p.Len,
Cap: p.Len,
}
return *(*[]byte)(unsafe.Pointer(&b))
}
// Bytes to String
func UnsafeBytesToString(bs []byte) string {
return *(*string)(unsafe.Pointer(&bs))
}
为什么字符串不能修改:只读字段,map 中的键
iota
iota
代表了 const
声明块的行索引(下标从 0 开始)
const
块中每一行在 Go
中使用 spec
数据结构描述,spec
声明如下:
语言特色
函数调用
C
int func(int a1,int a2,...) int
{
return ...;
}
参数 <=6
会使用 寄存器
传递,>6的参数会从右往左依次入栈
。通过 eax
寄存器返回返回值.
Go
Go 语言完全使用栈来传递参数和返回值并由调用者负责清栈,通过栈传递返回值使得 Go 函数能支持多返回值,调用者清栈则可以实现可变参数的函数。Go 使用值传递的模式传递参数,因此传递数组和结构体时,应该尽量使用指针作为参数来避免大量数据拷贝从而提升性能。
Go 方法调用的时候是将接收者作为参数传递给了 callee,接收者分值接收者和指针接收者。
当传递匿名函数的时候,传递的实际上是函数的入口指针。当使用闭包的时候,Go 通过逃逸分析机制将变量分配到堆内存,变量地址和函数入口地址组成一个存在堆上的结构体,传递闭包的时候,传递的就是这个结构体的地址。
Go 的数据类型分为值类型和引用类型,但 Go 的参数传递是值传递。当传递的是值类型的时候,是完全的拷贝,callee 里对参数的修改不影响原值;当传递的是引用类型的时候,callee 里的修改会影响原值。
带返回值的 return 语句对应的是多条机器指令,首先是将返回值写入到 caller 在栈上为返回值分配的空间,然后执行 ret 指令。有 defer 语句的时候,defer 语句里的函数就在 ret 指令之前执行。
闭包
当函数引用外部作用域的变量时,我们称之为闭包。在底层实现上,闭包由函数地址和引用到的变量的地址组成,并存储在一个结构体里,在闭包被传递时,实际是该结构体的地址被传递。因为栈帧上的值在该帧的函数退出后就失效了,因此闭包引用的外部作用域的变量会被分配到堆上。
defer
defer 语句调用的函数的参数是在 defer 注册时求值或复制的。因此局部变量作为参数传递给 defer 的函数语句后,后面对局部变量的修改将不再影响 defer 函数内对该变量值的使用。
但是 defer 函数里使用非参数传入的外部函数的变量,将使用到该变量在外部函数生命周期内最终的值。
接口
一组方法签名的集合。其存在静态类型(绑定的实例的类型)动态类型(方法签名)。
注:类型指针接受者实现接口,类型自身不可进行初始化接口。
类型自身实现接口,类型自身和类型指针均可初始化接口,且因为在调用方法时会对接受者进行复制,所以推荐指针接受者实现接口。
数据结构
//src/runtime/runtime2.go
//非空接口
type iface struct {
tab *itab // 用来存放接口自身类型和绑定的实例类型及实例相关的函数指针
data unsafe.Pointer // 数据
}
type itab struct {
inter *interfacetype // 接口自身静态类型
_type *_type // 数据类型
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
//空接口
type eface struct {
_type *_type //数据类型信息
data unsafe.Pointer //数据
}
// 类型信息
type _type struct {
size uintptr // 类型占用的内存空间
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // 用于判断类型是否相等
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
反射
反射是指在程序运行期对程序本身进行访问和修改的能力
reflect.TypeOf //能获取类型信息;
reflect.ValueOf //能获取数据的运行时表示;
三大法则
//第一法则:反射可以将接口类型变量转换为反射对象
/*
有了变量的类型之后,我们可以通过 Method 方法获得类型实现的方法,通过 Field 获取类型包含的全部字段。
对于不同的类型,我们也可以调用不同的方法获取相关信息:
结构体:获取字段的数量并通过下标和字段名获取字段 StructField;
哈希表:获取哈希表的 Key 类型;
函数或方法:获取入参和返回值的类型;
…
*/
var x int64 = 4
fmt.Println(reflect.TypeOf(x), reflect.ValueOf(x))
//第二法则:反射可以把反射对象还原为接口对象
/*
不过调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,
如果想要将其还原成最原始的状态还需要经过如下所示的显式类型转换:
v := reflect.ValueOf(1)
v.Interface().(int)
当然不是所有的变量都需要类型转换这一过程。
如果变量本身就是 interface{} 类型的,那么它不需要类型转换,
因为类型转换这一过程一般都是隐式的,所以我不太需要关心它,只有在我们需要将反射对象转换回基本类型时才需要显式的转换操作。
*/
var a interface{
} = 4.0
v := reflect.ValueOf(a) //反射对象
b := v.Interface() //接口对象
fmt.Println(a == b)
fmt.Println(reflect.TypeOf(a), reflect.TypeOf(b))
//第三法则:反射对象可修改,value值必须是可设置的
/*
func main() {
i := 1
v := reflect.ValueOf(i)
v.SetInt(10)
fmt.Println(i)
}
$ go run reflect.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
由于 Go 语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量,程序为了防止错误就会崩溃。
想要修改原变量只能使用如下的方法:
*/
i := 1
v = reflect.ValueOf(&i)
v.Elem().SetInt(20) //必须是通过目标的指针对其修改
fmt.Println(i)
当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换,将变量的类型和值转换成了 interface{} 并等待运行期间使用 reflect 包获取接口中存储的信息。
更新变量
/*
当我们想要更新 reflect.Value 时,就需要调用 reflect.Value.Set 更新反射对象,
该方法会调用 reflect.flag.mustBeAssignable 和 reflect.flag.mustBeExported
分别检查当前反射对象是否是可以被设置的以及字段是否是对外公开的:
func (v Value) Set(x Value) {
v.mustBeAssignable() //是否可以被设置 即必须是一个指针所指向的
x.mustBeExported() //是否对外公开
var target unsafe.Pointer
if v.kind() == Interface {
target = v.ptr
}
x = x.assignTo("reflect.Set", v.typ, target)
typedmemmove(v.typ, v.ptr, x.ptr)
}
*/
fmt.Println("更新变量======")
x := 3
v := reflect.ValueOf(&x)
v.Elem().Set(reflect.ValueOf(4))
fmt.Println("更新结构体并获取未导出字段值")
test1 := &test{
test: 1}
vtest := reflect.ValueOf(test1)
vtest.Elem().Set(reflect.ValueOf(test{
test: 2}))
fmt.Println(vtest.Elem().FieldByName("test").Int())
函数调用
//函数调用
/*
1.通过 reflect.ValueOf 获取函数 Add 对应的反射对象;
2.调用 reflect.rtype.NumIn 获取函数的入参个数;
3.多次调用 reflect.ValueOf 函数逐一设置 argv 数组中的各个参数;
4.调用反射对象 Add 的 reflect.Value.Call 方法并传入参数列表;
5.获取返回值数组、验证数组的长度以及类型并打印其中的数据;
*/
v := reflect.ValueOf(Add) //反射对象
if v.Kind() != reflect.Func {
return
}
t := v.Type()
argv := make([]reflect.Value, t.NumIn()) //入参个数
for i := range argv {
if t.In(i).Kind() != reflect.Int {
return
}
argv[i] = reflect.ValueOf(i) //填充参数
}
result := v.Call(argv) //调用方法
if len(result) != 1 || result[0].Kind() != reflect.Int {
return
}
fmt.Println(result[0].Int()) // #=> 1
获取匿名字段
func NiM(boy Boy) {
t := reflect.TypeOf(boy)
v := reflect.ValueOf(boy)
fmt.Println(t, v)
// Anonymous:匿名
for i := 0; i < t.NumField(); i++ {
fmt.Println(t.Field(i))
// 值信息
fmt.Println(v.Field(i))
}
fmt.Println(t.FieldByName("private"))
}
设置字段值
func SetValue(o interface{
}) {
v := reflect.ValueOf(o)
newUser := User{
Id: 19,
Name: "raja",
Age: 19,
}
// 设置值
v.Elem().Set(reflect.ValueOf(newUser))
fmt.Println(v.Elem())
// 获取指针指向的元素
v = v.Elem()
// 取字段
f := v.FieldByName("Name")
if f.Kind() == reflect.String {
f.SetString("Name")
}
}
调用方法
func CallFunc(o interface{
}) {
v := reflect.ValueOf(o)
// 获取方法 导出
// fmt.Println(v.MethodByName("hello"))
m := v.MethodByName("Hello")
t := m.Type()
// 构建参数
args := make([]reflect.Value, t.NumIn())
for i := 0; i < len(args); i++ {
if t.In(i).Kind() != reflect.String {
fmt.Println("Kind Err!")
return
}
args[i] = reflect.ValueOf(strconv.Itoa(i))
}
m.Call(args