Golang 学习笔记

基础

命名

关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64 bool byte rune string error
内建函数: make len cap new append copy close delete
complex real imag panic recover


break default func interface select case defer go map struct
chan else
const fallthrough continue for
goto package switch if range type
import return var

变量函数驼峰命名

几个单词组成优先使用大小写分隔,而不是下划线分隔

文件名/报名下划线

几个单词组成优先下划线分隔,而不是使用大小写分隔

基本知识

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 当前程序的包名
package main

// 导入其它的包
import std "fmt"

// 常量的定义
const PI = 3.14

// 全局变量的声明与赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 由 main 函数作为程序入口点启动
func main() {
std.Println("Hello world!你好,世界!")
}
  • main包下有main函数
  • import下.可直接使用内部函数不需要类似fmt,或者package取别名
  • import可()导入大批量

常量的定义

  • 常量的值在编译时就已经确定
  • 常量的定义格式与变量基本相同
  • 等号右侧必须是常量或者常量表达式
  • 常量表达式中的函数必须是内置函数

常量的初始化规则与枚举

  • 在定义常量组时,如果不提供初始值,则表示将使用上行的表达式
  • 使用相同的表达式不代表具有相同的值
  • iota是常量的计数器,从0开始,组中每定义1个常量自动递增1
  • 通过初始化规则与iota可以达到枚举的效果
  • 每遇到一个const关键字,iota就会重置为0

new函数

  • new(T)将创建一个T类型的匿名变量
  • 初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
1
2
3
4
p := new(int)   // p, *int 类垄, 挃向匿名的 int 发量
fmt.Println(*p) // "0"
*p=2 // 设置 int 匿名发量的值为 2
fmt.Println(*p) // "2"

由二new叧是一丧预定的函数,它并不是是关键字
因此我们可以将 new 名字重新定义为别的类型。
例如下面的例子:

1
func delta(old, new int) int { return new - old }

变量生命周期

1
2
3
4
5
包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的  

而局部遍历的声明周期是动态的
从每次创建一个新变量声明开始知道该变量不再被引用为止
然后变量的存储空间就会被回收

函数多返回值和多参数值需要最后末尾参数也加,逗号
是为了不会导致编译错误,这是Go编译器的一个特性

1
2
3
4
5
6
7
8
9
var global *int 
func f() {
var x int x=1 global = &x
}

func g() {
y := new(int)
*y = 1
}

f函数x变量必须堆上分配,因为函数退出后通过包一级变量global变量找到
x局部变量在函数f中逃逸了

相反g函数返回,变量y将是不可达,必须马上回收
因此
y并没有存函数g中逃逸,编译器将选择栈上分配*y的存储空间

作用域

语法块内部

1
2
3
4
5
6
7
8
语法快像函数体或循环体花括弧对应的语法块   
语法块内部声明是无法被外部语法块访问
语法决定了内部声明的名字的作用域范围


函数外部声明可在同一个包的任何源文件访问
对于导入包则对应源文件级的作用域
此称为全局域




基本数据类型

整型

1
2
3
4
5
6
7
8
9
10
Go 同时提供了有符号和无符号类垄的整数运算。
返里有 int8、 int16、int32 和 int64 四种截然不同大小的有符号整形数类型
分别对应 8、 16、32、64bit 大小的有符号整形数
不此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整形数类型。


其中int 是应用最广泛的数值类型。
返两种类型都有同样的大 小,32 或 64bit
但是我们不能对此做任何的假设;
因为不同的编诌器即使在相同癿硬件平台上可能产生不同的大小。

浮点数

1
2
3
4
5
6
Go提供两种精度的浮点数,float32或float64

常量 math.MaxFloat32 表示 float32 能表示的最大数值大约是 3.4e38;
对应的math.MaxFloat64 常量大约是 1.8e308。

它们分别能表示癿最小值近似为 1.4e-45 和 4.9e-324




基本命令

Go常用命令简介

  • go get:获取远程包(需 提前安装 git或hg)
  • go run:直接运行程序
  • go build:测试编译,检查是否有编译错误
  • go fmt:格式化源码(部分IDE在保存时自动调用)
  • go install:编译包文件并编译整个程序
  • go test:运行测试文件
  • go doc:查看文档(CHM手册)
1
2
3
4
5
6
默认情况,go build命令构建的指定包和依赖的包
然后丢弃除了最后可执行文件之外所有的中间编译结果

go install 和go build很相似,但是它会保存每个包的编译成果
而不是将它们丢弃,被编译的包会被保存到$GOPATH/pkg目录下
目录路径和src目录路径对应,可执行程序被保存到$GOPATH/bin目录




