《the go programming language》学习笔记

说明

  1. 《the go programming language》中文版,但是存在DMCA takedown问题,我猜测是版权问题,毕竟英文原版以及其他语言都已经交付出版社了。感谢weaming及时fork出来并搭建gitbook在线版本。
  2. 可以使用在线的也可以搭建自己的学习环境 playground tour

概述

  1. 语言基础包括变量、类型定义、包、文件、作用域等,复合类型有数组、字典、切片和动态列表。函数部分有错误处理、panic、recover及defer。
  2. go语言特有的方法、接口、并发、测试和反射。
  3. 基于CSP的并发编程,goroutines channels

程序结构

内置的常量,类型和函数

下面罗列存在但是笔者不熟悉的

1
2
3
itoa nil
uintptr rune complex128 complex64
make cap append close delete complex real imag panic recover

变量命名可以是htmlEscape HTMLEscape escapeHTML 但不应该是escapeHtml

变量

var 变量名字 类型 = 表达式

省略类型时可以通过值进行类型推导,省略值时可设置初始化零值

变量 := 表达式

简短变量声明语句中至少要声明一个新变量

指针会创建别名,slice map chan 结构体 数组 接口等引用类型也会创建变量别名

变量名 := new(T)

创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

new只是一个预定义的函数不是关键字

赋值

++ -- 递增和递减是语句不是表达式

元组赋值 x, y = y, x 右边表达式先求值,统一更新左边变量对应的值

1
2
3
4
> v = m[key]  //map查找,失败时返回零值
> v = x.(T) //type断言,失败时panic异常
> v = <-ch //管道接收,失败时返回零值
>

赋值时左右类型需匹配,进行比较时也需要存在可赋值性 slice map和chan的元素有隐式赋值行为

类型

type 类型名字 底层类型

类型声明语言一般出现在包一级,如果新创建的类型名字的首字母大写则外部包也可用

类型转换 T(x) 底层基础类型相同或者指向相同底层结构的指针类型才可以转型 T是指针需要小括号(*int)(x)

类型有方法集 参数出现在方法名前

1
2
> func (c Celsius) String() string { return fmt.Sprintf("%g°C",c) }
>

包声明前的注释是包注释,包含包的功能概要说明,说明较多回放到独立的doc.go中

goimports gofmt工具可以删除不必要的依赖包

包级变量的初始化是按照变量声明出现的顺序来的

一个文件可以有多个init初始化函数,不能被调用,引用。在程序开始执行时按照声明顺序被自动调用

包只会被初始化一次

初始化的逻辑可以封装成匿名函数

1
2
3
4
5
6
> var pc [256] byte = func() (pc [256] byte) {
> for i := range pc { // 省略值 只使用索引
> pc[i] = pc[i/2] + byte(i&1)
> }
> }()
>

基础数据类型(数字 字符串 布尔型)

  1. unicode字符rune类型是和int32等价的类型,byte是uint8的等价类型,uintptr是一种无符号的整数类型,没有指定具体的bit大小却足以容纳指针
  2. %仅用于整数间取模,结果符号与被取模数符号一致
  3. /整数除法会向着0方向截断余数
  4. 对于整数,+x是0+x的简写,-x是0-x的简写;对于浮点数和复数,+x就是x-x是x的负数
  5. 位操作运算符
1
2
3
4
5
6
& //AND
| // OR
^ // XOR
&^ //AND NOT(位清空)
<< //左移
>> //右移
  1. 任何大小的整数字面值都可以使用以0开始的八进制格式或者以0x开始的十六进制格式
  2. ascii := 'a'一对单引号直接包含对应字符
  3. complex64 complex128 就是复数类型。内置的complex函数用于构建复数,real imag函数分别返回复数的实部和虚部
  4. 常量的值在编译期间确定,常量间的算术运算、逻辑运算、比较运算、类型转换结果都是常量 即使len cap imag complex unsafe.Sizeof调用也是返回常量
  5. 无类型的常量可以提供高运算精度,且可直接用于更多表达式而不需要显式转换
  6. 字符串是一个不可改变的字节序列,== <= 的比较是逐字节比较的
  7. 原生字符串
  8. string接受到[]rune的类型转换,可将一个UTF8编码的字符串解码为Unicode字符序列
  9. byte.buffer 用于字节slice的缓存,可动态变化
  10. 字符串和字节slice相互转换s := "abc" b := []byte(s) s2 := string(b)
  11. 字符串和数字转换 fmt.Sprintf返回格式化字符串,或者 strcov.Itoa
  12. 使用fmt.Scanf解析输入的字符串和数字
    4 ^& 5

