基础
命名
关键字
1 | 内建常量: true false iota nil |
变量函数驼峰命名
几个单词组成优先使用大小写分隔,而不是下划线分隔
文件名/报名下划线
几个单词组成优先下划线分隔,而不是使用大小写分隔
基本知识
- 16位整型:int16/uint16
- 长度:2字节
- 取值范围:-32768~32767/0~65535
- 32位整型:int32(rune)/uint32
- 长度:4字节
- 取值范围:-2^32/2~2^32/2-1/0~2^32-1
- 64位整型:int64/uint64
- 长度:8字节
- 取值范围:-2^64/2~2^64/2-1/0~2^64-1
- 浮点型:float32/float64
- 长度:4/8字节
- 小数位:精确到7/15小数位
- 复数:complex64/complex128
- 长度:8/16字节
足够保存指针的 32 位或 64 位整数型:uintptr
其它值类型:
- array、struct、string
引用类型:
- slice、map、chan
接口类型:inteface
- 函数类型:func
1 | // 当前程序的包名 |
- main包下有main函数
- import下
.
可直接使用内部函数不需要类似fmt
,或者package
取别名 - import可
()
导入大批量
常量的定义
- 常量的值在编译时就已经确定
- 常量的定义格式与变量基本相同
- 等号右侧必须是常量或者常量表达式
- 常量表达式中的函数必须是内置函数
常量的初始化规则与枚举
- 在定义常量组时,如果不提供初始值,则表示将使用上行的表达式
- 使用相同的表达式不代表具有相同的值
- iota是常量的计数器,从0开始,组中每定义1个常量自动递增1
- 通过初始化规则与iota可以达到枚举的效果
- 每遇到一个const关键字,iota就会重置为0
new函数
- new(T)将创建一个T类型的匿名变量
- 初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
1 | p := new(int) // p, *int 类垄, 挃向匿名的 int 发量 |
由二new叧是一丧预定的函数,它并不是是关键字
因此我们可以将 new 名字重新定义为别的类型。
例如下面的例子:1
func delta(old, new int) int { return new - old }
变量生命周期
1 | 包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的 |
函数多返回值和多参数值需要最后末尾参数也加,逗号
是为了不会导致编译错误,这是Go编译器的一个特性
1 | var global *int |
f函数x变量必须堆上分配,因为函数退出后通过包一级变量global变量找到
x局部变量在函数f中逃逸了
相反g函数返回,变量y将是不可达,必须马上回收
因此y并没有存函数g中逃逸,编译器将选择栈上分配*y的存储空间
作用域
语法块内部
1 | 语法快像函数体或循环体花括弧对应的语法块 |
基本数据类型
整型
1 | Go 同时提供了有符号和无符号类垄的整数运算。 |
浮点数
1 | Go提供两种精度的浮点数,float32或float64 |
基本命令
Go常用命令简介
- go get:获取远程包(需 提前安装 git或hg)
- go run:直接运行程序
- go build:测试编译,检查是否有编译错误
- go fmt:格式化源码(部分IDE在保存时自动调用)
- go install:编译包文件并编译整个程序
- go test:运行测试文件
- go doc:查看文档(CHM手册)
1 | 默认情况,go build命令构建的指定包和依赖的包 |
数组
数组固定了大小
数组Array
- 定义数组的格式:var
[n] ,n>=0 - 数组长度也是类型的一部分,因此具有不同长度的数组为不同类型
- 注意区分指向数组的指针和指针数组
- 数组在Go中为值类型
- 数组之间可以使用==或!=进行比较,但不可以使用<或>
- 可以使用new来创建数组,此方法返回一个指向数组的指针
- Go支持多维数组
1 | var a [3] int |
Slice切片
slice具有len和cap两个,具体为长度和容量
如果容量不够将x2的方式扩展
切片Slice
- 其本身并不是数组,它指向底层的数组
- 作为变长数组的替代方案,可以关联底层数组的局部或全部
- 为引用类型
- 可以直接创建或从底层数组获取生成
- 使用len()获取元素个数,cap()获取容量
- 一般使用make()创建
- 如果多个slice指向相同底层数组,其中一个的值改变会影响全部
- make([]T, len, cap)
- 其中cap可以省略,则和len的值相同
- len表示存数的元素个数,cap表示容量
1 | a := [3]int{1:2, 2:3} |
map
- 类似其它语言中的哈希表或者字典,以key-value形式存储数据
- Key必须是支持==或!=比较运算的类型,不可以是函数、map或slice
- Map查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍
- Map使用make()创建,支持 := 这种简写方式
1 | make([keyType]valueType, cap),cap表示容量,可省略 |
1 | func main() { |
函数
函数function
- Go 函数 不支持 嵌套、重载和默认参数
- 但支持以下特性:
1 | 无需声明原型、不定长度变参、多返回值、命名返回值参数 |
- 定义函数使用关键字 func,且左大括号不能另起一行
- 函数也可以作为一种类型使用
1 | func A()(a, b, c int){ |
相关值和引用地址传递
1 |
|
Painc/Recover/defer
- defer的执行方式类似其它语言中的析构函数,在函数体执行结束后
按照调用顺序的相反顺序逐个执行
- 即使函数发生严重错误也会执行
- 支持匿名函数的调用
- 常用于资源清理、文件关闭、解锁以及记录时间等操作
- 通过与匿名函数配合可在return之后修改函数计算结果
- 如果函数体内某个变量作为defer时匿名函数的参数
则在定义defer时即已经获得了拷贝,否则则是引用某个变量的地址
- Go 没有异常机制,但有 panic/recover 模式来处理错误
- Panic 可以在任何地方引发,但recover只有在defer调用的函数中有效
1 | func A(){ |
Panic异常
当panic异常发生,程序中断运行并立即执行该goroutine的defer被延迟函数
随后输出崩溃日志
1 | 日志包含panic value和函数调用的堆栈跟踪信息 |
Recover捕获异常
本不应该去对panic异常做任何处理,但是也许有需求从异常中恢复
1 | 当web服务器遇到不可预料的严重问题时 |
1 | func Parse(input string) (s *Syntax, err error) { |
Struct
结构struct
- Go 中的struct与C中的struct非常相似,并且Go没有class
- 使用 type
struct{} 定义结构,名称遵循可见性规则 - 支持指向自身的指针类型成员
- 支持匿名结构,可用作成员或定义成员变量
- 匿名结构也可以用于map的值
- 可以使用字面值对结构进行初始化
- 允许直接通过指针来读写结构成员
-相同类型的成员可进行直接拷贝赋值 - 支持 == 与 !=比较运算符,但不支持 > 或 <
- 支持匿名字段,本质上是定义了以某个类型名为名称的字段
- 嵌入结构作为匿名字段看起来像继承,但不是继承
- 可以使用匿名字段指针
1 | type person struct{ |
Method
结构中带有method
- Go 中虽没有class,但依旧有method
- 通过显示说明receiver来实现与某个类型的组合
- 只能为同一个包中的类型定义方法
- Receiver 可以是类型的值或者指针
- 不存在方法重载
- 可以使用值或指针来调用方法,编译器会自动完成转换
- 从某种意义上来说,方法是函数的语法糖,因为receiver其实就是
方法所接收的第1个参数(Method Value vs. Method Expression)
- 如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
- 类型别名不会拥有底层类型所附带的方法
- 方法可以调用结构中的非公开字段
1 |
|
接口 interface
- 接口是一个或多个方法签名的集合
- 只要某个类型拥有该接口的所有方法签名,即算实现该接口
无需显示声明实现了哪个接口,这称为 Structural Typing
- 接口只有方法声明,没有实现,没有数据字段
- 接口可以匿名嵌入其它接口,或嵌入到结构中
- 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个
复制品的指针,既无法修改复制品的状态,也无法获取指针
- 只有当接口存储的类型和对象都为nil时,接口才等于nil
- 接口调用不会做receiver的自动转换
- 接口同样支持匿名字段方法
- 接口也可实现类似OOP中的多态
- 空接口可以作为任何类型数据的容器
1 |
|
反射
反射reflection
- 反射可大大提高程序的灵活性,使得 interface{} 有更大的发挥余地
- 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
- 反射会将匿名字段作为独立字段(匿名字段本质)
- 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
- 通过反射可以”动态”调用方法
1 |
|
并发
基础
1 | 从源码的解析来看,goroutine 只是由官方实现的超级"线程池"而已。 |
Channel
Channel 是 goroutine 沟通的桥梁,大都是阻塞同步的
通过 make 创建,close 关闭
Channel 是引用类型
可以使用 for range 来迭代不断操作 channel
可以设置单向或双向通道
可以设置缓存大小,在未被填满前不会发生阻塞
Select
可处理一个或多个 channel 的发送与接收
同时有多个可用的 channel时按随机顺序处理
可用空的 select 来阻塞 main 函数
可设置超时
1 | //channel 完成了消息的发生 |
goroutine
大致结构
Go的调度器内部有三个重要的结构:M,P,S;
- M:代表真正的内核OS线程,和POSIX里的thread差不多,真正干活的人
- G:代表一个
goroutine
,它有自己的栈,instruction pointer
和其他信息(正在等待的channel等等),用于调度。 - P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑
运行情况
普通运行
有2个物理线程M,每一个M都拥有一个context(P)
,每一个也都有一个正在运行的goroutine
。
P的数量可以通过GOMAXPROCS()
来设置,它其实也就代表了真正的并发度,即有多少个goroutine
可以同时运行。
1 | 如果是IO类型程序,设置GOMAXPROCS就像一个多路复用器 |
图中灰色的那些goroutine
并没有运行,而是出于ready的就绪态,正在等待被调度。
P维护着这个队列(称之为runqueue
)
Go语言里,启动一个goroutine很容易:go function 就行
所以每有一个go语句被执行runqueue队列就在其末尾加入一个
goroutine在下一个调度点,就从runqueue中取出一个goroutine执行。
阻塞运行
当一个OS线程被阻塞时,P可以转而投奔另一个OS线程!
1 | 当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。 |
M空闲
当有M0空闲的时候,它必须尝试取得一个context P来运行goroutine,
一般情况下,它会从其他的OS线程那里偷一个context P过来。
1 | 如果没有偷到的话,它就把goroutine放在一个global runqueue里 |
P空闲
1 | 另一种情况是P所分配的任务G很快就执行完了(分配不均) |
转自知乎
网络调用
所有网络io调用都与调度器集成在一起了
也就是说所有网络io操作都是nonblocking的
数据没收完时G不会占用物理线程
而网络poller(epoll,kqueue)发现G又有数据可读时会重新将G放回runqueue
所以调度器直接支持的网络层是这样的工作的。
自己基于epoll写大概也是这个样子。
结构体
结构体M
M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程
M必须关联了P才可以执行Go代码,但是当它处理阻塞或者系统调用中时,可以不需要关联P。
1 | struct M |
结构体G
- 栈信息:stackbase,stackguard
- 上下文:sched
- 运行函数:fnstart
- 传递参数:param
- goroutineId标识:goid
栈信息stackbase和stackguard,有运行的函数信息fnstart。
这些就足够成为一个可执行的单元了,只要得到CPU就可以运行。
goroutine切换时,上下文信息保存在结构体的sched域中,goroutine是轻量级的线程或者称为协程。
1 | struct G |
结构体P
Processor的缩写。P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。
当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好GOMAXPROCS个P。所有的P被组织为一个数组,在P上实现了工作流窃取的调度器。
1 | struct P |
goroutine优势
在程序中任何对系统
API
的调用,都会被runtime
层拦截来方便调度。Golang
的runtime
实现了goroutine
和OS thread
的M:N模型
其实
goroutine
用到的就是线程池的技术,当goroutine
需要执行时,会从thread pool
中选出一个可用的M
或者新建一个M
。而thread pool
中如何选取线程,扩建线程,回收线程,Go Scheduler
进行了封装,对程序透明,只管调用就行,从而简化了thread pool
的使用。Python coroutine
只会使用一个线程,所以只能利用单核。Goroutine
可以被多个线程调度,可以利用多核。
goroutine的并发模型定义为以下几个要点:
- 基于Thread的轻量级协程
- 通过channel来进行协程间的消息传递
- 只暴露协程,屏蔽线程操作的接口
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3中线程对应模型,也就是:1:1,1:N,M:N。
- N:1是说,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核。
- 1:1是说,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢,频繁切换效率很低。
- M:N是说, 多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。
垃圾回收
首先对于常见的垃圾回收算法做个简单的介绍:
- 引用计数:这是一种最简单的垃圾回收算法,对每一个对象维护一个应用的计数,当引用该对象的对象被销毁的时候,这个被引用对象的引用计数会自动减一;当有被引用的对象被创建时,计数器加一。当计数器为0的时候,就回收该对象。该GC算法的最大的好处是将内存的管理和用户程序的执行放在一起,这样可以把GC的代价分散到整个程序,不会出现STW;而且可以做到对象很快被回收,不像其他算法在heap被耗尽或者达到某一个阈值才回收。但是缺点是该算法不能处理循环引用;而且在实时地维护引用计数时会一定程度上需要额外资源。其中Python和PHP就是使用的该GC方式。
- 标记-清除:这是一个很古老的算法了,该算法分为两个步骤,首先从根变量迭代遍历所有被引用的对象,能够访问到的对象会被标记为“被引用”;然后会对没有标记过的进行回收。优点是解决了引用计数的不足。缺点则是需要STW,而且垃圾回收后可能存在大量的磁盘碎片。标记-清除算法后面有了一种变种三色标记法,Golang现在就是使用的该算法,后面我们再细说。
- 分代收集:分代收集的思想是把heap两个或者多个代空间,新创建的对象存放在称为新生代中,随着垃圾回收的重复执行,生命周期较长的对象会被提升到老年代中,对于新生代的区域的垃圾回收频率要明显高于老年代区域。这样对不同的区域可以使用不用回收算法,这样可以达到更优的性能,但是其缺点是实现太复杂。该算法在JVM的各种GC算法中大量被运用到。
Golang#GC
Golang的GC经历了以下几代的发展
- v1.1 STW
- v1.3 Mark STW, Sweep 并行
- v1.5 三色标记法
- v1.8 hybrid write barrier
Golang的GC的过程其实就是一个三色标记法的实现,对于三色标记法,”三色”的概念可以简单的理解为:
- 白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
- 灰色:正在搜索的对象
- 黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
其过程可以大体总结为:
1 | 1、首先创建三个集合:白、灰、黑。 |
何时触发 GC
自动垃圾回收
1
在堆上分配大于 32K byte 对象的时候进行检测此时是否满足垃圾回收条件,如果满足则进行垃圾回收。再判断满足gcTrigger的条件
主动垃圾回收
1
通过调用runtime.GC(),这是阻塞式的
gcTrigger/GC触发条件
空间
memstats.heap_live >= memstats.gc_trigger
当前堆上的活跃对象大于我们初始化时候设置的 GC 触发阈值
memstats.gc_trigger
相关的值如下
1 | trigger := ^uint64(0) |
时间
Golang程序运行时,会启动一个forcegc的helper goroutine
1 | //在gcTriggerTime该模式下需要满足当前时间距离上一次GC时间需大于forcegcperiod |
内存管理
基础内存
如何得知变量是分配在栈(stack)上还是堆(heap)上?
1 | 准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。 |
数据结构
关键数据结构:
mspan:作为内存管理的基本单位而存在
其数据结构为若干连续内存页,一个双端链表的形式,里面存储了它的一些位置信息。通过一个基地址+(页号*页大小),就可以定位到这个mspan的实际内存空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16type mspan struct {
next *mspan
prev *mspan
list *mSpanList
startAddr uintptr
npages uintptr // span中的页的数量
manualFreeList gclinkptr
freeindex uintptr
nelems uintptr // span中块的总数目
allocCache uint64
state mSpanState // span有四种状态:_MSpanDead,_MSpanInUse,_MSpanManual,_MSpanFree
elemsize uintptr // 通过spanClass或者npages算出来mcache:mcache是绑定在每个P上面的内存
主要用于小对象,正因为是每个P私有的,所以分配的时候就不用加锁。per-P cache,可以认为是 local cache。
1
2
3
4
5
6
7
8
9
10
11
12
13
14type mcache struct {
next_sample int32
local_scan uintptr
tiny uintptr
tinyoffset uintptr
local_tinyallocs uintptr
alloc [numSpanClasses]*mspan
stackcache [_NumStackOrders]stackfreelist
local_nlookup uintptr
local_largefree uintptr local_nlargefree uintptr
local_nsmallfree [_NumSizeClasses]uintptr
}mcentral:全局cache,如果P里面的mcache不够用的时候向 mcentral 申请。
其中nonempty是mspan的双向链表,表示当前mcentral中可用的mspan list;而empty是已经被用了的mspan list,或者是在mcache里面已经被缓存了。注意这里有一个lock,因为不同于mcache,mcentral是全局的,会存在多个P访问mcentral的情况,所以这里的lock是非常有必要的。
1
2
3
4
5
6
7
8
9
10
11
12
13type mcentral struct {
lock mutex
spanclass spanClass
nonempty mSpanList
empty mSpanList
nmalloc uint64
}
type mSpanList struct {
first *mspan
last *mspan
}mheap:当mcentral 也不够用的时候,通过 mheap 向操作系统申请。
1
2
3
4
5
6
7在初始化的时候,mheap会被初始化一个全局变量mheap_。可以看到其内存布局为:
+--------------+----------+-------------------------+
| spans .......| bitmap | arena ..................|
+--------------+----------+-------------------------+
arena是Golang中用于分配内存的连续虚拟地址区域。堆上申请的所有内存都来自arena。操作系统常见有两种做法标志内存可用:一种是用链表将所有的可用内存都串起来;另一种是使用位图来标志内存块是否可用。
总结分配逻辑
总结一下Golang内存分配的逻辑:
- object size>32KB, 则直接使用mheap来分配空间;
- object size<16Byte, 则通过mcache的tiny分配器来分配;
- object size在上面两者之间,首先尝试通过sizeclass对应的分配器分配;
- 如果mcache没有空闲的span,则向mcentral申请空闲块;
- 如果mcentral也没空闲块,则向mheap申请并进行切分;
- 如果mheap也没合适的span,则向操作系统申请。
指针操作/unsafe
简介
Pointer与uintptr
- unsafe.Pointer:只是一个指针的类型,但是不能像C中的指针那样作计算,而只能用于转化不同类型的指针;如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收;
- uintptr:是可以用于指针运算的,但是无法持有对象,GC并不把uintptr当做指针,所以uintptr类型的目标会被回收。
例子
通过unsafe.Pointer来转化类型
在此之前提示一下这里我们说的类型的转化,是转化前后变量为同一变量,而不是这样为两个变量:
1 | func main() { |
如果我们要来做一个强制的转化的话,a = float64(a),Golang会报错:cannot use float64(a) (type float64) as type int64 in assignment。
使用unsafe.Pointer来将T1转化为T2,一个大致的语法为(T2)(unsafe.Pointer(&t1))1
2
3
4
5
6
7
8
9
10
11func main() {
var n int64 = 3
var pn = &n // n的指针
var pf = (*float64)(unsafe.Pointer(pn)) // 通过Pointer来将n的类型转为float
fmt.Println(*pf) // 2.5e-323
*pf = 3.5
fmt.Println(n) // 4615063718147915776
fmt.Println(pf) // 0xc42007a050
fmt.Println(pn) // 0xc42007a050
}
这个例子虽然没有实际的意义,但是绕过了Golang类型系统和内存安全,将一个变量的类型作了转化。
通过uintptr来计算偏移量
我们可以通过增减偏移量来定位不同的成员变量1
2
3
4
5
6
7func main() {
a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1]) // index为1的元素
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0])) // 拿到index为3的指针
*(*int)(p3) = 4 // 重新赋值
fmt.Println(a) // a = [0 1 2 4]
}
网络/netpoller
所谓的netpoller,其实是Golang实利用了OS提供的非阻塞IO访问模式,并配合epll/kqueue等IO事件监控机制;
为了弥合OS的异步机制与Golang接口的差异,而在runtime
上做的一层封装。以此来实现网络IO优化。
实际的实现(epoll
/kqueue
)必须定义以下函数:
1 | func netpollinit(); //初始化轮询器 |
pollDesc包含了2个二进制的信号,分别负责读写goroutine的暂停,该信号的两个状态:
- pdReady:IO就绪通知,一个goroutine将状态置为nil来消费一个通知;
- pdWait:一个goroutine准备暂停在信号上,但是还没有完成暂停。
1 | const ( |
当一个goroutine进行io阻塞时,会去被放到等待队列。
这里面就关键的就是建立起文件描述符和goroutine之间的关联。
pollDesc结构体就是完成这个任务的。它的结构体定义如下
1 | type pollDesc struct { |
lock锁对象保护了pollOpen,pollSetDeadline,pollUnblock和deadlineimpl
操作。
而这些操作又完全包含了对seq,rt,tw
变量。
fd在PollDesc整个生命过程中都是一个常量。
处理pollReset,pollWait,pollWaitCanceled
和runtime.netpollready(IO就绪通知)
不需要用到锁
所以closing,rg,rd,wg和wd
的所有操作都是一个无锁的操作。
读取操作
当从网络连接的文件描述符读取数据时,调用system call,循环从fd.sysfd读取数据
1 | func (fd *FD) Read(p []byte) (int, error) { |
读取的时候只处理EAGAIN类型的错误,其他错误一律返回给调用者,因为对于非阻塞的网络连接的文件描述符,如果错误是EAGAIN,说明Socket的缓冲区为空,未读取到任何数据,则调用
fd.pd.WaitRead
1 | func (pd *pollDesc) waitRead(isFile bool) error { |
res是runtime_pollWait函数返回的结果,由conevertErr函数包装后返回,
其中0表示io已经准备好了,1表示链接已经关闭,2表示io超时。再来看看pollWait的实现
调用netpollblock来判断IO是否准备好了
1 | func netpollblock(pd *pollDesc, mode int32, waitio bool) bool { |
返回true说明IO已经准备好,返回false说明IO操作已经超时或者已经关闭。
否则当waitio为false,且io不出现错误或者超时才会挂起当前goroutine。
最后的gopark函数,就是将当前的goroutine(调用者)设置为waiting状态。
阻塞中唤醒操作
goroutine的调度在sysmon中会不断地调用epoll函数,1
2
3
4
5
6
7
8
9lastpoll := int64(atomic.Load64(&sched.lastpoll))
now := nanotime()
if lastpoll != 0 && lastpoll+10*1000*1000 < now {
atomic.Cas64(&sched.lastpoll, uint64(lastpoll), uint64(now))
gp := netpoll(false)
if gp != nil {
injectglist(gp)
}
}
这里的netpol
l会根据操作系统的不同而调用epll/kqueue,寻找到IO就绪的socket文件描述符,
并找到这些socket文件描述符对应的轮询器中附带的信息,
根据这些信息将之前等待这些socket文件描述符就绪的goroutine状态修改为Grunnable。
执行完netpoll之后,会找到一个就绪的goroutine列表,接下来将就绪的goroutine加入到调度队列中,等待调度运行。也就是injectglist(gp)
的作用,把g放到sched中去执行,
底层仍然是调用的之前在goroutine里面提到的startm
函数。
总结
netpoller的最终的效果就是用户层阻塞,底层非阻塞。
当goroutine读或写阻塞时会被放到等待队列,这个goroutine失去了运行权,
但并不是真正的整个系统“阻塞”于系统调用。
而通过后台的poller不停地poll,所有的文件描述符都被添加到了这个poller中的,
当某个时刻一个文件描述符准备好了,poller就会唤醒之前因它而阻塞的goroutine,于是goroutine重新运行起来。
优势
不同于使用Unix系统中的select或是poll方法,Golang的netpoller查询的是能被调度的goroutine而不是那些函数指针、包含了各种状态变量的struct等,这样就不用管理这些状态,也不用重新检查函数指针等,这些都是你在传统Unix网络I/O需要操心的问题。
其他
包和工具
Go语言编译器的编译速度明显快于其他编译语言主要得益于以下三点
- 所有导入的包必须在每个文件开头显式声明
- 禁止包的环状依赖,因为没有循环依赖,包的依赖关系就形成有向无环图
包的独立编译,诱导了并发编译
编译后的包目标文件不仅仅记录包本身的导出信息,目标文件还记录包的依赖关系
因此编译包的时候,编译器只需读取每个直接导入包的目标文件,而不需要遍历所有依赖的文件
包的匿名导入
有时候我们只是想利用导入包而产生的副作用
1 | 它会计算包级变量的初始化表达式和执行导入包的init初始化函数 |
1 | import _ "image/png" // register PNG decoder |
性能分析
pprof搜集
- pprof web端
- pprof 手动
pprof 手动搜集资源消耗
1 | //需要在运行项目时,在参数中声明 -cpuprofile cpu.prof -memprofile mem.prof |
web搜集pprof
1 | package main |
pprof分析
- cpu性能分析
- 内存性能分析
- 阻塞分析
CPU性能分析
当 CPU 性能分析启用后,Go runtime 会每 10ms
就暂停以下,记录当前运行的 Goroutine 的调用堆栈及相关数据。
内存性能分析
内存性能分析则是在堆(Heap)分配的时候,记录一下调用堆栈。默认512kb进行 一次采样,当我们认为数据不够细致时,可以调节采样率runtime.MemProfileRate,就意味着分析器将会在每分配指定的字节数量后对内存使用情况进行取样
栈(Stack)分配 由于会随时释放,因此不会被内存分析所记录。
由于内存分析是取样方式,并且也因为其记录的是分配内存,而不是使用内存。因此使用内存性能分析工具来准确判断程序具体的内存使用是比较困难的。
阻塞性能分析
阻塞分析是一个很独特的分析。它有点儿类似于 CPU 性能分析,但是它所记录的是 goroutine 等待资源所花的时间。
阻塞分析对分析程序并发瓶颈非常有帮助。阻塞性能分析可以显示出什么时候出现了大批的 goroutine 被阻塞了。阻塞包括:
- 发送、接受无缓冲的 channel;
- 发送给一个满缓冲的 channel,或者从一个空缓冲的 channel 接收;
- 试图获取已被另一个 go routine 锁定的
sync.Mutex
的锁;
1 | // rate = 1 时, 统计所有的 block event, |
pprof文件分析
在runtime/pprof生成对应的方法后,在命令行中使用go tool pprof工具可以对profile文件进行性能分析。
1 | go tool pprof XXX.prof #即可分析对应prof文件 |
另外可用go-torch 专门针对cpu文件进行火焰图分析
1 | go-torch -u http://localhost:6060 -t 30 |
国内查看评论需要代理~