🔰 全文字数 : 3K+
🕒 阅读时长 : 8min
📋 关键词汇 : golang / unsafe
👉 欢迎关注 : 大摩羯先生

Golangunsafe.Pointeruintptrunsafe.SizeofGolangunsafe
unsafe包
unsafeunsafeGo
JavaunsafeunsafeGo
unsafe构成
type ArbitraryType int

type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr

func Offsetof(x ArbitraryType) uintptr

func Alignof(x ArbitraryType) uintptr

可以看到,包的构成比较简单,下面我们主要结合源码中注释内容来展开剖析和学习。

type ArbitraryType int

Arbitrary
type ArbitraryType int
ArbitraryTypeunsafeGo

type Pointer *ArbitraryType

type Pointer *ArbitraryType
Pointerunsafe

灵活转换

  它表示指向任意类型的指针,有四种特殊操作可用于类型指针,而其他类型不可用,大概的转换关系如下:

PointerPointeruintptrPointerPointeruintptr

潜在的危险性

Pointer

  源码注释中列举了提到了一些正确错误使用的例子。它还提到更为重要的一点是:不使用这些模式的代码可能现在或者将来变成无效。即使下面的有效模式也有重要的警告。试图来理解下这句话的核心就是,它不能对你提供什么保证!

go vetgo vet
代码样例:
func TestErr(t *testing.T) {
  fmt.Printf("%d","hello world")
}
运行:
`go vet unsafe/unsafe_test.go`
控制台输出提示: 
unsafe/unsafe_test.go:9:2: Printf format %d has arg "hello world" of wrong type string

✅ 正确的使用姿势

Pointer
  • (1) 指针 *T1 转化为 指针 *T2.
      T1、T2两个变量共享等值的内存空间布局,在不超过数据范围的前提下,可以允许将一种类型的数据重新转换、解释为其他类型的数据。

  下面我们操作一个样例:声明并开辟一个内存空间,然后基于该内存空间进行不同类型数据的转换

  代码如下:

// 步骤:
// (1) 声明为一个int64类型
// (2) int64 -> float32
//(3) float32 -> int32
func TestPointerTypeConvert(t *testing.T) {
   //  (1) 声明为一个int64类型
   int64Value := int64(20)

   // int64数据打印
   fmt.Println("int64类型的值:", int64Value)
   //打印:int64类型的值: 20
   fmt.Println("int64类型的指针地址:", &int64Value)
   //打印:int64类型的指针地址: 0xc000128218

   // (2) int64 -> float32
   float32Ptr := (*float32)(unsafe.Pointer(&int64Value))
   fmt.Println("float32类型的值:", *(*float32)(unsafe.Pointer(&int64Value)))
   //打印:float32类型的值: 2.8e-44
   fmt.Println("float32类型的指针地址:", (*float32)(unsafe.Pointer(&int64Value)))
   //打印:float32类型的指针地址: 0xc000128218

   // (3) float32 -> int32
   fmt.Println("int32类型的指针:", (*int32)(unsafe.Pointer(float32Ptr)))
   //打印:int32类型的指针: 0xc000128218
   fmt.Println("int32类型的值:", *(*int32)(unsafe.Pointer(float32Ptr)))
   //打印:int32类型的值: 20
}
Pointer
uintptruintptruintptruintptruintptruintptruintptruintptruintptr
uintptr
// (1) 声明一个数组,持有两个元素
// (2) 输出第1个元素指针信息
// (3) 输出第2个元素指针信息
// (4) 通过第一个元素指针地址加上偏移量可以得到第二个元素地址
// (5) 还原第二个元素的值
func TestUintptrWithOffset(t *testing.T) {
  // (1) 声明一个数组,持有两个元素
  p := []int{1,2}
  
  // (2) 输出第1个元素指针信息
  fmt.Println("p[0]的指针地址:",&p[0])
  // p[0]的指针地址 0xc0000a0160
  ptr0 := uintptr(unsafe.Pointer(&p[0]))
  fmt.Println(ptr0)
  // 824634376544
  
  // (3) 输出第2个元素指针信息
  fmt.Println("p[1]的指针地址:",&p[1])
  // p[1]的指针地址 0xc0000a0168
  ptr1 := uintptr(unsafe.Pointer(&p[1]))
  fmt.Println(ptr1)
  // 824634376552
   
  // (4) 通过第一个元素指针地址加上偏移量可以得到第二个元素指针地址
  offset := uintptr(unsafe.Pointer(&p[0])) + 8 //int类型占8字节
  ptr1ByOffset := unsafe.Pointer(offset)
  fmt.Println("p[0]的指针地址 + offset偏移量可以得到p[1]的指针地址:",ptr1ByOffset)
  // p[0]的指针地址 + offset偏移量可以得到p[1]的指针地址 0xc0000a0168
  // (5) 还原第二个元素的值
  fmt.Println("通过偏移量得到的指针地址还原值:",*(*int)(ptr1ByOffset))
  // 通过偏移量得到的指针地址还原值:2
}
&^

