环境变量:
GOPATH:
window下默认值路径为%USERPROFILE%/go,可以删掉新建,然后所有的项目代码放在src子目录下
GOPATH路径下有三个目录src pkg bin
具体的子代码放在src/xxx/xxx.go,这样就可以go mod init了
GOROOT:
是我们安装go开发包的路径
默认情况下 GOROOT下的bin目录及GOPATH下的bin目录都已经添加到环境变量中,我们可能需要修改对应的path为自定义GOPATH/binGo1.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:configsdatabasedocsmiddlewaremodelrepositoryrouterserviceutilsclientcmdconfigscriptsbuildutils
企业:
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 stringfunc 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] // map查找,失败时返回零值v = x.(T) // type断言,失败时panic异常v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败)_, ok = m[key] // map返回2个值_, ok = mm[""], false // map返回1个值_ = mm[""] // map返回1个值
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:
定义:
分配一片内存空间并返回指向这片内存空间的指针,用于对象struct
对于值类型:
new函数使用常见相对比较少,因为对应结构体来说,可以直接用字面量语法创建新变量的方法会更灵活
var a = new(int) 得到不为nil的指针,稍后即可*a = 3来使用。
不等同于var a *int,该写法得到的是指针类型,值为nil。
对比var a int,得到的是零值
对于引用类型:
new可以用于引用类型,返回的是申请内存空间后的对应类型的非nil指针,是可以直接使用len和cap。
对于slice和map,返回的是nil slice和nil map,申请内存长度为0,cap为0区别:var a *[]int返回的是空指针,可以append,例如*list = append(*list, 1)var a []int返回的是空slice(零值),内存地址为空0x0,两种写法都可以len,cap,只是第一种写法需要取值*等价于:var f *[]int var h []int f = &h
对于channel,new之后返回的是nil channel,读写会阻塞,panic错误为deadlock!
示例:
c := new(Person)
c = &Person{"xuxiaofeng",26}
相当于
var c *Person # 看起来两者没区别,但 new(T) 返回 T 的指针 *T 并指向 T 的零值。注意是返回指针(区别所在)
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")
format:
%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 = 1b
)
iota:
特殊常量,可以认为是一个可以被编译器修改的常量。在第一个声明的常量所在的行,iota这个变量将会被置为0,然后在每一个有常量声明的行加一。
(iota 可理解为 const 语句块中的行索引)。
const (a = iota //0,如果没有第一行使用,而是后面使用了,也会从0开始递增。b //1c //2d = "ha" //独立值,iota += 1,就算没有使用,还是会增加e //"ha" iota += 1f = 100 //iota +=1g //100 iota +=1h = iota //7,恢复计数i //8
)
应用:
结合位运算符<<,可以表示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{ //记得同一行
}
switchswitch coinflip() { # switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较);case "heads":heads++case "tails":tails++default:fmt.Println("landed on edge!")}
select用于channel
循环语句:
for:
写法一:
for initialization; condition; post {// zero or more statements
}
initialization部分是可选的,在for循环之前这部分的逻辑会被执行,通常是一些简短的变量声明,一个赋值语句,或是一个函数调用
condition部分必须是一个结果为boolean值的表达式,在每次循环之前,语言都会检查当前是否满足这个条件,若不满足的话便会结束循环;
post部分的语句则是在每次循环迭代结束之后被执行,
示例:for i:=0;i<5;i++{}
写法二:
for _, arg := range os.Args[1:] {s += sep + argsep = " "
}
可以在循环里往原切片添加元素,在循环开始前会获取切片的长度 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为9summer := 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() intLess(i, j int) bool // i, j 是元素的索引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的行为和其它任意0长度的slice一样;例如reverse(nil)
除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。
长度为0的切片:
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
空slice:长度为 0,容量为0,区别是未初始化的。
append()函数:
用法:
numbers = append(numbers, 2,3,4) # 不能确定是否同一个slice,通常是将append返回的结果直接赋值给输入的slice变量
x = append(x, x...) # “...”省略号表示接收变长的参数为slice
扩容:
每次调用appendInt函数,必须先检测slice底层数组是否有足够的容量来保存新添加的元素。
1.如果有足够空间的话,直接扩展slice(依然在原有的底层数组之上),将新添加的y元素复制到新扩展的空间
2.如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大的slice用于保存新的结果,这个slice与输入的slice将会引用不同的底层数组。同时旧的数组内存地址仍然存在。自动扩容,每次扩容变为2倍(在1024前直接翻倍,cap超过1024的,新cap变为老cap的1.25倍),避免了多次内存分配,也确保了添加单个元素操的平均时间是一个常数时间。数组的内存是连续的
底层结构:
从这个角度看,slice并不是一个纯粹的引用类型,它实际上是一个类似下面结构体的聚合类型type IntSlice struct {ptr *intlen, cap int}
copy()函数:
copy(numbers1,numbers) /* 拷贝 numbers 的内容到 numbers1 */
返回成功复制的元素的个数,等于两个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)
声明并初始化:
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.在结构体比较大的时候,用指针效率会更好,因为不需要值copy2.先整体取出来,赋值给临时变量,然后再修改。最后放回去。
使用:
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 := aa["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 definitionmember 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 {PointRadius int}var w Circlew.X = 8 # 这样即可直接访问,也可以w.Point.X = 8,在访问子成员的时候可以忽略任何匿名成员部分
不幸的是,结构体字面值并没有简短表示匿名成员的语法只能w = Circle{Point{8, 8}, 5}, 20
作用: 匿名类型的方法集。简短的点运算符语法可以用于选择匿名成员嵌套的成员,也可以用于访问它们的方法。比如w.strings.lowcase()
嵌套结构体:
可以使用匿名字段,并且没有重复字段的情况下,支持直接访问而不用写父级字段
父层也有相同字段的情况下,访问的是父层,如果是子层内调用其他方法,调用的是子层的方法而不是父类的
子层都有该字段时,不能直接访问,需显示调用
自嵌套:
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。)
但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。type tree struct {value intleft, right *tree}
空结构体:
写作struct{}。它的大小为0,占用了0字节的内存空间。也不包含任何信息,但是有时候依然是有价值的。
有些Go语言程序员用map带模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。seen := make(map[string]struct{}) // set of strings// ...if _, ok := seen[s]; !ok {seen[s] = struct{}{}// ...first time seeing s...}
继承:
嵌套匿名结构体,父层结构体可以使用调用子层结构体的方法
与Java继承的比较:Java的继承,方法会自动继承,除了私有方法和构造方法而Golang的继承是通过结构体嵌套,而子结构体调用方法是调用自己的方法func (a *Animal) Play() {fmt.Println(a.Speak()) //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的对象,可以使用TagColor 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.ConfigCompatibleWithStandardLibraryresult, _ := json.Marshal(&s)
函数:
定义:
func function_name( [parameter list] ) [return_types] {函数体
}
示例: func hypot(x, y float64) float64func 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){} # a是一个切片,通过xxx(a...)拍扁或者xxx(1,2)调用
递归调用:
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) intf(3) // 此处f的值为nil, 会引起panic错误
作用: 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]) // 注意短声明的话无法递归调用compile error: undefined: visitAll // ...}
常见的一个问题:for _, d := range tempDirs() {dir := d // NOTE: necessary!rmdirs = append(rmdirs, func() {os.RemoveAll(dir) # 闭包的概念,循环变量的作用域,如果是d的话,函数值中记录的是循环变量的内存地址,而不是循环变量某一时刻的值})}
defer语句:
定义:
将跟随的语句进行延迟处理(函数的最后执行),多个defer将会逆序执行,先被defer的语句最后被执行
触发时机:
包裹defer的函数返回时
使用场景:
经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。func bigSlowOperation() {defer trace("bigSlowOperation")() // 两个调用,最后一个调用只会在最后执行,而其他的会在defer执行到的时候执行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 intdefer func() {result++fmt.Println("defer")}()return result
}
写法二:
func namedReturnValues() (result int) { # 有名返回值则是在函数声明的同时被声明defer func() { //defer语句中的函数会在return语句更新返回值变量后再执行result++fmt.Println("defer")}()return result // 对匿名函数采用defer机制,可以使其观察函数的返回值,或者修改
}
第一种写法:
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]byten := 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)}}()// ...parser...}
在panic之后,无法保证包级变量的状态仍然和我们预期一致。比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。
此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。
规范:
不应该试图去恢复其他包引起的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没有做任何操作。}}()
闭包:
匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。func getSequence() func() int {i:=0return func() int {i+=1return 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.Distancedistance(p, q) # 之前是方法选择器调用方法p.distance(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.Mutexmapping 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.QueryStringParameterserr := 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.ReadWriterw = rw.(io.Writer)
示例:
var x interface{}
x = ""
v,ok = x.(string) # v是转为T类型的变量var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) #rw变量的类型变为io.ReadWriter接口,值的动态值和动态类型不变
多次断言可以使用switch语句:
func whichType(n Shaper) { # 一般不用interface{}类型,否则不能使用x.方法(编译期不通过)。区别于x.(具体类型),这种编译期能确定具体类型。switch v:=x.(type){ # 单个case中,可以出现多个结果选项。case string:var o string = vdefault: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) { # 使用了关键词字面量typecase 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)// ...floating-point and complex cases omitted for brevity...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: // reflect.Array, reflect.Struct, reflect.Interfacereturn 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 stringPkgPath stringType Type //字段的类型Tag StructTag //字段的标签,通过在定义结构体时每个字段单独定义,可以使用tag.Get("json")Offset unitptr // 字段在结构体中的字节偏移量Index []int //Type.FieldByIndex时的索引切片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é.goGOMAXPROCS=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给Pmutex 调用互斥进入_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这个循环链表中的发送或者接收的indexlock是个互斥锁。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) // 缓存容量为3
声明并初始化:
ch := make(chan int) // 无缓冲通道,一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞
无缓存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) //自动识别是否close}如果通道接收不到数据后 ok 就为 false,v为类型的默认值,这时通道就可以使用 close() 函数来关闭。v, ok := <-chclose(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:// ...use x...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,然后returnreturn truedefault:return false}}
如何关闭channel:go func() {os.Stdin.Read(make([]byte, 1)) // read a single byteclose(done)}()
# 1.主goroutine要记得清空任务队列,避免子goroutine阻塞变为泄露。for {select {case <-done:for range fileSizes { # 避免其他goroutine写入阻塞,变为泄露。// Do nothing.}returncase 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) {// ...写入channel}}
唯一的缺点:当主函数返回时,所有后台的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 intend chan struct{}
}
func NewWaitGroup () WaitGroup {wg := WaitGroup{cnt: make(chan int,1),end: make(chan struct{}),}wg.cnt <- 0return wg
}
func(w WaitGroup)Add(i int){var rs intrs = <- w.cntrs += iif 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() // 加锁,已经锁上的mutex会阻塞
x = x + 1 // 临界区
lock.Unlock() // 解锁,复杂的情况需要defer:defer mu.Unlock()多个goroutine同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁:
概述:
读写锁在Go语言中使用sync包中的RWMutex类型。
当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;
当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。var rwlock sync.RWMutexrwlock.Lock() // 加写锁rwlock.Unlock() // 解写锁rwlock.RLock() // 加读锁rwlock.RUnlock() // 解读锁
实现原理:
写锁按照互斥锁来实现。channel容量都为1。
func(rw RWMutex)Rlock(){var rs intselect {case rw.write <- struct{}{}: //还没获取过读锁,等待写锁case rs = <- rw.read: //已经有读锁了,write也获取过了}rs++rw.read <- rs
}
func(rw RWMutex)Runlock(){rs := <- rw.readrs --if rs == 0{<- rw.writereturn}rw.read <- rs
}
sync.Once:
概述:
确保某些操作在高并发的场景下只执行一次
原理:
概念上来讲,一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;
示例:
var loadIconsOnce sync.Once
func xxx(){loadIconsOnce.Do(loadIcons)//do something
}
原理:
func NewOnce() Once {o := make(Once, 1)// 只允许一个goroutine接收,其他goroutine会被阻塞住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语言的构建工具将只在对应的平台编译这些文件。还有一个特别的构建注释注释可以提供更多的构建过程控制。例如,文件中可能包含下面的注释:// +build linux darwin在包声明和包注释的前面,该构建注释参数告诉go build只在编译程序对应的目标操作系统是Linux或Mac OS X时才编译这个文件。下面的构建注释则表示不编译这个文件:// +build ignore
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() # r.Read(buf)也可以按长度读取
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 = 1Microsecond = 1000 * NanosecondMillisecond = 1000 * MicrosecondSecond = 1000 * MillisecondMinute = 60 * SecondHour = 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()
时间格式化:
睡眠:
time.Sleep(100)
time.Sleep(time.Second)
time.Sleep(time.Duration(100))
而n:=100time.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.6github.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" // 匹配搜索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 stringwant 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 fmtvar 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
如何使用
分析收集到的数据:使用 pprofgo 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.SetParallelism(1) // 设置使用的CPU数,默认值为GOMAXPROCSb.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.Exitfunc TestMain(m *testing.M) {fmt.Println("write setup code here...") // 测试之前的做一些设置// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()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) // 测试之前执行setup操作defer teardownTestCase(t) // 测试之后执行testdoen操作 for name, tc := range tests {t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作defer teardownSubTest(t) // 测试之后执行testdoen操作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.示例函数只要包含了// Output:也是可以通过go test运行的可执行测试。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.Methodr.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.0dsn := "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)
方法:SetMaxOpenConnsSetMaxIdleConns
查询:
err := db.QueryRow(sqlStr, 100).Scan(&u.id, &u.name, &u.age)
// scan会释放数据库连接rows, err := db.Query(sqlStr)
defer rows.Close()
for rows.Next() {var u usererr := 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 usererr := 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 setDB: 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
}
// HandleMessage 是需要实现的处理消息的方法
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
<-c // 阻塞
Gin框架:
安装:
go get -u github.com/gin-gonic/gin
示例:
r := gin.Default()
r.GET("/hello", func(c *gin.Context) { // GET:请求方式;/hello:请求的路径c.JSON(200, gin.H{ // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数"message": "Hello world!", // c.JSON:返回JSON格式的数据})
})
r.Run() // 启动HTTP服务,默认在0.0.0.0:8080启动服务
渲染:
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") //没有则为空
获取form参数:
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”)
### 参数绑定:
json:"xxx" binding:"required"
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/”)
#### 路由重定向:
### Gin中间件:
登录认证、权限校验、数据分页、记录日志、耗时统计等
定义:
func(c *gin.Context) {
start := time.Now()
c.Set(“name”, “小王子”) // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
c.Next() // 调用该请求的剩余处理程序
// c.Abort() // 不调用该请求的剩余处理程序
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” # 还支持query
// @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{}) { //需要维护一个共用的channel
LOOP:
for {
fmt.Println(“worker”)
time.Sleep(time.Second)
select {
case <-exitChan: // 等待接收上级通知
break LOOP
default:
}
}
wg.Done()
}
exitChan <- struct{}{} //相比bool更省内存
缺点:需要维护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() // 通知子goroutine结束
嵌套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() // 当我们取完需要的整数后调用cancel
示例:
func Rpc(ctx context.Context, url string) error {
go func() {
// 进行RPC调用,并且返回是否成功,成功通过result传递成功信息,错误通过error传递错误信息
isSuccess := true
if isSuccess {
result <- 1
} else {
err <- errors.New(“some error happen”)
}
}()
select {
case <- ctx.Done():
// 其他RPC调用调用失败
return ctx.Err()
case e := <- err:
// 本RPC调用失败,返回错误信息
return e
case <- result:
// 本RPC调用成功,不返回错误信息
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())//case <-ctx.Done()后可以查看超时原因
#### 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) // 使用带超时的ctx创建一个新的client request
transport := http.Transport{
DisableKeepAlives: true, }// 请求频繁可定义全局的client对象并启用长链接,请求不频繁使用短链接
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():
//transport.CancelRequest(req) //旧版本手动取消
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 列表,以及调用关系
### go tool pprof命令:#### 概述:
不管是工具型应用还是服务型应用,我们使用相应的pprof库获取数据之后,下一步的都要对这些数据进行分析,我们可以使用go tool pprof命令行工具。
语法;
go tool pprof [option] [binary] [source]
binary 是应用的二进制文件,用来解析各种符号;
source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。
#### 命令:
top3 //来查看程序中占用CPU前3位的函数
list logicCode //详细分析
web //图形化
#### 图形化:
想要查看图形化的界面首先需要安装graphviz图形化工具。
### go-torch和火焰图:#### 安装:
go get -v github.com/uber/go-torch
安装 FlameGraph:
1.下载安装perl:https://www.perl.org/get.html
2.下载FlameGraph:git clone https://github.com/brendangregg/FlameGraph.git
3.将FlameGraph目录加入到操作系统的环境变量中。
4.Windows平台的同学,需要把go-torch/render/flamegraph.go文件中的GenerateFlameGraph按如下方式修改,
然后在go-torch目录下执行go install即可。
// GenerateFlameGraph runs the flamegraph script to generate a flame graph SVG.
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://localhost:8080/debug/pprof/profile获取 profiling 数据。
它有三个常用的参数可以调整:
-u –url:要访问的 URL,这里只是主机和端口部分
-s –suffix:pprof profile 的路径,默认为 /debug/pprof/profile
–seconds:要执行 profiling 的时间长度,默认为 30s
#### 示例:
使用wrk进行压测:go-wrk -n 50000 http://127.0.0.1:8080/book/list
监控:go-torch -u http://127.0.0.1:8080 -t 30
### 压测工具:
推荐使用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() 返回命令行参数后的其他参数,以[]string类型
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(N2^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由垃圾回收器
文字常量区:常量字符串
程序代码区:二进制代码
#### 垃圾回收:
为什么小对象多了会造成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 { // 16 字节
tab *itab 每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示
data unsafe.Pointer
}
itab结构:
type itab struct { // 32 字节
inter *interfacetype
_type *_type
hash uint32 hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致;
_ [4]byte
fun [1]uintptr
}
fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的。
eface结构:
type eface struct { // 16 字节
_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。