数组

数组固定了大小

数组Array

  • 定义数组的格式:var [n],n>=0
  • 数组长度也是类型的一部分,因此具有不同长度的数组为不同类型
  • 注意区分指向数组的指针和指针数组
  • 数组在Go中为值类型
  • 数组之间可以使用==或!=进行比较,但不可以使用<或>
  • 可以使用new来创建数组,此方法返回一个指向数组的指针
  • Go支持多维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var a [3] int
//[0, 0, 0]

a := [3]int{1,2,3}
//[1,2,3]

a := [3]int{2:1}
//[0, 0, 1]

a := [3]int{0:1, 1:2, 2:3}
//[1, 2, 3]

a := [...]int{1,2,3}
//[1, 2, 3]

var p *[3] int = &a
//数组的地址
//&[1, 2, 3]

x, y := 1, 2
a := [...]*int(&x, &y)
//[0X111地址, 0X222地址]

//数组=操作是拷贝非地址引用传递


p := new([3]int)
p[0] = 1
//p: &[1, 0, 0]




Slice切片

slice具有len和cap两个,具体为长度和容量
如果容量不够将x2的方式扩展

切片Slice

  • 其本身并不是数组,它指向底层的数组
  • 作为变长数组的替代方案,可以关联底层数组的局部或全部
  • 为引用类型
  • 可以直接创建或从底层数组获取生成
  • 使用len()获取元素个数,cap()获取容量
  • 一般使用make()创建
  • 如果多个slice指向相同底层数组,其中一个的值改变会影响全部
  • make([]T, len, cap)
  • 其中cap可以省略,则和len的值相同
  • len表示存数的元素个数,cap表示容量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a := [3]int{1:2, 2:3}
s1 := a[1:2]
//s1:[2, 3]

s2 := a[1:len(a)]
//s2:[2, 3]

s3 := a[1:]
//s3:[2, 3]

s4 := a[:1]
//s4:[1, 2]

s5 := make([]int, 3, 10)

s6 := []int{1:2}
//[0,2]




map

  • 类似其它语言中的哈希表或者字典,以key-value形式存储数据
  • Key必须是支持==或!=比较运算的类型,不可以是函数、map或slice
  • Map查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍
  • Map使用make()创建,支持 := 这种简写方式
1
2
3
4
5
6
make([keyType]valueType, cap),cap表示容量,可省略
超出容量时会自动扩容,但尽量提供一个合理的初始值
使用len()获取元素个数

键值对不存在时自动添加,使用delete()删除某键值对
使用 for range 对map和slice进行迭代操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)

/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "Paris"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"

/*使用键输出地图值 */ for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}

/*查看元素在集合中是否存在 */
captial, ok := countryCapitalMap [ "美国" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(captial) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("美国的首都是", captial)
} else {
fmt.Println("美国的首都不存在")
}
}

//France 首都是 Paris
//Italy 首都是 罗马
//Japan 首都是 东京
//India 首都是 新德里
//美国的首都不存在




函数

函数function

  • Go 函数 不支持 嵌套、重载和默认参数
  • 但支持以下特性:
1
2
无需声明原型、不定长度变参、多返回值、命名返回值参数
匿名函数、闭包
  • 定义函数使用关键字 func,且左大括号不能另起一行
  • 函数也可以作为一种类型使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func A()(a, b, c int){
a, b, c = 1, 2, 3
return a, b, c
}

func A(a ...int){
fmt.Println(a)
//a [1, 2]
}

a := func(a int){
//匿名函数
}

//函数执行完才执行,并且逆排
defer fmt.Println("a")
defer fmt.Println("b")

// b a

相关值和引用地址传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28


func main(){
s1 := []int {1, 2, 3}
A(s1)
fmt.Println(s1)
}

func A(s[] int){
s[0] = 4
s[1] = 5
s[2] = 6
}
//s1: 4, 5, 6


func main(){
a, b := 1, 2
A(a, b);
fmt.Println(a, b)
}

func A(a ...int){

a[0] = 3
a[1] = 4
}
//a = 1, b = 2

Painc/Recover/defer

  • defer的执行方式类似其它语言中的析构函数,在函数体执行结束后

按照调用顺序的相反顺序逐个执行

  • 即使函数发生严重错误也会执行
  • 支持匿名函数的调用
  • 常用于资源清理、文件关闭、解锁以及记录时间等操作
  • 通过与匿名函数配合可在return之后修改函数计算结果
  • 如果函数体内某个变量作为defer时匿名函数的参数

