这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
Slice
new和make的区别:
二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。 make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。 1、尽量初始化容量信息 slice由一个指向底层数组的指针、大小和容量构成,如图(图源来自b站幼麟实验室)
如果一个slice未进行初始化,那么它的容量为0,每次往此slice中append元素时,都会进行扩容,扩容规则如下(图源来自b站幼麟实验室)
所以使用slice时,如实现知道它的大致容量,应该初始化它的容量信息,避免内存发生拷贝
2、切片操作导致大内存未释放
因为切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组(图源来自b站幼麟实验室)
当一个小切片一直复用着大底层数组时,大底层数组一直得不到释放,就会占很大内存空间。
解决方案:使用copy替代re-slice
Map
map不支持并发读写,只能并发读
对于需要并发读写map的场景,常见的解决方案如下:
- map + sync.RWMutex
- 采用 sync.map,实现是小粒度的锁+读写分离+原子操作
超出容量时会自动扩容,但尽量提供一个合理的初始值
map有翻倍扩容和等量扩容2种:
翻倍扩容:当该定义下的装载因子达到 6.5 时便需要触发 map 的扩容,此时再不扩容,就会造成hash冲突严重,使map查找速率下降
等量扩容:若溢出桶过多也会触发 map 的扩容, 这是基于这样的考虑, 向 map 中插入大量的元素, 哈希桶将逐渐被填满, 这个过程中也可能创建了一些溢出桶, 但此时装载因子并没有超过设定的阈值, 然后对这些 map 做删除操作, 删除元素之后, map 中的元素数目变少, 使得装载因子降低, 而后又重复上述的过程, 最终使得整体的装载因子不大, 但整个 map 中存在了大量的溢出桶, 因此当溢出桶数目过多时, 即便没有达到装载因子 6.5 的阈值也会触发扩容,其目的是整理map桶。
字符串处理
使用+拼接性能最差,strings.Builder和bytes.Buffer相近,但strings.Builder最快
1、字符串在go语言中是不可变类型,占用内存大小是固定的,使用+会都会重新分配内存
2、strings.Builder和bytes.Buffer的底层都是是同[]byte数组,但strings.Builder只有一次内存分配,而bytes.Buffer需要2次
3、在预知字符串长度时,可以预分配长度来进一步提升性能
空结构体
1、使用空结构体来进行通信
package main
import (
"fmt"
"os"
"time"
)
func main() {
abort := make(chan struct{})
go func() {
os.Stdin.Read(make([]byte, 1)) // read a single byte
abort <- struct{}{}
}()
fmt.Println("Commencing countdown. Press return to abort.")
select {
case <-time.After(10 * time.Second):
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
launch()
}
func launch() {
fmt.Println("Lift off!")
}
2、使用空结构体来实现set
atomic包
锁的实现是由操作系统来实现,属于系统调用
atomic操作通过硬件实现,效率比锁高