golang of unsafe programming

wedge

Unsafe programming? With its golang I did not find what's unsafe ah, but golang garbage collection, we do not need to manage memory. When you hear the words unsafe program, it is the only way to think of pointers, only the pointer it may lead to insecurity. We know that there is golang pointer, but the pointer does not golang language like C pointers in the same operation can be carried out, so golang the pointer provides both convenience pointers, but also to ensure safety. But in golang can be called by a unsafepackage let pointer to break out, to perform operations, once with the bad can cause very serious problems, but good use in some scenes can bring great convenience, so we He said it was not safe programming. But even so, we can still use, but also a large number of internal golang unsafe use of this package.

The pointer golang

Although there is no C golang pointer pointers less powerful, but able to get the address of a variable, and can change the address stored value, I personally think that is enough.

package main

import "fmt"

func pass_by_value(num int){
    num = 3
}

func pass_by_pointer(num *int){
    *num = 3
    num = nil
}

func main() {

    num := 1
    pass_by_value(num)
    fmt.Println("传递值:", num)  //传递值: 1
    pass_by_pointer(&num)
    fmt.Println("传递指针:", num)  //传递指针: 3

We know golang transfer mode function is passed by value, whatever passes are a copy of it. And inside a function parameter What is the matter, here we shape function parameters is not called num, called the other does not matter.

pass_by_valueReceiving an integer, when we pass the num, num value of a copy will pass out in, this time function inside no matter what modifications do not affect the outside num, because it is not a thing.

pass_by_pointerReceive a pointer, then pass &numthe time, still will copy pointer, we say golang only value is passed, then passing a pointer is a pointer to a copy. Because it is a copy, so the two do not have any relationship, but they are stored in the address is the same, but it pass_by_pointeris inside the num this * inttype of variable and we pass &numis not related. Since the address stored is the same, so the two operations are the same piece of memory, so * num = 3after that will affect the outside num, but also a copy of the pointer, the function of which num = nilit does not matter with the outside.

So golang change the value of the pointer in the memory of the time, and C is the same, but it is, and C pointer compared, but also weakened a lot.

A weakening: golang the pointer can not perform mathematical operations.

package main

func main() {
    arr := [...]int{1, 2, 3}
    //获取数组首元素的地址
    p := &arr[0]
    //如果是C中,我们可以通过p++,获取当前元素的下一个元素的地址
    //但是在golang中不行,p++这种做法是不会通过编译的
    p++
}

//invalid operation: p++ (non-numeric type *int)

Weakening two: golang pointer can not be different types of assignment or transformation.

package main

func main() {
    var a int
    var b *float64
    b = &a
    
    //cannot use &a (type *int) as type *float64 in assignment
}

Weakening three: golang different types of indicators can not be compared.

Only be compared at the same type or two pointers can be transformed into each other, the comparator two addresses are the same. In addition, all pointers are available through ==and !=comparing and nil, to determine whether the pointer is NULL.

package main

import "fmt"

func main() {
    var a = 1
    var b = 1
    //值相同,为true
    fmt.Println(a == b)  // true
    //但是地址不一样,为false
    fmt.Println(&a == &b)  // false

    //但是不同类型的指针是无法比较的,别说指针了,就是值也是无法比较的
    //golang对于类型的要求是非常严格的
    var c float64
    /*
    fmt.Println(&a == &c)

    //invalid operation: &a == &c (mismatched types *int and *float64)
    */

    //但是它们都可以和nil进行比较
    fmt.Println(&c == nil, &c != nil)  // false true

    var m map[int]int
    //map是指针类型,对于slice、channel、map这种数据结构,我们一般使用make创建,会申请对应的内存,然后返回指针
    //如果只是这种声明的话,那么是没有所谓的0值的,直接会返回一个空指针
    //别看打印m的结果是map[],但是这只是表示m是一个map类型的数据,但是它还没有被分配内存,所以结果nil
    fmt.Println(m == nil, m) // true map[]
}

unsafe: unsafe programming

What is unsafe

By the time we saw above golang the pointer type is actually safe, because golang is very strict on the type of detection, so you enjoy the convenience brought by the pointer, and gave pointers put a lot of constraints to ensure safety. But the need to ensure the safety of the expense of efficiency, if you write a program that can guarantee security, then you can use unsafe pointers golang, thereby bypassing the detection type system, allowing you to run more programs fast.

If it is a higher-order golang programmer, then how can not unsafe bag? It can detect golang bypass type system, direct memory access, increased efficiency. golang the many restrictions, such as not operating structure is not exported members and so on, but with unsafe package, you can directly break through these limits. So this package called unsafe, we call for the use of unsafe unsafe program, because it is dangerous, the official is not recommended, because of this estimate also designed that name it. But you are in a lot of use of ground floor, then why can not we use.

The principle unsafe

We just mentioned the unsafe pointer, then we take a look at what is unsafe pointer.

package unsafe
type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

The following is only a unsafe.go unsafe package file, the file inside to remove the comments on the above six lines of code, yes you read right. Of course, functions certainly are embedded inside the compiler, as to how to achieve it we would not care to see how to use on the line. Let's take a look at these two lines:

type ArbitraryType int
type Pointer *ArbitraryType

Arbitrary an arbitrary, so the Pointer may be any type of pointer, such as: *int、*string、*float64and the like. That can be any type of pointer passed to it.

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a int
    var s string
    var f float64
    fmt.Println(unsafe.Pointer(&a))  //0xc000062080
    fmt.Println(unsafe.Pointer(&s))  //0xc00004e1c0
    fmt.Println(unsafe.Pointer(&f))  //0xc000062088
}

