Golang Go语言中如何使用 unsafe 操作结构体私有属性
Golang Go语言中如何使用 unsafe 操作结构体私有属性
开篇之前,咱们先考虑一个问题,golang 中如何访问其他包的一个公有结构的私有属性,如下:
user 包
package user
type Info struct {
name string
age int
}
func NewUser(name string, age int) Info {
return Info{
name: name,
age: age,
}
}
main 包
package main
import (
“grpcTest/grpcCodeRead/littlecases/unsafe/user” # 倒入 user 包
)
func main() {
u := user.NewUser(“wei.wei”, 18)
u.name = "wweeii"
u.age = 18
}
如上,我们在 main 包中调用了 user 包的公有函数 NewUser,创建了对象 u,想在 main 中通过 u.name = "wweeii"
和 u.age = 18
来修改对象 u 的 name 和 age 属性,是做不到了,运行 go run main.go 编译是会报错的 :
# command-line-arguments
./main.go:10:3: u.name undefined (cannot refer to unexported field or method name)
./main.go:11:3: u.age undefined (cannot refer to unexported field or method age)
我们能想到的一个可行的方法如下: user package
package user
type Info struct {
name string
age int
}
func NewUser(name string, age int) Info {
return Info{
name: name,
age: age,
}
}
func (i *Info) NameSetter(name string) {
i.name = name
}
func (i *Info) NameGetter()string {
return i.name
}
func (i *Info) AgeSetter(age int) {
i.age = age
}
func (i *Info) AgeGetter() int {
return i.age
}
main package
package main
import (
“fmt”
“grpcTest/grpcCodeRead/littlecases/unsafe/user”
)
func main() {
u := user.NewUser(“wei.wei”, 18)
//u.name = "wweeii"
//u.age = 18
u.NameSetter("wweeii")
u.AgeSetter(20)
fmt.Println(u)
}
在 user 包中添加公有的 getter 和 setter 方法,来访问私有的属性。
但是如果 user 包没有提供访问私有变量的方法呢?我们怎么才能读取到对象 u 的 name 和 age 属性,这里就可以用到 golang 中提供的 unsafe 包。
如下:user 包不变:
package user
type Info struct {
name string
age int
}
func NewUser(name string, age int) Info {
return Info{
name: name,
age: age,
}
}
main 包改成:
package main
import (
“fmt”
“grpcTest/grpcCodeRead/littlecases/unsafe/user”
“unsafe”
)
func main() {
u := user.NewUser(“wei.wei”, 18)
pName := (*string)(unsafe.Pointer(&u))
fmt.Println(*pName)
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Sizeof(string(""))))
fmt.Println(*pAge)
}
测试运行 go run main.go
就可以访问对象 u 的私有属性 name 和 age 了。
当然看到这里,大家估计还是一头雾水,没关系,不用明白上面代码是怎么做到的,那是因为咱们还不知道 unsafe 是什么,更不知道上面用到的 unsafe.Pointer 、unsafe.Sizeof 、uintptr 是什么,先往后看,等了解了 unsafe 后再来看这段代码,咱们就能明白了。
unsafe
官方文档: https://golang.org/pkg/unsafe
unsafe 是 golang 提供的一个包,通过这个包可以实现不同类型指针之间的转化,可以实现对指针的计算,来访问变量的属性。
unsafe 包是一种不安全的包,它能绕过编译器检查,直接快速的访问和修改一些变量,从它的命名也能看出设计者是希望谨慎使用它的,至少这个包名导致咱们在使用它的时候,会让人产生不舒服的感觉。
unsafe 提供了两个类型和三个函数:
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
ArbitraryType 是一个 int 类型的重定义,从字面看是任意类型,golang 中任意类型都可以赋值给 ArbitraryType,Sizeof 、Offsetof 、Alignof 三个方法的形参是 (x ArbitraryType),也就是这三个函数可以接受任意的一个类型,并返回一个 uintptr 类型的值。
Pointer 是一个 *ArbitraryType 的重定义,unsafe.Pointer(*x) 可以将 *x 指针转为 unsafe.Pointer 类型。
uintptr 是内置的类型,可以理解为可以参与计算的指针地址。
- unsafe.Sizeof Sizeof 可以接受任意类型,返回该类型在当前操作系统上占用的字节数,这个函数的返回值和系统是相关的,比如一个 int 型在 32 位操作系统上返回 4,在 64 位操作系统上返回 8,在我的 64 位电脑上返回如下:
fmt.Println(unsafe.Sizeof(string(""))) // 返回:16
fmt.Println(unsafe.Sizeof(int(0))) // 返回:8
fmt.Println(unsafe.Sizeof(user.Info{})) // 返回 24
看完上面的例子大家想想 unsafe.Sizeof(string("Hi")) 返回值是多少?没错这里返回的是 16,因为 string 这种类型在 64 位操作系统上站 16 个字节,和参数中是几个字符没有关系。
- unsafe.Alignof 和 unsafe.Offsetof
看如下例子:
package main
import (
“fmt”
“unsafe”
)
type XTest struct {
a bool
b int16
c []int
}
func main() {
x := XTest{}
fmt.Println(unsafe.Alignof(x.a))
fmt.Println(unsafe.Alignof(x.b))
fmt.Println(unsafe.Alignof(x.c))
fmt.Println("–")
fmt.Println(unsafe.Offsetof(x.a))
fmt.Println(unsafe.Offsetof(x.b))
fmt.Println(unsafe.Offsetof(x.c))
}
执行 go run main.go 后输入如下:
1
2
8
--
0
2
8
unsafe.Alignof 返回的是类型的对齐方式,unsafe.Offsetof 返回的是属性相对于结构体开头的偏移量。
看了上面的简介,相信大家一定还是思绪万千,甚至还有些小小的思维混乱,这里我们总结下 Pointer 和 uintptr 的使用,相信掌握了如下的规律,稍加琢磨就能知道掌握 unsafe 包怎么使用:
- 任何的指针类型 T 都可以转为一个 Pointer 类型 ,转化的方式是 unsafe.Pointer(T)
- 任何一个 Pointer 类型都可以转为 uintptr 类型
- 任何一个 uintptr 类型都可以转为一个 Pointer 类型,Pointer 类型可以转为指针类型 T
- uintptr 可以参与计算,Pointer 类型不能参与计算
看完上面的 4 个点,大家使用 unsafe 进行指针计算,脑子里一定有了如下的计算路线:
指针 T -> unsafe.Pointer -> uintptr -> 做加减计算 -> unsafe.Pointer -> T
有 C 语言基础的同学一定有通过指针来遍历数组的经历,这里的 uintptr 就是可以看作是一个和 c 中指针相同的东西,是可以计算的指针,现在我们再解释下上面的代码:
u := user.NewUser("wei.wei", 18)
pName := (*string)(unsafe.Pointer(&u))
fmt.Println(*pName)
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Sizeof(string(""))))
fmt.Println(*pAge)
这里的 (*string)(unsafe.Pointer(&u))
应该是可以理解的,就是拿到了指向对象 u 地址头部的一个指针,这个指针指向的正好是第一个 string 类型的变量,所以 *pName 就是 "wei.wei"。
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Sizeof(string(""))))
这一句中 unsafe.Pointer(&u)
指向的对象 u 的头部,u 的第一元素是 string 类型,第二个元素是一个 int 类型,在 u 的指针头部,加一个 string 类型的 unsafe.Sizeof,也就是加上代码中的 unsafe.Sizeof(string(""))
就正好是一个指向到第二个元素 age 的头部的指针,因为 unsafe.Pointer(&u) 返回的是一个不可计算的类型,所以使用 uintptr 先转为一个可计算的 uintptr 类型,而 unsafe.Sizeof(string(""))
返回的是一个可以计算的 uintptr 类型,这两者相加就得到了指向 age 元素的指针 pAge 所以 *pAge 就是 18
更多关于Golang Go语言中如何使用 unsafe 操作结构体私有属性的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html