Golang Go语言中如何使用 unsafe 操作结构体私有属性

发布于 1周前 作者 h691938207 来自 Go语言

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

  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


更多关于Golang Go语言中如何使用 unsafe 操作结构体私有属性的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

回到顶部