使用 golang 的 unsafe 操作结构体私有属性

2021-05-15 11:10:22 +08:00
 wewin

开篇之前,咱们先考虑一个问题,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 是内置的类型,可以理解为可以参与计算的指针地址。

 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 包怎么使用:

  1. 任何的指针类型 T 都可以转为一个 Pointer 类型 ,转化的方式是 unsafe.Pointer(T)
  2. 任何一个 Pointer 类型都可以转为 uintptr 类型
  3. 任何一个 uintptr 类型都可以转为一个 Pointer 类型,Pointer 类型可以转为指针类型 T
  4. 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

3462 次点击
所在节点    Go 编程语言
17 条回复
wewin
2021-05-15 11:59:36 +08:00
欢迎大家交流和指正
leoleoasd
2021-05-15 13:28:40 +08:00
反射不香吗,为啥非得用 unsafe 。。
leoleoasd
2021-05-15 13:31:14 +08:00
印象里,涉及到 uintptr 的指针运算都是不安全的(因为好像 gc 会改变量的地址但是不会改 uintptr 的值?)
https://stackoverflow.com/a/43918797/8031146
这种方法是安全的。
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.
文中操作安全。

但是还是感觉用反射的方式更合理一点
codehz
2021-05-15 13:49:33 +08:00
@leoleoasd 新版本反射不允许修改私有字段了
march1993
2021-05-15 14:11:21 +08:00
满满的 java 味
人家设计就是不暴露出来你为什么非要去访问呢。。
janxin
2021-05-15 16:35:21 +08:00
实际上除了极限优化性能外实在想不到为什么用 unsafe…甚至你都可以改依赖的代码…
402124773
2021-05-15 17:42:51 +08:00
那为什么开始的时候,你要把这个属性设置为私有,然后去别的地方访问?
wewin
2021-05-15 18:09:41 +08:00
这里之前看一个那个框架的源码发现其中用了 unsafe, 然后就顺手了解了下 unsafe 的作用;实际上我们做开发的时候,确实很少有需要访问私有属性的场景
leoleoasd
2021-05-15 18:23:58 +08:00
@codehz #5 https://stackoverflow.com/a/43918797/8031146
这种方法测试过 1.14 里可用。
labulaka521
2021-05-15 20:03:15 +08:00
@402124773 调用别人的库,然后要获取某个私有属性,可能就会用这种方法或者 reflect 来获取
wewin
2021-05-15 20:43:21 +08:00
@leoleoasd 👍
wewin
2021-05-15 20:43:55 +08:00
@labulaka521 当时看到的那个代码也是这种场景下用的
gamexg
2021-05-15 22:22:09 +08:00
我是创建一个一样的结构,但是私有属性改为公开。
然后 unsafe.Pointer 将对方的结构强制转换为自己的结构,就可以直接访问了。

不过很少这样做,
比较常用的是和 c 语言交互时将 c 语言内存转换为 go 切片。
wewin
2021-05-16 09:10:15 +08:00
@gamexg 👍
xfriday
2021-05-16 14:42:04 +08:00
field 偏移不会被优化器优化吗?
lance6716
2021-05-17 00:04:09 +08:00
go 真不愧是新时代的 C…连 void*都有

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/777059

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX