超级苦工
阅读 213
《快学 Go 语言》第 14 课 —— 魔术变性指针

本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包提供的功能,直接操纵指定内存地址的内存。有了 unsafe 包,我们就可以洞悉 Go 语言内置数据结构的内部细节。

unsafe.Pointer

Pointer 代表着变量的内存地址,可以将任意变量的地址转换成 Pointer 类型,也可以将 Pointer 类型转换成任意的指针类型,它是不同指针类型之间互转的中间类型。Pointer 本身也是一个整型的值。

type Pointer int

在 Go 语言里不同类型之间的转换是要受限的。普通的基础变量转换成不同的类型需要进行内存浅拷贝,而指针变量类型之间是禁止直接转换的。要打破这个限制,unsafe.Pointer 就可以派上用场,它允许任意指针类型的互转。

指针的加减运算

Pointer 虽然是整型的,但是编译器禁止它直接进行加减运算。如果要进行运算,需要将 Pointer 类型转换 uintptr 类型进行加减,然后再将 uintptr 转换成 Pointer 类型。uintptr 其实也是一个整型。

type uintptr int

下面让我们就来尝试一下刚刚学到的魔法

package main

import "fmt"
import "unsafe"

type Rect struct {
    Width int
    Height int
}

func main() {
    var r = Rect {50, 50}
    // *Rect => Pointer => *int => int
    var width = *(*int)(unsafe.Pointer(&r))
    // *Rect => Pointer => uintptr => Pointer => *int => int
    var height = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8)))
    fmt.Println(width, height)
}

------
50 50

上面的代码是用 unsafe 包来读取结构体的内容,形式上比较繁琐,注意看代码中的注释,读者需要稍微转一转脑袋来理解一下上面的代码。接下来我们再尝试修改结构体的值

package main

import "fmt"
import "unsafe"

type Rect struct {
    Width int
    Height int
}

func main() {
    var r = Rect {50, 50}
    // var pw *int
    var pw = (*int)(unsafe.Pointer(&r))
    // var ph *int
    var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + uintptr(8)))
    *pw = 100
    *ph = 100
    fmt.Println(r.Width, r.Height)
}

--------
100 100

代码中的 uintptr(8) 很不优雅,可以使用 unsafe 提供了 Offsetof 方法来替换它,它可以直接得到字段在结构体内的偏移量

var ph = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&r)) + unsafe.Offsetof(r.Height))

你也许会抱怨为啥指针操作这么繁琐,不能简单一点么?Go 语言的设计者故意这样设计的,因为指针操作非常的不安全,所以它要给用户设置障碍。

探索切片内部结构

在切片小节,我们知道了切片分为切片头和内部数组两部分,下面我们使用 unsafe 包来验证一下切片的内部数据结构,看看它和我们预期的是否一样。

package main

import "fmt"
import "unsafe"

func main() {
    // head = {address, 10, 10}
    // body = [1,2,3,4,5,6,7,8,9,10]
    var s = []int{1,2,3,4,5,6,7,8,9,10}
    var address = (**[10]int)(unsafe.Pointer(&s))
    var len = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
    var cap = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
    fmt.Println(address, *len, *cap)
    var body = **address
    for i:=0; i< len(body); i++ {
        fmt.Printf("%d ", body[i])
    }
}

------------------
0xc42000a080 10 10
1 2 3 4 5 6 7 8 9 10

输出的结果正是我们锁期望的,不过读者需要仔细思考一下 address 为什么是二级指针变量。

字符串与字节切片的高效转换

在字符串小节我们提到字节切片和字符串之间的转换需要复制内存,如果字符串或者字节切片的长度较大,转换起来会有较高的成本。下面我们通过 unsafe 包提供另一种高效的转换方法,让转换前后的字符串和字节切片共享内部存储。


字符串和字节切片的不同点在于头部,字符串的头部 2 个 int 字节,切片的头部 3 个 int 字节


package main

import "fmt"
import "unsafe"

func main() {
    fmt.Println(bytes2str(str2bytes("hello")))
}

func str2bytes(s string) []byte {
    var strhead = *(*[2]int)(unsafe.Pointer(&s))
    var slicehead [3]int
    slicehead[0] = strhead[0]
    slicehead[1] = strhead[1]
    slicehead[2] = strhead[1]
    return *(*[]byte)(unsafe.Pointer(&slicehead))
}

func bytes2str(bs []byte) string {
    return *(*string)(unsafe.Pointer(&bs))
}

