Go语言源码分析之unsafe

1.什么是unsafe

unsafe 库让 golang 可以像C语言一样操作计算机内存,但这并不是golang推荐使用的,能不用尽量不用,就像它的名字所表达的一样,它绕过了golang的内存安全原则,是不安全的,容易使你的程序出现莫名其妙的问题,不利于程序的扩展与维护。

先简单介绍下Golang指针类型:

  1. *类型:普通指针,用于传递对象地址,不能进行指针运算。
  2. unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算。
  3. uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象,uintptr 类型的目标会被回收

unsafe.Pointer 可以和 普通指针 进行相互转换。

unsafe.Pointer 可以和 uintptr 进行相互转换。

也就是说 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。

unsafe底层源码如下:

两个类型:

// go 1.14 src/unsafe/unsafe.go
type ArbitraryType int
type Pointer *ArbitraryType

ArbitraryType是int的一个别名,在Go中对ArbitraryType赋予特殊的意义。代表一个任意Go表达式类型。

Pointer 是 int指针类型 的一个别名,在Go中可以把Pointer类型,理解成任何指针的父类型。

三个函数:

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

通过分析发现,这三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。

  1. Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。
  2. Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数。
  3. Alignof返回变量对齐字节数量

2.unsafe包的操作

2.1大小Sizeof

unsafe.Sizeof函数返回的就是uintptr类型的值,表示所占据的字节数(表达式,即值的大小):

package main

import (
"fmt"
"reflect"
"unsafe"
) func main() {
var a int32
var b = &a
fmt.Println(reflect.TypeOf(unsafe.Sizeof(a))) // uintptr
fmt.Println(unsafe.Sizeof(a)) // 4
fmt.Println(reflect.TypeOf(b).Kind()) // ptr
fmt.Println(unsafe.Sizeof(b)) // 8
}

对于 a来说,它是int32类型,在内存中占4个字节,而对于b来说,是*int32类型,即底层为ptr指针类型,在64位机下占8字节。

2.2偏移Offsetof

对于一个结构体,通过 Offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个字段的地址。

举个例子:

package main

import (
"fmt"
"unsafe"
) type user struct {
id int32
name string
age byte
} func main() {
var u = user{
id: 1,
name: "xiaobai",
age: 22,
}
fmt.Println(u)
fmt.Println(unsafe.Offsetof(u.id)) // 0 id在结构体user中的偏移量,也是结构体的地址
fmt.Println(unsafe.Offsetof(u.name)) // 8
fmt.Println(unsafe.Offsetof(u.age)) // 24 // 根据偏移量修改字段的值 比如将id字段改为1001
// 因为结构体的地址相当于第一个字段id的地址
// 直接用unsafe包自带的Pointer获取id指针
id := (*int)(unsafe.Pointer(&u))
*id = 1001 // 更加相对于id字段的偏移量获取name字段的地址并修改其内容
// 需要用到uintptr进行指针运算 然后再利用unsafe.Pointer这个媒介将uintptr类型转换成一般的指针类型*string
name := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Offsetof(u.name)))
*name = "花花" // 同理更改age字段
age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&u)) + unsafe.Offsetof(u.age)))
*age = 33 fmt.Println(u)
}

2.3对齐Alignof

要了解这个函数,你需要了解数据对齐。简单的说,它让数据结构在内存中以某种的布局存放,是该数据的读取性能能够更加的快速。

CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。

普通字段的对齐值

fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{}))

输出结果:

bool align: 1
int32 align: 4
int8 align: 1
int64 align: 8
byte align: 1
string align: 8
map align: 8

在 Go 中可以调用 unsafe.Alignof 来返回相应类型的对齐系数。通过观察输出结果,可得知基本都是 2n,最大也不会超过 8。这是因为我们的64位编译器默认对齐系数是 8,因此最大值不会超过这个数。

对齐规则

  1. 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度#pragma pack(n))或当前成员变量类型的长度unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
  2. 结构体本身,对齐值必须为编译器默认对齐长度结构体的所有成员变量类型中的最大长度取最大数的最小整数倍作为对齐值

结合以上两点,可得知若编译器默认对齐长度超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的

结构体的对齐值