复杂数据类型

(数组 结构体 slice map)前两者是有固定内存大小的数据结构,后两者动态数据结构按需动态增长

  1. 数组在默认情况下会被初始化为元素类型对应的零值 q := [...]int{1, 2, 3}数组长度由初始化值的个数来计算。
  2. 初始化索引的顺序无关紧要,可以省略,未指定初始值的元素使用零值初始化
  3. crypto/sha256包的Sum256可以对任意字节的slice类型数据生成一个对应的消息摘要。
  4. 一个slice由三部分组成:指针、长度和容量,len cap 分别返回slice的长度和容量
  5. s[i:j]返回长度为j-i的slice,cap是cap(s)-i
  6. s := []int{0,1,2,3,4,5}不同于数组,slice没指定序列长度,会隐式创建一个合适大小的数组
  7. slice不能使用==来判断相等,bytes.Equal提供了字节型slice的比较。if s == nil一个零值的slice等于nil,一个nil值的slice没有底层数组且长度和容量都是0。例如var s []int s = []int(nil)
  8. make用于创建一个指定元素类型、长度和容量的slice,append用于向slice追加元素
  9. map是无序的key/value集合,ages := make(map[string]int),map中的元素不是变量,不能取地址操作
  10. map 的value的类型可以是聚合的,例如map或slice
  11. 结构体变量的成员也是变量,成员名字的首字母大写的表示是导出的
  12. 一个命名为S的结构体类型不能再包含S类型的成员但是可以是*S指针类型的成员
  13. 结构体面值type Point struct{X, Y int},有匿名成员
  14. 结构体类型可以用==进行比较,还可以作为map的key
  15. json.Marshal函数将类结构体slice转为json(叫做编组),json.MarshalIndent(movies,""," ")输出便于阅读的格式(两参数代表每行输出前缀和每层缩进)
  16. 下面的代码片段先new并返回一个模版在注册自定义函数最后分析模版
1
2
3
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)

引用类型

(指针 切片 字典 函数 通道)

  1. func Sin(x float64) float 没有函数体的函数声明,表示不是用go实现的
  2. go语言的垃圾回收机制不会处理操作系统层面的资源,例如打开的文件网络链接等,需要手动显式处理
  3. 按照惯例,函数的最后一个bool类型的返回值表示函数是否成功,error表示对应的错误
  4. bare return 省略的一种写法,返回和返回值列表次序一致
  5. 错误处理:log.Fatalf("site is down %v\n, err)相当于fmt.Fprintf(os.Stderr,"site is down %v\n, err) os.Exit(1)
  6. 函数类型的零值是nil
  7. defer可以在一个函数中被执行多次,顺序与声明顺序相反。常用于处理成对的操作,打开、关闭、连接、断开、加锁、释放锁等。defer resp.Body.Close() defer f.Close() defer mu.Unlock()
  8. defer语句中的函数会在return语句更新返回值变量后执行也就是在函数执行完执行,其中定义的匿名函数可以访问该函数包括返回值在内的所有变量。可以自如修改返回值
  9. panic异常,可以通过直接调用内置panic函数的方式引发
  10. 在go的panic机制中,延迟函数的调用在释放堆栈信息之前
  11. recover可以从panic中恢复,被建议的做法是,判断panic类型选择性recover
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// for循环中生成的所有函数都共享相同的循环变量
for _, i := range ranges{
i := i
append(funcs, func() {
doFunc(i)
})
}

