season_01_episode_12_part1

底层编程

Go语言的设计包含了诸多安全策略,限制了可能导致程序运行出现错误的用法。编译时类型
检查检查可以发现大多数类型不匹配的操作,例如两个字符串做减法的错误。字符串、
map、slice和chan等所有的内置类型,都有严格的类型转换规则。

对于无法静态检测到的错误,例如数组访问越界或使用空指针,运行时动态检测可以保证程
序在遇到问题的时候立即终止并打印相关的错误信息。自动内存管理(垃圾内存自动回收)
可以消除大部分野指针和内存泄漏相关的问题。

Go语言的实现刻意隐藏了很多底层细节。我们无法知道一个结构体真实的内存布局,也无法
获取一个运行时函数对应的机器码,也无法知道当前的goroutine是运行在哪个操作系统线程
之上。事实上,Go语言的调度器会自己决定是否需要将某个goroutine从一个操作系统线程转
移到另一个操作系统线程。一个指向变量的指针也并没有展示变量真实的地址。因为垃圾回
收器可能会根据需要移动变量的内存位置,当然变量对应的地址也会被自动更新。

总的来说,Go语言的这些特性使得Go程序相比较低级的C语言来说更容易预测和理解,程序
也不容易崩溃。通过隐藏底层的实现细节,也使得Go语言编写的程序具有高度的可移植性,
因为语言的语义在很大程度上是独立于任何编译器实现、操作系统和CPU系统结构的(当然
也不是完全绝对独立:例如int等类型就依赖于CPU机器字的大小,某些表达式求值的具体顺
序,还有编译器实现的一些额外的限制等)。
有时候我们可能会放弃使用部分语言特性而优先选择更好具有更好性能的方法,例如需要与
其他语言编写的库互操作,或者用纯Go语言无法实现的某些函数。

我们将展示如何使用unsafe包来摆脱Go语言规则带来的限制,讲述如何创建C语言
函数库的绑定,以及如何进行系统调用。

要注意的是,unsafe包是一个采用特殊方式实现的包。虽然它可以和普通包一样的导入和使
用,但它实际上是由编译器实现的。它提供了一些访问语言内部特性的方法,特别是内存布
局相关的细节。将这些特性封装到一个独立的包中,是为在极少数情况下需要使用的时候,
同时引起人们的注意。此外,有一些环境因为安全的因素可能限制这个包的使用。

不过unsafe包被广泛地用于比较低级的包, 例如runtime、os、syscall还有net包等,因为它们
需要和操作系统密切配合,但是对于普通的程序一般是不需要使用unsafe包的。

unsafe.Sizeof, Alignof 和 Offsetof

unsafe.Sizeof函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式,但是它
并不会对表达式进行求值。一个Sizeof函数调用是一个对应uintptr类型的常量表达式,因此返
回的结果可以用作数组类型的长度大小,或者用作计算其他的常量。
import "unsafe"
fmt.Println(unsafe.Sizeof(float64(0))) // "8"

Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符
串长度部分,但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定
的大小,尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性,引用类型
或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节。
计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。例如2字节大小的
int16类型的变量地址应该是偶数,一个4字节大小的rune类型变量的地址应该是4的倍数,一
个8字节大小的float64、uint64或64-bit指针类型变量的地址应该是8字节对齐的。但是对于再
大的地址对齐倍数则是不需要的,即使是complex128等较大的数据类型最多也只是8字节对齐。

由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小
的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存
空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐。

Go语言的规范并没有要求一个字段的声明顺序和内存中的顺序是一致的,所以理论上一个编
译器可以随意地重新排列每个字段的内存位置,随然在写作本书的时候编译器还没有这么
做。下面的三个结构体虽然有着相同的字段,但是第一种写法比另外的两个需要多50%的内
存。
// 64-bit 32-bit
struct{ bool; float64; int16 } // 3 words 4words
struct{ float64; int16; bool } // 2 words 3words
struct{ bool; int16; float64 } // 2 words 3words

关于内存地址对齐算法的细节超出了本书的范围,也不是每一个结构体都需要担心这个问
题,不过有效的包装可以使数据结构更加紧凑。内存使用率和性能都可能会受益。

unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数. 和 Sizeof 类似, Alignof 也是返回
一个常量表达式, 对应一个常量. 通常情况下布尔和数字类型需要对齐到它们本身的大小(最多
8个字节), 其它的类型对齐到机器字大小.

unsafe.Offsetof 函数的参数必须是一个字段 x.f , 然后返回 f 字段相对于 x 起始地址的
偏移量, 包括可能的空洞.

显示了一个结构体变量 x 以及其在32位和64位机器上的典型的内存. 灰色区域是空洞.
var x struct {
a bool
b int16
c []int
}

32位系统:
Sizeof(x) = 16 Alignof(x) = 4
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 12 Alignof(x.c) = 4 Offsetof(x.c) = 4

64位系统:
Sizeof(x) = 32 Alignof(x) = 8
Sizeof(x.a) = 1 Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2 Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24 Alignof(x.c) = 8 Offsetof(x.c) = 8

虽然这几个函数在不安全的unsafe包,但是这几个函数调用并不是真的不安全,特别在需要
优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。

上一篇:自定义圆角和园边的实现


下一篇:Go语言与数据库开发:01-11