[https://go.dev](https://go.dev)
# 准备
## [下载安装](https://go.dev/doc/install)
下载安装包,并解压到安装路径(如`$HOME/golibexec`),最后配置环境变量。
`go env -w GOROOT=$HOME/golibexec` 安装路径
`go env -w GOPATH=$HOME/go` 存放 module 缓存,工具等
```
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
```
## [工具集](http://golang.org/doc/cmd)
建议的 [Project layout](https://go.dev/doc/modules/layout)
编译并直接运行:`go run main.go`
清除src目录下build生成的临时对象文件:`go clean`
编译链接过程: `go build -x`
交叉编译:
如在mac上:`GOOS=linux GOARCH=amd64 go build`
AMD64 优化:
GOAMD64=v1/v2/v3/v4,默认v1,越高兼容性越差性能越好
Build info:
`runtime/debug.ReadBuildInfo` 读取 build 信息
`go version -m ./app`
[Presentation](https://pkg.go.dev/golang.org/x/tools/present):
`go install golang.org/x/tools/cmd/present`
run locally in slides dir: `present -base ~/go/src/golang.org/x/tools/cmd/present`
host in github: http://go-talks.appspot.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide
## 开发辅助工具
github.com:
[godef](https://github.com/rogpeppe/godef)
[gotags](https://github.com/jstemmer/gotags)
[gocode](https://github.com/nsf/gocode) 在`$GOPATH/pkg/$GOOS_$GOARCH`和`$GOROOT/pkg/$GOOS_$GOARCH`找lib,本地lib必须先install到pkg下
[errcheck](https://github.com/kisielk/errcheck)
[golint](https://github.com/golang/lint)
https://go.googlesource.com/tools/
goimports
oracle
gorename
用[vim-go](https://github.com/fatih/vim-go)的`:GoInstallBinaries`命令安装
github.com/nsf/gocode
github.com/rogpeppe/godef
github.com/golang/lint/golint
github.com/kisielk/errcheck
github.com/jstemmer/gotags
golang.org/x/tools/cmd/goimports
golang.org/x/tools/cmd/oracle
golang.org/x/tools/cmd/gorename
# Basics
源文件以UTF-8编码。
语句以分号结尾,一个语句时通常省略,全局域中以及括号后边也不需要。
圆括号通常只用在参数列表中。
花括号的位置在一行末尾。(gofmt工具可以用来格式化代码风格)
标识: 数字,字母,下划线。
注释: `/* */`,`//`
命名:camelCasing风格。
## 基本类型
数字: int, int8/16/32/64, uint, uint8/16/32/64, float32/64, byte(unit8), rune(int32, 代表一个Unicode码), uintptr, complex64/128。支持八进制(0),十六进制(0x)及科学记数法。
布尔(bool): true, false (预定义常量)
[字符串(string)](https://blog.golang.org/strings):
* 字面值以UTF-8表示。
* 默认空串(`""`)。不以0结尾,字符串可以包含任意字节。
* 两个字符串比较时,逐字节比较。
* 可复制但不可修改其中字符,要修改必须先转为`[]byte`, `[]rune`再转回来并会造成复制(`*(*string)(unsafe.Pointer(&b))`通过指针避免复制)。
* 下标访问其中的字节/字符(字符可以用`''`表示)。
* `+`连接字符串。内部是UTF8的字节数组,可以用slice相关的操作。
* 16进制: `"\xFF" // 1 byte`
* Unicode: `"\u2318" // 1 unicode char, 3 bytes of UTF-8` 亦叫rune。
* `len`返回字节数。用`utf8.RuneCountInString()`返回实际字符数。
```
s:="\u2318"
len([]byte(s)) // 3 bytes
len([]int32(s)) // 1 rune == utf8.RuneCountInString()
len([]int8(s)) // compile error
```
* Raw:
```
`\n\.abc\t\` 相当于 "\\n\\.abc\\t\\"
```
* 底层结构`reflect.StringHeader`
```
type StringHeader struct {
Data uintptr
Len int
}
```
指针:
* 默认值`nil`
* 有指针(`*Type`)及指针的指针(`**Type`),但没有指针算术。
* 用`&`取得变量的指针,一律用`.`访问目标。
* 函数中可以返回局部变量的指针,因为局部变量不一定分配在栈上。
对象的地址可能变化,对应指针类型变量会同步更新,但从其它非指针类型的值生成的指针不行:
```
func main() {
var x int = 10
var p uintptr = uintptr(unsafe.Pointer(&x))
runtime.GC()
var px *int = (*int)(unsafe.Pointer(p))
println(*px)
}
```
类型转换
* 转换只适用于几种简单的情况:转换整数(int)到去其他的精度和大小,整数(int)与浮点数(float)的转换,底层一样但类型名不一样之间的转换
* 没有隐式转换
* 显式转换T(v):
* 转换会不会造成复制取决于转换的类型
```
uint8(int_var) // truncate to size
int(float_var) // truncate fraction
float64(int_var) // convert to float
string(0x1234) // == "\u1234"
string(array_of_bytes) // bytes -> bytes
string(array_of_ints) // ints -> Unicode/UTF-8
[]byte("hello") // string -> array of byte
```
操作符优先级:`* / % << >> & &^(位清除)^(取反)`,`+ - | ^(异或)`,`== != < <= > >=`,`<-(通信)`,`&&`,`||`。
Bit Hacking:
```
& AND
| OR
^ XOR(不同为1,相同为0)/NOT
&^ AND NOT (清除位)
<< left shift
>> right shift (算术移位,对于有符号数,保留符号位)
```
## 变量
var(变量): 必须有类型和初始化表达式之一。
未初始化的变量自动为0, 指针和引用为nil。
不用的变量可以使用`_`。
`:=`缩写声明并初始化变量,只可以在函数中使用。注意会隐藏上一级 scope 的变量,函数参数和返回值变量与函数体同一 scope。
未被使用的局部非常变量或导入的包被认为是个错误。
多重赋值的时候,先计算等号右边的表达式,再进行赋值(都是从左到右的顺序)。多重赋值可以用于函数调用时实参传给形参。
```
var i int
var j = 365.245 // 自动推导类型
var a, b int = 1, 2 // a,b都是int
v := value // 省略var的简写
a, b, c, d := 1, 2.0, "three", FOUR
// 初始化表
var (
i int;
j = 356.245; // 分号可省
k int = 0;
l, m uint64 = 1, 2;
billion int64 = 1e9;
inter, floater, stringer = 1, 2.0, "hi"
)
```
赋值,可以多个变量一起
```
a = b
x, y, z = f1(), f2(), f3()
a, b = b, a
nbytes, error := Write(buf)
```
`new` 是一个函数,分配了一个零初始化的类型值,返回指向它的指针,没有delete。不一定分配在堆上。
```
var p *Point = new(Point) // 或 &Point{}
v := new(int) // v has type *int
```
`make`
* 对于引用语义的复杂类型(slice, map, channel)用make来初始化引用(分配内存和初始化结构)。引用类型的赋值可以理解成这个类型头结构的值传递赋值。
* 未初始化的引用为nil, 如: `var ar []int`, `var m map[string]int`, `var chan int`
* 字面定义`ar := []int {}` 和 `ar := make([]int)`
* 字面定义`m := map[string]int {}` 和 `m := make(map[string]int)`
`++`/`--` 是语句不是表达式,只能后置使用。
模拟三元表达式:
```
func If(condition bool, trueVal, falseVal interface{}) interface{} {
if condition {
return trueVal
}
return falseVal
}
x, y := 1, 2
max := If(x > y, x, y).(int)
```
### predeclared type
#### defined type
`type`: 给一个匿名类型(一般是复合类型,如`[]int`, `[]MyInt`, `map[int]int`, `struct {}`, `func() {}`)起个名字(命名!简单区别:命名类型只含有标识没有其它符号,包括内置基本类型)或一个命名类型起个别名,类似C里的typedef,但不完全一样:
* 如果两个类型的变量都是命名类型,即使底层一样也不能通用(可以强转)。
* 如果两个类型的变量其中有一个是匿名类型,如果底层一样则可以通用。
```
type Point struct {
x, y, z float32;
name string
}
type Operator func(a, b int) int
type ArrayOfIntPointers []*int
```
当把一个(非interface)的类型定义为一个新的类型时,可以在新的类型上增加方法,但新的类型不会继承现有类型的方法,而interface类型和组合成员的方法集会保留。
```
type C struct {
}
func (t *C) foo() { println("foo in C") }
type A struct {
C
}
func (t *A) foo() { println("foo in A") }
type B A
func main() {
b := new(B)
b.C = C{}
b.foo() // foo in C
}
```
对于以上可以用匿名内嵌的方式:
```
type B struct {
A
}
```
#### alias type
语法:`type T1 = T2`
* 没有定义新类型,可以理解成字符替换,使用时不需要类型转换。
* method set 相同,在alias type上定义新方法也会反映到原类型上(如果是非本地类型则无法添加新方法)。
* 可以通过`type Foo = foo`将package内的非导出类型foo导出为Foo。
内置:
byte: alias for uint8
rune: alias for int32
## [常量](https://blog.golang.org/constants)
必须有编译期确定初始化表达式,只可以是数字,字符,布尔类型,类型说明可省(untyped)。
untyped: 没有指定类型或字面常量,可以赋值给相应适合的类型而不用强转(在类型范围之内,类似隐式转换),遇到`:=`, `interface{}`则使用默认类型。
* 默认类型:整形数值int, 浮点数值float64, 字符rune (int32), 复数complex128。
* 可以超过uint64的限制表示溢出值`const N = 0xFFFFFFFFFFFFFFFFF`,不能赋值但可参与表达式运算。
* 表示高精度值`const PI = 3.14159265358979323846264338327950288419716939937510582097494459`,赋值时丢失精度。
```
const (
a = "xyz"
b // 同上
)
// 使用iota自增长counter
const (
loc0, bit0 uint32 = iota, 1< y ? x : y`。没有逗号操作符。
```
if v := f(); v < 10 {
fmt.Printf("%d less than 10\n", v)
} else {
fmt.Printf("%d not less than 10", v)
}
```
`for`: 表示循环(没有while)
```
for i := 0; i < 10; i++ { ... }
for i < 10 { ... } // while
for { fmt.Printf("Mine! ") } // endless loop
for key, value := range m { // m is map, 也可只取key,对于array和slice, 返回index及value,key&value每次迭代都会被重用
fmt.Printf("key %s, value %g\n", key, value)
}
```
`range`
range表达式只会在开始前计算一次。
会复制一份对象出来迭代,循环里引用的也是复制过的item,尽量使用引用或指针类型。
range迭代数组结束时,index不会递增到下一个。
迭代值的重用:注意 [range和goroutine结合使用的问题](http://stackoverflow.com/questions/30577212/go-for-range-slice-and-goroutine-method-invocation-the-logic-behind), [common mistakes](https://github.com/golang/go/wiki/CommonMistakes)。[FIX](https://go.dev/wiki/LoopvarExperiment): [1.22](https://go.dev/blog/loopvar-preview) 之后不再重用。
```
list := []int{1, 2, 3}
var ret []*int
for _,v := range list {
ret = append(ret, &v)
}
// 结果:ret里三个地址一样
```
```
s := "[\u00ff\u754c]";
for i, c := range s { // 对于string,按照rune类型迭代
fmt.Printf("%d:%c ", i, c) // 打印 0:[ 1:ÿ 3:界 6:]
}
for range s { } // iterate only
a := []int{1, 2, 3}
for _, v := range a {
v = 10
a = append(a, v)
}
// [1 2 3 10 10 10]
```
`switch`: 任意类型,除非以`fallthrough`语句结束并进入下一个分支,否则分支会自动终止,一个case可以带多个值
```
a, b := x[i], y[j];
switch { // true
case a < b: return -1
case a == b: return 0
case a > b: return 1
}
```
`break`(可用于for/switch/select), `continue`(可用于for): 后面可以接一个标识循环(for)的label,表示这个continue/break是针对这个循环的(一般是内层循环使用,label标识外层循环,label为函数scope)。
label也可以在`goto`中使用。
## 复杂类型
### Array
声明: `var ar [3]int`, 长度: `len(ar)`,长度是类型的一部分。
按值传递,但可以用指针避免复制(更多的是使用slice)。
初始化
```
[3]int { 1, 2, 3 }
[10]int { 1, 2, 3 } // 其它为0
[...]int { 1, 2, 3 } // 自己计算长度
[10]int { 2:1, 3:1, 5:1, 7:1 } // 个别赋值
[...][2]int {{1,2}, {3,4}, {5,6}} // 2维数组
```
### [Slice](http://blog.golang.org/go-slices-usage-and-internals)
Slice是一个引用类型,是对一个Array的部分引用,长度不是类型的一部分。只要有slice在引用着,底层Array就不会被GC掉。
声明: `var a []int`, 不指定长度。
`len()`可以取得长度,而`cap()`则返回slice的容量(默认一直至被引用数组结尾的大小)。基于slice创建的slice不能超过cap大小。
string也可以被slice。不可修改。
底层结构`reflect.SliceHeader`, slice 值传递实际上是 header。
```
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
```
初始化,赋值
```
a = ar[7:9] // 长度为2, [7,9)
a2 = ar[7:9:10] // 指定容量为3, [7,10),下标都是指索引值,如果不指定默认容量和ar相同
a3 = &ar // same as a = ar[0:len(ar)] or ar[:]
header, data := buf[0:n], buf[n:len(buf)] // split
var slice = []int{ 1,2,3,4,5 } // 字面定义,可以个别赋值,自动创建底层数组
var s100 = make([]int, 100) // slice: 100 ints, cap = len
var sl = make([]int, 0, 100) // len 0, cap 100
s1 = nil // clear
data := [][]int {[]int{1,2}, []int{3,4}} // slice of slice
slice = slice[5:10] // reslice, reuse name
```
`append(slice, v1, v2, ...)`: 在slice(可以是nil)结尾添加数据,返回新slice。如果未超出cap限制,会修改原数组,否则会复制并重新分配底层数组,且cap变为原来的2倍。
扩容策略:
* 如果需求容量大于当前容量的两倍,则用需求容量
* 如果当前 slice 的长度< 1024 则将容量翻倍
* 如果当前 slice 的长度>= 1024 则每次增加 25% 的容量,直到新容量大于需求容量
`copy(dest, src)`: 复制一个slice到另一个,直到其中一个长度的结尾,返回copy的个数,可正确处理重叠部分。复制到一个数组:`copy(array[:], src)`
> a slice argument can be modified by a function, but its header cannot
```
a := make([]int, 0, 3)
b := a[:1]
b = append(b, 1)
fmt.Println(a) // []
a := make([]int, 3, 3)
b := a[:1]
b = append(b, 1)
fmt.Println(a) // [0, 1, 0]
a := make([]int, 0, 3)
b := a
b = append(b, 1)
_ = b
fmt.Println(a) // []
```
以上 a 和 b 的底层都引用了同样的数组,但 a 的 len 没有被修改。如果要在函数里修改传入的slice,一般用返回值返回。
filtering without allocating:
```
b := a[:0]
for _, x := range a {
if f(x) {
b = append(b, x)
}
}
```
更多:[Slice详解](http://blog.golang.org/slices), [SliceTricks](https://github.com/golang/go/wiki/SliceTricks)
### [Map](https://blog.golang.org/go-maps-in-action)
Map是哈希表(key可以为任何定义了等于操作符的类型),引用类型。
声明: `var m map[string]float32 // key:string, value:float32`, 长度: `len(m)`返回key的个数,cap不可作用于map。(set: `var s map[string]struct{}`)
底层结构 `runtime.hmap` struct,map 值传递实际上是 *hmap。
初始化,赋值
```
m = map[string]float32 { "1":1, "pi":3.1415 } // 字面定义
m = map[Point]string {{1,2} : "foo", {5,6} : "bar"}
m = make(map[string]float32, 500) // len为0,500类似cap
var m1 map[string]float32 = m // m1 and m now refer to same map
```
访问元素
```
one := m["1"] // 取值,会复制一份出来
null := m["not present"] // 0,如果没有key则取出空值(即使是nil map也可以,但nil map在未make之前不能直接赋值),不会报错,注意:如果是slice越界会panic
m["2"] = 2
v, ok := m[x] // the "comma ok" form, 测试是否有值
m[x].name = "abc" // 如果没有值则会空指针panic(值必须是指针类型,如果是struct则会因为无法取地址而编译不通过)
delete(m, x) // deletes entry for x,若不存在不会出错
for key, value := range m { ... } // 迭代,顺序不固定,其中可以有删除操作,删除的元素不会被后续迭代到
```
迭代 map 时,对其作修改:
* 已经迭代过的元素,可以安全删除
* 还未迭代到的元素,如果被删了,就不会被迭代到
* 迭代的时候新加一个元素,可能被迭代到,或者被跳过
如果需要有序遍历 map,则要维护一个有序的 key slice,然后用其来迭代。
### Struct
Struct是值类型,声明: `var p struct { x, y float32 }`
可用`_`表示占位字段。成员可以是自身类型的指针。
`var pp *Point = new(Point); pp.x = 1; pp.y = 2 // 没有"->",统一用"."访问成员,或(*pp).x`
初始化,赋值:
```
type Point struct { x, y float32 };
p = Point{ 7.2, 8.4 } // 顺序初始化必须包括所有成员,或者都不提供(空初始化)
p = Point{ y:8.4, x:7.2 }
pp := &Point{ 23.4, -1 } // idiomatic
```
可以为字段定义一个字面字符串标签(struct tag, 属于类型的一部分),用于反射读取。`var x struct {pos int "position"}`
如果一个struct成员开头大写,则表示对包外可见。
匿名域:如果一个struct包含一个没有命名的变量,则用它的类型名字命名(同时用匿名类型及其指针类型会同名)。`type B struct {string}; fmt.Println(b.string);` `type B struct {*string}; fmt.Println(b.string); // 注意panic, b.string是*string类型`
如果成员是个匿名struct或其指针,则还可以平展开,就像自己有这样的成员(该类型的所有方法会变成外部类型的方法,但被调用时其接收的参数仍是内部类型),但外面的名字会具有高优先级。同级不能有相同名字的成员。(类似继承)
```
type B struct{ a, b int }
type C struct{ s string }
type D struct {
B
b float64
*C
}
var d = D{B{1, 2}, 3, &C{"xyz"}}
fmt.Println(d.a) // 1
fmt.Println(d.b) // 3
fmt.Println(d.B) // {1,2}
fmt.Println(d.B.b) // 2
fmt.Println(d.C) // &{xyz}
fmt.Println(d.s) // xyz
```
同一包内的struct可以使用另一个包内struct的域及方法(包内全可见)。
转型:具有相同成员组成(成员数量和顺序相同,成员名字和类型都相同(同一个包里,不能是不同包里的同样类型),tag可以不同)的两个struct类型可以互相转换。
### 比较
除基本内置类型外只有Struct, Array可以进行`==`, `!=`比较,逐个元素比较,成员必须也可以比较。
Slice, Map, Function都只可以和nil比较。可以用`reflect.DeepEqual()`函数来比较。
Interface的等同比较包括(动态)类型和值都要相同。
```
type key int
func equal(a, b interface{}) bool {
return a == b
}
equal(1, key(1))) // false
```
## 函数及接口
### function
first class, 定义:
```
func MySqrt(f float32) (v float32, ok bool) {
if f >= 0 { v,ok = math.Sqrt(f),true }
return // implicit
}
```
参数和返回值都是以传值的方式。
可以指定每个参数的名字或者不指定任何参数名字,不能只指定部分函数参数的名字。
如果是相邻的参数是相同的类型,也可以统一指定类型。
如果命名了返回值参数(局部变量),return可以不带参数返回当前值(不被其他同名变量遮盖(shadowed)的情况下)。
不支持默认参数。
多返回值可以直接作为函数调用的实参。
命名返回参数和传入参数不同,可以直接指针赋值:
```
func foo(in *int) (out *int) {
i := 100
out = &i
in = &i
return
}
i := 10
n := foo(&i)
fmt.Println(i,*n) // 10, 100
```
闭包:
```
func adder() (func(int) int) {
var x int
return func(delta int) int { // lambda
x += delta
return x
}
}
var f = adder();
fmt.Print(f(1));
fmt.Print(f(20));
fmt.Print(f(300));
```
闭包以引用的方式使用外部变量。
可变参数(variadic parameters):只能有一个,必须放在所有参数最后。类似slice的处理。
```
func foo(x int, y ...int) { // like y []int
for i, v := range y {
x += v
}
}
a := []int {1, 2, 3, 4}
foo(100, a...) // 展开slice
foo(100, 101, a...) // 参数数目不对,compile error
foo(100, append([]int{101}, a...)...) // 复制了a的元素,效率低
```
### method
可以把方法附加在本包内的**任意非指针或接口类型**上(不仅仅是struct,对内置类型要type改名一下),只要指明接收者即可(可以是值或指针类型)。编译时静态绑定。
无论接收者是值类型还是指针类型的方法,用值或指针实例都可以进行调用(编译器会转成接收者对应的类型),但只有用指针接收者的形式才能避免值拷贝传递以及做修改(类似函数参数)。
用值调用指针接收方法时,值要可以取址。有着非指针类型struct元素值的Map如`m[1]`不可取址(slice却可以),也不能直接赋值`m[1].x=2`,必须使用临时变量。
只有struct没有class,method是一种特殊的函数,不支持重载,接收者(相当于this)是方法签名的一部分。
Accessor方法命名惯例直接用属性名表示get,“set属性名”表示set。
```
type Point struct { x, y float64 }
func (p Point) Abs() float64 {
return math.Sqrt(p.x*p.x + p.y*p.y)
}
func (p *Point) Swap() {
p.x, p.y = p.y, p.x
}
p := &Point{3, 4};
fmt.Print(p.Abs()); // will print 5
p2 := Point{5,6}
p2.Swap()
fmt.Print(p2); // will print {6,5}
```
类型和它的方法必须在同一包内。
当一个匿名类型嵌入一个struct,它的所有方法也被“继承”。同名方法可以被此struct重载。
```
type Mutex struct { ... }
func (m *Mutex) Lock() { ... }
type Buffer struct {
data [100]byte
Mutex // need not be first in Buffer
}
var buf = new(Buffer);
buf.Lock(); // == buf.Mutex.Lock()
```
如果为一个类型定义`String()`方法,则可以直接被`fmt.Print[ln]`打印使用。
method value & method expression:可以把method像函数一样赋值给其他变量
```
mv := p.Abs // method value
mv() // 已绑定实例
me := Point.Abs // method exp: func(p Point) float64
me(p)
sv := p.Swap
se := (*Point).Swap // func(p *Point)
se(&p)
```
### interface
> [Accept interfaces, return structs.](https://blog.chewxy.com/2018/03/18/golang-interfaces/)
一个interface是许多method的集合。运行时绑定。接口命名惯以er为后缀,只有签名没有实现,没有数据字段,可以包含其他接口。
```
// interface{}
type eface struct {
_type *_type
data unsafe.Pointer
}
// interface{ ... }
type iface struct {
tab *itab
data unsafe.Pointer
}
```
interface变量仅在实际类型和值都为nil时才为nil: `val==nil`。如果只是值为nil,那么也会打印出``。
```
func foo() *int {
return nil
}
var a interface{} = foo()
if v, ok := a.(*int); ok {
if v == nil {
fmt.Printf("%v, %v, %v", nil, a, a == nil)
}
}
// output: , , false
type causer interface {
Cause() error
}
var b causer
b == nil // true
```
如果你的类型实现了一个和众所周知的类型具有相同含义的方法,那么就使用相同的名字和签名;如Read,Write,Close,Flush,String等。
定义:
```
type EmptyInterface interface { }
type AbsInterface interface {
Abs() float32 // receiver is implied
}
```
任何实现了其接口的类型的变量都可以赋值给interface变量(隐式接口,无需声明,静态检查),它存储了receiver的值(只读复制品,如果要修改成员变量必须传递指针类型)以及一张method表。
每个类型都有与之关联的方法集,这个集合决定了实现了接口的哪些方法。
* 类型`T`包含全部接收者类型为`T`的方法。
* 类型`*T`包含全部接收者类型为`T`和`*T`的方法。
* 如类型`S`包含匿名字段`T`,则包含`T`的方法。
* 如类型`S`包含匿名字段`*T`,则包含`T`和`*T`的方法。
* 不管包含`T`还是`*T`,`*S`总是包含`T`和`*T`的方法。
```
type Vector struct {
a []EmptyInterface // 可以放任意类型的vector
}
```
将接口类型转回原类型:`interface_value.(type_to_extract)`,在运行期检测(使用comma,ok或switch检测,否则会panic,相比普通类型转换在编译期静态检查)。
接口之间也可以互相转换,取决于底层所包含的类型能否转换,超集可以转换为子集。
```
// 检查是否实现了String接口
type Stringer interface { String() string }
if sv, ok := v.(Stringer); ok {
fmt.Printf("implements String(): %s\n", sv.String()); // note: sv not v
}
// 习惯在switch里重用一个名字,实际上是在每个case里声名一个新的变量,其具有相同的名字,但是不同的类型。
switch v := v.(type) {
case Stringer:
...
}
```
编译期检查类型的接口实现:`var _ Stringer = (*MyType)(nil)`
Print等利用反射机制("reflect" package)知道其参数类型,也可以用`%v`表示任意类型格式。
接口一般只针对method,如果一般函数要实现接口,则在函数类型上附加方法:
```
type ToString func() string
func (this ToString) String() string {
return this()
}
var s Stringer = ToString(func() string { return "test" })
s.String()
```
接口嵌入:一个接口可以嵌入其它接口,即使这些接口中的方法有交集,结果为这些嵌入接口的并集。有交集的接口不能嵌入 struct,因为调用时有歧义。
# Concurrency
> [Do not communicate by sharing memory; instead, share memory by communicating.](http://blog.golang.org/share-memory-by-communicating)
并发模型:[CSP模型(Communicating Sequential Processes)](https://en.wikipedia.org/wiki/Communicating_sequential_processes)
## [内存模型](https://golang.org/ref/mem)
发生顺序(逻辑顺序):如果事件e1在e2之前发生,那么e2就在e1之后发生;如果e1既不在e2之前发生,也不在e2之后发生,则e1和e2并发。
在单独的goroutine的内部,发生顺序由程序指定。
多个goroutine情况下,必需使用同步机制保证不同goroutine变量之间的读写发生顺序。
多个goroutine下而没有同步机制保证,则为并发顺序。
一个对变量v的读操作r,
允许读到(可能读到)对其的写操作w的值:r不在w之前发生,且没有其它的写操作在w之后在r之前发生。
保证读到对其的写操作w的值:w在r之前发生,其它写操作要么在w之前发生,要么在r之后发生。(更严格)
在一个goroutine内部,因为没有并发,观察到和能够读到是等价的。
变量初始化为0值的操作等价于写操作。
读写大于一个机器字长的变量的操作顺序是未定义的。
注意:r能读到与其并发的w,并不代表在r之后发生的r'能读到在w之前发生的w'。
问题:双重检测锁问题,重复检测问题,解决:使用显式的同步机制。
* 语句可能乱序,造成另一个goroutine逻辑判断错误
* 语句直接被优化掉,对另一个goroutine不可见
同步机制:
程序初始化:程序的初始化都在一个goroutine内(但可能创建多个goroutine导致并发);被导入的包init在本包init之前发生;main函数在所有init函数完成之后发生。
goroutine:go语句(执行完成)在goroutine执行之前发生;goroutine的退出不保证在任何事件之前发生。
channel: 一次send操作在对应的receive操作完成之前发生;关闭操作在一次接收0值的操作之前发生;一次在无buffer的channel上的接收操作在对应的发送完成之前发生。第k次在cap为C的channel上的接收操作在第k+C次发送完成之前发生(用来限定并发数)。
lock: 对任何Mutex/RWMutex变量l,以及n any' .`
__comparable__
```
// comparable is an interface that is implemented by all comparable types
// (booleans, numbers, strings, pointers, channels, interfaces,
// arrays of comparable types, structs whose fields are all comparable types).
// The comparable interface may only be used as a type parameter constraint,
// not as the type of a variable.
type comparable interface { comparable }
```
## Misc
https://go.dev/blog/deconstructing-type-parameters
https://go.dev/blog/type-inference
泛形函数赋值,类型自动推导
```
func IsZero[T comparable](a T) bool {
return a == *new(T)
}
var is0 func(int) bool = IsZero
var isEmpty func(string) bool = IsZero
var isNilStr func(*string) bool = IsZero
slices.IndexFunc([]int{1, 2, 3}, IsZero)
```
泛形接口赋值
```
type RandomElementer[T any] interface {
RandomElement() (T, bool)
}
func MustRandom[R RandomElementer[T], T any](collection R) T {
val, ok := collection.RandomElement()
if !ok {
panic("collection is empty")
}
return val
}
type MyList[T any] []T
func (l MyList[T]) RandomElement() (T, bool) { }
type IPSet map[netip.Addr]bool
func (s IPSet) RandomElement() (netip.Addr, bool) { }
randomInt := MustRandom(MyList[int]{})
randomIP := MustRandom(IPSet{})
```
# Error Handling
## [errors](http://blog.golang.org/error-handling-and-go) ([pkg](https://golang.org/pkg/errors/))
内置的error接口(还有`errors`包),当用`fmt`包的多种不同的打印函数输出一个`error`时,会自动的调用`Error`方法。
error的描述一般小写开头,最后不带句号,因为经常被嵌入到其他描述中。
```
type error interface {
Error() string
}
```
`errors.New`/`fmt.Errorf`:创建error对象。
## [panic and recover](http://blog.golang.org/defer-panic-and-recover)
`defer`
* defer后面的语句在函数return时候调用(最终返回之前,(如果有)计算return表达式之后,并可改变返回参数的值)。
* defer函数的参数值在defer的定义点确定并复制(如果需要实时的值,可以使用闭包)。
* 可以有多个defer,执行时按LIFO顺序,即使中间发生panic也都保证后续执行。
* defer性能不高,不要在循环内使用。
* 如果要在语句块中使用,可以写成lambda代替语句块。
利用defer构造析构:
```
func ctor() func() {
println("ctor")
return func() {
println("dtor")
}
}
func main() {
defer ctor()()
fmt.Println("Hello, playground")
}
// output:
// ctor
// Hello, playground
// dtor
```
`panic`: 类似抛出异常,一直向上传递,并终止程序(不能跨越goroutine的边界,只能触发当前goroutine的defer)。参数类型为`interface{}`,可接受任何参数。一些运行时错误比如数组越界,引用空指针也会引发panic。defer函数中可以再次panic。
`recover`: 终止panic展开,返回最后一个的panic的参数,只能在defer函数中**直接**调用。没有捕获到panic则返回nil。
不能被 recover 的情况:
* 并发写 map
* 标准库或 runtime 里面的 throw
* 超出最大系统线程数 `debug.SetMaxThreads`,默认1万
# Packages
> [magic is bad; global state is magic → no package level vars; no func init](https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html)
每个go的源文件第一行都必须是`package xxx`
* 可执行文件包名`package main`(在单独目录),内含一个main函数。
* 库文件包名(一般和所在目录名相同,包里的每个源文件都必须有相同的名字): `package fmt`
导入包: `import myfmt "path/to/fmt" // use the name myfmt,相对src的路径`
导入本地包:`import "./myfmt" // 仅对go run或go build main.go有效`
导入内部包(内部包只能此内部包所在目录及以下的包可用,`go run`不受此限制,也只能使用导出的结构)。`import "myfmt/internal/util"`
不导入仅初始化:`import _ "fmt"`
全部导入(无须以包名开头使用):`import . "fmt"`
导入多个包:
```
import (
"fmt"
"math"
)
```
在一个包内部,所有全局变量函数等对于包的源文件可见。对于包的使用者来说,只有开头字母大写的可见。
全局变量的初始化:声明的时候初始化;使用`init()`函数,在包所有全局变量初始化后执行`main.main()`之前执行。初始化总是单线程的,根据包的依赖顺序执行。
多个包依赖的情况:先执行被依赖的包的全局变量初始化,再执行被依赖包的init函数;执行本包的全局变量初始化,再执行本包的init函数;执行main的全局变量初始化,再执行main的init函数。
导入一个包时,这个包内除了`*_test.go`每个源文件的`init()`都会被调用,不管有没有用到里面的变量或函数。
一个包在被不同的包导入后,里面的全局变量只有一个实例, `init()`也只会运行一次。
一般来说,一个目录一个包,作为一个单元一起编译,忽略测试文件。
按照惯例,包名使用小写,且为一个单词的名字;不要使用下划线或者混合大小写。
包并没有层级之分,父目录的包可以导入子目录的包,反之亦可,只要没有循环依赖,例如 [img](https://github.com/golang/go/tree/master/src/image)。
## Vendoring ([cmd](https://github.com/golang/dep))
在src下如果有形如`d/vendor`的目录(一般vendor不直接放在src下),那么在`d`及其子目录下面build的时候import路径也会在`d/vendor`下面查找。如果有同名冲突,则选择路径长的。
`./...`不会匹配vendor目录。
## [Modules](https://blog.golang.org/using-go-modules) ([zh](https://www.4async.com/2019/03/2019-03-20-using-go-modules/) [doc](https://golang.org/doc/#developing-modules))
https://go.dev/ref/mod
https://github.com/golang/go/wiki/Modules
`GO111MODULE=on` 默认auto自动检测是否启用。
module: 带版本的包的集合。版本符合 [semver](https://semver.org/) 形式:`v(major).(minor).(patch)`。如果没有 semver,会使用伪版本。
module 本身不再依赖`GOPATH`,不再需要 `bin`, `src`, `pkg` 目录。依赖的缓存会下载到`GOMODCACHE`定义的路径(默认`~/go/pkg/mod`),`go install`会安装到`~/go/bin`(如果 GOPATH 没有定义)。
module 可以有子 module (go.mod 定义)。
### go.mod
module 定义文件,位于 module 根目录。
```
module github.com/my/module/v3
go 1.20
require (
github.com/some/dependency v1.2.3
github.com/another/dependency v0.1.0
github.com/additional/dependency/v4 v4.0.0
)
exclude (
)
replace (
)
```
module: mod-patch,module 路径
require: 依赖包
exclude: 只作用于当前 module
replace: 只作用于当前 module
当一个 module 依赖 package A 时,它就直接依赖 A 所在的 module B,间接依赖 A 所依赖的其他 module。但不依赖 A 中 test 所依赖的 module,也不依赖 module B 其他未使用 package 所依赖的 module。go.mod 文件中就是所有直接依赖和间接依赖的 module 所有列表。
### command
相关命令从当前目录开始向上找`go.mod`,第一个找到的 module 作为 main module(命令运行目录)。
`go mod init `: 初始化,mod-path不指定会自动检测
`go mod vendor` 可以把依赖从缓存 copy 到 module 的 vendor 目录下。
`go mod graph` 显示模块依赖图。
`go mod why -m` 显示模块依赖顺序。
`go build -mod=mod` 自动分析下载依赖并更新`go.mod`(require),然后编译。
`go list -m -json all` 显示 build list,编译依赖的所有 module 及实际使用的版本。
`go list -m -versions github.com/pkg/errors` 显示指定包的所有版本。
`go clean -modcache` && `go mod tidy` 重新更新缓存。
以上命令会显示完整的依赖 module(go.sum 也会包括test依赖,用于 MVS 的版本等),使用 `go tool nm` / `go version -m` 查找实际编译到 binray 的 module (对应 go.mod 依赖)。
生成调用依赖:`go build -ldflags='-dumpdep' -gcflags='-N -l'`
[使用 go list 查找](https://stackoverflow.com/questions/64371466/with-go-list-how-to-list-only-go-modules-used-in-the-binary)
```
go list -deps -f '{{define "M"}}{{.Path}}@{{.Version}}{{end}}{{with .Module}}{{if not .Main}}{{if .Replace}}{{template "M" .Replace}}{{else}}{{template "M" .}}{{end}}{{end}}{{end}}' | sort -u
go list -deps -f '{{define "mod"}}{{.Path}}@{{.Version}}{{end}}{{if .Module}}{{if not .Module.Main}}{{if .Module.Replace}}{{template "mod" .Module.Replace}}{{else}}{{template "mod" .Module}}{{end}}{{"\t"}}{{.ImportPath}}{{end}}{{end}}' | sort
go list -deps -json | jq -r 'select(.Module and (.Module.Main | not)) | .Module.Path + "@" + .Module.Version + "\t" + .ImportPath' | sort
```
https://github.com/golang/go/issues/27900
### 依赖管理
`go get -u` to use the latest minor or patch releases
`go get -u=patch` to use the latest patch releases
`go get` 默认获取有 `semver` tag 的最新版本。要指定版本,可以用`@version`后缀或"module query"。
### 语义化版本导入
规则:
* tag 符合 semver。
* 如果 tag 主版本是 v2 或更高,mod-path 和 import path 都必须包含主版本(后缀)。如果是 v0 或 v1 则不需要包含。
用不同 branch 区分不同主版本进行开发。
主版本不同,则认为是不同的 module。
### [MVS (Minimal Version Selection)](https://www.ardanlabs.com/blog/2019/12/modules-03-minimal-version-selection.html) ([zh](https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/))
当多个 module 依赖同一个(相同主版本的) target module 时,选择(满足多个 module 需求的 target module 的)最小而非最大版本。
基于 MVS 升级:`go get -t -d -v ./...`
最新最大版本升级:`go get -u -t -d -v ./...` (会在 go.mod 中增加对最大版本的显式依赖,无论有没有代码使用)
重置依赖:删除 go.mod/go.sum,重新 init。
没有 go.mod 的 legacy go 包(版本有 incompatible 后缀)不适用此规则(认为是同一个module,无视大版本,选择同时满足的最小版本)。
## [Workspace](https://go.dev/blog/get-familiar-with-workspaces) ([zh](https://mp.weixin.qq.com/s/QSWf3QKYsVDkPufKB9gzRQ))
用工作区目录根部的 go.work 文件来控制所有的依赖关系,使多个 module 可以优先本地依赖,而不用 module replace 到本地路径(也不用显式 require 依赖)。
可以在 go.work 里面统一 replace 其它非本地的 module。
模块感知的构建命令和一些 go mod 子命令会检查 GOWORK 环境变量,以确定它们是否处于工作区环境中。
go work init [moddir] ... 初始化并生成 go.work 文件
go work use [moddir] 添加 module
go work sync 将 go.work 文件中的依赖关系同步到每个工作区模块的 go.mod 文件中
## Plugin
必须是main包(可以没有main函数),用`-buildmode=plugin`或`-buildmode=c-shared`编译,生成动态库文件。
导出的包函数和变量成为动态库符号。
调用(使用plugin库):
1. `p = plugin.Open(mod)` 打开动态库文件
1. `s = p.Lookup("Type")` 查找符号,返回`Symbol`(`interface{}`)
1. `s.(Type)` 转成实际类型使用
plugin可以导出任何类型的包函数和变量,但一般导出接口类型,这样可以使用接口的全部方法集。
# Testing ([pkg](http://golang.org/pkg/testing/))
## Test
* 命令: `go test`, 运行所有测试文件。`go help testflag`查看测试相关参数。`-run regexp` 运行指定测试函数。
* 文件名: `*_test.go`,里面定义的变量在非测试时不可用。
* 用例函数签名: `func TestXxxx(t *testing.T)`,如果调用了`t.Error`, `t.Fail`则测试失败。
* 测试工具包: `import "testing"`, 支持logging, error reporting...
* 重写`TestMain`以进行初始化/清理工作。
```
func TestMain(m *testing.M) {
// do setup
code := m.Run()
// do teardown
os.Exit(code)
}
```
* subtests: 可以创建子测试用例,subbench类似
```
func TestFoo(t *testing.T) {
// do setup
t.Run("sub1", func(t *testing.T) {
// do test
})
// do teardown
}
```
[Go advanced testing tips & tricks](https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859#.smbsuk2mg)
## Benchmark
`go test -bench=. -benchtime=3s -benchmem`
`-cpu=1,2,4` 分别指定`GOMAXPROCS`测试。
`-benchtime=1000x` 指定迭代次数。
测试函数必须形如:`b.N`是本轮测试循环次数,会根据每个测试的要求时间自动调整(默认1s)。
```
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
```
用`b.ResetTimer()`重新计时以忽略准备过程。
输出如`50000000 24.1 ns/op 8 B/op 1 allocs/op` 表示跑了50000000次循环,每次循环用时24.1 ns, 分配了8 Byte堆内存,有1次堆内存分配。
用 [benchstat](https://godoc.org/golang.org/x/perf/cmd/benchstat) 比较结果。
## Fuzzing ([doc](https://go.dev/doc/fuzz/) ([zh](https://mp.weixin.qq.com/s/l1jv6G9S_8LrOh94fSHHqw)))
`go test -fuzz={FuzzTestName} -fuzztime=10s`
`-fuzztime` 指定运行迭代时间或次数,默认无限
测试函数必须形如:
```
func FuzzFoo(f *testing.F) {
f.Add(5, "hello") // 可选手动添加case (seed corpus) 作为基础输入
f.Fuzz(func(t *testing.T, i int, s string) {
out, e := Foo(i, s)
if e != nil && out != "" {
t.Errorf("%q, %v", out, e)
}
})
}
```
fuzzing 参数必须是以下类型:string, []byte, int, int8, int16, int32/rune, int64, uint, uint8/byte, uint16, uint32, uint64, float32, float64, bool
每次 fuzz testing 都会先执行 seed corpus 和 generated corpus。
失败的 case 都作为 seed corpus 记录在 `testdata/fuzz/{FuzzTestName}` 目录,以后每次 `go test` 都会自动加载执行(作为regression)。
`fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)`
execs: 执行的case/input数目
interesting: 新加入的 generated corpus(一般是导致了覆盖率的增加), `$GOCACHE/fuzz` 记录了系统维护的 generated corpus,清除`go clean -fuzzcache`
## [Coverage](https://go.dev/blog/cover) ([cmd](http://golang.org/cmd/cover/))
单元测试:
生成:`go test -coverprofile=cover.out`
查看html:`go tool cover -html=cover.out`, `-o cover.html` 生成html
查看func:`go tool cover -func=cover.out`
[集成测试](https://go.dev/blog/integration-test-coverage) [doc](https://go.dev/doc/build-cover):
生成:`go build -cover -o myprogram`,`-coverpkg` 指定需要包含的包路径(`go list -m`)
运行:`GOCOVERDIR=mydata ./myprogram` 可以运行多次(产生多个 covcounters 文件)
查看:`go tool covdata percent -i=mydata`
转成 profile 格式:`go tool covdata textfmt -i=mydata -o cover.out`
查看html:`go tool cover -html=cover.out`
## [Examples](https://blog.golang.org/examples)
示例函数命名(suffix可选):
* package: Example_suffix
* func: ExampleFuncA_suffix
* type: ExampleTypeA_suffix
* method: ExampleTypeA_MethodA_suffix
examples在go doc里可以直接run。
examples也是测试,通过stdout输出和`Output:`注释比较来判断测试是否通过,如果没有`Output:`则不会执行。(内置函数`print/println`输出到stderr)
## util packages
`net/http/httptest`: 测试http
`testing/iotest`:模拟io读写错误等
`testing/quick`: 产生随机数做黒盒测试,[详解](http://blog.matttproud.com/2015/06/testingquick-blackbox-testing-in-go-for.html)
https://github.com/dvyukov/go-fuzz : 随机数据测试
https://github.com/stretchr/testify : 辅助测试工具
https://github.com/vektra/mockery: mock 生成工具
# Logging
## 非结构化
[Logging Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md)
## 结构化
[logr](https://github.com/go-logr/logr)
[slog](https://go.dev/blog/slog)
# Diagnostics ([doc](https://golang.org/doc/diagnostics.html))
## Debuging ([pkg](https://golang.org/pkg/runtime/debug/))
查看运行状态:`GOMAXPROCS=2 GODEBUG="schedtrace=1000, scheddetail=1" ./test.out`
查看GC情况:`GODEBUG="gctrace=1" ./test.out` 用[gcvis](https://github.com/davecheney/gcvis)可视化。
查看go相关环境变量:`go env`
查看import及依赖:`go list -json`
竞态检测:`go run/build/test -race`
产生coredump:`GOTRACEBACK=crash` (none 不显示堆栈,single 为默认值显示当前堆栈,all 显示 user 堆栈,system 显示 user+runtime 堆栈,crash 生成 coredump)
GC tune: GOGC环境变量 `goal = reachable * (1 + GOGC/100)`, 内存上限软限制 GOMEMLIMIT, [GC Guide](https://go.dev/doc/gc-guide)
Debugger: [GDB](https://golang.org/doc/gdb): v7.1+, [delve](https://github.com/derekparker/delve)
[Debugging](https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs)
* debug: 禁止内联和优化 `-gcflags "-N -l"`。(查看全部`go tool compile -help`)
* release: 删除调试信息和符号表 `-ldflags "-w -s"`。
## [Profiling](http://blog.golang.org/profiling-go-programs) ([cmd](http://golang.org/cmd/pprof/) [pkg](https://golang.org/pkg/runtime/pprof/))
测试时:`go test -bench=. -cpuprofile/-memprofile/-blockprofile out.prof`
独立程序:`pprof.StartCPUProfile()/pprof.StopCPUProfile()/pprof.WriteHeapProfile()` 或使用 [profile](https://github.com/pkg/profile) `defer profile.Start().Stop()`
查看:`go tool pprof out.prof`/`go tool pprof -http=:8080 out.prof`
[使用label](https://rakyll.org/profiler-labels/):`pprof.Do()`
独立程序嵌入http接口:`import _ "net/http/pprof"`,并运行一个server:
```
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
```
查看:`http://localhost:6060/debug/pprof/` 可以生成各种 prof, trace 文件(不加 debug 参数,并指定时长参数),查看 stack 信息等。
其他:[gops](https://github.com/google/gops) 集成诊断工具。
### Frame Graphs
安装:
go install github.com/uber/go-torch
git clone https://github.com/brendangregg/FlameGraph (add dir to PATH)
http pprof: `go-torch -t 10`
file pprof: `go-torch --binaryname binary.test -b pprof.cpu`
使用原生[pprof](http://github.com/google/pprof): `pprof -http=:8080 out.prof`
`go tool pprof -http=` 已支持直接查看,不需要 go-torch。
## Tracing
main里加上:
```
func main() {
_ = trace.Start(os.Stdout)
defer trace.Stop
...
}
```
使用 [profile](https://github.com/pkg/profile):
`defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()`
`go run main.go > trace.out`/`go test -bench=. -trace=trace.out`
`go tool trace trace.out`
使用 [gotrace](https://github.com/divan/gotrace) 3D 可视化。
## Escape Analysis
https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-stacks-and-pointers.html
https://www.ardanlabs.com/blog/2018/01/escape-analysis-flaws.html
编译时 `-gcflags -m` / `-gcflags "-m -m"` (最多4个)查看逃逸情况
常见情况:
* 函数返回内部创建变量的指针,或被超出函数 scope 的指针所引用。
* make 时不能在编译时确定大小,或者太大超出栈的限制。
* 当创建的变量赋值给一个接口,并接口调用了其方法时。
* 发送指针或带有指针的值到 channel 中。
* 在一个切片上存储指针或带指针的值,这些值分配在堆上。
* slice 的背后数组被重新分配了,因为 append 时可能会超出其容量。
如果变量分配在栈上,其指针可能随着栈的扩张或收缩而变化。
```
var gp *string
func main() {
s := "test"
p := &s
// gp = p // 如果赋值给全局变量,则 s 逃逸到堆上,地址不会变化
grow(p, 0, [1000]int{})
}
func grow(p *string, c int, a [1000]int) {
println(c, p, *p)
c++
if c == 10 {
return
}
grow(p, c, a)
}
// output:
0 0xc000097f40 test
1 0xc000097f40 test
2 0xc0000a7f40 test
3 0xc0000a7f40 test
4 0xc0000a7f40 test
5 0xc0000a7f40 test
6 0xc0000c7f40 test
7 0xc0000c7f40 test
8 0xc0000c7f40 test
9 0xc0000c7f40 test
```
# Comments
## Documentation ([doc](https://go.dev/doc/comment) [cmd](http://godoc.org/golang.org/x/tools/cmd/godoc))
规范化注释格式,任何直接属于顶层声明的注释都会可被`godoc`提取为文档注释(除了package main)。
* 要和被注释的成员相邻
* 用空行分隔段落
* 缩进表示格式化文本(代码片段)
* bug: `// BUG(author): overflow`
* 每个程序包都应该有一个包注释,一个位于package语句之前的注释(只需要出现在一个文件中)。
* 每一个被导出的(大写的)名字,都应该有一个注释。
* 注释的第一句话应该为一条概括,并且使用被声明的名字作为开头。`// FuncName does ...`
本地运行:`godoc -http=:6060 -play`
godoc 已废弃,使用 [pkgsite](golang.org/x/pkgsite/cmd/pkgsite)
## [CodingStyle](http://blog.golang.org/go-fmt-your-code) ([cmd](http://golang.org/cmd/gofmt/))
[Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
[What's in a name?](https://talks.golang.org/2014/names.slide)
[When in Go, do as Gophers do](https://talks.golang.org/2014/readability.slide)
[Effective go](https://go.dev/doc/effective_go)
[Package names](https://go.dev/blog/package-names)
[Style packages](https://rakyll.org/style-packages/)
[Pacakge names](https://blog.golang.org/package-names)
## [Code Generation](https://blog.golang.org/generate)
指令:在任意go文件中,以注释:
`//go:generate goyacc -o gopher.go -p parser gopher.y`
`//go:generate stringer -type=Pill`
调用`go generate`执行注释后的命令生成文件。
## Embed ([pkg](https://pkg.go.dev/embed))
`//go:embed`
可以嵌入文件(编译期)到包级别的导出或未导出的字符串, bytes, embed.FS
```
import _ "embed"
//go:embed version.txt
var version string
//go:embed hello.txt
var b []byte
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS
// usage
data, _ := content.ReadFile("html/index.html")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))
template.ParseFS(content, "*.tmpl")
```
嵌入FS时,可以是目录(包括子目录)。
## Build Constraints([pkg](https://golang.org/pkg/go/build/))
又称build tag或条件编译。在`go run/build/test`的时候以`-tags`指定。
指令:在源文件(不一定是go文件)中以`// +build`表示,必须在package之前并以空行隔开。
条件:`空格:或`,`逗号:与`,`!:非`,多行表示“与”。比如`// +build linux,386 darwin,!cgo`表示`(linux OR darwin) AND 386`
特殊的tag:操作系统`runtime.GOOS`,架构`runtime.GOARCH`,编译器`gc`/`gccgo`,`cgo`,版本`go1.x`,忽略`ignore`。
操作系统和架构也可以以文件名的形式说明:`*_GOOS`, `*_GOARCH`, `*_GOOS_GOARCH`,如`source_windows_amd64.go`
新语法 `//go:build`
```
BuildLine = "//go:build" Expr
Expr = OrExpr
OrExpr = AndExpr { "||" AndExpr }
AndExpr = UnaryExpr { "&&" UnaryExpr }
UnaryExpr = "!" UnaryExpr | "(" Expr ")" | tag
tag = tag_letter { tag_letter }
tag_letter = unicode_letter | unicode_digit | "_" | "."
```
## pragmas
https://dave.cheney.net/2018/01/08/gos-hidden-pragmas
`//go:noescape` 表示函数里的内存分配不会逃逸到堆上
`//go:norace`
`//go:nosplit`
`//go:noinline` 禁止内联函数
# [Std Packages](https://pkg.go.dev/std)
## errors
`errors.As(err error, target any) bool`: `target` 必须是非空指针,`*target` 必须是一个 interface 或者实现了 error 接口。返回 `*target = err` 赋值是否可行,或者 err 实现了 As(target)。
check by error pointer:
```
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.Message
}
func IsMyError(e error) bool {
me := &MyError{}
return errors.As(e, &me)
}
```
check by error concrete type:
```
type MyError struct {
Message string
}
func (e MyError) Error() string {
return e.Message
}
func IsMyError(e error) bool {
me := &MyError{}
return errors.As(e, me)
}
```
## sort
都是二分查找,要求有序:
`Search(n int, f func(int) bool) int`: f 定义了查找条件,返回满足条件的第一个索引(插入位置),不能直接判断一个元素存不存在。
`Find(n int, cmp func(int) int) (i int, found bool)`: cmp 返回一个整数,目标值小于当前元素时返回-1,大于返回1,等于返回0。返回满足`cmp(i) <= 0`的第一个索引,及存不存在标识。
如果是slice,直接用`slices.BinarySearch`或`slices.BinarySearchFunc`
## [context](http://blog.golang.org/context)
> [Pass context.Context in as an argument; don't store it in structs.](https://blog.golang.org/context-and-structs)
Context是一个请求的上下文,一般用于服务端在一系列goroutine里处理请求时带上取消通知,超时通知,相关请求参数(如ID,token等)以便主动超时或取消请求时(或发生异常时)退出所有相关goroutine释放资源。
Context本身是线程安全的,它携带的参数也必须是线程安全的。
Context可以有继承关系,当一个Context被取消,它所有的子Context都会被取消(收到通知)。子Context取消不会影响父Context。
```
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
```
`func Background() Context` 返回可以作为所有Context的父Context,它永远不会被取消。
`func TODO() Context` 返回一个待用的Context,不确定的时候用。和 Background 只是语义上不同。
`func WithCancel(parent Context) (ctx Context, cancel CancelFunc)` 返回子Context和cancel函数。当cancel被调用到时(一般在处理这个请求的handler返回时调用,并取消子Context和其子孙,删除父Context的引用,停止所有相关联的timer),ctx得到通知(`Done()返回`)。
`func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)` 当超过某段时间时,ctx得到通知。
`func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)` 当某个时间点到达时,ctx得到通知。
`func WithValue(parent Context, key interface{}, val interface{}) Context` 用于设置请求相关参数。用`ctx.Value()`取得值,如果找不到也会到父Context中查找。一般用于传递auth信息,请求ID等。
注意不同包间使用 key 时可能会冲突,一般将 key 定义为自定义类型`type ctxKey struct{}`,这样不同包的 key 类型就肯定不一样,即使值一样也代表不同的 key。
http中使用`Request.WithContext`,net中使用`Dialer.DialContext`
1.7中使用方法:https://github.com/golang/net/blob/master/context/ctxhttp/ctxhttp.go deprecates `Request.Cancel`/`Transport.CancelRequest`
## net/http
### connection
client:
* 在得到http请求的响应且没有错误的情况下(resp不为nil),在读取完body之后(或没有body),都要手动进行`resp.Body.Close()`。只有body读完并关闭之后才能重用连接。
* 如果不想http保持连接,在发送请求之前设置`req.Close = true`或者`req.Header.Add("Connection", "close")`。
* 对于请求发送的body,由client的transport负责关闭。
server:
* server的接收请求的body,由Server负责关闭,handler不必关心。
### handler
http://www.alexedwards.net/blog/a-recap-of-request-handling
* `http.ListenAndServe`:新建一个Server并`Server.ListenAndServe`没有设超时等参数,如果handler为nil则使用DefaultServerMux。一般不直接传普通handler给Server,而是传一个mux,否则要自己做路由。
* `http.Handle/HandleFunc`: 等于在`DefaultServerMux`设置。
* `http.ServerMux`: 是一个handler,处理路由。`Handle/HandleFunc`设置路由对应的handler。
* `HandlerFunc`: 将一个函数转为一个handler。(被HandleFunc调用)
```
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world!"))
})
http.ListenAndServe(":8080", nil)
```
等于下面的简写:
```
type myHandler struct {
}
func (t *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world!"))
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", &myHandler{})
s := &http.Server{
Addr: ":8080",
Handler: mux,
}
s.ListenAndServe()
}
```
### 超时
http://www.oschina.net/translate/the-complete-guide-to-golang-net-http-timeouts
* 服务端:对于不需要返回流的服务器,使用http.Server并设置ReadTimeout和WriteTimeout。(http.ListenAndServe无法设置)
* 客户端:对于 API 请求,使用 http.Client 设置超时,从发起连接到接收响应结束。对于流式请求,使用默认 transport + ResponseHeaderTimeout,从发起请求到收到 header。
```
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.ResponseHeaderTimeout = 10 * time.Second
```
使用timeout transport控制超时:
```
type TimeoutTransport struct {
rt http.RoundTripper
timeout time.Duration
}
func NewTimeoutTransport(rt http.RoundTripper, timeout time.Duration) http.RoundTripper {
transport := &TimeoutTransport{
rt: rt,
timeout: timeout,
}
if rt == nil {
transport.rt = http.DefaultTransport
}
return transport
}
func (t *TimeoutTransport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
if ctx == nil {
ctx = context.TODO()
}
toctx, _ := context.WithTimeout(ctx, t.timeout)
resp, e := t.rt.RoundTrip(req.WithContext(toctx)) // implicit dial
if e != nil {
select {
case <-toctx.Done():
e = toctx.Err()
default:
}
}
return resp, e
}
```
### [tracing](https://go.dev/blog/http-tracing)
跟踪 http client 请求过程中的事件,比如连接重用。
使用例子:https://github.com/davecheney/httpstat
## [reflection](http://blog.golang.org/laws-of-reflection)
通过reflection可以取得接口里面的类型和值的信息。
1. Reflection goes from interface value to reflection object. (接口 -> 反射对象)
2. Reflection goes from reflection object to interface value. (反射对象 -> 接口)
3. To modify a reflection object, the value must be settable. (要修改反射对象,值必须可以更改)
### 从接口得到reflection对象
`reflect.ValueOf()` 取得值对象
`reflect.TypeOf()/v.Type()` 取得类型对象
`v.Kind()/v.Type().Kind()` 取得类型的枚举值(底层类型),可以用getter/setter方法转为相应的(最大范围)类型。
### 从reflection对象到接口
`v.Interface()` 将reflect值对象转为interface。
`v.Elem()` 如果v底层是Interface或Ptr对象,取得其包含或指向的值对象。
`t.Elem()` 如果v底层是Array, Chan, Map, Ptr, 或Slice类型,取得其包含元素的类型对象。
### 更新可设置reflection对象的值
`v.CanSet()` 测试一个值是否可以设置(是否可以通过reflection对象来设置原始值,这就要求取得reflection对象时必须传递指针给ValueOf(),则`v.Elem().CanSet()`为true)。
### Struct
通过value对象的field方法取得field对象。
通过type对象的field方法取得field元信息。
struct的field可设置必须首先是导出的。
```
type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
// out:
// 0: A int = 23
// 1: B string = skidoo
```
查看字段对齐:CPU更好地访问位于2字节边界上的2个字节,位于4字节边界上的4个字节,直到CPU的位大小(64位CPU为8个字节)。
```
typ := reflect.TypeOf(Data{})
fmt.Printf("Data is %d bytes\n", typ.Size())
n := typ.NumField()
for i := 0; i < n; i++ {
field := typ.Field(i)
fmt.Printf("%s at offset %v, size=%d, align=%d\n", field.Name, field.Offset, field.Type.Size(), field.Type.Align())
}
```
### select
执行类似 select 语句的操作。
```
var cases []reflect.SelectCase
for _, c := range channels {
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(c),
})
}
index, value, ok := reflect.Select(cases) // return from channels[index]
```
## [encoding/json](https://blog.golang.org/json-and-go)
### encoding/marshal
* 对于map类型必须以string为key
* channel, complex, function类型不能被encode
* 循环数据结构不能被encode
* 指针类型会被encode成其指向的值
* 只有导出的字段会被encode,其他xml, gob编码类似
* 数据流使用`NewEncoder`
Go|Json
-|-
bool|boolean
int, float...|numbers
string|string
array,slice|array
map[string]T|object
[]byte|base64 string
struct|object
nil|null
### decoding/unmarshal
* decode匹配字段struct tag > 字段名 > 不区分大小写字段名,找不到匹配的则忽略
* 对于指针,slice,map类型若没预先分配则会自动分配内存,如果没有匹配字段则为nil
* 数据流使用`NewDecoder`
* 使用`json.RawMessage`([]byte)保留某个字段的原始json。
Json|Go
-|-
boolean|bool
number|float64
string|string
null|nil
array|[]interface{}
object|map[string]interface{}
### struct tag
`json:"-"` 忽略这个字段
`json:"myName"` 在json里key为myName
`json:"myName,omitempty"` 如果字段为空值则忽略
`json:",string"` 数值,布尔类型在json里转为string编码。如果是json里为数值,可以用`Decoder.UseNumber`decode为json.Number类型(string)从而不丢失精度,进而转为数值类型。
## text/template
[playground](https://repeatit.io/)
`template.Parse()` 可以重复执行,但会覆盖当前模版定义,而不是返回新的模版定义。
data: 模版执行当前作用域的上下文数据,叫dot用`.`表示。`{{with pipeline}}` or `{{ range pipeline}}` 限定作用域。
`{{.Count}} items are made of {{.Material}}` // "17 items are made of wool"
`{{with "output"}}{{printf "%q" .}}{{end}}` // "output"
Trim: `{{- ...`和`... -}}` 可以trim左边和右边的空白。
`{{23 -}} < {{- 45}}` // "23<45"
Action: 以`{{...}}`表示,进行求值或组成控制结构。
```
{{/* a comment */}}
{{- /* a comment with white space trimmed from preceding and following text */ -}}
{{pipeline}}
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
{{range pipeline}} T1 {{end}}
{{range pipeline}} T1 {{else}} T0 {{end}}
{{break}}
{{continue}}
{{template "name"}}
{{template "name" pipeline}}
{{block "name" pipeline}} T1 {{end}}
{{with pipeline}} T1 {{end}}
{{with pipeline}} T1 {{else}} T0 {{end}}
```
Argument: 字面常量,模版变量`$var`,data struct成员(成员必须大写导出,或map的key,或成员函数调用)`.Field`,变量成员(或map的key,或成员函数调用)`$x.Field`,函数调用`fun`。
command: 一个简单的值或一个(带有参数的)函数调用,结果是一个值或一个值带有一个错误。`Argument`, `.Method [Argument...]`, `functionName [Argument...]`。
pipeline: command序列,以`|`隔开,前一个command的结果作为后一个command的最后一个参数。或用括号优先求值。
`{{"put" | printf "%s%s" "out" | printf "%q"}}` // "output"
`{{println (len "output")}}`
Variables: `$var := pipeline`, `range $index, $element := pipeline`,变量作用域为控制结构结束`{{end}}`或到模版结束。顶级作用域变量 `$`(Execute 传入的参数)。
```
{{$l := (len "output")}}
{{println $l}} // 6
```
function: 预定义的全局函数 `and, call, html, index, slice, js, len, not, or, print, printf, println, urlquery, eq, ne, lt, le, gt, ge`。或自定义函数 `template.New("").Funcs(template.FuncMap{ ... })`。
一个列出k8s deploy cpu的例子(trim不必要的空行):
```
{{- range .items -}}
{{ $name := .metadata.name }}
{{- range .spec.template.spec.containers -}}
{{$name}} {{": "}} {{ .resources.limits.cpu }} {{"\n"}}
{{- end -}}
{{- end -}}
```
## regexp
https://golang.org/pkg/regexp/syntax/
https://github.com/StefanSchroeder/Golang-Regex-Tutorial
## sync
### Cond
条件变量:有一个关联锁,当条件改变的时候(保护条件检查相关数据),及调用 Wait 的时候必须持有这个锁。
```
type Cond struct {
// L is held while observing or changing the condition
L Locker
// contains filtered or unexported fields
}
```
Wait 自动释放锁,然后暂停此 goroutine 的执行,当被唤醒的时候,重新获得锁然后才返回。
因为在等待的时候,是不持有锁的,所有当 Wait 返回后需要重新检查条件是否满足(用循环):
```
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()
```
当调用 Signal/Broadcast 的时候,可以但并不要求持有锁。
### [扩展](https://pkg.go.dev/golang.org/x/sync)
* WaitGroup 更方便的封装版本 errgroup
* 带权重的信号量 semaphore
* 同步重复请求 singleflight
## log
`log.Fatal*()`和`log.Panic*()`会导致进程退出和panic。
## unsafe
### Pointer
`*T <--> unsafe.Pointer <--> uintptr`
`uintptr` 没有指针语义,对于 GC,无法保留引用。用 `-gcflags=all=-d=checkptr` 检查。
### String / Slice
使用 `unsafe.String`, `unsafe.StringData` 代替 `StringHeader`
使用 `unsafe.Slice`, `unsafe.SliceData` 代替 `SliceHeader`
无 copy 转换:
`mySlice := unsafe.Slice(unsafe.StringData(myString), len(myString))`
`myString := unsafe.String(unsafe.SliceData(myBytes), len(myBytes))`
# [cgo](http://blog.golang.org/c-go-cgo) ([cmd](https://golang.org/cmd/cgo/) [eg](https://golang.org/misc/cgo/))
用`CGO_ENABLED`环境变量启用/禁用调用 C 实现。默认启用时,`net`, `os/user` 包用到 libc 实现,Linux 下会导致动态链接 glibc(alpine 下链接 musl)。可以用 go 内部实现(`-tags netgo -tags osusergo`)代替(会禁用cgo),但会导致[dns 解析(nsswitch),user ID 查询](https://stackoverflow.com/a/64531740/797539)等行为和标准不一致。
[glibc](https://lwn.net/Articles/117972/) 禁止静态链接,[musl](http://dominik.honnef.co/posts/2015/06/statically_compiled_go_programs__always__even_with_cgo__using_musl/) 可以。
查看哪里用到了 cgo:
```
dep=$(go list -f "{{.ImportPath}}{{range .Deps}} {{.}}{{end}}")
go list -f "{{if .CgoFiles}}{{.ImportPath}} {{.CgoFiles}}{{end}}" $dep
```
## preamble
preamble以注释的形式在`import "C"`前面,会在编译这个包的时候一起编译。
* 直接代码。
* `#include`: 包含头文件,可以以`C.xxx`的方式使用里面的类型,函数,变量等(不论大小写,但静态变量不可引用)。(实现文件如果在包内目录会一起参与编译)
* `#cgo`: 传递CFLAGS, CPPFLAGS, CXXFLAGS和LDFLAG编译参数(`CGO_XXX`)给编译器。也可以用`#cgo pkg-config`指定CPPFLAGS和LDFLAGS参数。`${SRCDIR}`会被扩展成包源文件所在目录。
```
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #cgo pkg-config: png cairo
// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo
// #include
import "C"
func Random() int {
return int(C.random())
}
func Seed(i int) {
C.srandom(C.uint(i))
}
```
## C->Go
所有C/C++类型对应在Go里都是非导出的。
不能在Go里直接调用C函数指针。但可以存在变量里传递。
类型
Go|C/C++
-|-
`C.char, C.uint, etc.`|`char, unsigned int, etc.`
`C.size_t`|`size_t`
`unsafe.Pointer`|`void*`
`x._type` (reserved keyword)|`x.type`
`C.sturct_S`|`struct S`
`C.enum_E`|`enum E`
`C.union_U`|`union U`
`n, err := C.sqrt(-1)`|`n = sqrt(-1); err = errno`
`C.f(&C.x[0])` (array arg)|`f(x)`
`nil`|`NULL`
一些转string换函数(复制数据),注意使用完调用`C.free(unsafe.Pointer(p))`(stdlib.h)释放内存。
```
// Go string to C string
func C.CString(string) *C.char
// C string to Go string
func C.GoString(*C.char) string
// C string, length to Go string
func C.GoStringN(*C.char, C.int) string
// C pointer, length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
```
## Go->C
使用`export`导出函数。
```
// export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {...}
```
-> 在`_cgo_export.h`头文件中:
```
extern int64 MyFunction(int arg1, int arg2, GoString arg3);
```
# Reference
https://go.dev
https://golang.org/ref/spec
https://tip.golang.org/cmd/go/
https://github.com/golang/go/wiki
https://github.com/campoy/go-tooling-workshop
https://go-proverbs.github.io
https://the-zen-of-go.netlify.com/
https://google.github.io/styleguide/go/