Skip to content

go 语言圣经

前言

Go 从 C 语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针

拥有自动垃圾回收、一个包系统、函数作为一等公民、词法作用域、系统调用接口、只读的 UTF8 字符串

没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储

提供了基于顺序通信进程(CSP)的并发特性支持。Go 语言的动态栈使得轻量级线程 goroutine 的初始栈可以很小,因此,创建一个 goroutine 的代价很小,创建百万级的 goroutine 完全是可行的

入门

go
// Echo1 prints its command-line arguments.
package main

import (
    "fmt"
    "os"
)

func main() {
    var s, sep string
    for i := 1; i < len(os.Args); i++ {
        s += sep + os.Args[i]
        sep = " "
    }
    fmt.Println(s)
}
  • var 声明定义了两个 string 类型的变量 s 和 sep。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值(zero value),数值类型是 0,字符串类型是空字符串""。这个例子里,声明把 s 和 sep 隐式地初始化成空字符串

  • 符号 := 是短变量声明(short variable declaration)的一部分, 这是定义一个或多个变量并根据它们的初始值为这些变量赋予适当类型的语句。只能用在函数内部

程序结构

  • 一个大的程序是由很多小的基础构件组成
  • 变量保存值,简单的加法和减法运算被组合成较复杂的表达式
  • 基础类型被聚合为数组或结构体等更复杂的数据结构
  • 使用 if 和 for 之类的控制语句来组织和控制表达式的执行流程
  • 多个语句被组织到一个个函数中,以便代码的隔离和复用
  • 函数以源文件和包的方式被组织

命名

包级名字以大写字母开头的,它将是导出的,也就是说可以被外部的包访问

如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义

声明

var const type func

go
// 包名声明
package main
// 导入依赖
import "fmt"
// 包一级的各种声明(包一级的顺序无关紧要)
const boilingF = 212.0

func main() {
    // 但函数内部的名字则必须先声明之后才能使用
    var f = boilingF
    var c = (f - 32) * 5 / 9
    fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
}

变量

var 变量名字 类型 = 表达式

以下部分省略时 “类型” - 将根据初始化表达式来推导变量的类型信息 “= 表达式” - 将用零值(0 false '' nil)初始化该变量

名字 := 表达式

go
// 声明一组变量
i, j := 0, 1
in, err := os.Open(infile)
f, err := os.Open(infile) // 声明至少要有新的变量,已存在的变量是赋值行为
f, err := os.Create(outfile) // 编译报错

指针

go
x := 1
// p 指针 指向 变量 x
p := &x         // &x 取 x 变量的地址,此时 `指针 p` 保存的是一个地址,对应的数据类型是 *int
fmt.Println(*p) // *p 表达式 -> 对应 p 指针指向的变量值
*p = 2          // x = 2
fmt.Println(x)  // "2"

指针的零值时 nil

go
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"

每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名

很多其他引用类型也会创建别名,例如 slice、map 和 chan,甚至结构体、数组和接口都会创建所引用变量的别名

new 函数 new(T) *T 并不是关键字

go
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
// var a int
// p := &a
fmt.Println(*p) // 0 指定类型的零值
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // 2
go
for t := 0.0; t < cycles*2*math.Pi; t += res {
    x := math.Sin(t)
    y := math.Sin(t*freq + phase)
    img.SetColorIndex(
        size+int(x*size+0.5), size+int(y*size+0.5),
        blackIndex, // 这里加逗号,避免编译器在行尾插分号
    )
}

赋值

数值变量 ++ 递增和 -- 递减语句(自增和自减是语句,而不是表达式,因此 x = i++ 之类的表达式是错误的)

元组赋值

go
func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x + y
    }
    return x
}

类型

go
type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在外部包也可以使用。

go
package tempconv

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

// 这两个是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算

// 如 boilingF-FreezingC  将会编译错误

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

// Fahrenheit() 显式的转换类型
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

类型转换操作 T(x),用于将 x 转为 T 类型(两者底层类型相同才能转)

类型的方法集

go

// 表示声明的是 Celsius 类型的一个名叫 String 的方法,该方法返回该类型对象 c 带着 °C 温度单位的字符串
func (c Celsius) String() string {
    return fmt.Sprintf("%g°C", c)
}

ccc := FToC(212.0) // ccc 是 Celsius 类型的变量

fmt.Println(ccc.String()) // "100°C"

包和文件

多个文件,可以指定同一个包名,就好像所有代码都在一个文件一样。

包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的

包注释,通常只放在一个文件中,如果注释很多,通常是放在一个专用的文件里,如 doc.go

go
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv

导入包

一个包的名字和包的导入路径的最后一个字段相同,例如 gopl.io/ch2/tempconv 包的名字一般是 tempconv

go
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main

import (
    "fmt"
    "os"
    "strconv"
    "gopl.io/ch2/tempconv"
)

// tempconv.CToF

包的初始化

编译器根据依赖顺序进行初始化,也可以将初始化程序放在 init 函数里,或者使用匿名函数自执行

go
package popcount

var pc [256]byte