则在定义defer时即已经获得了拷贝,否则则是引用某个变量的地址

  • Go 没有异常机制,但有 panic/recover 模式来处理错误
  • Panic 可以在任何地方引发,但recover只有在defer调用的函数中有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func A(){
fmt.Println("func A");
}

func B(){
painc("Painc in B");
}

func C(){
fmt.Println("func C")
}

func main(){
A()
B()
C()
}
//Func A
//Painc in B


func B(){
defer func(){
if err := recover(); err!=nil{
fmt.Println("Recover In B")
}
}
painc("Painc in B");
}

//Func A
//Recover In B
//Func C

Panic异常

当panic异常发生,程序中断运行并立即执行该goroutine的defer被延迟函数
随后输出崩溃日志

1
2
3
日志包含panic value和函数调用的堆栈跟踪信息  
runtime.Stack为何能输出被释放的函数堆栈信息
因为延迟函数的调用在释放堆栈信息之前

Recover捕获异常

本不应该去对panic异常做任何处理,但是也许有需求从异常中恢复

1
2
3
当web服务器遇到不可预料的严重问题时
做一些操作,在崩溃前应该将所有的连接关闭
如果不作任何处理,客户端会一直处于等待状态
1
2
3
4
5
6
7
8
func Parse(input string) (s *Syntax, err error) { 
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
// ...parser...
}




Struct

结构struct

  • Go 中的struct与C中的struct非常相似,并且Go没有class
  • 使用 type struct{} 定义结构,名称遵循可见性规则
  • 支持指向自身的指针类型成员
  • 支持匿名结构,可用作成员或定义成员变量
  • 匿名结构也可以用于map的值
  • 可以使用字面值对结构进行初始化
  • 允许直接通过指针来读写结构成员
    -相同类型的成员可进行直接拷贝赋值
  • 支持 == 与 !=比较运算符,但不支持 > 或 <
  • 支持匿名字段,本质上是定义了以某个类型名为名称的字段
  • 嵌入结构作为匿名字段看起来像继承,但不是继承
  • 可以使用匿名字段指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
type person struct{
Name string
Age int
}


type Person struct {
Name string
Age int
Contract struct{
Phone string
}
}

type Person1 struct{
string
int
}


type human struct {
Sex int
}

type student struct{
human
Name string
Age int
}


func main(){
//a := person{}
//a.Name = "张三"
//a.Age = 13

//简便初始化
a := person{
Name:"张三",
Age:13,
}
fmt.Println(a) //13
A(a) //15
fmt.Println(a) //13
B(&a) //15
fmt.Println(a) //15

//或者初始化的时候直接取地址符号
b := &person{
Name:"张三",
Age:10
}


//直接匿名结构
c := &struct{
Name string
Age int
}{
Name:"张三",
Age:13,
}
fmt.Println(c)


//结构套结构
d := & Person{
Name:"张三",
Age: 10,
}
d.Contract.Phone = "13912345678"
fmt.Println(d)


//匿名字段结构
e := &Person1{
"nihao", 10
}

//结构组合,go是无继承的
f := &student{
Name:"张三",
Age: 10,
human:human{
Sex:1,
},
}
//f.human.Sex = 20 如果有名称冲突可以这样用
f.Sex = 10
fmt.Println(f)
}




//只是值的拷贝
func A(per person){
per.Age = 15
fmt.Println("A", per)
}

func B(per *person){
per.Age = 15;
fmt.Println("A", per)
}




Method

结构中带有method

  • Go 中虽没有class,但依旧有method
  • 通过显示说明receiver来实现与某个类型的组合
  • 只能为同一个包中的类型定义方法
  • Receiver 可以是类型的值或者指针
  • 不存在方法重载
  • 可以使用值或指针来调用方法,编译器会自动完成转换
  • 从某种意义上来说,方法是函数的语法糖,因为receiver其实就是

    方法所接收的第1个参数(Method Value vs. Method Expression)

  • 如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
  • 类型别名不会拥有底层类型所附带的方法
  • 方法可以调用结构中的非公开字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

type A struct{
Name string
}

type B struct{
Name string
}



func StructMethodTest(){
a := A{
Name :"张三",
}
a.Say()

b := B{
Name:"李四",
}
b.Say()

fmt.Println(a)
fmt.Println(b)


//不仅仅struct类型 仅仅一个int也能支持增加method方法
tz := TZ(10)
tz.Say()
(*TZ).Say(&tz)
(*TZ).Eat(&tz, 10)
}

