goland基础包——unsafe的使用

本文详细介绍了Go语言中的unsafe包,包括它的作用、定义及使用场景。unsafe包允许进行类型安全的内存操作,如转换指针类型和获取结构体的内存布局信息。文中通过示例展示了如何使用unsafe Pointer进行类型转换,以及uintptr在内存地址运算中的应用。同时,文章强调了使用unsafe可能导致的安全问题,特别是在涉及垃圾回收时,不恰当的uintptr转换可能导致程序崩溃。
摘要由CSDN通过智能技术生成

温馨提示:适合有内存操作经验的同学阅读

一、unsafe的作用

从golang的定义来看,unsafe 是类型安全的操作。顾名思义,它应该非常谨慎地使用; unsafe可能很危险,但也可能非常有用。例如,当使用系统调用和Go结构必须具有与C结构相同的内存布局时,您可能别无选择,只能使用unsafe。关于指针操作,在unsafe包官方定义里有四个描述:

  1. 任何类型的指针都可以被转化为Pointer

  2. Pointer可以被转化为任何类型的指针

  3. uintptr可以被转化为Pointer

  4. Pointer可以被转化为uintptr

额外在加上一个规则:指向不同类型数据的指针,是无法直接相互转换的,必须借助unsafe.Pointer(类似于C的 void指针)代理一下再转换也就是利用上述的1,2规则。 举例:

func Float64bits(f float64) uint64 {
  // 无法直接转换,报错:Connot convert expression of type *float64 to type *uint64
  // return *(*uint64)(&f)   
​
 // 先把*float64 转成 Pointer(描述1),再把Pointer转成*uint64(描述2)
  return *(*uint64)(unsafe.Pointer(&f)) 
}

关键词:C语言的空指针类型void*

二、unsafe的定义:

整体代码比较简单,2个类型定义和3个uintptr的返回函数

package unsafe
//ArbitraryType仅用于文档目的,实际上并不是unsafe包的一部分,它表示任意Go表达式的类型。
type ArbitraryType int
​
//任意类型的指针,类似于C的*void
type Pointer *ArbitraryType
​
//确定结构在内存中占用的确切大小
func Sizeof(x ArbitraryType) uintptr
​
//返回结构体中某个field的偏移量
func Offsetof(x ArbitraryType) uintptr
​
//返回结构体中某个field的对其值(字节对齐的原因)
func Alignof(x ArbitraryType) uintptr
下来举个例子助于理解:

package main
​
import (
    "fmt"
    "unsafe"
)
​
type Human struct {
    sex  bool   // one byte
    age  uint8  // one byte
    uAge uint8  // four byte
    min  int    // eight byte
    name string // sixteen byte
}// thirty two byte
​
func main() {
    h := Human{
        true,
        30,
        31,
        2,
        "hello",
    }
    i := unsafe.Sizeof(h)
    j := unsafe.Alignof(h.sex)
    k := unsafe.Offsetof(h.sex)
    fmt.Println(i, j, k)
    fmt.Println("size:",j,"pos:", k)
    j = unsafe.Alignof(h.age)
    k = unsafe.Offsetof(h.age)
    fmt.Println("size:",j,"pos:", k)
    j = unsafe.Alignof(h.uAge)
    k = unsafe.Offsetof(h.uAge)
    fmt.Println("size:",j,"pos:", k)
    j = unsafe.Alignof(h.min)
    k = unsafe.Offsetof(h.min)
    fmt.Println("size:",j,"pos:", k)
    j = unsafe.Alignof(h.name)
    k = unsafe.Offsetof(h.name)
    fmt.Println("size:",j,"pos:", k)
    fmt.Printf("%p\n", &h)
    var p unsafe.Pointer
    p = unsafe.Pointer(&h)
    fmt.Println(p)
}
​
// output :
//32 1 0
//size: 1 pos: 0
//size: 1 pos: 1
//size: 1 pos: 2
//size: 8 pos: 8
//size: 8 pos: 16
//0xc0000be000
//0xc0000be000
​

根据字节对其我们很容易可以得出 Human的大小为32byte

三、Pointer使用

前面已经说了,pointer是任意类型的指针,可以指向任意类型数据。参照Float64bits的转换和上述栗子的unsafe.Pointer(&h),所以主要用于转换各种类型

四、uintptr

在golang中uintptr的定义是 type uintptr uintptr uintptr是golang的内置类型,是能存储指针的整型

  1. 根据描述3,一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)

  2. 这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。

  3. 许多将unsafe.Pointer指针转为uintptr,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:

package main
​
import (
    "fmt"
    "unsafe"
)
​
func main() {
​
    var x struct {
        a bool
        b int16
        c []int
    }
​
    /**
    unsafe.Offsetof 函数的参数必须是一个字段 x.f, 
然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
    */
​
    /**
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    指针的运算
    */
    // 和 pb := &x.b 等价
    pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
    *pb = 42
    fmt.Println(x.b) // "42"
}

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(注:这是真正可以体会unsafe包为何不安全的例子)。

下面段代码是错误的:

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

产生错误的原因很微妙。

有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存改变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动是对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

打个不恰当的比方:

上帝让A认识S,A把B的名字告诉C,C也知道有B这个人了。

然后S改名为B,不叫S了, 这时上帝会告诉A这件事。但是上帝不会告诉C这件事。

所以C再去找S就找不到了,然后C就炸了

 

友情客串:上帝=GC、 A==指针 (*int16)、 C= unsafe.Pointer、S(B)=地址空间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值