// 可变参数
func sum(vals ...int) {
total := 0
for _, val := range vals {
total += val
}
return total
}
// 对应的函数调用
fmt.Println(sum(1,2,3))
values := []int{4,5,6}
fmt.Println(sum(values...))
// 可变或者省略参数就是一个slice


// recover机制
func soleTitle(doc *html.Node) (title string, err error){
type bailout struct{}
defer func() {
switch p := recover(); p {
case nil:
case bailout{}:
err := fmt.Errorf("multiple title elements")
default:
panic(p)
}
}()
// ...
}

OOP(面向对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Point struct {X, Y float64}

// p叫receiver 一般会简写
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X - p.X, q.Y - p.Y)
}

// 调用
fmt.Println(p.Distance(q))

// 方法值
distanceFromP := p.Distance
// 可以不通过指定器被调用
fmt.Println(distanceFromP(q))

// 方法表达式
distance := Point.Distance
fmt.Println(Distance(p,q))
// 通过Point.Distance得到的函数比实际Distance方法多一个参数,第一额外参数指定接收器。

go能够给任意类型定义方法。只要不是指针和interface

  1. 一般约定如果Point类有一个指针作为接收器的方法,那么所有Point的方法都必须有一个指针接收器。即使是那些并不需要这个指针接收器的函数。
  2. 一个类型本身是一个指针,不允许出现在接收器中。
  3. 编译器会隐式做一些转换,例如接收器形参是类型T,实参是T或者接收器形参类型是 T,实参是T类型。
  4. 当你定义一个允许nil作为接收器值的方法的类型时,在类型前面的注释中指出nil变量代表的意义是很有必要的。
  5. nil的字面量编译器无法判断其准备类型。
  6. 通过嵌入结构体可以扩展类型。嵌入体的方法也会被引入。
  7. 在类型中内嵌的匿名字段也可以是一个命名类型的指针。
  8. 比map[T]bool类型表现更理想– bit数组,通常会用一个无符号数的slice来表示。
  9. 只用来访问和修改内部变量的函数称为getter/setter,在命名一个getter的时候经常省略Get前缀。

接口类型

  1. LSP里氏替换,一个类型可以自由的使用另一个满足相同接口的类型来进行替换。
  2. 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型就是这个接口类型的实例。
  3. 接口也可以像结构体一样内嵌。
  4. 一个类型拥有一个接口需要的所有方法,那么这个类型实现了这个接口。
  5. 即使具体类型有其他的方法也只有接口类型暴露出来的方法会被调用到。接口类型封装和隐藏具体类型和它的值
  6. T类型的值不拥有所有*T指针的方法
  7. 接口值是可以使用==来进行比较,两个接口值动态类型相同但动态类型是切片等,不可比较,这时进行比较会失败并panic
  8. 在fmt包内部,使用反射来获取接口动态类型的名称。
  9. 一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。
  10. errors包只有4行代码,其中errorString是结构体而非字符串,保证errors.New的时候每次调用都获取一个新实例,防止错误消息相同导致错误相同。
  11. 类型断言是使用在接口上的操作,x.(T)就叫断言类型,x为接口类型,T表示一个类型。类型断言检查它操作对象的动态类型是否和断言的类型匹配。
  12. 对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(变大),但是它保护了接口值内部的动态类型和值的部分。[理解起来还是有点困难]
  13. 如果类型断言出现在预期有两个结果的赋值操作中,那么失败的时候就不会发生panic但是代替的返回一个额外的第二个结果,一个标示成功的布尔值。var w io.Writer = os.Stdout b,ok := w.(*bytes.Buffer)结果就是断言失败,b = nil,ok = false
  14. 类型分支,存在简洁写法,否则需要if-else-if判断
  15. xml/encoding 包中有很多类型推断的应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var w io.Writer
w = os.Stdout // ok
w = new(bytes.Buffer) // ok
w = time.Duration // compile error: time.Duration lacks write method


var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // ok
w.Close() // compile error: io.Writer lacks Close method


// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*byte.Buffer)(nil)

// 常见的使用场景如下
if w, ok := w.(*os.File); ok {
// ... use w...
// 直接覆盖原来的变量w
}


// 类型开关
switch x.(type) {
case nil: // ...
case int,uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
// 应用如下

func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int,uint:
return fmt.Sprintf("%d", x)
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x)
default:
panic(fmt.Sprintf("unexpected type %T: %v", x,x))
}
}
1
2
3
4
5
6
7
8
9
// net/http 包中的一段示例代码如下
type HandlerFunc func(w ResponseWriter, r *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w,r)
}
// ServeHTTP方法的行为调用了它本身的函数
// http.HandlerFunc是一个类型,一个实现了接口http.Handler方法的函数类型
// 让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。

并发编程

  1. goroutinue的概念类似于线程但不同,在main goroutinue结束后,会立即通知别的goroutinue结束。
  2. go 关键字后面的函数的参数会在go语句自身执行时被求值。
  3. channelsgoroutinue之间的通信机制。ch := make(chan int),channel的零值是nil,作为参数是引用。
  4. ch <- x发送 x = <-ch接收并赋给x <-ch接收
  5. channels的接收操作有一个变体形式v,ok := <-ch,ok为false表示通道关闭且没有值可接收
  6. 一般使用range持续从ch中取值,直到被关闭for x := range ch{ fmt.Println(x) }
  7. 单方向channel,分别是<-chan int(发送型)和chan<- int(接收型)类型
  8. 带缓存的channel,ch := make(chan string, 3),可以通过cap(ch) len(ch)查看内部缓存的容量和缓存队列中的有效元素个数
  9. 注意匿名函数的循环变量快照问题,所以一般go func(string s){ fmt.Println(s) }(f),显式传给闭包函数
  10. sync.WaitGroup提供了多个goroutine操作时安全并且提供在其减为零之前一直等待的一种方法,一般在goroutinue前wg.Add(1)defer wg.Done()相当于wg.Add(-1)
  11. 使用select实现多路复用,语法和switch类似,没有case的语句会一直等待下去。default中什么操作也没有叫做“轮询channel”
  12. channel的零值是nil,对nil接收或发送会永远阻塞,但是在select中nil的channel永远不会被选中。
  13. 编程实践:在go build,go run或者go test命令后面加上-race的flag,编译器会帮助完成竞争检测

并发的一些基本概念

  1. 如果一个类型是并发安全的,那么所有它的访问方法和操作就是并发安全的。
  2. 数据竞争会在两个以上的goroutinue并发访问相同的变量且至少一个为写操作。
  3. 不要使用共享数据来通信,使用通信来共享数据。
  4. 串行绑定或者只使用一个channel来发送给指定的goroutine请求来查询更新变量。
  5. 一个智能为1和0的信号量叫做二元信号量。
  6. 惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。
  7. 当你使用mutex的时候要确保mutex和其保护的变量没有被导出(变量既要小写也要防止被大写字母开头的函数访问到)
  8. 多读单写锁sync.RWMutex允许多个只读锁并行执行,但写操作会完全互斥。
  9. 尽可能将变量限定在goroutinue内部,如果是多个goroutinue都需要访问的变量则需要使用互斥条件来访问。
  10. sync.Once 一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了。 sync.Once.Do(f)接收初始化函数作为参数
  11. os线程都有一个固定大小的内存栈,goroutinue一般只需要2KB,也可达1G,动态栈。
  12. OS线程被操作系统内核调度,硬件计时器会中断处理器,切换用户台和内核态。golang提供了一套调度器,原理类似scheduler内核函数,但是开销比较小。
1
2
3
4
var (
sema sync.Mutex
balance int
)

包和工具

  1. 每个包由一个全局唯一的字符串所标示的导入路径定位。
  2. 导入包要重命名避免冲突。
  3. 包的匿名导入:import _ "image/png" ,会计算包级变量的初始化表达式和执行init初始化函数。
  4. // +build linux darwin类似的构建注释可以提供更多的构建过程控制。
  5. go doc time.Duration.Seconds godoc -http :9999两个查看文档的工具
  6. go list -json hash以json的格式输出详细信息

