Go高并发下的数据结构
什么变量的大小是0字节
- 空结构体变量有自己的地址,但是变量大小为0.
- 空结构体独立出现,也就是它没有被包在其他结构体里面时,这些空结构体都指向同一个地址 zerobase。定义在runtime/malloc.go文件中:
// base address for all 0-byte allocations
var zerobase uintptr
type 葛诗颖 struct {
}
type shishi struct {
}
type Age_11 struct {
}
func main() {
var a = 葛诗颖{}
var b = shishi{}
var c = Age_11{}
fmt.Println("a的大小:", unsafe.Sizeof(a))
fmt.Println("b的大小:", unsafe.Sizeof(b))
fmt.Println("c的大小:", unsafe.Sizeof(c))
fmt.Printf("a的地址:%p\n", &a)
fmt.Printf("b的地址:%p\n", &b)
fmt.Printf("c的地址:%p\n", &c)
}
运行结果:
a的大小: 0
b的大小: 0
c的大小: 0
a的地址:0x450518
b的地址:0x450518
c的地址:0x450518
- 如果空结构体作为其他结构体的成员时,
type shishi struct {
}
type Age_11 struct {
shiyin shishi
age int
}
func main() {
// var a = 葛诗颖{}
var b = shishi{}
var c = Age_11{}
// fmt.Println("a的大小:", unsafe.Sizeof(a))
fmt.Println("b的大小:", unsafe.Sizeof(b))
fmt.Println("c的大小:", unsafe.Sizeof(c))
// fmt.Printf("a的地址:%p\n", &a)
fmt.Printf("zerobase的地址:%p\n", &b)
fmt.Printf("c变量中空结构体的地址:%p\n", &c.shiyin)
}
运行结果:
b的大小: 0
c的大小: 8
zerobase的地址:0x4d0518
c变量中空结构体的地址:0xc0000aa058
- 空结构体主要是为了节约内存,比如使用Go语言提供的数据结构map实现hashset时,可以用到空结构体。比如新建不需要携带什么信息的channel时。
数组,字符串,切片底层是一样的么?
字符串的大小
func main() {
var a = "葛诗颖"
var b = "葛诗颖LiuXuan"
fmt.Println("size of a :", unsafe.Sizeof(a))
fmt.Println("size of b :", unsafe.Sizeof(b))
}
运行结果:
size of a : 16
size of b : 16
原因:字符串变量的类型的字符串结构体,打印的是字符串结构体的大小
// 位置:runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
- 字符串本质是个结构体
- Data指针指向底层Byte数组
问题:len表示的是Byte数组的长度?还是字符个数?
func main() {
var a = "葛诗颖"
str := (*reflect.StringHeader)(unsafe.Pointer(&a))
fmt.Println(`字符串“葛诗颖”的长度为: `, str.Len)
}
运行结果:
字符串“葛诗颖”的长度为: 9
- Len 表示的是Byte数组的长度。
为什么三个汉字的len的值为9?
UTF-8
- go语言默认字符编码格式为UTF-8,这是变长编码。所以字符串"葛诗颖LiuXuan"的长度为:16.
- 代码验证:
func main() {
var b = "葛诗颖LiuXuan"
str := (*reflect.StringHeader)(unsafe.Pointer(&b))
fmt.Println(`字符串“葛诗颖LiuXuan”的长度为: `, str.Len)
}
运行结果:
字符串“葛诗颖LiuXuan”的长度为: 16
字符串的访问
- 因为字符串是变成编码的,所以使用角标方式遍历,可能得不到正确的结果
- 通过range语法糖遍历字符串
func main() {
var a = "葛诗颖"
for _, ch := range a {
fmt.Printf("%c:", ch)
fmt.Println(ch)
}
}
运行结果:
葛:33883
诗:35799
颖:39062
字符串的访问
- 对字符串使用len方法得到的是字节数而不是字符数。
- 对字符串直接使用下标访问,得到的是字节。
- 字符串被range编历时,被解码成rune类型的字符。
- utf-8编码解码算法位于runtime/utf8.go
字符串的切分
- 需要切分时:先转为rune数组,然后切片,最后转为string。
s = string([]rune(s)[:3])
- For example:
func main() {
var a = "葛诗颖"
var newStr = string([]rune(a)[1:2])
fmt.Println("newStr: ", newStr)
}
运行结果:
newStr: 诗
切片
- 切片也是一个结构体,位置 runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
- 切片的本质是对数组的引用。
切片的创建
// 根据数组创建
arr[0:3] or slice[0:3]
// 字面量:编译时插入创建数组的代码
slice := []int{1,2,3}
// make:运行时创建数组
slice := make([]int, 10)
切片的访问
- 下标直接访问元素
- range遍历元素
- len(slice)查看切片长度
- cap(slice)查看数组容量
切片的追加
- 不扩容时,只调整len(编译器负责)
- 扩容时,编译时转为调用 runtime.growslice()
- 扩容时,1. 期望容量大于当前容量的两倍,会使用期望容量。
- 扩容时,2. 如果当前切片的长度小于1024,将容量翻倍。
- 扩容时,3.如果当前切片的长度大于1024,每次增加25%。
- 切片扩容时,并发不安全。注意切片并发要加锁
map
HashMap的基本方案
- 开放寻址法
- 拉链法
Go的map
map的初始化
- make
m := make(map[string]int, 10)
- 字面量
- 元素少于25个时,转化为简单赋值
hash := map[string]int {
"葛":11,
"诗":11,
"颖":11,
}
// 会被转化为
hash := make(map[string]int, 3)
hash["葛"] = 11
hash["诗"] = 11
hash["颖"] = 11
- 元素多于25个时,转化为循环赋值
map的访问
总结
- Go语言使用拉链实现了HashMap。
- 每一个桶中存储键哈希的前8位。
- 桶超出8个数据,就会存储到溢出桶中。
map的扩容
- map溢出的桶太多时会导致严重的性能下降。
- runtime.mapassign()可能会触发扩容的情况:
- 装载因子超过6.5(平均每个槽6.5个key)
- 使用了太多溢出桶(溢出桶超过了普通桶)
map扩容的类型
- 等量扩容:数据不多但是溢出桶太多了(整理)
- 翻倍扩容:数据太多了。
map扩容步骤
步骤一
- 创建一组新桶。
- oldbuckets指向原有的桶数组。
- buckets指向新的桶数组
- 将map标记为扩容状态
步骤二
- 将所有的数据从旧桶驱逐到新桶
- 采用渐进式驱逐
- 每次操作一个旧桶时,将旧桶数据驱逐到新桶
- 读取时不进行驱逐,只判断读取新桶还是旧桶
步骤三
- 所有的旧桶驱逐完成后
- oldbuckets回收
总结
- 装载系数或者溢出桶的增加,会触发map扩容
- “扩容”可能并不是增加桶数,而是整理
- map扩容采用渐进式,桶被操作时才会重新分配。
怎么解决map的并发问题?
- 一个小例子
func main() {
m := make(map[int]int)
go func() {
for {
_ = m[1]
}
}()
go func() {
for {
m[2] = 2
}
}()
select {}
}
- 运行结果
fatal error: concurrent map read and map write
map的并发问题
- map的读写有并发问题。
- A协程在桶中读数据时,B协程驱逐了这个桶。
- A协程会读到错误的数据,或者找不到数据。
map并发问题解决方案
- 给map加锁(mutex),但是map的并发性能会受到极大的影响。
- 使用sync.Map
sync.Map 删除
- 相比与查询、修改、新增、删除更麻烦
- 删除可以分为正常删除和追加后删除。
- 提升后,被删的key还需要特殊处理。
总结
- map在扩容时会有并发问题
- sync.Map使用了两个map,分离了扩容问题
- 不会引发扩容的操作(查、改)使用read map
- 可能引发扩容的操作(新增)使用dirty map。
接口:隐式更好还是显式更好?
Go隐式接口特点
- 只要实现了接口的全部方法,就是自动实现接口。
- 可以在不修改代码的情况下抽象出新的接口。
接口值的底层表示
- 接口数据使用runtime.iface表示
- iface记录了数据的地址
- itab字段记录了接口的类型信息和实现的方法。
类型断言
- 类型断言是一个使用在接口值上的操作
- 可以将接口值转换为其他类型值(实现或者兼容接口)
- 可以配合switch进行类型判断。
type 葛诗颖 interface {
playFootBall()
}
type girl struct {
Name string
}
func (receiver girl) playFootBall() {
fmt.Println(receiver.Name, "在踢足球。")
//fmt.Println("葛诗颖在踢足球")
}
func main() {
var g 葛诗颖 = girl{Name: "葛诗颖"}
ge11 := g.(葛诗颖)
ge11.playFootBall()
switch g.(type) {
case 葛诗颖:
g.playFootBall()
}
}
葛诗颖 在踢足球。
葛诗颖 在踢足球。
结构体和指针实现接口
如果是结构体实现了接口的,则通过结构体初始化变量和通过结构体指针初始化变量都可以成功,因为编译时系统自动添加了 结构体指针实现的接口的方法。 但是如果是结构体指针实现了接口,则只可以通过结构体指针初始化变量,因为此时系统不会再自动添加 结构体实现接口的方法。 具体细节可以看汇编代码。
- 例子
这个代码在Goland不报错
type 葛诗颖 interface {
playFootBall()
}
type girl struct {
Name string
}
func (receiver girl) playFootBall() {
fmt.Println(receiver.Name, "在踢足球。")
//fmt.Println("葛诗颖在踢足球")
}
func main() {
var g 葛诗颖 = &girl{Name: "葛诗颖"}
ge11 := g.(葛诗颖)
ge11.playFootBall()
}
这个版本的代码在goland会报错
type 葛诗颖 interface {
playFootBall()
}
type girl struct {
Name string
}
func (receiver *girl) playFootBall() {
fmt.Println(receiver.Name, "在踢足球。")
//fmt.Println("葛诗颖在踢足球")
}
func main() {
var g 葛诗颖 = girl{Name: "葛诗颖"}
ge11 := g.(葛诗颖)
ge11.playFootBall()
}
空接口值
- runtime.eface结构体
- 空接口底层不是普通接口
- 空接口值可以承载任何数据
空接口的用途
- 空接口最大的用途是作为任意类型的函数入参
- 函数调用时,会新生成一个空接口再传参。
nil,空接口,空结构体有什么区别?
nil
- nil是空,但不一定是空指针。
- nil是 pointer channel function interface map slice这六种类型的零值。
- 每种类型的nil是不同的,无法比较。
空结构体
- 空结构体是Go语言中非常特殊的类型
- 空结构体的值不是nil
- 空结构体的指针也不是nil,但是都相同(zerobase)
空接口
- 空接口不一定是nil接口
- eface结构体的两个属性都是nil,才是nil接口。
type shishi interface {
}
func main() {
var 葛诗颖 shishi
fmt.Println(葛诗颖 == nil)
var c *int
fmt.Println(c == nil)
葛诗颖 = c
fmt.Println(葛诗颖 == nil)
}
运行结果
true
true
false
内存对齐是如何优化程序效率的?
- 非内存对齐:内存的原子性与效率受到影响。
- 内存对齐:提高内存操作效率,有利于内存原子性。
对齐系数
- 为了方便内存对齐,Go提供了对齐系数。
unsafe.Alignof()
- 对齐系数的含义是:变量的内存地址必须被对齐系数整除。
- 如果对齐系数是8,表示变量的内存地址必须是8的倍数。
- 基本类型的对齐系数就是它的长度
结构体对齐
- 结构体对齐分为内部对齐和结构体之间对齐
- 内部对齐:考虑成员大小和成员的对齐系数
- 结构体长度填充:考虑自身对齐系数和系统字长。
结构体内部对齐
- 指的是结构体内部成员的相对位置(偏移量)
- 每个成员的偏移量是自身大小与其对齐系数较小值的倍数。
结构体长度填充
- 指的是结构体通过增加长度,对齐系统字长。
- 结构体长度是最大成员长度与系统字长较小的整数倍。
节约结构体空间
- 可以尝试通过调整成员顺序,节约空间。
结构体对齐系数
- 结构体的对齐系数是其成员的最大对齐系数。
空结构体的对齐
- 空结构体单独出现时,地址为zerobase。
- 空结构体出现在结构体中时,地址跟随前一个变量。
- 空结构体出现在结构体末尾时,需要补齐字长。
一个小作业
type User struct {
A int32
B []int32
C string
D bool
E struct{}
}
func main() {
user := User{}
fmt.Println("size: ", unsafe.Sizeof(user))
fmt.Println("对齐系数: ", unsafe.Alignof(user))
}
运行结果
size: 56
对齐系数: 8