func(b B)Say(){
b.Name = "say test"
fmt.Println("I'm say in "+ b.Name)
}

func(a *A)Say(){
//如果name小写则会在package范围内才能修改
a.Name = "say test"
fmt.Println("I'm " +a.Name)
}

//func(a A)Say(string msg){
// fmt.Println("I'm " + a.Name + " " + msg)
//}
//这是不可行,不支持重载

type TZ int

func(tz *TZ)Say(){
fmt.Println("haha tz")
}

func(tz *TZ)Eat(numb int){
fmt.Println("hhahaha")
}




接口 interface

  • 接口是一个或多个方法签名的集合
  • 只要某个类型拥有该接口的所有方法签名,即算实现该接口

    无需显示声明实现了哪个接口,这称为 Structural Typing

  • 接口只有方法声明,没有实现,没有数据字段
  • 接口可以匿名嵌入其它接口,或嵌入到结构中
  • 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个

    复制品的指针,既无法修改复制品的状态,也无法获取指针

  • 只有当接口存储的类型和对象都为nil时,接口才等于nil
  • 接口调用不会做receiver的自动转换
  • 接口同样支持匿名字段方法
  • 接口也可实现类似OOP中的多态
  • 空接口可以作为任何类型数据的容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

type USB interface{
Name() string
Connecter
}

type PhoneUSB struct{
name string

}

//嵌入接口
type Connecter interface{
Connect()
}

func (phoneUsb PhoneUSB)Name() string{
return phoneUsb.name
}

func (phoneUsb PhoneUSB) Connect(){
phoneUsb.name = "手机连接成功"
fmt.Println("connect")
}

func Disconnect(usb USB){
if mobile, ok := usb.(PhoneUSB); ok{
fmt.Println("Disconnect "+ mobile.Name())
}

//用switch去判断
switch usb.(type) {
case Connecter:
fmt.Println("this is connecter")
case PhoneUSB:
fmt.Println("this is phoneUsb")
default:
fmt.Println("Unknow")
}
}

func TestInterface(){
usb := PhoneUSB{
name:"手机",
}
usb.Connect()
Disconnect(usb)
}




反射

反射reflection

  • 反射可大大提高程序的灵活性,使得 interface{} 有更大的发挥余地
  • 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
  • 反射会将匿名字段作为独立字段(匿名字段本质)
  • 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
  • 通过反射可以”动态”调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

type User struct {
Id int
Name string
}

func(u User) Hello(){
fmt.Println(u.Name)
}

func(u User) Say(msg string){
fmt.Printf("I'm say %v \n", msg)
}

func Set(o interface{}){
v := reflect.ValueOf(o)

//判断类型是否指针 && 是否可写
if k:=v.Kind();k==reflect.Ptr&&!v.Elem().CanSet(){
fmt.Println("XXX")
}else{
v = v.Elem()
}

//字段为string并且是否有找到
if f:=v.FieldByName("Name"); f.Kind() == reflect.String && f.IsValid(){
f.SetString("Set")
}

}

func Info(o interface{}){
t := reflect.TypeOf(o)
v := reflect.ValueOf(o)

//判断当前对象是否属于某个对象
if k:=t.Kind(); k!= reflect.Struct{
fmt.Println("当前不为Struct,无法反射")
return
}else{
fmt.Println("当前匹配成功")
}

fmt.Println("Type:", t.Name())

for i := 0; i< t.NumField(); i++ {
f := t.Field(i)
val := v.Field(i).Interface()
fmt.Printf("%5s %3v %3v\n", f.Name, f.Type, val)
}

//这里的Method和Field都是要公共方法和字段
for j := 0; j<t.NumMethod(); j++ {
m := t.Method(j)
fmt.Printf("%6s : %v \n", m.Name, m.Type)
}
}


type Manager struct {
User
title string
}

func TestReflect(){
user := User{1, "李四"}
Info(user)

//取复杂字段的操作
u := Manager{User: User{1, "ok"}, title:"title"}
t := reflect.TypeOf(u)
fmt.Printf("%v \n", t.Field(0))
fmt.Printf("%v \n", t.FieldByIndex([]int{0,0}))
fmt.Printf("%v \n", t.FieldByIndex([]int{0,1}))
fmt.Printf("%v \n", t.Field(1))

//基本类型操作
x := 123
v := reflect.ValueOf(&x)
v.Elem().SetInt(999)
fmt.Println(x)

//user复杂struct操作
Set(&user)
fmt.Println(user.Name)
userV := reflect.ValueOf(user)
//取args等信息
method := userV.MethodByName("Say")
args := []reflect.Value{reflect.ValueOf("Hi")}
method.Call(args)
}