-----
hello

切记通过这种形式转换而成的字节切片千万不可以修改,因为它的底层字节数组是共享的,修改会破坏字符串的只读规则。其次使用这种形式得到的字符串或者切片只可以用作临时的局部变量,因为被共享的字节数组随时可能会被回收,原字符串或者字节切片的内存由于不再被引用,让垃圾回收器解决掉了。

深入接口变量的赋值

在接口变量的小节,有一个问题还悬而未决,那就是接口变量在赋值时发生了什么?


通过 unsafe 包,我们就可以看清里面的细节,下面我们将一个结构体变量赋值给接口变量,看看修改结构体的内存会不会影响到接口变量的数据内存

package main

import "fmt"
import "unsafe"

type Rect struct {
    Width int
    Height int
}

func main() {
    var r = Rect{50, 50}
    // {typeptr, dataptr}
    var s interface{} = r

    var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
    // var dataptr *Rect
    var sdataptr = sptrs[1]
    fmt.Println(sdataptr.Width, sdataptr.Height)

    // 修改原对象,看看接口指向的对象是否受到影响
    r.Width = 100
    fmt.Println(sdataptr.Width, sdataptr.Height)
}

-------
50 50
50 50

从输出中可以得出结论,将结构体变量赋值给接口变量,结构体内存会被复制。那如果是两个接口变量之间的赋值呢,会不会同样也需要复制指向的数据呢?

package main

import "fmt"
import "unsafe"

type Rect struct {
    Width int
    Height int
}

func main() {
    // {typeptr, dataptr}
    var s interface{} = Rect{50, 50}
    var r = s

    var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
    var rdataptr = rptrs[1]
    var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
    var sdataptr = sptrs[1]

    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)

    // 修改原对象
    sdataptr.Width = 100
    // 再对比一下原对象和目标对象
    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)
}

-----------
50 50
50 50
100 50
100 50

从输出中可以发现赋值前后两个接口变量共享了数据内存,没有发生数据的复制。接下来我们再引入第 3 个问题,不同类型的接口变量赋值会不会发生复制?

package main

import "fmt"
import "unsafe"

type Areable interface {
    Area() int
}

type Rect struct {
    Width int
    Height int
}

func (r Rect) Area() int {
    return r.Width * r.Height
}

func main() {
    // {typeptr, dataptr}
    var s Areable = Rect{50, 50}
    var r interface{} = s

    var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
    var rdataptr = rptrs[1]
    var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
    var sdataptr = sptrs[1]

    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)

    // 修改原对象
    sdataptr.Width = 100
    // 再对比一下原对象和目标对象
    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)
}

------
50 50
50 50
100 50
100 50

结果是不同类型接口之间赋值指向的数据对象还是共享的。接下来我们再引入第 4 个 问题,接口类型之间在造型时是否会发生内存的复制。

package main

import "fmt"
import "unsafe"

type Areable interface {
    Area() int
}

type Rect struct {
    Width int
    Height int
}

func (r Rect) Area() int {
    return r.Width * r.Height
}

func main() {
    // {typeptr, dataptr}
    var s interface{} = Rect{50, 50}
    var r Areable = s.(Areable)

    var rptrs = *(*[2]*Rect)(unsafe.Pointer(&r))
    var rdataptr = rptrs[1]
    var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
    var sdataptr = sptrs[1]

    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)

    // 修改原对象
    sdataptr.Width = 100
    // 再对比一下原对象和目标对象
    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(rdataptr.Width, rdataptr.Height)
}

------
50 50
50 50
100 50
100 50

答案是不同接口类型之间造型数据还是共享的。最后再提一个问题,将接口类型造型成结构体类型,是否会发生内存复制?

package main

import "fmt"
import "unsafe"

type Areable interface {
    Area() int
}

type Rect struct {
    Width int
    Height int
}

func (r Rect) Area() int {
    return r.Width * r.Height
}

func main() {
    // {typeptr, dataptr}
    var s interface{} = Rect{50, 50}
    var r Rect = s.(Rect)

    var sptrs = *(*[2]*Rect)(unsafe.Pointer(&s))
    var sdataptr = sptrs[1]

    // 修改原对象
    sdataptr.Width = 100
    // 再对比一下原对象和目标对象
    fmt.Println(sdataptr.Width, sdataptr.Height)
    fmt.Println(r.Width, r.Height)
}

