Golang基础知识
golang
map
实现:哈希查找表,通过链表法解决哈希冲突
- 随机遍历,顺序无法预测
- 扩容特点:逐步进行,新,旧bucket
- 遍历中出现扩容
- 遍历前已经开始扩容
slice
底层结构:本质上是引用类型,底层结构有三个变量:len,cap,底层数组的指针
- 空切片时,cap=len=0
- 一般初始化方式
a:=[]int{}
cap=len=初始化长度 - 使用make进行初始化:
a:=make([]int,4,5)
:cap=5,len=4,a为[0,0,0,0] var a []int
默认cap=len=0
append操作
- cap足够,直接修改len,追加
- cap不足时…
- cap足够时,一次追加多个和多次追加一个结果相同,cap不足时会出现不一样的结果(cap的结果不同,多次追加一个的情况可能会更大)
切片截取操作:
- 切片进行截取时,数组容量的末尾和原切片数组末尾对齐,数组地址为截取单元的首地址,且可以超过其len的范围进行截取,当其中一个数组发生扩容时,另一个数组的地址不变
切片扩容时会改变:cap和数字指针
垃圾回收
- 引用计数,0回收,无法处理 循环引用
- 标记-清除: 需要STW
- 分代收集:按照生存周期老/新不同的算法,效率高,算法设计复杂
- 三色标记法:白-垃圾,灰-遍历到的对象,黑,遍历灰色,标记为黑色并将引用的变量标记为灰色,直到没有灰色
- 每次GC循环时,不需要将所有对象移动到白色区域,只要将黑色和白色的颜色互换即可,更高效
- STW:影响性能<1ms
- 写屏障技术缩短STW
- 引用对象丢失:黑色节点添加了指向白色结点的引用,但无法被扫描
- 破坏以下二者之一:
- dijistra写屏障(强三色不变性),不允许黑色节点引用白色节点,引用则将白色节点改为灰色
- yuasa写屏障(弱三色不变性),白色节点被删除了一个引用时,认为会被黑色节点新增引用(悲观),设置为灰色
go的垃圾回收器是和主程序并行的,关键在于三色标记法能让系统的gc暂停时间能够预测
GPM调度和CSP模型
不要以共享内存的方式来通信,要以通信的方式来共享内存
- CSP模型:以通信的方式共享内存(channel进行通信)
GPM含义
- G:go协程Goroutine
- M:工作线程,CPU数量
- P:处理器,用来调度G,M的关联关系,M拥有P才能执行G的代码
Goroutine的调度策略
- 队列轮转:P周期性地调度G到M中运行,一段时间后保存上下文切换(队列)
- 系统调用:G0即将进入系统调用时,M0释放P,某个M1获得P运行剩下的G,G0结束后等待其他的P调度(或空闲P),此后进入缓存池睡眠
Chan原理(channel)
type hchan struct {
qcount uint // 队列中的总元素个数
dataqsiz uint // 环形队列大小,即可存放元素的个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 //每个元素的大小
closed uint32 //标识关闭状态
elemtype *_type // 元素类型
sendx uint // 发送索引,元素写入时存放到队列中的位置
recvx uint // 接收索引,元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex //互斥锁,chan不允许并发读写
}
- 写数据
- recvq不为空,缓冲区无数据/无缓冲区,则直接从recvq取出G,写入数据,并把G唤醒
- 缓冲区有空余位置,写入缓冲区
- 缓冲区无空余位置,将数据写入G,将当前G加入sendq,进入睡眠被唤醒
- 读数据
- sendq不为空,且无缓冲区,直接从sendq去除G,把G数据读走并唤醒
- sendq不为空(缓冲区已满),从缓冲区首部读出数据,将G中的数据写入缓冲区尾部,唤醒G
- 缓冲区有数据,从缓冲区读取数据
- 将当前G加入recvq进入睡眠
- 关闭channel
- 唤醒recvq中所有的G,本该写入数据的内容为nil,sendq中的G唤醒(会出现panic)
- panic出现的场景
- 关闭值为nil的channel
- 关闭已经关闭的channel
- 向已关闭的channel写入数据
无缓冲区情况:读和写同步(会阻塞)
context上下文结构
并发安全,树状的goroutine
只定义了接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- deadline:到达ddl自动发起取消请求
- Done:返回只读的channel,如果可以读取说明已经发出取消信号,可以清理并释放
- err:返回被取消的原因
- value:获取context上绑定的值,key-value
竞态与内存逃逸
竞态:在程序中,同一个内存块被多个gorountine访问
- 解决:对资源进行加锁,sync.Mutex,sync.RWMutex
- 检测:添加
-race
逃逸分析:内存的分配位置由编译器决定,分配速度慢且会形成内存碎片
以下场景
- 指针逃逸
- 栈空间不足逃逸
- 动态类型逃逸
- 闭包引用对象逃逸
零碎
安全读写共享变量方式
- Mutex锁
- goroutine通过channel
new和make的区别
- make用来分配和初始化类型为slice,map,chan的数据,new任意数据并返回内存指针
- make返回引用,即type,分配空间后进行初始,new分配的空间会被清零
对nil的slice和空silce的处理区别
- slice:=make([]int,0):空slice,不为0
- slice:=[]int{} 值是nil,保证返回slice的函数异常时仍可保证返回nil
协程,进程,线程的区别
进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
golang的内存模型中,为什么小对象多了会加大GC压力
小对象过多会导致GC三色法消耗过多CPU
解决思路:减少对象分配
channel为什么可以做到线程安全
- 先进先出,本身用来在多任务间传递数据,通过通信共享内存,有天然优势
GC触发条件
- 手动触发(主动):调用runtime.GC,阻塞式
- 被动触发:
- 系统监控,超过两分钟没有GC则触发
- pacing(步调)算法,控制内存增长的比例,每次分配内存时检测是否以达到阈值,默认100%
goroutine的数量查看和限制
- 查看: GOMAXPROCS控制未被阻塞的所有goroutine,通过GOMAXPROCE即可查看
- 限制:使用channel,每次执行goroutine前向通道写入值
Channel是同步还是异步的
异步的
- channel的状态:
- nil,初始或手动赋值,无法关闭(panic),send/recv被永久阻塞
- closed,关闭或send会panic,recv不会阻塞
- active,可关闭,send/recv
goroutine 和线程的区别
- 线程可以有多个goroutine
- 线程,进程都是同步的,协程是异步的
- 协程可以保留上一次调用的状态
- 协程需要线程来承载运行,不能代替线程
- 线程是分割的CPU资源,协程是组织好的代码流程
struct能不能比较
- 相同struct类型可以
- 不同struct不能比较,编译不通过
go主协程如何等待其他协程
用sync.WaitGroup,内部实现计数器计数未完成的操作个数,Add()添加计数,Done()计数减一,Wait()等待所有操作结束,计数为0(立即返回)
slice扩容
append追加元素,空间不足则扩容,重新分配slice
扩容规则(只对容量)
- 小于1024:扩容时翻倍,超过1024,增长因子变为1.25
- 容量够用则追加,len++
- 容量不够用,先扩容再追加
map顺序读取
(一般是随机的)
先把map中的key用sort排序后根据key读取
值接收者和指针接收者
方法的接收者
- 值类型:可以调用值接收者的方法和指针接收者的方法
- 指针类型:可以调用值接收者的方法和指针接收者的方法
接口实现不同:
- 值类型接口:类型本身和该类型的指针类型都实现了该接口
- 指针类型接口:只有对应的指针类型才被认为实现了接口
通常使用指针作为方法的接收者
- 能够修改接收者指向的值
- 避免每次调用方法时复制该值,更高效
发生内存泄漏的原因
- goroutine需要维护用户代码的上下文信息,运行过程中需要消耗一定的内存来保存此类信息,如果一个程序不断产生goroutine,不结束已创建的goroutine并复用该部分内存,会造成内存泄漏
协程泄露:(某段代码卡住,陷入死循环等)应该被释放的协程没有被正确释放
如何检测内存泄漏
自带的工具:pprof,或者用Gops检测当前运行的go程占用的资源
两个nil可能不相等
接口interface是对接口值的封装,内部包含类型T和值V,一个接口为nil当且仅当T=nil,V=unset
两个接口比较时,先比较T再比较V(接口值和非接口值进行比较会将非接口值转化为接口值)
例如var p *int =nil
转化为接口值T=*int,显然和值为nil的接口不相等
函数传参是值类型还是引用类型
只存在值传递(值或指针的副本),都会开辟新的空间
不要混淆值传递,引用传递和值类型,引用类型
内存对齐
CPU都是以字长访问的(32位,64位),不进行内存对齐会增加CPU访问内存的次数,内存对齐对实现变量的原子性操作有好处(并发场景下)
两个interface的比较
- 判断类型是否一样:
reflect.TypeOf(a).Kind()
- 判断接口是否相等:
reflect.DeepEqual(a,b,interface{})
- 将interface赋值给另一个:
reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))
打印%v
,%+v
,%#v
的区别
(输出struct中的元素)
- %v输出所有的值
- %+v先输出字段名字,再输出该字段的值(name:value)
- %#v先输出结构体名字,再输出结构体(name:value)
rune类型
go的字符有以下两种
- uint8(byte),表示ASCII的字符
- rune类型,表示UTF-8字符,等价于int32
string底层通过byte数组实现,对string求len计算了字节长度
[]rune(str) :string转化为rune类型,统计长度示例:
- “hello 你好” string length=12(汉字3个),rune length=8
空struct{}占用的空间
使用unsafe.Sizeof(struct{})=0,不占用任何内存空间
空struct的用途
优点:不占内存空间,通常被当作占位符
- map作为集合使用(
type set map[string]struct{}
) - 不发送数据的channel,只是通知子协程执行任务或控制协程并发(
make(chan struct{})
) - 有可能结构体中只包含方法,不含任何字段
变量的分配位置在堆上还是栈上
由编译器决定,如果无法判断变量作用域和大小,通常会分配到堆上(堆上的变量在函数出栈自行释放,无需gc)
select执行
select:多个可用操作:随机选一个
- select 中只要有一个 case 能 return,则立刻执行
- 当如果同一时间有多个 case 均能 return 则伪随机方式抽取任意一个执行
- 如果没有一个 case 能 return 则可以执行”default” 块
array和slice的区别
- array:固定长度,长度是数组类型的一部分,需指定大小或根据初始化值确定
- slice:可变长度,三个属性:指针,长度,容量,通过make初始化,可以扩容
defer的作用
在调用普通函数或方法前加上关键词defer即可。defer被执行时,defer后面的函数被延迟执行,直到包含该defer语句的函数执行完毕(不管return结束还是panic结束),即为在函数返回之前调用,defer是在return之前完成的。
常被用于处理成对的操作,保证资源能被释放等(打开/关闭,连接/断开,加锁/释放锁)
释放资源的defer跟在请求资源的语句后
最后面的defer最先被调用(类似于栈)
return xxx并不是一条原子语句:先给返回值赋值,再调用defer语句,例如:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
返回值为5
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
输出结果
打印后
打印中
打印前
panic:触发异常
go关键字
go func(x,y,z)
只有fun(x,y,z)在新的goroutine中运行,参数的计算在原goroutine中完成。
var和:=定义变量的区别
:=
只能在声明局部变量使用,var
无限制
代码
type student struct {
Name string
Age int
}
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
}
错误:for _, stu := range stus {m[stu.Name] = &stu}
一句中,stu实际上是副本,所以此处返回的均为同一个地址。
修改:
for i:=0;i<len(stus);i++ {
m[stus[i].Name] = &stus[i]
}
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
输出结果 A:
均为输出 10,B:
从 0~9 输出 (顺序不定)
defer多层嵌套
func main() {
fmt.Println("A")
defer func() {
fmt.Println("B")
defer fmt.Println("C")
fmt.Println("D")
}()
defer fmt.Println("E")
fmt.Println("F")
}
结果输出顺序:AFEDBC
func main() {
for i:=0; i<5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
输出:5 5 5 5 5
func main() {
for i:=0; i<5; i++ {
defer func(i int) {
fmt.Println(i)
}(i)
}
}
输出:4 3 2 1 0
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
输出
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4