您的位置:首页 > 教程 > 其他脚本 > Golang 中的 unsafe.Pointer 和 uintptr详解

Golang 中的 unsafe.Pointer 和 uintptr详解

2022-08-05 18:03:42 来源:易采站长站 作者:

Golang 中的 unsafe.Pointer 和 uintptr详解

目录
前言uintptrunsafe.Pointer使用姿势常规类型互转Pointer => uintptr指针算数计算:Pointer => uintptr => Pointerreflect 包中从 uintptr => Ptr实战案例string vs []bytesync.Pool

前言

日常开发中经常看到大佬们用各种>

uintptr

uintptr>

// 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

    我们来看一下什么是>

    // 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>

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

        这里还是要在强调一下:

          uintptr 指的是具体的内存地址,不是个指针,没有指针的语义,你可以将 uintptr 打印出来比对地址是否相同。即便某个对象因为 GC 等原因被回收,uintptr的值也不会连带着变动。uintptr地址关联的对象可以被垃圾回收。GC不认为uintptr是活引用,因此unitptr地址指向的对象可以被垃圾收集。

          指针算数计算: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>

          我们知道,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>

          活学活用,其实参照上面转换的第一个案例就可以实现,不需要 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>

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

          如有侵权,请发邮件到 [email protected]

相关文章

  • 使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    使用Go基于WebSocket构建千万级视频直播弹幕系统的代码详解

    (1)业务复杂度介绍 开门见山,假设一个直播间同时500W人在线,那么1秒钟1000条弹幕,那么弹幕系统的推送频率就是: 500W * 1000条/秒=50亿条/秒 ,想想B站2019跨年晚会那次弹幕系统得是
    2020-07-08
  • golang中import cycle not allowed解决的一种思路

    golang中import cycle not allowed解决的一种思路

    发现问题 项目中碰到了一些问题,使用了指针函数的思路来解决相应问题 在实际项目中,因为两个项目互相引了对方的一些方法,导致了循环引用的错误,原本可以使用http的请求来解
    2019-11-10
  • 从go语言中找&和*区别详解

    从go语言中找&和*区别详解

    *和的区别 : 是取地址符号 , 即取得某个变量的地址 , 如 ; a*是指针运算符 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值 . 从
    2020-06-23
  • Go语言中利用http发起Get和Post请求的方法示例

    Go语言中利用http发起Get和Post请求的方法示例

    关于 HTTP 协议 HTTP(即超文本传输协议)是现代网络中最常见和常用的协议之一,设计它的目的是保证客户机和服务器之间的通信。 HTTP 的工作方式是客户机与服务器之间的 “请求-应答
    2019-11-10
  • golang如何实现mapreduce单进程版本详解

    golang如何实现mapreduce单进程版本详解

    前言 MapReduce作为hadoop的编程框架,是工程师最常接触的部分,也是除去了网络环境和集群配 置之外对整个Job执行效率影响很大的部分,所以很有必要深入了解整个过程。元旦放假的第一天
    2019-11-10
  • Go打包二进制文件的实现

    Go打包二进制文件的实现

    背景 众所周知,go语言可打包成目标平台二进制文件是其一大优势,如此go项目在服务器不需要配置go环境和依赖就可跑起来。 操作 需求:打包部署到centos7 笔者打包环境:mac os 方法:
    2020-03-11
  • GO语言实现简单的目录复制功能

    GO语言实现简单的目录复制功能

    本文实例讲述了GO语言实现简单的目录复制功能。分享给大家供大家参考。具体实现方法如下: 创建一个独立的 goroutine 遍历文件,主进程负责写入数据。程序会复制空目录,也可以设
    2019-11-10
  • golang中定时器cpu使用率高的现象详析

    golang中定时器cpu使用率高的现象详析

    前言: 废话少说,上线一个用golang写的高频的任务派发系统,上线跑着很稳定,但有个缺点就是当没有任务的时候,cpu的消耗也在几个百分点。 平均值在3%左右的cpu使用率。你没有任务
    2019-11-10