Go语言unsafe包
Go语言的unsafe包提供了一些底层操作的函数,这些函数可以绕过Go语言的类型系统,直接操作内存。虽然这些函数很强大,但是使用不当可能会导致程序崩溃或者产生不可预料的行为。因此,使用unsafe包时必须小心谨慎。
此外,他提供了一些方法和类型如下
其中Pointer
的功能是很强大的,reflect
包的功能大多基于它实现,具体可看下面内容
类型
type ArbitraryType int // 代表go中任意类型
type IntegerType int // int 类型
type Pointer *ArbitraryType // 是可以指向ArbitraryType的指针
Pointer详解
关于Pointer有一些规范如下:
- 任意类型的指针都可以转换为Pointer,同理,Pointer可以转换为任意类型的指针。
- uintptr可以转换为pointer,pointer也可以转换为uintptr。
需要注意,Pointer可以操作任意内存,使用起来要小心
Pointer的使用有下面的几种情景:
利用Pointer将*T1
转换为*T2
ps:需要注意,底层数据长度可以对上,就可以转换,不一定要固定的类型
package mainimport ("fmt""unsafe"
)func main() {var a = 4.5println(Float64bits(a))var b = []string{"1","2"}bits := SliceBits(b)fmt.Printf("%v \n",bits)var c = trueprintln(BoolBits(c))println(BoolBitsInt(c))
}
func Float64bits(f float64) uint64 {// 将float64转换为uint64// 首先要注意,是指针和指针的转换。return *(*uint64)(unsafe.Pointer(&f))
}
func SliceBits(data []string) [3]int{// 切片的底层结构是一个 uintptr,和两个int,这里就可以将他转换为一个长度为3的数组// 需要注意的是,这样的是可以的,因为他们的底层数据是长度是一样,都是8个字节。return *(*[3]int)(unsafe.Pointer(&data))
}
func BoolBits(data bool) uint8 {return *(*uint8)(unsafe.Pointer(&data))
}
func BoolBitsInt(data bool) int {return *(*int)(unsafe.Pointer(&data))
}//outPut:
4616752568008179712
[1374389948232 2 2]
1
286655889801473 // 底层数据不一致,出了问题
解释如下:
利用Pointer可以来移动指针
func main() {var p = Point{x: 2,y: 3,}yUintptr := uintptr(unsafe.Pointer(&p)) + 8 // 在p的地址上增加了8个偏移量,找到了p,这个时候p还是uintptrprintln(*(*int)(unsafe.Pointer(yUintptr))) // output:3 // 将uintptr转换为*int的指针,然后访问值// 上面的操作相当于// println(*(*int)(unsafe.Add(unsafe.Pointer(&p), 8)))// 从p开始,移动8个,然后访问值
}
将reflect.Value.Pointer or reflect.Value.UnsafeAddr方法的返回值转换为Pointer
这底层用的是Pointer来做的
func main() {var p = Point{x: 2,y: 3,}reflect.ValueOf(p).Pointer() // 返回值为 uintptrreflect.ValueOf(p).UnsafePointer() // 返回值为 unsafe.Pointerreflect.ValueOf(p).UnsafeAddr() // 返回值为 uintptr
}
我们看一个reflect.ValueOf()
的实现
将Pointer转换为reflect.SliceHeader和reflect.StringHeader
可以通过这种方式来查看String和slice底层的数据结构,直接做转换.
func main() {var a = "this"s := (*reflect.StringHeader)(unsafe.Pointer(&a))fmt.Printf("%+v \n",s) //&{Data:4304632499 Len:4}var a1 = []string{"1","2","3"}s2 := (*reflect.SliceHeader)(unsafe.Pointer(&a1))fmt.Printf("%+v",s2) // &{Data:1374389961088 Len:3 Cap:3}
}
利用将string转为[]byte或者[]byte转string的时候不copy底层数组
string转[]byte不拷贝底层数组
-
验证copy操作
利用汇编可以看到在转[]byte的时候调用了函数
源码链接:https://github.com/golang/go/blob/261e26761805e03c126bf3934a8f39302e8d85fb/src/runtime/string.go#L166
-
自己做,省略copy操作
func main() {var a = "test" // 创建str// 本质来说就是切片和stringHead的转换ints := *(*[2]int)(unsafe.Pointer(&a))fmt.Printf("%+v \n",ints) // [4297030033 4]var b = [3]int{ // 这里我将stringHead和sliceHead看成了int类型的数组,0 index表示data的ptr,1 index为长度,2index为容量ints[0],ints[1],ints[1],}s := (*[]byte)(unsafe.Pointer(&b))fmt.Printf("%+v\n",*s) // [116 101 115 116] }
上面的操作就是现将String转为长度为2的数组,然后构建一个长度为3的数组,转为[]byte切片。
ps: 需要注意,在转换的时候只要底层的数据长度能对上,就可以转换
下面的代码是我按照正常流程写的
func main() {var a = "test" // 创建strs := *(*reflect.StringHeader)(unsafe.Pointer(&a)) // 转出stringHeadheader := reflect.SliceHeader{ // 构建sliceHeaderData: s.Data,Len: s.Len,Cap: s.Len,}i := *(*[]byte)(unsafe.Pointer(&header)) // 转为[]bytefmt.Printf("%+v \v",i)
}
例子
int转char
func main() {var a = 65s := *(*[1]byte)(unsafe.Pointer(&a)) // int看为长度为1的byte数组fmt.Printf(string(s[0])) // A 65对应的asill码是A
}
字符串转byte
func main() {var a = "test"i := *(*[]byte)(unsafe.Pointer(&a))fmt.Printf("%+v",i) // [116 101 115 116]
}
操作非导出字段
方法
Sizeof
func Sizeof(v ArbitraryType) uintptr
Sizeof函数返回类型v的大小。ArbitraryType表示任意类型。
package mainimport ("fmt""unsafe"
)type Point struct {x, y int
}func main() {var p Pointsize := unsafe.Sizeof(p)fmt.Println(size) // Output: 16var p1 = ""size1 := unsafe.Sizeof(p1)fmt.Println(size1) // Output: 16 // string对应的结构体是reflect.StringHeader,StringHeader有两个字段 Data:uintptr类型,表示底层数组,Len int类型,表示数组长度,
}
SizeOf返回的是p类型的的大小,也就是Point
类型所占的空间大小
int占8个字节,这里有两个变量,x,y结果就是16
顺便来看看,go中用map实现set的时候,v为什么是空结构体?
func main() {var a = []string{"a", "a", "b"}set := Set(a)fmt.Printf("%v \n", set) // outPut Setvar b = struct {}{}sizeof := unsafe.Sizeof(b) println(sizeof) // output: 0
}
// Set 用泛型实现set
func Set[T comparable](data []T) []T {m := make(map[T]struct{}, len(data))for _, item := range data {m[item] = struct{}{}}res := make([]T, 0, len(m))for t := range m {res = append(res, t)}return res
}
因为空结构体不占内存空间。
Offsetof
func Offsetof(x ArbitraryType) uintptr
Offsetof函数返回结构体字段x相对于结构体起始位置的偏移量。ArbitraryType表示任意类型。
type Point struct {x, y int
}func main() {var p Pointoffset := unsafe.Offsetof(p.y)fmt.Println(offset) // Output: 8
}
Point
结构体中,y变量相对于起始位置偏移量为8,因为y前面有x,x为int,int占在64机器上占8个字节
Alignof
func Alignof(v ArbitraryType) uintptr
Alignof函数返回类型v的对齐方式。ArbitraryType表示任意类型。
type Point struct {x, y int
}func main() {align := unsafe.Alignof(Point{})fmt.Println(align) // Output: 8
}
Add
func Add(p unsafe.Pointer, x uintptr) unsafe.Pointer
Add函数返回指针p加上偏移量x后的指针。注意,返回的指针仍然是unsafe.Pointer类型,需要转换为具体的指针类型才能使用。
package mainimport ("unsafe"
)type Point struct {x, y int
}func main() {var p = Point{x: 123,y: 3123,}pointer := unsafe.Pointer(&p) // 将p的指针转换为 pointeryPointer := unsafe.Add(pointer, unsafe.Sizeof(p.x)) // 在p的指针上,增加了8个字节的长度,其实就是int类型的长度yPtr := (*int)(yPointer) // 此时pointer已经指向了y,将此pointer转换为 int类型的指针println(*yPtr) // 通过指针访问y的值}
add
方法就是移动指针,在go里面不支持指针的运算,所以不能像c那样直接位移指针,但go作为新时代的c语言,在加上确实有这个需求,就提供了这种方式来操作指针。
Slice
func Slice(p unsafe.Pointer, len, cap int) unsafe.Pointer
Slice函数返回指针p开始的长度为len、容量为cap的切片。注意,返回的指针仍然是unsafe.Pointer类型,需要转换为具体的切片类型才能使用。
func main() {arr := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //数组data := unsafe.Slice(&arr[0], len(arr)) // 从数组第0个元素开始,长度为len,返回一个地址的切片array := *(*[len(arr)]byte)(data) // 转换为 数组类型fmt.Printf("%v \n",array) // [0 1 2 3 4 5 6 7 8 9]array[5] = 3fmt.Printf("%v \n",array) // [0 1 2 3 4 3 6 7 8 9]
}
Slice
函数返回一个切片,切片的开始位置是底层数组的开始位置,长度和容量都是len
Slice(ptr, len) 相当于 (*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
或者下面的例子
func main() {arr := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} //数组data := unsafe.Slice(&arr[0], 3) // 从数组第0个元素开始,长度为len,返回一个地址的切片array := *(*[3]byte)(data) // 转换为 数组类型fmt.Printf("%v \n",array) // [0 1 2]array[2] = 9fmt.Printf("%v \n",array) // [0 1 9]]
}
// 从arr创建了一个长度为3的切片
可以看这篇回答 :
How to create an array or a slice from an array unsafe.Pointer in golang?
以上就是unsafe包中常用的一些函数,使用时需要谨慎,避免出现不可预料的错误。