pinter_safe指针分析与运用

golang 2019-04-27

go unsafe pointer 指针 & c pointer 指针

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
在go里可以使用unsafe包 里的pinter 来达到 像c语言那样任意的操作指针和内存,因为go里的指针是有许多限制的
比如 go 里的结构体 有私有、公有属性 也就是可否导出的属性,go里的指针是不能直接操作私有属性的成员的,但是使用原始指针就可以实现

一、使用unsafe指针修改 结构体值

go 里正常的操作是访问结构体的字段来进行修改,而原始的c语言是可以通过指针地址的偏移量来修改值的

type Prog struct {
    name string
    aget string
}
func main(){
    p := Prog{"a","1"}
    //正常的修改方式如下
    p.name = "go"
    p.age = "2"
    fmt.Println(p) //{go 2 xxxx}
    
    /**
     *而基于c语言的通过内存地址偏移量来进行修改值如下
     */
     //name 指向的是结构体首地址 同时也是结构体第一个字段name的地址
    name := (*string)(unsafe.Pointer(&p)) //name 是字符串指针
    *name  = "pointer name"
    //这样就可以定位到age所在的内存地址    
    age := (*string)(unsafe.Pointer( uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))  
    *age = "pointer age"
    fmt.Println(p) //{pointer name pointer age xxxx}
    
}

内存分配的一个知识点 结构体地址 也是结构体首个成员的地址

内存地址是连续的 数组相邻的元素 可以通过指针位移 该元素字节大小 的位置来定位下一个元素

通过unsafe.Offsetof() 偏移量可以定位 具体的成员地址

二、@unsafe.Sizeof() 计算字节大小

上面定位结构体成员的方法时使用 unsafe.Offsetof()方法定位的,还可以用sizeof来计算,如下

    //相当于计算 出age成员 string类型的字节大小 然后在指针进行位移 就可以定位到age的地址 因为内存地址是连续的
    age := (*string)(unsafe.Pointer( uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string("")))) 
    *age = "pointer age"
    fmt.Println(p) //{pointer name pointer age xxxx}

三、go通过指针操作结构体私有成员

通过unsafe 包的指针直接越过go的 私有公有指针限制,操作结构体的私有成员,要保证该结构体不在当前包下

package test
type Pro struct {
    name string
    age int
    des string
}


//------ 现在我们要直接更改des的值
pcakge main
p := test.pro{"xiaodo",18,"do"}
//p.name = "do" go的指针直接操作是非法的,直接编译错误

des := (*string)( unsafe.Pointer( uintptr(unsafe.Pointer(&p) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(strint(""))    )  )
*des = "change"
这样就可以直接操作go 内存了

四、通过指针 零拷贝 转换 string slice

在网络编程中,基本上都是接受的byte 切片 如果采用默认的转换方式 string 转换 会发生多次内存拷贝,因此增加了负担

完成这个任务,我们需要了解 slice 和 string 的底层数据结构:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

进行转换

//也可以直接转换 return *(*[]byte)(unsafe.Pointer(&s))
func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len:  stringHeader.Len,
        Cap:  stringHeader.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&bh))
}

//也可以直接进行转换  return *(*string)(unsafe.Pointer(&b)))
func bytes2string(b []byte) string{
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }

    return *(*string)(unsafe.Pointer(&sh))
}

两种方式的压相差10倍的数量级

//采用默认的拷贝方式进行 转换
BenchmarkByteToStringDefault-4          300000000                5.38 ns/op            0 B/op          0 allocs/op
//采用指针o拷贝进行转换
BenchmarkByteToStringPointer-4          2000000000               0.30 ns/op            0 B/op          0 allocs/op

本文由 小东@xiaodo 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。