数据结构
String¶
底层结构¶
runtime 包下:
字符串的操作包括 写 / 拼接 等都是通过拷贝来实现的
作为函数参数¶
当 string 作为函数参数时,传递的是底层结构体的副本。当对字符串进行拼接操作时,修改的是副本中 str 指针指向的内存地址:其指向凭借后新字符串的地址。函数外字符串不会受影响
类型转换¶
- string(100) 这里的 100 被视为 rune,会被转换为 Unicode 编码的 "d" (rune 本质是 int32)
- 如果想得到 "100",需要 strconv.Itoa(100) 或 fmt.Sprint(100)
- 相反,如果想吧 string 类型的 s 的 s[0] 转换为 string 类型,就可以使用 var tmp = string(s[0])
- 如果数字超过 Unicode 的编码范围,就会返回 �
与 []byte 互转¶
string 是不可变的,比如 s[0] = 'A' 会报错。可以通过 []byte 来实现修改:
需要注意的是,这里发生了两次拷贝过程
``初始化¶
使用 `` 初始化字符串可以初始化跨行、内部带有 " " 的字符串而避免使用转义字符
json := `{"hello": "go", "name": ["xu"]}`
type TestStruct struct {
Text string `json:"hello"` // 映射 JSON 的 "hello" 字段
Names []string `json:"name"` // 映射 JSON 的 "name" 字段
}
var data TestStruct
err := json.Unmarshal([]byte(json), &data)
json.Unmarshal 大小写不敏感
转义问题¶
`` 是原始字符串转义的定界符
// 普通字符串(需要转义)
str1 := "\"nginx reload enabled\"" // 需要转义双引号
// 原始字符串(不需要转义)
str2 := `"nginx reload enabled"` // 直接包含双引号
字符串拼接¶
追加写入的方式:
- +
- 注意这里如果要加 'a' 需要强转换 string('a')
- append
- strings.Builder (性能最优,分配内存时自动扩容)
- bytes.Buffer
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteByte(' ')
builder.WriteRune('世') // unicode
builder.WriteRune('界')
builder.Write([]byte("!"))
result := builder.String() // 复用内存
fmt.Println(result) // 输出: Hello 世界!
builder.Reset()
切片¶
切片是 go 中的一种类型,零值为 nil
底层结构¶
与数组的区别¶
数组作为一个整体性的结构存储(因为是定长的)
将数组转为 slice:arr[:] 即得到一个 slice 类型
拷贝方式¶
- 使用赋值拷贝/函数传参时,拷贝的是 slice header,二者共享底层数组;但是当发生扩容时,新切片将指向新的数组
- 使用内置的 copy(dst, src) 方法可以进行深拷贝,拿到一个完全独立的副本
作为参数传递¶
切片作为函数参数传递时,传入函数的是切片底层结构体的副本;
特别地,当传入 arr[1:] 这类切片的截取时,传入结构体的 array 指针指向了 arr[1] 所在的地址
- 在函数内部设置 slice[0] = 1,会修改原切片(指针相同,相同地址处的值被修改了)
- 在函数内因为 append 操作导致切片扩容,不会修改原切片:会导致底层结构体指针指向的地址重分配
- 在函数内部进行 slice = slice[1:] ,不会修改原切片:底层结构体的副本中指针更新
扩容策略¶
1.20.4
首先得到新的长度:
- 如果扩容的目标长度(oldLen + num(append)) > 2 * 旧容量,直接扩容为目标长度
- 否则
- 如果旧切片的容量 < 256,新容量扩容为旧容量的两倍
- 否则,旧切片容量按照 1.25 倍递增,直到新容量 >= 目标长度
随后需要进行内存对齐,同时将旧的切片中的元素复制到新分配的内存中
例子¶
package main
import "fmt"
func main() {
doAppend := func(s []int) {
s = append(s, 1)
printLenAndCap(s)
}
s := make([]int, 8, 8)
doAppend(s[:4])
printLenAndCap(s)
doAppend(s)
printLenAndCap(s)
}
func printLenAndCap(s []int) {
fmt.Println(s)
fmt.Printf("len: %d, cap: %d\n", len(s), cap(s))
}
输出结果
00001, len 5, cap 8
00001000, len 8, cap 8
000010001, len 9, cap 16 (小于256,*2)
00001000, len 8, cap 8
map¶
结构¶
map 底层是一个指针 *hmap
hash 值的位数取决于操作系统的位数(32 / 64)
上图就是 bucket 的内存模型,HOB Hash
指的就是 top hash。 注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/...
这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。
hi 是进一步的 hash(对应 hash 值的高八位),通过对齐后的内存偏移计算 value 的位置
溢出桶目的是为了减少扩容的次数(可能会预分配连续的内存作为溢出桶)
例如,有这样一个类型的 map:
考虑到内存对齐问题,按 8 个字节对齐:
如果按照 key/value/key/value/...
这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 key/key/.../value/value/...
,则只需要在最后添加 padding。
扩容¶
采用渐进式扩容的方式,避免一次性扩容带来的性能抖动:每次写入和删除时迁移桶
nevacuate 就用来存储下一次扩容需要迁移的桶号
条件 | 扩容方式 | 目的 |
---|---|---|
负载因子 > 6.5 | 双倍扩容(2×桶) | 减少冲突,提高查询效率 |
溢出桶过多(≥ 桶数量) | 等量扩容(整理) | 减少溢出链,优化内存布局 |
- 每个桶在不考虑溢出桶的情况下,可以存储最多 8 个 kv,为什么到 6.5 就需要扩容?平均 6.5 时,有些哈希碰撞非常集中的桶其实已经有很多溢出桶了
- 为什么等量扩容是有效的?因为发生等量扩容是一般是 map 中由于删除操作过多产生了空洞,溢出桶很多并且不满
JDK8 及以后,Map 扩容也使用渐进式扩容,但是查询顺序是先新桶后旧桶;go 是先旧桶后新桶:因为 java 的迁移更加积极,不仅在写入和删除时执行迁移,还会在查询时也执行迁移
接口¶
值接收者和指针类型接收者¶
实现了值类型作为接收者的方法,会自动实现指针类型接收者的相同方法;指针类型接收者方法不能自动实现值类型的接受方法。
接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。
检查某个结构体的 值 / 指针 是否实现了某个接口:
iface 和 eface¶
iface 描述的接口包含方法
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized, functions
}
eface 描述空接口
_type :
type _type struct { // 类型大小 size uintptr ptrdata uintptr // 类型的 hash 值 hash uint32 // 类型的 flag,和反射相关 tflag tflag // 内存对齐相关 align uint8 fieldalign uint8 // 类型的编号,有bool, slice, struct 等等等等 kind uint8 alg *typeAlg // gc 相关 gcdata *byte str nameOff ptrToThis typeOff }
Go 语言各种数据类型都是在
_type
字段的基础上,增加一些额外的字段来进行管理的:
接口的动态类型和动态值¶
iface
包含两个字段:tab
是接口表指针,指向类型信息;data
是数据指针,则指向具体的数据。它们分别被称为动态类型
和动态值
。而接口值包括动态类型
和动态值
。
只有当接口的动态类型和动态值都为 nil 时,这个接口值才会被认为是 nil
多态¶
同一种操作(函数)应用于不同的对象时,产生不同的影响
package main
import (
"fmt"
)
type person interface {
grow() int
speak([]byte)
}
type student struct {
name string
age int
}
func (stu *student)grow() int {
stu.age += 1
return stu.age
}
func (stu *student)speak(words []byte) {
fmt.Printf("student: %s", string(words))
}
type teacher struct {
subject string
age int
}
func (t *teacher)grow() int {
t.age += 1
return t.age
}
func (t *teacher)speak(words []byte) {
fmt.Printf("teacher %s, %s", string(words), t.subject)
}
/* 可传入任意实现了 person 的结构体 */
func getGrown(p person) {
fmt.Println(p.grow())
}
func say(p person) {
p.speak([]byte("sssss"))
}
func main() {
stu := &student{name: "bbb", age: 18}
t := &teacher{subject: "math", age: 45}
getGrown(stu)
getGrown(t)
say(stu)
say(t)
}