// 一个包可以有多个 init 函数
func init() {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
}

// 或者使用匿名函数自执行进行初始化
var pc [256]byte = func() (pc [256]byte) {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
    return
}()

// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
    return int(pc[byte(x>>(0*8))] +
        pc[byte(x>>(1*8))] +
        pc[byte(x>>(2*8))] +
        pc[byte(x>>(7*8))])
}

作用域

go
var cwd string

func init() {
    // var err error
    cwd, err := os.Getwd() // NOTE: 如果没有声明过 err 那这里就是一个声明 cwd err 的作用,而全局的 cwd 并不会被赋值
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd) //
}

基础数据类型

  • 基础类型 - 数字 字符串 布尔
  • 复合类型 - 数组 结构体
  • 引用类型 - 指针 切片 字典 函数 通道
  • 接口类型 -

整型

int int32 虽然表示的是同样大小,但仍然是 2 种不同的类型,需要转换。

-5 % 3-5 % -3 结果都是 -2 , 取决与被取模的符号,% 只用于整型。

5.0 / 4.0 的结果是 1.25,但是 5 / 4 的结果是 1(去尾), 取决与运算的类型。

^      位运算 XOR
&^     位清空(AND NOT)
go
var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2

fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}
fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}

浮点型

通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大

复数

复数类型:complex64 complex128 分别对应 float32 float64 ,函数 complex 创建一个复数

go
x := 1 + 2i
var x complex128 = complex(1, 2) // 1+2i

y := 3 + 4i

fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5" 实部
fmt.Println(imag(x*y))           // "10" 虚部

字符串

原生的字符串面值形式是 `...`,使用反引号代替双引号

UTF8 是一个将 Unicode 码点(rune)编码为字节序列的变长编码

go
import "unicode/utf8"

s := "Hello, 世界"
fmt.Println(len(s))                    // "13" 个 Unicode
fmt.Println(utf8.RuneCountInString(s)) // "9" 个 utf8

// range 循环会隐身解码
for i, r := range "Hello, 世界" {
	fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

s := "プログラム"
r := []rune(s) // 返回码点序列

fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"

字符串和字节 slice 之间可以相互转换:

go
s := "abc"
b := []byte(s)
s2 := string(b)

bytes 包还提供了 Buffer 类型 var buf bytes.Buffer

字符串的转换 strconv

Go
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"

常量

常量表达式的值在编译期计算,而不是运行期

常量可以是构成类型的一部分

Go
const IPv4Len = 4

func parseIPv4(s string) IP {
	var p [IPv4Len]byte
}

iota 常量生成器

Go
type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

// iota 会给后面的依次 +1

无类型常量

例如 math.Pi

Go
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

复合数据类型

  • 数组(同构的元素)和结构体(异构的元素)都是有固定内存大小的数据结构

  • slice 和 map 则是动态的数据结构,它们将根据需要动态增长

数组

数组 和 slice

初始化

Go
// 长度是数组类型的一部分
var q [3]int = [3]int{1, 2, 3}
q := [...]int{1, 2, 3} // ... 表示根据初始值的个数来指定数组大小

var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0" 未被赋值的元素是零值

// 索引和对应值列表的方式
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

// 定义了一个 长度为 100,最后一个元素是 -1 其他都是零值的数组
r := [...]int{99: -1}

// 数组的比较

q == r // 所有元素都相等就是 true

当数组作为函数参数传进去时,函数接收的是一个副本,而不是引用,除非显式的传入一个指针

数组的类型包含了僵化的长度信息,很少使用,更常用的时 slice

slice 切片

slice 引用了底层数组(注意引用操作,当作参数传入函数后副作用)

由三个部分构成:指针、长度、容量,lencap 函数分别返回 slice 的长度和容量

slice 的切片操作 s[i:j],其中 0 ≤ i≤ j≤ cap(s),用于创建一个新的 slice

差异:slice 初始化并没有指定长度、不可比较

只能和 nil 比较

Go
// nil 可以和 slice 相同对待
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil


s = []int{}    // len(s) == 0, s != nil

// 使用 make 函数
make([]T, len, cap) // same as make([]T, cap)[:len]

slice 实际上是一个引用了底层数组的聚合类型

Go
type IntSlice struct {
	ptr      *int
	len, cap int
}

append 函数

go
// x := []{1, 2, 3}
x = append(x, x...) // ... 表示接收变长的参数为 slice (类似解构)
// x [1, 2, 3, 1, 2, 3]

stack = x[:len(x)-1] // pop

Map

无序的 key/value 对的集合,key 必须是可比较的类型

go
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

// 相当于
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34

// 使用内置的 delete 函数
delete(ages, "alice")

// 不存在的元素返回零值
ages["bob"] = ages["bob"] + 1 // 0 + 1

age, ok := ages["bob"] // 通过元组的第二位 ok, 判断该元素是否真的存在

// map 的元素不是变量,不可取址(因为 map 会重新分配空间,地址会变)
go
for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

结构体

由零个或多个任意类型的值聚合成的实体

结构体的成员可以取址

结构体零值是每个成员都是零值

结构体成员也是要首字母大写,才能被外部包访问

go
type Employee struct {
    ID        int
    Name      string
    Address   string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

var dilbert Employee

// 成员类型不能包含自身,但可以包含指针,如
type tree struct {
    value       int
    left, right *tree
}

作为参数/返回值

go
type Point struct{ X, Y int }

func Scale(p Point, factor int) Point {
    return Point{ p.X * factor, p.Y * factor }
}

fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"

所有的函数参数都是值拷贝传入,如果要修改外部结构体成员,就要传入指针

go
// 匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel

w.X = 8            // w.Circle.Point.X = 8

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // , 号结尾
}

JSON

将结构体 slice 转为 JSON 的过程叫编组(marshaling)

go
type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}},
    {Title: "Bullitt", Year: 1968, Color: true,
        Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
    // ...
}