unsafe.Pointer()是有返回值的,返回的当然也是一个指针,但是这个指针同样是无法进行运算的。如果无法运算,那么我们还是无法实现通过指针自增的方式,访问数组的下一个元素啊。别急,所以还有一个整数类型:uintptr,我们unsafe.Pointer()是可以和uintptr互相转化的,而这个uintptr是可以运算的,并且它还足够大。因此我们目前看到了两个功能:

1.任何类型的指针都可以和unsafe.Pointer相互转化

2.unsafe.Pointer可以和uintptr互相转化

但是需要注意的是,uintptr并没有指针的含义,所以它指向的内存是会被回收的,而unsafe.Pointer有指针的含义,可以确保其指向的对象不会被回收。

使用unsafe带你突破限制

像C语言一样访问数组或切片

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    //这里把数字弄成没有规律的,就不用1 2 3 4 5 6了
    var arr = []int{177, 123, 3, 221, 5, 1211}

    //获取第二个元素的指针,我们也不从头获取
    //因为从中间获取都可以的话,那么从头获取肯定可以
    //然后传给unsafe.Pointer(),将*int转化成Pointer类型
    pointer := unsafe.Pointer(&arr[1])

    //注意了:下面要将Pointer转成uintptr,因为Pointer是不能运算的
    u_pointer := uintptr(pointer)
    //此时的u_pointer就相当于C中的指针了,但是还有一点不同
    //C中的指针直接++即可,指针会自动移到到下一个元素的位置
    //而golang中的uintptr相当于一个整型,我们不能++,而是需要+8,因为一个int占8个字节,所以golang中需要加上元素所占的大小
    //所以我们发现C中的+n是从当前元素开始,移动n个元素,不管元素是什么类型。
    //但是golang的+n是移动n个字节。
    //所以C中的指针+2 等于 golang中uintptr + 2 * (元素类型所占的字节)
    u_pointer += 16 //移动两个元素

    //然后再转回来,要先转成Pointer,再转成对应的指针类型
    pointer = unsafe.Pointer(u_pointer)

    //这个pointer我们通过&arr[1]也就是*int类型的指针得到了,那么结果也要转成*int
    int_pointer := (*int)(pointer)
    // 打印了221,结果是正确的
    fmt.Println(*int_pointer) // 221

    //这里也可以转成*string,即便我们的pointer是通过*int得到的
    //因为Pointer可以是任何指针类型
    string_pointer := (*string)(pointer)
    //也是可以打印的,但是通过*来访问内存的话就会报错,panic: runtime error: invalid memory address or nil pointer dereference
    fmt.Println(string_pointer) //0xc00008c048

    //这里我们再加上1,不加8,那么会出现什么后果
    //我们知道再加上8,就会访问221后面的5
    u_pointer += 1
    fmt.Println(*(*int)(unsafe.Pointer(u_pointer))) // 360287970189639680
    //我们看到此时得到的是一个我们也不知道从哪里来的脏数据,所以一定要加上对应的字节
}

