环境变量:
GOPATH:
window下默认值路径为%USERPROFILE%/go,可以删掉新建,然后所有的项目代码放在src子目录下
GOPATH路径下有三个目录src pkg bin
具体的子代码放在src/xxx/xxx.go,这样就可以go mod init了
GOROOT:
是我们安装go开发包的路径
默认情况下 GOROOT下的bin目录及GOPATH下的bin目录都已经添加到环境变量中,我们可能需要修改对应的path为自定义GOPATH/bin
Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,不再强制把代码必须写在GOPATH下面的src目录
在任意路径go mod init即可
项目目录结构:
个人开发:
GOPATH/src/项目1/模块A
流行的项目结构:
GOPATH/src/github.com/czl/项目1/模块A
GOPATH/src/golang.org/czl/项目1/模块B
项目的具体模块划分:
api:
configs
database
docs项目文档
middleware
model
repository
router
service
utils项目工具
client
cmd可执行文件目录
config
scripts执行各种构建、安装、分析等操作的脚本
build
utils
第二种项目划分:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
企业:
GOPATH/src/code.xxx.com/前端组/项目1/模块A
基础知识:
1.变量定义后必须使用
var age int;
如果没有赋值,则是零值
场景:
推荐使用较多
2.省略类型:
变量定义并初始化时,可以省略掉type,自动推断 var name = "string"
场景:
使用得很少,除非同时声明多个变量。
3.简短声明:
:= 是一个声明语句,左侧如果没有声明新的变量,就产生编译错误
简短变量声明语句中必须至少要声明一个新的变量
场景:
只能用在一个函数内部,而package级别的变量不应该这么做
4.匿名变量:
_ := func(),避免必须用到这个值
5.声明并初始化:
var age int = 29
a :=[3]int {1,2,3} # 不用使用new和make来创建了
a := []xxx{yyy} # 可以直接使用[]type{type{value1,value2},type{value1,value2}}这种声明并初始化的方式
6.编码风格:
换行:
不需要分号作为语句或者声明结束,除非要在一行中将多个语句、声明隔开
在编译时,编译器会主动在一些特定的符号(译注:比如行末是,一个标识符、一个整数、浮点数、虚数、字符或字符串文字、关键字break、continue、fallthrough或return中的一个、运算符和分隔符++、--、)、]或}中的一个) 后添加分号,所以在哪里加分号合适是取决于Go语言代码的。
go语言编译器会自动在以标识符、数字字面量、字母字面量、字符串字面量、特定的关键字(break、continue、fallthrough和return)、增减操作符(++和--)、或者一个右括号、右方括号和右大括号(即)、]、})结束的非空行的末尾自动加上分号。
所以,要注意多行的写法问题,比如下面的写法是不对的。
x := []int{
1, 2, 3,
4, 5, 6
}
gofmt工具进行格式化:
手动运行:go fmt xxx.go
goimports:
会自动地添加你代码里需要用到的import声明以及需要移除的import声明。
go get golang.org/x/tools/cmd/goimports
许多编辑器都可以集成goimports工具,然后在保存文件的时候自动运行。
go vet工具:
会做代码静态检查发现可能的bug或者可疑的构造。vet是Go tool套件的一部分
7.注释:
多行注释
8.命名:
名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的.
包本身的名字一般总是用小写字母。
推荐使用 驼峰式 命名
9.作用域:
短声明:
不可在package作用域内使用
局部的短声明变量将屏蔽外部的声明
var cwd string
func init(){cwd, err := os.Getwd()}
语法块:由花括弧所包含的一系列语句
全局语法块
包语法决
每个for、if和switch语句的语法决,显式的部分是for的循环体部分词法域,另外一个隐式的部分则是循环的初始化部分
每个switch或select的分支也有独立的语法决
显式书写的语法块
控制流标号:
就是break、continue或goto语句后面跟着的那种标号,则是函数级的作用域。
次序:
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。
10.生命周期:
对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,在局部变量的声明周期则是动态的:从每次创建一个新变量的声明语句开始,直到该变量不再被引用为止
11.元素赋值:
map查找(§4.3)、类型断言(§7.10)或通道接收(§8.4.2)出现在赋值语句的右边,它们都可能会产生两个结果,有一个额外的布尔结果表示操作是否成功
v, ok = m[key]
v, ok = x.(T)
v, ok = <-ch
并不一定是产生两个结果,也可能只产生一个结果。对于值产生一个结果的情形,map查找失败时会返回零值,类型断言失败时会发送运行时panic异常,通道接收失败时会返回零值(阻塞不算是失败)。
v = m[key]
v = x.(T)
v = <-ch
_, ok = m[key]
_, ok = mm[""], false
_ = mm[""]
new和make:
概述:
new和make都是用来内存分配的原语。
make:
定义:
make用于引用类型的初始化,申请堆内存空间,如slice map channel
但是接口(多态,指向子类实现)、指针、函数除外(引用类型,依据:可以是nil)
示例:
slice := make([]int, 0, 100)
hash := make(map[int]bool, 10)
ch := make(chan int, 5)
参数传递:
作为参数类型时,传递的是指针的值,相当于传了引用
栈引用指向堆内存空间
如何回收?
指向nil即可0号堆内存
new:
定义:
根据传入的类型分配一片内存空间,初始化为对应类型的零值,并返回指向这片内存空间的指针
等价于:
var v 类型
i := &v
对于值类型:
结构体:得到结构体的零值指针。new函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵活
基础类型:
var a = new(int) 得到不为nil的指针,稍后即可*a = 3来使用。
不等同于var a *int,该写法得到的是int类型的指针,零值,为nil。
对比var a int,得到的是零值
对于引用类型:
new可以用于引用类型,返回的是申请内存空间后的对应类型的非nil指针,是可以直接使用len和cap。
对于slice和map,返回的是nil slice和nil map,申请内存长度为0,cap为0
slice:
var a = new([]int) 返回nil slice的指针。*a==nil成立。
var a *[]int返回的是nil slice的指针,零值,为nil。
var a []int返回的是nil slice(零值),内存地址为空0x0。
map:
类似分析
对于channel,new之后返回的是nil channel,读写会阻塞,panic错误为deadlock!
示例:
c := new(Person)
c = &Person{"xuxiaofeng",26}
相当于
var c *Person
c = &Person{"xuxiaofeng",26}
数据类型:
概述:
基础类型、复合类型、引用类型和接口类型
基础类型:
数字、字符串和布尔型
复合数据类型:
数组和结构体
引用类型:
指针、切片、字典、函数、通道,对程序中一个变量或状态的间接引用
接口类型:
bool:
int uinit:
int8、int16、int32和int64 1 Byte = 8 Bits,分别占据8\16\32\64bits,占据1\2\4\8bytes
uint8、uint16、uint32和uint64
还有两种一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint,32或64bit,因为不同的编译器即使在相同的硬件平台上可能产生不同的大小。
整数环绕
比较大的数可以用uint64\float64\big包setstring
示例:d := new(big.Int) d.SetString("240000",10)10进制
尽管Go语言提供了无符号数和运算,即使数值本身不可能出现负数我们还是倾向于使用有符号的int类型,就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择。
无符号类型i>= 0则永远为真
出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。
float:
float32和float64
浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
通常应该优先使用float64类型,因为float32类型的累计计算误差很容易扩散,并且float32能精确表示的正整数并不是很大。
很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分:
math.IsNaN用于测试一个数是否是非数NaN,math.NaN和任何数都是不相等的
复数:
complex64和complex128
内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部
var x complex128 = complex(1, 2)
fmt.Println(real(x*y))
x := 1 + 2i
uintptr:
一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。
字符byte:
写法:
单引号
字符串字面值(包含转义字符)
类型:
byte:代表了ASCII码的一个字符,uint8的别名,变长字节。
rune:unicode字符,采用4个字节存储,utf8.RuneCountInString()适合统计多字节的字符的长度,int32的一个类型别名。
需要注意的是对于非ASCII,索引更新的步长将超过1个字节。
Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。不然需要r, size :=utf8.DecodeRuneInString(s[i:])取出字节的长度
错误的UTF8编码输入生成一个特别的Unicode字符'\uFFFD'
UTF8字符串作为交换格式是非常方便的,但是在程序内部采用rune序列可能更方便,因为rune大小一致,支持数组索引和方便切割。
r := []rune(s) # 解码为Unicode字符序列
string(r) # UTF8编码
默认字符推断类型
string:
写法:
双引号""
反引号``:转义字符,也可以用于定义多行字符串
len函数:
内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),对于非ASCII字符的UTF8编码会要两个或多个字节
常量池:
需要自己用map实现,不像java,string interning(字符串驻留)是内置模式。
标准库:
bytes参数是[]byte类型,还提供了Buffer类型用于字节slice的缓存,bytes.Buffer的WriteRune/WriteByte
strings提供了Contains、Compare等方法
strconv提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
strconv.Itoa(“整数到ASCII”)
FormatInt和FormatUint函数可以用不同的进制来格式化数字
Sprintf的%b、%d、%o和%x等参数提供功能更强大
strconv包的Atoi或ParseInt、ParseUint
unicode包,提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,参数为rune类型
path和path/filepath包提供了关于文件路径名更一般的函数操作
索引:
"sss"[0]返回的是字节uint8
[0:]返回子字符串
遍历:
for _,one := range "ssss"{}返回的也是字节
不变性:
不变性意味如果两个字符串共享相同的底层数据的话也是安全的,这使得复制任何长度的字符串代价是低廉的。
修改:
转为[]byte,完成后再转为string
如何转?类型转换:[]byte("test")
%c 打印字符
%v 打印实际类型的值。
%d int变量
%x, %o, %b 分别为16进制,8进制,2进制形式的int
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔变量:true 或 false
%c rune (Unicode码点),Go语言里特有的Unicode字符类型
%s string
%q 带双引号的字符串 "abc" 或 带单引号的 rune 'c'
%v 会将任意变量以易读的形式打印出来
%T 打印变量的类型
%% 字符型百分比标志(%符号本身,没有其他操作)
零值:
声明却不初始化时,使用默认值
基础类型与引用类型区别:
int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
通过 &i 来获取变量 i 的内存地址
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
自定义类型:
定义:
type Newint int
var a Newint
type 类型名字 底层类型 类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。
注意:
不能直接赋值
var a int = 8
var i1 MyInt1 = a,需要类型强转才行。
但var i1 MyInt1 = 9是可以的,解释器隐式转换。
方法:
不共享原类型的方法
类型别名:
type xxx = int
只存在代码编写中,编译时不存在
共享原类型的所有方法
类型转换:
基本格式:
type_name(expression)
在任何情况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)。
命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。
环绕行为:
超过最大值时,从最小值开始继续
可以用math.MinInt16等常量来判断
数字转字符串:
strconv.Itoa
Sprintf函数%v
字符串转数值:
strconv.Atoi
布尔转字符串:
string(false)会报错,只能通过sprintf
内存拷贝:
字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。
指针:
定义:
一个指针变量指向了一个值的内存地址。
取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
声明:
在使用指针前你需要声明指针。* 号用于指定变量是作为一个指针
var var_name *var-type
取值:
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
*ip
作用域:
在Go语言中,返回函数中局部变量的地址也是安全的。
使用规范:
每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。要找到一个变量的所有访问者并不容易,我们必须知道变量全部的别名。
空指针:
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
任何类型的指针的零值都是nil。如果p != nil测试为真,那么p是指向某个有效变量。
相等性:
指针之间也是可以进行相等测试的,只有当它们指向同一个变量或全部是nil时才相等。
指针数组:
var ptr [MAX]*int;
指向指针的指针:
var ptr **int;
访问指向指针的指针变量值需要使用两个 * 号
一种是内置类型uintptr,本质是一个整型,另一种是unsafe包提供的Pointer,表示可以指向任意类型的指针。
通常uintptr用来进行指针计算,因为它是整型,所以很容易计算出下一个指针所指向的位置,而unsafe.Pointer用来进行桥接,用于不同类型的指针进行互相转换。
常量const:
概述:
var换为const,定义的时候必须赋值
常量表达式的值在编译期计算,而不是在运行期,
不能取址,const修饰的全局变量和static变量存储在全局的只读空间中,这时候的地址,在运行阶段不可以更改
类型限制:
只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
批量声明:
除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法
const (
a = 1
b
)
iota:
特殊常量,可以认为是一个可以被编译器修改的常量。在第一个声明的常量所在的行,iota这个变量将会被置为0,然后在每一个有常量声明的行加一。
(iota 可理解为 const 语句块中的行索引)。
const (
a = iota
b
c
d = "ha"
e
f = 100
g
h = iota
i
)
应用:
结合位运算符<<,可以表示KiB,MiB,GiB单位 KiB=1<<(10*iota) 1024,常量表达式,赋值一次即可,后续的可以省略
但不能用于产生1000的幂(KB、MB等)
无类型常量:
概述:
const后面接的是基础类型的常量,但是许多常量并没有一个明确的基础类型。分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。
只有常量可以是无类型的。
声明:
var x float32 = math.Pi
var z complex128 = math.Pi
右边的math.Pi即为无类型常量
const (
Pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
类型转换:
隐式转换:
当一个无类型的常量被赋值给一个变量的时候,会隐式转换。
var i int8 = 0
对于一个没有显式类型的变量声明语法(包括短变量声明语法),无类型的常量会被隐式转为默认的变量类型。
无类型的整数常量默认转换为int,对应不确定的内存大小,但是浮点数和复数常量则默认转换为float64和complex128。
显式转换:
var i = int8(0)
运算符:
一元的加法和减法运算符:
自增表达式:
i++,语句,非表达式,所以j = i++是非法的
二元比较运算符:
bit位操作运算符:
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空 (AND NOT)
<< 左移
>> 右移
逻辑运算符:
&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高
流程控制:
条件语句:
if(){
}else{
}
switch
switch coinflip() { # switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
select用于channel
循环语句:
for:
写法一:
for initialization; condition; post {
}
initialization部分是可选的,在for循环之前这部分的逻辑会被执行,通常是一些简短的变量声明,一个赋值语句,或是一个函数调用
condition部分必须是一个结果为boolean值的表达式,在每次循环之前,语言都会检查当前是否满足这个条件,若不满足的话便会结束循环;
post部分的语句则是在每次循环迭代结束之后被执行,
示例:
for i:=0;i<5;i++{
}
写法二:
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
可以在循环里往原切片添加元素,在循环开始前会获取切片的长度 len(切片),然后再执行len(切片)次数的循环。
s := []int{1,2,3,4,5}
for _, v:=range s {
s =append(s, v)
fmt.Printf("len(s)=%v\n",len(s))
}
goto语句:
无条件地转移到过程中指定的行
如何标注代码块
OnHead:{
fmt.Println("sss")
}
break tag也可以使用
死循环的写法:
for ;; {}
for {}
注意go没有while
范围range:
概述:
用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
在数组和切片中它返回元素的索引和索引对应的值(拷贝),在集合中返回 key-value 对。
因为每一次遍历都是对列表中元素的拷贝.
for _, num := range nums {
sum += num
}
for name := range ages { # 自动忽略第二个参数
names = append(names, name)
}
注意:在迭代时,返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以值的地址总是相同的。
range中等号左边的变量,是提前定义好的,并不是临时创建.
对比python:
list会动态添加,yield,所以无穷遍历
map会报错dictionary changed size during iteration
对golang:
slice会提前求值,提前将len(infos)的值计算出来的
修改后面的元素值,会动态生效
map会动态添加
删除,会动态生效。循环次数减少。
内部实现:
range本质是for语句的一个语法糖。
编译器会在循环开始前copy一次循环对象。
switch语句:
格式:
switch var1 {
case val1:
...
case val2:
...
default:
...
}
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
数组:
概述:
值类型
声明:
var variable_name [SIZE] variable_type
# 不声明size则为切片
初始化数组:
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} # ...表示编译器自动数数量,与[]一样
例子:
var a [3] int # 默认情况下,数组的每个元素都被初始化为元素类型对应的零值
a =[3]int {1,2,3}
或
a :=[3]int {1,2,3}
也可以指定一个索引和对应值列表的方式初始化
symbol := [...]string{0: "$", 1: "€", 2: "£", 3: "¥"} # 索引的顺序是无关紧要的,可以不连续
索引:
balance[4] = 50.0
var salary float32 = balance[9]
比较:
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的
只有当两个数组的所有元素都是相等的时候数组才是相等的
多维数组:
var a [3][2]int
a[1][1]=2
二维数组的初始化:
a := [][]int {{1,2,3}}
作为函数参数:
因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。
可以显式地传入一个数组指针,那样的话函数通过指针对数组的任何修改都可以直接反馈到调用者
func zero(ptr *[32]byte)
缺点:
数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。
切片:
概述:
引用类型
要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。要更新这些信息需要像上面例子那样一个显式的赋值操作。
定义:
一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。
一个slice由三个部分构成:指针、长度和容量。
指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。
长度对应slice中元素的数目;长度不能超过容量
容量一般是从slice的开始位置到底层数据的结尾位置。
内置的len和cap函数分别返回slice的长度和容量。
如:
months := [...]string{1: "January", , 12: "December"}
Q2 := months[4:7] # len为3,cap为9
summer := months[6:9] # len为3,cap为7
声明:
var identifier []type
初始化:
var slice1 []type = make([]type, len) # 初始化,容量部分可以省略,在这种情况下,容量将等于长度。在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。
slice1 := make([]type, len)
slice1 := make([]T, length, capacity) # 避免扩容时申请内存空间,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
s :=[] int {1,2,3 } # 自动识别当前初始化的长度和容量,会隐式地创建一个对应长度的数组,然后slice的指针指向底层的数组。
s := arr[startIndex:endIndex] # 切片修改时,数组也随之修改(扩容后,则是新的内存地址,修改不影响原来的)
s := arr[startIndex:]
s := []int(nil)
切片:
slice可以由数组或者slice切片而来
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,新slice的长度会变大
切片操作对应常量时间复杂度
len() 和 cap() 函数:
长度和容量
排序:
内置方法:
sort.Ints
sort.Float64s
sort.Strings
。。。
自定义比较器:
sort.Slice(b,func(i,j int) bool {return b[i]<b[j]})
稳定排序:
sort.SliceStable()
实现排序接口:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
删除特定元素:
for range遍历,找到该位置后,将后面的元素赋值到j-1的位置
函数参数:
因为是引用类型,作为函数参数将会传递引用(或者说是对底层的引用复制了)。
原理:
slice本身是对底层数组的引用,slice值包含指向第一个slice元素的指针,复制一个slice只是对底层的数组创建了一个新的slice别名。slice本身决定的。
示例:
func reverse(s []int)
相等性:
slice之间不能比较
slice的元素是间接引用的,slice甚至可以包含自身。处理麻烦
slice的元素是间接引用的,一个固定值的slice在不同的时间可能包含不同的元素。并且Go语言中map等哈希表之类的数据结构的key只做简单的浅拷贝,它要求在整个声明周期中相等的key必须对相同的元素。
slice唯一合法的比较操作是和nil比较
标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte)
其他类型的slice需要展开每个元素进行比较
nil切片(零值):
定义:
var slice []int
一个切片在未初始化之前默认为 nil,长度为 0,容量为0(也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。)
与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。
判断:
if summer == nil { }
操作:
除了和nil相等比较外,一个nil值的slice的行为和其它任意长度的slice一样;例如reverse(nil)
除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
长度为0的切片(空切片):
定义:
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
长度为 0,容量为0
已经初始化
与nil切片区别:
nil切片是未初始化的。
append()函数:
用法:
numbers = append(numbers, 2,3,4)
x = append(x, x...)
扩容:
每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。
1.如果有足够空间的话,
直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间
2.如果没有足够的增长空间的话,
appendInt函数则会先分配一个足够大的slice用于保存新的结果,这个slice与输入的slice将会引用不同的底层数组。同时旧的数组内存地址仍然存在。
自动扩容,每次扩容变为2倍(在1024前直接翻倍,cap超过1024的,新cap变为老cap的1.25倍),避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。
数组的内存是连续的
底层结构:
从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型
type IntSlice struct {
ptr *int
len, cap int
}
copy()函数:
copy(numbers1,numbers)
返回成功复制的元素的个数,等于两个slice中较小的长度
Map(集合):
概述:
引用类型
可以像迭代数组和切片那样迭代它。不过,Map 是无序的
声明:
var map_variable map[key_data_type]value_data_type # map[K]V,K需要支持==比较运算符,最好不要用浮点数。
初始化:
map_variable = make(map[key_data_type]value_data_typ)
推荐,可以避免写入panic
声明并初始化:
var a = map[string]int{"ss":1}
map_variable := make(map[key_data_type]value_data_type)
推荐
未初始化的字典(零值):
如果不初始化 map,那么就会创建一个 nil map,ages == nil。长度为0。len(ages) == 0
nil map 不能用来存放键值对,其他大部分操作,包括查找(返回类型零值)、删除、len和range循环都可以安全工作在nil值的map上(slice不同,可以进行任何操作)
字面值语法:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
赋值操作:
map中的元素不是变量,因此不能寻址,只能直接找到元素的值。
如何修改?
1.在结构体比较大的时候,用指针效率会更好,因为不需要值copy
2.先整体取出来,赋值给临时变量,然后再修改。最后放回去。
使用:
countryCapitalMap[ "Japan" ] = "东京"
v,ok := m2["xxx"] # ok在取到值时返回true,false时v为对应类型的零值
delete(ages, "alice")函数用于删除集合的元素, 参数为 map 和其对应的 key。
key的要求:
必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。
比如:bool, 数字,string, 指针, channel(原则是结构体), 还有 只包含前面几个类型的 interface types, structs, arrays
不能的类型:
slice不能做相等性比较(底层元素可能变化)
map类似
function不能取址,虽然reflect.Value.Pointer可以取到地址。
相等性比较:
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。
通过循环判断值
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
元素取址:
map中的元素并不是一个变量(hash后的东西?),而是一个值,因此我们不能对map的元素进行取址操作。
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效,旧的指针变量可能无效。
数组和slice可以对元素取址,因为数组的内存地址是固定的,slice内存增长时,先创建更大的内存地址,然后迁移旧数据,同时旧的数组仍旧会存在,旧的slice的copy仍可读到值。
示例:
var b = make([]int,1,1)
c := &b[0]
e := b # 复制引用,深复制使用copy函数(注意与e内存地址有关)
fmt.Println(&b[0])
b = append(b,1)
fmt.Println(&b[0])
fmt.Println(c,e)
而对于字典:
a := make(map[string]interface{},1)
a["s"]= struct{}{}
d := a
a["ss"]= struct{}{}
fmt.Println(a["s"])
fmt.Println(d) # d会随着a的变化而变化,这点和slice不同,字典要复制只能通过for range的方式
key如何用slice等不可比较类型:
定义一个辅助函数k,将slice转为map可以用的可比较类型(整数、数组或结构体等)的key,每次先用辅助函数k转化一下key即可。
示例:
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
存储:
哈希函数:又称散列算法、散列函数。主要作用是通过特定算法将数据根据一定规则组合重新生成得到一个散列值,在哈希表中,其生成的散列值常用于寻找其键映射到哪一个桶上。
链地址法:采用的就是 "链地址法 " 去解决哈希冲突,又称 "拉链法"。其主要做法是数组(内存结构) + 链表的数据结构,其溢出节点的存储内存都是动态申请的,因此相对更灵活。映射在内存层面仍然是数组。
Go map 中的桶和溢出桶的概念,在其桶中只能存储 8 个键值对元素。当超过 8 个时,将会使用溢出桶进行存储或进行扩容
内存本身是晶体管数组。
扩容:
触发时机:
触发 load factor 的最大值,负载因子已达到当前界限
溢出桶 overflow buckets 过多
流程:
1.确定扩容容量规则。
若不是负载因子 load factor 超过当前界限,也就是属于溢出桶 overflow buckets 过多的情况。因此本次扩容规则将是 sameSizeGrow,即是不改变大小的扩容动作,重新映射hash
若是负载因子 load factor 达到当前界限,将会动态扩容当前大小的两倍作为其新容量大小
2.初始化、交换新旧 桶/溢出桶
主要是针对扩容的相关数据前置处理,涉及 buckets/oldbuckets、overflow/oldoverflow 之类与存储相关的字段
内部只会先进行预分配,当使用的时候才会真正的去初始化
3.扩容
扩容是采取增量扩容的方式,并非一步到位。当有访问到具体 bukcet 时,才会逐渐的进行迁移(将 oldbucket 迁移到 bucket)
迁移:计算得到所需数据的位置。再根据当前的迁移状态、扩容规则进行数据分流迁移。结束后进行清理,促进 GC 的回收
扩展:
若正在进行扩容,就会不断地进行迁移。待迁移完毕后才会开始进行下一次的扩容动作
删除:
delete()是安全的,可以一边delete一边遍历。
结构体struct:
概述:
一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。
不支持类,继承,结构体+接口实现面向对象
值类型
定义:
type struct_variable_type struct {
member definition
member definition
...
member definition
}
声明:
var Book1 Books
初始化:
1.p := Point{1, 2} # 要记住结构体的每个成员的类型和顺序,一般只在定义结构体的包内部使用
2.anim := gif.GIF{LoopCount: nframes} # 以成员名字和相应的值来初始化
访问:
结构体.成员名
结构体指针:
定义:
1.var struct_pointer *Books
2.var struct_pointer = new(Books)
初始化:
1.struct_pointer = &Book1
2.*struct_pointer = Point{1, 2}
使用结构体指针访问结构体成员,使用 "." 操作符:
struct_pointer.title 访问到值,而不是内存地址
相当于(*struct_pointer).title
函数参数:
func EmployeeByID(id int) *Employee { }
不会写结构体类型,而是写指针(但写了结构体类型也没问题,注意使用场合就行)
原因:
1.因为在赋值语句的左边并不确定是一个变量(译注:调用函数返回的是值,并不是一个可取地址的变量)。比如return Employee{}时,EmployeeByID(id).Salary=1的左边是一个值,而不是变量,不能赋值。
2.return Employee{}虽然可以返回一个新的结构体,但是效率不如操作原结构体的指针高。如果要在函数内部修改结构体成员的话,用指针传入是必须的。
3.作为传入参数时,也一般传指针,拷贝指针,空间和时间的开销都很小,效率较高。
内存布局:
占用一块连续的内存
构造函数:
自己实现一个函数,返回实例结构体即可。
值类型,返回时会拷贝,所以返回指针
比较:
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的。如果成员不可比较或者仅比较字段内容,可以用reflect.DeepEqual(sm1, sm2)这种方法。
可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。
相同struct类型的可以比较,不同struct类型的不可以比较,编译都不过,类型不匹配。不但与属性类型个数有关,还与属性顺序相关。
匿名字段:
概述:
只有类型,没有名字。
注意:
string类型作为名字,注意类型不能重复,匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突
因为成员的名字是由其类型隐式地决定的,所有匿名成员也有可见性的规则约束
一般应用场景:自定义struct+嵌套
示例:
type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
var w Circle
w.X = 8 # 这样即可直接访问,也可以w.Point.X = 8,在访问子成员的时候可以忽略任何匿名成员部分
不幸的是,结构体字面值并没有简短表示匿名成员的语法
只能w = Circle{Point{8, 8}, 5}, 20
作用:
匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。
比如w.strings.lowcase()
嵌套结构体:
优点:
可以使用匿名字段,并且没有重复字段的情况下,支持直接访问而不用写父级字段
顺序:
父层也有相同字段的情况下,访问的是父层,如果是子层内调用其他方法,调用的是子层的方法而不是父类的
子层都有该字段时,不能直接访问,需显示调用
编码规范:
嵌入类型会泄漏实现细节、禁止类型演化、产生模糊的文档,应该尽可能地避免。(因为可以直接访问,顺序模糊)
自嵌套:
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)
但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
type tree struct {
value int
left, right *tree
}
空结构体:
写作struct{}。它的大小为0,占用了0字节的内存空间。也不包含任何信息,但是有时候依然是有价值的。
有些Go语言程序员用map带模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。
seen := make(map[string]struct{})
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
}
继承:
嵌套匿名结构体,父层结构体可以使用调用子层结构体的方法
与Java继承的比较:
Java的继承,方法会自动继承,除了私有方法和构造方法
而Golang的继承是通过结构体嵌套,而子结构体调用方法是调用自己的方法
func (a *Animal) Play() {
fmt.Println(a.Speak())
}
继承通过结构体,多态通过接口
字段的可见性:
大写开头表示可公开访问,小写表示私有,仅当前包可访问。
特别注意的场景:
json.Unmarshal包的方法要想序列化struct的字段,必须要求大写。
序列化:
v,err := json.Marsha1(book)
v,err := json.UnMarsha1([]byte(str),book)
别名:
ID int `json:"id"`
字段:
需大写,才能被json包访问到
JSON:
概述:
数字(十进制或科学记数法)、布尔值(true或false)、字符串、数组和对象类型
编码marshaling:
data, err := json.Marshal(movies)
json.MarshalIndent # 函数将产生整齐缩进的输出
在编码时,默认使用Go语言结构体的成员名字作为JSON的对象,可以使用Tag
Color bool `json:"color,omitempty"` # omitempty选项,表示当Go语言结构体成员为空或零值时不生成JSON对象(这里false为零值)
只有导出的结构体成员才会被编码
解码unmarshaling:
json.Unmarshal
json.Decoder # 基于流式的解码器,还有针对输出流的json.Encoder编码对象
序列化:
问题:
Go语言中的json包在序列化空接口存放的数字类型(整型、浮点型等)都序列化成float64类型。
解决:
1.标准库gob是golang提供的“私有”的编解码方式,它的效率会比json,xml等更高,特别适合在Go语言程序间传递数据。
2.MessagePack是一种高效的二进制序列化格式。它允许你在多种语言(如JSON)之间交换数据。但它更快更小。
encoding/json:
通过reflection和interface来完成工作, 性能低。
json-iterator:
性能高于充满反射的官方提供的编码库
示例:
json := jsoniter.ConfigCompatibleWithStandardLibrary
result, _ := json.Marshal(&s)
函数:
定义:
func function_name( [parameter list] ) [return_types] {
函数体
}
示例:
func hypot(x, y float64) float64
func hypot(x, y float64) (z float64){} # return语句可以省略操作数,直接用return即可,声明的z可以在函数内部使用
func f(i, j, k int, s, t string) { }
特殊函数:
func Sin(x float64) float # 不是以Go实现的
函数参数:
机制:
当调用一个函数的时候,函数的每个调用参数(实参)将会被赋值给函数内部的参数变量(形参),所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。
Go里边函数传参只有值传递一种方式。
值传递 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
指针传递 形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
引用传递 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
Go中函数调用只有值传递,但是类型引用有引用类型。
打印引用类型的形参地址,再比较实参的地址,
test(&a) test(a *string) *a = "hellp"
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
可变参数:
func xxx(a...int){}
递归调用:
Go语言使用可变栈,栈的大小按需增加(初始时很小)
错误:
一种预期的值而非异常,各种异常机制仅被使用在处理那些未被预料到的错误,即bug
策略:
1.传播错误
if err != nil {
return nil, fmt.Errorf("parsing %s as HTML: %v", url,err) # fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回
}
2.重新尝试失败的操作
3.输出错误信息并结束程序,这种策略只应在main中执行
if err := WaitForServer(url); err != nil {
fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
os.Exit(1)
}
或者log.Fatalf("Site is down: %v\n", err)
4.只需要输出错误信息,不需要中断程序的运行
函数值:
引用类型
函数值之间是不可比较的,也不能用函数值作为map的key。
Go将函数被看作第一类值(first-class values),可以传递给函数。
func forEachNode(n *html.Node, pre, post func(n *html.Node))
函数类型的零值是nil。调用值为nil的函数值会引起panic错误,可以与nil比较。
var f func(int) int
f(3)
作用:
strings.Map(add1, "HAL-9000") 通过行为来参数化函数
匿名函数:
拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制。
函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。
使用方式:
1.直接作为参数使用
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
2.先声明,后初始化
var visitAll func(items []string)
visitAll = func(items []string) {
visitAll() # 这样可以递归调用
}
3.短声明方式:
visitAll := func(items []string) {
visitAll(m[item])
}
常见的一个问题:
for _, d := range tempDirs() {
dir := d
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) # 闭包的概念,循环变量的作用域,如果是d的话,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值
})
}
defer语句:
定义:
将跟随的语句进行延迟处理(函数的最后执行),多个defer将会逆序执行,先被defer的语句最后被执行
触发时机:
包裹defer的函数返回时
使用场景:
经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。
func bigSlowOperation() {
defer trace("bigSlowOperation")()
time.Sleep(10 * time.Second)
operation by sleeping
}
func trace(msg string) func() {
start := time.Now()
log.Printf("enter %s", msg)
return func() {
log.Printf("exit %s (%s)", msg,time.Since(start)) # trace函数的执行返回的是一个函数
}
}
与return的例子:
写法一:
func returnValues() int { # 匿名返回值是在return执行时被声明
var result int
defer func() {
result++
fmt.Println("defer")
}()
return result
}
写法二:
func namedReturnValues() (result int) { # 有名返回值则是在函数声明的同时被声明
defer func() {
result++
fmt.Println("defer")
}()
return result
}
第一种写法:
1.会将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
2.然后检查是否有defer,如果有则执行
3.返回刚才创建的返回值(retValue)
return:
第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);
第二步是调用RET返回指令并传入返回值,而RET则会检查defer是否存在,若存在就先逆序插播defer语句,最后RET携带返回值退出函数;
defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着defer开始执行一些收尾工作;最后RET指令携带返回值退出函数。
panic异常:
概述:
panic("x is nil")
程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。
堆栈信息:
为了方便诊断问题,runtime包允许程序员输出堆栈信息。
func main() {
defer printStack()
f(3)
}
func printStack() {
var buf [4096]byte
n := runtime.Stack(buf[:], false) # 在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。
os.Stdout.Write(buf[:n])
}
Recover捕获异常:
概述:
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。
导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
func Parse(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("internal error: %v", p)
}
}()
}
在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。
此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
规范:
1.不应该试图去恢复其他包引起的panic
例外情况:
web服务器遇到处理函数导致的panic时会调用recover,输出堆栈信息,继续运行。
基于以上原因,安全的做法是有选择性的recover。
在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理
defer func() {
switch p := recover(); p {
case nil:
case bailout{}:
err = fmt.Errorf("multiple title elements") # 预期错误,但对可预期的错误采用了panic这种做法不建议
default:
panic(p) # 等同于recover没有做任何操作。
}
}()
2.panic 捕获只能到 goroutine 最顶层,每个自行启动的 goroutine,必须在入口处捕获 panic,并打印详细堆栈信息或进行其它处理
3.建议在 main 包中使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题
闭包:
匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
方法:
概述:
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
函数指定接收者之后,就是方法
定义:
func (variable_name variable_data_type) function_name() [return_type]{
}
接收器命名:
可以使用其类型的第一个字母,比如这里使用了Point的首字母p。
使用:
var book Books
book.method() # 表达式叫做选择器,会选择合适的对应book这个对象的method方法
选择器:
p.Distance叫作“选择器”,选择器会返回一个方法"值"->一个将方法(Point.Distance)绑定到特定接收器变量的函数。
这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:
选择器也会被用来选择一个struct类型的字段,比如p.X。
由于方法和字段都是在同一命名空间,所以如果我们在这里声明一个X方法的话,编译器会报错,因为在调用p.X时会有歧义
方法值:
选择器不调用即方法值,p.Distance
和方法"值"相关的还有方法表达式。
方法表达式:
当T是一个类型时,T.f或者(*T).f为方法表达式。会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用
distance := Point.Distance
distance(p, q)
好处:
将方法抽离出来当作一个函数,方便用于不同的接收器
基于指针对象的方法:
场景:
需要修改接收者中的值时(值类型不会修改值)
定义:
接收者一般为(variable_name *variable_data_type)传递引用
方法的名字是(*Point).ScaleBy
约定:
如果Point这个类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。
规则:
在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的。compile error: invalid receiver type
使用:
方式1(较正经):
r := &Point{1, 2}
r.ScaleBy(2)
方式2:
p := Point{1, 2}
pptr := &p 更复杂的正经做法
pptr.ScaleBy(2)
方式3:
p := Point{1, 2}
(&p).ScaleBy(2) 更复杂的正经做法
方式4(最好的):
p := Point{1, 2}
p.ScaleBy(2)
如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,可以用这种写法,编译器会隐式地帮我们用&p去调用ScaleBy这个方法。
这种简写方法只适用于“变量”,包括struct里的字段比如p.X,以及array和slice内的元素比如perim[0]
编译器会隐式地为我们取变量的地址
类型作为接收器时:
可以用指针进行调用类型的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号*来取到该变量即可。编译器在这里也会给我们隐式地插入*这个操作符。编译器会隐式地为我们解引用,取到指针指向的实际变量
func (p Point) Distance(factor float64){}
pptr.Distance(q)
(*pptr).Distance(q)
临时变量的场景:
不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
func (p *Point) ScaleBy(factor float64){}
Point{1, 2}.ScaleBy(2) # 错误
(&Point{1, 2}).ScaleBy(2) # 类型相同是可以的
但可以用一个*Point这样的临时接收器来调用Point的方法,
func (p Point) ScaleBy2(factor float64){}
(&Point{1, 2}).ScaleBy2(2) # 这样是可以的,因为通过可以地址找到变量值
Point{1, 2}.ScaleBy2(2) # 临时变量可以,因为形参和实参类型相同
总结:
不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
只是要明白receiver是指针类型还是非指针类型时,值拷贝和引用拷贝的区别
nil作为实参:
当nil对于对象来说是合法的零值时,比如map或者slice,可以用nil指针作为其接收器
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
其他接收者:
方法可以被声明到任意类型,只要不是一个指针或者一个interface。
只有类型(Point)和指向他们的指针(*Point),才是可能会出现在接收器声明里的两种接收器。
如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,type a *int,只能*T的形式
注意不能给别的包定义的类型添加方法,需要自定义类型
嵌入结构体来扩展类型(继承):
概念:
嵌入结构体中,嵌入的字段可以认为是自身的字段。
方法也类似,外层类型可以直接调用嵌入结构体的方法,即使没有声明(继承基类后,方法也继承了。只是类型没有继承)。参数还得对应。
匿名字段:
可以使用匿名字段。
此外,在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。
解析顺序:
先在本结构体中找指定方法,然后在内嵌字段中寻找引入的方法,再内嵌的内嵌,一直递归。
如果选择器有二义性的话编译器会报错,比如你在同一级里有两个同名的方法。
如何给匿名struct类型定义方法(其他类型不能内嵌):
方法只能在命名类型(像Point)或者指向类型的指针上定义,但是多亏了内嵌,有些时候我们给匿名struct类型来定义方法也有了手段。
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}
这样cache.Lock()、v := cache.mapping[key]都可以使用
封装:
一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。
想要封装一个对象,我们必须将其定义为一个struct。结构体的变量只通过包内的方法进行修改和获取,其他包只使用结构体和方法即可。
最小单元:
这种基于名字的手段使得在语言中最小的封装单元是package,因为一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
优点:
1.首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
2.隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
简单来说,就是设计包的时候,可以用一些内部变量,调用方修改不了。
3.阻止了外部调用方对对象内部的值任意地进行修改。
示例:
type IntSet struct {
words []uint64
}
接口:
概述:
另外一种数据类型即接口,接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。。
接口类型变量可以用来保存所有实现了接口的类型。
一个类型可以同时实现多个接口,而接口间彼此独立
与其他语言的区别:
非侵入式的,我们不需要显式的说明实现了哪个接口,只需要根据该类型已有的方法来判断就可以。
接口约定:
这个类型和调用者之间约定需要调用者提供具体类型的值就像*os.File和*bytes.Buffer,这些类型都有一个特定签名和行为的Write的函数。
如果满足了这个约定,保证了调用者的任何满足io.Writer接口的值都可以工作。
定义:
type interface_name interface {
method_name1([params]) [return_type]
method_name2([params]) [return_type]
method_name3([params]) [return_type]
...
method_namen([params]) [return_type]
}
接下来实现所有的方法
func (struct_name_variable struct_name) method_name1([params]) [return_type] {
}
命名:
使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
作用:
接口类型变量能够存储所有实现了该接口的实例,相当于一个泛型
1.Golang接口是协议、是虚的,有隔离的作用;
2.能够实现高内聚低耦合高复用,可以防止出现面条式程序;
2.更容易划分模块和多人开发
2.有了接口很容易实现各种设计模式
3.连通各种东西,减少了开发量,提高了通用性
接口值:
概念:
概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。
类型是编译期的概念;因此一个类型不是一个值。一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。
比如var w io.Writer,类型为io.Writer接口,但值的两个部分(具体的类型和那个类型的值)都为nil(接口的零值),这个时候可以使用w==nil或者w!=nil来判读接口值是否为空
变量总是被一个定义明确的值初始化,即使接口类型也不例外。
调用一个空接口值上的任意方法都会产生panic
隐式转换:
var w io.Writer
w = new(os.Stdout) # 调用了一个具体类型到接口类型的隐式转换,和显式的使用io.Writer(os.Stdout)是等价的。
# 这个接口值的动态类型被设为*os.Stdout指针的类型描述符,它的动态值持有os.Stdout的拷贝
编译细节:
通常在编译期,我们不知道接口值的动态类型是什么,所以一个接口上的调用必须使用动态分配。因为不是直接进行调用,所以编译器必须把代码生成在类型描述符的方法Write上,然后间接调用那个地址。
这个调用的接收者是一个接口动态值的拷贝,os.Stdout。
w.Write([]byte("hello"))
os.Stdout.Write([]byte("hello")) # 等价,间接调用
比较:
1.接口值可以使用==和!=来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。
因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。
2.然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic:
一个包含nil指针的接口不是nil接口:
var buf *bytes.Buffer # 传给函数参数中的接口类型时,动态类型不为空,但动态值(指针)为空
buf = new(bytes.Buffer) # 区别的是,传给接口时,动态类型是*bytes.Buffer,动态值(也即指针)不为nil,初始化过了。
动态分配机制依然决定(*bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。违反了(*bytes.Buffer).Write方法的接收者非空的隐含先觉条件,所以将nil指针赋给这个接口是错误的。
如何判断:
vi := reflect.ValueOf(i)
if vi.Kind() == reflect.Ptr {
return vi.IsNil()
}
接口与struct的区别(用途示例):
定义了一个接口后,必须初始化。因为接口本身要存储其他类型才行。
var queryStringParameters models.QueryStringParameters
err := c.ShouldBindQuery(&queryStringParameters)
然后可以将queryStringParameters传递给func,func的参数为QueryStringParamInterface,可以接收实现了该接口的queryStringParameters
错误示例:
var queryStringParameters models.QueryStringParamInterface = models.QueryStringParameters{}
err := c.ShouldBindQuery(&queryStringParameters)
因为接口本身不应该当作任何struct来使用(比如绑定,会获取不了数据),接口是用来接收struct后调用struct实现的方法的
调用:
var phone Phone
phone = new(NokiaPhone)
phone.call()
或:
a := NokiaPhone{xxx}
a.call()
接口内嵌:
type animal interface {
Sayer # 需要实现Sayer中的方法即可视为实现了Sayer接口
Mover #
}
如何为非自定义类型定义接口:
go语言不允许为简单的内置类型、导入的第三方库等添加方法,参考:方法
解决:
1.定义一个新类型
type newRequest engine.Request
然后就可以为这个新类型定义接口
2.用一个struct嵌入(embedding)一下
type newRequest struct {
requester *engine.Request
}
func (req newRequest) myselfMethod() {
req.requester.xxxx
}
空接口:
没有定义任何方法的接口,任何类型都实现了空接口
使用空接口可以保存任意值的字典
var a = make(map[string]interface{})
比较:
map 不可比较,如果比较,程序会报错
切片([]T) 不可比较,如果比较,程序会报错
通道(channel) 可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false
数组([容量]T) 可比较,编译期知道两个数组是否一致
结构体 可比较,可以逐个比较结构体的值
函数 可比较
值和指针的接收者:
值类型接收者实现方法时,指针可以赋给接口
func (c cat) xxx() (xx){}
var x animal = &cat{} # 判断指针时自动使用go语法糖,(*tom.speak()),但类型断言时不能调用指针方法,不断言时,都可以调用值方法和指针方法。
指针作为接收者时,不能传值类型给接口
func (c *cat) xxx() (xx){}
var x animal = &cat{} # 不能是var x animal = cat{},提示Type does not implement 'People' as 'Speak' method has a pointer receiver
只可以传指针,但多使用这个接收者(推荐,兼容其他语言显式this指针的含义),类型断言时可以调用指针方法和值方法,不断言时,都可以调用值方法和指针方法。
类型的断言:
概述:
x.(T)
一个类型断言检查它操作对象(只能是接口)的动态类型是否和断言的类型匹配。
1.如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。
2.第二种,如果相反断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型T。
换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保护了接口值内部的动态类型和值的部分。
3.如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。
var w io.ReadWriter
w = rw.(io.Writer)
示例:
var x interface{}
x = ""
v,ok = x.(string)
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter)
多次断言可以使用switch语句:
func whichType(n Shaper) { # 一般不用interface{}类型,否则不能使用x.方法(编译期不通过)。区别于x.(具体类型),这种编译期能确定具体类型。
switch v:=x.(type){ # 单个case中,可以出现多个结果选项。
case string:
var o string = v
default:xxx
}
}
注:要想访问结构体的某个字段,需要实现接口的方法,然后通过方法来获取。
问题:
尝试对json进行断言,转换为特定的类型,但拿不到结果。
但可以映射为interface{},是可以拿到数据的
应用场景:
1.基于类型断言区别错误类型:
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
2.通过类型断言询问行为
可以定义一个只有这个方法的新接口并且使用类型断言来检测是否w的动态类型满足这个新接口。如果满足,它会使用这个更具体接口的行为。
if err, ok := x.(error); ok {
return err.Error()
}
if str, ok := x.(Stringer); ok {
return str.String()
}
类型开关:
1.一个接口的方法表达了实现这个接口的具体类型间的相思性,但是隐藏了代表的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。
2.第二个方式利用一个接口值可以持有各种具体类型值的能力并且将这个接口认为是这些类型的union(联合)。
可以用类型断言用来动态地区别这些类型
示例:
switch x := x.(type) { # 使用了关键词字面量type
case nil:
return "NULL" # 当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
建议:
1.接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。抽离小的接口,当新的类型出现时,小的接口更容易满足。
当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好好方式。
反射:
定义:
在运行时更新变量和检查它们的值, 调用它们的方法, 和它们支持的内在操作
检查未知类型的表示方式
reflect包:
定义了两个重要的类型, Type 和 Value
一个 Type 表示一个Go类型. 它是一个接口, 有许多方法来区分类型和检查它们的组件,满足 fmt.Stringer 接口的
一个 reflect.Value 可以持有一个任意类型的值,也满足 fmt.Stringer 接口
reflect.TypeOf() 接受任意的 interface{} (接口)类型, 并返回对应动态类型的reflect.Type(一个动态类型的接口值)。
将一个具体的值转为接口类型会有一个隐式的接口转换操作, 它会创建一个包含两个信息的接口值: 操作数的动态类型(这里是int)和它的动态的值(这里是3).
reflect.ValueOf() 函数reflect.ValueOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Value(一个动态类型的接口值,持有的方法不同).
value.Type 调用 Value 的 Type 方法将返回具体类型所对应的 reflect.Type
Value.Interface 返回一个 interface{} 类型表示 reflect.Value 对应类型的具体值,即对应的具体类型了,如string、int等,可以使用类型断言i := x.(int)
reflect.Value 和 interface{}的区别:
都能保存任意的值.
一个空的接口隐藏了值对应的表示方式和所有的公开的方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值
一个 Value 则提供了自己的很多方法来检查其内容,无论它的具体类型是什么.
应用:
fmt.Printf(%T)和代码补全原理就是反射
类型划分:
name和kind
类型和种类
v := reflect.TypeOf()
v.name v.kind
获取值:
v := reflect.ValueOf()
k := v.kind()
判断类型后v.Int() Float()
基础类型的组合很多,但kinds类型却是有限的,使得反射可以有限枚举类型,而类型断言则不行:
Bool, String 和 所有数字类型的基础类型;
Array 和 Struct 对应的聚合类型;
Chan, Func, Ptr, Slice, 和 Map 对应的引用类似;
接口类型;
还有表示空值的无效类型. (空的 reflect.Value 对应 Invalid 无效类型.)
示例:
switch v.Kind() {
case reflect.Invalid:
return "invalid"
case reflect.Int, reflect.Int8, reflect.Int16,reflect.Int32, reflect.Int64:
return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16,reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return strconv.FormatUint(v.Uint(), 10)
case reflect.Bool:
return strconv.FormatBool(v.Bool())
case reflect.String:
return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
return v.Type().String() + " 0x" +strconv.FormatUint(uint64(v.Pointer()), 16)
default:
return v.Type().String() + " value"
}
Slice和数组:Index(i)索引i对应的元素,返回的也是一个reflect.Value类型的值;如果索引i超出范围的话将导致panic异常,这些行为和数组或slice类型内建的len(a)和a[i]等操作类似。
结构体:下面说明
Maps: MapKeys方法返回一个reflect.Value类型的slice,每一个都对应map的可以。和往常一样,遍历map时顺序是随机的。MapIndex(key)返回map中key对应的value。
指针: Elem方法返回指针指向的变量,还是reflect.Value类型。即使指针是nil,这个操作也是安全的,在这种情况下指针是Invalid无效类型,但是我们可以用IsNil方法来显式地测试一个空指针
v.Elem()
v.IsNil() 判断v的值是否为空,常用于判断指针是否为空
IsValid() 判断v是否持有一个值,常用于判断结构体和map是否有该字段
接口: 再一次,我们使用IsNil方法来测试接口是否是nil,如果不是,我们可以调用v.Elem()来获取接口对应的动态值,并且打印对应的类型和值。
v.Elem().Type()
通过反射修改值:
背景:
空接口为参数时,不能通过* = xxx来修改,因为空接口没有*操作
Go中的变量可以通过内存地址来更新。
对于reflect.Values也有类似的区别。有一些reflect.Values是可取地址的;其它一些则不可以
可以取址的场景:
所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。
所以可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。
每当我们通过指针间接地获取的reflect.Value都是可取地址的,即使开始的是一个不可取地址的Value。
slice的索引表达式e[i]
reflect.ValueOf(e).Index(i)
判断方法:
reflect.Value的CanAddr方法来判断其是否可以被取地址
CanSet方法是用于检查对应的reflect.Value是否是可取地址并可被修改的
如何通过可取地址的reflect.Value来修改值
1.调用Addr()方法,返回一个Value,里面保存了指向变量的指针。然后是在Value上调用Interface()方法,也就是返回一个interface{},里面通用包含指向变量的指针。
最后,如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了:
d := reflect.ValueOf(&x).Elem()
px := d.Addr().Interface().(*int)
*px = 3
2.通过调用可取地址的reflect.Value的reflect.Value.Set方法来更新
v := reflect.ValueOf()
if v.Elem().Kind() == reflect.Int64{
v.Elem().SetInt(200)
}
或者v.Elem().Set(reflect.ValueOf(4)) # Set方法将在运行时执行和编译时类似的可赋值性约束的检查。所以运行时如果类型不对的话会panic
# 对一个不可取地址的reflect.Value调用Set方法也会导致panic异常
# 对于一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,
基本数据类型的Set方法:SetInt、SetUint、SetString和SetFloat等
注意:
利用反射机制并不能修改这些未导出的成员
结构体反射:
使用t:=reflect.TypeOf(xxx)后,t可以使用以下方法
方法:
Filed(i int) StryctField,以reflect.Value类型返回第i个成员的值,成员列表包含了匿名成员在内的全部成员
NumField() int # 结构体中成员的数量
FieldByName(name string)(StructField,bool)
NumMethod() int 显示一个类型的方法集数量
Method(i int) reflect.Method(),返回一个reflect.Value以表示对应的值,可以调用Call方法
MethodByName(string)(Method,bool)
StructField类型介绍:
用来描述结构体中一个字段的信息
type StructField struct{
Name string
PkgPath string
Type Type
Tag StructTag
Offset unitptr
Index []int
Anonymous bool
}
优缺点:
反射中的类型错误在运行时才报错
性能低
灵活
基于反射的代码是比较脆弱的,运行时才报错
错误处理:
概述:
error类型是一个接口类型
定义:
type error interface {
Error() string
}
使用:
我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
}
result, err:= Sqrt(-1)
协程goroutine:
定义:
每一个并发的执行单元叫作一个goroutine
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。
只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
内存共享:
同一个程序中的所有 goroutine 共享同一个地址空间。
goroutine 语法格式:
go 函数名( 参数列表 )
守护性:
主函数执行完后,不管goroutine是否执行完毕,都会退出
与python协程的区别:
单线程内切换,适用于IO密集型程序中,可以最大化IO多路复用的效果。
无法利用多核。
协程间完全同步,不会并行。不需要考虑数据安全。
用法多样,可以用在web服务中,也可用在pipeline数据/任务消费中
而go:
协程间需要保证数据安全,比如通过channel或锁。
可以利用多核并行执行。
协程间不完全同步,可以并行运行,具体要看channel的设计。
抢占式调度,可能无法实现公平。
与线程的区别:
1.可增长的栈
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。
一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。
所以在Go语言中一次创建十万左右的goroutine也是可以的。
函数的递归调用受到栈大小的限制,可达1Gb。
2.goroutine调度
OS线程会被操作系统内核调度。保存一个用户线程的状态到内存,恢复另一个线程的到寄存器,然后更新调度器的数据结构。
这几步操作很慢,因为其局部性很差需要几次内存访问,并且会增加运行的cpu周期。
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,
这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。
其一大特点是goroutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,
不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。
另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上,再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能
3.GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来决定会有多少个操作系统的线程同时执行Go的代码(活跃的)。默认值是机器上的CPU核心数。系统调用导致阻塞中的线程数不归入这个计数。
实际线程数量可能更多,包含了cgo调用,系统调用的线程
runtime.GOMAXPROCS(2)
在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。调度器会使其进入休眠并开始执行另一个goroutine直到时机到了再去唤醒第一个goroutine。
在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的
测试:
for {
go fmt.Print(0)
fmt.Print(1)
}
GOMAXPROCS=1 go run hacker-cliché.go
GOMAXPROCS=2 go run hacker-cliché.go
4.Goroutine没有ID号
当前的线程都有一个独特的身份(id),典型的可以是一个integer或者指针值。thread-local storage(线程本地存储)。
导致一个函数的行为可能不是由其自己内部的变量所决定,而是由其所运行在的线程所决定。
Go鼓励更为简单的模式,这种模式下参数对函数的影响都是显式的。
GPM调度器:
含义:
G(GoRoutine):协程,应用层看到的“线程”。由 M 调度和执行。
P(Processor): “处理器”(队列),主要用来限制实际运行的 M 的数量。默认数量跟 CPU 的物理线程数一致,受 GOMAXPROCS 控制。提供了相关的执行环境(Context)
分本地队列和全局队列,优先轮流放入所有M的本地队列P,然后放入全局队列。
任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。
M(Machine): OS Thread,由 OS 调度。M 的数量不一定。但是处于非阻塞状态的 M 由 P 决定。M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行。本地没有的话会从全局队列获取一些。其次是获取其他空闲的本地P。
队列:
GRQ,全局运行队列,尚未分配给P的G
LRQ,本地运行队列,每个P都有一个LRQ,用于管理分配给P执行的G
状态:
_Gidle: 分配了G,但是没有初始化
_Grunnable: 在run queue运行队列中,LRQ或者GRQ
_Grunning: 正在运行指令,有自己的stack。不在runq运行队列中,分配给M和P
_Gsyscall: 正在执行syscall,而非用户指令,不在runq,分给M,P给找idle的M
_Gwaiting: block。不在RQ,但是可能会在channel的wait queue等待队列
_Gdead: unused。在P的gfree list中,不在runq。idle闲置状态
_Gcopystack: stack扩容或者gc收缩
调度:
调度的目的就是防止M堵塞,空闲,系统进程切换。
调度过程:
0.初始化M和P
1.创建一个 G 对象;获取了结构体G之后,将调用参数保存到g的栈,将sp,pc等上下文环境保存在g的sched域
2.将 G 保存至 P中(本地中的一个or全局);
3.M 寻找空闲的 P,读取该 P 要分配的 G;
当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall状态,M也处于block on syscall状态,此时仍然可被抢占调度: 执行该G的M会与P解绑,而P则尝试被其它idle的M绑定,继续执行其它G。
如果没有其它idle的M,但队列中仍然有G需要执行,则创建一个新的M。
4.接下来 M 执行一个调度循环,调用 G → 执行 → 清理线程 → 继续找新的 G 执行。
5.当没有G可被执行时,M会与P解绑,然后进入休眠(idle)状态。
异步调用:
Linux可以通过epoll实现网络调用,统称网络轮询器N(Net Poller)。
G1在M上运行,P的LRQ有其他3个G,N空闲;
G1进行网络IO,因此被移动到N,M继续从LRQ取其他的G执行。比如G2就被上下文切换到M上;
G1结束网络请求,收到响应,G1被移回LRQ,等待切换到M执行。
同步调用:
文件IO操作
G1在M1上运行,P的LRQ有其他3个G;
G1进行同步调用,堵塞M;
调度器将M1与P分离,此时M1下只有G1,没有P。
将P与空闲M2绑定,并从LRQ选择G2切换
G1结束堵塞操作,移回LRQ。M1空闲备用。(尝试获取其他内核线程context,如果没有把G放到LRQ或GRQ,自己放回线程池进入睡眠状态)
任务窃取:
上面都是防止M堵塞,任务窃取是防止M空闲
两个P,P1,P2
如果P1的G都执行完了,LRQ空,P1就开始任务窃取。
第一种情况,P2 LRQ还有G,则P1从P2窃取了LRQ中一半的G
第二种情况,P2也没有LRQ,P1从GRQ窃取。
一个协程P如果阻塞了线程会释放吗?(上面的第3点):
当系统调用(比如等待 I/O)阻塞协程时,其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。
如果并发的blocking的系统调用很多,Go就会创建大量的线程,但是当系统调用完成后,这些线程因为Go运行时的设计,却不会被回收掉。
Go运行时不会回收线程,而是会在需要的时候重用它们。
使用debug.SetMaxThreads函数进行设置
标准库中的net包对网络IO做了封装,底层实际基于epoll机制,并不会block线程。
但是其他system call就会真的block线程了,比如文件IO,这时候线程会被block, 当前调度器上剩下的协程队列会被转移到新的线程中执行,IO操作结束后,会将协程放入GRQ尾部。
至于新线程是怎么来的,可能是用的runtime线程池里的空闲线程,也可能是新创建的。
调度器会将当前goroutine 切出等待:
chan 收发 将对应协程状态改为_Gowaiting,加入chan上的读或者写阻塞队列
go 语句调用函数
net io runtime通过netpoller检查对应的IO是否就绪,如果未,解除协程和P的绑定 findrunnable中,会检查是否有就绪io,将就绪io绑定的协程状态改为_Grunnable,插入GRQ尾部
gc
time 一个P会持有一个Timer优先队列,runtime会将协程放入优先队列中(堆),堆顶为最早到期的协程。每个P都有一个协程轮询判断是否有协程到达时间,runqput给P
mutex 调用互斥进入_Gowaiting
栈拷贝 状态设置为_Gocopystack,从P移除 放入GRQ队头
试验time.sleep,然后前后unix包打印协程的线程id,发现前后不一,可能与P分配给空闲M有关。
通道(channel):
定义:
通道(channel)是用来传递数据的一个数据结构。
实现原理:
Channel 是一个用于同步和通信的有锁(互斥锁)队列(链表)。hchan的结构体。并返回一个ch指针(所以channel是引用类型)
后来提出使用无锁的数据结构实现先进先出队列,暂时未实现?
结构:
buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表。
在缓存列表在动态的send和recv过程中,定位当前send或者recvx的位置、选择send的和recvx的位置比较方便吧,只要顺着链表顺序一直旋转操作就好。
把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。
sendx和recvx用于记录buf这个循环链表中的发送或者接收的index
lock是个互斥锁。
recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
当buff满了send的时候:
这个时候G1正在正常运行,当再次进行send操作(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用
同时G1也会被抽象成含有G1指针和send元素的sudog结构体保存到hchan的sendq中等待被唤醒。
G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。
recv:
G2会主动调用Go的调度器,让G2等待,并从让出M,让其他G去使用。G2还会被抽象成含有G2指针和recv空元素的sudog结构体保存到hchan的recvq中等待被唤醒
G1并没有锁住channel,然后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。让G2停止等待,放到可运行的队列中。
最早被阻塞的goroutine会最先被唤醒。
类型:
引用类型,对应make创建的底层数据结构的引用。零值为nil。
场景:
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
比较:
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。
一个channel也可以和nil进行比较。
声明:
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
var ch chan int
nil channel:
channel的零值是nil。对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。
原因: buffer未初始化。
初始化:
ch = make(chan int, 3)
声明并初始化:
ch := make(chan int)
无缓存channels:
时机:
当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前。
备注:当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,
例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。
强调通讯发生的时刻时,我们将它称为消息事件
goroutines泄露:
如果我们使用了无缓存的channel,那么两个慢的goroutines的x<-ch接收操作将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。
和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
解决:
最简单的解决办法就是用一个具有合适大小的buffered channel
单方向的channel:
类型chan<- int表示一个只发送int的channel,只能发送不能接收。
类型<-chan int表示一个只接收int的channel,只能接收不能发送。
类型转换:
任何双向channel向单向channel变量的赋值操作都将导致隐式转换。
但不可以反向转换
用途:
约束其他代码的行为。编写模版代码或者可扩展程序库的时候约束参数
带缓存的channel:
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
容量:
fmt.Println(cap(ch))
长度:
fmt.Println(len(ch))
应用场景:
1.控制并发的数量
2.避免同时写入时的死锁(其他方法是写入无缓存的channel时,用一个单独的子goroutine)
操作:
Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。
close(ch)
对一个已经被close过的channel之行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话讲产生一个零值的数据。
遍历通道与关闭通道
通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。
for i := range c { # # 当channel被关闭并且没有值可接收时跳出循环。当它没有被引用时将会被Go语言的垃圾自动回收器回收。
fmt.Println(i)
}
如果通道接收不到数据后 ok 就为 false,v为类型的默认值,这时通道就可以使用 close() 函数来关闭。
v, ok := <-ch
close(c)
并发循环:
通过channel来实现并发循环的计数,让主goroutine阻塞等待才退出。
ch := make(chan struct{})
for _, f := range filenames {
go func(f string) {
thumbnail.ImageFile(f)
ch <- struct{}{}
}(f) # 注意这里如何封装了匿名函数,并将迭代的参数传递给goroutine。显式将变量传递给函数,而不是闭包中声明再赋值,否则当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。
} # 显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。
for range filenames {
<-ch
}
避免“循环变量快照”的问题
另外解决:
sync.WaitGroup
多路复用:
select {
case <-ch1:
case x := <-ch2:
case ch3 <- y: # select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
default: # 假设通道已关闭,每次都会执行到这个case。如果只有一个case,而这个case被关闭了,则会出现死循环。
}
并发的退出:
不要向channel发送值,而是用关闭一个channel来进行广播。
先定义一个channel。
var done = make(chan struct{})
工具函数
func cancelled() bool {
select {
case <-done: # done这个channel如果关闭了会一直返回零值。select执行这个case,然后return
return true
default:
return false
}
}
如何关闭channel:
go func() {
os.Stdin.Read(make([]byte, 1))
close(done)
}()
# 1.主goroutine要记得清空任务队列,避免子goroutine阻塞变为泄露。
for {
select {
case <-done:
for range fileSizes { # 避免其他goroutine写入阻塞,变为泄露。
}
return
case size, ok := <-fileSizes:
}
}
# 2.避免在取消事件发生时还去创建goroutine。
func walkDir(dir string, n *sync.WaitGroup, fileSizes chan<- int64) {
defer n.Done()
if cancelled() {
return
}
for _, entry := range dirents(dir) {
}
}
唯一的缺点:
当主函数返回时,所有后台的goroutine都会迅速停止并且主函数会返回,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(子routine可能正在计算中,还没到返回的判断逻辑那里)。
解决:
取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。
如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间
用channel实现定时器?(实际上是两个协程同步):
Timer(到达指定时间触发且只触发一次)
实现原理:
sleep一定时间后,往channel写入值,然后close
Ticker(间隔特定时间循环触发)
实现原理:
for死循环,然后sleep一定时间后,往channel写入值
channel的实现原理:
类型是一个结构体,qcount、dataqsiz、buf、sendx、recv字段构建底层循环数组。
对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。
Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”,带缓冲的则称为“异步模式”。
同步模式下,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(后面会看到,实际上就是内存拷贝)。否则,任意一方先行进行发送或接收操作,都会被挂起,等待另一方的出现才能被唤醒。
异步模式下,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒。
利用通信来保证原子性。
WaitGroup:
定义:
var wg sync.WaitGroup
wg.Add(1)
wg.Done() # 和Add(-1)是等价的
wg.Wait()
阻塞等待示例:
前面每起一个子routine前,先执行wg.Add(1),然后defer wg.done() # waitGroup可以用遍历总数次的<-channel来让主进程等待。
go func() {
wg.Wait()
close(sizes) # 子goroutine去关闭掉主goroutine的阻塞channel
}()
for size := range sizes { # 主进程在等待接收,直到channel关闭
total += size
}
还有其他实现方法,比如计数
实现原理:
type WaitGroup struct {
cnt chan int
end chan struct{}
}
func NewWaitGroup () WaitGroup {
wg := WaitGroup{
cnt: make(chan int,1),
end: make(chan struct{}),
}
wg.cnt <- 0
return wg
}
func(w WaitGroup)Add(i int){
var rs int
rs = <- w.cnt
rs += i
if rs==0{
close(w.end)
}
w.cnt <- rs
}
func(w WaitGroup)Done(){w.Add(-1)}
func(w WaitGroup)Wait(){
<- w.end
}
select多路复用:
概述:
Go内置了select关键字,可以同时响应多个通道的操作。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。
select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。
格式:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作,当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。不能多个default
}
如果多个case同时满足,select会随机选择一个。
一个没有任何case的select语句写作select{},会永远地等待下去。
实现原理:
扫描文件描述符
并发数量控制:
方案1:channel带缓存,但有个缺点,main线程不知道什么时候退出
方案2:WaitGroup,无法控制上限
方案3:WaitGroup + channel,goroutine开始前先往channel写值,造成阻塞
方案4:channel + WaitGroup,让主线程等待退出,和方案3差不多,主进程调用wait()等待。
并发安全:
概述:
如果这个类型是并发安全的话,那么所有它的访问方法和操作就都是并发安全的。
方法:
将变量局限在单一的一个goroutine内
用互斥条件维持更高级别的不变性
分类:
导出包级别的函数一般情况下都是并发安全的。
由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。
竞争条件:
概述:
指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果。
数据竞争:
无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。
解决:
1.不要去写变量。
2.避免从多个goroutine访问变量。限制单一的goroutine,使用一个channel来发送给指定的goroutine请求来查询更新变量。
提供对一个指定的变量通过channel来请求的goroutine叫做这个变量的监控(monitor)goroutine
3.允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。
锁
通信
互斥锁:
概述:
能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
原理:
一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。
type Once chan struct{}
func (o Once) Lock() {
o <- struct{}{}
}
注意:
锁不能拷贝,传递给外部使用的时候,需要传指针,不然传的是struct的拷贝,相当于重新定义了一把新锁。
惯例:
惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。
用封装的概念,确保mutex和其保护的变量没有被导出
示例:
var lock sync.Mutex
lock.Lock()
x = x + 1
lock.Unlock()
多个goroutine同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁:
概述:
读写锁在Go语言中使用sync包中的RWMutex类型。
当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
var rwlock sync.RWMutex
rwlock.Lock()
rwlock.Unlock()
rwlock.RLock()
rwlock.RUnlock()
实现原理:
写锁按照互斥锁来实现。channel容量都为1。
func(rw RWMutex)Rlock(){
var rs int
select {
case rw.write <- struct{}{}:
case rs = <- rw.read:
}
rs++
rw.read <- rs
}
func(rw RWMutex)Runlock(){
rs := <- rw.read
rs --
if rs == 0{
<- rw.write
return
}
rw.read <- rs
}
sync.Once:
概述:
确保某些操作在高并发的场景下只执行一次
原理:
概念上来讲,一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;
示例:
var loadIconsOnce sync.Once
func xxx(){
loadIconsOnce.Do(loadIcons)
}
原理:
func NewOnce() Once {
o := make(Once, 1)
o <- struct{}{}
return o
}
func (o Once)Do(f func()){
_,ok := <- o
if !ok {
return
}
f()
close(o)
}
添加锁可以,但有性能问题。
sync.Map:
概述:
并发安全版map
内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
示例:
var sm sync.Map
sm.Store(1,"a")
sm.Load(1)
sm.LoadOrStore(1,"c")
实现原理:
读写锁+普通map
但内置的实现有几个优化点:
1.空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
2.使用只读数据(read),避免读写冲突。
3.动态调整,miss次数多了之后,将dirty数据提升为read。
4.double-checking。
5.延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
6.优先从read读取、更新、删除,因为对read的读取不需要锁。
不能拷贝。
sync.Pool:
增加对象重用的几率,减少 gc 的负担
竞争条件检测:
只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,
并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的调用。
竞争检查器会检查这些事件,会寻找在哪一个goroutine中出现了这样的case
竞争检查器会报告所有的已经发生的数据竞争。然而,它只能检测到运行时的竞争条件;并不能证明之后不会发生数据竞争。所以为了使结果尽量正确,请保证你的测试并发地覆盖到了你到包。
go test -run=TestConcurrent -race -v
包管理:
定义(声明):
package xxx
包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。
命名规范:
名字都简洁明了
包名一般采用单数的形式
要避免包名有其它的含义
可见性:
包里的标识符,要想对外可见的,必须首字母大写
1.通过控制哪些名字是外部可见的来隐藏内部实现信息。
2.而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样
语言特性:
第一点,所有导入的包必须在每个文件的开头显式声明,这样的话编译器就没有必要读取和分析整个源文件来判断包的依赖关系。
第二点,禁止包的环状依赖,因为没有循环依赖,包的依赖关系形成一个有向无环图,每个包可以被独立编译,而且很可能是被并发编译。
第三点,第三点,编译后包的目标文件不仅仅记录包本身的导出信息,目标文件同时还记录了包的依赖关系。
因此,在编译一个包的时候,编译器只需要读取每个直接导入包的目标文件,而不需要遍历所有依赖的的文件
导入:
import
对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。
在默认情况下,导入的包绑定到tempconv名字(译注:这包声明语句指定的名字)
如果遇到包循环导入的情况,Go语言的构建工具将报告错误。
规范:
按照惯例,一个包的名字和包的导入路径的最后一个字段相同。例如gopl.io/ch2/tempconv包的名字一般是tempconv。
例外:
第一个例外,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。
第二个例外,包所在的目录中可能有一些文件名是以test.go为后缀的Go源文件,,并且这些源文件声明的包名也是以_test为后缀名的。
所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译。测试的外部扩展包一般用来避免测试代码中的循环导入依赖
第三个例外,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如"gopkg.in/yaml.v2"。这种情况下包的名字并不包含版本号后缀,而是yaml。
为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。
包的初始化:
包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化
自定义导入:
import 别名 "包1"
好处:
如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。选择用简短名称重命名导入包时候最好统一,以避免包名混乱。
选择另一个包名称还可以帮助避免和本地普通变量名产生冲突。
匿名导入包:
import _ "xxx"
会被编译,且执行init()函数
它会计算包级变量的初始化表达式和执行导入包的init初始化函数
示例:
image/png包,可以让image.Decode正确识别和解码PNG格式的图像:
数据库包database/sql也是采用了类似的技术,让用户可以根据自己需要选择导入必要的数据库驱动。
多个包:
import (
...
)
import了一个包路径包含有多个单词的package时,比如image/color(image和color两个单词),通常我们只需要用最后那个单词表示这个包就可以。
init函数:
导入包时触发调用,没有参数和返回值。这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。
执行时机:全局声明(包括导入包)=》init =》main
所以,A引用B时,先执行B的init函数
构建包:
因为每个目录只包含一个包,因此每个对应可执行程序或者叫Unix术语中的命令的包,会要求放到一个独立的目录中。
这些目录有时候会放在名叫cmd目录的子目录下面,例如用于提供Go文档服务的golang.org/x/tools/cmd/godoc命令就是放在cmd子目录
命令:
每个包可以由它们的导入路径指定
用一个相对目录的路径知指定,相对路径必须以.或..开头
默认指定为当前目录对应的包
多架构:
如果一个文件名包含了一个操作系统或处理器类型名字,例如net_linux.go或asm_amd64.go,Go语言的构建工具将只在对应的平台编译这些文件。
还有一个特别的构建注释注释可以提供更多的构建过程控制。
例如,文件中可能包含下面的注释:
下面的构建注释则表示不编译这个文件:
run运行:
第一行的参数列表中,第一个不是以.go结尾的将作为可执行程序的参数运行。
如:
go run quoteargs.go one "two three" four\ five
go install:
go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。
被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。
因为编译对应不同的操作系统平台和CPU架构,go install命令会将编译结果安装到GOOS和GOARCH对应的目录。
例如,在Mac系统,golang.org/x/net/html包将被安装到$GOPATH/pkg/darwin_amd64目录下的golang.org/x/net/html.a文件。
比较:
go build 生成可执行文件在当前目录下, go install 生成可执行文件在bin目录下($GOPATH/bin)
go build 经常用于编译测试.go install主要用于生产库和工具.
内部包:
Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。
例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。
查询包:
go list命令可以查询可用包的信息。其最简单的形式,可以测试包是否在工作区并打印它的导入路径。
go list github.com/go-sql-driver/mysql
go list命令的参数还可以用"..."表示匹配任意的包的导入路径。
go list gopl.io/ch3/...
go list ...xml...
go list命令还可以获取每个包完整的元信息,而不仅仅只是导入路径,这些元信息可以以不同格式提供给用户。其中-json命令行参数表示用JSON格式打印每个包的元信息。
go list -json hash
命令行参数-f则允许用户使用text/template包(§4.6)的模板语言定义输出文本的格式。
go list -f '{{join .Deps " "}}' strconv
打印compress子目录下所有包的依赖包列表
go list -f '{{.ImportPath}} -> {{join .Imports " "}}' compress/...
文件操作:
读取:
os包:
os.Open(path) *file,err := os.Open("./main.go")
var tmp = make([]byte, 128)
file.Read(tmp) # 按需指定长度
file.Close
bufio按行读取:
reader := bufio.NewReader(openFile)
line, prefix, err := reader.ReadLine()
io/ioutil的ReadFile:
content,err := ioutil.ReadFile("xxx")
或者直接用content ,err :=ioutil.ReadFile(filepath)
返回[]byte,不用自己申请多少空间
写入:
os包:
file,err:=os.OpenFile(name string,flag int,perm FileMode) # 写入操作
flag:
os.O_WRONLY等文件的打开方式
perm:文件权限,一个八进制数,r4 w2 x1
file.write([]byte)
bufio.NewWriter:
writer := bufio.NewWriter(file)
write.WriteString("")
writer.Flush()
io包:
io.WriteString(file,string)
ioutil.WriteFile每次都覆盖:
err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)
复制:
io.copy(dst,src)
插入:
fileobj.Seek(offset int64,whence int)
whence:0为相对文件开头,1为相对当前位置,2为相对文件结尾
遍历:
日期时间:
时间类型:
time.Time类型表示时间。
time.Now()函数获取当前的时间对象
now := time.Now() //获取当前时间
year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
时间戳:
时机转时间戳:
timestamp1 := now.Unix() //时间戳
timestamp2 := now.UnixNano() //纳秒时间戳
时间戳转为时间格式:
time.Unix()函数可以将时间戳转为时间格式。
timeObj := time.Unix(timestamp, 0)
时间间隔:
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
时间操作:
Add:
later := now.Add(time.Hour)
Sub:
返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。
Equal:
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。
Before:
如果t代表的时间点在u之前,返回真;否则返回假。
After:
如果t代表的时间点在u之后,返回真;否则返回假。
定时器:
用法一:
使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。
ticker := time.Tick(time.Second)
for i := range ticker {
fmt.Println(i)
}
用法二:
timeTicker := time.NewTicker(time.Second * 2)
<-timeTicker.C
timeTicker.Stop()
时间格式化:
时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S
而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。
时机转字符串:
now.Format("2006-01-02 15:04:05.000 Mon Jan")
now.Format("2006-01-02 03:04:05.000 PM Mon Jan") //补充:如果想格式化为12小时方式,需指定PM。
now.Format("2006/01/02 15:04")
now.Format("15:04 2006/01/02")
字符串转时间:
time.Parse("2006/01/02","2020/05/04")
loc, err := time.LoadLocation("Asia/Shanghai")
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
睡眠:
time.Sleep(100)
time.Sleep(time.Second)
time.Sleep(time.Duration(100))
而n:=100
time.Sleep(n)则会报类型错误
依赖管理:
工作区结构:
GOPATH对应的工作区目录有三个子目录。
src子目录用于存储源代码。每个包被保存在与$GOPATH/src的相对路径为包导入路径的子目录中,例如gopl.io/ch1/helloworld相对应的路径目录。
pkg子目录用于保存编译后的包的目标文件
bin子目录用于保存编译后的可执行程序,例如helloworld可执行程序。
GOROOT用来指定Go的安装目录,还有它自带的标准库包的位置。
GOROOT的目录结构和GOPATH类似,因此存放fmt包的源代码对应目录应该为$GOROOT/src/fmt。
安装第三方包:
go get xxx
go get .. 下载整个子目录里面的每个包
-u 简单地保证每个包是最新版本
关于依赖:
默认下载到$GOAPTH/src/路径下
需要注意的是项目要按照$GOAPTH/src/github.com/username/projectname/的形式去存放
注意IDE需要勾选index entire GOPATH,才能索引源码
关于版本:
高版本可以使用低版本的依赖管理,比如1.14可以使用vender和GOPATH的方式
vendor机制:
用途:
external packages 的概念,在项目的目录下增加一个 vendor 目录来存放外部的包
版本精确管理
优点:
1.解决了问题:无法适用于各个工程对于不同版本的依赖包的使用,不便于更新某个依赖包
2.将源码拷贝到当前目录下(并上传到github上),这样导包当前工程代码到任意的机器的 ¥GOPATH/src 都可以编译通过,避免项目代码外部依赖过多
前提:
要求项目位于$GOAPTH/src/路径下
注意:即使在项目中已经使用了vendor,该项目及vendor文件夹路径也必须在GOPATH中。在go项目及其工具链中,目前是逃不掉GOPATH的。
依赖查找顺序:
在Go1.5之前,一般需要修改包的导入路径,所以复制后golang.org/x/net/html导入路径可能会变为gopl.io/vendor/golang.org/x/net/html(按照GOPATH放置项目代码后)。
首先会在项目根目录下的vender文件夹中查找,如果没有找到就会去$GOAPTH/src目录下查找。
自包引用处,从其所在文件夹查询是否有vendor文件夹包含所引用包;若没有,然后从其所在文件夹的上层文件夹寻找是否有vendor文件夹包含所引用包,若没有,则再搜索上层文件夹的上层文件夹...,
直至搜索至GOPATH/src并搜索完成时止。
若不同的vendor文件夹包含相同的包,且该包在某处被引用,寻找策略仍遵循如上规则。即从包引用处起,逐层向上层文件夹搜索,首先找到的包即为所引
发展:
版本1.5后出现,现在基本不用。
Go 1.5引入了vendor文件夹,其对语言使用,go命令没有任何影响。若某个路径下边包含vendor文件夹,则在某处引用包时,会优先搜索vendor文件夹下的包。
Go 1.5开启该项特性需设置GO15VENDOREXPERIMENT=1,而从Go 1.6开始,该项特性默认开启。
使用规约:
如果是开发依赖使用三方库,需要固定使用某个版本,请完全提交vendor\文件夹
当欲将某包vendor时,可能想将所有依赖包均vendor;
尽量将vendor依赖包结构扁平化,不要vendor套vendor。
godep:
godep是一个通过vender模式实现的Go语言的第三方依赖管理工具
godep依赖vendor
安装:
go get github.com/tools/godep
基本命令:
安装好godep之后,在终端输入godep查看支持的所有命令。
godep save 将依赖项输出并复制到Godeps.json文件中,生成Godeps文件夹
godep go 使用保存的依赖项运行go工具
godep get 下载并安装具有指定依赖项的包
godep path 打印依赖的GOPATH路径
godep restore 在GOPATH中拉取依赖的版本
godep update 更新选定的包或go版本
godep diff 显示当前和以前保存的依赖项集之间的差异
godep version 查看版本信息
与vendor机制的联系:
在没有 Godeps\ 文件的情况下,生成模组依赖目录vendor\文件夹
如果是开发依赖使用三方库,需要固定使用某个版本,请完全提交Godeps\和vendor\文件夹
低版本的 godep 生成的是Godeps/_workspace,建议升级
开发流程:
1.保证程序能够正常编译
2.执行godep save保存当前项目的所有第三方依赖的版本信息和代码
3.提交Godeps目录和vender目录到代码库。
4.如果要更新依赖的版本,可以直接修改Godeps.json文件中的对应项
go module:
概述:
Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。
包不再保存在GOPATH中,而是被下载到了$GOPATH/pkg/mod路径下
解决的问题:
1.vendor 目录下的依赖包还是需要手动加入
2.没有依赖包的版本记录,那么 vendor 下的依赖包的进行升级更新也还是有困难
优点:
1.自动管理依赖包
2.有版本记录,方便更新升级。
兼容vender:
在运行go build时,优先引用的是Module依赖包的逻辑,所以Vendor目录就被“无视”了,进而可能发生编译错误, moudle 说还是很想他,于是 提供了 go mod vendor 命令用来生成 vendor 目录。这样能避免一些编译问题,依赖可以先从 vendor 目录进行扫描。
go mod vendor 会将依赖包放到 vendor 目录
GO111MODULE:
要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto。
1.GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
2.GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据go.mod下载依赖。
3.GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。
简单来说,设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。
使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod和go.sum。
GOPROXY:
export GOPROXY=https://goproxy.cn
Go1.13之后GOPROXY默认值为https://proxy.golang.org,在国内是无法访问的
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
或者
export GOPROXY=https://mirrors.aliyun.com/goproxy/ export GO111MODULE=on
命令:
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
go install 命令会完成类似 go build 的功能 ,但go install 命令执行生成的可执行文件是在【$GOPATH/bin】目录中
go get -u/-u=patch/package@version
go.mod:
依赖的版本
o mod支持语义化版本号,比如go get foo@v1.2.3,也可以跟git的分支或tag,比如go get foo@master,
当然也可以跟git提交哈希,比如go get foo@e3702bed2。
replace
替换库
开发流程:
1.在项目目录下执行go mod init,生成一个go.mod文件。
2.执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。依赖放到pkg/mod目录下
3. 通过go build或go run跑程序
go.mod与GOPATH的关系:
1.GOPATH控制的是全局的库,GOPATH目录下有src和bin,GOPATH安装的第三方库在bin下面会有可执行文件bin
go get没有全局安装的概念,看GOPATH和PATH路径,bin文件会下载到GOPATH下,其他代码新版本则放到当前目录
export PATH=$GOPATH/bin:$PATH
2.go.mod与每个项目相关,go.mod下载的库只会关联到对应的项目。与其他库无关。
版本冲突:
场景1: 包A依赖包C的v1.0.0版本,包B依赖包C的v2.0.0版本。go build时会按照高位兼容原则,取依赖包的v2.0.0版本
解决1:
的确会存在,需要取舍。下载指定版本的C,再去看有没有对应版本的B
解决2:
拉私库修改,替换包B中的地址,同时import也要修改。
解决3:
B的mod文件里添加replace,replace指定target的地址为替换后的地址,import也替换
要想import不替换,那么replace前后的地址要相同,但版本不同即可。
尝试编译:
1.注意module第一部分必须包含.
2.每个go.mod文件里面指定了module名称,所以复制要注意,要先删除,然后重新init才行。
3.如果子目录只是用来import,那么不需要go mod init,直接import即可
如果是子模块,那么replace来指定本地路径(D:\workstate\GoProJect\src\sample.com\testMod),然后再import
或者使用vendor机制
编译后:
可以执行
场景2: 自己引用同时import两个版本的依赖
解决1:
replace github.com/qiniu/qmgo077 => github.com/qiniu/qmgo v0.7.7
会自动生成require(
github.com/qiniu/qmgo077 v0.0.0-00010101000000-000000000000 //编译时会根据replace找到真实代码目录。
)
然后import qmgo077 "github.com/qiniu/qmgo077"
使用通过qmgo077
解决2:
import 不同路径的包。
"github.com/robteix/testmod"
testmodML "github.com/robteix/testmod/v2"
导入路径完全不一样,在mymod项目里可以同时存在。Go在编译时可以根据import路径自动下载依赖包。
版本更新:
场景:服务A调用B,B更新了如何同步到A(不是latest)
解决:使用commit(需要更新)、branch或者tags
命令: go get package-path@vX.X.X,go.mod中的依赖记录被更新
go sum作用:
go.sum 的本意在于提供防篡改的保障,如果拉第三方库的时候发现其实际内容和记录的校验值不同,就让构建过程报错退出。然而它能做的也就只限于此。go.sum 的检测功能,给库的使用者带来的负担更甚于库的开发者。
同时引用多个版本的module?
replace github.com/qiniu/qmgo077 => github.com/qiniu/qmgo v0.7.7
require (
github.com/qiniu/qmgo v0.7.6
github.com/qiniu/qmgo077 v0.0.0-00010101000000-000000000000
)
import (
"github.com/qiniu/qmgo"
qmgo077 "github.com/qiniu/qmgo077"
)
单元测试:
运行:
go test
go test -v # 打印每个测试函数的名字和运行时间
go test -v -run="More"
go test -cover
在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
测试分类:
在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能,go test命令会多次运行基准函数以计算一个平均的执行时间。
示例函数 函数名前缀为Example 为文档提供示例文档
测试函数:
定义:
每个测试函数必须导入 testing 包. 测试函数有如下的签名:
func TestName(t *testing.T) { # t 参数用于报告测试失败和附件的日志信息.
}
示例:
func TestCanalPalindrome(t *testing.T) {
input := "A man, a plan, a canal: Panama" # 一般有个列表,可以多次添加,更好的测试各个情况的输入
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input) # 格式化函数
}
}
表格驱动的测试:
var tests = []struct {
input string
want bool
}{
{"kayak", true} # 方便添加输入
}
停止测试:
t.Errorf 调用也没有引起 panic 或停止测试的执行.
可以使用 t.Fatal 或 t.Fatalf 停止测试. 它们必须在和测试函数同一个 goroutine 内调用.
随机测试:
对于一个随机的输入, 我们如何能知道希望的输出结果呢?
第一个是编写另一个函数, 使用简单和清晰的算法, 虽然效率较低但是行为和要测试的函数一致, 然后针对相同的随机输入检查两者的输出结果.
第二种是生成的随机输入的数据遵循特定的模式, 这样我们就可以知道期望的输出的模式.
定期运行的自动化测试集成系统, 随机测试将特别有价值.
白盒测试:
分类:
一个测试分类的方法是基于测试者是否需要了解被测试对象的内部工作原理.
黑盒测试只需要测试包公开的文档和API行为, 内部实现对测试代码是透明的.
场景:
仅仅使用导出的函数
相反, 白盒测试有访问包内部函数和数据结构的权限, 因此可以做到一下普通客户端无法实现的测试.
场景:
调用了内部的 echo 函数, 并且更新了内部的 out 全局变量,而不仅仅是调用导出的函数。
好处:
将产品代码的其他部分也替换为一个容易测试的伪对象. 使用伪对象的好处是我们可以方便配置, 容易预测, 更可靠, 也更容易观察. 同时也可以避免一些不良的副作用,
例如更新生产数据库或信用卡消费行为,在测试中用伪邮件发送函数替代真实的邮件发送函数。
问题:
当更新全局对象的时候,记得还原,以便后续其他的测试没有影响,要确保所有的执行路径后都能恢复, 包括测试失败或 panic 情形.
解决:
在这种情况下, 我们建议使用 defer 处理恢复的代码.
defer func() { notifyUser = saved }()
可以用来暂时保存和恢复所有的全局变量, 包括命令行标志参数, 调试选项, 和优化参数;安装和移除导致生产代码产生一些调试信息的钩子函数;
还有有些诱导生产代码进入某些重要状态的改变, 比如 超时, 错误, 甚至是一些刻意制造的并发行为.
扩展测试包:
通过测试扩展包的方式解决循环依赖的问题
场景1:
因为测试扩展包是一个独立的包, 因此可以导入测试代码依赖的其他的辅助包; 包内的测试代码可能无法做到.
示例:
在 net/url 包所在的目录声明一个url_test 测试扩展包. 其中测试扩展包名的 _test 后缀告诉 go test 工具它应该建立一个额外的包来运行测试.
场景2:
有时候测试扩展包需要访问被测试包内部的代码, 例如在一个为了避免循环导入而被独立到外部测试扩展包的白盒测试.可以定义 export_test.go 文件,专门用于测试扩展包的秘密出口.
示例:
fmt 包的 fmt.Scanf 需要 unicode.IsSpace 函数提供的功能,除了导入包含巨大表格数据的 unicode包,还可以定义 export_test.go 文件
package fmt
var IsSpace = isSpace
通过 fmt.IsSpace 简单导出了内部的 isSpace 函数,提供给测试扩展包使用.
测试覆盖率:
使用:
go test -run=Coverage -coverprofile=c.out gopl.io/ch7/eval
这个标志参数通过插入生成钩子代码来统计覆盖率数据. 也就是说, 在运行每个测试前, 它会修改要测试代码的副本, 在每个块都会设置一个布尔标志变量.
当被修改后的被测试代码运行退出时, 将统计日志数据写入 c.out 文件, 并打印一部分执行的语句的一个总结.
-covermode=count在每个代码块插入一个计数器而不是布尔标志量. 在统计结果中记录了每个块的执行次数, 这可以用于衡量哪些是被频繁执行的热点代码.
go tool cover -html=c.out基于输出生成一个HTML报告
性能剖析:
go test -cpuprofile=cpu.out
go test -blockprofile=block.out
go test -memprofile=mem.out
如何使用
分析收集到的数据:
使用 pprof
go tool pprof -text -nodecount=10 ./http.test cpu.log
使用 pprof 的图形显示功能. 这个需要安装 GraphViz 工具
子测试:
t.Run(name, func(t *testing.T){xxxx}
还可以通过/来指定要运行的子测试用例
go test -v -run=Split/simple
基准测试:
格式:
func BenchmarkName(b *testing.B){
}
运行:
go test -bench=Split
go test -bench=Split -benchmem
性能比较函数:
func benchmarkFib(b *testing.B, n int)
func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
运行:
go test -bench=.
go test -bench=Fib40 -benchtime=20s
重置时间:
time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
b.ResetTimer()
并行测试:
func BenchmarkSplitParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Split("沙河有沙又有河", "沙")
}
})
}
go test -bench=. -cpu 1
示例函数:
定义:
以 Example 为函数名开头
示例函数没有函数参数和返回值
用途:
1.最主要的一个是用于文档: 一个包的例子可以更简洁直观的方式来演示函数的用法, 会文字描述会更直接易懂, 特别是作为一个提醒或快速参考时.
2.在 go test 执行测试的时候也运行示例函数测试. 如果示例函数内含有类似上面例子中的 / Output: 这样的注释, 那么测试工具会执行这个示例函数, 然后检测这个示例函数的标准输出和注释是否匹配.
3.提供一个真实的演练场
Setup与TearDown:
测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。
TestMain:
如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试。
TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown)。
退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit
func TestMain(m *testing.M) {
fmt.Println("write setup code here...")
retCode := m.Run()
fmt.Println("write teardown code here...")
os.Exit(retCode)
}
需要注意的是:在调用TestMain时, flag.Parse并没有被调用。
所以如果TestMain 依赖于command-line标志 (包括 testing 包的标记), 则应该显示的调用flag.Parse。
子测试的Setup与Teardown:
有时候我们可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:测试之前的setup")
return func(t *testing.T) {
t.Log("如有需要在此执行:测试之后的teardown")
}
}
teardownTestCase := setupTestCase(t)
defer teardownTestCase(t)
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
teardownSubTest := setupSubTest(t)
defer teardownSubTest(t)
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
}
})
}
示例函数:
被go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀。它们既没有参数也没有返回值。
标准格式如下:
func ExampleName() {
}
好处:
1.示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
2.示例函数只要包含了
3.示例函数提供了可以直接运行的示例代码,可以直接在golang.org的godoc文档服务器上使用Go Playground运行示例代码。
网络编程net/http:
TCP:
server端:
listen, err := net.Listen("tcp", "127.0.0.1:20000")
conn, err := listen.Accept()
go process(conn)
_,err := conn.Read(buf[:])
client端:
conn, err := net.Dial("tcp", "127.0.0.1:20000")
_, err = conn.Write([]byte(inputInfo))
粘包:
1.定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
2.发送后先接收,再发送
UDP:
属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
server端:
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
n, addr, err := listen.ReadFromUDP(data[:])
_, err = listen.WriteToUDP(data[:n], addr)
client端:
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
http:
client端:
net包:
conn, err := net.Dial("tcp", "www.baidu.com:80")
conn.Write([]byte("GET / HTTP/1.1\r\n\r\n"))
n,err:= conn.Read(buf[:])
net/http:
resp, err := http.Get("http://example.com/")
defer resp.Body.Close()
resp, err := http.Post(url, contentType, strings.NewReader(data))
自定义client:
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
req, err := http.NewRequest("GET", "http://example.com", nil)
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
server端:
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9090", nil)
func sayHello(w http.ResponseWriter, r *http.Request) {
r.Method
r.ParseForm()
pw:=r.Form.Get("password")
}
ServeMux:
一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器
mux := http.NewServeMux()
mux.Handle("/foo", rh)
Handler:
负责输出HTTP响应的头和正文。
fasthttp:
一款不同于标准库 net/http 的 HTTP 实现。
优点:
1.net/http 的实现是一个连接新建一个 goroutine;fasthttp 是利用一个 worker 复用 goroutine,减轻 runtime 调度 goroutine 的压力
2.net/http 解析的请求数据很多放在 map[string]string(http.Header) 或 map[string][]string(http.Request.Form),有不必要的 []byte 到 string 的转换,是可以规避的
3.net/http 解析 HTTP 请求每次生成新的 *http.Request 和 http.ResponseWriter; fasthttp 解析 HTTP 数据到 *fasthttp.RequestCtx,然后使用 sync.Pool 复用结构实例,减少对象的数量
4.fasthttp 会延迟解析 HTTP 请求中的数据,尤其是 Body 部分。这样节省了很多不直接操作 Body 的情况的消耗
路由:
使用第三方的 fasthttp 的路由库 fasthttprouter 来辅助路由实现
请求处理:
RequestCtx 操作*RequestCtx 综合 http.Request 和 http.ResponseWriter 的操作,可以更方便的读取和返回数据。
模板渲染:
解析和渲染模板文件:
tmpl, err := template.ParseFiles("./hello.tmpl")
tmpl.Execute(w, "沙河小王子")
模板语法:
模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。
{{.Name}}
变量:
$obj := {{.}}
Mysql:
下载依赖:
go get -u github.com/go-sql-driver/mysql
连接:
docker run --network=host --name mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:8.0
dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
db, err := sql.Open("mysql", dsn)
err = db.Ping()
连接池:
var db *sql.DB
db, err = sql.Open("mysql", dsn)
方法:
SetMaxOpenConns
SetMaxIdleConns
查询:
err := db.QueryRow(sqlStr, 100).Scan(&u.id, &u.name, &u.age)
rows, err := db.Query(sqlStr)
defer rows.Close()
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
}
插入:
插入、更新和删除操作都使用Exec方法
sqlStr := "insert into user(name, age) values (?,?)"
ret, err := db.Exec(sqlStr, "王五", 38)
预处理:
用处:
1.优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
2.避免SQL注入问题。
实现:
sqlStr := "select id, name, age from user where id > ?"
stmt, err := db.Prepare(sqlStr)
defer stmt.Close()
rows, err := stmt.Query(0)
事务:
tx, err := db.Begin()
_, err = tx.Exec(sqlStr1, 2)
if err != nil {
tx.Rollback()
fmt.Printf("exec sql1 failed, err:%v\n", err)
return
}
err = tx.Commit()
sqlx:
第三方库sqlx能够简化操作,提高开发效率。
go get github.com/jmoiron/sqlx
查询:
sqlStr := "select id, name, age from user where id=?"
var u user
err := db.Get(&u, sqlStr, 1)
err := db.Select(&users, sqlStr, 0)
插入、更新和删除
与原生sql中的exec使用基本一致
事务操作:
可以使用sqlx中提供的db.Beginx()和tx.MustExec()
SQL注入:
我们任何时候都不应该自己拼接SQL语句!
用户可以查询我们给定条件以外的数据
gorm:
连接:
db, err := gorm.Open("mysql", "user:password@(localhost)/dbname?charset=utf8mb4&parseTime=True&loc=Local")
defer db.Close()
使用:
db.AutoMigrate(&UserInfo{})
db.Create(&u1)
db.First(u)
db.Model(&u).Update("hobby", "双色球")
db.Delete(&u)
Redis:
安装:
go get -u github.com/go-redis/redis
使用:
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
_, err = rdb.Ping().Result()
err := rdb.Set("score", 100, 0).Err()
val, err := rdb.Get("score").Result()
NSQ:
概述:
NSQ是Go语言编写的一个开源的实时分布式内存消息队列,其性能十分优异。
安装:
go get -u github.com/nsqio/go-nsq
使用:
生产者:
var producer *nsq.Producer
config := nsq.NewConfig()
producer, err = nsq.NewProducer("127.0.0.1:4150", config)
err = producer.Publish("topic_demo", []byte(data))
消费者:
config := nsq.NewConfig()
config.LookupdPollInterval = 15 * time.Second
c, err := nsq.NewConsumer(topic, channel, config)
type MyHandler struct {
Title string
}
func (m *MyHandler) HandleMessage(msg *nsq.Message) (err error) {
fmt.Printf("%s recv from %v, msg:%v\n", m.Title, msg.NSQDAddress, string(msg.Body))
return
}
consumer := &MyHandler{
Title: "沙河1号",
}
c.AddHandler(consumer)
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT)
<-c
Gin框架:
安装:
go get -u github.com/gin-gonic/gin
示例:
r := gin.Default()
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
r.Run()
渲染:
r.LoadHTMLGlob("templates/**/*")
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.html", gin.H{
"title": "posts/index",
})
})
XML:
c.XML(http.StatusOK, gin.H{"message": "Hello world!"})
c.XML(http.StatusOK, msg)
YMAL渲染:
c.YAML(http.StatusOK, gin.H{"message": "ok", "status": http.StatusOK})
获取参数:
获取querystring参数:
username := c.DefaultQuery("username", "小王子")
address := c.Query("address") //没有则为空
username := c.DefaultPostForm("username", "小王子")
username := c.PostForm("username")
获取path参数:
r.GET("/user/search/:username/:address", func(c *gin.Context) {
username := c.Param("username")
address := c.Param("address")
}
c.GetHeader(key string) string
获取文件:
file, header , err := c.Request.FormFile("upload")
参数绑定:
c.ShouldBind()强大的功能,它能够基于请求自动提取JSON、form表单和QueryString类型的数据,并把值绑定到指定的结构体对象。
使用:
err := c.ShouldBind(&login);
login结构体的字段需注释`json:"xxx" binding:"required"`
解析顺序:
1.如果是 GET 请求,只使用 Form 绑定引擎(query)。
2.如果是 POST 请求,首先检查 content-type 是否为 JSON 或 XML,然后再使用 Form(form-data)。
使用了c.Request.Body,不能多次绑定
ShouldBindQuery:
绑定querystring到结构体
多次绑定:
c.ShouldBindBodyWith(&obj,binding.JSON)
绑定前将数据缓存,以多次绑定
只有JSON\XML\MsgPack\ProtoBuf格式需要,其他如query、Form、FormPost、FormMultipart可以多次调用ShouldBind
路由:
普通路由:
r.GET("/index", func(c *gin.Context) {...})
所有方法:
r.Any("/test", func(c *gin.Context) {...})
默认路由:
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由组:
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
路由原理:
Gin框架中的路由使用的是httprouter这个库。其基本原理就是构造一个路由地址的前缀树。
文件接收:
单文件:
file, err := c.FormFile("f1")
err = c.SaveUploadedFile(file, dst)
router.MaxMultipartMemory = 8 << 20 // 处理multipart forms提交文件时默认的内存限制是32 MiB,减小占用
多文件:
form, _ := c.MultipartForm()
files := form.File["file"]
for index, file := range files {
c.SaveUploadedFile(file, dst)
}
发送往其他server用第三方库:
方法一:
fd, err := grequests.FileUploadFromDisk("./"+jobId)
ro := &grequests.RequestOptions{
Files: fd,
}
方法二:
ro := &grequests.RequestOptions{
Files: []grequests.FileUpload{{FileContents: ioutil.NopCloser(bytes.NewReader(image))}},
}
如何保存为字节:
方法一:
file, err := c.FormFile("image")
a,_:= file.Open()
defer a.Close()
buf := bytes.NewBuffer(nil) # d,err := a.Read(b) b为[]byte的话,read会按照b的长度来读,而b是切片,初始长度为0,需要make申请一下空间。空间不能超,否则文件传输
# 不知道图片大小,还不如使用bytes.NewBuffer方法,不用先确定大小
io.Copy(buf, a)
logger.Info(buf.Bytes())
方法二:
file, err := c.FormFile("image")
a,_:= file.Open()
defer a.Close()
var b = make([]byte,1024*100) # 要确保空间大于,或者循环make和read也行
d,err := a.Read(b)
logger.Info(d,b[:d]) # 可能会超出,超出部分byte为0。有些情况下0不影响文件整体(文件写入和模型识别图片都不影响)
重定向:
HTTP重定向:
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
路由重定向:
c.Request.URL.Path = "/test2"
r.HandleContext(c)
Gin中间件:
登录认证、权限校验、数据分页、记录日志、耗时统计等
定义:
func(c *gin.Context) {
start := time.Now()
c.Set("name", "小王子")
c.Next()
cost := time.Since(start)
log.Println(cost)
}
注册:
在gin框架中,我们可以为每个路由添加任意数量的中间件。
全局路由
r.Use(StatCost())
某个路由
r.GET("/test2", StatCost(), func(c *gin.Context)
路由组:
shopGroup := r.Group("/shop", StatCost())
shopGroup.Use(StatCost())
默认中间件:
gin.Default()默认使用了Logger和Recovery中间件
1.Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
2.Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
新建路由:
gin.New()新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine:
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。
Cookie:
设置Cookie:
net/http中提供了如下SetCookie函数,它在w的头域中添加Set-Cookie头,该HTTP头的值为cookie。
func SetCookie(w ResponseWriter, cookie *Cookie)
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
获取Cookie:
获取Cookie的两种方法:
1.func (r Request) Cookies() []Cookie // 解析并返回该请求的Cookie头设置的所有cookie
2.func (r *Request) Cookie(name string) (*Cookie, error)// 返回请求中名为name的cookie,如果未找到该cookie会返回nil, ErrNoCookie。
添加Cookie的方法:
func (r *Request) AddCookie(c *Cookie)// AddCookie向请求中添加一个cookie。
Session:
能支持更多的字节,并且他保存在服务器,有较高的安全性。
在服务端为每个用户创建一个特定的session和一个唯一的标识,它们一一对应。
1.Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
2.唯一标识通常称为Session ID会写入用户的Cookie中。
这样该用户后续再次访问时,请求会自动携带Cookie数据(其中包含了Session ID),服务器通过该Session ID就能找到与之对应的Session数据
内存版:
建立一个id string,data map的struct,然后以uuid为key,通过data,ok:=session[id]来取值
建立一个中间件,用于认证。在其他包导入即可使用包名.xxxmiddleware。
redis版:
set和get通过redis client进行
swagger文档:
安装:
go get -u github.com/swaggo/swag/cmd/swag
添加route:
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)
生成文档:
swag init
添加注释:
// @Summary delete dataset
// @Description delete dataset info for data-platform project
// @Produce json
// @Param projectId path string true "project id"
// @Param body body models.PostInference true "json body"
// @Success 200 {object} APISuccessResp "success"
// @Router /ai_arts/api/annotations/projects/:projectId/datasets [delete]
context:
概述:
gin中也有这个context,上下文
如何接收外部命令实现退出:
1.全局变量:
a. 使用全局变量在跨包调用时不容易统一
b. 如果worker中再启动goroutine,就不太好控制了。
2.通道方式:
func worker(exitChan chan struct{}) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-exitChan:
break LOOP
default:
}
}
wg.Done()
}
exitChan <- struct{}{}
缺点:需要维护channel,确定传输的类型
3.context标准库:
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default:
}
}
wg.Done()
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
嵌套gorotuine也起作用
介绍:
专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
当最上层的 Goroutine 因为某些原因执行失败时,下两层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 Context 时,就可以在下层及时停掉无用的工作减少额外资源的消耗
这其实就是 Golang 中上下文的最大作用,在不同 Goroutine 之间对信号进行同步避免对计算资源的浪费,与此同时 Context 还能携带以请求为作用域的键值对信息。
接口:
context.Context是一个接口,该接口定义了四个需要实现的方法。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
1.如果当前Context被取消就会返回Canceled错误;
2.如果当前Context超时就会返回DeadlineExceeded错误;
Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
Background()和TODO():
Go内置两个函数:Background()和TODO(),这两个函数分别返回一个实现了Context接口的background和todo。
我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。
Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
background和todo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
With系列函数:
1.func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel返回带有新Done通道的父节点的副本。
当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
示例:
func Rpc(ctx context.Context, url string) error {
go func() {
isSuccess := true
if isSuccess {
result <- 1
} else {
err <- errors.New("some error happen")
}
}()
select {
case <- ctx.Done():
return ctx.Err()
case e := <- err:
return e
case <- result:
return nil
}
}
在主进程上启动协程:
ctx, cancel := context.WithCancel(context.Background())
go func(){
defer wg.Done()
err := Rpc(ctx, "http://rpc_2_url")
if err != nil {
cancel() # 返回错误时取消,幂等性
}
}()
2.func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。
当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
fmt.Println(ctx.Err())
3.func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
通常用于数据库或者网络连接的超时控制
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
示例:
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
4.func WithValue(parent Context, key, val interface{}) Context
仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。
键应该定义自己的类型。
type TraceCode string
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234") # 上下文传递
key := TraceCode("TRACE_CODE")
traceCode, ok := ctx.Value(key).(string)
客户端超时控制:
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
req = req.WithContext(ctx)
transport := http.Transport{
DisableKeepAlives: true, }
client := http.Client{
Transport: &transport,
}
go func() {
resp, err := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err,
}
respChan <- rd
wg.Done()
}()
select {
case <-ctx.Done():
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
fmt.Printf("call server api failed, err:%v\n", result.err)
return
}
defer result.resp.Body.Close()
data, _ := ioutil.ReadAll(result.resp.Body)
fmt.Printf("resp:%v\n", string(data))
}
性能分析:
Go语言项目中的性能优化主要有以下几个方面:
CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
Memory Profile(Heap Profile):报告程序的内存使用情况
Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的
标准库:
runtime/pprof:采集工具型应用运行数据进行分析
net/http/pprof:采集服务型应用运行时数据进行分析
工具型应用:
import "runtime/pprof"
CPU性能分析
pprof.StartCPUProfile(w io.Writer)
pprof.StopCPUProfile()
内存性能优化:
pprof.WriteHeapProfile(w io.Writer)
得到采样数据之后,使用go tool pprof工具进行内存性能分析。
服务型应用:
gin框架推荐使用"github.com/DeanThompson/ginpprof"
如果使用了默认的http.DefaultServeMux(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil))
import _ "net/http/pprof"
访问这个连接可以看到一些基本的信息。
如果你使用自定义的 Mux,则需要手动注册一些路由规则:
r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
HTTP 服务都会多出/debug/pprof endpoint
/debug/pprof/profile:访问这个链接会自动进行 CPU profiling,持续 30s,并生成一个文件供下载
/debug/pprof/heap: Memory Profiling 的路径,访问这个链接会得到一个内存 Profiling 结果的文件
/debug/pprof/block:block Profiling 的路径
/debug/pprof/goroutines:运行的 goroutines 列表,以及调用关系
概述:
不管是工具型应用还是服务型应用,我们使用相应的pprof库获取数据之后,下一步的都要对这些数据进行分析,我们可以使用go tool pprof命令行工具。
语法;
go tool pprof [option] [binary] [source]
binary 是应用的二进制文件,用来解析各种符号;
source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。
命令:
top3
list logicCode
web
图形化:
想要查看图形化的界面首先需要安装graphviz图形化工具。
go-torch和火焰图:
安装:
go get -v github.com/uber/go-torch
安装 FlameGraph:
1.下载安装perl:https:
2.下载FlameGraph:git clone https:
3.将FlameGraph目录加入到操作系统的环境变量中。
4.Windows平台的同学,需要把go-torch/render/flamegraph.go文件中的GenerateFlameGraph按如下方式修改,
然后在go-torch目录下执行go install即可。
func GenerateFlameGraph(graphInput []byte, args ...string) ([]byte, error) {
flameGraph := findInPath(flameGraphScripts)
if flameGraph == "" {
return nil, errNoPerlScript
}
if runtime.GOOS == "windows" {
return runScript("perl", append([]string{flameGraph}, args...), graphInput)
}
return runScript(flameGraph, args, graphInput)
}
介绍:
火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短。
使用:
go-torch 工具的使用非常简单,没有任何参数的话,它会尝试从http:
它有三个常用的参数可以调整:
-u –url:要访问的 URL,这里只是主机和端口部分
-s –suffix:pprof profile 的路径,默认为 /debug/pprof/profile
–seconds:要执行 profiling 的时间长度,默认为 30s
示例:
使用wrk进行压测:go-wrk -n 50000 http:
监控:go-torch -u http:
压测工具:
推荐使用https://github.com/wg/wrk 或 https://github.com/adjust/go-wrk
pprof与性能测试结合:
go test命令有两个参数和 pprof 相关,它们分别指定生成的 CPU 和 Memory profiling 保存的文件:
-cpuprofile:cpu profiling 数据要保存的文件地址
-memprofile:memory profiling 数据要报文的文件地址
Profiling 一般和性能测试(基准测试)一起使用
go test -bench . -cpuprofile=cpu.prof
go test -bench . -memprofile=./mem.prof
断点调试工具:
下载:
go get github.com/go-delve/delve/cmd/dlv
使用:
dlv debug ./main.go
b main.main b /home/goworkspace/src/github.com/mytest/main.go:20
c
n单步运行
print xxx
locals 打印所有的本地变量
args 打印出所有的方法参数信息
使用Delve附加到运行的golang服务进行调试:
go build main.go
dlv attach 29260
然后相同的操作
flag标准库:
概述:
Go语言内置的flag包实现了命令行参数的解析,flag包使得开发命令行工具更为简单。
os.Args:
fmt.Printf(os.Args)
用法和python类似
flag包:
参数类型:
lag包支持的命令行参数类型有bool、int、int64、uint、uint64、float float64、string、duration。
定义命令行flag参数:
1.flag.Type()
flag.Type(flag名, 默认值, 帮助信息)*Type
示例:name := flag.String("name", "张三", "姓名")
需要注意的是,此时name、age、married、delay均为对应类型的指针。
2.flag.TypeVar()
flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
示例:var name string
flag.StringVar(&name, "name", "张三", "姓名")
flag.Parse():
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()来对命令行参数进行解析。
flag.Parse()
如何传递命令行参数:
-flag xxx (使用空格,一个-符号)
--flag xxx (使用空格,两个-符号)
-flag=xxx (使用等号,一个-符号)
--flag=xxx (使用等号,两个-符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。
flag其他函数:
flag.Args()
flag.NArg()
flag.NFlag()
ko:
Dockerfile:
ADD go.mod .
ADD go.sum .
RUN go mod download
ADD . .
RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-s -w" -installsuffix cgo -o myapp main.go
底层编程:
unsafe.Sizeof, Alignof 和 Offsetof
unsafe.Sizeof函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式,但是它并不会对表达式进行求值。
Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。
unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数. 和 Sizeof 类似, Alignof 也是返回一个常量表达式, 对应一个常量.
通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节), 其它的类型对齐到机器字大小.
unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
unsafe.Pointer:
大多数指针类型会写成*T,表示是“一个指向T类型变量的指针”。
unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。
比较:
和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。
转换:
一个普通的*T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的*T类型相同。
通过cgo调用C代码:
内存分配:
虚拟内存:
概述:
计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上物理内存通常被分隔成多个内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
优点:
与没有使用虚拟内存技术的系统相比,使用这种技术使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。此外,虚拟内存技术可以使多个进程共享同一个运行库,并通过分割不同进程的内存空间来提高系统的安全性。
引入虚拟内存后,让内存的并发访问问题的粒度从多进程级别,降低到多线程级别。
这是更快分配内存的第一个层次。
TCMalloc:
介绍:
TCMalloc是Thread Cache Malloc的简称,是Go内存管理的起源,Go的内存管理是借鉴了TCMalloc,随着Go的迭代,Go的内存管理与TCMalloc不一致地方在不断扩大,但其主要思想、原理和概念都是和TCMalloc一致的。
背景:
同一进程的所有线程共享相同的内存空间,他们申请内存时需要加锁,如果不加锁就存在同一块内存被2个线程同时访问的问题。
解决:
TCMalloc的做法是什么呢?为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有2个好处:
为线程预分配缓存需要进行1次系统调用,后续线程申请小内存时,从缓存分配,都是在用户态执行,没有系统调用,缩短了内存总体的分配和释放时间,这是快速分配内存的第二个层次。
多个线程同时申请小内存时,从各自的缓存分配,访问的是不同的地址空间,无需加锁,把内存并发访问的粒度进一步降低了,这是快速分配内存的第三个层次。
基本原理:
Page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
ThreadCache:每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,
快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
CentralCache:是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。
由于CentralCache是共享的,所以它的访问是要加锁的。
PageHeap:PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,
会放回PageHeap。如下图,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。
Go内存管理:
介绍:
Go内存管理源自TCMalloc,但它比TCMalloc还多了2件东西:逃逸分析和垃圾回收,这是2项提高生产力的绝佳武器。
1.线程私有性
2.内存分配粒度
原理:
Go中的内存分类并不像TCMalloc那样分成小、中、大对象,但是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象。小对象和大对象只用大小划定,无其他区分。
概念:
Page:与TCMalloc中的Page相同,x64下1个Page的大小是8KB。
Span:Span是内存管理的基本单位,代码中为mspan,一组连续的Page组成1个Span
mcache:与TCMalloc中的ThreadCache类似,mcache保存的是各种大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问。
mcentral:与TCMalloc中的CentralCache类似,是所有线程共享的缓存,需要加锁访问
mheap: 与TCMalloc中的PageHeap类似,它是堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。
大小转换:
object size:代码里简称size,指申请内存的对象大小。
size class:代码里简称class,它是size的级别,相当于把size归类到一定大小的区间段,比如size[1,8]属于size class 1,size(8,16]属于size class 2。
span class:指span的级别,但span class的大小与span的大小并没有正比关系。span class主要用来和size class做对应,1个size class对应2个span class,
2个span class的span大小相同,只是功能不同,1个用来存放包含指针的对象,一个用来存放不包含指针的对象,不包含指针对象的Span就无需GC扫描了。
num of page:代码里简称npage,代表Page的数量,其实就是Span包含的页数,用来分配内存。
转换公式:
size和size class的关系,并不是正比的。这些数据是使用较复杂的公式计算出来的,其中存在指数运算与for循环,造成每次大小转换的时间复杂度为O(N*2^N)
对一个程序而言,内存的申请和管理操作是很多的,如果不能快速完成,就是非常的低效。把以上大小转换写死到数组里,做到了把大小转换的时间复杂度直接降到O(1)。
class_to_size,size_to_class*和class_to_allocnpages的转换关系用数组维护了,用空间换时间,不用每次计算。
分配原理:
在内存分配时,会从span中拿大于或等于40的最小的span中的一个块给这个对象。
而sizeclass中这个块的大小值为48,所以虽然s1的大小是40bytes,但实际分配给这个对象的内存大小是48。
按照sizeclass划分span,然后每个span中的page又分成一个个小格子(大小相同的对象object)
span是golang内存管理的基本单位,是由一片连续的8KB(golang page的大小)的页组成的大块内存。
每个span管理指定规格(以golang 中的 page为单位)的内存块,内存池分配出不同规格的内存块就是通过span体现出来的,应用程序创建对象就是通过找到对应规格的span来存储的
要想区分不同规格的span,必须要有一个标识,每个span通过spanclass标识属于哪种规格的span,golang的span规格一共有67种。可以存放多种bytes大小的objects:span size(一般为8192) = bytes(size class) * objects数量
如8/16/32等8*n的size
每个mspan按照它自身的属性Size Class的大小分割成若干个object,每个object可存储一个对象。并且会使用一个位图来标记其尚未使用的object。
属性Size Class决定object大小,而mspan只会分配给和object尺寸大小接近的对象,当然,对象的大小要小于object大小。
小分配:
对于32kb以下的小分配,Go会尝试从本地缓存中获取,并称之为mcache。Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象
大量分配:
Go不会使用本地缓存来管理大量分配。这些大于32kb的分配将舍入到页面大小,然后将页面直接分配给堆。
数据结构对齐:
大小保证:
在Go中,如果两个值的类型为同一种类的类型,并且它们的类型的种类不为接口、数组和结构体,则这两个值的尺寸总是相等的。
一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序。为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的内存地址对齐要求。
地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节。 所以,一个结构体类型的尺寸必定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。
一个数组类型的尺寸取决于它的元素类型的尺寸和它的长度。它的尺寸为它的元素类型的尺寸和它的长度的乘积。
对齐保证:
类型对齐保证也称为值地址对齐保证。 如果一个类型T的对齐保证为N(一个正整数,一般是其中最大字节的一个字段),则在运行时刻T类型的每个(可寻址的)值的地址都是N的倍数。 我们也可以说类型T的值的地址保证为N字节对齐的。
事实上,每个类型有两个对齐保证。当它被用做结构体类型的字段类型时的对齐保证称为此类型的字段对齐保证,其它情形的对齐保证称为此类型的一般对齐保证。
一般对齐保证:
unsafe.Alignof(t)
字段对齐保证:
unsafe.Alignof(x.t)
重排优化示例:
type t1 struct {
a [2]int8 # b已经对齐,那么a需要填充6bytes
b int64 # 最大为8bytes
c int16 # 后面无其他字段了,所以要填充6bytes
}
type t2 struct {
a [2]int8 # a可以和b合并,再填充4bytes,使得对齐
b int16
c int64
}
合理重排字段可以减少填充,使 struct 字段排列更紧密。内存对齐是为了让 cpu 更高效访问内存中数据
零大小字段对齐:
零大小字段(zero sized field)是指struct{},大小为 0,按理作为字段时不需要对齐,但当在作为结构体最后一个字段(final field)时需要对齐的。
原因:
假设有指针指向这个final zero field, 返回的地址将在结构体之外(即指向了别的内存),如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放),
go会对这种final zero field也做填充,使对齐。
例外情况:
当然,有一种情况不需要对这个final zero field做额外填充,也就是这个末尾的上一个字段未对齐,需要对这个字段进行填充时,final zero field就不需要再次填充,而是直接利用了上一个字段的填充。
所以,零大小字段要避免作为 struct 最后一个字段,会有内存浪费。
64 位字安全访问保证(必须要手动对齐的):
在 32 位系统上想要原子操作 64 位字(如 uint64)的话,需要由调用方保证其数据地址是 64 位对齐的,否则原子访问会有异常。
拿uint64来说,大小为 8bytes,32 位系统上按 4字节 对齐,64 位系统上按 8字节对齐。在 64 位系统上,8bytes 刚好和其字长相同,所以可以一次完成原子的访问,不被其他操作影响或打断。
而 32 位系统,4byte 对齐,字长也为 4bytes,可能出现uint64的数据分布在两个数据块中,需要两次操作才能完成访问。如果两次操作中间有可能别其他操作修改,不能保证原子性。这样的访问方式也是不安全的。
在32位系统上,开发者有义务使64位字长的数据的原子访问是64位(8字节)对齐的。在全局变量,结构体和切片的的第一个字长数据可以被认为是64位对齐的。
保证:
变量或开辟的结构体、数组和切片值中的第一个 64 位字可以被认为是 8 字节对齐
开辟的意思是通过声明,make,new 方式创建的,就是说这样创建的 64 位字可以保证是 64 位对齐的。
内存逃逸:
介绍:
如果一个函数返回对一个变量的引用,那么它就会发生逃逸。任何时候,一个值被分享到函数栈帧范围之外,它都会在堆上被重新分配。
编译器会根据变量是否被外部引用来决定是否逃逸:
如果函数外部没有引用,则优先放到栈中;
如果函数外部存在引用,则必定放到堆中;
情况:
1.在方法内把局部变量指针返回
2.发送指针或带有指针的值到 channel 中,不知道哪个线程的goroutine会接收。
3.在一个切片上存储指针或带指针的值,切片背后的数组在堆上。
4.slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap ),一开始是栈。
5.调用接口类型的方法。接口类型的方法调用是动态调度 - 实际使用的具体实现只能在运行时确定。
6.尽管能够符合分配到栈的场景,但是其大小不能够在在编译时候确定的情况,也会分配到堆上
逃逸分析:
go build -gcflags '-m'命令来观察变量逃逸情况
如何避免:
1.如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型。
2.由于切片一般都是使用在函数传递的场景下,而且切片在 append 的时候可能会涉及到重新分配内存,如果切片在编译期间的大小不能够确认或者大小超出栈的限制,多数情况下都会分配到堆上。
3.避免函数内的变量返回指针。
4.通过unsafe包的noescape函数,遮蔽输入和输出的依赖关系。使编译器不认为 p 会通过 x 逃逸, 因为 uintptr() 产生的引用是编译器无法理解的。函数返回指针的情况下可以使用。
坏处:
堆上动态分配内存比栈上静态分配内存,开销大很多。
堆是用的时候才向系统申请的,用完了还回去,这个申请和交还的过程开销相对就比较大了。
栈是程序启动的时候,系统分好了给你的,你自己用,系统不干预。
尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。
内存溢出:
栈溢出:
栈一开始大小是2k,最大大小也就1GB,如果循环引用多次可能会发生栈溢出。
解决: 判断递归深度。
内存泄漏:
由于疏忽或错误造成程序未能释放已经不再使用的内存。 由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
分类:
1.常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2.偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3.一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。
但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
危害:
一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到
场景:
python中的长时间引用对象,比如请求结束后,cpu可以恢复到执行之前的水平;而VIRT,RES,内存占比却有显著提升,且执行完成后并未下降。多次执行,内存占用累积上涨。
暂时性内存泄露
获取长字符串中的一段导致长字符串未释放
获取长slice中的一段导致长slice未释放
在长slice新建slice导致泄漏
永久性内存泄露
golang中的goroutine无法预期退出,直到进程结束。比如channel阻塞。select阻塞。
time.Ticker未关闭导致泄漏(推荐defer)
Finalizer导致泄漏
Deferring Function Call导致泄漏
内存碎片化:
频繁申请很小的内存空间,容易出现大量内存碎片,增大操作系统整理碎片的压力。
解决:
对象池
比如连接池。
垃圾回收和内存释放:
使用垃圾回收收集不再使用的span,调用mspan.scavenge()把span释放给OS(并非真释放,只是告诉OS这片内存的信息无用了,如果你需要的话,收回去好了),然后交给mheap,mheap对span进行span的合并,
把合并后的span加入scav树中,等待再分配内存时,由mheap进行内存再分配
这个内存地址区间的内存已经不再使用,可以回收。但内核是否回收,以及什么时候回收,这就是内核的事情了。
总结:
1.使用缓存提高效率。在存储的整个体系中到处可见缓存的思想,Go内存分配和管理也使用了缓存,利用缓存一是减少了系统调用的次数,二是降低了锁的粒度,减少加锁的次数,从这2点提高了内存管理效率。
2.2以空间换时间,提高内存管理效率。空间换时间是一种常用的性能优化思想,这种思想其实非常普遍,比如Hash、Map、二叉排序树等数据结构的本质就是空间换时间,
在数据库中也很常见,比如数据库索引、索引视图和数据缓存等,再如Redis等缓存数据库也是空间换时间的思想。
内存结构:
全局区(静态区):存放全局变量、static变量等
栈区:函数中的基础类型的局部变量,比如通常情况下的值类型
堆区:几种情况,编译器优化决定,逃逸分析,比如通常情况下的引用类型,Java/golang由垃圾回收器
文字常量区:常量字符串
程序代码区:二进制代码
垃圾回收:
从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间:
1.局部变量从函数f中逃逸,被包的一级变量引用,必须在堆上分配
2.不逃逸,编译器可以选择在栈上分配*y的存储空间(也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间)
逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
分类:
1.非增量式垃圾回收
非增量式垃圾回收需要STW,在STW期间完成所有垃圾对象的标记,STW结束后慢慢的执行垃圾对象的清理。
2.增量式垃圾回收。
增量式垃圾回收也需要STW,在STW期间完成部分垃圾对象的标记,然后结束STW继续执行用户线程,一段时间后再次执行STW再标记部分垃圾对象,这个过程会多次重复执行,直到所有垃圾对象标记完成。
比较:
GC算法有3大性能指标:吞吐量、最大暂停时间(最大的STW占时)、内存占用率。
增量式垃圾回收不能提高吞吐量,但和非增量式垃圾回收相比,每次STW的时间更短,能够降低最大暂停时间,就是Go每个版本Release Note中提到的GC延迟、GC暂停时间。
优化:
然而Golang GC STW的时候减少最大暂停时间还有一种思路:并发垃圾回收,注意不是并行垃圾回收。
并行垃圾回收是每个核上都跑垃圾回收的线程,同时进行垃圾回收,这期间为STW,会暂停用户线程的执行。
并发垃圾回收是先STW找到所有的Root对象,然后结束STW,让垃圾标记线程和用户线程并发执行,垃圾标记完成后,再次开启STW,再次扫描和标记,以免释放使用中的内存。
并发垃圾回收和并行垃圾回收的重要区别就是不会持续暂停用户线程,并发垃圾回收也降低了STW的时间,达到了减少最大暂停时间的目的。
触发时间:
自动:
在分配内存时,会判断当前的Heap内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置),如果超过阈值,则启动一轮GC。
sysmon是运行时的守护进程,当超过 forcegcperiod (2分钟)没有运行GC会启动一轮GC。
主动:调用 runtime.GC(),这是阻塞式的。
标记-清除(mark and sweep)算法:
标记(Mark phase)
清除(Sweep phase),都需要stw
版本1.3以及之前使用。
三色标记:
出现:1.5版本
目的:利用Tracing GC(Tracing GC 是垃圾回收的一个大类,另外一个大类是引用计数)做增量式垃圾回收,降低最大暂停时间。
背景:原生Tracing GC只有黑色和白色,没有中间的状态,这就要求GC扫描过程必须一次性完成,得到最后的黑色和白色对象。这种方式会存在较大的暂停时间。
介绍:三色标记增加了中间状态灰色,增量式GC运行过程中,应用线程的运行可能改变了对象引用树,只要让黑色对象直接引用白色对象,GC就可以增量式的运行,减少停顿时间。
黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。
灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。
白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。
三色标记规则:黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。
原理:
起初所有对象都是白色。
从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
重复3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
流程:
start-scan -> Mark -> Mark termination -> Sweep -> Off
1.初始所有对象被标记为白色。从 root 开始遍历,root 包括全局指针和 goroutine 栈上的指针。
Mark Prepare: 初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),
第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列。这个过程需要STW
2. GC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行。
第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;
Mark Termination: 完成标记工作,重新扫描(re-scan)全局指针和栈。
因为Mark和用户程序是并行的,所以在Mark过程中可能会有新的对象分配和指针赋值,这个时候就需要通过写屏障(write barrier)记录下来,re-scan 再检查一下。
这个过程也是会STW的。
标记阶段的前后需要STW一定时间来做GC的准备工作和栈的re-scan。
第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录
3.Sweep: 按照标记结果回收所有的白色对象,该过程后台并行执行
Sweep Termination:对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC。
STW:
目前整个GC流程会进行两次STW(Stop The World), 第一次是Mark阶段的开始, 第二次是Mark Termination阶段.
第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist).
第二次STW会重新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist).
需要注意的是, 不是所有根对象的扫描都需要STW, 例如扫描栈上的对象只需要停止拥有该栈的G.
写屏障的实现使用了Hybrid Write Barrier, 大幅减少了第二次STW的时间.
三色标记的缺点:
一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性。
条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下),黑色已经扫完,那么扫描结束,白色将不被扫描到,将会被清除。
条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)
解决:
使用一个机制,来破坏上面的两个条件之一就可以了.
屏障机制:
强三色不变式
不存在黑色对象引用到白色对象的指针。(上面的两个条件都不满足时)
弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态.黑色对象可以指向白色对象,但此白色对象链路的上游中必须有一个灰色对象
满足条件1,但不满足条件2.
写屏障(Write Barrier)/插入屏障(insertion barrier)技术:
具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
流程:
1.程序起初创建,全部标记为白色,将所有的对象放入白色集合中。
2.遍历Root set(非递归形式,只遍历一次),得到灰色对象(最外层的父对象)。
3.遍历灰色标记表,将可达的对象,从白色标记为灰色,遍历之后的灰色,标记为黑色。
4.如果堆区和栈区的黑色对象,添加对象,那么堆区的黑色对象将触发插入屏障机制而栈区黑色不触发。
5.堆区的黑色对象添加的白色对象将标记为灰色。
6.继续循环上述流程。
由于栈没有屏障,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(黑色已扫描后再引用的白色),所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.
7.在准备回收白色前,重新遍历一次栈空间,此时需要STW,防止又有新的白色被黑色添加
8.STW期间,完成栈区的三色标记
9.停止STW。
10.清除白色。
缺点:
结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;
Yuasa 删除屏障:
具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
流程:
1.程序起初创建,全部标记为白色,将所有的对象放入白色集合中。
2.遍历Root set(非递归形式,只遍历一次),得到灰色对象(最外层的父对象)。
3.遍历灰色标记表,此时灰色对象删除指向下面的白色对象的引用,如果不触发删除屏障,后面的几个白色链上的对象均被删除。
4.触发删除屏障,删除引用的白色自身标记为灰色
5.遍历灰色标记表,将可达的对象,从白色标记为灰色,遍历之后的灰色,标记为黑色。
6.继续循环上述流程。
7.清除白色。
缺点:
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
混合写屏障:
1.8版本出现
避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。
整体过程几乎不需要stw,全面去除 STW 的一个改进,转而并发一个一个栈处理的方式(每个栈单独暂停)。但没有解决删除屏障的缺点?
流程:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW),
2、GC期间,任何在栈上创建的新对象,均为黑色。(去掉了STW)
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。
辅助GC
将一定量的标记和清扫工作交给用户goroutine来执行
Go 语言如果发现扫描后回收的速度跟不上分配的速度它依然会把用户逻辑暂停,用户逻辑暂停了以后也就意味着不会有新的对象出现,同时会把用户线程抢过来加入到垃圾回收加快垃圾回收的速度。
这样一来原来的并发还是变成了STW,还是得把用户线程暂停掉,要不然扫描和回收没完没了了停不下来,因为新分配对象比回收快,所以这种东西叫做辅助回收。
为什么小对象多了会造成gc压力?
通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配.
并发赋值:
概述:
为什么会并发不安全,比如一个变量简单的自增操作count++其实是分成两步执行的,当分成了两步执行,那么其他协程就可以趁着这个时间间隙作怪。
所以,当执行一个操作的时候,能划分为多个指令,那么就会并发不安全。
结构体:
struct结构体中有多个字段,赋值时,并不是原子操作,各个字段的赋值是独立的,在并发操作的情况下可能会出现异常。
一个字段是并发安全的。
解决方法:
atomic.Value一个开箱即用的类型,来保证赋值的并发安全
用法:
var v atomic.Value
v.Store(Test{1,2})
g := v.Load().(Test)
哪些类型并发赋值是安全的:
Golang 中数据类型可以分类两大类:基本数据类型和复合数据类型。
基本数据类型有:字节型,布尔型、整型、浮点型、字符型、复数型、字符串。
复合数据类型包括:指针、数组、切片、结构体、字典、通道、函数、接口。
复合数据类又可细分为如下三类:
(1)非引用类型:数组、结构体;
(2)引用类型:指针、切片、字典、通道、函数;
(3)接口。
基本类型的并发赋值:
字节型、布尔型、整型、浮点型、字符型(安全):
由于字节型、布尔型、整型、浮点型、字符型的位宽不会超过 64 位,在 64 位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的。
复数型(不安全):
按照上面的分析,因为复数型分为实部和虚部,两者的赋值是分开进行的,所以复数类型并发赋值是不安全的。
注意:如果复数并发赋值时,有相同的虚部或实部,那么两个字段赋值就退化成一个字段,这种情况下时并发安全的。
字符串(不安全):
字符串在 Go 中是一个只读字节切片。
字符串有两个重要特点:
(1)string 可以为空(长度为 0),但不会是 nil;
(2)string对象不可以修改。
底层数据结构:
type stringStruct struct {
str unsafe.Pointer
len int
}
str 为字符串的首地址;
len 为字符串的长度(单位字节);
string 数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上 string 和字节切片间经常强制互转。
总结:
因为 string 底层结构是个 struct,前面已经讨论过 struct 并发赋值是不安全的,所以 string 的并发赋值同样是不安全。
总结:
只要底层结构是 struct 的类型,那么并发赋值都是不安全的。
注意不安全不代表一定发生错误。就是说不安全不代表任何并发赋值的情况下都会发生错误。
1.比如上面测试代码循环次数少的情况下,很难出现出现异常情况。
2.只要不同的值满足一定特点,不管多少次并发,都是安全的。
因为 struct 多个字段的赋值是独立,所以如果两个字段中只有一个字段是不同的,那么并发赋值就变成了一个字段的并发赋值,这样就不会出现问题。
复合数据类型的并发赋值:
指针(安全):
指针是保存另一个变量的内存地址的变量。指针的零值为 nil。
因为是内存地址,所以位宽为 32位(x86平台)或 64位(x64平台),赋值操作由一个机器指令即可完成,不能被中断,所以也不会出现并发赋值不安全的情况。
函数(安全):
Go 函数可以像值一样传递。
函数类型的变量赋值时,实际上赋的是函数地址,一条机器指令便可以完成,所以并发赋值是安全的。
查看函数类型的宽度(字节)
type Add func(int, int) int
var add Add
fmt.Println(unsafe.Sizeof(add))
数组(不安全):
array 是相同类型值的集合,数组的长度是其类型的一部分。
整个数组的数据,所以数组不是引用类型。
数组的底层数据结构就是其本身,是一个相同类型不同值的顺序排列。所以如果数组位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值其实也是安全的,只不过这个大部分情况并非如此,所以其并发赋值是不安全的。
示例:
位宽为 32 位的数组 [4]byte,虽然有四个元素,但是赋值时由一条机器指令完成,所以也是原子操作。
把字节数组的长度换成[3]byte、[5]byte、[7]byte,即使没有超过 64 位,也需要多条指令完成赋值,因为 CPU 中并没有这样位宽的寄存器,需要拆分为多条指令来完成。
切片、字典、通道、接口(不安全):
概述:
底层数据结构都是 struct,所以并发都不是安全的
切片:
切片是动态调整大小的,内部是对数组的引用,相当于动态数组。如上所述,数组的大小是固定的,因此切片为数组提供了更灵活的接口。
底层结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
map:
map 并发读写会引发 panic,一般使用读写锁 sync.RWMutex 来保证安全。
通道:
因为 channel 通常用法是初始化后作为共享变量在 goroutine 之间提供同步和通信,很少会发生赋值,就是把一个 channel 赋给另一个 channel,所以这里就不过多讨论其并发赋值的安全性。如果真的有这种情况,那么只要知道其底层数据结构是个 struct,并发赋值时不安全的即可。
接口:
接口是 Go 中的一个类型,它是方法的集合。实现接口的所有方法的任何类型都属于该接口类型。接口的零值为 nil。
定义一个接口类型的变量后,如果具体类型实现了接口的所有方法,我们可以将任何具体类型的值赋给这个变量。
实际上 Go 中的接口有个特殊情况,就是空接口,其不包含任何方法。因此,默认情况下,所有具体类型都实现空接口。
在底层实现上使用runtime.iface表示非空接口,使用runtime.eface表示空接口 interface{}。
runtime.iface结构:
type iface struct {
tab *itab 每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示
data unsafe.Pointer
}
itab结构:
type itab struct {
inter *interfacetype
_type *_type
hash uint32 hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致;
_ [4]byte
fun [1]uintptr
}
fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的。
eface结构:
type eface struct {
_type *_type
data unsafe.Pointer 只包含指向底层数据和类型的两个指针
}
其中runtime._type是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
注意:
接口底层数据结构包含两个字段,相互赋值时如果是相同具体类型不同值并发赋给一个接口,那么只有一个字段 data 的值是不同的,此时退化成指针的并发赋值,所以是安全的。但如果是不同具体类型的值并发赋给一个接口,那么并引发 panic。