data, err := json.Marshal(movies) // 用 MarshalIndent 可以将返回字符串缩进

if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}

fmt.Printf("%s\n", data)
go
var titles []struct{ Title string }

// 指定结构体类型 解码 JSON 字符串
if err := json.Unmarshal(data, &titles); err != nil {
    log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"

文本和 HTML 模板

"text/template"

"html/template"

html 用于处理 html 模板

go
func main() {
	const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
	t := template.Must(template.New("escape").Parse(templ))
	var data struct {
			A string        // html 字符会被转义
			B template.HTML
	}
	data.A = "<b>Hello!</b>"
	data.B = "<b>Hello!</b>"
	if err := t.Execute(os.Stdout, data); err != nil {
        log.Fatal(err)
    }
    // <p>A: &lt;b&gt;Hello!&lt;/b&gt;</p><p>B: <b>Hello!</b></p>
}

函数

函数声明

当返回是一个无名变量或没有返回时,返回值列表的括号可以省略

go
func name(parameter-list) (result-list) {
    body
}

函数的形参是实参的拷贝,如果实参包括引用类型,如指针,slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改

go
package math

func Sin(x float64) float // 没有函数体的声明,表示该函数不是以 Go 实现的

递归

Go 语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题

多返回值

期望值, error

bare return 减少重复代码,但不利于阅读

如果一个函数将所有的返回值都显示的变量名,那么该函数的 return 语句可以省略操作数。

go
func CountWordsAndImages(url string) (words, images int, err error) {
    resp, err := http.Get(url)
    if err != nil {
        return
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    if err != nil {
        err = fmt.Errorf("parsing HTML: %s", err)
    return
    }
    words, images = countWordsAndImages(doc)
    return
}

func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

// 函数体内的每个 return 都相当于 return words, images, err

错误

error 类型可能是 nil 或者 non-nil。nil 意味着函数运行成功,non-nil 表示失败

格式化错误信息

go
doc, err := html.Parse(resp.Body)

resp.Body.Close()

if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)
}

错误重试

输出错误信息并结束程序 只应在 main 函数中使用

go
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

使用 log.Fatalf 达到上面的效果

go
// log 中的所有函数,都默认会在错误信息之前输出时间信息
if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err) // 也能退出程序
    log.Printf("ping failed: %v; networking disabled",err) // 只输出
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err) // 标准错误输出
}

文件结尾错误(EOF)

go
package io

import "errors"

var EOF = errors.New("EOF") //  EOF 错误常量

函数值

函数和其他值一样拥有类型(也是一等公民)

函数之间不可比较,不可作为 map 的 key

匿名函数

函数闭包

go
func squares() func() int {
    var x int
    // squares()
	return func() int {
		x++
		return x * x
	}
}

匿名函数的递归调用

Go
// var visitAll func(items []string)
visitAll := func(items []string) {
	visitAll(m[item]) // 如果没有先声明,编译会报错 undefined: visitAll
}

可变参数

go
// `...int` 表示接收任意个数的 `int` 类型的参数
func sum(vals ...int) int {
    total := 0
    // vals 被当作 []int 切片
    for _, val := range vals {
        total += val
    }
    return total
}

Deferred 函数

defer 在函数 A 内用 defer 关键字调用的函数 B 会在在函数 A return 后执行

defer 语句经常被用于处理成对的操作,如打开/关闭、连接/断开连接、加锁/释放锁

可以修改返回值

go
func triple(x int) (result int) {
    defer func() { result += x }() // defer 后的语句会在 函数 return 之后执行
    return double(x)
}
fmt.Println(triple(4)) // 12

Panic 异常

一般运行时错误会引起 painc 异常,程序中断运行,并立即执行在该 goroutine 中被延迟的函数(defer),随后程序崩溃并输出日志信息

输出堆栈信息

go
func main() {
    defer printStack()
    f(3)
}
// 如果发生 painc 异常
func printStack() {
    var buf [4096]byte
    n := runtime.Stack(buf[:], false) // runtime.Stack 获取异常信息
    os.Stdout.Write(buf[:n])
}
// panic 函数主动抛出异常
panic(err_info)

Recover 捕获异常

