internal/race
unsafe
sync:mutex, rwmutex, once, runtime, cond, map, atomic, waitgroup, pool, poolqueue,
StringHeader,SliceHeader
import . "fmt",调用时可以省略包名。
import _ "fmt",只调用fmt的init函数,无法使用fmt包中的变量和函数
import x "fmt",相当于别名。
WaitGroup对象不是一个引用类型
Go语言中,在函数调用时,引用类型(slice、map、interface、channel)都默认使用引用传递
函数接收者要传指针,因为是值传递
goroutine:
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程间切换只需要保存任务的上下文,没有内核的开销。
Goroutine 非常轻量,主要体现在以下两个方面:
上下文切换代价小: Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;
内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;
Golang 程序中可以轻松支持10w 级别的 Goroutine 运行,而线程数量达到 1k 时,内存占用就已经达到 2G。
Go 调度器实现机制:
Go 调度器模型我们通常叫做G-P-M 模型,他包括 4 个重要结构,分别是G、P、M、Sched。
P 的数量由用户设置的 GoMAXPROCS 决定,但是不论 GoMAXPROCS 设置为多大,P 的数量最大为 256。在Go1.5之后GOMAXPROCS被默认设置可用的核数
M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。
M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。
schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取。
调度器循环的机制大致是从各种队列、P 的本地队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 Goexit 做清理工作并回到 M,如此反复。
Go 调度器中有两个不同的运行队列:全局运行队列(GRQ,global runqueue)和本地运行队列(LRQ)。
每个 P 都有一个 LRQ,用于管理分配给在 P 的上下文中执行的 Goroutines,这些 Goroutine 轮流被和 P 绑定的 M 进行上下文切换。GRQ 适用于尚未分配给 P 的 Goroutines。
P的状态有Pidle, Prunning, Psyscall, Pgcstop, Pdead
为了更加充分利用线程的计算资源,Go 调度器采取了以下几种调度策略:
1、任务窃取(work-stealing),当每个 P 之间的 G 任务不均衡时,调度器允许从 GRQ,或者其他 P 的 LRQ 中获取 G 执行。
2、减少阻塞
在 Go 里面阻塞主要分为一下 4 种场景:
场景 1:由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;
场景 2:由于网络请求和 IO 操作导致 Goroutine 阻塞,这种阻塞的情况下,我们的 G 和 M 又会怎么做呢?
Go 程序提供了网络轮询器(NetPoller)来处理网络请求和 IO 操作的问题,其后台通过 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现 IO 多路复用。
网络轮询器使用系统线程,它时刻处理一个有效的事件循环。
G1 想要进行网络系统调用,它被移动到网络轮询器并且处理异步网络系统调用。然后,M 可以从LRQ 执行另外的 Goroutine。当异步网络系统调用由网络轮询器完成,G1 被移回到 P 的 LRQ 中。
用户层眼中看到的 Goroutine 中的“block socket”,实际上是通过 Go runtime 中的 netpoller 通过 Non-block socket +I/O 多路复用机制“模拟”出来的。Go 中的 net 库正是按照这方式实现的。
场景 3:当调用一些系统方法的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用(如文件 I/O)的 Goroutine 将阻塞当前 M。
调度器介入后:识别出 G1 已导致 M1 阻塞,此时,调度器将 M1 与 P 分离,同时也将 G1 带走。然后调度器引入新的 M2 来服务 P。此时,可以从 LRQ 中选择 G2 并在 M2 上进行上下文切换。
阻塞的系统调用完成后:G1 可以移回 LRQ 并再次由 P 执行。如果这种情况再次发生,M1 将被放在旁边以备将来重复使用。
场景 4:如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。
Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。sysmon也是唯一一个脱离GPM模型只需GM即可运行的特例。
只要下次这个 Goroutine 进行函数调用,那么就会被强占,同时也会保护现场,然后重新放入 P 的本地队列里面等待下次执行。
Grunning
所有状态为Grunnable的任务都可能通过findrunnable函数被调度器(P&M)获取,进而通过execute函数将其状态切换到Grunning, 最后调用runtime·gogo加载其上下文并执行。
Gsyscall
Go运行时为了保证高的并发性能,当会在任务执行OS系统调用前,先调用runtime·entersyscall函数将自己的状态置为Gsyscall——如果系统调用是阻塞式的或者执行过久,则将当前M与P分离——当系统调用返回后,执行线程调用runtime·exitsyscall尝试重新获取P,如果成功且当前任务没有被抢占,则将状态切回Grunning并继续执行;否则将状态置为Grunnable,等待再次被调度执行。
Gwaiting
当一个任务需要的资源或运行条件不能被满足时,需要调用runtime·park函数进入该状态,channel、定时器、网络IO操作都可能引起任务的阻塞。
Gdead
最后,当一个任务执行结束后,会调用runtime·goexit结束自己的生命——将状态置为Gdead,并将结构体链到一个属于当前P的空闲G链表中,以备后续使用。
协程泄漏:没有符合预期的退出,直到程序结束,此goroutine才退出
1、死循环
2、channel阻塞
解决pprof可以看到block的协程,哪里创建的。
指针:
限制一:指针不能参与运算。
限制二:不同类型的指针不允许相互转换,不能比较和相互赋值。
unsafe.Pointer与任何类型的指针,与uintptr可以相互转换。
unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型。
uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。
还有一点要注意的是,uintptr 并没有指针的语义,意思就是存储 uintptr 值的内存地址在Go发生GC时会被回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。goroutine 会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个 unsafe.Pointer 是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr 类型的临时变量只是一个普通的数字,所以其值不会该被改变。
map:
触发扩容的条件有二个:
负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
overflow数量 > 2^15时,也即overflow数量超过32768时。
hmap.B指示用了几位hash的低位,也是buckets的长度。
存tophash(8位):快速判断key是否存在。
data区存放的是key-value数据,存放顺序是key/key/key/...value/value/value,如此存放是为了节省字节对齐带来的空间浪费。设想如果是这样的一个map[int64]int8,考虑到字节对齐,会浪费很多存储空间。
增量扩容Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
等量扩容,实际上并不是扩大容量,buckets数量不变,把松散的键值对重新排列一次,以使bucket的使用率更高。在极端场景下,比如不断的增删,而键值对正好集中在一小部分的bucket,这样会造成overflow的bucket数量增多,但负载因子又不高,从而无法执行增量搬迁的情况。
如果当前处于搬迁过程,则优先从oldbuckets查找。
defer:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer return:return 语句并不是一条原子指令
1. 返回值赋值
2. 调用 defer 函数 (在这里是可以修改返回值的)
3. return 返回值
closure:闭包找到的是同一地址中父级函数中对应变量最终的值
recover 必须在 defer 函数中运行。recover 捕获的是祖父级调用时的异常,直接调用时无效, 多层嵌套依然无效.
defer, recover(),
<-time.After(2 * time.Second)
内置函数:
len(), cap(), make(), append(), copy(), delete()
语法:
在多重循环中,可以用标号 label 标出想 break 的循环。
for select配合时,break 并不能跳出循环。
main 函数不能带参数;main 函数不能定义返回值。main 函数所在的包必须为 main 包;main 函数中可以使用 flag 包来获取和解析命令行参数。
在 range 迭代中,得到的值其实是元素的一份值拷贝。
range 迭代 map是无序的。Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。
虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil。
当访问 map 中不存在的 key 时,Go 则会返回元素对应数据类型的零值,比如 nil、'' 、false 和 0,取值操作总有值返回,故不能通过取出来的值,来判断 key 是不是在 map 中。检查 key 是否存在可以用 map 直接访问,检查返回的第二个参数即可。
在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理。
方法施加的对象显式传递,没有被隐藏起来。
go中使用 sort.searchXXX 方法,在排序好的切片中查找指定的方法,但是其返回是对应的查找元素不存在时,待插入的位置下标(元素插入在返回下标前)。
零值:读map没有的key,读关闭的channel
interface, 函数接收者,gc,context,
context:
context 是线程安全的,可在多个 goroutine 中传递;
使用 context 作为函数参数时,需作为第一个参数,并且命名为 ctx;
不要把 context 放在结构体中,要以参数的方式传递;
当不知道传递什么类型 context 时,可以使用 context.TODO();
context 只能被取消一次,应当避免从已取消的 context 衍生 context;
只有父 context 和创建了该 context 的函数才能调用取消函数,避免传递取消函数 cancelFunc;
context取消,所有goroutine都能收到。goroutine 接收到取消信号的方式就是 select 语句中的 读c.done 被选中。因为channel被close,所有goroutine都能读到零值。
context取消,递归取消所有子节点,从父节点中移除自己。
select:
注意:如果 ch1 或者 ch2 信道都阻塞的话,就会立即进入 default 分支,并不会阻塞。但是如果没有 default 语句,则会阻塞直到某个信道操作成功为止。
select语句只能用于信道的读写操作
select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行
对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句
如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句
对于空的select{},会引起死锁
对于for中的select{}, 也有可能会引起cpu占用过高的问题
channel:
如果channel c已经被关闭,继续往它发送数据会导致panic: send on closed channel。
关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值。
通过range读取,channel关闭后for循环会跳出
通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。
wait.group():
我们不能使用Add() 给wg 设置一个负值,否则代码将会报错
WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址
WaitGroup里还对使用逻辑进行了严格的检查,比如Wait()一旦开始不能Add().
waitgroup:某个routine出错
// 创建两个channel,一个用于传递错误,另一个表示WaitGroup是否结束。
errCh := make(chan error)
wgCh := make(chan bool)
go func() {
wg.Wait()
close(wgCh)
}()
// 当有错误返回或WaitGroup执行结束时会被执行。
select {
case <-wgCh:
break
case err := <-errCh://某个goroutine出错,waitgroup不能正常wait()
close(errCh)
panic(err)
}
CSP(communicating sequential processes)并发模型:通过goroutine和channel来实现的
并发控制:
waitgroup
context
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间)
ticker := time.NewTicker(time.Millisecond * 500)
for t := range ticker.C
框架:
beego,GoStub,goconvey
运行空间
启动时间
并发量
在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址。
golang在 1.1 之后引入了竞争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。
要想解决数据竞争的问题可以使用互斥锁sync.Mutex,解决数据竞争(Data race),也可以使用管道解决,使用管道的效率要比互斥锁高。
IDE:
go env -w GOPROXY=https://goproxy.cn清空缓存 go clean --modcache
set GOPROXY=https://goproxy.io