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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang利用unsafe操作未导出变量-Pointer使用详解

    前言 unsafe.Pointer其实就是类似C的void *,在golang中是用于各种指针相互转换的桥梁.uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换.uintptr和unsafe.Pointer的区别就是:unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算:而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 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 就发憷,不了解二者的区别和场景,自然心里没数.

  • Golang中的错误处理的示例详解

    目录 1.panic 2.包装错误 3.错误类型判断 4.错误值判断 1.panic 当我们执行panic的时候会结束下面的流程: package main import "fmt" func main() { fmt.Println("hello") panic("stop") fmt.Println("world") } 输出: go run 9.go hellopanic: stop 但是panic也是可以捕获的,我们可

  • go中的unsafe包及使用详解

    Unsafe code是一种绕过go类型安全和内存安全检查的Go代码.大多数情况,unsafe code是和指针相关的.但是要记住使用unsafe code有可能会损害你的程序,所以,如果你不完全确定是否需要用到unsafe code就不要使用它. 以下面的 unsafe.go 为例,看一下unsafe code的使用 package main import ( "fmt" "unsafe" ) func main() { var value int64 = 5 v

  • golang中defer的关键特性示例详解

    前言 大家都知道golang的defer关键字,它可以在函数返回前执行一些操作,最常用的就是打开一个资源(例如一个文件.数据库连接等)时就用defer延迟关闭改资源,以免引起内存泄漏.本文主要给大家介绍了关于golang中defer的关键特性,分享出来供大家参考学习,下面话不多说,来一起看看详细的介绍: 一.defer 的作用和执行时机 go 的 defer 语句是用来延迟执行函数的,而且延迟发生在调用函数 return 之后,比如 func a() int { defer b() return

  • Golang中如何使用lua进行扩展详解

    前言 最近在项目中需要使用lua进行扩展,发现github上有一个用golang编写的lua虚拟机,名字叫做gopher-lua.使用后发现还不错,借此分享给大家,下面话不多说了,来一起看看详细的介绍吧. 数据类型 lua中的数据类型与golang中的数据类型对应关系作者已经在文档中说明,值得注意的是类型是以L开头的,类型的名称是以LT开头的. golang中的数据转换为lua中的数据就必须转换为L开头的类型: str := "hello" num := 10 L.LString(st

  • Golang中数据结构Queue的实现方法详解

    前言 本文主要给大家介绍了关于Golang中数据结构Queue实现的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 需求 队列的特性较为单一,基本操作即初始化.获取大小.添加元素.移除元素等.最重要的特性就是满足先进先出. 实现 接下来还是按照以前的套路,一步一步来分析如何利用Go的语法特性实现Queue这种数据结构. 定义 首先定义每个节点Node结构体,照例Value的值类型可以是任意类型,节点的前后指针域指针类型为node type node struct {

  • golang中snappy的使用场合实例详解

    前言 项目中遇到的压缩/解压缩需求应该是很多的,比如典型的考虑网络传输延时而对数据进行压缩传输,又或者其他各种省空间存储需求等.这次同样是遇到了类似需求,在做一个爬虫时,因为抓取项目还未确定,所以考虑将整个html页面压缩存储于数据库,于是又是各种google,最后不出意外的google到了google家的Snappy :-) google 自家的snappy 压缩优点是非常高的速度和合理的压缩率.压缩率比gzip 小,CPU 占用小. golang中snappy使用场合 下面是对几个简单的字符

  • 关于Golang中range指针数据的坑详解

    前言 在Golang中使用 for range 语句进行迭代非常的便捷,但在涉及到指针时就得小心一点了. 下面的代码中定义了一个元素类型为 *int 的通道 ch : package main import ( "fmt" ) func main() { ch := make(chan *int, 5) //sender input := []int{1,2,3,4,5} go func(){ for _, v := range input { ch <- &v } cl

  • Golang中的Unicode与字符串示例详解

    背景: 在我们使用Golang进行开发过程中,总是绕不开对字符或字符串的处理,而在Golang语言中,对字符和字符串的处理方式可能和其他语言不太一样,比如Python或Java类的语言,本篇文章分享一些Golang语言下的Unicode和字符串编码. Go语言字符编码 注意: 在Golang语言中的标识符可以包含 " 任何Unicode编码可以标识的字母字符 ". 被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果就将会是 "�",即:一个

  • golang中为什么不存在三元运算符详解

    三元运算符广泛存在于其他语言中,比如: python: val = trueValue if expr else falseValue javascript: const val = expr ? trueValue : falseValue c.c++: const char *val = expr ? "trueValue" : "falseValue"; 然而,被广泛支持的三目运算符在golang中却是不存在的!如果我们写出类似下面的代码: val := ex

随机推荐