golang低级编程:一.unsafe包

go语言在设计上确保了一些安全的属性,限制了程序可能出错的途径。例如严格的类型转换规则。但也使得很多实现的细节无法通过go程序来访问,例如对于聚合类型(如结构体)的内存布局,或者一个函数对应的机器码。

这里我们将讨论unsafe包,它是由编译器实现的,实现了对语言内置特性的访问功能,这些特性一般是不可见的,因为它们暴露了go详细的内存布局。虽然包的名字叫unsafe,但是这些函数本身是安全的,并且在做内存优化的时候,它们对理解函数底层内存布局很有帮助。

unsafe.Sizeof

unsafe.Sizeof 报告传递给它的参数在内存中所占的字节长度,这个参数可以是任意类型的表达式。Sizeof仅会报告每个数据结构固定部分的内存所占字节长度,例如指针或者字符串所占的长度,但不会报告例如字符串内容的间接长度。为了可移植性,以字来表示引用类型的长度或者包含引用类型的长度,在32位系统上字的长度是4个字节,而在64位系统上字的长度是8个字节。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x struct{
		a bool
		b int16
		c []int
	}

	fmt.Println(unsafe.Sizeof(x.a))
	fmt.Println(unsafe.Sizeof(x.b))
	fmt.Println(unsafe.Sizeof(x.c))
	fmt.Println(unsafe.Sizeof(x))
}
// 64位机器结果:
// 1  : 1个字节,bool
// 2  : 两个字节,16/8 = 2
// 24 : 切片24个字节,3个字,因为切片包含一个指针,一个长度,一个容量。
// 32 :前面两个加上内存空位后就是一个字,8个字节。所以 8 + 24 = 32

如果b和c交换位置,那么内存空位将会更大,8 + 24 + 8 = 40

func main() {
	var x struct{
		a bool
		c []int
		b int16
	}

	fmt.Println(unsafe.Sizeof(x.a))
	fmt.Println(unsafe.Sizeof(x.c))
	fmt.Println(unsafe.Sizeof(x.b))
	fmt.Println(unsafe.Sizeof(x))
}
// 64位机器结果:
// 1
// 24
// 2
// 40   这里8 + 24 = 32和我们的猜想一样。

unsafe.Alignof

unsafe.Alignof报告参数类型所要求的对齐方式。这个参数可以是任意类型的表达式,并返回一个常量。布尔类型和数值类型对齐到它们的长度(最大8个字节),其他类型按字对齐。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x struct{
		a bool
		b int16
		c []int
	}

	fmt.Println(unsafe.Alignof(x.a))
	fmt.Println(unsafe.Alignof(x.b))
	fmt.Println(unsafe.Alignof(x.c))
	fmt.Println(unsafe.Alignof(x))
}

// 64位机器结果:
// 1  :这是布尔类型,布尔类型和数值类型对齐到它们长度
// 2  :这里数值类型,同上
// 8  :其他类型按字对齐,在64位机器上,一个字是8个字节
// 8

unsafe.Offsetof(f)

计算成员f相对于结构体的起始地址的偏移量。如果有内存空位也计算在内,该函数的操作数,必须是一个成员选择器:x.a。

func main() {
	var x struct{
		a bool
		c []int
		b int16
	}

	fmt.Println(unsafe.Offsetof(x.a))
	fmt.Println(unsafe.Offsetof(x.c))
	fmt.Println(unsafe.Offsetof(x.b))
}
// 64位机器结果:
// 0 : 结构体的第一个成员
// 8 :这里有7个字节的内存空位
// 32

unsafe.Pointer

unsafe.Pointer是一种特殊类型的指针,它可以存储任何变量的地址。对于一个unsafe.Pointer类型的指针,由于我们不知道它的具体类型,导致我们不能间接的通过*p来获取它的实际值。普通类型的指针也可以转换为unsafe.Pointer类型的指针,unsafe.Pointer类型的指针可以转换为普通类型的指针,而且不必和原来的类型相同。使用unsafe.Pointer进行类型转换可以将任意的值写入内存,并因此破坏类型系统。

uintper类型

uintper类型保存了指针所指向地址的数值,这样就可以进行数值运算。(uintpter类型是一个足够大的无符号整型,可以用来表示任何地址。)unsafe.Pointer也可以转换为uintptr,当然uintptr也可以转换为unsafe.Pointer(这里也会破坏类型系统)。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x struct{
		a bool
		b int16
		c []int
	}

	//pb := &x.b
	pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))

	*pb = 42
	fmt.Println(x.b)
}

这里首先将x的地址转换为unsafe.Pointer类型从而转换为uintptr类型,uintptr类型就可以用于计算。计算后再转换为原来的类型,对内存地址指向的区域赋值。但是这里不能引入uintptr类型的临时变量,例如下面这样。因为垃圾回收器会移动内存中的变量,为了减少内存碎片。但是在垃圾回收器uintptr类型仅仅是一个数值,所以移动过后,不会改变uintptr存的指针对应的内存里的数据。

    ptr := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    pb := (*int16)(unsafe.Pointer(ptr))

总结

unsafe包可以用来操作内存,从而增加go语言的灵活性,但是unsafe包无法保证在未来go语言升级中能够兼容。uintptr类型不能作为临时变量。并且在uintptr类型转换到unsafe.Pointer的过程中要尽量减少uintptr的操作次数。

posted @ 2022-03-02 00:15  EthanWell  阅读(657)  评论(0编辑  收藏  举报