所以我们发现unsafe.Pointer就类似于一座桥,*T通过Pointer转成uintptr,然后进行指针运算,运算完成之后,再通过Pointer转回*T,此时的*T就是我们想要的了。

指针访问结构体

我们知道结构体是可以有字段的,那么我们也可以把结构体想象成数组,字段想象成数组的元素。

package main

import (
    "fmt"
    "unsafe"
)

type score struct {
    math    int
    english int
    history int
}

func main() {
    s := score{math: 90, english: 92, history: 85}

    //我们看到通过unsafe.Pointer的方式,获取结构体的指针,可以直接转换为结构体第一个字段的指针
    p := unsafe.Pointer(&s)
    fmt.Println(*(*int)(p))
    //math字段是一个整型,那么p转为uintptr之后加上8,就可以转换成第二个字段的指针
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 8))) //92
    //同理加上16就是第三个
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 16))) //85
    
    //这里显然就是一个乱七八糟的值了
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 29))) //92
}

我们知道切片是一个结构体,有三个字段,分别是指向底层数组的指针,以及大小和容量。

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    //申请大小为5,容量为10的切片
    s := make([]int, 5, 10)

    //第一个元素显然是指向底层数组的指针,大小也是8个字节。我们来看第二个和第三个
    //虽然有些长,但是从内往外的话,还是很好看懂的。如果不习惯的话可以写成一行
    fmt.Printf("长度:%d\n", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + 8)))  //长度:5
    fmt.Printf("容量:%d\n", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + 16)))  //容量:10
}

我们看到unsafe包还是很强大的,之所以叫unsafe是因为如果用不好后果会很严重。但是如果能正确使用的话,能够做到很多之前做不到的事情。

获取对象的大小

我们目前可以使用unsafe做很多事情了,但是还不够,我们看到unsafe这个包除了给我们提供了Pointer这个类型之外,还给我们提供了三个函数。

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

这三个函数返回的都是uintptr类型,这个类型你就看成是整型即可,它是可以和数字进行运算的,可以转为int。我们先来看看Sizeof

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    a := 123
    b := "h"
    c := []int{1, 2, 3}
    fmt.Println(unsafe.Sizeof(a)) //8
    //关于字符串为什么是16
    //golang中的字符串在底层是一个结构体,这个结构体有两个元素
    //一个是字符串的首地址,一个是字符串的长度
    //所以是16,因为golang的字符串底层对应的是一个字符数组
    fmt.Println(unsafe.Sizeof(b)) //16

    //切片我们说过底层也是一个结构体,有三个字段,指向底层数组的指针、大小、容量,所以是24个字节
    fmt.Println(unsafe.Sizeof(c)) //24
}

golang中的Sizeof和C中的sizeof还是比较类似的,但是golang中的Sizeof不能接收类型本身, 比如你可以传入一个123,但是你不能传入一个int,这是不行的。至于获取一个字符串的大小结果是16,这个是由golang底层字符串的结构决定的。对了,当我们获取一个结构体的大小的时候,我们看到貌似是将结构体中的每一个字段的值的大小进行相加,至少目前看来是这样的。

获取结构体成员的偏移量

