https://www.bilibili.com/video/BV1hv411x7we
string
unicode字符集,每个字符对应01串
定长编码–浪费内存
UTF-8:变长编码,有类似于IP地址分类的前缀
c存储字符串:\0结束
go:go语言认为字符串内容不会被修改
go语言修改字符串
|
|
slice
new和make的区别:
二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。
make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。
https://www.kancloud.cn/aceld/golang/1958314
切片的截取
切片的扩容
为啥本来容量为3 但append一个元素容量就为6了呢
结构体和内存对齐
CPU支持访问任意地址,是内部做了一些操作
可以优化这个struct
golang map
hash桶的确认
在map中对key的定位会先使用hash函数获取hash值,写入位置 index = hash%m,得到真正的写入位置。但是在golang中对index的定位有一点小优化,golang使用位与操作进行bucket的定位:
index = hash & (m-1) 为了保证key落在区间[0,m-1],所以要保证m是2的整幂次,如果m是5,那么1-3注定是空桶。
取模操作相对于位与操作,cpu的开销更大
扩容
hmap
bmap
bmap中比较特殊的是kv的存储,bmap将8个key存在一起,将8个value存在一起,是key/key/….value/value…这种存储,相对于将key/value/key/value这种存储设计,将key和value分开存储能使bmap内存布局更加紧密,只需要在最后增加padding对齐即可。
eg:可以计算下map[int64]int8 这种类型的map在上述两种存储设计中每个bmap分别占用的空间大小。
如果将kv存在一起,总共需要的每对kv后需要填充7字节的padding,总共的padding为7*8 = 56字节
但是如果将kv分开存储,8个key是对齐的,8个value也是对齐的,不需要额外的padding填充。
溢出桶
常见的坑点
1、为了防止开发人员依赖map的顺序,对于每一次map遍历(range),得到结果的顺序都是不同的,因为在初始化迭代器(runtime.mapiterinit)时都对startBucket和startCell进行了random操作。
2、无法比较的类型无法作为key
3、map的value是无法寻址的,也就是说无法获取map value的地址。
常见的case为:当value是struct类型,其值无法修改(最简单的解决方法就是value存成*T类型即可);对map[key]取地址时报错。
https://www.kancloud.cn/aceld/golang/1958315
4、map不支持并发读写,只能并发读
对于需要并发读写map的场景,常见的解决方案如下:
-
map + sync.RWMutex
-
采用 sync.map,实现是小粒度的锁+读写分离+原子操作
5、len(map)返回的是map中元素的个数,不是map的容量
6、new出来的map是无法使用的,因为new只是malloc了8字节大小的内存(new返回指针)。
相对的,make 是个语法糖,最终被编译器翻译成runtime.makemap 函数调用,会进行内存开辟和对象初始化操作。
7、通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。
8、超出容量时会自动扩容,但尽量提供一个合理的初始值
9、map作为函数的出参时不需要以指针形式进行传递,因为map本身就是指针,上文中map初始化一节也可以看到runtime.makemap返回的是*runtime.hmap。
10、delete是不会真正地把map的内存释放的,要回收map还是需要设为nil
总结
本文我们首先介绍了map的基本概念,了解到golang的map是基于hash表实现的且冲突解决方式采用链地址法,并简单介绍了map的基本操作。
接下来我们深入源码,分别研究了map的内存模型、哈希函数、初始化、读取、写入、删除、扩容和遍历过程。map中非常核心的概念是桶,桶作为map数据查找和存储的基本单位,每个桶中有8个cell,key的hash值的低位决定了落入哪个bucket,高位则决定了落入哪个cell中,对于超过8个元素的桶,会在后边链上溢出桶做额外存储,而如果有太多的溢出桶或map的空闲容量较小的情况下,则会触发map的扩容,为了防止一次扩容带来的内存抖动和时延开销,map的扩容是渐进式的,会将扩容过程离散到每一次的数据写入操作中。
channel
发送数据
接收数据
多路select
总结
对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操作和关闭一个channel操作混淆。对于每个打开的文件,都需要在不使用的时候调用对应的Close方法来关闭文件。)
试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制
nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。
这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。
堆内存管理1
https://zhuanlan.zhihu.com/p/76802887
https://zhuanlan.zhihu.com/p/308054212
arena span(不同大小) page 内存块
mheap管理整个堆内存,arena对应heapArena结构,span对应mspan结构
mheap中有个全局的mspan管理中心mheap.central,长度为136的数组,数组元素是一个mcentral结构
一个mcentral对应一种mspan规格类型,记录在spanclass中
全局mspan管理中心方便取用各种规格类型的mspan,为了保障多个p之间并发安全,免不了加锁解锁
go语言的每个p都有一个本地小对象缓存
当p需要用到指定规格类型的mspan时,先去本地缓存这里找对应的mspan,如果没有或用完了就去mcentral获取一个放到本地