go基础知识
# 01.go的祖先
Go 的第一个祖先是 C 语言,其实,Go 有时被描述为 21 世纪的 C 语言,这是因为 Go 在许多方面(表达式语法,控制流语句,基本数据类型,参数的值传递,指针)都和 C 语言比较相似。 同时,Go 和 C 都致力于更接近机器,编译出高效的机器识别的二进制代码。这和 Python 这样的脚本语言以及需要把代码转换为字节码的 Java 有本质的不同。Go 在 C 语言语法的基础上做了许多改进,包括在 if 和 for 中不用加入(),也不用在每个语句的末尾加入;等等。
Go 的第二个祖先来自于 Pascal → Modula-2 → Oberon → Oberon-2 这条语言分支,Go 语法设计中的 package、import、声明以及特殊的方法声明的灵感都来源于此。
Go 的第三个祖先来自于 CSP→ Squeak → Newsqueak → Alef 这条语言分支,Go 从中借鉴了 CSP,并引入了 Channel 用于协程间的通信,这同时也是 Go 区别于其他语言的重要特性。
# 02.Go 基础知识体系
# 2.1 go开发环境
准备开发环境。这主要包括以下五点:
安装语言处理系统(从而能够解释、编译或运行编写的代码)
配置好 Go 语言的环境变量,包括 GOPATH、GOPROXY。
搭建好舒适的集成开发环境(GoLand、Vim、VSCode 或者 Emacs),以便快速开发代码。
挑选集成开发环境需要考虑的因素很多,主要包括下面几点:
有没有语法高亮?语法高亮是必不可少的功能,这也是为什么每个开发工具都提供配置文件,让我们自定义配置的原因。
有没有较好的项目文件纵览和导航能力?我们希望可以同时编辑多个源文件并设置书签,能够匹配括号,能够跳转到某个函数或类型的定义部分。
有没有完美的查找和替换功能?替换之前最好还能预览结果。
当有编译错误时,双击错误提示能否跳转到发生错误的位置?
能否跨平台运作?比如,能够在 Linux、Mac OS X 和 Windows 下工作,这样我们就可以只专注于一个开发环境了。
此外,我们还需要确保集成开发环境具有如下功能:
能够通过插件架构来轻易扩展和替换某个功能;
拥有断点、检查变量值、单步执行、按照过程顺序执行标识库中代码的能力;
能够方便地存取最近使用过的文件或项目;
拥有对包、类型、变量、函数和方法的智能代码补全功能;
能够方便地在不同的 Go 环境之间切换;
针对一些特定的项目有项目模板(如 Web 应用、App Engine 项目等),这样能够更快地开始开发工作。
合格的开发者需要熟悉编辑器中的快捷键
例如上移 (Up)、下移 (Down)、右移 (Right)、左移 (Left)、复制当前或选中行(Duplicate Line or Selection)、提取选中内容为函数(Extract Method),还有众多的快捷键。
掌握 Go 的一些命令行工具,特别是一些基础的命令。只要在命令行中执行 Go,就有多种子命令可供选择。这些命令及其含义如下:
go基础命令
# 2.2 go基础语法
# 2.2.1 变量与类型
变量的声明与赋值。特别是在 Go 函数中使用相当频繁的变量赋值语句 := ,其将在编译时对类型进行自动推断。
Go 中的内置类型。
int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string
1
2
3
4变量的命名规则。
变量的生命周期。需要了解变量何时存在,何时消亡。
变量的作用域。Go 的词法范围使用花括号{…}作为分割。根据作用域的范围大小,可以分为全局作用域、包作用域、文件作用域、函数作用域。
# 2.2.2 表达式与运算符
运算符包括:
- 算术运算符;
- 关系运算符;
- 逻辑运算符;
- 位运算符;
- 赋值运算符;
运算符优先级的顺序:
优先级(由高到低) 操作符
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
2
3
4
5
6
# 2.2.3 基本控制结构
程序并不都是一行一行顺序执行的,还可能根据条件跳转到其他语句执行,这就涉及到基本控制结构了。理论和实践表明,无论多复杂的算法,都可以通过顺序、选择、循环 3 种基本控制结构构造出来。
if else 语句;
if{ }else if { }else { }
1
2
3
4
5
6
7
8switch 语句;
switch var1 { case val1: ... case val2,val3: ... default: ... }
1
2
3
4
5
6
7
8
94 种 for 循环语句
1)完整的 C 风格的 for 循环
for i := 0; i < 10; i++ { fmt.Println(i) }
1
2
32)只有条件判断的 for 循环
i := 1 for i < 100 { fmt.Println(i) i = i * 2 }
1
2
3
4
53)无限循环的 for 循环
func main() { for { fmt.Println("Hello") } }
1
2
3
4
54)for-range 循环
evenVals := []int{2, 4, 6, 8, 10, 12} for i, v := range evenVals { fmt.Println(i, v) }
1
2
3
4
# 2.2.4 函数
函数是一种定义过程的强大抽象技术,它可以帮助我们构建大规模的程序。
基本的函数声明;
func name(parameter-list) (result-list) { body }
1
2
3函数的多返回值特性;
func div (a,b int) (int,error){ if b == 0 { return 0, errors.New("b cat't be 0") } return a/b,nil }
1
2
3
4
5
6
7可变参数函数。
func Println(a ...interface{}) (n int, err error)
1递归函数
func f(n int) int { if n == 1 { return 1 } return n * f(n-1) }
1
2
3
4
5
6
7
函数作为一等公民拥有一些灵活的特性:
函数作为参数时,可以提升程序的扩展性。
package main import ( "fmt" ) // 遍历切片的每个元素, 通过给定函数进行元素访问 func visit(list []int, f func(int)) { for _, v := range list { f(v) } } func main() { // 使用匿名函数打印切片内容 visit([]int{1, 2, 3, 4}, func(v int) { fmt.Println(v) }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18函数作为返回值时,一般在闭包和构建功能中间件时使用得比较多,在不修改过去核心代码的基础上,用比较小的代价增加了新的功能。
func logging(f http.HandlerFunc) http.HandlerFunc{ return func(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.Path) f(w,r) } }
1
2
3
4
5
6
7函数作为值时,可以用来提升服务的扩展性。
var opMap = map[string]func(int, int) int{ "+": add, "-": sub, "*": mul, "/": div, } f := opMap[op] f()
1
2
3
4
5
6
7
8
9
10
# 2.2.5 复合类型
如果说函数是对功能的抽象,那么复合类型带来了数据的抽象。
它增加了程序的模块化程度,并增强了语言的表达能力。例如,我们要处理分数。分数有分子和分母之分,如果我们只有基础的数据类型,这种处理将变得繁琐。而如果有了复合类型。我们就可以将分子和分母看做一个整体了。
Go 语言中内置的复合类型包括:数组、切片、哈希表,以及用户自定义的结构体。
切片
相比于数组,在 Go 语言中使用最多的是切片。
声明与赋值。
var slice1 []int numbers:= []int{1,2,3,4,5,6,7,8} var x = []int{1, 5: 4, 6, 10: 100, 15}
1
2
3
4使用 append 往切片中添加元素。
y := []int{20, 30, 40} x = append(x, y...)
1
2
3切片的截取。
numbers:= []int{1,2,3,4,5,6,7,8} // 从下标2 一直到下标4,但是不包括下标4 numbers1 :=numbers[2:4] // 从下标0 一直到下标3,但是不包括下标3 numbers2 :=numbers[:3] // 从下标3 一直到结尾 numbers3 :=numbers[3:]
1
2
3
4
5
6
7
8
Map 哈希表
Map 声明与初始化。
var hash map[T]T var hash = make(map[T]T,NUMBER) var country = map[string]string{ "China": "Beijing", "Japan": "Tokyo", "India": "New Delhi", "France": "Paris", "Italy": "Rome", }
1
2
3
4
5
6
7
8
9
10Map 的两种访问方式。
v := hash[key] v,ok := hash[key]
1
2
3Map 赋值与删除。
m := map[string]int{ "hello": 5, "world": 10, } delete(m, "hello")
1
2
3
4
5
6
自定义结构体
自定义结构体是对程序进行数据抽象、提高编程语言的表达能力的强有力的工具。
例如,假设现在我们想实现一个分数的加法逻辑。如果没有自定义结构体的抽象,可能的实现方式是下面这样:
func add(n1 int,d1 int,n2 int,d2 int) (int,int){
return (n1*d2 + n2*d1), (d1*d2)
}
2
3
4
当我们调用 add 函数时,还需要保证正确地传递了每一个参数,例如第一个参数为第一个数字的分子,第二个参数为第二个数字的分母……这也意味着 add 函数的调用者不仅仅需要小心地排列其传递的参数,还需要关注 add 函数内部的执行和返回的细节。自定义结构体可以为我们解决这样的问题。
结构体声明与赋值;
type Nat struct { n int d int } var nat Nat nat := Nat{ 2, 3 } nat.n = 4 natq := Nat{ d: 3, n: 2, }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15匿名结构体,经常在测试或者在 JSON 序列化反序列化等场景使用;
var person struct { name string age int pet string } pet := struct { name string kind string }{ name: "Fido", kind: "dog", }
1
2
3
4
5
6
7
8
9
10
11
12
13
14结构体的可比较性 ;
# 2.3 语法特性
# 2.3.1 defer
defer 是 Go 语言中的关键字,也是 Go 语言的重要特性之一,defer 在资源释放、panic 捕获等场景中的应用非常广泛。
defer func(...){
// 实际处理
}()
2
3
defer 的重要特性:
- 延迟执行;
- 参数预计算;
- LIFO 执行顺序。
除此之外,Go 语言用于异常恢复的内置 recover 函数,也需要与 defer 函数结合使用才有意义:
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
2
3
4
5
6
7
8
9
10
11
# 2.3.2 接口
Go 中的接口有两种类型,分别为“带方法的接口”和“空接口”(不考虑泛型的情况)。
带方法的接口内部有一系列方法签名:
type InterfaceName interface {
fA()
fB(a int,b string) error
...
}
2
3
4
5
空接口内部不包含任何东西,可存储任意类型:
type Empty interface{}
接口的重要特性:
- 接口的声明与定义:
type Shape interface {
perimeter() float64
area() float64
}
var s Shape
2
3
4
5
- 隐式地让一个类型实现接口:
type Rectangle struct {
a, b float64
}
func (r Rectangle) perimeter() float64 {
return (r.a + r.b) * 2
}
func (r Rectangle) area() float64 {
return r.a * r.b
}
2
3
4
5
6
7
8
9
- 接口的动态调用方式:
var s Shape
s = Rectangle{3, 4}
s.perimeter()
s.area()
2
3
4
- 接口的嵌套:
type ReadWriter interface {
Reader
Writer
}
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
2
3
4
5
6
7
8
9
10
- 接口类型断言:
func main(){
var s Shape
s = Rectangle{3, 4}
rect := s.(Rectangle)
fmt.Printf("长方形周长:%v, 面积:%v \\n",rect.perimeter(),rect.area())
}
2
3
4
5
6
根据空接口中动态类型的差异选择不同的处理方式:
这在参数为空接口函数的内部处理中使用广泛,例如 fmt 库、JSON 库
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
p.fmtFloat(f, 64, verb)
2
3
4
5
6
7
接口的比较性,具体规则为:
动态类型值为 nil 的接口变量总是相等的
如果只有 1 个接口为 nil,那么比较结果总是 false
如果两个接口都不为 nil,且接口变量具有相同的动态类型和动态类型值,那么两个接口是相同的。
如果接口存储的动态类型值是不可比较的,那么在运行时会报错。
# 2.4 并发编程
进程、线程与协程
进程是操作系统资源分配的基本单位
线程是操作系统资源调度的基本单位
而协程位于用户态,是在线程基础上构建的轻量级调度单位
并发与并行
并行指的是同时做很多事情
并发是指同时管理很多事情
主协程与子协程
main 函数是特殊的主协程,它退出之后整个程序都会退出
而其他的协程都是子协程,子协程退出之后,程序正常运行。
Go 语言运行时为我们托管了协程的启动与调度工作,我们关心的重点只要放在如何优雅安全地关闭协程,以及如何进行协程间的通信就可以了。
Go 使用通道完成协程间的通信,通道的基本使用方式包括:
- 通道声明与初始化:
chan int
chan <- float
<-chan string
2
3
- 通道写入数据:
c <- 5
- 通道读取数据:
data := <-c
- 通道关闭:
close(c)
- 通道作为参数:
func worker(id int, c chan int) {
for n := range c {
fmt.Printf("Worker %d received %c\\n",
id, n)
}
}
2
3
4
5
6
- 通道作为返回值(一般用于创建通道的阶段):
func createWorker(id int) chan int {
c := make(chan int)
go worker(id, c)
return c
}
2
3
4
5
- 单方向的通道,用于只读和只写场景:
func worker(id int, c <-chan int)
- select 监听多个通道实现多路复用。当 case 中多个通道状态准备就绪时,select 随机选择一个分支进行执行:
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
2
3
4
5
6
7
8
9
10
Go 除了使用通道完成协程间的通信外,还提供了一些其他手段:
- 用 context 来处理协程的优雅退出和级联退出
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
2
3
4
5
6
7
8
9
10
11
12
- 传统的同步原语:原子锁
Go 提供了 atomic 包用于处理原子操作
func add() {
for {
if atomic.CompareAndSwapInt64(&flag, 0, 1) {
count++
atomic.StoreInt64(&flag, 0)
return
}
}
}
2
3
4
5
6
7
8
9
- 传统的同步原语:互斥锁
var m sync.Mutex
func add() {
m.Lock()
count++
m.Unlock()
}
2
3
4
5
6
- 传统的同步原语:读写锁
适合多读少写场景
type Stat struct {
counters map[string]int64
mutex sync.RWMutex
}
func (s *Stat) getCounter(name string) int64 {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.counters[name]
}
func (s *Stat) SetCounter(name string){
s.mutex.Lock()
defer s.mutex.Unlock()
s.counters[name]++
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 除此之外,Go 语言在传统的同步原语基础上还提供了许多有用的同步工具,包括 sync.Once、sync.Cond、sync.WaitGroup。
# 2.5 项目组织
要构建一个大型系统,我们还需要使用其他人已经写好的代码库。这时,要管理好项目依赖的第三方包。
# 2.5.1 依赖管理
Go 的依赖管理经历了长时间的演进。 现如今,Go Module 已经成为了依赖管理的事实标准,掌握 Go Module 的基础用法已经成为 Go 语言使用者的必备技能。
module github.com/dreamerjackson/crawler
go 1.18
require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/antchfx/htmlquery v1.2.5 // indirect
github.com/antchfx/xpath v1.2.1 // indirect
)
2
3
4
5
6
7
8
9
10
除此之外,理解 GOPATH 这种单一工作区的依赖管理方式也是非常有必要的,因为它现阶段并没有完全被废弃。
# 2.5.2 面向组合
构建大规模程序需要我们完成必要的抽象,这样才能屏蔽一些细节,然后从更高的层面去构建大规模程序。一些比较经典的思想,比如函数用于过程的抽象、自定义结构体用于数据的抽象。
如果你是一个“老学究”,建议你去阅读一下《Structure and Interpretation of Computer Programs》这本书,感受一下这些我们习以为常的简单元素背后的非凡哲学。
在理解了过程抽象与数据抽象之后,再来看另一种、简单而又强大的设计哲学——面向组合。
面向组合可以帮助我们完成功能之间的正交组合,轻松构建起复杂的程序,还可以使我们灵活应对程序在未来的变化。
举一个在 IO 操作中实现面向组合思想的例子:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 组合了Read与Write功能
type ReadWriter interface {
Reader
Writer
}
type doc struct{
file *os.File
}
func (d *doc) Read(p []byte) (n int, err error){
p,err := ioutil.ReadAll(d.file)
...
}
// v1 版本
func handle(r Reader){
r.Read()
...
}
// v2 版本
func handle(rw ReadWriter){
rw.Read()
rw.Write()
...
}
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
Reader 接口包含了 Read 方法,Writer 接口包含了 Write 方法。
假设我们的业务是处理文档相关的操作,类型 doc 一开始实现了 Read 功能,将文件内容读取到传递的缓冲区中,函数 handle 中的参数为接口 Reader。
后来随着业务发展,我们又需要实现文档写的功能,这时我们将函数 handle 的参数修改为功能更强大的接口 ReadWriter。而 doc 只需要实现 Writer 接口,就隐式地实现了这个 ReadWriter 接口。
这样,通过组合,我们就不动声色地完成了代码与功能的扩展。
# 2.6 工具与库
# 2.6.1 工具:代码分析与代码规范
Go 自带了许多工具,它们可以规范代码、提高可读性,促进团队协作、检查代码错误等。 可将这种工具分为 静态与动态 两种类型。
静态工具对代码进行静态扫描,检查代码的结构、代码风格以及语法错误,这种工具也被称为 Linter。
- go fmt
它可以格式化代码,规范代码的风格
go fmt path/to/your/package
除了go fmt,也可以直接使用gofmt 命令对单独的文件进行格式化。
gofmt -w yourcode.go
- go doc
go doc 工具可以生成和阅读代码的文档说明。
文档是使软件可访问和可维护的重要组成部分。当然,它需要写得好且准确,也需要易于编写和维护。理想情况下,文档注释应该与代码本身耦合,以便文档与代码一起发展。go doc 可以解析 Go 源代码(包括注释), 并生成 HTML 或纯文本形式的文档。例如可以查看标准库 bufio 的文档 (opens new window) 。
- go vet
go vet 是 Go 官方提供的代码静态诊断器,它可以对代码风格进行检查,并且报告可能有问题的地方,例如错误的锁使用、不必要的赋值等。
go vet 启发式的问题诊断方法不能保证所有输出都真正有问题,但它确实可以找到一些编译器无法捕获的错误。 由于 go vet 本身是多种 Linter 的聚合器,我们可以通过go tool vet help 命令查看它拥有的功能。Go 语言为这种代码的静态分析提供了标准库 go/analysis (opens new window) ,这意味着我们只用遵循一些通用的规则就可以写出适合自己的分析工具。这还意味着我们可以对众多的静态分析器进行选择、合并。
- golangci-lint
不过,当前企业中使用得最普遍的不是 go vet 而是golangci-lint。这是因为 Go 中的分析器非常容易编写,社区已经创建了许多有用的 Linter,而 golangci-lint 正是对多种 Linter 的集合。
要查看 golangci-lint 支持的 Linter 列表以及 golangci-lint 启用 / 禁用哪些 Linter,可以通过golangci-lint help linters 查看帮助文档或者查看golangci-lint 的官方文档 (opens new window)。
- go race
Go 1.1 后提供了强大的检查工具 race,它可以排查数据争用问题。race 可以用在多个 Go 指令中。
$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg
2
3
4
当检测器在程序中发现数据争用时,将打印报告。这份报告包含发生 race 冲突的协程栈,以及此时正在运行的协程栈。
» go run -race 2_race.go
==================
WARNING: DATA RACE
Read at 0x00000115c1f8 by goroutine 7:
main.add()
bookcode/concurrence_control/2_race.go:5 +0x3a
Previous write at 0x00000115c1f8 by goroutine 6:
main.add()
bookcode/concurrence_control/2_race.go:5 +0x56
2
3
4
5
6
7
8
9
go race 工具可以完成静态分析,但是有些并发冲突是静态分析难以发现的,所以 go race 在运行时也可以开启,完成动态的数据争用检测,一般在上线之前使用。
动态工具指的是需要实际运行指定代码才能够分析出问题的工具,还包括了代码测试、调试等阶段使用到的工具。
# 2.6.2 工具:代码测试
- go test
在 Go 中,测试函数位于单独的以 _test.go 结尾的文件中,测试函数名以Test 开头。go test 会识别这些测试文件并进行测试。测试包括单元测试、Benchmark 测试等。
单元测试指的是测试代码中的某一个函数或者功能,它能帮助我们验证函数功能是否正常,各种边界条件是否符合预期。单元测试是保证代码健壮性的重要手段。Go 中比较有特色的单元测试叫做表格测试。通过表格测试可以简单地测试多个场景。如下所示。另外,测试中还有一些特性例如 t.Run 支持并发测试,这能加快测试的速度,即便某一个子测试(subtest)失败,其他子测试也会完成测试。
func TestSplit(t *testing.T) {
tests := map[string]struct {
input string
sep string
want []string
}{
"simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
"no sep": {input: "abc", sep: "/", want: []string{"abc"}},
"trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("expected: %#v, got: %#v", tc.want, got)
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- go test -cover
执行 go test 命令时,加入 cover 参数能够统计出测试代码的覆盖率。
% go test -cover
PASS
coverage: 42.9% of statements
ok size 0.026s
%
2
3
4
5
- go tool cover
另外,还可以收集覆盖率文件并进行可视化的展示。 具体的做法是,在执行 go test 命令时加入 coverprofile 参数,生成代码覆盖率文件。然后使用 go tool cover 可视化分析代码覆盖率的信息。
> go test -coverprofile .coverage.txt
> go tool cover -func .coverage.txt
2
- go test -bench
Go 还可以进行 Benchmark 测试,要测试函数的前缀名需要为 Benchmark。
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Fibonacci(30)
}
}
2
3
4
5
默认情况下,执行 go test -bench 之后,程序会在 1 秒后打印出在这段时间里内部函数的运行次数和时间。
在进行 BenchMark 测试时,还可以指定一些其他运行参数。例如,“-benchmem”可以打印每次函数的内存分配情况,“cpuprofile”、“memprofile”还能收集程序的 CPU 和内存的 profile 文件,方便后续 pprof 工具进行可视化分析。
go test ./fibonacci \\
-bench BenchmarkSuite \\
-benchmem \\
-cpuprofile=cpu.out \\
-memprofile=mem.out
2
3
4
5
# 2.6.3 工具:代码调试
在程序调试阶段,除了可以借助原始的日志打印消息,我们还可以使用一些常用的程序分析与调试的工具。
dlv 是 Go 官方提供的一个简单、功能齐全的调试工具,它和传统的调试器 gdb 的使用方式比较类似,但是 dlv 还专门提供了与 Go 匹配的功能,例如查看协程栈,切换到指定协程等。
gdb 是通用的程序调试器,但它并不是 Go 程序在调试时最优的选项。gdb 可能在有些方面比较有用,例如调试 cgo 程序或运行时。不过在一般情况下,建议优先选择 dlv。
- pprof
pprof 是 Go 语言中对指标或特征进行分析的工具。通过 pprof,不仅可以找到程序中的错误(内存泄漏、race 冲突、协程泄漏),也能找到程序的优化点(CPU 利用率不足等)。
pprof 包含了样本数据的收集以及对样本进行分析两个阶段。收集样本简单的方式是借助 net/http/pprof 标准库提供的一套 HTTP 接口访问。
curl -o cpu.out http://localhost:9981/debug/pprof/profile?seconds=30
而要对收集到的特征文件进行分析,需要依赖谷歌提供的分析工具,该工具在 Go 语言处理器安装时就存在:
go tool pprof -http=localhost:8000 cpu.out
- trace
在 pprof 的分析中,能够知道一段时间内 CPU 的占用、内存分配、协程堆栈等信息。这些信息都是一段时间内数据的汇总,但是它们并没有提供整个周期内事件的全貌。例如,指定的协程何时执行,执行了多长时间,什么时候陷入了堵塞,什么时候解除了堵塞,GC 是如何影响协程执行的,STW 中断花费的时间有多长等。而 Go1.5 之后推出的 trace 工具解决了这些问题。
trace 的强大之处在于,提供了程序在指定时间内发生的事件的完整信息,让我们可以精准地排查出程序的问题所在。
- gops
gops 是谷歌推出的调试工具,它的作用是诊断系统当前运行的 Go 进程。gops 可以显示出当前系统中所有的 Go 进程,并可以查看特定进程的堆栈信息、内存信息等。
# 2.6.4 标准库
标准库是官方维护的,用于增强和扩展语言的核心库。标准库一般涵盖了通用的场景以及现代开发所需的核心部分。
例如,用于数学运算的 math 包、用于 I/O 处理的 io 包,用于处理字符串的 strings 包,用于处理网络的 net 包,以及用于处理 HTTP 协议的 http 包。
由于标准库经过了大量的测试,有稳定性的保证,并且提供了向后兼容性。开发者可以借助标准库快速完成开发。
Go 提供了众多的标准库 (opens new window):
archive bufio bytes compress container crypto database
debug encoding errors expvar flag fmt go
hash html image index io log math
mime net os path reflect regexp runtime
sort strconv strings sync syscall testing text
time unicode unsafe
2
3
4
5
6
相比其他语言,开发者对 Go 标准库的依赖更多。Go 标准库中提供了丰富的内容和强有力的封装,比如 HTTP 库就对 HTTP 协议进行了大量的封装,TCP 连接底层也通过封装 epoll/kqueue/iocp 实现了 I/O 多路复用。开发者使用这种开箱即用的特性就可以相对轻松地写出简洁、高性能的程序。
# 2.6.5 第三方库
Go 语言中,优秀的第三方库、框架和软件可谓汗牛充栋,就拿 HTTP 框架来说吧,比较熟悉的就有 Echo、Gin、Beego 等知名的框架。可以参考 awesome-go (opens new window) 中列出的众多优秀的 Go 代码库。