深入Go底层原理,重写Redis中间件实战wumi

286 阅读4分钟

深入Go底层原理,重写Redis中间件实战下——栽

下——栽の地 止:ukoou.com/resource/1205

Go快速入门

1 Go 安装 最新版本下载地址官方下载 golang.org,当前是 1.13.6。如无法访问,可以在 studygolang.com/dl 下载

使用 Linux,可以用如下方式快速安装。

$ wget https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz
$ tar -zxvf go1.13.6.linux-amd64.tar.gz
$ sudo mv go /usr/local/
$ go version

go version go1.13.6 linux/amd64 从 Go 1.11 版本开始,Go 提供了 Go Modules 的机制,推荐设置以下环境变量,第三方包的下载将通过国内镜像,避免出现官方网址被屏蔽的问题。

$ go env -w GOPROXY=goproxy.cn,direct 或在 ~/.profile 中设置环境变量

export GOPROXY=https://goproxy.cn

2 Hello World 新建一个文件 main.go,写入

package main

import "fmt"

func main() {
	fmt.Println("Hello World!")
}

执行go run main.go 或 go run .,将会输出

$ go run .

Hello World!

3 变量与内置数据类型 3.1 变量(Variable) Go 语言是静态类型的,变量声明时必须明确变量的类型。Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。比如 java 中,声明一个整体一般写成 int a = 1,在 Go 语言中,需要这么写:

var a int // 如果没有赋值,默认为0
var a int = 1 // 声明时赋值
var a = 1 // 声明时赋值
var a = 1,因为 1int 类型的,所以赋值时,a 自动被确定为 int 类型,所以类型名可以省略不写,这种方式还有一种更简单的表达:

a := 1
msg := "Hello World!"

3.2 简单类型 空值:nil

整型类型: int(取决于操作系统), int8, int16, int32, int64, uint8, uint16, …

浮点数类型:float32, float64

字节类型:byte (等价于uint8)

字符串类型:string

布尔值类型:boolean,(true 或 false)

深入Go底层原理,重写Redis中间件实战 - Go结构体的内存布局

结构体大小 结构体是占用一块连续的内存,一个结构体变量的大小是由结构体中的字段决定。

type Foo struct {
	A int8 // 1
	B int8 // 1
	C int8 // 1
}

var f Foo
fmt.Println(unsafe.Sizeof(f))  // 3

内存对齐 但是结构体的大小又不完全由结构体的字段决定,例如:

type Bar struct {
	x int32 // 4
	y *Foo  // 8
	z bool  // 1
}

var b1 Bar
fmt.Println(unsafe.Sizeof(b1)) // 24
有的同学可能会认为结构体变量b1的内存布局如下图所示,那么问题来了,结构体变量b1的大小怎么会是24呢?

memory layout of Bar1

很显然结构体变量b1的内存布局和上图中的并不一致,实际上的布局应该如下图所示,灰色虚线的部分就是内存对齐时的填充(padding)部分。

memory layout of Bar1

Go 在编译的时候会按照一定的规则自动进行内存对齐。之所以这么设计是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。如果不进行内存对齐的话,很可能就会增加CPU访问内存的次数。例如下图中CPU想要获取b1.y字段的值可能就需要两次总线周期。

word size

因为 CPU 访问内存时,并不是逐个字节访问,而是以字(word)为单位访问。比如 64位CPU的字长(word size)为8bytes,那么CPU访问内存的单位也是8字节,每次加载的内存数据也是固定的若干字长,如8words(64bytes)、16words(128bytes)等。

对齐保证 我们上面已经知道了可以通过内置unsafe包的Sizeof函数来获取一个变量的大小,此外我们还可以通过内置unsafe包的Alignof函数来获取一个变量的对齐系数,例如:

// 结构体变量b1的对齐系数
fmt.Println(unsafe.Alignof(b1))   // 8
// b1每一个字段的对齐系数
fmt.Println(unsafe.Alignof(b1.x)) // 4:表示此字段须按4的倍数对齐
fmt.Println(unsafe.Alignof(b1.y)) // 8:表示此字段须按8的倍数对齐
fmt.Println(unsafe.Alignof(b1.z)) // 1:表示此字段须按1的倍数对齐
unsafe.Alignof()的规则如下:

对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1。 对于 struct 类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值。 对于 array 类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐倍数。 在了解了上面的规则之后,我们就可以通过调整结构体 Bar 中字段的顺序来减少其大小:

type Bar2 struct {
	x int32 // 4
	z bool  // 1
	y *Foo  // 8
}

var b2 Bar2
fmt.Println(unsafe.Sizeof(b2)) // 16
此时结构体 Bar2 变量的内存布局示意图如下:

memory layout of Bar2

或者将字段顺序调整为以下顺序。

type Bar3 struct {
	z bool  // 1
	x int32 // 4
	y *Foo  // 8
}

var b3 Bar3
fmt.Println(unsafe.Sizeof(b3)) // 16