Go 进阶
一些总结
-
哪些种类型的值可以有间接底层部分?
在 Go 中,下列种类的类型的值可以有间接底层部分:
- 字符串类型 string
- 函数类型 func
- 切片类型 slice
- 映射类型 map
- 通道类型 channel
- 接口类型 interface
注意:此答案基于标准编译器的实现。事实上,函数类型的值是否有间接底层部分是难以证明的。 另外,字符串和接口类型的值在逻辑上应该被认为是不含间接底层部分。
-
哪些种类型的值可以用做内置
len
(以及cap
、close
、delete
和make
)函数调用的实参?len cap close delete make string 可以 array / array ptr 可以 可以 slice 可以 可以 可以 map 可以 可以 可以 chan 可以 可以 可以 可以 可以被用做内置函数
len
调用的参数的值的类型都可以被称为(广义上的)容器类型。 这些容器类型的值都可以跟在for-range
循环的range
关键字后。 -
各种容器类型比较
类型 容器值是否支持添加新的元素? 容器值中的元素是否可以被替换? 容器值中的元素是否可寻址? 访问容器值元素是否会更改容器长度? 容器值是否可以有间接底层部分? 字符串 string 否 否 否 否 是 (1) 数组 array 否 是 (2) 是 (2) 否 否 切片 slice 否 (3) 是 是 否 是 映射 map 是 是 否 否 是 通道 channel 是 (4) 否 否 是 是 (1) 对于标准编译器和运行时来说。
(2) 对于可寻址的数组值来说。
(3) 一般说来,一个切片的长度只能通过将另外一个切片赋值给它来被整体替换修改,这里我们不视这种情况为“添加新的元素”。 其实,切片的长度也可以通过调用
reflect.SetLen
来单独修改。增加切片的长度可以看作是一种变相的向切片添加元素。 但reflect.SetLen
函数的效率很低,因此很少使用。(4) 对于带缓冲并且缓冲未满的通道来说。
哪些种类型的值可以用组合字面量(T{...}
)表示?
下面在四种类型的值(除了切片和映射类型的零值)可以用组合字面量表示。
类型(T ) | T{} 是类型T 的零值? |
---|---|
结构体类型 | 是 |
数组类型 | 是 |
切片类型 | 否 (零值用nil 表示) |
映射类型 | 否 (零值用nil 表示) |
各种类型的尺寸
详见值复制成本一文。
哪些种类型的零值使用预声明的 nil
标识符表示?
下面这些类型的零值可以用预声明的 nil
标识符表示。
类型(T ) | T(nil) 的尺寸 |
---|---|
指针 | 1 word |
切片 | 3 words |
映射 | 1 word |
通道 | 1 word |
函数 | 1 word |
接口 | 2 words |
上表列出的尺寸为标准编译器的结果。 一个 word(原生字)在 32 位的架构中为 4 个字节,在 64 位的架构中为 8 个字节。 一个 Go 值的间接底层部分未统计在尺寸中。
一个类型的零值的尺寸和其它非零值的尺寸是一致的。
我们可以为什么样的类型声明方法?
详见方法一文。
什么样的类型可以被内嵌在结构体类型中?
详见类型内嵌一文。
哪些函数调用将在编译时刻被估值?
如果一个函数调用在编译时刻被估值,则估值结果为一个常量。
函数 | 返回类型 | 其调用是否总是在编译时刻估值? |
---|---|---|
unsafe.Sizeof | uintptr | 是 |
unsafe.Alignof | ||
unsafe.Offsetof | ||
len | int | 否 Go语言白皮书中提到:如果表达式s 表示一个字符串常量,则表达式len(s) 将在编译时刻估值;如果表达式s 表示一个数组或者数组的指针,并且s 中不含有数据接收操作和估值结果为非常量的函数调用,则表达式len(s) 和cap(s) 将在编译时刻估值。 |
cap | ||
real | 默认类型为 float64 (结果为类型不确定值) | 否 Go语言白皮书提到: 表达式real(s) 和imag(s) 在s 为一个复数常量表达式时才在编译时刻估值。 |
imag | ||
complex | 默认类型为 complex128 (结果为类型不确定值) | 否 Go语言白皮书提到: 表达式complex(sr, si) 只有在sr 和si 都为常量表达式的时候才在编译时刻估值。 |
哪些值是可寻址的?
请阅读此条问答获取详情。
哪些类型不支持比较?
请阅读此条问答获取详情。
哪些代码元素允许被声明却不使用?
允许被声明却不使用? | |
---|---|
包引入 | 不允许 |
类型 | 允许 |
变量 | 包级全局变量允许,但局部变量不允许(对于官方标准编译器)。 |
常量 | 允许 |
函数 | 允许 |
跳转标签 | 不允许 |
哪些有名代码元素可多个被一起声明在一对小括号()
中?
下面这些同种类的代码元素可多个被一起声明在一对小括号()
中:
- 包引入
- 类型
- 变量
- 常量
函数是不能多个被一起声明在一对小括号()
中的。跳转标签也不能。
哪些代码元素的声明可以被声明在函数内也可以被声明在函数外?
下面这些代码元素的声明既可以被声明在函数内也可以被声明在函数外:
- 类型
- 变量
- 常量
包引入必须被声明在其它种类的代码元素的声明之前。
函数必须声明在任何函数体之外。匿名函数可以定义在函数体内,但那不属于声明。
跳转标签必须声明在函数体内。
哪些表达式的估值结果可以包含一个额外的可选的值?
下列表达式的估值结果可以包含一个额外的可选的值:
语法 | 额外的可选的值(语法示例中的ok )的含义 | 舍弃额外的可选的值会对估值行为发生影响吗? | |
---|---|---|---|
映射元素访问 | e, ok = aMap[key] | 键值key 对应的条目是否存储在映射值中 | 否 |
数据接收 | e, ok = <- aChannel | 被接收到的值e 是否是在通道关闭之前发送的 | 否 |
类型断言 | v, ok = anInterface.(T) | 接口值的动态类型是否为类型T | 是 (当可选的值被舍弃并且断言失败的时候,将产生一个恐慌。) |
几种导致当前协程永久阻塞的方法
无需引入任何包,我们可以使用下面几种方法使当前协程永久阻塞:
-
向一个永不会被接收数据的通道发送数据。
make(chan struct{}) <- struct{}{}
// 或者
make(chan<- struct{}) <- struct{}{} -
从一个未被并且将来也不会被发送数据的(并且保证永不会被关闭的)通道读取数据。
<-make(chan struct{})
// 或者
<-make(<-chan struct{})
// 或者
for range make(<-chan struct{}) {} -
从一个 nil 通道读取或者发送数据。
chan struct{}(nil) <- struct{}{}
// 或者
<-chan struct{}(nil)
// 或者
for range chan struct{}(nil) {} -
使用一个不含任何分支的
select
流程控制代码块。select{}
几种衔接字符串的方法
详见字符串一文。
官方标准编译器中实现的一些优化
在Go程序运行中将会产生恐慌或者崩溃的情形
内存对齐
引入
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}
在开始之前,希望你计算一下 Part1
共占用的大小是多少呢?
func main() {
fmt.Printf("bool size: %d\n", unsafe.Sizeof(bool(true)))
fmt.Printf("int32 size: %d\n", unsafe.Sizeof(int32(0)))
fmt.Printf("int8 size: %d\n", unsafe.Sizeof(int8(0)))
fmt.Printf("int64 size: %d\n", unsafe.Sizeof(int64(0)))
fmt.Printf("byte size: %d\n", unsafe.Sizeof(byte(0)))
fmt.Printf("string size: %d\n", unsafe.Sizeof("EDDYCJY"))
}
输出结果:
bool size: 1
int32 size: 4
int8 size: 1
int64 size: 8
byte size: 1
string size: 16
这么一算,Part1
这一个结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。相信有的小伙伴是这么算的,看上去也没什么毛病
真实情况是怎么样的呢?我们实际调用看看,如下:
type Part1 struct {
a bool
b int32
c int8
d int64
e byte
}
func main() {
part1 := Part1{}
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}
输出结果:
part1 size: 32, align: 8
最终输出为占用 32 个字节。这与前面所预期的结果完全不一样。
在这里要提到 “内存对齐” 这一概念,才能够用正确的姿势去计算。
定义
有的小伙伴可能会认为内存读取,就是一个简单的字节数组摆放
上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。
相反 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。
块大小我们称其为内存访问粒度。如下图:
在样例中,假设访问粒度为 4。 CPU 是以每 4 个字节大小的访问粒度去读取和写入内存的。这才是正确的姿势
例
在上图中,假设从 Index1 开始读取,将会出现很崩溃的问题。因为它的内存访问边界是不对齐的。因此 CPU 会做一些额外的处理工作。如下:
- CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不需要的字节 0
- CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不需要的字节 5、6、7 字节
- 合并 1-4 字节的数据
- 合并后放入寄存器
从上述流程可得出,不做 “内存对齐” 是一件有点 "麻烦" 的事。因为它会增加许多耗费时间的动作。
而假设做 了内存对齐,从 Index0 开始读取 4 个字节,只需要读取一次,也不需要额外的运算。这显然高效很多,是标准的空间换时间做法。
为什么要关心对齐
- 你正在编写的代码在性能(CPU、Memory)方面有一定的要求
- 你正在处理向量方面的指令
- 某些硬件平台(ARM)体系不支持未对齐的内存访问
另外作为一个工程师,你也很有必要学习这块知识点哦 :)
为什么要做对齐
- 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
- 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作
默认系数
在不同平台上的编译器都有自己默认的 “对齐系数”,可通过预编译命令 #pragma pack(n)
进行变更,n 就是代指 “对齐系数”。一般来讲,我们常用的平台的系数如下:
- 32 位:4
- 64 位:8
另外要注意,不同硬件平台 占用的大小和对齐值都可能是不一样的。因此本文的值不是唯一的,调试的时候需按本机的实际情况考虑
类型的对称系数
在 Go 中可以调用 unsafe.Alignof
来返回相应类型的对齐系数。
func main() {
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
通过观察输出结果,可得知基本都是 2^n
,最大也不会超过 8。这是因为当前测试(64 位)编译器默认对齐系数是 8,因此最大值不会超过这个数
结构体的整体对齐
在上小节中,提到了结构体中的成员变量要做字节对齐。那么想当然身为最终结果的结构体,也是需要做字节对齐的
对齐规则
- 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(
#pragma pack(n)
)或当前成员变量类型的长度(unsafe.Sizeof
),取最小值作为当前类型的对齐值,其偏移量必须为对齐值的整数倍 - 结构体本身,对齐值必须为编译器默认对齐长度(
#pragma pack(n)
)或结构体的所有成员变量类型中的最大长度,取