[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<<iota; // 0, 1 loc1, bit1; // 1, 2 重复上一个表达式 loc2, bit2; // 2, 4 ) ``` 枚举(只可以此类型自己或常量匹配), 可以用 [stringer](https://pkg.go.dev/golang.org/x/tools/cmd/stringer) 生成字符串 name: ``` type Weekday int const ( Monday Weekday = iota // iota 是当前const块中常量的索引。 Tuesday Wednesday Thursday Friday Saturday Sunday ) ``` [safer enums](https://npf.io/2022/05/safer-enums/), 但不是常量: ``` type FlagID struct { name string } func (f FlagID) String() { return f.name } var ( FooBar = FlagID{ “FooBar” } FizzBuzz = FlagID{ “FizzBuzz” } ) func IsEnabled(id FlagID) bool { ``` ## 控制结构 `if`: 可以有一个简单的语句(初始化变量的作用域仅在语句和if/else内,内部是新的作用域可以定义同名变量),也可以什么也没有表示true(switch同) 其他类型不能自动转为bool,因此不能直接用于条件语句。 不支持三元操作`x > 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,那么也会打印出`<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: <nil>, <nil>, 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<m,调用n次Unlock在m次Lock之前发生;对于RWMutex变量l,RLock在n次Unlock之后发生,对应地RUnlock在n+1次Lock之前发生。 once: 一个单独的`once.Do(f)`内的`f()`调用在任何`once.Do(f)`调用返回之前发生。 ## Goroutine goroutine指在同一地址空间内并发执行的函数,非常廉价和轻量级。执行: `go f()` `go`也可以用于启动新定义的内部函数(闭包)为goroutines。 可以在不同的处理器上并发运行,而且共享内存。(gccgo目前是使用pthread实现的) goroutine的默认堆栈大小8KB,最大1GB(64bit),250M(32bit)。 goroutine是系统资源,从外部无法结束它,除非自己退出,否则会造成资源泄露(通常是一直阻塞在里面的channel上)。 ## 调度 `runtime.GOMAXPROCS(n)` 指定最多可以并行执行的goroutine数目(实际系统线程数可以大于PROC数,最大256),默认为CPU核心线程数。`runtime.NumGoroutine()`返回当前存在的goroutine的数目。`runtime.NumCPU()`返回CPU核心数目。 `runtime.Goexit()`终止当前goroutine的执行(defer仍起作用)。 `runtime.Gosched()`暂停当前goroutine,等待被再次调度(类似协程里的yield)。 goroutine如果内部调用了非内联函数(内联函数如lambda,编译运行时可以用`-gcflags -m`查看),会在函数入口采用抢占式调度。其他调度时机:在`Gosched()`,GC、go声明、阻塞channel、阻塞系统调用和lock操作后进行调度。 进程退出时并不等待goroutine结束,可以用`sync.WaitGroup`等待。 关于goroutine的调度,可以看[go-scheduler](http://morsmachine.dk/go-scheduler) ([中文](http://mikespook.com/2013/07/%E7%BF%BB%E8%AF%91go-%E7%9A%84%E8%B0%83%E5%BA%A6%E5%99%A8/)), [netpoller](http://morsmachine.dk/netpoller),以及[原始设计文档](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw)。 ### G-P-M 模型 G: Goroutine P: 逻辑处理单元(GOMAXPROCS个) M: 物理处理单元(系统线程) ## Channel 声明: `chan element_type`,创建: `var c = make(chan int)` channel是引用类型,内部同步,并发安全。 `len`返回未被读取的缓冲元素个数,`cap`返回缓冲区大小。 ``` var c = make(chan int) c <- 1 // send i := <-c // receive ``` channel是同步的,发送要等待接收,接收要等待发送。只有一个goroutine的情况下(main),等待会抛出死锁异常。 在一个nil的channel上发送和接收会永久阻塞,可用来控制select中的某个case。 可以用make指定buffer: `make(chan int, 50)`, buffer只是channel的属性不是类型,默认大小为0(无缓冲)。 range会从channel迭代数据,直到channel关闭。 ``` func suck(ch chan int) { go func() { for v := range ch { fmt.Println(v) } }() } ``` 关闭channel: `close()`, 一般由发送端来关闭channel,而不是接收端,因为向一个已经关闭的channel发送数据(或关闭2次)会引起panic(无论阻不阻塞,如果发送先阻塞,再channel被关闭,一样panic),而从一个已经关闭(如果有buffer数据会先读完)的channel接收数据为0/nil和false: `v, ok = <-c`。 默认channel是双向的,也可以转换为指定的方向:`var recv_only <-chan int; var send_only chan<- int;` 可以用双向channel赋值给单向的,反之不可,单向不同方向也不可。 `select` * select的case必须是channel操作。如果有表达式都会首先被依次求值再做条件判断(default也不会进入)。 * 如果每个条件都阻塞,select就会阻塞(如果有default则进入操作)。 * 如果多个条件满足,则随机选择一个执行。 * `select {}`阻塞goroutine。 * break/continue作用于select * select中向closed的channel发送也会导致panic。 ``` c1 := make(chan int) c2 := make(chan int) select { case v := <-c1: fmt.Printf("received %d from c1\n", v) case v := <-c2: fmt.Printf("received %d from c2\n", v) case t := <-time.After(1 * time.Second): fmt.Printf("received timeout, current time: %s\n", t) default: time.Sleep(10000) } ``` `chanOfChans := make(chan chan int)` ## Concurrency Patterns ### [Timing out, moving on](http://blog.golang.org/go-concurrency-patterns-timing-out-and) #### timeouts ``` timeout := make(chan bool, 1) go func() { time.Sleep(1 * time.Second) timeout <- true }() select { case <-ch: // a read from ch has occurred case <-timeout: // the read from ch has timed out } ``` 实际中直接使用`<-time.After(1 * time.Second)`代替case timeout条件。`time.After`返回一个阻塞的chan,时间到后变为可读,返回当前时间。 #### race condition 取query中第一个返回的response: ``` func Query(conns []Conn, query string) Result { ch := make(chan Result, 1) for _, conn := range conns { go func(c Conn) { select { case ch <- c.DoQuery(query): default: } }(conn) } return <-ch } ``` 带有空default的select:非阻塞,保证ch满(其它case阻塞)时也能正常退出,释放goroutine资源。 带有buffer的channel:保证还未开始接收时,也能存放结果,否则(不带缓存)发送全部失败(default退出),接收永远阻塞。 ### [Pipelines and cancellation](http://blog.golang.org/pipelines) fan-out: 多个函数从同一个channel里面读直至关闭。 fan-in: 一个函数从不同的输入读取和处理并汇总到一个channel输出直到所有输入都关闭。 pipeline: 一系列由channel连接的处理过程,每个处理过程由一组goroutine执行完成相同的功能。 原则: 每个处理过程当所有发送都完成后要关闭输出channel(使用defer close)。 每个处理过程从输入channel里读取,直到所有输入都关闭(使用range loop)或发送者退出(使用done channel通知,也应关闭channel)。 ``` func gen(done <-chan struct{}, nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { select { case out <- n * n: case <-done: return } } }() return out } func sq(done <-chan struct{}, in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { select { case out <- n * n: case <-done: return } } }() return out } func merge(done <-chan struct{}, cs ...<-chan int) <-chan int { var wg sync.WaitGroup out := make(chan int) wg.Add(len(cs)) for _, c := range cs { go func(ch <-chan int) { defer wg.Done() for n := range ch { select { case out <- n: case <-done: return } } }(c) } go func() { // 全部发送完最后才能关闭 wg.Wait() close(out) }() return out } func main() { // done channel在整个pipeline中共享,在pipeline退出时关闭来通知所有工作goroutine退出 done := make(chan struct{}) defer close(done) in := gen(done, 2, 3) // fan-out c1 := sq(done, in) c2 := sq(done, in) // fan-in out := merge(done, c1, c2) fmt.Println(<-out) // 4 or 9 } ``` ### Concurrent Pool ``` concurrency := 5 sem := make(chan bool, concurrency) urls := []string{"url1", "url2"} for _, url := range urls { sem <- true go func(url) { defer func() { <-sem }() // get the url }(url) } // wait all done for i := 0; i < cap(sem); i++ { sem <- true } ``` ### Others https://talks.golang.org/2013/advconc.slide # [Generics](https://go.dev/blog/intro-generics) ([zh](https://tonybai.com/2022/03/25/intro-generics)) [when generics](https://go.dev/blog/when-generics) ([zh](https://mp.weixin.qq.com/s/IukarzjVlRyU57r5t33H6w)) ## type parameter 泛型 function ``` func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s } // 调用 ints := map[string]int64{ "first": 34, "second": 12 } SumIntsOrFloats[string, int64](ints) SumIntsOrFloats(ints) // 自动类型推断 ``` 自动类型推断不适用于只在函数返回值中使用的类型形参或只在函数主体中使用的类型形参的情况。 泛型 struct ``` type ModelX[T any] struct { data []T T // error, 不能在接口或结构体里直接嵌入一个类型参数 } func (t *Modelx[T]) len() int { return len(t.data) } func (t *Modelx[T]) foo(s T) { // 类型的方法中可以使用 struct 本身的类型参数,但不支持其它函数类型参数 } // 初始化 modelInt := ModelX[int]{Data: []int{1, 2, 3}} // constraint 的 type 不能省 ``` ## type constraint 一个接口类型可以作为一个值类型使用,也可以作为一个元类型(meta-type)使用。 元类型接口表示为一个 type constraint 的集合,包含一个 type list,如果接口包含方法,则实参类型必须实现所有这些方法,且泛型参数在使用的时候只能调用这些方法。反之泛型参数如果要调用这些共同方法(共同成员变量无法访问),必须显式在接口里声明。 含有 type list 的接口(元类型)不能用于普通参数类型(值类型)。type list 里如果有接口类型,则接口类型不能包含方法。 字面表示时,最外层的 interface{} 可以省略:`[S interface{~[]E}, E interface{}]` 等价于 `[S ~[]E, E interface{}]` 如: ``` type Number interface { int64 | float64 } ``` 内置: __constraints__ 包: ``` type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type Ordered interface { Integer | Float | ~string } ``` `~int` 表示底层类型是 int 就可以满足 __any__ ``` type any = interface{} ``` 替换:`gofmt -w -r 'interface{} -> 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>`: 初始化,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 <stdlib.h> 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/