《快学 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 包来读取结构体的内容,形式上比较繁琐,注意看代码中的注释,读者须要稍微转一转脑壳来理解一下上面的代码。接下来咱们再尝试修改结构体的值ui

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 方法来替换它,它能够直接获得字段在结构体内的偏移量spa

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

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

探索切片内部结构

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

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 包咱们还能够分析不少细节,在高级内容部分,咱们将会频繁使用这个工具。

阅读《快学 Go 语言》更多章节,长按图片识别二维码关注公众号「码洞」

相关文章
相关标签/搜索