并发

基础

1
2
3
4
5
6
7
8
9
10
从源码的解析来看,goroutine 只是由官方实现的超级"线程池"而已。   
不过话说回来,每个实例 4-5KB 的栈内存占用和由于实现机制而大幅
减少的创建和销毁开销,是制造 Go 号称的高并发的根本原因。
另外,goroutine 的简单易用,也在语言层面上给予了开发者巨大的便利。

并发主要由切换时间片来实现"同时"运行
在并行则是直接利用多核实现多线程的运行
但 Go 可以设置使用核数,以发挥多核计算机的能力。

Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

Channel

Channel 是 goroutine 沟通的桥梁,大都是阻塞同步的
通过 make 创建,close 关闭
Channel 是引用类型
可以使用 for range 来迭代不断操作 channel
可以设置单向或双向通道
可以设置缓存大小,在未被填满前不会发生阻塞

Select

可处理一个或多个 channel 的发送与接收
同时有多个可用的 channel时按随机顺序处理
可用空的 select 来阻塞 main 函数
可设置超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//channel 完成了消息的发生
v := make(chan int)
go func() {
fmt.Println("go")
v <- 10
}()
fmt.Println(<- v)




//channel 完成了消息的发生使用for range迭代不断操作channel
//必须要close
v := make(chan int)
go func() {
fmt.Println("go")
v <- 10
close(v)
}()
//fmt.Println(<- v)

for v := range v{
fmt.Println(v)
}

goroutine

大致结构

img

Go的调度器内部有三个重要的结构:M,P,S;

  • M:代表真正的内核OS线程,和POSIX里的thread差不多,真正干活的人
  • G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
  • P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑



运行情况

普通运行

img

有2个物理线程M,每一个M都拥有一个context(P),每一个也都有一个正在运行的goroutine

P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。

1
2
3
4
如果是IO类型程序,设置GOMAXPROCS就像一个多路复用器
goroutine设置为大于CPU的核数,io性能提升明显,直到达到最大的IOPS(磁盘每秒输入输出量)

如果是CPU类型程序,默认即可,默认值已经设置为CPU的核数

图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。
P维护着这个队列(称之为runqueue)

Go语言里,启动一个goroutine很容易:go function 就行

所以每有一个go语句被执行runqueue队列就在其末尾加入一个

goroutine在下一个调度点,就从runqueue中取出一个goroutine执行。

阻塞运行

img

当一个OS线程被阻塞时,P可以转而投奔另一个OS线程!

1
2
3
当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。 
调度器保证有足够的线程来运行所有的context P。
图中的M1可能是被创建,或者从线程缓存中取出。

M空闲

当有M0空闲的时候,它必须尝试取得一个context P来运行goroutine,
一般情况下,它会从其他的OS线程那里偷一个context P过来。

1
2
3
4
如果没有偷到的话,它就把goroutine放在一个global runqueue里
然后自己就去睡大觉了(放入线程缓存里)。
Context P们也会周期性的检查global runqueue
否则global runqueue上的goroutine永远无法执行。

P空闲

1
2
3
4
5
另一种情况是P所分配的任务G很快就执行完了(分配不均)
这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。
但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。
如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半
这就确保了每个OS线程都能充分的使用。

转自知乎

网络调用

所有网络io调用都与调度器集成在一起了

也就是说所有网络io操作都是nonblocking的

数据没收完时G不会占用物理线程

而网络poller(epoll,kqueue)发现G又有数据可读时会重新将G放回runqueue

所以调度器直接支持的网络层是这样的工作的。

自己基于epoll写大概也是这个样子。

结构体

结构体M

M是machine的缩写,是对机器的抽象,每个m都是对应到一条操作系统的物理线程

M必须关联了P才可以执行Go代码,但是当它处理阻塞或者系统调用中时,可以不需要关联P。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct M
{
G* g0; // 带有调度栈的goroutine
G* gsignal; // signal-handling G 处理信号的goroutine
void (*mstartfn)(void);
G* curg; // M中当前运行的goroutine
P* p; // 关联P以执行Go代码 (如果没有执行Go代码则P为nil)
P* nextp;
int32 id;
int32 mallocing; //状态
int32 throwing;
int32 gcing;
int32 locks;
int32 helpgc; //不为0表示此m在做帮忙gc。helpgc等于n只是一个编号
bool blockingsyscall;
bool spinning;
Note park;
M* alllink; // 这个域用于链接allm
M* schedlink;
MCache *mcache;
G* lockedg;
M* nextwaitm; // next M waiting for lock
GCStats gcstats;
...
};