测试

  1. 我们须小心处理边界条件,思考合适的数据结构,推断合适的输入应该产生什么样的结果输出,至于测试的编写则与普通golang程序无异。
  2. go test会遍历*_test.go文件并生成一个临时的main包用来调用相应的测试函数,然后构建并运行、报告测试结果并清理临时文件。
  3. 测试函数、基准测试函数、示例函数(Test为前缀,Benchmark为前缀,Example为前缀)
  4. 代码程序中如果有意外的事情导致函数panic异常,测试驱动应该尝试用recover捕获异常,然后讲当前测试当作失败处理
  5. go test不会同时并发地执行多个测试
  6. 开始一个好的测试的关键是通过实现你真正想要的具体行为,然后才是考虑简化测试代码。
1
2
3
4
// 一个测试函数应该有如下签名
func TestRambo(t *testing.T){
// t参数用于报告测试失败和附加的日志信息
}

反射

底层编程

代码片段(小技巧)

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

import (
"strings"
"fmt"
"os"
)

func main() {
fmt.Println(strings.Join(os.Args[:]," "))

for i,arg := range os.Args[:] {
fmt.Prinln(i," ",arg)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"io/ioutil"
"os"
"strings"
)

func main() {
counts := make(map[string]int)

for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr,"dup: %v\n", err)
continue
}

for _, line := range strings.Split(string(data),"\n") {
counts[line] ++
}
}

for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
1
2
3
4
5
6
7
8
9
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n",o)
/*
%之后的[1]副词告诉Printf函数再次使用第一个操作数
%后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。
*/


fmt.Printf("%*s<%s>\n", depth*2, "", n.Data)
// 输出depth*2数量的空格
1
2
// 函数的多返回值,Errorf返回也是error类型
return nil, fmt.Errorf("getting %s %s",url, resp.status)
1
//常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),..., k次方阶O(n^k),指数阶O(2^n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// bit数组
package main

import (
"bytes"
"fmt"
)

type IntSet struct {
words []uint64
}

func (s *IntSet) Has(x int) bool {
word, bit := x/64, uint(x%64)
return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

func (s *IntSet) Add(x int) {
word, bit := x/64, uint(x%64)
for word >= len(s.words) {
s.words = append(s.words, 0)
}

s.words[word] |= 1 << bit
}

func (s *IntSet) UnionWith(t *IntSet) {
for i, tword := range t.words {
if i < len(s.words) {
s.words[i] |= tword
} else {
s.words = append(s.words, tword)
}
}
}

func (s *IntSet) Len() int {
var count int
for _, word := range s.words {
if word == 0 {
continue
}

for j := 0; j < 64; j++ {
if word&(1<<uint(j)) != 0 {
count++
}
}
}
return count
}

func (s *IntSet) Remove(x int) {
if s.Has(x) {
s.words[x/64] -= 1 << uint(x%64)
}
}

func (s *IntSet) Clear() {
s.words = nil
}

func (s *IntSet) Copy() *IntSet {
return &IntSet{
s.words,
}
}

func (s *IntSet) String() string {
var buf bytes.Buffer
buf.WriteByte('{')

for i, word := range s.words {
if word == 0 {
continue
}

for j := 0; j < 64; j++ {
if word&(1<<uint(j)) != 0 {
if buf.Len() > len("{") {
buf.WriteByte(' ')
}

fmt.Fprintf(&buf, "%d", 64*i+j)
}
}
}

buf.WriteByte('}')
fmt.Fprintf(&buf, "=>%d", *s)
return buf.String()

}

func main() {
var test = new(IntSet)

test.Add(10)
test.Add(1)
test.Add(10)
test.Add(100)

fmt.Println(test)
test.Remove(1000)

//fmt.Println(test.Has(1))

// 平台自动判断的智能表达式
//fmt.Println(32 << (^uint(0) >> 63))

}