❌ 错误的使用姿势

与C中不同的是,将指针指向到其原始分配结束之后是无效的:

//❌ 无效:分配空间外的端点
func TestOverOffset(t *testing.T) {
   // 声明字符串变量str
   str := "abc"
   // 在str的内存偏移量基础上增加了额外的一个偏移量得到一个新的内存偏移量,该内存地址是不存在的
   newStr := unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof(str))
   // 这里由于不存在该内存偏移量的对象,肯定求不到值,这里的表现是一直阻塞等待
   fmt.Println(*(*string)(newStr))
}

注意,两个转换必须出现在同一个表达式中,它们之间只有中间的算术运算。

//❌ 无效:在转换回指针之前,uintptr不能存储在变量中
u := uintptr(p)
p = unsafe.Pointer(u + offset)

//推荐如下这种方式,不要依靠中间变量来传递uintptr
p = unsafe.Pointer(uintptr(p) + offset)

请注意,指针必须指向已分配的对象,因此它不能是零。

//❌ 无效:零指针的转换
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
syscall.SyscalluintptrsyscallSyscalluintptruintptr
uintptr
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
uintptr

要使编译器识别此模式,转换必须出现在参数列表中:

//❌ 无效:在系统调用期间隐式转换回指针之前,uintptr不能存储在变量中,和上面提到的问题类似
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))
uintptrPointerReflectReflect.Value.PointerReflect.Value.UnsafeAddr
reflectPointerUnsafeAddruintptrunsafeunsafePointer
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

与上述情况一样,在转换之前存储结果是无效的

//❌ 无效:在转换回指针之前,uintptr不能存储在变量中,和上面提到的问题类似
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))
reflect.SliceHeaderreflect.StringHeaderPointerreflect.SliceHeaderreflect.StringHeaderuintptrunsafeSliceHeaderStringHeaderslicestring
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n
hdr.Datauintptr
reflect.SliceHeaderreflect.StringHeaderslicestring*reflect.SliceHeader*reflect.StringHeader
// ❌ 无效: 直接声明的Header不会将数据作为引用。
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // p可能已经被回收

func Sizeof(x ArbitraryType) uintptr

Sizeofvv

Go语言中非聚合类型通常有一个固定的大小
引用类型包含引用类型的大小在32位平台上是4字节,在64位平台上是8字节

类型分类大小
bool非聚合1个字节
intN, uintN, floatN, complexN非聚合N/8个字节(例如float64是8个字节)
int, uint, uintptr非聚合1个机器字 (32位系统:1机器字=4字节; 64位系统:1机器字=8字节)
*T聚合1个机器字
string聚合2个机器字(data,len)
[]T聚合3个机器字(data,len,cap)
map聚合1个机器字
func聚合1个机器字
chan聚合1个机器字
interface聚合2个机器字(type,value)
type Model struct {
   //Field...
}