答案是将接口造型成结构体类型,内存会发生复制,它们之间的数据不会共享。

从上面 5 个 问题,我们可以得出结论,接口类型和结构体类型似乎是两个不同的世界。只有接口类型之间的赋值和转换会共享数据,其它情况都会复制数据,其它情况包括结构体之间的赋值,结构体转接口,接口转结构体。不同接口变量之间的转换本质上只是调整了接口变量内部的类型指针,数据指针并不会发生改变。

通过 unsafe 包我们还可以分析很多细节,在高级内容部分,我们将会频繁使用这个工具。

阅读更多精品文章,微信扫一扫上面的二维码关注公众号「码洞」

关注下面的标签,发现更多相似文章
评论
相关推荐
国内知名Wchat团队荣誉出品顶级IM通讯聊天系统

国内知名Wchat团队荣誉出品顶级IM通讯聊天系统 团队言语在先: 想低价购买者勿扰(团队是在国内首屈一指的通信公司离职后组建,低价购买者/代码代码贩子者/同行勿扰/基础版本低于30w勿扰) 。想购买...

test

&lt;div style=&quot;margin-top:20px&quot;&gt; &lt;el-tabs v-model=&quot;editableTabsValue&quot; tab-...

测试标题

chenyuncaic...

我的测试文章

这里我的标题 非 这里是被信息,我希望得到备注 我的第二个标题...

f'g'h'g'h'f'g'h'g'f'h

发布告白夫妇...

nestjs+vue+ts打造一个酷炫的星空聊天室

简介😛 闲暇时间想做一个聊天室来巩固前端技能,于是在2020年6月24号就开始了阿童木聊天室的开发之旅。😈 项目采用全 typescript 开发,这是为了以后的功能迭代打基础。当然,我本身也是很...

好几个放假

东方红格当然多层次发鬼地方...

《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor

来源: 知乎 原文: 《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor 到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构...

dddd

...

《快学 Go 语言》第 15 课 —— 反射

来源: 知乎 原文: 《快学 Go 语言》第 15 课 —— 反射 反射是 Go 语言学习的一个难点,但也是非常重要的一个知识点。反射是洞悉 Go 语言类型系统设计的法宝,Go 语言的 ORM 库离不...

hello

*《快学 Go 语言》第 14 课 —— 魔术变性指针来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包...

《快学 Go 语言》第 14 课 —— 魔术变性指针

来源: 知乎 原文: 《快学 Go 语言》第 14 课 —— 魔术变性指针 本节我们要学习一些 Go 语言的魔法功能,通过内置的 unsafe 包提供的功能,直接操纵指定内存地址的内存。有了 unsa...

《快学 Go 语言》第 13 课 —— 并发与安全

来源: 知乎 原文: 《快学 Go 语言》第 13 课 —— 并发与安全 上一节我们提到并发编程不同的协程共享数据的方式除了通道之外还有就是共享变量。虽然 Go 语言官方推荐使用通道的方式来共享数据,...

《快学 Go 语言》第 12 课 —— 通道

来源: 知乎 原文: 《快学 Go 语言》第 12 课 —— 通道 不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列...

《快学 Go 语言》第 11 课 —— 千军万马跑协程

来源: 知乎 原文: 《快学 Go 语言》第 11 课 —— 千军万马跑协程 协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用...

测试发布

测试文章发布...

《快学 Go 语言》第 10 课 —— 错误与异常

来源: 知乎 原文: 《快学 Go 语言》第 10 课 —— 错误与异常 Go 语言的异常处理语法绝对是独树一帜,在我见过的诸多高级语言中,Go 语言的错误处理形式就是一朵奇葩。一方面它鼓励你使用 C...

《快学 Go 语言》第 9 课 —— 接口

来源: 知乎 原文: 《快学 Go 语言》第 9 课 —— 接口 接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪...

《快学 Go 语言》第 8 课 —— 结构体

来源: 知乎 原文: 《快学 Go 语言》第 8 课 —— 结构体 本节我们要开讲 Go 语言在数据结构上最重要的概念 —— 结构体。如果说 Go 语言的基础类型是原子,那么结构体就是分子。分子是原子...

《快学 Go 语言》第 7 课 —— 字符串

来源: 知乎 原文: 《快学 Go 语言》第 7 课 —— 字符串 字符串通常有两种设计,一种是「字符」串,一种是「字节」串。「字符」串中的每个字都是定长的,而「字节」串中每个字是不定长的。Go 语言...