今天我们开始深入学习,Go 语言语法的复合数据类型: slice、map、string
1. slice
学习 slice 之前,我们先来一下 Go 语言中的数组。
Go语言数组是一个固定长度的、容纳同构类型元素的连续序列,因此Go数组类型具有两个属性:元素类型和数组长度。这两个属性都相同的数组类型是等价的。所以下面三个数组都属于不同类型
1
2
3
4
|
// a、c 类型相同,但是长度不同,属于不同类型的数组
var a [8]int
var b [8]byte
var c [9]int
|
Go数组是值语义的,数组在内部表示为连续的字节序列,虽然长度是Go数组类型的一部分,但长度并不包含在数组类型在Go运行时的内部表示中,数组长度是由编译器在编译期计算出来。这点与C语言完全不同。在C语言中,数组变量可视为指向数组第一个元素的指针。在Go语言中传递数组是纯粹的值拷贝,大数组在作为参数进行传递会有性能损耗。相比于数组,在 go 语言中更通用的是 slice。
slice 是基于数组的容器类型,可以自动对数组进行扩缩容。
1.1 切片的初始化和操作
声明和初始化切片有如下几种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 1. 变量声明
var s []int // s 为 nil 未分配空间
// 2. 字面量
s := []int{}
s := []int{1, 2, 3}
// 3. make
s := make([]int, 2, 100)
// 4. 切片
array := [5]int{1, 2, 3, 4, 5}
s := array[0:2:5]
|
内置函数 append() 用于向切片中追加元素:
1
2
3
4
|
s := make([]int, 9)
s = append(s, 1)
s = append(s, 2, 3, 4)
s = append(s []int{5, 6}...)
|
1.2 slice 实现
slice 定义在 src/runtime/slice.go 中,由 makeslice 函数创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
type slice struct {
array unsafe.Pointer // 指向底层数组
len int
cap int
}
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
// slice 扩缩容处理
func growslice(et *_type, old slice, cap int) slice {}
// slice copy 操作实现
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int
|
每个切片包含以下三个字段。
- array:指向下层数组某元素的指针,该元素也是切片的起始元素
- len:切片的长度,即切片中当前元素的个数
- cap:切片的最大容量,cap >= len
slice 的 struct 定义以及底层的数组指针决定了其具有如下的赋值特性:
- 赋值操作会复制整个 struct 但是共享底层的数组
- 赋值前后,数组的 len 和 cap 是独立的,但共享的底层数组是可能导致读写冲突的
1.3 slice 扩缩容
另外,slice 的扩容和拷贝规则如下:
- slice 的容量小于 1024 时,新的容量扩大为原来的 2 倍
- slice 的容量大于 1024 时,新的容量扩大为原来的 1.25 倍
- 使用 copy() 内置函数拷贝两个切片是,会将源切片数据逐个拷贝到目的切片指向的数组,拷贝数量取两个切片长度的最小值。
最后 slice 的切片有一个扩展表达式 a[low:high:max]
max 用于限定新生成的切片容量,注意 max 表示的是到哪,而不是有多少个,新生成的切片:
- len=high-low
- cap=max-low
一旦发生扩容,切片就会和原数组解除绑定,并绑定到扩容后的新数组。最后,虽然数组可以动态扩容,但是如果可以预估出切片底层数组需要承载的元素数量,强烈建议在创建切片时带上cap参数。这样可以减少数组分配和拷贝的次数,提升性能。
1.4 slice 数据共享问题
由于 append 扩容后与底层数组的解绑行为;共享底层数组的情况,也有可能导致意外的覆盖写。我们看下面这例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB
}
|
dir1 = append(dir1,"suffix"...)
由于 append 操作没有超过 path cap 的容量,导致,对 dir1 的修改覆盖了 dir2 中的数据。如果 dir2/dir1 被传递到程序的不同地方使用。导致的问题将更加隐蔽。解决这个问题的方法有两个,一个是 copy dir1,然后再修改。更好的方法是使用: dir1 := path[:sepIndex:sepIndex]
因为限定了 dir1 的 cap,后续的 append 操作就会重新分配新的数组。
2. map
2.1 map 的初始化和操作
首先 Go 对 map 有些使用上的限制:
- map对value的类型没有限制,但是对key的类型有严格要求:key的类型应该严格定义了作为“==”和“!=”两个操作符的操作数时的行为,因此函数、map、切片不能作为map的key类型
- map类型不支持“零值可用”,未显式赋初值的map类型变量的零值为nil。对处于零值状态的map变量进行操作将会导致运行时panic
map 的常见操作如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
var m map[string]int // m = nil
m["key"] = 1 // panic: assignment to entry in nil map
// 1. 声明和初始化
var statusText = map[int]string{
StatusOK: "OK",
StatusCreated: "Created",
StatusAccepted: "Accepted",
...
}
icookies = make(map[string][]*Cookie)
// 2. 插入和删除
m := make(map[K]V)
m[k1] = v1
m[k2] = v2
m[k3] = v3
fmt.Println(len(m)) // 3
delete(m, "key2")
// 3. 查找
// 即使 key 不在 map 中,也会返回 value 类型的零值,要判断 key 是否在 map 内,需要使用 comma ok
_, ok := m["key"]
if !ok {
// "key"不在map中
}
// 4.遍历
// Go运行时在初始化map迭代器时对起始位置做了随机处理,所以元素的遍历次序是不一定的
for k, v := range m {
fmt.Printf("[%d, %d] ", k, v)
}
|
2.2 map 的底层实现
map 定义在 src/runtime/map.go 中,由 makemap 函数创建,在编译阶段,Go编译器会将语法层面的map操作重写成运行时对应的函数调用:
1
2
3
4
5
|
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
v := m["key"] → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"] → v, ok := runtime.mapaccess2(maptype, m, "key")
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") // v是用于后续存储value 的空间的地址
delete(m, "key") → runtime.mapdelete(maptype, m, "key")
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap
}
// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
// If hint is large zeroing this memory could take a while.
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap){}
|
通过 makemap 函数,我们可以看到 map 在运行时是一个指向 hmap 的指针,hmap 是 go 语言实现的哈希表,一个哈希表有如下实现要点:
- 首先哈希表由哈希函数和底层的数组组成
- 哈希函数用来定位 key 的存储位置,并可能存在哈希冲突
- 当负载因子高或者低时,哈希表需要动态扩缩容(rehash)
下面是 map 类型在运行时层实现的示意图
字段 |
作用 |
count |
当前map中的元素个数;对map类型变量运用len内置函数时,len函数返回的就是count这个值 |
flags |
当前map所处的状态标志,目前定义了4个状态值——iterator、oldIterator、hashWriting和sameSizeGrow |
B |
B的值是bucket数量的以2为底的对数,即2^B = bucket数量 |
noverflow |
overflow bucket的大约数量 |
hash0 |
哈希函数的种子值 |
buckets |
指向bucket数组的指针 |
oldbuckets |
在map扩容阶段指向前一个bucket数组的指针 |
nevacuate |
在map扩容阶段充当扩容进度计数器。所有下标号小于nevacuate的bucket都已经完成了数据排空和迁移操作 |
extra |
可选字段。如果有overflow bucket存在,且key、value都因不包含指针而被内联(inline)的情况下,该字段将存储所有指向overflow bucket的指针,保证overflow bucket是始终可用的(不被垃圾回收掉) |
2.1 底层数组
hmap.buckets 就是 hmap 的底层数组,它由 bmap 定义,注意 bmap 的完整定义如下:
1
2
3
4
5
|
type bmap struct {
tophash [bucketCnt]uint8
data []byte
overflow *bmap
}
|
其中:
- tophash:
- 用来存储 Hash 值的高 8 位
- 这个字段与 hmap 的定位过程有关,后面就会看到它存在的意义
- data:
- 存放的是key-value 数据
- 内存布局是 key/key/…..value/value
- 这么存储是为了节省字节对齐带来的空间浪费
- overflow:
- 指向下一个 bucket
- hmap 使用链表法解决哈希冲突
注意: data 和 overflow 并没有显示的定义在结构体中,运行时在访问 bucket 时直接通过指针的偏移来访问这些虚拟成员。
2.2 key 定位过程
hmap 的增删改查都需要先定位 key 在 buckets 中的位置, hmap 定位过程如下:
- 计算 key 的 Hash 值
- 将 Hash 值分为高 8 位和剩余低位
- 取 Hash 值低位与 hmap.B 取模确定 bucket 的位置
- 取 Hash 值高 8 位,在 bucket.tophash 数组中查询,如果在索引 i 处查找到,则获取索引 i 对应的 key 进行比较
- 当前 bucket 中没有找到,则依次从溢出的 bucket 中查找,如果 map 处于扩缩容过程中,优先从 oldbuckets 数组中查找
所以 bmap 的 tophash 的类型为 [bucketCnt]uint8
保存的是存储在当前bucket 的所有key 的Hash 的值高 8 位,目的是加快 key 的索引过程。
2.3 数据存储
运行时在分配bucket时需要知道key/value的大小。那么运行时是如何知道key的大小的呢?当我们声明一个map类型变量时,比如var m map[string]int,Go运行时就会为该变量对应的特定map类型生成一个runtime.maptype实例:
1
2
3
4
5
6
7
8
9
10
11
|
// $GOROOT/src/runtime/type.go
type maptype struct {
typ _type
key *_type
elem *_type
bucket *_type // 表示hash bucket的内部类型
keysize uint8 // key的大小
elemsize uint8 // elem的大小
bucketsize uint16 // bucket的大小
flags uint32
}
|
maptype 包含了我们所需的map类型的所有元信息。前面提到过编译器会将语法层面的map操作重写成运行时对应的函数调用,这些运行时函数有一个共同的特点:第一个参数都是maptype指针类型的参数。Go运行时就是利用maptype参数中的信息确定key的类型和大小的,map所用的hash函数也存放在maptype.key.alg.hash(key, hmap.hash0)中。
另外还有一点要提及的是,如果key或value的数据长度大于一定数值,那么运行时不会在bucket中直接存储数据,而是会存储key或value数据的指针。目前Go运行时定义的最大key和value的长度分别如下:
1
2
3
4
5
|
// $GOROOT/src/runtime/map.go
const (
maxKeySize = 128
maxElemSize = 128
)
|
2.4 扩缩容 rehash
扩容条件
负载因子 = 键数量/bucket数量,Go 触发 hmap 扩容的条件有两个:
- 负载因子 > 6.5
- overflow 数量大于2^15
扩容过程
hmap 扩容时,会新建一个 bucket 数组,长度为原来的 2 倍,Go 会采用逐步搬迁的策略,每次访问 map 时都会触发一次搬迁,每次搬迁 2 个键值对。因此 hmap 的结构体中包含了 oldbuckets 成员,指向了扩容前的原 buckets 数组。buckets 则指向新分配的数组。带 oldbuckets 数组中所有键值对搬迁完毕后,oldbuckets就会被释放。
如果是因为overflow bucket过多导致的“扩容”,实际上运行时会新建一个和现有规模一样的bucket数组。
缩容过程
缩容过程发生在大量key 被删除之后,过程与扩容类似。
2.5 并发
从上面的实现原理来看,充当map描述符角色的hmap实例自身是有状态的(hmap.flags)且对状态的读写是没有并发保护的,因此map实例不是并发写安全的,不支持并发读写。如果对map实例进行并发读写,程序运行时会发生panic。另外考虑到map可以自动扩容,map中数据元素的value位置可能在这一过程中发生变化,因此Go不允许获取map中value的地址,这个约束是在编译期间就生效的。
最后与 slice 类似,最好对map使用规模做出粗略的估算,并使用cap参数对map实例进行初始化。这样可以减少内存分配和数据迁移的次数,提升性能。
3. string
go string类型具有如下功能特点:
- string类型的数据是不可变的
- 零值可用
- 支持通过+/+=操作符进行字符串连接
- 支持各种比较关系操作符:==、!= 、>=、<=、>和<
3.1 string 实现
string 定义在 src/runtime/string.go
中:
1
2
3
4
5
6
7
8
9
10
|
type stringStruct struct {
str unsafe.Pointer
len int
}
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}
|
stringStruct 中:
- str: 字符串的首地址
- len: 字符串的长度
在 runtime 包中使用 gostringnocopy 函数来生成字符串,gostringnocopy 会先构建 stringStruct 对象,然后在转换成 string,string 定义在 buildin 包中:
1
2
3
4
|
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
|
从注释中可以看到:
- string 是 8bit 的集合,通常是 UTF-8 的文本
- string 可以为空,但不会是 nil
- string 对象不可修改
3.2 字符串拼接
Go 中字符串可以直接使用 + 号进行拼接: str := "str1" + "str2" + "str3"
,拼接过程在内部则会调用 string 包的 concatstrings() 函数,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
if n == 0 {
continue
}
if l+n < l {
throw("string concatenation too long")
}
l += n
count++
idx = i
}
if count == 0 {
return ""
}
// If there is just one string and either it is not on the stack
// or our result does not escape the calling frame (buf != nil),
// then we can return that string directly.
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
// 返回一个 string 和切片,二者共享内存空间
s, b := rawstringtmp(buf, l)
for _, x := range a {
copy(b, x)
b = b[len(x):]
}
return s
}
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
if buf != nil && l <= len(buf) {
b = buf[:l]
s = slicebytetostringtmp(&b[0], len(b))
} else {
s, b = rawstring(l)
}
return
}
// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
func rawstring(size int) (s string, b []byte) {
p := mallocgc(uintptr(size), nil, false)
stringStructOf(&s).str = p
stringStructOf(&s).len = size
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
return
}
|
concatstrings 实现中:
- 所有待拼接字符串都被编译器组织到一个切片中并传入 concatstrings 函数
- 拼接需要遍历两次切片,第一次遍历获取总的字符串长度,据此申请内存
- 第二次遍历把字符串逐个拷贝过去
运行时修改字符串的方式是,rawstring 创建一个临时slice,该slice的array指针也指向存储字符串数据的底层内存区域。rawstring调用后,新申请的内存区域还未被写入数据,该slice就是供后续运行时层向其中写入数据用的。写完数据后,该slice就可以被回收掉了,
字符串高效构建
Go 支持通过+/+=操作符来连接多个字符串以构造一个更长的字符串,并且通过+/+=操作符的字符串连接构造是最自然、开发体验最好的一种。但Go还提供了其他一些构造字符串的方法,比如:
- 使用fmt.Sprintf;
- 使用strings.Join;
- 使用strings.Builder;
- 使用bytes.Buffer。
可以编写一些单元测试来测试不同方法的性能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// 1. 做了预初始化的strings.Builder -- 效率最高
func concatStringByStringsBuilderWithInitSize(sl []string) string {
var b strings.Builder
b.Grow(64)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
// 2. join -- 其次
func concatStringByJoin(sl []string) string {
return strings.Join(sl, "")
}
// 3. 带有预初始化的bytes.Buffer -- 与 join 差不多
func concatStringByBytesBufferWithInitSize(sl []string) string {
buf := make([]byte, 0, 64)
b := bytes.NewBuffer(buf)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
// 4. fmt.Sprintf 效率最低
func concatStringBySprintf(sl []string) string {
var s string
for _, v := range sl {
s = fmt.Sprintf("%s%s", s, v)
}
return s
}
|
这里给一下结论:
- 在能预估出最终字符串长度的情况下,使用预初始化的strings.Builder连接构建字符串效率最高;
- strings.Join连接构建字符串的平均性能最稳定,如果输入的多个字符串是以[]string承载的,那么strings.Join也是不错的选择;
- 使用操作符连接的方式最直观、最自然,在编译器知晓欲连接的字符串个数的情况下,使用此种方式可以得到编译器的优化处理;
- fmt.Sprintf虽然效率不高,但也不是一无是处,如果是由多种不同类型变量来构建特定格式的字符串,那么这种方式还是最适合的。
3.3 类型转换
字符串可以与rune slice、byte slice相互转换,转换过程需要一次内存拷贝,转换对应的函数如下:
1
2
3
4
5
|
// $GOROOT/src/runtime/string.go
slicebytetostring: []byte -> string
slicerunetostring: []rune -> string
stringtoslicebyte: string -> []byte
stringtoslicerune: string -> []rune
|
[]byte -> string
[]byte -> string 调用的是 string 包的 slicebytetostring 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// slicebytetostring converts a byte slice to a string.
// It is inserted by the compiler into generated code.
// ptr is a pointer to the first element of the slice;
// n is the length of the slice.
// Buf is a fixed-size buffer for the result,
// it is not nil if the result does not escape.
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
if n == 0 {
// Turns out to be a relatively common case.
// Consider that you want to parse out data between parens in "foo()bar",
// you find the indices and convert the subslice to string.
return ""
}
if raceenabled {
racereadrangepc(unsafe.Pointer(ptr),
uintptr(n),
getcallerpc(),
funcPC(slicebytetostring))
}
if msanenabled {
msanread(unsafe.Pointer(ptr), uintptr(n))
}
if n == 1 {
p := unsafe.Pointer(&staticuint64s[*ptr])
if sys.BigEndian {
p = add(p, 7)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
if buf != nil && n <= len(buf) {
// 如果预留 buf 够用,则用预留的 buf
p = unsafe.Pointer(buf)
} else {
// 否则重新申请内存
p = mallocgc(uintptr(n), nil, false)
}
// 构建字符串
stringStructOf(&str).str = p
stringStructOf(&str).len = n
// 将切片底层数组中数据拷贝到字符串
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return
}
func memmove(to, from unsafe.Pointer, n uintptr)
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer
|
string -> []byte
[]byte -> string 调用的是 string 包的 slicebytetostring 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
// buf 小直接从栈上分配
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
// 生成新的切片
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.
func rawbyteslice(size int) (b []byte) {
cap := roundupsize(uintptr(size))
p := mallocgc(cap, nil, false)
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
}
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}
type Type int
func copy(dst, src []Type) int
|
3.4 编译优化
[]byte 和 string 相互转化都会进行一次内存拷贝,而在某些临时场景下,byte 切换在转化成 string 是并不会拷贝内存,而是直接返回一个 string ,string.str 的指针指向切片的内存,这些场景都符合一个特征,即在 string 的存续期间 []byte 肯定不会修改:
- 使用
map[string(b)]
- 字符串拼接
"a" + string(b) + "c"
- 字符串比较
string(b) == "foo"
- 用在for-range循环中的string到[]byte的转换
1
2
3
4
5
6
7
|
func convertWithOptimize() {
s := "中国欢迎您,北京欢迎您"
// range 内的转换不会拷贝内存
for _, v := range []byte(s) {
_ = v
}
}
|
4. struct
Go 语言中 struct 的一个特点是允许为字段标记 Tag,如下所示:
1
2
3
|
type TypeMeta struct {
Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
}
|
4.1 Tag 的本质
首先 Tag 是 struct 的一部分,用于标识结构体字段的额外属性。在 reflect 包中,使用结构体 StructField 表示结构体的一个字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// A StructField describes a single field in a struct.
type StructField struct {
// Name is the field name.
Name string
// PkgPath is the package path that qualifies a lower case (unexported)
// field name. It is empty for upper case (exported) field names.
// See https://golang.org/ref/spec#Uniqueness_of_identifiers
PkgPath string
Type Type // field type
Tag StructTag // field tag string
Offset uintptr // offset within struct, in bytes
Index []int // index sequence for Type.FieldByIndex
Anonymous bool // is an embedded field
}
type StructTag string
func (tag StructTag) Get(key string) string {
v, _ := tag.Lookup(key)
return v
}
|
可以看到,Tag 也是字段的一个组成部分。从类型可以看出 Tag 是一个字符串,它有一个约定的格式,就是由 key:“value” 组成:
- key: 必须是非空字符串,字符串不能包含控制字符、空格、引号、冒号
- value: 以双引号括住的字符串
- key 和 value 之间使用冒号相隔,冒号前后不能有空格,多个 key:“value” 由空格分割
4.2 Tag 的获取
通过反射可以获取 Tag 中 key 对应的 value,下面是一个代码示例:
1
2
3
4
5
6
7
8
|
func PrintTag(){
t := TypeMeta{}
ty := reflect.TypeOf(t)
for i := 0; i < ty.NumField(); i++ {
fmt.Printf("Field: %s, Tag: %s\n", ty.Field(i).Name, ty.Field(i).Tag.Get("json"))
}
}
|
Go 语言的反射特性可以动态的给结构体成员赋值,Tag 就可以给这种赋值提供"指引"。
5. iota
Go 中 iota 用于声明连续的整型常量,iota 的取值与其出现的额位置强相关。从编译器的角度看 iota,其取值规则只有一条: iota 代表了 const 声明块的行索引。除此之外,const 声明还有一个特点,如果为常量指定了一个表达式,但后续的常量没有表达式,则继承上面的表达式。
5.1 实现原理
在编译器代码中,每个常量或者变量的声明语句使用 ValueSpec 结构表示,ValueSpec 定义在 src/go/ast/ast.go
1
2
3
4
5
6
7
|
ValueSpec struct {
Doc *CommentGroup // associated documentation; or nil
Names []*Ident // value names (len(Names) > 0)
Type Expr // value type; or nil
Values []Expr // initial values; or nil
Comment *CommentGroup // line comments; or nil
}
|
ValueSpec 仅表示一行声明语句,比如:
1
2
3
4
|
const (
// 常量块注释
a, b = iota, iota // 常量行注释
)
|
上面的常量声明中仅包括一行声明语句,对应一个 ValueSpec 结构:
- Doc: 表示注释
- Name: 常量的名字,使用切片表示当行语句中声明的多个变量
- Type: 常量类型
- Value: 常量值
- Comment: 常量行注释
如果 const 包含多行常量声明,就会对应多个 ValueSpec,编译器在遍历时会使用类似下面的伪代码:
1
2
3
4
5
|
for iota, spec := range ValueSpecs {
for i, name := range spec.Names }{
obj := NewConst(name, iota)
}
}
|
从上面的代码就可以看出,iota 的本质: 仅代表常量声明的索引。
6. channel
channel 的定义和使用,在 go 语言的第四部分-go 并发中有详细的介绍,内容如下:
- Channel 使用与实现
- Channel 应用
虽然后面有更详细的介绍,但这里不妨我们对它有个基本的认识。
6.1 hchan 数据结构
channel 定义在源码包 src/runtime/chan.go 中,对应类型为 *hchan,由 makechan 函数创建。hchan 结构如下,其中比较难理解的是 elementype *_type
,这个跟 Go 类型系统的实现有关,我们暂时放一放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
type nameOff int32
type typeOff int32
type tflag uint8
|
hchan 的结构成员分成了如下几类
- 环形队列:
- dataqsiz: 队列的长度
- buf: 队列的内存
- qcount: 队列中元素个数
- sendx: 插入数据的位置索引
- recvx: 读取数据的位置索引
- 等待队列:
- recvq: 读取等待队列
- sendq: 发送等待队列
- 类型信息: 一个管道只能发送一种类型的值
- elemtype: 元素类型
- elemsize: 元素类型大小
- 互斥锁: lock
6.2 channel 创建
hchan 由 makechan 函数创建,函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
type chantype struct {
typ _type
elem *_type
dir uintptr
}
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}
|
6.3 channel 的使用
向 channel 收发数据是否会阻塞,与我们理解的环形队列的使用基本无异,只不过里面包含了一些小技巧:
- 首先环形队列没有空间时,发送数据会阻塞
- 同样环形队列中没有数据时,读取数据会阻塞
- 技巧一,当接收队列 recvq 不为空时,说明缓冲区肯定没有数据但有协程在等待,所以会把写入的数据直接传递给 recvq 中的第一个协程,而不必写入 buf
- 于技巧一类似,当 sendq 不为空时,会直接将 sendq 中第一个协程的数据发送给读取的协程
6.4 channel的关闭
关闭管道时,recvq/sendq 中的协程会全部唤醒,但是行为不同:
- recvq 中的协程会获取对应类型的零值
- sendq 中的协程会触发异常,所以 channel 只能发送方关闭,发送方可以确保没有协程阻塞在 sendq
除此之外下面的 channel 操作也会触发 panic:
- 关闭值为 nil 的管道
- 关闭已经被关闭的管道
- 向已经关闭的管道写入数据