下面来看一下结构体的对齐:

type part struct {
a bool // 1
b int32 //4
c int8 // 1
d int64 // 8
e byte // 1
} func main() {
var p part
fmt.Println(unsafe.Sizeof(p)) // 32
}

按照普通字段(结构体内成员变量)的对齐方式,我们可以计算得出,这个结构体的大小占1+4+1+8+1=15个字节,但是用unsafe.Sizeof计算发现part结构体32字节,是不是有点惊讶

这里面就涉及到了内存对齐,下面我们来分析一下:

成员变量 类型 偏移量 自身占用
a bool 0 1
数据对齐 - 1 3
b int32 4 4
c int8 8 1
数据对齐 - 9 7
d in64 16 8
e byte 24 1
数据对齐 - 25 7
总占用大小 - - 32
  • 对于变量a而言

    类型是bool;大小/对齐值本身为1字节;偏移量为0,占用了第0位;此时内存中表示为a

  • 对于变量b而言

    类型是int32;大小/对齐值本身为4字节;根据对齐规则一,偏移量必须为对齐值4的整数倍,故这里的偏移量为4,占用了第47位**,则**第13位用padding字节填充;此时内存中表示为a---|bbbb,(|只起到分隔作用,表示方便一些)

  • 对于变量c而言

    类型是int8;大小/对齐值本身为1字节;当前偏移量为8,无需扩充,占用了第8位;此时内存中表示为a---|bbbb|c

  • 对于变量d而言

    类型是int64;大小/对齐值本身为8字节;根据对齐规则一,偏移量必须为对齐值8的整数倍,故这理的偏移量为16,占用了第1623位**,则**第915为用padding字节填充;此时内存中表示为a---|bbbb|c---|----

  • 对于变量e而言

    类型是byte;大小/对齐值本身为1字节;当前偏移量为24,无需扩充,占用了第24位;此时内存中表示为a---|bbbb|c---|----|e

这里计算后,发现总共占用25字节,哪里又来的32字节呢? :flushed:

再让我们回顾一下对齐原则的第二点,“结构体本身,对齐值必须为编译器默认对齐长度或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值”

  1. 这里编译器默认对齐长度为8字节(64位机)

  2. 结构体中所有成员变量类型的最大长度为int64,8字节

  3. 取二者最大数的最小整数倍作为对齐值,我们算的part结构体大小为25字节,不是8字节的整数倍,故还需要填充到32字节。

综上,part结构体在内存中表示为a---|bbbb|c---|----|e----|----

扩展

让我们改变一下part结构体中字段的顺序看看(part结构体完全相同)

type part struct {
a bool // 1
c int8 // 1
e byte // 1
b int32 //4
d int64 // 8
} func main() {
var p part
fmt.Println(unsafe.Sizeof(p)) // 16
}

这时候再用unsafe.Sizeof查看会发现,part结构体的内存占用只有16字节,瞬间减少了一般的内存空间,大家可以按照前面的步骤分析一下~

这里建议在构建结构体时,按照字段大小的升序进行排序,会减少一点的内存空间。

反射包的对齐方法

反射包也有某些方法可用于计算对齐值:

unsafe.Alignof(w)等价于reflect.TypeOf(w).Align
unsafe.Alignof(w.i)等价于reflect.Typeof(w.i).FieldAlign()

总结

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

  • unsafe 包定义了 Pointer 和三个函数:

    type ArbitraryType int
    type Pointer *ArbitraryType
    func Sizeof(x ArbitraryType) uintptr
    func Offsetof(x ArbitraryType) uintptr
    func Alignof(x ArbitraryType) uintptr

    通过三个函数可以获取变量的大小、偏移、对齐等信息。

  • uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。

  • 通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。

参考:

Go语言源码分析之unsafe的更多相关文章

  1. [编织消息框架][netty源码分析]7 Unsafe 实现类NioSocketChannelUnsafe职责与实现

    Unsafe 是channel的内部接口,从书写跟命名上看是不公开给开发者使用的,直到最后实现NioSocketChannelUnsafe也没有公开出去 public interface Channe ...

  2. u-boot源码分析之C语言段

    题外话: 最近一直在学习u-boot的源代码,从代码量到代码风格,都让我认识到什么才是真正的程序.以往我所学到的C语言知识和u-boot的源代码相比,实在不值一提.说到底,机器都是0和1控制的.感觉这 ...

  3. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  4. Java并发编程笔记之Unsafe类和LockSupport类源码分析

    一.Unsafe类的源码分析 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子操作,Unsafe里面的方法都是native方法,通过使用JNI的方式来访问本地C++实现库. rt.jar ...

  5. [源码分析]读写锁ReentrantReadWriteLock

    一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...

  6. 并发-AtomicInteger源码分析—基于CAS的乐观锁实现

    AtomicInteger源码分析—基于CAS的乐观锁实现 参考: http://www.importnew.com/22078.html https://www.cnblogs.com/mantu/ ...

  7. NIO-SocketChannel源码分析

    目录 NIO-SocketChannel源码分析 目录 前言 ServerSocketChannelImpl 创建ServerSocketChannel 绑定和监听 接收 SocketChannelI ...

  8. 超详细Go语言源码目录说明

    开源项目「go home」聚焦Go语言技术栈与面试题,以协助Gopher登上更大的舞台,欢迎go home~ 导读 学习Go语言源码的第一步就是了解先了解它的目录结构,你对它的源码目录了解多少呢?今天 ...

  9. Golang源码分析之目录详解

    开源项目「go home」聚焦Go语言技术栈与面试题,以协助Gopher登上更大的舞台,欢迎go home~ 导读 学习Go语言源码的第一步就是了解先了解它的目录结构,你对它的源码目录了解多少呢? 目 ...

随机推荐

  1. UI透明欺诈

    判断是否存在的代码:   private static boolean c(Activity paramActivity)   {     List localList = ((ActivityMan ...

  2. ES6学习笔记(1)- 块级作用域

    1. var声明变量和变量提升(Hoisting)机制的问题 在JS中通过var关键字声明的变量,无论在函数作用域中亦或是全局作用域中,都会被当成当前作用域顶部的变量,和就是所谓的提升机制(Hoist ...

  3. ECharts系列 (01):地图三级下钻

    前言 最近项目中用到了地图下钻功能,GitHub上找到了一个轮子 - echarts3-chinese-map-drill-down,启动项目看了一下Demo,动画衔接的很流畅,感觉做的非常棒,膜拜大 ...

  4. linux库文件编程

    参考博文链接: https://www.cnblogs.com/guochaoxxl/p/7141447.html https://www.cnblogs.com/tuhooo/p/8757192.h ...

  5. 2019第十届蓝桥杯省赛及国赛个人总结(java-B组)

    省赛: 今年省赛的题目比18年简单的多,基本都是暴力枚举.BFS之类.还记得去年在山师考蓝桥杯,我这种辣鸡连题目都没看懂.本以为蓝桥会变得越来越难,没想到今年就被打脸了.今年省赛后面三个编程大题一个没 ...

  6. springboot源码解析-管中窥豹系列之BeanPostProcessor(十二)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  7. Elasticsearch 分页查询

    目录 前言 from + size search after scroll api 总结 参考资料 前言 我们在实际工作中,有很多分页的需求,商品分页.订单分页等,在MySQL中我们可以使用limit ...

  8. Distributed | ZooKeeper

    ZooKeeper与之前看的论文不太一样,它主要是描述了一个分布式协调服务,提供了wait-free的api,可以让用户自己设计要求更高的原语.通过Zab协议保证sever之间的一致性,同时让读请求在 ...

  9. shell脚本 5 sed和awk

    文本处理三剑客 在 Shell 下使用这些正则表达式处理文本最多的命令有下面几个工具: 命令 描述 grep 默认不支持扩展表达式,加-E 选项开启 ERE.如果不加-E 使用花括号要加转义符\{\} ...

  10. 老和尚给小和尚讲故事引发了Java设计模式:组合模式

    目录 示例 组合模式 定义 意图 主要解决问题 优缺点 安全式和透明式的组合模式 安全式的合成模式的结构 透明式的合成模式的结构 老和尚和小和尚的故事 示例 有一个绘图系统,可以描绘各种图形,假设现在 ...