结构体G

  • 栈信息:stackbase,stackguard
  • 上下文:sched
  • 运行函数:fnstart
  • 传递参数:param
  • goroutineId标识:goid

栈信息stackbase和stackguard,有运行的函数信息fnstart。
这些就足够成为一个可执行的单元了,只要得到CPU就可以运行。
goroutine切换时,上下文信息保存在结构体的sched域中,goroutine是轻量级的线程或者称为协程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct G
{
uintptr stackguard; // 分段栈的可用空间下界
uintptr stackbase; // 分段栈的栈基址
Gobuf sched; //goroutine切换时,利用sched域来保存上下文
uintptr stack0;
FuncVal* fnstart; // goroutine运行的函数
void* param; // 用于传递参数,睡眠时其它goroutine设置param,唤醒时此goroutine可以获取
int16 status; // 状态Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
int64 goid; // goroutine的id号
G* schedlink;
M* m; // for debuggers, but offset not hard-coded
M* lockedm; // G被锁定只能在这个m上运行
uintptr gopc; // 创建这个goroutine的go表达式的pc
...
};

结构体P

Processor的缩写。P的加入是为了提高Go程序的并发度,实现更好的调度。M代表OS线程。P代表Go代码执行时需要的资源。

当M执行Go代码时,它需要关联一个P,当M为idle或者在系统调用中时,它也需要P。有刚好GOMAXPROCS个P。所有的P被组织为一个数组,在P上实现了工作流窃取的调度器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct P
{
Lock;
uint32 status; // Pidle或Prunning等
P* link;
uint32 schedtick; // 每次调度时将它加一
M* m; // 链接到它关联的M (nil if idle)
MCache* mcache;

G* runq[256];
int32 runqhead;
int32 runqtail;

// Available G's (status == Gdead)
G* gfree;
int32 gfreecnt;
byte pad[64];
};


//结构体P中也有相应的状态:
Pidle,
Prunning,
Psyscall,
Pgcstop,
Pdead,

goroutine优势

在程序中任何对系统API的调用,都会被runtime层拦截来方便调度。Golangruntime实现了goroutineOS 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
2
3
4
5
6
7
1、首先创建三个集合:白、灰、黑。
2、将所有对象放入白色集合中。
3、然后从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合。
4、之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。
5、重复4直到灰色中无任何对象。
6、通过写屏障检测对象有变化,重复以上操作。
7、回收所有白色对象

何时触发 GC

  • 自动垃圾回收

    1
    在堆上分配大于 32K byte 对象的时候进行检测此时是否满足垃圾回收条件,如果满足则进行垃圾回收。再判断满足gcTrigger的条件
  • 主动垃圾回收

    1
    通过调用runtime.GC(),这是阻塞式的

gcTrigger/GC触发条件

空间

memstats.heap_live >= memstats.gc_trigger

当前堆上的活跃对象大于我们初始化时候设置的 GC 触发阈值

memstats.gc_trigger相关的值如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trigger := ^uint64(0)
if gcpercent >= 0 {
trigger = uint64(float64(memstats.heap_marked) * (1 + triggerRatio))
minTrigger := heapminimum
if trigger < minTrigger {
trigger = minTrigger
}
}
memstats.gc_trigger = trigger


//minTrigger默认是4MB*GOGC/100,而GOGC默认为100,所有这个minTrigger为4MB
//uint64(float64(memstats.heap_marked) * (1 + triggerRatio))
//而triggerRatio的规则复杂,其大体结论是根据当前与上次的heap size的比例来决定,默认情况下是GOGC=100,即新增一倍就会触发

时间

Golang程序运行时,会启动一个forcegc的helper goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//在gcTriggerTime该模式下需要满足当前时间距离上一次GC时间需大于forcegcperiod
//这个值在Golang里面为两分钟var forcegcperiod int64 = 2 * 60 * 1e9

func init() {
go forcegchelper()
}

func forcegchelper() {
forcegc.g = getg()
for {
lock(&forcegc.lock)
if forcegc.idle != 0 {
throw("forcegc: phase error")
}
atomic.Store(&forcegc.idle, 1)
goparkunlock(&forcegc.lock, "force gc (idle)", traceEvGoBlock, 1)
if debug.gctrace > 0 {
println("GC forced")
}
gcStart(gcBackgroundMode, gcTrigger{kind: gcTriggerTime, now: nanotime()})
}
}