对于一个结构来说,可以使用Offsetof来获取结构体成员的偏移量,进而获取成员的地址,从而改变内存的值。这里提一句:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。但是你懂的,我们不可能直接通过对结构体的地址加上*来获取第一个成员的值,只能通过unsafe.Pointer转化,然后再转化成对应类型的指针,才能获取。

package main

import (
    "fmt"
    "unsafe"
)

type girl struct {
    //对应的字节数
    name string  // 16
    age int  // 8
    gender string  //16
    hobby []string //24
}

func main() {
    g := girl{"mashiro", 17, "f", []string{"画画", "开车"}}
    //首先这几步操作应该不需要解释了,直接想象成数组即可
    fmt.Println(*(*string)(unsafe.Pointer(&g))) // mashiro
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16))) // 17
    fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8)))  // f
    fmt.Println(*(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)))  // [画画 开车]

    //我们看到即使对具有不同字段类型的结构体,依旧可以自由操作,只要搞清楚每个字段的大小即可
    *(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)) =
        append(*(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)), "料理")
    fmt.Println(g) // {mashiro 17 f [画画 开车 料理]}

    //我们看到,即便操作起来没有问题,但是有一个缺陷,就是我们必须要事先计算好每一个字段占多少个字节,尽管我们可以通过unsafe.Sizeof可以很方便的计算。
    //但是有没有不用计算的方法呢?显然有,就是我们说的Offsetof。但是这个Offsetof又有点特殊,它表示的是偏移量
    //比如我想访问hobby这个字段,那么这么做可以,直接以&g为起点,此时偏移量为0,加上unsafe.Offsetof(g.hobby),直接偏移到hobby
    fmt.Println(*(*[]string)(unsafe.Pointer(   uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.hobby)     ))) // [画画 开车 料理]
    
    //其余的也是一样,获取哪个字段,直接传入哪个字段即可,个人觉得这个Offsetof比自己计算要方便一些
    fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.name)))) // mashiro
    fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.age)))) // 17
    fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.gender)))) // f
}

而且我们知道,如果在别的包里面,结构体里的字段没有大写,那么是无法导出的,然鹅即便如此,我们依旧可以通过unsafe包绕过这些限制。

package hahaha

type OverWatch struct {
    name   string
    age    int
    Gender string
    weapon string
}

这些字段有三个没有大写,理论上是无法导出的,因为golang会进行检测,但是使用unsafe就可以绕过这些检测。

package main

import (
    "fmt"
    "hahaha"
    "unsafe"
)

func main() {
    hero := new(hahaha.OverWatch)
    //设置name
    *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)))) = "麦克雷"
    //设置age,因为Offsetof需要指定访问的字段,而字段又没有被导出,所以无法通过Offsetof的方式
    //因此需要手动计算对应类型的偏移量,因为是string类型,所以加上一个Sizeof(""),当然也可以手动填上16
    *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)) + unsafe.Sizeof(""))) = 37
    //这个就可以直接设置了,因为被导出了
    hero.Gender = "男"
    //老规矩,这里是两个string加上一个int的大小
    *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)) + unsafe.Sizeof("") * 2 + unsafe.Sizeof(123))) = "维和者"
    fmt.Println(*hero)  // {麦克雷 37 男 维和者}
}

字段对齐

通过unsafe.Alignof可以获取字段的对齐值,不过这里用不上,可以自己尝试一下,上面的用的比较多。

参考于:https://qcrao.com/2019/06/03/dive-into-go-unsafe/,用原文作者的话来说就是:

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。关于 unsafe 包,我们更多关注它的用法。

顺便说一句,unsafe 包用多了之后,也不觉得它的名字有多么地不“美观”了。相反,因为使用了官方并不提倡的东西,反而觉得有点酷炫。这就是叛逆的感觉吧。个人非常赞同,觉得真的很酷。

Guess you like

Origin www.cnblogs.com/traditional/p/12210822.html