func TestSizeOf(t *testing.T) {
   boolSize := false
   intSize := 1
   int8Size := int8(1)
   int16Size := int16(1)
   int32Size := int32(1)
   int64Size := int64(1)
   arrSize := make([]int, 0)
   mapSize := make(map[string]string, 0)
   structSize := &Model{}
   funcSize := func() {}
   chanSize := make(chan int, 10)
   stringSize := "abcdefg"

   fmt.Println("bool sizeOf:", unsafe.Sizeof(boolSize))
   //bool sizeOf: 1
   fmt.Println("int sizeOf:", unsafe.Sizeof(intSize))
   //int sizeOf: 8
   fmt.Println("int8 sizeOf:", unsafe.Sizeof(int8Size))
   //int8 sizeOf: 1
   fmt.Println("int16 sizeOf:", unsafe.Sizeof(int16Size))
   //int16 sizeOf: 2
   fmt.Println("int32 sizeOf:", unsafe.Sizeof(int32Size))
   //int32 sizeOf: 4
   fmt.Println("int64 sizeOf:", unsafe.Sizeof(int64Size))
   //int64 sizeOf: 8
   fmt.Println("arrSize sizeOf:", unsafe.Sizeof(arrSize))
   //arrSize sizeOf: 24
   fmt.Println("structSize sizeOf:", unsafe.Sizeof(structSize))
   //structSize sizeOf: 8
   fmt.Println("mapSize sizeOf:", unsafe.Sizeof(mapSize))
   //mapSize sizeOf: 8
   fmt.Println("funcSize sizeOf:", unsafe.Sizeof(funcSize))
   //funcSize sizeOf: 8
   fmt.Println("chanSize sizeOf:", unsafe.Sizeof(chanSize))
   //chanSize sizeOf: 8
   fmt.Println("stringSize sizeOf:", unsafe.Sizeof(stringSize))
   //stringSize sizeOf: 16
}

func Offsetof(x ArbitraryType) uintptr

Offsetofvf

内存对齐 计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。由于地址对齐这个因素,一个聚合类型的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。\

内存空洞 编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐

bool、string、int16
type BoolIntString struct {
   A bool
   B int16
   C string
}

type StringIntBool struct {
   A string
   B int16
   C bool
}

type IntStringBool struct {
   A int16
   B string
   C bool
}

type StringBoolInt struct {
   A string
   B bool
   C int16
}

func TestOffsetOf(t *testing.T) {
   bis := &BoolIntString{}
   isb := &IntStringBool{}
   sbi := &StringBoolInt{}
   sib := &StringIntBool{}
   fmt.Println(unsafe.Offsetof(bis.A)) // 0
   fmt.Println(unsafe.Offsetof(bis.B)) // 2
   fmt.Println(unsafe.Offsetof(bis.C)) // 8
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(isb.A)) // 0
   fmt.Println(unsafe.Offsetof(isb.B)) // 8
   fmt.Println(unsafe.Offsetof(isb.C)) // 24
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(sbi.A)) // 0
   fmt.Println(unsafe.Offsetof(sbi.B)) // 16
   fmt.Println(unsafe.Offsetof(sbi.C)) // 18
   fmt.Println("")
   fmt.Println(unsafe.Offsetof(sib.A)) // 0
   fmt.Println(unsafe.Offsetof(sib.B)) // 16
   fmt.Println(unsafe.Offsetof(sib.C)) // 18
}

  以上是针对单个结构体内的内存对齐的测试演示,当多个结构体组合在一起时还会产生内存对齐,感兴趣可以自行实践并打印内存偏移量来观察组合后产生的内存空洞。

func Alignof(x ArbitraryType) uintptr

Alignofvvf
type Fields struct {
   Bool    bool
   String  string
   Int     int
   Int8    int8
   Int16   int16
   Int32   int32
   Float32 float32
   Float64 float64
}

func TestAlignof(t *testing.T) {
   fields := &Fields{}
   fmt.Println(unsafe.Alignof(fields.Bool)) // 1
   fmt.Println(unsafe.Alignof(fields.String))// 8
   fmt.Println(unsafe.Alignof(fields.Int)) // 8
   fmt.Println(unsafe.Alignof(fields.Int8)) // 1
   fmt.Println(unsafe.Alignof(fields.Int16)) // 2
   fmt.Println(unsafe.Alignof(fields.Int32))  // 4
   fmt.Println(unsafe.Alignof(fields.Float32))  // 4
   fmt.Println(unsafe.Alignof(fields.Float64))  // 8
}

  不同类型有着不同的内存对齐方式,总体上都是以最小可容纳单位进行对齐的,这样可以在兼顾以最小的内存空间填充来换取内存计算的高效性。

参考

Golang标准库文档
《Go语言圣经》底层编程章节