func sysmon() {
// ......
if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
lock(&forcegc.lock)
forcegc.idle = 0
forcegc.g.schedlink = 0
injectglist(forcegc.g)
unlock(&forcegc.lock)
}
}




内存管理

基础内存

如何得知变量是分配在栈(stack)上还是堆(heap)上?

1
2
3
4
5
准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数 return 之后,变量不再被引用,则将其分配到栈上。

数据结构

关键数据结构:

  • mspan:作为内存管理的基本单位而存在

    其数据结构为若干连续内存页,一个双端链表的形式,里面存储了它的一些位置信息。通过一个基地址+(页号*页大小),就可以定位到这个mspan的实际内存空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    type 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
    14
    type 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
    13
    type 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
2
3
4
5
6
func main() {
var a int64 = 3
var b float64 = float64(a)
fmt.Println(&a) // 0xc42000e248
fmt.Println(&b) // 0xc42000e250
}

如果我们要来做一个强制的转化的话,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
11
func 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
7
func 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
2
func netpollinit();     //初始化轮询器
func netpollopen(fd uintptr, pd *pollDesc) int32; //为fdpd启动边缘触发通知

pollDesc包含了2个二进制的信号,分别负责读写goroutine的暂停,该信号的两个状态:

  • pdReady:IO就绪通知,一个goroutine将状态置为nil来消费一个通知;
  • pdWait:一个goroutine准备暂停在信号上,但是还没有完成暂停。
1
2
3
4
const (
pdReady uintptr = 1
pdWait uintptr = 2
)

当一个goroutine进行io阻塞时,会去被放到等待队列。
这里面就关键的就是建立起文件描述符和goroutine之间的关联。
pollDesc结构体就是完成这个任务的。它的结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type pollDesc struct {
link *pollDesc
lock mutex
fd uintptr
closing bool
seq uintptr
rg uintptr
rt timer
rd int64
wg uintptr
wt timer
wd int64
user uint32
}

lock锁对象保护了
pollOpen,pollSetDeadline,pollUnblock和deadlineimpl操作。
而这些操作又完全包含了对seq,rt,tw变量。

fd在PollDesc整个生命过程中都是一个常量。
处理pollReset,pollWait,pollWaitCanceledruntime.netpollready(IO就绪通知)不需要用到锁
所以closing,rg,rd,wg和wd的所有操作都是一个无锁的操作。

读取操作

当从网络连接的文件描述符读取数据时,调用system call,循环从fd.sysfd读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (fd *FD) Read(p []byte) (int, error) {
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return 0, err
}
if fd.IsStream && len(p) > maxRW {
p = p[:maxRW]
}
for {
n, err := syscall.Read(fd.Sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN && fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
}

读取的时候只处理EAGAIN类型的错误,其他错误一律返回给调用者,因为对于非阻塞的网络连接的文件描述符,如果错误是EAGAIN,说明Socket的缓冲区为空,未读取到任何数据,则调用fd.pd.WaitRead

1
2
3
4
5
6
7
8
9
10
11
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}

func (pd *pollDesc) wait(mode int, isFile bool) error {
if pd.runtimeCtx == 0 {
return errors.New("waiting for unsupported file type")
}
res := runtime_pollWait(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}

res是runtime_pollWait函数返回的结果,由conevertErr函数包装后返回,
其中0表示io已经准备好了,1表示链接已经关闭,2表示io超时。再来看看pollWait的实现

调用netpollblock来判断IO是否准备好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
for {
old := *gpp
if old == pdReady {
*gpp = 0
return true
}
if old != 0 {
throw("runtime: double wait")
}
if atomic.Casuintptr(gpp, 0, pdWait) {
break
}
}
if waitio || netpollcheckerr(pd, mode) == 0 {
gopark(netpollblockcommit, unsafe.Pointer(gpp), "IO wait", traceEvGoBlockNet, 5)
}
old := atomic.Xchguintptr(gpp, 0)
if old > pdWait {
throw("runtime: corrupted polldesc")
}
return old == pdReady
}

返回true说明IO已经准备好,返回false说明IO操作已经超时或者已经关闭。
否则当waitio为false,且io不出现错误或者超时才会挂起当前goroutine。
最后的gopark函数,就是将当前的goroutine(调用者)设置为waiting状态。

阻塞中唤醒操作

goroutine的调度在sysmon中会不断地调用epoll函数,

1
2
3
4
5
6
7
8
9
lastpoll := 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)
}
}

