Golang 中的 unsafe.Pointer 和 uintptr详解

Golang 中的 unsafe.Pointer 和 uintptr详解

目录

前言

uintptr

unsafe.Pointer

使用姿势

常规类型互转

Pointer => uintptr

指针算数计算:Pointer => uintptr => Pointer

reflect 包中从 uintptr => Ptr

实战案例

string vs []byte

sync.Pool

前言

日常开发中经常看到大佬们用各种 unsafe.Pointer, uintptr 搞各种花活,作为小白一看到 unsafe 就发憷,不了解二者的区别和场景,自然心里没数。今天我们就来学习下这部分知识。

uintptr

uintptr 的定义在 builtin 包下,定义如下:

// uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr

参照注释我们知道:

uintptr 是一个整数类型(这个非常重要),注意,他不是个指针;

但足够保存任何一种指针类型。

unsafe 包支持了这些方法来完成【类型】=> uintptr 的转换:

func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr

你可以将任意类型变量转入,获取对应语义的 uintptr,用来后续计算内存地址(比如基于一个结构体字段地址,获取下一个字段地址等)。

unsafe.Pointer

我们来看一下什么是 unsafe 包下的 Pointer:

// 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. type ArbitraryType int // Pointer represents a pointer to an arbitrary type. There are four special operations // available for type Pointer that are not available for other types: //- A pointer value of any type can be converted to a Pointer. //- A Pointer can be converted to a pointer value of any type. //- A uintptr can be converted to a Pointer. //- A Pointer can be converted to a uintptr. // Pointer therefore allows a program to defeat the type system and read and write // arbitrary memory. It should be used with extreme care. type Pointer *ArbitraryType

这里的 ArbitraryType 仅仅是为了便于开发者理解。语义上来讲你可以把 Pointer 理解为一个可以指向任何一种类型的【指针】。

这一点很关键。我们此前遇到的场景一般都是,先定义一个类型,然后就有了这个类型对应的指针。而 unsafe.Pointer 则是一个通用的解法,不管你是什么类型都可以。突破了这层限制,我们就可以在运行时具备更多能力,也方便适配一些通用场景。

官方提供了四种 Pointer 支持的场景:

任意类型的指针可以转换为一个 Pointer;

一个 Pointer 也可以被转为任意类型的指针;

uintptr 可以被转换为 Pointer;

Pointer 也可以被转换为 uintptr。

这样强大的能力使我们能够绕开【类型系统】,丢失了编译期的校验,所以使用时一定要小心。

使用姿势 常规类型互转 func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

我们取 f 的指针,将其转为 unsafe.Pointer,再转为一个 uint64 的指针,最后解出来值。

其实本质就是把 unsafe.Pointer 当成了一个媒介。用到了他可以从任意一个类型转换得来,也可以转为任意一个类型。

这样的用法有一定的前提:

转化的目标类型(uint64) 的 size 一定不能比原类型 (float64)还大(二者size都是8个字节);

前后两种类型有等价的 memory layout;

比如,int8 转为 int64 是不支持的,我们测试一下:

package main import ( "fmt" "unsafe" ) func main() { fmt.Println("int8 => int64", Int8To64(5)) fmt.Println("int64 => int8", Int64To8(5)) } func Int64To8(f int64) int8 { return *(*int8)(unsafe.Pointer(&f)) } func Int8To64(f int8) int64 { return *(*int64)(unsafe.Pointer(&f)) }

运行后你会发现,int64 => int8 转换正常,从小到大则会出问题:

int8 => int64 1079252997 int64 => int8 5 Program exited. Pointer => uintptr

从 Pointer 转 uintptr 本质产出的是这个 Pointer 指向的值的内存地址,一个整型。

这里还是要在强调一下:

uintptr 指的是具体的内存地址,不是个指针,没有指针的语义,你可以将 uintptr 打印出来比对地址是否相同。

即便某个对象因为 GC 等原因被回收,uintptr的值也不会连带着变动。

uintptr地址关联的对象可以被垃圾回收。GC不认为uintptr是活引用,因此unitptr地址指向的对象可以被垃圾收集。

指针算数计算:Pointer => uintptr => Pointer

将一个指针转为 uintptr 将会得到它指向的内存地址,而我们又可以结合 SizeOf,AlignOf,Offsetof 来计算出来另一个 uintptr 进行计算。

这类场景最常见的是【获取结构体中的变量】或【数组中的元素】。

比如:

f := unsafe.Pointer(&s.f) f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f)) e := unsafe.Pointer(&x[i]) e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

上面这两组运算本质是相同的,一种是直接拿地址,一种是通过计算 size,offset 来实现。

注意:变量到 uintptr 的转换以及计算必须在一个表达式中完成(需要保证原子性):

错误的案例:

u := uintptr(p) p = unsafe.Pointer(u + offset)

uintptr 到 Pointer 的转换一定要在一个表达式,不能用 uintptr 存起来,下个表达式再转。

uintptr + offset 算地址,再跟 Pointer 转化其实是一个很强大的能力,我们再来看一个实际的例子:

