go中指针unsafe.Pointer和uintptr

CKeenGolanggo语言基础约 1919 字大约 6 分钟

作者:程序员CKeen
博客:http://ckeen.cnopen in new window

长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄


如果你看go的源码,尤其是runtime的部分的源码,你一定经常会发现unsafe.Pointer和uintptr这两个函数,例如下面就是runtime里面的map源码实现里面的一个函数:

func (b *bmap) overflow(t *maptype) *bmap {
	return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))
}

那么这两个方法有什么用呢?下面我们来重点介绍一下。

Go中的指针及与指针对指针的操作主要有以下三种:

  • 一普通的指针类型,例如 var intptr *T,定义一个T类型指针变量。

  • 二内置类型uintptr,本质是一个无符号的整型,它的长度是跟平台相关的,它的长度可以用来保存一个指针地址。

  • 三是unsafe包提供的Pointer,表示可以指向任意类型的指针。

1.普通的指针类型

count := 1
Counter(&count)
fmt.Println(count)

func Counter(count *int) {
	*count++
}

普通指针可以通过引用来修改变量的值,这个跟C语言指针有点像。

2.uintptr类型

uintptr用来进行指针计算,因为它是整型,所以很容易计算出下一个指针所指向的位置。uintptr在runtime的builtin包中定义,定义如下:

// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
// uintptr是一个能足够容纳指针位数大小的整数类型
type uintptr uintptr

uintptr是一个能足够容纳指针位数大小的整数类型,意思他这个整数能表示内存所有地址的大小。虽然uintpr保存了一个指针地址,但它只是一个值,不引用任何对象。因此使用的时候要注意以下情况:

  • 如果uintptr地址相关联对象移动,则其值也不会更新,因为他只是一个地址的值。例如goroutine的堆栈信息发生变化
  • uintptr地址关联的对象可以被垃圾回收,因为uintpr只是一个地址值,没有持有对象的引用。GC不认为uintptr是活引用,因此unitptr地址指向的对象可以被垃圾收集。

因为uinptr是一个整数类型, 所有我们可以使用证书类型来初始化它

// 构建方式1: 直接使用整型数据构造
fmt.Printf("p0:%+v\n",uintptr(0))
// 构造数值为0的ptr, 打印结果:p0:0

// 构建方式2: 通过go提供的方法
str1 := "hello"
p1 := uintptr(unsafe.Pointer(&str1))
fmt.Printf("p1:%+v\n", p1)
// 通过unsafe.Pointer()去构造,打印结果:p1:824634686704

我们还可以uinptr进行运算:

n := 10

b := make([]int, n)
for i:= 0;i< n;i++ {
	b[i] = i
}
fmt.Println(b)
//打印结果: [0 1 2 3 4 5 6 7 8 9]

// 取slice的最后的一个元素
end := unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + 9 * unsafe.Sizeof(b[0]))
// 等价于unsafe.Pointer(&b[9])
fmt.Println(*(*int)(end))
//打印结果: 9

这里我们通过切片的第一个地址值,对uintptr进行操作后,取到最后一个切片的值。

3. unsafe.Pointer类型

unsafe.Pointer是特别定义的一种指针类型, 可以指向任意类型的int指针。

Pointer在unsafe包中定义,定义如下:

package unsafe

// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
// ArbitraryType在这里不是unsafe包的实际的一部分,仅仅是为了文档记录
type ArbitraryType int

// Pointer类型指向是类型的指针
type Pointer *ArbitraryType

// 返回指针所指向类型在内存中的占用大小
func Sizeof(x ArbitraryType) uintptr

// 在struct结构中,某个字段的偏移量
func Offsetof(x ArbitraryType) uintptr

// 返回任意类型在内存的中对齐字节数量
func Alignof(x ArbitraryType) uintptr

官方文档unsafe.Pointer的使用有以下说明:

Pointer represents a pointer to an arbitrary type. There are four special operations
available for type Pointer that are not available for other types:    
//  Pointer代表了一个任意类型的指针。Pointer类型有四种特殊的操作是其他类型不能使用的:
   - A pointer value of any type can be converted to a Pointer.       
//  任意类型的指针可以被转换为Pointer
   - A Pointer can be converted to a pointer value of any type.       
//  Pointer可以被转换为任务类型的值的指针
   - A uintptr can be converted to a Pointer.                         
//  uintptr可以被转换为Pointer
   - A Pointer can be converted to a uintptr.                         
//  Pointer可以被转换为uintptr
Pointer therefore allows a program to defeat the type system and read and write
arbitrary memory. It should be used with extreme care.                
//  因此Pointer允许程序不按类型系统的要求来读写任意的内存,应该非常小心地使用它。

从文档上描述, unsafe.Pointer有几点需要注意的:

  • unsafe.Pointer可以是任意类型的指针,那么指向任意类型的指针都可以转换为unsafe.Pointer,同时unsafe.Pointer的指向类型可以转换为任何的类型
  • unsafe.Pointer和uintptr可以互相转换
  • 通过unsafe.Pointer让你能够操作内存地址,是不安全的,容易使你的程序出现莫名其妙的问题,所以使用的时候要特别小心

另外要知道的

  • 所以unsafe.Pointer做的主要是用来进行桥接,用于不同类型的指针进行互相转换。
  • unsafe.Pointer不能进行指针运算,可以借助uintptr来进行指针运算。
  • 在任何情况下,结果都必须继续指向原分配的对象。

注:unsafe包下设计到类型所占内存对齐的,可以查询相关资料深入了解

4.unsafe.Pointer,uintptr与普通指针的互相转换

  • unsafe.Pointer和普通指针的相互转换

    var f float64 = 1.0
    fmt.Println(Float64bits(f))
    // 4607182418800017408
    
    func Float64bits(f float64) uint64 {
    	return *((*uint64)(unsafe.Pointer(&f)))
    }
    

    借助unsafe.Pointer指针,实现float64转换为uint64类型。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。另外一个重要的要注意的是,在进行普通类型转换的时候,要注意转换的前后的类型要有相同的内存布局,下面两个结构也能完成转换,就因为他们有相同的内存布局

    type s1 struct {
    	id int
    	name string
    }
    
    type s2 struct {
    	field1 *[5]byte
    	filed2 int
    }
    
    b := s1{name:"123"}
    var j s2
    j = *(*s2)(unsafe.Pointer(&b))
    fmt.Println(j)
    
  • unsafe.Pointer和uintrptr的互相转换及配合

    uintptr类型的主要是用来与unsafe.Pointer配合使用来访问和操作unsafe的内存。unsafe.Pointer不能执行算术操作。要想对指针进行算术运算必须这样来做:

    1. 将unsafe.Pointer转换为uintptr

    2. 对uintptr执行算术运算

    3. 将uintptr转换回unsafe.Pointer,然后访问uintptr地址指向的对象

    需要小心的是,上面的步骤对于垃圾收集器来说应该是原子的,否则可能会导致问题。

    例如,在第1步之后,引用的对象可能被收集。如果在步骤3之后发生这种情况,指针将是一个无效的Go指针,并可能导致程序崩溃

    unsafe.Pointer和uintrptr和转换:

    package main
    
    import (
        "fmt"
        "unsafe"
    )
    
    type Person struct {
        age int
        name string
    }
    func main() {
        p := &Person{age: 30, name: "Bob"}
        
       //获取到struct s中b字段的地址
        p := unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.name))
        
        //将其转换为一个string的指针,并且打印该指针的对应的值
        fmt.Println(*(*string)(p))
    }