这里的netpoll会根据操作系统的不同而调用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
2
3
4
5
6
它会计算包级变量的初始化表达式和执行导入包的init初始化函数
这时候又需要抑制"unused import"编译错误
所以就需要用下划线_重命名导入的包

数据库包 database/sql 也是采用了类似癿技术
让用户可以根据自己需要选择导入必要的数据库驱动
1
import _ "image/png" // register PNG decoder




性能分析

pprof搜集

  • pprof web端
  • pprof 手动

pprof 手动搜集资源消耗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//需要在运行项目时,在参数中声明 -cpuprofile cpu.prof -memprofile mem.prof
//获取命令行入参
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func main(){
go cpuProfile()
//程序的退出函数
quit()
}
---------------------------------------------------------------------
func quit(){
//结束CPU记时
log.Info("停止CPU运行记录")
pprof.StopCPUProfile()
//记录当前内存状况 以主协程运行 开子协程若主协程执行完 将无法记录下内存状态
memProfile()
}

func cpuProfile() {
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Error("创建CPU运行记录文件失败: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Error("创建CPU运行状态记录失败: ", err)
}
}
}

func memProfile() {
if *memprofile != "" {
f, err := os.Create(*memprofile)
defer f.Close()
if err != nil {
log.Error("创建内存使用情况记录文件失败: ", err)
}
//runtime.GC() // 调用GC清理内存 可以查看GC后的内存使用情况
if err := pprof.WriteHeapProfile(f); err != nil {
log.Error("获取内存使用情况失败: ", err)
}
}
}

web搜集pprof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"net/http"
_ "net/http/pprof"
"sync"
)

func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
wg.Wait()
}

pprof分析

  • cpu性能分析
  • 内存性能分析
  • 阻塞分析

CPU性能分析

当 CPU 性能分析启用后,Go runtime 会每 10ms 就暂停以下,记录当前运行的 Goroutine 的调用堆栈及相关数据。

内存性能分析

内存性能分析则是在堆(Heap)分配的时候,记录一下调用堆栈。默认512kb进行 一次采样,当我们认为数据不够细致时,可以调节采样率runtime.MemProfileRate,就意味着分析器将会在每分配指定的字节数量后对内存使用情况进行取样

栈(Stack)分配 由于会随时释放,因此不会被内存分析所记录。

由于内存分析是取样方式,并且也因为其记录的是分配内存,而不是使用内存。因此使用内存性能分析工具来准确判断程序具体的内存使用是比较困难的。

阻塞性能分析

阻塞分析是一个很独特的分析。它有点儿类似于 CPU 性能分析,但是它所记录的是 goroutine 等待资源所花的时间。

阻塞分析对分析程序并发瓶颈非常有帮助。阻塞性能分析可以显示出什么时候出现了大批的 goroutine 被阻塞了。阻塞包括:

  • 发送、接受无缓冲的 channel;
  • 发送给一个满缓冲的 channel,或者从一个空缓冲的 channel 接收;
  • 试图获取已被另一个 go routine 锁定的 sync.Mutex 的锁;
1
2
3
4
5
// rate = 1 时, 统计所有的 block event, 
// rate <=0 时,则关闭block profiling
// rate > 1 时,为 ns 数,阻塞时间t>rate的event 一定会被统计,小于rate则有t/rate 的几率被统计
// 参考 https://github.com/golang/go/blob/release-branch.go1.9/src/runtime/mprof.go#L397
runtime.SetBlockProfileRate(1 * 1000 * 1000)

pprof文件分析

在runtime/pprof生成对应的方法后,在命令行中使用go tool pprof工具可以对profile文件进行性能分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go tool pprof XXX.prof #即可分析对应prof文件

go tool pprof localhost:[port]/debug/pprof/profile #cpu性能分析
go tool pprof localhost:[port]/debug/pprof/heap #内存性能分析数据
go tool pprof localhost:[port]/debug/pprof/block #阻塞性能分析

进入命令行后
top [number] #输出对应前N位的数据信息
web #使用grapgviz生成对应svg图
web > [name].svg #生成svg图后修改名字 存放到同级目录下
list func #显示耗时几个函数

#使用go-torch进行性能分析 svg更好的表现了其中的关系但若想要直观看到所使用资源的占比 falmeGraph会更为直观
go-torch xxx.prof #生成对应火焰图 X轴显示占用资源量 Y轴显示调用栈深度

另外可用go-torch 专门针对cpu文件进行火焰图分析

1
go-torch -u http://localhost:6060 -t 30

链接