package main import ( "fmt" "unsafe" ) func main() { length := 6 arr := make([]int, length) for i := 0; i < length; i++ { arr[i] = i } fmt.Println(arr) // [0 1 2 3 4 5] // 取slice的第5个元素:通过计算第1个元素 + 4 个元素的size 得出 end := unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 4*unsafe.Sizeof(arr[0])) fmt.Println(*(*int)(end)) // 4 fmt.Println(arr[4]) // 4 }

unsafe.Pointer 不能进行算数计算,uintptr 其实是很好的一个补充。

reflect 包中从 uintptr => Ptr

我们知道,reflect 的 Value 提供了两个方法 Pointer 和 UnsafeAddr 返回 uintptr。这里不使用 unsafe.Pointer 的用意在于避免用户不 import unsafe 包就能将结果转成任意类型,但这也带来了问题。

上面有提到,千万不能先保存一个 uintptr,再转 unsafe.Pointer,这样的结果是很不可靠的。所以我们必须在调用完 Pointer/UnsafeAddr 之后就立刻转 unsafe.Pointer。

正例:

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

反例:

u := reflect.ValueOf(new(int)).Pointer() p := (*int)(unsafe.Pointer(u)) 实战案例 string vs []byte

活学活用,其实参照上面转换的第一个案例就可以实现,不需要 uintptr。还是一样的思路,用 unsafe.Pointer 作为媒介,指针转换结束后,解指针拿到值即可。

import ( "unsafe" ) func BytesToString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } func StringToBytes(s string) []byte { return *(*[]byte)(unsafe.Pointer(&s)) }

其实这里从 []byte 转 string 的操作就是和 strings 包下 Builder 的设计一致的:

// A Builder is used to efficiently build a string using Write methods. // It minimizes memory copying. The zero value is ready to use. // Do not copy a non-zero Builder. type Builder struct { addr *Builder // of receiver, to detect copies by value buf []byte } // String returns the accumulated string. func (b *Builder) String() string { return *(*string)(unsafe.Pointer(&b.buf)) } // Reset resets the Builder to be empty. func (b *Builder) Reset() { b.addr = nil b.buf = nil } // Write appends the contents of p to b's buffer. // Write always returns len(p), nil. func (b *Builder) Write(p []byte) (int, error) { b.copyCheck() b.buf = append(b.buf, p...) return len(p), nil } // WriteString appends the contents of s to b's buffer. // It returns the length of s and a nil error. func (b *Builder) WriteString(s string) (int, error) { b.copyCheck() b.buf = append(b.buf, s...) return len(s), nil }

strings.Builder 设计之处就是为了最大程度降低内存拷贝。本质是维护了一个 buf 的字节数组。

sync.Pool

sync.Pool 的设计中在本地 pool 没有可以返回 Get 的元素时,会到其他 poolLocal 偷一个元素回来,这个跳转到其他 pool 的操作就是用 unsafe.Pointer + uintptr + SizeOf 实现的,参考一下:

func indexLocal(l unsafe.Pointer, i int) *poolLocal { lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{})) return (*poolLocal)(lp) }

到此这篇关于Golang 中的 unsafe.Pointer 和 uintptr详解的文章就介绍到这了,更多相关Golang uintptr内容请搜索易知道(ezd.cc)以前的文章或继续浏览下面的相关文章希望大家以后多多支持易知道(ezd.cc)!

推荐阅读

    golang代理怎么设置

    golang代理怎么设置,代理,请求,设置,语言,客户端,应用程序,Go语言(或称Golang)是一种广受欢迎的编程语言,尤其适用于网络编程。在创建Web应用程序

    什么是golang

    什么是golang,语言,执行,编程语言,机器语言,高级语言,运行,golang是一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言;它可以在不损

    Golang之wait.Until 简单测试用例

    Golang之wait.Until 简单测试用例,结束,主程序,测试目的,验证wait.Until的用法 //测试wait.Until() 的用途package mainimport ( "fmt

    使用Golang实现简单Ping过程

    使用Golang实现简单Ping过程,报文,字节,摘要: Ping的基本原理是发送和接受ICMP请求回显报文,利用Go语言可以轻松实现这一过程,较之C/C++语言

    golang二进制读写文件-二进制文件

    golang二进制读写文件-二进制文件有些时候,我突然想知道MySQL的如何存储数据的.她是如何完整的获取某行的数据,并且获得每个字段的值.比如,我

    GoLang下载安装-bin文件安装

    GoLang下载安装-bin文件安装GoLang IDEA https://www.jetbrains.com/go/download/download-thanks.htmlgolang特性:Go原生支持并发(Concur

    Golang gRPC HTTP协议转换示例

    Golang gRPC HTTP协议转换示例gRPC HTTP协议转换
    正当有这个需求的时候,就看到了这个实现姿势。源自coreos的一篇博客,转载到了grpc官方

    Golang泛型的使用方法详解

    Golang泛型的使用方法详解目录1. 泛型是什么2. 泛型的简单使用2.1. 泛型示例2.2. 自定义泛型类型2.3. 调用带泛型的函数3. 自定义泛型类