使用unsafe包,可以绕过Go语言的类型系统,直接读写内存。Go官方不提倡使用该包,使用该包时应从安全性、兼容性等多方面进行综合考量。
unsafe.Sizeof() 函数用于查看变量所占用的内存大小(多少字节)
unsafe.Offsetof() 函数用于查看结构体中字段相对于结构体起始地址的偏移量
unsafe.Alignof() 函数用于查看变量的内存对齐方式
type Structure struct {
BOOL bool
INT64 int64
}
s := Structure{}
fmt.Println(unsafe.Sizeof(s)) // 16
fmt.Println(unsafe.Sizeof(s.BOOL)) // 1
fmt.Println(unsafe.Sizeof(s.INT64)) // 8
fmt.Println(unsafe.Offsetof(s.BOOL)) // 0
fmt.Println(unsafe.Offsetof(s.INT64)) // 8
fmt.Println(unsafe.Alignof(s)) // 8
fmt.Println(unsafe.Alignof(s.BOOL)) // 1
fmt.Println(unsafe.Alignof(s.INT64)) // 8
unsafe.Pointer 非类型安全指针,可以与类型安全指针相互转换,修改内存中的内容
unsafe.Add() 用于unsafe.Pointer的位置偏移运算
type str struct { // string 内存布局
data unsafe.Pointer
len int
}
type Student struct {
Name str
age int64
}
s := Student{}
sptr := unsafe.Pointer(&s)
*(*string)(sptr) = "张三"
*(*int)(unsafe.Pointer(uintptr(sptr) + unsafe.Sizeof(s.Name))) = 18
*(*int)(unsafe.Add(sptr, unsafe.Sizeof(s.Name))) = 19
fmt.Println(*(*string)(unsafe.Pointer(&s.Name)))
fmt.Println(s.age)
应直接在unsafe.Pointer()的括号中使用uintptr强转的变量进行地址运算,以免某些变量被GC。Go语言中栈是动态变化的,一个栈上变量的地址是极有可能发生变化的。
package main
import (
"fmt"
"unsafe"
)
func main() {
bs := []byte{'H', 'E', 'L', 'L', 'O'}
str := bytes2string(bs)
fmt.Println(str)
bs[0] = 'h'
fmt.Println(str)
bs = string2bytes(str)
bs[0] = 'H'
fmt.Println(str)
fmt.Println(len(bs))
fmt.Println(cap(bs))
}
func bytes2string(bytes []byte) string {
return *(*string)(unsafe.Pointer(&bytes))
}
func string2bytes(str string) []byte {
return *(*[]byte)(unsafe.Pointer(&(struct {
string
int
}{string: str, int: len(str)})))
}
零拷贝,打破了string变量不可修改的规范。byte切片与string共用底层byte数组。修改一方另一方也随之变动。
unsafe.Slice() 用已有的数组创建一个切片,其长度和容量应为数组的长度
array := []int{1, 2, 3, 4, 5}
fmt.Println(unsafe.Slice(&array[0], len(array))) // [1 2 3 4 5]
闲扯两句
通过unsafe包,可以修改任意结构体的字段,即使是非导出字段(任意包下的,只要你知道其内存布局),这种行为非常hack,在绕过类型系统的同时,为Go语言带来了一定的自由;但稍有不慎,就可能让你的runtime崩溃。若两个结构体类型内存布局相同,则可以通过unsafe.Pointer无缝转换。在使用unsafe.Pointer强转时,uintptr类型的中间量不应轻易的赋值给其他变量使用,它是一个整数,代表的是地址,但它不会像指针一样引用一片内存区域,所以中间态的uintptr变量保存的就是一个“数”,当Go runtime 开心时,对垃圾进行回收,根本不会顾及到uintptr的引用,虽然runtime中有些syscall方法中的参数也是uintptr类型,但编译器会对其进行特殊处理,当uintptr指向的内存被回收后,你仍操作该内存,会产生十分诡异的问题,像野指针一样,不容易排查。
再闲扯两句
Go语言出身名门,最初是业内的几位泰斗级的大佬亲自操刀,不可否认的,这是一门优秀的语言。但在逐渐的深入了解后你会发现,其封装程度非常之高,当你遇见性能瓶颈的时候,可优化的余地非常之有限。Go有很多适合自己的领域,但其不是万能的。GC很爽,但也很拖后腿。若想让一个Go函数效率更高一些怎么做?答案是用汇编重写该函数…Go使用的伪汇编是古老的plan 9操作系统的plan 9汇编。
Reference
https://pkg.go.dev/unsafe@go1.19.4#Sizeof