go
func Parse(input string) (s *Syntax, err error) {
    defer func() {
        // 使用内置的 recover 函数,使程序从 panic 异常中恢复
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p) // 可以把错误信息放到返回值 err 里
        }
    }()
}

方法

可以给同一个包内的任意命名类型定义方法,只要这个命名类型的底层类型不是指针或者 interface

go
type P *int
func (P) f() {} // 编译错误: invalid receiver type

type P int
func (*P) f() {} // 这样是可以的,基于指针的对象方法

方法声明

go
package geometry

import "math"

type Point struct{ X, Y float64 }

// 附加参数 p 叫做方法 Distance 的接收器,p 就取命名类型 Point 首字母即可
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X - p.X, q.Y - p.Y)
}

// 之后 Point 类型的变量都具有 Distance 方法
p := Point{1, 2}
q := Point{4, 6}

fmt.Println(Distance(p, q)) // 包的调用 geometry.Distance
fmt.Println(p.Distance(q))  // 方法调用 Point.Distance

基于指针的对象方法

这样可以在函数里使用引用,而不是副本

go
// (*Point).ScaleBy
func (p *Point) ScaleBy(factor float64) {
	p.X *= factor
	p.Y *= factor
}

r := &Point{1, 2} // 寻址
r := Point{1, 2} // 如果方法需要一个指针作为接收器,编译器会隐式 &r 去调用
r.ScaleBy(2)

fmt.Println(*r) // 取值

解引用符号 *

通过嵌入结构体来扩展类型

go
import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
	Point
	Color color.RGBA
}

var cp ColoredPoint

cp.X = 1 // cp.Point.X
cp.Point.Y = 2 // cp.Y

// 我们可以把 ColoredPoint 类型当作接收器来调用 Point 里的方法,即使 ColoredPoint 里没有声明这些方法

cp.Distance() // 调用 Point 里的方法,因为他 has a Point

当调用一个方法时,编译器会在结构体里一层一层递归找下去,如果同一级里存在同名的方法就会报错

方法值 和 方法表达式

p.Distance() 其中 p.Distance 叫做选择器,选择器返回一个方法的值

go
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
// r.Launch 作为一个方法的值传入即可
time.AfterFunc(10 * time.Second, r.Launch)

方法表达式

当 T 是一个类型时,方法表达式可能会写作 T.f 或者 (*T).f,会返回一个函数,这种函数会将其第一个参数用作接收器

go
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance
fmt.Println(distance(p, q))  // p 作为接收器
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

Bit 数组

???

封装

如果我们要封装一个对象,必须将其定义成一个 struct 同样的根据成员的首字母大小写还表示对外是否可见

接口

接口类型是对其它类型行为的抽象和概括,满足隐式实现的

接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要

接口约定

结构体用于封装数据,接口用于描述行为。

go
package io
// 当某个函数参数要求是 Writer 类型时,那么传入的变量必须有 Write 这个方法
type Writer interface {
	Write(p []byte) (n int, err error)
}

接口类型

接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例

go
package io
type Reader interface {
	Read(p []byte) (n int, err error)
}
type Closer interface {
	Close() error
}

// 内嵌
type ReadWriteCloser interface {
	Reader
	Closer
}

实现接口的条件

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口

在 T 类型的参数上调用一个*T的方法是合法的,但这仅仅是一个语法糖。如果 T 实现了一个接口,但 *T 没有实现

空接口类型,可以是任意类型,但不能对他持有的值做操作,因为它没有任何方法

go
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

标准的接口类型 flag.Value

???

接口值

具体的类型 + 那个类型的值

go
var w io.Writer // nil 接口值
w = os.Stdout   // 具体类型到接口类型的隐式转换
w = new(bytes.Buffer)
w = nil

一个不包含任何值的 nil 接口值和一个刚好包含 nil 指针的接口值是不同的

go
var buf *bytes.Buffer // nil 指针的接口值
// 此时
buf != nil

sort.Interface 接口

sort.Sort 使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定

go
package sort

type Interface interface {
	Len() int // 长度
	Less(i, j int) bool // 比较结果
	Swap(i, j int) // 交换
}

http.Handler 接口

go
type database map[string]dollars

func (db database) list(w http.ResponseWriter, req *http.Request) {
	for item, price := range db {
		fmt.Fprintf(w, "%s: %s\n", item, price)
	}
}

