go version go1.14.4 linux/amd64
要说unsafe.Pointer, 不得不说这个话题.
学过c语言的同学都知道, p++
, p--
, p1 == p2
这种代码在c语言太过常见.
但是go中的指针有很多的限制:
i := 3
p := &i
p++ // Invalid operation: p++ (non-numeric type *int)
p = &i + 5 // Invalid operation: &i + 5 (mismatched types *int and untyped int)
i := 3
var f *float64
f = &i // Cannot use '&i' (type *int) as type *float64
var i int64 = 1
var f float64 = 1.0
pi, pf := &i, &f
pi == pf // Invalid operation: pi == pf (mismatched types *int64 and *float64)
pi != pf // Invalid operation: pi != pf (mismatched types *int64 and *float64)
下面的代码来自于: $GOROOT/src/unsafe/unsafe.go
package unsafe
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
原来unsafe.Pointer
就是*ArbitraryType
, 任意类型的指针, 其最终是*int
类型.
上面还出现了uintptr
, 其定义是:
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
也就是说uintptr
是个整数类型, 足够表示任意指针.
unsafe 包提供了 2 点重要的能力:
unsafe.Pointer
可以相互转换。uintptr
类型和 unsafe.Pointer
可以相互转换。go中的pointer不能直接进行数学运算,但可以把它转换成 uintptr
,对 uintptr
类型进行数学运算,再转换成 pointer 类型。
还有一点要注意的是,uintptr
并没有指针的语义,意思就是 uintptr
所指向的对象会被 gc 无情地回收。而 unsafe.Pointer
有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
unsafe
包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 $GOROOT/src/cmd/compile/internal/gc/unsafe.go
路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。
在go中我们常说私有成员不能直接修改, 甚至我们都调用不了私有成员.
其实利用unsafe包可以做到这一点(确实是unsafe, 不安全.)
对于一个结构体,通过 offset
函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址, 这在c语言中应该算一个常识。
事实上, 在之前关于linux内核之offset_of和container_of理解中, 也体现了这一点.
我们来看一个例子:
package main
import (
"fmt"
"unsafe"
)
type Programmer struct {
name string
age uint8
}
func main() {
p := Programmer{"gerrylon", 18}
fmt.Println(p) // {gerrylon 18}
// name是结构体的第一个成员, 和结构体地址相同, 所以下面两句都行
// name := (*string)(unsafe.Pointer(&p))
name := (*string)(unsafe.Pointer(&p.name))
*name = "gl"
// 结构体的起始地址, 加上age字段的偏移, 也就是age的地址
age := (*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))
*age = 19
fmt.Println(p) // {gl 19}
// 直接取得age字段的地址
age = (*uint8)(unsafe.Pointer(&p.age))
*age = 20
fmt.Println(p) // {gl 20}
}
上面的例子比较简单, 但是很基础, 仔细看肯定能看懂.
当然这没什么意思, 都在一个包了, 直接调字段都能修改了, 还用什么unsafe…
那如果我们要修改别的包的私有字段呢?
// unsafe_model包
package unsafe_model
type Programmer struct {
name string
age uint8
}
// main包
p := unsafe_model.Programmer{}
fmt.Println(p) // { 0}
// p.name = "gl" // Unresolved reference 'name'
name := (*string)(unsafe.Pointer(&p))
*name = "gl"
fmt.Println(p) // {gl 0}
// unsafe.Sizeof("") 即name字段的占的内存大小
age := (*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof("")))
*age = 19
fmt.Println(p) // {gl 19}
可以看到, 我们通过unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(""))
取得age字段的地址, 再进行后续运算.
这也说明, 在结构体中, 字段的顺序是很重要的, 不要随意更改.
$GOPATH/reflect/value.go中, 描述了string和slice的header:
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
那么我们就可以这样做:
func string2bytes(s string) []byte {
str := (*reflect.StringHeader)(unsafe.Pointer(&s))
by := reflect.SliceHeader{
Data: str.Data,
Len: str.Len,
Cap: str.Len,
}
ret := *(*[]byte)(unsafe.Pointer(&by))
// ret := *(*[]byte)(unsafe.Pointer(&s)) // 不行, 会导致返回的slice的cap有问题
return ret
}
func bytes2string(bs []byte) string {
// 现在两种方法都行
// by := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
// str := reflect.StringHeader{
// Data: by.Data,
// Len: by.Len,
// }
// return *(*string)(unsafe.Pointer(&str))
return *(*string)(unsafe.Pointer(&bs))
}
https://qcrao91.gitbook.io/go/biao-zhun-ku/unsafe
欢迎补充指正
(完)