使用 golang 的 unsafe 操作结构体私有属性 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
wewin

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

  •  
  •   wewin 2021 年 5 月 15 日 4064 次点击
    这是一个创建于 1806 天前的主题,其中的信息可能已经有所发展或是发生改变。

    开篇之前,咱们先考虑一个问题,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 包怎么使用:

    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

    17 条回复    2021-05-17 00:04:09 +08:00
    wewin
        1
    wewin  
    OP
       2021 年 5 月 15 日
    欢迎大家交流和指正
    leoleoasd
        2
    leoleoasd  
       2021 年 5 月 15 日
    反射不香吗,为啥非得用 unsafe 。。
    leoleoasd
        3
    leoleoasd  
       2021 年 5 月 15 日
    印象里,涉及到 uintptr 的指针运算都是不安全的(因为好像 gc 会改变量的地址但是不会改 uintptr 的值?)
    https://stackoverflow.com/a/43918797/8031146
    这种方法是安全的。
    leoleoasd
        4
    leoleoasd  
       2021 年 5 月 15 日
    查了一下 go 文档:
    It is valid both to add and to subtract offsets from a pointer in this way.
    文中操作安全。

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

    不过很少这样做,
    比较常用的是和 c 语言交互时将 c 语言内存转换为 go 切片。
    wewin
        15
    wewin  
    OP
       2021 年 5 月 16 日
    xfriday
        16
    xfriday  
       2021 年 5 月 16 日
    field 偏移不会被优化器优化吗?
    lance6716
        17
    lance6716  
       2021 年 5 月 17 日 via Android
    go 真不愧是新时代的 C…连 void*都有
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3109 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 70ms UTC 06:09 PVG 14:09 LAX 23:09 JFK 02:09
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86