Go高并发下的数据结构

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
  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值