func main() {
	db := database{"shoes": 50, "socks": 5}
	http.HandleFunc("/list", db.list)
	http.HandleFunc("/price", db.price)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

error 接口

go
type error interface {
	Error() string
}

表达式求值

???

类型断言

x.(T) 一个类型断言检查它操作对象的动态类型是否和断言的类型匹配,如果检查成功了,类型断言的结果是 x 的动态值

go
var w io.Writer
w = os.Stdout // w 是一个动态类型
f, ok := w.(*os.File)      // success: f == os.Stdout
c, ok := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

基于类型断言区别错误类型

go
package os

func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool

通过类型断言询问行为

go
func writeString(w io.Writer, s string) (n int, err error) {
	type stringWriter interface {
		WriteString(string) (n int, err error)
	}
	// 断言 w 类型
	if sw, ok := w.(stringWriter); ok {
		return sw.WriteString(s) // avoid a copy
	}
	return w.Write([]byte(s)) // allocate temporary copy
}

类型分支

go
func sqlQuote(x interface{}) string {
	switch x := x.(type) {
		case nil:
			return "NULL"
		case int, uint:
			return fmt.Sprintf("%d", x) // x has type interface{} here.
		case bool:
			if x {
				return "TRUE"
			}
			return "FALSE"
		case string:
			return sqlQuoteString(x) // (not shown)
		default:
			panic(fmt.Sprintf("unexpected type %T: %v", x, x))
	}
}

使用了关键词 type 通过 case 匹配 x 的类型,并新创建一个变量 x 只作用于 switch 此法块

虽然 x 的类型是 interface{},但是我们把它认为是一个 int,uint,bool,string,和 nil 值的 discriminated union(可识别联合)

基于标记的 XML 解码

go
package main

import (
	"encoding/xml"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	dec := xml.NewDecoder(os.Stdin)
	var stack []string // stack of element names
	for {
		tok, err := dec.Token()
		if err == io.EOF {
			break
		} else if err != nil {
			fmt.Fprintf(os.Stderr, "xmlselect: %v\n", err)
			os.Exit(1)
		}
		switch tok := tok.(type) {
			case xml.StartElement:
				stack = append(stack, tok.Name.Local) // push
			case xml.EndElement:
				stack = stack[:len(stack)-1] // pop
			case xml.CharData:
				if containsAll(stack, os.Args[1:]) {
					fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
				}
		}
	}
}

// x 是否包含 y
func containsAll(x, y []string) bool {
	for len(y) <= len(x) {
		if len(y) == 0 {
			return true
		}
		if x[0] == y[0] {
			y = y[1:]
		}
		x = x[1:]
	}
	return false
}

Goroutines 和 Channels

两种并发手段:

goroutine 和 channel,支持 CSP(communicating sequential processes)顺序通信进程,值会在不同的运行实例(goroutine)中传递

传统并发:多线程共享内存

Goroutines

当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫它 main goroutine。新的 goroutine 会用 go 语句来创建

go
f()
go f() // go 创建一个协程

主函数返回时,所有的 goroutine 都会被直接打断,程序退出

Channels

goroutine 之间发送值信息的通信机制

和 map 类似,channel 也对应一个 make 创建的底层数据结构的引用,复制或函数传参,只是拷贝了一个 channel 引用

Go
// 一个可以发送 int 类型数据的 channel 一般写为 chan int
ch := make(chan int) // 不带缓存

ch = make(chan int, 3) // 带缓存,内部持有一个队列,队列容量是 3 个值

ch <- x  // x 是要发送的值
x = <-ch // x 接收值
<-ch     // 不使用接收结果的接收操作

close(ch) // 关闭

不带缓存的 Channels 也被称为 同步 Channels

发送操作将导致 goroutine 阻塞,直到执行了接收操作,成功传输之后才可以执行后面的语句 反之,接收操作先发生也会阻塞,直到执行了发送操作

x, y 事件并发,并不意味着同时发生,而是不能确定这两件事发生的先后顺序

Go
func main() {
	conn, err := net.Dial("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	// channel
	done := make(chan struct{})
	// 开一个 goroutine
	go func() {
		io.Copy(os.Stdout, conn)
		log.Println("done")
		done <- struct{}{} // 给 channel 发消息(通知 main goroutine)
	}()
	mustCopy(conn, os.Stdin)
	conn.Close()
	<-done // 等待接收 channel 的消息(也就是等待后台 goroutine 结束)
}

串联的 Channels(Pipeline)

Go
func main() {
	naturals := make(chan int)
	squares := make(chan int)

	go func() {
		for x := 0; x < 100; x++ {
			naturals <- x
		}
		close(naturals)
	}()

	go func() {
		for x := range naturals {
			squares <- x * x
		}
		close(squares)
	}()

	for x := range squares {
		fmt.Println(x)
	}
}

使用 range 循环依次从 channel 接收数据,当 channel 被关闭并且没有值可接收时跳出循环

当 channel 没有被引用时将会被回收器回收

单方向的 Channel

提供了单方向的 channel 类型,分别用于只发送或只接收的 channel

chan <- int 表示只接收 <- chan int 表示只发送

Go
// 只发送
func counter(out chan<- int) {
	for x := 0; x < 100; x++ {
		out <- x
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

// 只接收
func printer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	naturals := make(chan int)
  squares := make(chan int)
  // 双向 channel 传给接收 单向 channel 时,发生隐式转换
	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
}

带缓存的 Channels

Go
ch = make(chan string, 3)

fmt.Println(len(ch)) // 缓存元素个数
fmt.Println(cap(ch)) // 缓存元素容量

向缓存 Channel 的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。 如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个 goroutine 执行接收操作而释放了新的队列空间。 相反,如果 channel 是空的,接收操作将阻塞直到有另一个 goroutine 执行发送操作而向队列插入元素

就像一个生产线上的多个工位,最高效的方式是平衡各个工位的效率,而加缓存队列并不能提高整体效率。

并发的循环

go
func makeThumbnails6(filenames <-chan string) int64 {
	sizes := make(chan int64)
	var wg sync.WaitGroup // 开启的 goroutines 计数,开启一个就 +1,结束一个就 -1
	for f := range filenames {
		wg.Add(1)
		go func(f string) {
			defer wg.Done() // == wg.Add(-1)
			thumb, err := thumbnail.ImageFile(f) // f 闭包传入
			if err != nil {
				log.Println(err)
				return
			}
			info, _ := os.Stat(thumb)
			sizes <- info.Size()
		}(f)
	}

	go func() {
		wg.Wait() // 都结束后,关闭 channel
		close(sizes)
	}()

	var total int64
	for size := range sizes {
		total += size
	}
	return total
}

并发的 Web 爬虫

???

基于 select 的多路复用

每个 case 代表一个通信操作(接收 or 发送),select 会等待有能够执行的 case 时去执行。 如果有多个 case 准备就绪,select 会随机选择一个去执行,select 有一种轮询的效果

go
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
	select {
		case x := <-ch:
			fmt.Println(x)  // "0" "2" "4" "6" "8"
		case ch <- i:     // i 为基数时不会执行这个 case, 所以上面的打印值都是偶数
	}
}
go
ticker := time.NewTicker(1 * time.Second)
<-ticker.C
ticker.Stop() // 停止发送事件

并发的目录遍历

go
var verbose = flag.Bool("v", false, "show verbose progress messages")

func main() {
    var tick <-chan time.Time
    if *verbose {
        tick = time.Tick(500 * time.Millisecond) // channel 每 0.5s 发出一个值
    }
    var nfiles, nbytes int64
loop:
    // for 轮询 fileSizes 是否有发出值,及时消费
    for {
        select {
					case size, ok := <-fileSizes:
							if !ok {
									break loop // 如果没有 loop 标签,只会跳出 select,这里要跳出 for 所以在外面打了一个 loop 标签
							}
							nfiles++
							nbytes += size
					case <-tick:
							printDiskUsage(nfiles, nbytes)
        }
    }
    printDiskUsage(nfiles, nbytes)
}

并发的退出

广播机制:不要向 channel 发送值,而是用关闭一个 channel 来进行广播

go
var done = make(chan struct{})

func cancelled() bool {
    select {
			case <-done:
					return true
			default:
					return false
    }
}

cancelled() // 以此判断退出信号, 然后在 goroutine 里直接 return

goroutine 结束时,在主函数里无法确认其已经释放了所有资源, 那我们不用 return 而是调用一个 panic , 最后如果 main 是唯一剩下的 goroutine 话,他会清理掉自己的一切资源。

聊天服务

go
func main() {
	listener, err := net.Listen("tcp", "localhost:8000")
	if err != nil {
		log.Fatal(err)
	}
	go broadcaster()
	// 轮询客户端的连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn)
	}
}

type client chan<- string

var (
	entering = make(chan client) // 值为 chan 类型的 chan 类型
	leaving  = make(chan client)
	messages = make(chan string)
)

// 轮询广播
func broadcaster() {
	clients := make(map[client]bool) // chan 类型的集合
	for {
		select {
			case msg := <-messages:
				for cli := range clients {
					cli <- msg // 消息广播给所有客户端
				}
			case cli := <-entering:
				clients[cli] = true
			case cli := <-leaving:
				delete(clients, cli)
				close(cli) // 同时关闭 chan
		}
	}
}

// 处理客户端连接
func handleConn(conn net.Conn) {
	ch := make(chan string)
	go clientWriter(conn, ch)

	who := conn.RemoteAddr().String()
	ch <- "You are " + who
	messages <- who + " has arrived"
	entering <- ch // 记录客户端

	input := bufio.NewScanner(conn)
	for input.Scan() {
		messages <- who + ": " + input.Text()
	}
	leaving <- ch // 注销客户端
	messages <- who + " has left"
	conn.Close()
}

func clientWriter(conn net.Conn, ch <-chan string) {
	for msg := range ch {
		fmt.Fprintln(conn, msg)
	}
}

基于共享变量的并发

了解并发机制,解释 goroutine 和操作系统线程之间的技术上的一些区别

竞争条件

数据竞争:只要有两个 goroutine 并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争

并发并不是简单的语句交叉执行

go
var x []int
go func() { x = make([]int, 10) }()
go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!

如果 x 的指针从第一个 make 来,而长度从第二个 make 来,x 就变成了一个复合体,一个自称长度为 1000000 但实际上只有 10 个元素的 slice ,这会导致存储 999999 的位置碰撞一个遥远的内存位置

不要使用共享数据来通信;使用通信来共享数据

sync.Mutex 互斥锁

在 Lock 和 Unlock 之间的代码段中的内容 goroutine 可以随便读取或者修改,这个代码段叫做临界区

go
import "sync"

var (
	mu      sync.Mutex
	balance int
)

func Withdraw(amount int) bool {
	mu.Lock()
	defer mu.Unlock()
	deposit(-amount)
	if balance < 0 {
		deposit(amount)
		return false
	}
	return true
}

func Deposit(amount int) {
	mu.Lock()
	defer mu.Unlock()
	deposit(amount)
}

func Balance() int {
	mu.Lock()
	defer mu.Unlock()
	return balance
}

// 此函数要求 持有锁
func deposit(amount int) { balance += amount }

sync.RWMutex 读写锁

多读单写锁

go
var mu sync.RWMutex
var balance int
func Balance() int {
	mu.RLock() // 读操作可以并行执行,在临界区的共享变量没有写操作时可用
	defer mu.RUnlock()
	return balance
}

比一般的无竞争锁 mutex 慢一些

内存同步

go
var x, y int
go func() {
	x = 1 // A1
	fmt.Print("y:", y, " ") // A2
}()
go func() {
	y = 1                   // B1
	fmt.Print("x:", x, " ") // B2
}()
x:0 y:0
y:0 x:0

如果两个 goroutine 在不同的 CPU 上执行,每一个核心有自己的缓存,这样一个 goroutine 的写入对于其它 goroutine 的 Print,在主存同步之前就是不可见的了

cpu 自己的缓存 -> 同步数据到内存

sync.Once 惰性初始化

go
var loadIconsOnce sync.Once
var icons map[string]image.Image

func loadIcons() {
	icons = map[string]image.Image{
		"spades.png":   loadIcon("spades.png"),
		"hearts.png":   loadIcon("hearts.png"),
		"diamonds.png": loadIcon("diamonds.png"),
		"clubs.png":	loadIcon("clubs.png"),
	}
}

func Icon(name string) image.Image {
	loadIconsOnce.Do(loadIcons) // Do 接收一个初始化函数
	return icons[name]
}

这样能避免变量在被构建完成之前和其他 goroutine 共享该变量

竞争条件检测

使用 -race 参数,在运行期间对共享变量访问的 test

sh
go build -race

并发的非阻塞缓存

???

Goroutines 和线程

goroutine 和线程的区别实际上只是一个量的区别

一个 OS 线程都有一个固定大小的内存块(一般 2MB)来做栈,这个栈会用来存储当前正在被调用或挂起的函数的内部变量

一个 goroutine 会以一个很小的栈(一般 2kb)开始其生命周期,栈的大小会根据需要动态伸缩(最大 1GB)—— 动态栈

OS 线程会被操作系统内核调度 vs GO 运行包含了自己的调度器

Go 调度器并不是用一个硬件定时器,而是被 Go 语言建筑本身进行调度的,这种调度方式不需要进入内核的上下文,所以重新调度一个 goroutine 比调度一个线程代价要低得多

go
for {
	go fmt.Print(0)
	fmt.Print(1)
}

go 调度器使用了一个 GOMAXPROCS 变量来决定有多少个操作系统的线程同时执行 go 的代码

$ GOMAXPROCS=1 go run hacker-cliché.go
111111111111111111110000000000000000000011111...

$ GOMAXPROCS=2 go run hacker-cliché.go
010101010101010101011001100101011010010100110...

OS 线程有一个 id 用于获取到值,Goroutine 没有 ID 号

包和工具

导入路径

Go
import (
	"fmt" // 1. 全局唯一导入路径
	"math/rand" // 2. 包名就是路径的最后一段
	crand "crypto/rand" // 3. 指定别名 crand 避免冲突

	"golang.org/x/net/html" // 3. 建议非标准库以组织互联网域名为前缀
	"gopkg.in/yaml.v2" // 4. 包名不包含 v2 版本后缀,所以包名是 yaml
	"github.com/go-sql-driver/mysql"
)

导入路径

每个 Go 语言源文件的开头都必须有包声明语句

例如 math/rand 包的每个源文件的开头都包含 package rand 包声明语句,然后就可以使用 rand.Int()

go
func main() {
	fmt.Println(rand.Int())
}

_. 开头的源文件会被构建工具忽略

main 包是给 go build构建

_test 为后缀包名的测试外部扩展包 都由 go test 命令独立编译

包的匿名导入

go
import (
	"image"
	_ "image/jpeg"
	_ "image/png" // 注册 PNG decoder
)

image 包拆分出了功能包,按需导入。

go
package png // image/png

func Decode(r io.Reader) (image.Image, error)
func DecodeConfig(r io.Reader) (image.Config, error)

// 注册
func init() {
	const pngHeader = "\x89PNG\r\n\x1a\n"
	image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

工具

	build            compile packages and dependencies
	clean            remove object files
	doc              show documentation for package or symbol
	env              print Go environment information
	fmt              run gofmt on package sources
	get              download and install packages and dependencies
	install          compile and install packages and dependencies
	list             list packages
	run              compile and run Go program
	test             test packages
	version          print Go version
	vet              run go tool vet on packages

工作区结构

下载包

go get 命令获取的代码是真实的本地存储仓库,可以使用版本管理工具切换到其他版本,例如 golang.org/x/net 包目录对应一个 Git 仓库

指定 -u 参数,将拉取所有的包和依赖包,然后重新编译和安装他们

使用 vendor 目录用于存储依赖包的固定版本的源代码

构建包

go build 编译参数指定的包和它依赖的包,如果包是库则忽略输出结果,如果是 main 则调用连接器在当前目录创建一个可执行程序

每个目录只包含一个包

$ cd $GOPATH/src/gopl.io/ch1/helloworld
$ go build

绝对路径

$ cd anywhere
$ go build gopl.io/ch1/helloworld

相对路径

$ cd $GOPATH
$ go build ./src/gopl.io/ch1/helloworld

go run 命令结合了构建运行

go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃

被编译的包会被保存到$GOPATH/pkg目录下,目录路径和src目录路径对应,可执行程序被保存到$GOPATH/bin目录

go build -i命令将安装每个目标所依赖的包

构建约束

在包声明和包注释的前面,该构建注释参数告诉go build只在编译程序对应的目标操作系统是 Linux 或 Mac OS X 时才编译这个文件

Go
// +build linux darwin

表示不编译这个文件:

Go
// +build ignore

包文档

go doc 打印指定实体的注释与文档注释,实体可以是一个包 or 包的成员

sh
go doc time

go doc time.Since

在线文档 https://godoc.org/fmt

内部包

一个 internal 包只能被和 internal 目录有同一个父目录的包所导入

例如 net/http/internal/chunked 内部包只能被 net/http/httputil 或 net/http 包导入,但是不能被 net/url 包导入

net/url
net/http
net/http/httputil
net/http/internal/chunked

查询包

go list 查询包的信息,打印导入路径

go list ... 列出工作区的所有包

go list file/... 指定目录下的包

go list ...file... 相关的包(类似关键词搜索)

go list -json hash 以 json 展示包的元信息

测试

*_test.go build 时不会被构建,包含:测试函数、基准测试(benchmark)函数、示例函数

测试函数:以 Test 为函数名前缀,go test 会调用测试函数并报告测试结果 PASS 或 FAIL

基准测试:以 Benchmark 为函数名前缀,go test 会多次运行基准测试函数以计算一个平均执行时间

示例函数:以 Example 为函数名前缀,提供一个由编译器保证正确性的示例文档

Go
// Package word provides utilities for word games.
package word

func IsPalindrome(s string) bool {
	for i := range s {
		if s[i] != s[len(s)-1-i] {
			return false
		}
	}
	return true
}

在相同目录下创建 word_test.go 测试文件

Go
package word

// 引入 testing 包
import "testing"

func TestPalindrome(t *testing.T) {
	if !IsPalindrome("detartrated") {
		// 报告失败信息
		t.Error(`IsPalindrome("detartrated") = false`)
	}
	if !IsPalindrome("kayak") {
		t.Error(`IsPalindrome("kayak") = false`)
	}
}

func TestNonPalindrome(t *testing.T) {
	if IsPalindrome("palindrome") {
		t.Error(`IsPalindrome("palindrome") = true`)
	}
}

go test -v 打印每个测试函数名和运行时间

go test -v -run="French|Canal" run 使用正则匹配需要测试的函数

随机测试

白盒测试

黑盒测试:只测试包公开的文档和 api

白盒测试:可以操作访问包内部函数和数据(测试函数注意保存和恢复全局变量)

外部测试包

测试覆盖率

在测试中至少被运行一次的代码占总代码数的比例

基准测试

Go
import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
	for i := 0; i < b.N; i++ {
		IsPalindrome("A man, a plan, a canal: Panama")
	}
}

go test -bench=. 运行基准测试

go test -bench=. -benchmem 打印内存使用报告

剖析

go test -cpuprofile=cpu.out CPU 剖析数据 - 标识了最耗 CPU 时间的函数

go test -blockprofile=block.out 堆剖析 - 标识了最耗内存的语句

go test -memprofile=mem.out 阻塞剖析 - 记录阻塞 goroutine 最久的操作

go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http

-run=NONE 禁止基准测试

分析日志 go tool pprof -text -nodecount=10 ./http.test cpu.log

-text 指定输出格式

示例函数

以 Example 开头,没有参数和返回值,可作为文档,是真实的 go 代码

go test 会执行示例函数,并检查输出与注释Output是否一致

Go
func ExampleIsPalindrome() {
	fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
	fmt.Println(IsPalindrome("palindrome"))
	// Output:
	// true
	// false
}

反射

很少需要用到

reflect 能在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时知道这些变量的类型

反射是一个复杂的内省技术,不应随意使用

golang
type User struct {
  ID int
  Name string
}

func reflectTest(u User) {
  t := reflect.TypeOf(u) // 获取类型对象
  fmt.Println(t.Kind()) // struct
  
  v := reflect.ValueOf(u) // 获取值对象
  fmt.Println(v.FieldByName("ID").Int()) // 获取结构体成员 
  v.FieldByName("Name").SetString("new name") // 修改成员值
}

底层编程

很少需要用到

unsafe