开篇之前,咱们先考虑一个问题,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 后再来看这段代码,咱们就能明白了。
官方文档: 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 是内置的类型,可以理解为可以参与计算的指针地址。
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 个字节,和参数中是几个字符没有关系。
看如下例子:
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 包怎么使用:
看完上面的 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
1
wewin OP 欢迎大家交流和指正
|
2
leoleoasd 2021-05-15 13:28:40 +08:00
反射不香吗,为啥非得用 unsafe 。。
|
3
leoleoasd 2021-05-15 13:31:14 +08:00
印象里,涉及到 uintptr 的指针运算都是不安全的(因为好像 gc 会改变量的地址但是不会改 uintptr 的值?)
https://stackoverflow.com/a/43918797/8031146 这种方法是安全的。 |
4
leoleoasd 2021-05-15 13:35:19 +08:00
查了一下 go 文档:
It is valid both to add and to subtract offsets from a pointer in this way. 文中操作安全。 但是还是感觉用反射的方式更合理一点 |
6
march1993 2021-05-15 14:11:21 +08:00
满满的 java 味
人家设计就是不暴露出来你为什么非要去访问呢。。 |
7
janxin 2021-05-15 16:35:21 +08:00 via iPhone 1
实际上除了极限优化性能外实在想不到为什么用 unsafe…甚至你都可以改依赖的代码…
|
8
402124773 2021-05-15 17:42:51 +08:00
那为什么开始的时候,你要把这个属性设置为私有,然后去别的地方访问?
|
9
wewin OP 这里之前看一个那个框架的源码发现其中用了 unsafe, 然后就顺手了解了下 unsafe 的作用;实际上我们做开发的时候,确实很少有需要访问私有属性的场景
|
10
leoleoasd 2021-05-15 18:23:58 +08:00
|
11
labulaka521 2021-05-15 20:03:15 +08:00
@402124773 调用别人的库,然后要获取某个私有属性,可能就会用这种方法或者 reflect 来获取
|
13
wewin OP @labulaka521 当时看到的那个代码也是这种场景下用的
|
14
gamexg 2021-05-15 22:22:09 +08:00
我是创建一个一样的结构,但是私有属性改为公开。
然后 unsafe.Pointer 将对方的结构强制转换为自己的结构,就可以直接访问了。 不过很少这样做, 比较常用的是和 c 语言交互时将 c 语言内存转换为 go 切片。 |
16
xfriday 2021-05-16 14:42:04 +08:00
field 偏移不会被优化器优化吗?
|
17
lance6716 2021-05-17 00:04:09 +08:00 via Android
go 真不愧是新时代的 C…连 void*都有
|