【Go语言从入门到实战】反射编程、Unsafe篇
反射编程
reflect.TypeOf vs reflect.ValueOf
func TestTypeAndValue(t *testing.T) {
var a int64 = 10
t.Log(reflect.TypeOf(a), reflect.ValueOf(a))
t.Log(reflect.ValueOf(a).Type())
}
判断类型 - Kind()
当我们需要对反射回来的类型做判断时,Go 语言内置了一个枚举,可以通过 Kind()
来返回这个枚举值:
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
// ...
)
package reflect
import (
"fmt"
"reflect"
"testing"
)
// 检查反射类型
// 用空接口接收任意类型
func CheckType(v interface{}) {
t := reflect.TypeOf(v)
switch t.Kind() {
case reflect.Int, reflect.Int32, reflect.Int64:
fmt.Println("Int")
case reflect.Float32, reflect.Float64:
fmt.Println("Float")
default:
fmt.Println("unknown type")
}
}
func TestBasicType(t *testing.T) {
var f float32 = 1.23
CheckType(f)
}
利用反射编写灵活的代码
reflect.TypeOf()
和 reflect.ValueOf()
都有 FieldByName()
方法。
// s必须是一个 struct 类型
// reflect.ValueOf()只会返回一个值
reflect.ValueOf(s).FieldByName("Name")
// reflect.TypeOf()可以返回两个值,第二个值可以用来判断这个值有没有;
reflect.TypeOf(s).FieldByName("Name")
FieldByName()
方法返回的是一个 StructField
类型的值。
我们可以通过这个 StructField
来访问 Struct Tag
:
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
FieldByName()
方法调用者必须是一个 struct
,而不是指针,源码如下:
// 访问 MethodByName() 必须是指针类型
reflect.ValueOf(&s).MethodByName("method_name").Call([]reflect.Value{reflect.ValueOf("new_value")})
type Employee struct {
EmployeeID string
// 注意后面的 struct tag 的写法,详情见第5点讲解
Name string `format:"normal"`
Age int
}
// 更新名字,注意这里的 e 是指针类型
func (e *Employee) UpdateName(newVal string) {
e.Name = newVal
}
// 通过反射调用结构体的方法
func TestInvokeByName(t *testing.T) {
e := Employee{"1", "Jane", 18}
// reflect.TypeOf()可以返回两个值,第二个值可以用来判断这个值有没有;
// 而reflect.ValueOf()只会返回一个值
t.Logf("Name: value(%[1]v), Type(%[1]T)", reflect.ValueOf(e).FieldByName("Name"))
if nameField, ok := reflect.TypeOf(e).FieldByName("Name"); !ok {
t.Error("Failed to get 'Name' field")
} else {
// 获取反射取到的字段的 tag 的值
t.Log("Tag:Format", nameField.Tag.Get("format"))
}
// 访问 MethodByName() 必须是指针类型
reflect.ValueOf(&e).MethodByName("UpdateName").
Call([]reflect.Value{reflect.ValueOf("Mike")})
t.Log("After update name: ", e)
}
Elem()
因为 FieldByName()
必须要结构体才能调用,如果参数是一个指向结构体的指针,我们需要用到 Elem()
方法,它会帮你获得指针指向的结构。
Elem()
用来获取指针指向的值- 如果参数不是指针,会报 panic 错误
- 如果参数值是 nil,获取的值为 0
// reflect.ValueOf(demoPtr)).Elem() 返回的是字段的值
reflect.ValueOf(demoPtr).Elem()
// reflect.ValueOf(st)).Elem().Type() 返回的是字段类型
reflect.ValueOf(demoPtr).Elem().Type()
// 传递指针类型参数调用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().FieldByName("Name")
// 传递指针类型参数调用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().Type().FieldByName("Name")
Struct Tag
结构体里面可以对某些字段做特殊的标记,它是一个 `key: “value”` 的格式。
type Demo struct {
// 先用这个符号(``)包起来,然后写上 key: value 的格式
Name string `format:"normal"`
}
Go 内置的 Json 解析会用到 tag 来做一些标记。
反射是把双刃剑
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个:
- 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发 panic,那很可能是在代码写完的很长时间之后。
- 大量使用反射的代码通常难以理解。
- 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
万能程序
DeepEqual
我们都知道两个 map
类型之间是不能互相比较的,两个 slice 类型之间也不能进行比较,但是反射包中的 DeepEqual()
可以帮我们实现这个功能。
用 DeepEqual() 比较 map
// 用 DeepEqual() 比较两个 map 类型
func TestMapComparing(t *testing.T) {
m1 := map[int]string{1: "one", 2: "two", 3: "three"}
m2 := map[int]string{1: "one", 2: "two", 3: "three"}
if reflect.DeepEqual(m1, m2) {
t.Log("yes")
} else {
t.Log("no")
}
}
用 DeepEqual() 比较 slice
// 用 DeepEqual() 比较两个切片类型
func TestSliceComparing(t *testing.T) {
s1 := []int{1, 2, 3, 4}
s2 := []int{1, 2, 3, 5}
if reflect.DeepEqual(s1, s2) {
t.Log("yes")
} else {
t.Log("no")
}
}
用反射实现万能程序
场景:我们有 Employee
和 Customer
两个结构体,二者有两个相同的字段(Name 和 Age),我们希望写一个通用的程序,可以同时填充这两个不同的结构体。
type Employee struct {
EmployeeId int
Name string
Age int
}
type Customer struct {
CustomerId int
Name string
Age int
}
// 用同一个数据填充不同的结构体
// 思路:既然是不同的结构体,那么要想通用,所以参数必须是一个空接口才行。
// 因为是空接口,所有我们需要对参数类型写断言
func fillDifferentStructByData(st interface{}, data map[string]interface{}) error {
// 先判断传过来的类型是不是指针
if reflect.TypeOf(st).Kind() != reflect.Ptr {
return errors.New("第一个参数必须传一个指向结构体的指针")
}
// 再判断指针指向的类型是否为结构体
// Elem() 用来获取指针指向的值
// 如果参数不是指针,会报 panic 错误
// 如果参数值是 nil, 获取的值为 0
if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {
return errors.New("第一个参数必须是一个结构体类型")
}
if data == nil {
return errors.New("填充用的数据不能为nil")
}
var (
field reflect.StructField
ok bool
)
for key, val := range data {
// 如果结构体里面没有 key 这个字段,则跳过
// reflect.ValueOf(st)).Elem().Type() 返回的是字段类型
// reflect.ValueOf(st)).Elem().Type() 等价于 reflect.TypeOf(st)).Elem()
if field, ok = reflect.TypeOf(st).Elem().FieldByName(key); !ok {
continue
}
// 如果字段的类型相同,则用 data 的数据填充这个字段的值
if field.Type == reflect.TypeOf(val) {
// reflect.ValueOf(st)).Elem() 返回的是字段的值
reflect.ValueOf(st).Elem().FieldByName(key).Set(reflect.ValueOf(val))
}
}
return nil
}
// 填充姓名和年龄
func TestFillNameAndAge(t *testing.T) {
// 声明一个 map,用来存放数据,这些数据将会填充到 Employee 和 Customer 这两个结构体中
data := map[string]interface{}{"Name": "Jane", "Age": 18}
e := Employee{}
// 传给通用的填充方法
if err := fillDifferentStructByData(&e, data); err != nil {
t.Fatal(err)
}
c := Customer{}
// 传给通用的填充方法
if err := fillDifferentStructByData(&c, data); err != nil {
t.Fatal(err)
}
t.Log(e)
t.Log(c)
}
两个结构体的 name 和 age 都填充上了,符合预期。
不安全编程-UnSafe
不安全编程指的是 go 语言中有一个 package 叫:unsafe
,它的使用场景一般是要和外部 c 程序实现的一些高效的库来进行交互。
“不安全行为”的危险性
Go 语言中是不支持强制类型转换的,而我们一旦使用 unsafe.Pointer
拿到指针后,我们可以将它转换为任意类型的指针,这样我们是否能利用它来实现强制类型转换呢?我们可以用代码来测试一下:
func TestUnsafe(t *testing.T) {
i := 10
f := *(*float64)(unsafe.Pointer(&i))
t.Log(unsafe.Pointer(&i))
t.Log(f)
}
可以看到结果根本不是 10
,是一串数字字母的组合,所以这是非常危险的。
合理的类型转换
在 Go 语言中,不同类型的指针是不允许相互赋值的,但是通过合理地使用 unsafe
包,则可以打破这种限制。
例如:int 类型是可以进行转换赋值的。
func TestConvert1(t *testing.T) {
var num int = 10
var uintNum uint = *(*uint)(unsafe.Pointer(&num))
var int32Num int32 = *(*int32)(unsafe.Pointer(&num))
t.Log(num, uintNum, int32Num)
t.Log(reflect.TypeOf(num), reflect.TypeOf(uintNum), reflect.TypeOf(int32Num))
}
访问修改结构体私有成员变量
type User struct {
name string
id int
}
func TestOperateStruct(t *testing.T) {
user := new(User)
user.name = "张三"
fmt.Printf("%+v\n", user)
// 突破第一个私有变量,因为是结构体的第一个字段,所以不需要额外的指针计算
*(*string)(unsafe.Pointer(user)) = "李四"
fmt.Printf("%+v\n", user)
// 突破第二个私有变量,因为是第二个成员字段,需要偏移一个字符串占用的长度即 16 个字节
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(user)) + uintptr(16))) = 1
fmt.Printf("%+v\n", user)
}
当然我们可以更简单的获取到结构体变量的偏移量,这样就不需要自己计算了:
type Person struct {
Name string
Age int
Height float64
}
func TestUnSafeOffSet(t *testing.T) {
nameOffset := unsafe.Offsetof(Person{}.Name)
ageOffset := unsafe.Offsetof(Person{}.Age)
heightOffset := unsafe.Offsetof(Person{}.Height)
t.Log(nameOffset, ageOffset, heightOffset) // 输出字段的偏移量
}
实现 []byte 和字符串的零拷贝转换
通过查看源码,可以发现 slice
切片类型和 string
字符串类型具有类似的结构。
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 底层数组指针,真正存放数据的地方
len int // 切片长度,通过 len(slice) 返回
cap int // 切片容量,通过 cap(slice) 返回
}
// runtime/string.go
type stringStruct struct {
str unsafe.Pointer // 底层数组指针
len int // 字符串长度,可以通过 len(string) 返回
}
看到这里,你是不是发现很神奇,这两个数据结构底层实现基本相同,而 slice 只是多了一个cap 字段。可以得出结论:slice 和 string 在内存布局上是对齐的,我们可以直接通过 unsafe 包进行转换,而不需要申请额外的内存空间。
代码实现
func StringToBytes(str string) []byte {
var b []byte
// 切片的底层数组、len字段,指向字符串的底层数组,len字段
*(*string)(unsafe.Pointer(&b)) = str
// 切片的 cap 字段赋值为 len(str) 的长度,切片的指针、len 字段各占8个字节,直接偏移16个字节
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*uintptr(8))) = len(str)
return b
}
func BytesToString(data []byte) string {
// 直接转换
return *(*string)(unsafe.Pointer(&data))
}
func TestStringAndBytesConvert(t *testing.T) {
str := "hello"
b := StringToBytes(str)
t.Log(reflect.TypeOf(b), b)
// 此时 b 已经是切片类型,我们再将它转换为string类型
s := BytesToString(b)
t.Log(reflect.TypeOf(s), s)
}
符合预期。
原子类型操作
我们会用到 golang 内置 package 中的 atomic
原子操作,它提供了指针的原子操作,通常用在并发读写一块共享缓存时,保证线程安全。
我们在写数据的时候写在另外一块空间,完全写完之后,我们使用原子操作把读的指针和写的指针指向我们新写入的空间,保证下次再读的时候就是新写好的内容了。指针的切换要具有线程安全
的特性。
func TestAtomic(t *testing.T) {
var shareBufPtr unsafe.Pointer
// 写方法
writeDataFn := func() {
data := []int{}
for i := 0; i < 9; i++ {
data = append(data, i)
}
// 使用原子操作将data的指针指向shareBufPtr
atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
}
// 读方法
readDataFn := func() {
data := atomic.LoadPointer(&shareBufPtr) // 使用原子操作读取shareBufPtr
fmt.Println(data, *(*[]int)(data)) // 打印shareBufPtr中的数据
}
var wg sync.WaitGroup
writeDataFn()
// 启动3个读协程,3个写协程,每个协程执行3次读/写操作
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
for i := 0; i < 3; i++ {
writeDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
wg.Add(1)
go func() {
for i := 0; i < 3; i++ {
readDataFn()
time.Sleep(time.Microsecond * 100)
}
wg.Done()
}()
}
wg.Wait()
}
使用 atomic + unsafe
来实现共享 buffer 安全的读写。
总结
通过 unsafe 包,我们可以绕过 golang 编译器的检查,直接操作地址,实现一些高效的操作。但正如 golang 官方给它的命名一样,它是不安全的,滥用的话可能会导致程序意外的崩溃。关于 unsafe 包,我们应该更关注于它的用法,生产环境不建议使用!!!
- 笔记整理自极客时间视频教程:Go语言从入门到实战
- UnSafe部分内容参考:go unsafe包使用指南