V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
wewin
V2EX  ›  Go 编程语言

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

  •  
  •   wewin · 2021-05-15 11:10:22 +08:00 · 3063 次点击
    这是一个创建于 1048 天前的主题,其中的信息可能已经有所发展或是发生改变。

    开篇之前,咱们先考虑一个问题,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-05-15 11:59:36 +08:00
    欢迎大家交流和指正
    leoleoasd
        2
    leoleoasd  
       2021-05-15 13:28:40 +08:00
    反射不香吗,为啥非得用 unsafe 。。
    leoleoasd
        3
    leoleoasd  
       2021-05-15 13:31:14 +08:00
    印象里,涉及到 uintptr 的指针运算都是不安全的(因为好像 gc 会改变量的地址但是不会改 uintptr 的值?)
    https://stackoverflow.com/a/43918797/8031146
    这种方法是安全的。
    leoleoasd
        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.
    文中操作安全。

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

    不过很少这样做,
    比较常用的是和 c 语言交互时将 c 语言内存转换为 go 切片。
    wewin
        15
    wewin  
    OP
       2021-05-16 09:10:15 +08:00
    @gamexg 👍
    xfriday
        16
    xfriday  
       2021-05-16 14:42:04 +08:00
    field 偏移不会被优化器优化吗?
    lance6716
        17
    lance6716  
       2021-05-17 00:04:09 +08:00 via Android
    go 真不愧是新时代的 C…连 void*都有
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1157 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 18:30 · PVG 02:30 · LAX 11:30 · JFK 14:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.