Skip to content

Golang 学习笔记 ​

#Golang · #基础 · #入门 · #gopl

基于《The Go Programming Language》(gopl.io, Alan Donovan & Brian Kernighan) 全书 13 章整理。 代码示例来源于 code/gopl.io 官方配套源码。


第1章:入门教程 ​

本章通过几个实用的程序快速展示 Go 语言的面貌——从最简单的文件处理到图像生成、再到并发的 Web 客户端和服务端。学习新语言时容易按照熟悉的套路来写,但请尽量用"Go 的方式"来思考。

1.1 Hello, World ​

Go 是编译型语言,所有可执行程序必须从 package main 和 func main() 开始。import 导入依赖包,fmt 是格式化 I/O 的基础包。

go
package main
import "fmt"
func main() {
    fmt.Println("Hello, 世界")
}

1.2 命令行参数 — echo ​

os.Args 是字符串切片,os.Args[0] 是命令本身,os.Args[1:] 是参数。三种实现展示了 Go 的迭代演化思想:

go
// 方式一:传统 for 循环
for i := 1; i < len(os.Args); i++ {
    s += sep + os.Args[i]
    sep = " "
}
// 方式二:for range 遍历
for _, arg := range os.Args[1:] {
    s += sep + arg
    sep = " "
}
// 方式三:strings.Join 最高效
fmt.Println(strings.Join(os.Args[1:], " "))

关键点:_ 是空白标识符用于丢弃不需要的值。字符串拼接在循环中效率低下(每次创建新字符串),strings.Join 预先计算内存,性能最好。

1.3 统计重复行 — dup ​

用 map 统计输入中每行出现的次数。bufio.Scanner 是读取输入的标准方式:

go
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
    counts[input.Text()]++  // key 不存在时,value 自动初始化为零值 0
}

自定义 Split 函数实现按单词或按逗号拆分:

go
// 按单词分割
scanner.Split(bufio.ScanWords)

// 自定义逗号拆分函数
onComma := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    for i := 0; i < len(data); i++ {
        if data[i] == ',' {
            return i + 1, data[:i], nil
        }
    }
    if !atEOF { return 0, nil, nil }
    return 0, data, bufio.ErrFinalToken
}
scanner.Split(onComma)

关键点:make 用于创建 map、slice、chan 三种引用类型。map 中不存在的 key 返回对应类型的零值。new 创建任意类型并返回指针,make 只用于引用类型。

1.4 GIF 动画 — lissajous ​

go
var palette = []color.Color{
    color.RGBA{255,255,255,math.MaxUint8},
    color.Black,
    color.RGBA{0,0,128,math.MaxUint8},
}
const (cycles = 5; res = 0.001; size = 100; nframes = 64; delay = 8)
anim := gif.GIF{LoopCount: nframes}
for i := 0; i < nframes; i++ {
    img := image.NewPaletted(image.Rect(0, 0, 2*size+1, 2*size+1), palette)
    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), uint8(c))
    }
    phase += 0.1
    anim.Delay = append(anim.Delay, delay)
    anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim)

1.5 获取 URL — fetch ​

go
resp, err := http.Get(url)
if err != nil { /* 处理错误 */ }
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()  // 必须关闭,避免资源泄露

关键点:Go 的错误处理是显式的——每个可能出错的操作都返回 error。

1.6 并发获取多个 URL — fetchall ​

goroutine + channel 首次登场:

go
ch := make(chan string)
for _, url := range os.Args[1:] {
    go fetch(url, ch)     // 启动 goroutine 并发抓取
}
for range os.Args[1:] {
    fmt.Println(<-ch)     // 从 channel 接收结果
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())

关键点:go 关键字启动 goroutine。channel 是 goroutine 之间通信的管道——"不要通过共享内存来通信,而要通过通信来共享内存"。

1.7 Web 服务器 — server ​

go
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))

log.Fatal 在打印日志后调用 os.Exit(1)。


第2章:程序结构 ​

2.1 命名 ​

  • 25 个关键字:break, case, chan, const, continue, default, defer, else, fallthrough, for, func, go, goto, if, import, interface, map, package, range, return, select, struct, switch, type, var
  • 导出规则:大写字母开头的标识符对外可见(exported),小写字母开头的包内私有
  • 命名风格:驼峰式(parseRequestLine),缩略词全大写(HTTPServer)

关键点:Go 没有 public/private/protected 关键字,仅靠大小写控制可见性。

2.2 声明 ​

四种主要声明:var(变量)、const(常量)、type(类型)、func(函数)。

go
const boilingF = 212.0          // 包级别常量
func main() { ... }              // 函数
type Celsius float64             // 类型声明

2.3 变量 ​

var 声明语句创建一个特定类型的变量。可以省略类型(由表达式推导)或省略表达式(用零值初始化)。

零值机制:数值 0、布尔 false、字符串 ""、引用类型(slice/指针/map/chan/函数)nil。Go 中不存在未初始化的变量,这个特性简化了大量边界条件处理。包级别变量在 main 执行前初始化,局部变量在执行到声明语句时初始化。

短变量声明 := 是函数内最常用的声明方式,类型由表达式自动推导:

go
anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0

var 形式一般用于:需要显式指定类型、变量稍后会被重新赋值而初始值不重要、或包级别声明。

go
i := 100                  // int
var boiling float64 = 100 // float64
var names []string
var err error

:= vs =::= 是声明语句,= 是赋值操作。多重赋值 i, j = j, i 可交换值。

短变量声明的微妙之处:左边的变量不必全是新声明的,已存在的变量只做赋值:

go
in, err := os.Open(infile)  // 声明 in + err
out, err := os.Create(outfile) // 声明 out,对 err 赋值(必须至少有一个新变量)

注意:短变量声明只在同级词法域内等价于赋值。如果变量在外部作用域声明,:= 会在当前域重新声明一个新变量(造成变量遮蔽)。

指针:& 取地址,* 解引用。Go 的指针不能做算术运算。

go
x := 1
p := &x         // p 是 *int 类型,指向 x
*p = 2          // 等价于 x = 2

// Go 中返回局部变量地址是安全的——变量会逃逸到堆上
func f() *int {
    v := 1
    return &v  // 安全!v 逃逸到堆
}

任何类型的指针零值都是 nil。指针可比较:指向同一变量或都为 nil 时相等。

new vs make:new(T) 为 T 分配零值内存并返回 *T;make 只用于 slice/map/chan,返回初始化后的 T。

2.4 赋值 ​

go
x, y = y, x                  // 直接交换(不需要临时变量)
_, err = io.Copy(dst, src)   // 丢弃不需要的返回值

2.5 类型声明 — 以温度转换为例 ​

go
type Celsius float64
type Fahrenheit float64

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)

func (c Celsius) String() string    { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
func CToF(c Celsius) Fahrenheit { return Fahrenheit(Round(float64(c*9/5+32), 2)) }
func FToC(f Fahrenheit) Celsius { return Celsius(Round(float64((f-32)*5/9), 2)) }

关键点:Celsius 和 float64 是不同类型,不能直接混用(需要显式转换)。方法让类型拥有了行为,这是 Go 面向对象的基础。

2.6 包和文件 ​

  • 包由同一目录下的 .go 文件组成
  • 包的初始化顺序:先初始化包级变量,再按声明顺序调用 init() 函数
  • import 未使用的包会编译报错

2.7 作用域 ​

Go 是词法作用域。注意区分声明作用域(编译期)和生命周期(运行期)。if/for/switch 可带初始化语句,变量作用域仅限于语句内。


第3章:基本数据类型 ​

3.1 整型 ​

类型范围
int8-128 ~ 127
int16-32768 ~ 32767
int32-2^31 ~ 2^31-1
int64-2^63 ~ 2^63-1
uint80 ~ 255(byte 是其别名)
uint160 ~ 65535
uint320 ~ 2^32-1(rune 是 int32 别名,表示 Unicode 码点)
uint640 ~ 2^64-1
  • int / uint 大小取决于平台(32/64位)
  • uintptr 大小足够容纳任意指针
  • 位运算:& | ^ &^(位清空,Go 独有) << >>

PopCount — 查表法统计 uint64 中 1 的个数,O(1):

go
var pc [256]byte
func init() {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
}
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>>(3*8))] +
        pc[byte(x>>(4*8))] + pc[byte(x>>(5*8))] +
        pc[byte(x>>(6*8))] + pc[byte(x>>(7*8))])
}

3.2 浮点数与复数 ​

  • float32 约6位精度,float64 约15位精度——推荐默认用 float64
  • complex64 / complex128 —— Go 原生支持复数

Mandelbrot 分形图:

go
func mandelbrot(z complex128) color.Color {
    const iterations = 200; const contrast = 15
    var v complex128
    for n := uint8(0); n < iterations; n++ {
        v = v*v + z
        if cmplx.Abs(v) > 2 {
            return color.Gray{255 - contrast*n}
        }
    }
    return color.Black
}

3.3 字符串 ​

Go 字符串是不可变的字节序列。len(s) 返回字节数(不是字符数),s[i] 返回第 i 个字节(不是第 i 个字符)。

go
// 提取文件名(去掉路径和扩展名)
func basename(s string) string {
    for i := len(s) - 1; i >= 0; i-- {
        if s[i] == '/' { s = s[i+1:]; break }
    }
    for i := len(s) - 1; i >= 0; i-- {
        if s[i] == '.' { s = s[:i]; break }
    }
    return s
}

// 数字千分位格式化 — 递归实现
func comma(s string) string {
    n := len(s)
    if n <= 3 { return s }
    return comma(s[:n-3]) + "," + s[n-3:]
}
// comma("1234567") → "1,234,567"

常用包:strings(Contains, Count, Fields, HasPrefix, Index, Join, Split, ToLower, Trim)、strconv(Atoi, Itoa, FormatInt, ParseFloat)、bytes、unicode。

3.4 布尔型 ​

go
// bool 类型只有两个值:true 和 false
// 不能隐式转换为数值,不能与 0/1 混用
func isEven(n int) bool {
    return n%2 == 0
}

3.5 常量 ​

常量在编译期计算,可以无类型(untyped)。iota 是常量生成器:

go
const (
    FlagUp Flags = 1 << iota  // 1  (iota=0)
    FlagBroadcast             // 2  (iota=1)
    FlagLoopback              // 4  (iota=2)
    FlagPointToPoint          // 8  (iota=3)
)

第4章:复合数据类型 ​

4.1 数组 ​

go
var a [3]int                // 长度是类型一部分,[3]int 和 [4]int 是不同类型
q := [...]int{1, 2, 3}      // 自动推断长度
r := [...]int{99: -1}       // 100个元素,最后一个为-1

关键点:Go 中很少直接用数组,几乎总是用 slice。

4.2 Slice(切片) ​

每个 slice 包含三个字段:指针(底层数组)、长度(len)、容量(cap)。

go
s := []int{1, 2, 3, 4, 5}
s[1:3]  // [2, 3],半开区间,cap 从起始位置到数组末尾

// append 自动扩容(小于1024时翻倍,之后增长25%)
var runes []rune
for _, r := range "Hello, 世界" { runes = append(runes, r) }

// 原地去空字符串——共享底层数组的技巧
func nonempty(strings []string) []string {
    out := strings[:0]  // 零长度,保留底层数组
    for _, s := range strings {
        if s != "" { out = append(out, s) }
    }
    return out
}

// 原地反转
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}

关键点:append 可能重新分配底层数组。两个 slice 可能共享底层数组,修改一个会影响另一个。用 copy 做深拷贝。

4.3 Map ​

map 是无序的键值对集合,迭代顺序是随机的(故意为之,防止依赖顺序)。零值是 nil(不能存放元素,需用 make 初始化)。

go
ages := make(map[string]int)
ages["alice"] = 31

age, ok := ages["bob"]   // ok == false 表示 key 不存在
delete(ages, "alice")    // 安全删除,即使 key 不存在也不报错

关键点:不能对 map 中的元素取地址(扩容时地址会变)。

4.4 Struct(结构体) ​

go
type Employee struct {
    ID        int
    Name      string
    Salary    int
    ManagerID int
}
dilbert.Salary -= 5000  // 字段访问

// 结构体字面量
p := Point{1, 2}          // 按顺序(不推荐)
p := Point{X: 1, Y: 2}    // 按名称(推荐)

结构体比较:如果所有字段都是可比较的,则 struct 可用 == 比较。

结构体嵌入(匿名成员):让一个类型直接获得另一个类型的方法——这是 Go 的"继承"机制。

go
type Point struct{ X, Y float64 }
type ColoredPoint struct {
    Point                   // 匿名嵌入,ColoredPoint 自动拥有 Point 的方法
    Color color.RGBA
}

red := color.RGBA{255, 0, 0, 255}
p := ColoredPoint{Point{1, 1}, red}
fmt.Println(p.Distance(Point{5, 4})) // 可直接调用 Point 的方法

关键点:匿名嵌入是组合(has-a),不是继承(is-a)。接受 Point 参数的函数不能直接接受 ColoredPoint。

4.5 JSON ​

go
type Issue struct {
    Number    int       `json:"number"`
    HTMLURL   string    `json:"html_url"`
    Title     string    `json:"title"`
    State     string    `json:"state"`
    CreatedAt time.Time `json:"created_at"`
}

// Marshal: 结构体 → JSON
data, _ := json.MarshalIndent(issues, "", "  ")

// Unmarshal: JSON → 结构体
var result IssuesSearchResult
json.Unmarshal(data, &result)

关键点:struct tag 控制编解码行为。只有导出的大写字段才会被编解码。omitempty 表示零值时忽略该字段。

4.6 文本和 HTML 模板 ​

text/template — 文本模板:

go
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`

// 自定义模板函数
func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
}
report := template.Must(template.New("issuelist").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ))
report.Execute(os.Stdout, result)

html/template — HTML 模板(自动转义防 XSS):

go
var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
{{range .Items}}
<tr>
  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
  <td>{{.State}}</td>
  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
</tr>
{{end}}
</table>`))

关键区别:text/template 用于纯文本,html/template 自动对 HTML/JS/CSS/URL 做转义,防止注入攻击。

4.7 SHA256 散列 ​

go
c1 := sha256.Sum256([]byte("x"))
c2 := sha256.Sum256([]byte("X"))
fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1)
// 输出两个完全不同的 256bit 散列值

第5章:函数 ​

5.1 函数声明 ​

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

Go 函数是一等公民:可以赋值给变量、作为参数传递、作为返回值。

5.2 递归 — 遍历 HTML DOM 树 ​

go
func visit(links []string, n *html.Node) []string {
    if n.Type == html.ElementNode && n.Data == "a" {
        for _, a := range n.Attr {
            if a.Key == "href" { links = append(links, a.Val) }
        }
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        links = visit(links, c)
    }
    return links
}

遍历 DOM 树并打印结构(outline):

go
func outline(stack []string, n *html.Node) {
    if n.Type == html.ElementNode {
        stack = append(stack, n.Data)  // push tag
        fmt.Println(stack)
    }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        outline(stack, c)
    }
}

forEachNode 通用遍历器:

go
func forEachNode(n *html.Node, pre, post func(n *html.Node)) {
    if pre != nil { pre(n) }
    for c := n.FirstChild; c != nil; c = c.NextSibling {
        forEachNode(c, pre, post)
    }
    if post != nil { post(n) }
}

关键点:goroutine 栈初始仅 2KB 但可按需增长,递归在 Go 中比固定栈大小的语言安全得多。

5.3 多返回值与错误处理 ​

go
func findLinks(url string) ([]string, error) {
    resp, err := http.Get(url)
    if err != nil { return nil, err }
    // ...
    return visit(nil, doc), nil
}

错误处理策略演进 — title 函数三版对比:

go
// title1: 最原始——手动关闭,出错时容易遗漏 Close
func title(url string) error {
    resp, err := http.Get(url)
    if err != nil { return err }
    ct := resp.Header.Get("Content-Type")
    if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
        resp.Body.Close()  // 别忘!
        return fmt.Errorf("%s has type %s, not text/html", url, ct)
    }
    doc, err := html.Parse(resp.Body)
    resp.Body.Close()
    // ...
}

// title2: 用 defer 简化——无论怎样返回都会 Close
func title(url string) error {
    resp, err := http.Get(url)
    if err != nil { return err }
    defer resp.Body.Close()  // ← 关键差异
    ct := resp.Header.Get("Content-Type")
    if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
        return fmt.Errorf(...)  // 不需要手动 Close了
    }
    // ...
}

// title3: 用 panic+recover 处理"应该有恰好一个 title"的约束
func soleTitle(doc *html.Node) (title string, err error) {
    type bailout struct{}
    defer func() {
        switch p := recover(); p {
        case bailout{}: err = fmt.Errorf("multiple title elements")
        default: panic(p)  // 非预期 panic,继续传播
        }
    }()
    forEachNode(doc, func(n *html.Node) {
        if n.Type == html.ElementNode && n.Data == "title" {
            if title != "" { panic(bailout{}) }
            title = n.FirstChild.Data
        }
    }, nil)
    return
}

关键点:Go 用多返回值处理错误而非异常——这是最核心的设计哲学。

5.4 函数变量与闭包 ​

go
func squares() func() int {
    var x int
    return func() int { x++; return x * x }
}
f := squares()
f() // 1
f() // 4
f() // 9  —— x 在闭包中"逃逸"到堆上

5.5 匿名函数与拓扑排序 ​

go
var prereqs = map[string][]string{
    "algorithms": {"data structures"},
    "compilers":  {"data structures", "formal languages"},
    // ...
}

func topoSort(m map[string][]string) []string {
    var order []string
    seen := make(map[string]bool)
    var visitAll func(items []string)
    visitAll = func(items []string) {
        for _, item := range items {
            if !seen[item] {
                seen[item] = true
                visitAll(m[item])             // 先处理前置课程
                order = append(order, item)   // 再输出本课程
            }
        }
    }
    for _, items := range m { visitAll(items) }
    return order
}

关键点:匿名函数需要递归时,必须先声明 var visitAll func(...) 再赋值。

5.6 可变参数 ​

go
// ... 定义可变参数,函数内部是 slice
func sum(vals ...int) int {
    total := 0
    for _, val := range vals { total += val }
    return total
}
sum()           // 0
sum(3)          // 3
sum(1, 2, 3, 4) // 10

// 将 slice 展开为可变参数
values := []int{1, 2, 3, 4}
sum(values...)  // 10

5.7 Defer ​

defer 在函数返回前执行,多个 defer 按后进先出顺序执行:

go
func title(url string) error {
    resp, err := http.Get(url)
    if err != nil { return err }
    defer resp.Body.Close()   // return 之前一定会执行
    // ...
}

defer 追踪函数调用链:

go
func bigSlowOperation() {
    defer trace("bigSlowOperation")()  // trace()在defer语句时调用
    //                                    返回的func()在退出时调用
    time.Sleep(10 * time.Second)
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { log.Printf("exit %s (%s)", msg, time.Since(start)) }
}
// 输出:
// enter bigSlowOperation
// exit bigSlowOperation (10.000589217s)

关键点:defer 的参数在声明时求值(不是执行时)。适合:关闭文件、释放锁、记录耗时。避免在循环中使用 defer(会累积到函数结束才执行)。

5.8 Panic 与 Recover ​

  • panic:程序遇到无法恢复的错误时调用,逐层向上传播
  • recover:只能在 defer 中捕获 panic,让程序恢复正常执行
go
func Parse(input string) (s *Syntax, err error) {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Errorf("internal error: %v", p)
        }
    }()
    // ... 正常解析逻辑,发生 panic 时被 recover 捕获
}

关键点:不要滥用 panic,Go 的惯例是返回 error。panic 仅用于真正不可恢复的错误。


第6章:方法 ​

6.1 方法声明 ​

方法声明和函数声明类似,只是在 func 和方法名之间多了接收器(receiver)参数。这个附加参数将函数附加到该类型上:

go
type Point struct{ X, Y float64 }

// 普通函数
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// 方法 —— 接收器放在 func 和方法名之间
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

接收器命名建议使用类型首字母(如 Point → p),保持简短和一致。Go 不使用 this 或 self。

方法调用的选择器:p.Distance(q) 中的 p.Distance 是选择器(selector),编译器根据接收器类型自动选择正确的方法。方法和字段在同一命名空间,不能同名。

可以为任意命名类型定义方法(只要底层类型不是指针或接口),不仅仅是 struct:

go
type Path []Point  // 命名 slice 类型

func (path Path) Distance() float64 {
    sum := 0.0
    for i := range path {
        if i > 0 { sum += path[i-1].Distance(path[i]) }
    }
    return sum
}

perim := Path{{1, 1}, {5, 1}, {5, 4}, {1, 1}}
fmt.Println(perim.Distance()) // "12"

同一个包内,不同类型可以有同名方法(Point.Distance 和 Path.Distance 互不冲突)。方法名可以短,因为在包外调用时无需加类型名前缀。

6.2 指针接收者 vs 值接收者 ​

go
func (p *Point) ScaleBy(factor float64) { p.X *= factor } // 修改原始值
func (p Point) ScaleBy(factor float64)  { p.X *= factor } // 修改副本!

// 方法值(绑定了接收者)
distanceFromP := p.Distance  // p.Distance 是方法值
distanceFromP(q)             // 直接调用

// 方法表达式(需要显式传接收者)
distance := Point.Distance   // 方法表达式
distance(p, q)               // 显式传入接收者
scale := (*Point).ScaleBy
scale(&p, 2)

关键原则:不要在同一个类型上混用值接收者和指针接收者。

6.3 结构体嵌入与方法提升 ​

go
type ColoredPoint struct {
    Point                   // 匿名嵌入
    Color color.RGBA
}
// ColoredPoint 自动"继承"Point 的所有方法
p := ColoredPoint{Point{1, 1}, red}
p.Distance(q)   // OK! 方法被提升
p.ScaleBy(2)    // OK! 指针方法也被提升

// 也可以嵌入指针
type ColoredPoint struct {
    *Point                  // 嵌入指针
    Color color.RGBA
}

6.4 封装 — IntSet 位向量 ​

go
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) String() string {
    var buf bytes.Buffer
    buf.WriteByte('{')
    for i, word := range s.words {
        for j := 0; j < 64; j++ {
            if word&(1<<uint(j)) != 0 {
                fmt.Fprintf(&buf, "%d", 64*i+j)
            }
        }
    }
    buf.WriteByte('}')
    return buf.String()
}

关键点:小写字段和方法对外不可见。这是 Go 封装的全部机制。


第7章:接口 ​

接口是 Go 中最重要的抽象机制——让不同类型通过共同的行为关联起来。

7.1 接口即约定(隐式实现) ​

到目前为止我们看到的都是具体类型——它们精确描述所代表的值,并展示对类型本身的操作。Go 中还存在另一种类型:接口类型。接口类型是抽象的——它不暴露内部结构和基础操作,只表现出自己的方法。看到接口类型时,你不知道它是什么,只知道可以通过它的方法做什么。

fmt.Printf 写入标准输出,fmt.Sprintf 返回字符串。它们都基于同一个核心——fmt.Fprintf,而 Fprintf 的第一个参数不是文件,是 io.Writer 接口:

go
package io
type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer 定义了 Fprintf 和调用者之间的约定:

  • 调用方提供 Write 方法的类型(如 *os.File、*bytes.Buffer)
  • Fprintf 只依赖 Write 方法,不关心是写入文件还是内存

一个类型可以自由地被满足相同接口的类型替换——这就是可替换性(里氏替换原则),面向对象的核心理念。

go
type Writer interface {
    Write(p []byte) (n int, err error)
}

// ByteCounter 自动实现了 Writer——只需实现 Write 方法
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
    *c += ByteCounter(len(p))
    return len(p), nil
}
var c ByteCounter
c.Write([]byte("hello"))                    // 直接调用
fmt.Fprintf(&c, "hello, %s", "Dolly")       // Fprintf 不关心 c 是什么类型
fmt.Println(c) // "12" = len("hello, Dolly")

另一个重要接口 fmt.Stringer——只要定义 String() 方法就可自定义打印格式:

go
package fmt
type Stringer interface {
    String() string
}

核心哲学:"接口是调用方定义的,不是实现方定义的。"调用方只要声明需要什么方法,任何实现了这些方法的类型都可以使用。

7.2 接口值 ​

接口值由两部分组成:动态类型 + 动态值。零值接口的 type 和 value 都是 nil。当接口值的动态值是指针时,接口本身可以是 nil 但动态指针不为 nil——这是 Go 的经典陷阱。

7.3 sort.Interface ​

go
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

type byArtist []*Track
func (x byArtist) Len() int           { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

sort.Sort(byArtist(tracks))
sort.Sort(sort.Reverse(byArtist(tracks)))  // 逆序

// 多字段自定义排序
type customSort struct {
    t    []*Track
    less func(x, y *Track) bool
}
func (x customSort) Len() int           { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int)      { x.t[i], x.t[j] = x.t[j], x.t[i] }

7.4 http.Handler 接口 — 三种写法演进 ​

go
// 方式一:直接实现 ServeHTTP(所有路径自己处理)
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/list":
        for item, price := range db { fmt.Fprintf(w, "%s: %s\n", item, price) }
    case "/price":
        item := req.URL.Query().Get("item")
        if price, ok := db[item]; ok { fmt.Fprintf(w, "%s\n", price)
        } else { w.WriteHeader(http.StatusNotFound) }
    default:
        w.WriteHeader(http.StatusNotFound)
    }
}
log.Fatal(http.ListenAndServe("localhost:8000", db))

// 方式二:通过 ServeMux 路由
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))

// 方式三:直接使用全局 HandleFunc(最简洁)
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))

7.5 flag.Value 接口 — 自定义命令行参数 ​

go
// flag.Value 接口
type Value interface {
    String() string
    Set(string) error
}

// 实现后即可用 flag.Var 定义自定义命令行参数
var period = flag.Duration("period", 1*time.Second, "sleep period")
// time.Duration 实现了 flag.Value 接口

自定义 CelsiusFlag(实现了 flag.Value 接口后,可直接用 flag.Var):

go
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")
func main() { flag.Parse(); fmt.Println(*temp) }

7.6 接口值模型与 error 接口 ​

error 只是一个普通的接口:

go
type error interface {
    Error() string
}

使用 fmt.Errorf 构造 error,用 errors.Is / errors.As 做错误链判断。

7.7 XML 解析 — Decoder Token API ​

go
dec := xml.NewDecoder(os.Stdin)
var stack []string
for {
    tok, err := dec.Token()
    if err == io.EOF { break }
    switch tok := tok.(type) {  // 类型 switch!
    case xml.StartElement:
        stack = append(stack, tok.Name.Local)
    case xml.EndElement:
        stack = stack[:len(stack)-1]
    case xml.CharData:
        if containsAll(stack, os.Args[1:]) {
            fmt.Printf("%s: %s\n", strings.Join(stack, " "), tok)
        }
    }
}

7.8 类型断言与类型 switch ​

go
// 安全断言(ok 模式,不 panic)
if f, ok := w.(*os.File); ok { /* w 是 *os.File */ }

// 类型 switch
func sqlQuote(x interface{}) string {
    switch x := x.(type) {
    case nil:       return "NULL"
    case int, uint: return fmt.Sprintf("%d", x)
    case bool:      return fmt.Sprintf("%t", x)
    case string:    return sqlQuoteString(x)
    default:        panic(fmt.Sprintf("unexpected type %T", x))
    }
}

关键点:interface{} / any 是空接口,所有类型都实现了它。但要慎用——它放弃了类型安全。


第8章:Goroutine 与 Channel ​

"不要通过共享内存来通信,而要通过通信来共享内存。"——这是 Go 并发编程的核心哲学。

8.1 Goroutine ​

在 Go 中,每一个并发执行的活动称为 goroutine。程序启动时,main 函数运行在主 goroutine 中。用 go 关键字创建新的 goroutine:

go
go spinner(100 * time.Millisecond) // 启动旋转动画——新 goroutine
fibN := fib(45)                    // 同步计算斐波那契——当前 goroutine
// 两个操作并发执行,spinner 在等待时不阻塞 fib

goroutine 初始栈仅 2KB,可按需增长,可以轻松创建成千上万个。与线程不同,goroutine 的创建和切换开销极低(用户态切换,纳秒级)。

当主 goroutine 结束时(main 返回),所有 goroutine 都会被暴力终止——程序直接退出。因此需要等待机制(如 channel、sync.WaitGroup)来确保其他 goroutine 完成。

GMP 调度模型:G(goroutine)→ P(processor,逻辑处理器)→ M(machine,OS 线程)。P 的数量由 GOMAXPROCS 决定,默认等于 CPU 核数。详见 [GMP 调度模型]。

8.2 Channel 基础 ​

go
ch := make(chan int)      // 无缓冲 channel(同步)
ch := make(chan int, 3)   // 有缓冲 channel(容量为 3)

ch <- x   // 发送(缓冲区满时阻塞)
x = <-ch  // 接收(缓冲区空时阻塞)
close(ch) // 关闭(关闭后不能再发送,可继续接收至排空)

关键点:从关闭且排空的 channel 接收得到零值。用 x, ok := <-ch 判断是否已关闭。

8.3 并发管道(Pipeline) ​

go
naturals := make(chan int)
squares  := make(chan int)

go func() {                         // 阶段1:生成序列
    for x := 0; x < 100; x++ { naturals <- x }
    close(naturals)
}()
go func() { for x := range naturals { squares <- x * x }; close(squares) }()  // 阶段2
for x := range squares { fmt.Println(x) }  // 阶段3:range 自动检测 channel 关闭

8.4 单向 Channel ​

go
func counter(out chan<- int)  // 只能发送
func printer(in <-chan int)   // 只能接收

编译器检查 channel 方向,防止误用。

8.5 select 多路复用 ​

select 同时等待多个 channel 操作,随机选择就绪的 case:

go
select {
case <-ch1:                                  // ch1 就绪
case x := <-ch2:                             // ch2 就绪
case ch3 <- y:                               // 可向 ch3 发送
case <-time.After(1 * time.Second):          // 超时
default:                                     // 非阻塞
}

关键点:多个 case 同时就绪时,select 随机选择一个(防止饥饿)。

8.6 示例:并发的 Clock 服务 ​

网络编程是并发大显身手的领域。Go 的 net 包提供 TCP/UDP/Unix domain socket 基础组件。Listen 创建监听器,Accept 阻塞等待新连接,返回 net.Conn 对象。net.Conn 实现了 io.Writer 接口,可以直接写入。

clock1(串行,一个连接处理完才能处理下一个):

go
listener, _ := net.Listen("tcp", "localhost:8000")
for {
    conn, _ := listener.Accept()
    handleConn(conn)  // 阻塞!下一个客户端必须等待
}

测试:nc localhost 8000 连接一个客户端,另一个终端再连一个——第二个不会收到时间,因为服务器在处理第一个连接。

clock2(并发,仅加一个 go 就翻天覆地):

go
for {
    conn, _ := listener.Accept()
    go handleConn(conn)  // 一行之差!
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
        if err != nil { return }
        time.Sleep(1 * time.Second)
    }
}

time.Format 使用特殊的参考时间模板:Mon Jan 2 03:04:05PM 2006 UTC-0700("1月2日下午3点4分5秒2006年")。Go 的格式化模板和其他语言不同,需要记住这个固定的参考时间。

netcat 客户端:用 net.Dial 创建 TCP 连接,io.Copy 从连接拷贝到 stdout:

go
conn, _ := net.Dial("tcp", "localhost:8000")
defer conn.Close()
mustCopy(os.Stdout, conn)

8.7 并发的 Echo 服务(reverb) ​

go
func echo(c net.Conn, shout string, delay time.Duration) {
    fmt.Fprintln(c, "\t", strings.ToUpper(shout))
    time.Sleep(delay)
    fmt.Fprintln(c, "\t", shout)
    time.Sleep(delay)
    fmt.Fprintln(c, "\t", strings.ToLower(shout))
}

func handleConn(c net.Conn) {
    input := bufio.NewScanner(c)
    for input.Scan() {
        echo(c, input.Text(), 1*time.Second) // 串行echo,需要等3秒
    }
    c.Close()
}

reverb2 并发处理(go echo 让多个 shout 并发):

go
func handleConn(c net.Conn) {
    input := bufio.NewScanner(c)
    for input.Scan() {
        go echo(c, input.Text(), 1*time.Second)  // ← 加 go!
    }
}

8.8 Channel 使用模式:倒计时发射 ​

countdown1 — 简单 Ticker:

go
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
    fmt.Println(countdown); <-tick
}

countdown2 — 加入 abort 信号(用 select 同时监听 tick 和 abort):

go
abort := make(chan struct{})
go func() {
    os.Stdin.Read(make([]byte, 1))  // 等待任意按键
    abort <- struct{}{}
}()
select {
case <-time.After(10 * time.Second):
    // 发射!
case <-abort:
    fmt.Println("Launch aborted!")
    return
}

countdown3 — Tick + select 多路复用(每次倒计时都检查 abort):

go
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
    fmt.Println(countdown)
    select {
    case <-tick:
        // 继续倒计时
    case <-abort:
        fmt.Println("Launch aborted!"); return
    }
}

8.9 并发目录遍历 — du 命令 ​

du1 — 串行遍历,全部结束后输出:

go
func walkDir(dir string, fileSizes chan<- int64) {
    for _, entry := range dirents(dir) {
        if entry.IsDir() { walkDir(subdir, fileSizes)
        } else { fileSizes <- entry.Size() }
    }
}

du2 — 加入定时打印进度(select + time.Tick):

go
var tick <-chan time.Time
if *verbose { tick = time.Tick(500 * time.Millisecond) }
loop:
for {
    select {
    case size, ok := <-fileSizes:
        if !ok { break loop }
        nfiles++; nbytes += size
    case <-tick:
        printDiskUsage(nfiles, nbytes)  // 定时输出
    }
}

du3 — 并发遍历 + 信号量限流(sync.WaitGroup + chan):

go
var n sync.WaitGroup
for _, root := range roots {
    n.Add(1); go walkDir(root, &n, fileSizes)  // 并发遍历
}
go func() { n.Wait(); close(fileSizes) }()

// 信号量限制并发数量(最多 20 个 dirents 并发)
var sema = make(chan struct{}, 20)
func dirents(dir string) []os.FileInfo {
    sema <- struct{}{}        // 获取令牌
    defer func() { <-sema }() // 释放令牌
    entries, _ := ioutil.ReadDir(dir)
    return entries
}

8.10 聊天服务(chat) ​

用 channel 实现广播——Go 并发的标志性示例:

go
type client chan<- string
var (
    entering = make(chan client)
    leaving  = make(chan client)
    messages = make(chan string)
)

func broadcaster() {
    clients := make(map[client]bool)
    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)
        }
    }
}

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()
}

第9章:基于共享变量的并发 ​

9.1 竞态条件 ​

在只有一个 goroutine 的线性程序中,执行顺序完全由程序逻辑决定。但在多个 goroutine 的程序中,无法判断两个事件 x 和 y 的执行顺序——它们就是并发的。

数据竞争(data race):两个或更多 goroutine 并发访问同一变量,且至少有一个是写操作时发生。

竞态条件非常恶劣:一直潜伏在程序里,只在极少数情况下才出现(高负载、特定平台、特定编译器),难以复现和诊断。

银行账户的数据竞争示例:

go
var balance int
func Deposit(amount int) { balance = balance + amount }  // 不是原子操作!
func Balance() int { return balance }

两个 goroutine 并发存款时:

  • Alice 存 $200(A1r 读余额 + A1w 写余额两步操作)
  • Bob 存 $100

可能出现:A1r 读到 balance=0 → Bob 写入 100 → A1w 写入 200(覆盖了 Bob 的存款!)

Alice 和 Bob 各存了钱,最终余额却是 $200——Bob 的 $100 丢失了。

更大的类型更危险:interface、string、slice 由多个机器字组成。当 make([]int, 10) 和 make([]int, 1000000) 并发执行,len 和 ptr 可能来自不同调用,产生"混合体"——自称长度 100 万但只有 10 个元素。这属于未定义行为。

没有任何"良性的数据竞争"——必须全部避免。

避免数据竞争的三种方法:

方法一:不写变量。 在 goroutine 创建前初始化完所有数据,之后只读。不可变数据天然并发安全。

go
var icons = map[string]image.Image{
    "spades.png":   loadIcon("spades.png"),
    "hearts.png":   loadIcon("hearts.png"),
    // ... 初始化阶段填充完毕,之后只读
}
func Icon(name string) image.Image { return icons[name] }  // 并发安全!

方法二:限制在单个 goroutine。 变量只由一个 goroutine 访问,其他 goroutine 通过 channel 发送请求。

这就是 Go 的口头禅——"不要通过共享内存来通信,而要通过通信来共享内存"。

方法三:使用互斥锁。 允许多个 goroutine 访问,但用锁保护临界区(见 9.2)。

用 go run -race 检测数据竞争,开发阶段务必开启。

9.2 互斥锁 sync.Mutex ​

go
var (
    mu      sync.Mutex
    balance int
)
func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

关键点:defer mu.Unlock() 是惯用法。锁不保护它不知道的变量。

9.3 读写锁 sync.RWMutex ​

允许多个并发读取,但写入互斥:

go
var mu sync.RWMutex
func Balance() int {
    mu.RLock(); defer mu.RUnlock()
    return balance
}

9.4 通过 Channel 替代锁 — 监视器模式 ​

将共享变量限制在单个 goroutine 中,从根本上消除竞态条件:

go
var deposits = make(chan int)
var balances = make(chan int)

func Deposit(amount int) { deposits <- amount }
func Balance() int       { return <-balances }

func teller() {
    var balance int
    for {
        select {
        case amount := <-deposits: balance += amount
        case balances <- balance:
        }
    }
}

9.5 sync.Once 延迟初始化 ​

go
var loadIconsOnce sync.Once
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)  // 保证只执行一次
    return icons[name]
}

9.6 并发缓存 Memo — 渐进演化 ​

这是一个经典的并发设计演进案例,展示了从有竞态→正确→高效的渐进过程:

memo1:非并发安全(最简单实现,有 data race):

go
type Memo struct {
    f     Func
    cache map[string]result
}
func (memo *Memo) Get(key string) (interface{}, error) {
    res, ok := memo.cache[key]
    if !ok {
        res.value, res.err = memo.f(key)
        memo.cache[key] = res
    }
    return res.value, res.err
}

memo2:加锁保证安全(同一时间只有一个 goroutine 执行):

go
type Memo struct {
    f     Func
    mu    sync.Mutex
    cache map[string]result
}
func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    res, ok := memo.cache[key]
    if !ok { res.value, res.err = memo.f(key); memo.cache[key] = res }
    memo.mu.Unlock()
    return res.value, res.err
}

memo3:缩小临界区(不同 key 可以并发,但相同 key 可能重复计算):

go
func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    res, ok := memo.cache[key]
    memo.mu.Unlock()
    if !ok {
        res.value, res.err = memo.f(key)  // 放锁外执行!但可能多个goroutine重复计算
        memo.mu.Lock()
        memo.cache[key] = res  // 重复写入相同的值(没问题,但浪费了计算)
        memo.mu.Unlock()
    }
    return res.value, res.err
}

memo4:用 channel 广播就绪条件(避免重复计算):

go
type entry struct {
    res   result
    ready chan struct{}  // 计算结果就绪时 close,通知所有等待者
}

func (memo *Memo) Get(key string) (value interface{}, err error) {
    memo.mu.Lock()
    e := memo.cache[key]
    if e == nil {
        e = &entry{ready: make(chan struct{})}  // 第一个请求
        memo.cache[key] = e
        memo.mu.Unlock()
        e.res.value, e.res.err = memo.f(key)
        close(e.ready)  // 广播:结果就绪!
    } else {
        memo.mu.Unlock()
        <-e.ready  // 等待第一个请求完成
    }
    return e.res.value, e.res.err
}

memo5:用 monitor goroutine 替代锁(已在第8章讲解过)。

核心教训:并发安全的设计往往需要多次迭代。memo1→memo4 体现了"先写对,再优化"的工程原则。

9.7 竞态检测 ​

bash
go run -race main.go
go test -race ./...

开发阶段务必开启。


第10章:包与 Go 工具 ​

10.1 包管理 ​

  • 一个包由一个或多个 .go 文件组成,放在同一目录下
  • 包名通常等于目录名(main 包除外)
  • 导入路径 = 模块名 + 包路径

10.2 常用命令 ​

命令说明
go build编译
go run编译并运行
go test运行测试
go get下载依赖
go mod init初始化模块
go mod tidy清理依赖
go fmt格式化代码
go vet静态分析
go doc查看文档

10.3 交叉编译 ​

bash
GOOS=linux GOARCH=amd64 go build    # Linux x86-64
GOOS=darwin GOARCH=arm64 go build   # macOS Apple Silicon
GOOS=windows GOARCH=386 go build    # Windows 32位

第11章:测试 ​

Go 将测试视为一等公民。测试文件以 _test.go 结尾。

11.1 单元测试 ​

go
// word.go —— 被测试的函数
func IsPalindrome(s string) bool {
    for i := range s {
        if s[i] != s[len(s)-1-i] { return false }
    }
    return true
}

// word_test.go —— 测试文件
func TestIsPalindrome(t *testing.T) {
    if !IsPalindrome("detartrated") {
        t.Error(`IsPalindrome("detartrated") = false`)
    }
}

11.2 表格驱动测试 — Go 的倡导模式 ​

go
func TestIsPalindrome(t *testing.T) {
    var tests = []struct {
        input string
        want  bool
    }{
        {"", true},
        {"a", true},
        {"ab", false},
        {"kayak", true},
        {"A man, a plan, a canal: Panama", true},
    }
    for _, test := range tests {
        if got := IsPalindrome(test.input); got != test.want {
            t.Errorf("IsPalindrome(%q) = %v", test.input, got)
        }
    }
}

关键点:表格驱动测试把测试数据和测试逻辑分离,新增用例只需加一行数据。

11.3 基准测试 ​

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

11.4 示例函数 ​

go
func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("kayak"))
    // Output:
    // true
}

示例函数既是文档又是测试——go test 验证 // Output: 注释。

11.5 外部测试与白盒测试 ​

  • 外部测试(_test 外部测试包):只能访问导出的 API,模拟真实用户
  • 白盒测试(同包测试):可访问未导出函数,用于内部逻辑验证

测试中的 mock(以发邮件为例):

go
// 通过变量替换实现 mock
var notifyUser = func(username, msg string) {
    auth := smtp.PlainAuth("", sender, password, hostname)
    smtp.SendMail(hostname+":587", auth, sender, []string{username}, []byte(msg))
}

// 测试中替换为假实现
func TestCheckQuotaNotifiesUser(t *testing.T) {
    saved := notifyUser
    defer func() { notifyUser = saved }()
    var notifiedUser, notifiedMsg string
    notifyUser = func(user, msg string) { notifiedUser, notifiedMsg = user, msg }
    // ... 执行测试逻辑,检查 notifiedUser 和 notifiedMsg
}

11.6 测试覆盖率 ​

bash
go test -coverprofile=cover.out
go tool cover -html=cover.out  # 浏览器查看覆盖详情

第12章:反射 ​

reflect 包在运行时检查变量的类型和值。反射强大但危险——失去了编译期类型安全。

核心是两个类型:reflect.Type(通过 reflect.TypeOf(x) 获取)和 reflect.Value(通过 reflect.ValueOf(x) 获取)。

12.1 递归显示任意值 — Display ​

go
func Display(name string, x interface{}) {
    fmt.Printf("Display %s (%T):\n", name, x)
    display(name, reflect.ValueOf(x))
}

func display(path string, v reflect.Value) {
    switch v.Kind() {
    case reflect.Invalid:
        fmt.Printf("%s = invalid\n", path)
    case reflect.Slice, reflect.Array:
        for i := 0; i < v.Len(); i++ {
            display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
        }
    case reflect.Struct:
        for i := 0; i < v.NumField(); i++ {
            display(fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name), v.Field(i))
        }
    case reflect.Map:
        for _, key := range v.MapKeys() {
            display(fmt.Sprintf("%s[%s]", path, formatAtom(key)), v.MapIndex(key))
        }
    case reflect.Ptr:
        if v.IsNil() { fmt.Printf("%s = nil\n", path)
        } else { display(fmt.Sprintf("(*%s)", path), v.Elem()) }
    case reflect.Interface:
        if v.IsNil() { fmt.Printf("%s = nil\n", path)
        } else {
            fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
            display(path+".value", v.Elem())
        }
    default:
        fmt.Printf("%s = %s\n", path, formatAtom(v))
    }
}

12.2 通过反射修改值 ​

go
x := 2
d := reflect.ValueOf(&x).Elem()  // 必须取地址获取可修改的 Value
d.Set(reflect.ValueOf(4))        // x = 4

// 或者:
d.SetInt(4)

// 判断是否可修改:
v.CanAddr()  // 是否可以取地址
v.CanSet()   // 是否可设置(CanAddr + 非导出字段不可设置)

关键点:只有通过指针解引用获取的 Value 才是可设置的。非导出字段不可修改。

12.3 反射实现 HTTP 参数解析 — Unpack ​

go
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil { return err }
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem()
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i)
        tag := fieldInfo.Tag          // 获取 struct tag
        name := tag.Get("http")
        if name == "" { name = strings.ToLower(fieldInfo.Name) }
        fields[name] = v.Field(i)
    }
    for name, values := range req.Form {
        f := fields[name]
        if !f.IsValid() { continue }
        for _, value := range values {
            if err := populate(f, value); err != nil {
                return fmt.Errorf("%s: %v", name, err)
            }
        }
    }
    return nil
}

12.4 反射构建 S 表达式编解码器 ​

通过反射可以将任意 Go 值转为 S 表达式(类似 Lisp 的括号语法),并支持解码回 Go 值。


第13章:底层编程 ​

13.1 unsafe.Sizeof 与 Offsetof ​

go
import "unsafe"

var x struct {
    a bool
    b int16
    c []int
}
// 通过偏移量直接操作内存!
pb := (*int16)(unsafe.Pointer(
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"

危险警告:uintptr 是一个整数而非引用,GC 不会追踪它。绝不要将 uintptr 保存到变量中再使用——必须在一个表达式中完成 uintptr → unsafe.Pointer 的转换。

13.2 深度相等判断 — 处理循环引用 ​

go
func Equal(x, y interface{}) bool {
    seen := make(map[comparison]bool)
    return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}

type comparison struct {
    x, y unsafe.Pointer
    t    reflect.Type
}

func equal(x, y reflect.Value, seen map[comparison]bool) bool {
    if !x.IsValid() || !y.IsValid() { return x.IsValid() == y.IsValid() }
    if x.Type() != y.Type() { return false }
    // 循环引用检测
    if x.CanAddr() && y.CanAddr() {
        xptr := unsafe.Pointer(x.UnsafeAddr())
        yptr := unsafe.Pointer(y.UnsafeAddr())
        if xptr == yptr { return true }  // 同一指针
        c := comparison{xptr, yptr, x.Type()}
        if seen[c] { return true }
        seen[c] = true
    }
    switch x.Kind() {
    // ... 递归比较所有类型 ...
    }
}

关键点:用 seen 记录已比较过的指针对,防止无限递归(处理循环引用)。

13.3 cgo — 调用 C 代码 ​

go
/*
#cgo CFLAGS: -I/usr/include
#include <bzlib.h>
*/
import "C"

// 通过 C.bz2 系列函数调用 bzip2 压缩库
func NewWriter(out io.Writer) io.WriteCloser {
    // C.bz2... Call C functions
}

关键点:unsafe.Pointer 可绕过类型系统但可能破坏 GC。cgo 是 Go 与 C 的桥梁,但跨语言调用有性能开销。


Go 基础面试速查 ​

以下是高频基础面试题速查,深入原理见对应专题文档。

1. make vs new ​

维度makenew
适用类型仅 slice / map / channel任意类型
返回值引用值本身(非指针)*T 指针
初始化初始化内部结构(如 slice 的 len/cap)置零,不初始化
内存分配并初始化底层数据结构仅分配内存,填零值
go
// make 返回初始化好的 slice
s := make([]int, 3, 5)  // len=3, cap=5, 底层数组已分配

// new 返回 *[]int,指向 nil slice
p := new([]int)          // *[]int,指向 nil
fmt.Println(*p == nil)   // true

记忆:make 只给三个引用类型用,返回的是"可直接用的值";new 给任意类型,返回指针。

2. 数组 vs 切片 ​

维度数组 [N]T切片 []T
长度编译期固定,是类型的一部分动态,运行时可变
传递方式值传递(复制整个数组)引用传递(复制 header 24B)
底层结构连续内存块{ptr, len, cap} 三元组
比较== 可比较(元素类型可比时)只能与 nil 比较
go
// 数组:类型包含长度
var a [3]int = [3]int{1, 2, 3}
var b [4]int              // [3]int 和 [4]int 是不同的类型

// 值传递:复制整个数组
func modify(arr [3]int) { arr[0] = 999 }
x := [3]int{1, 2, 3}
modify(x)
fmt.Println(x) // [1, 2, 3],未改变

// 切片:引用底层数组
func modifySlice(s []int) { s[0] = 999 }
y := []int{1, 2, 3}
modifySlice(y)
fmt.Println(y) // [999, 2, 3],已改变

本质:数组是"固定大小的值",切片是"动态窗口的引用"。函数传参时数组拷贝开销大,切片只拷贝 24B header。详见 Slice 底层实现。

3. for range 变量捕获(Go 1.22 变化) ​

Go 1.21 及以前:循环变量在整个循环中复用同一个地址,导致闭包/goroutine 捕获到最后一个值。

go
// Go 1.21:全部输出 3
var funcs []func() int
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() int { return i })
}
for _, f := range funcs { fmt.Println(f()) } // 3, 3, 3

// Go 1.21:for range 同理
for _, v := range []int{0, 1, 2} {
    go func() { fmt.Println(v) }() // 大概率都是 2
}

Go 1.22+:每次迭代创建新变量,闭包自然捕获正确值。

go
// Go 1.22+:输出 0, 1, 2
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() int { return i })
}
for _, f := range funcs { fmt.Println(f()) } // 0, 1, 2

// for range 同理:每次迭代 v 是新变量
for _, v := range []int{0, 1, 2} {
    go func() { fmt.Println(v) }() // 0, 1, 2(顺序不定)
}

兼容写法(1.21 前必须):i := i 在闭包内创建局部副本。Go 1.22 后不再需要。

4. struct{} 零大小类型 ​

struct{} 不占任何内存空间,unsafe.Sizeof(struct{}{}) == 0。

go
// 用途一:实现 set
set := make(map[string]struct{})
set["a"] = struct{}{}
if _, ok := set["a"]; ok { /* 存在 */ }

// 用途二:仅作信号的 channel
done := make(chan struct{})
go func() {
    // ... 工作完成
    done <- struct{}{}
}()
<-done

// 用途三:仅方法的载体
type Validator struct{}
func (Validator) Check(s string) bool { return len(s) > 0 }
var v Validator
v.Check("hello")

常见面试题:struct{} 类型的 channel 和 bool 类型的 channel 有什么区别?—— struct{} 零内存,语义更明确("通知"而非"传值")。

5. 打印格式 %v / %+v / %#v ​

go
type User struct {
    Name string
    Age  int
}
u := User{Name: "张三", Age: 28}

fmt.Printf("%v\n", u)   // {张三 28}
fmt.Printf("%+v\n", u)  // {Name:张三 Age:28}
fmt.Printf("%#v\n", u)  // main.User{Name:"张三", Age:28}
格式输出用途
%v字段值默认格式
%+v字段名: 字段值调试时看字段对应关系
%#v包名.类型名完整的 Go 语法表示
%T类型查看变量类型

6. interface{} 比较与 nil 陷阱 ​

比较规则:两个 interface 相等当且仅当动态类型相同且动态值相等。

go
var a interface{} = 1
var b interface{} = 1
fmt.Println(a == b) // true(类型相同,值相同)

var c interface{} = []int{1}
var d interface{} = []int{1}
// fmt.Println(c == d) // panic! slice 不可比较

经典陷阱 — nil 接口 ≠ nil 指针:

go
var p *int = nil          // p 是 nil 指针
var i interface{} = p     // i 不是 nil!动态类型是 *int
fmt.Println(i == nil)     // false ← 面试必考题

// eface{_type: *int, data: nil} ≠ eface{_type: nil, data: nil}
表达式动态类型动态值== nil
var i interface{}nilniltrue
var p *int = nil; var i interface{} = p*intnilfalse

根本原因:interface 底层是两个字段(_type + data),仅当两个字段都为 nil 时才是 nil interface。详见 sync 包与接口底层。

7. init() 完整执行顺序 ​

mermaid
flowchart LR
    A["import 包依赖解析"] --> B["初始化包级 const"]
    B --> C["初始化包级 var"]
    C --> D["调用 init() 函数"]
    D --> E["执行 main()"]

规则:

  • 同一包内多个文件:按文件名字母序执行 init()
  • 同一文件内多个 init():按声明顺序执行
  • 被依赖包的 init() 先于依赖包的 init() 执行
  • 禁止循环依赖
go
// a.go
var A = initA()  // ① 包级 var 先初始化

func initA() int { return 1 }

func init() {    // ② 再执行 init()
    fmt.Println("a.init")
}

// b.go(同包)
func init() {    // ③ 按文件名字母序,b.go 在 a.go 后
    fmt.Println("b.init")
}

// main.go
func main() {    // ④ 最后执行 main
    fmt.Println("main")
}
// 输出:a.init → b.init → main

记忆口诀:import → const → var → init → main。依赖先于我,字母先于后。


全书要点总结 ​

理念说明
简洁25 个关键字,无泛型(1.18前)、无继承、无异常
组合优于继承struct 嵌入 + interface 抽象
错误即值不用 try-catch,error 是普通返回值
CSP 并发模型goroutine + channel,"通过通信共享内存"
隐式接口不声明实现,鸭子类型
向后兼容Go 1 兼容性承诺,旧代码永不过时
工具链强大go fmt / vet / test / race / pprof / mod

标准库速查 ​

包用途
fmt格式化 I/O
io / ioutil基础 I/O 接口
os操作系统接口
bufio缓冲 I/O(Scanner 最常用)
net/httpHTTP 客户端 / 服务端
encoding/jsonJSON 编解码
encoding/xmlXML 解析
text/template文本模板
html/templateHTML 模板(防 XSS)
strings / bytes字符串 / 字节切片操作
strconv字符串与数值转换
time时间处理
syncMutex / RWMutex / WaitGroup / Once
sync/atomic原子操作
context请求上下文与超时传播
testing测试框架
reflect运行时反射
unsafe底层内存操作
flag命令行参数解析
image / image/gif图像处理

Go vs C / Python / Java — 语法差异速查 ​

学习 Go 时最容易被"其他语言的肌肉记忆"坑到。以下对比帮助你快速适应 Go 的思维方式:

特性GoCPythonJava
入口函数func main() 在 package mainint main()文件顶层代码public static void main
分号词法器自动插入,不写必须 ;不写必须 ;
花括号func() { 同一行强制任意无(缩进)任意
变量声明var x int / x := 1int x = 1x = 1int x = 1 / var x
类型位置变量名在前,类型在后类型在前无声明类型在前
循环for 做所有循环(无 while)for / while / do-whilefor / whilefor / while / do-while
switch默认不穿透(无需 break)必须 break无 switch (3.10+)默认穿透
指针✅ 有(无指针算术)✅ 有(含指针算术)❌ 无(引用语义)❌ 无(引用类型)
类/继承❌ 无(struct+interface 组合)❌ 无(struct)✅ class + 多重继承✅ class + 单继承
构造函数func NewXxx() 惯例❌ 无__init__与类同名方法
异常处理panic/recover(不用作常规错误)errno / longjmptry/excepttry/catch
错误返回(result, error) 多返回值errno / 返回值 -1异常异常 + 受检异常
泛型✅ 1.18+(类型参数)❌(宏模拟 / C11 _Generic)鸭子类型(动态)✅ 类型擦除
包管理go modC 无标准(make/cmake)pip / poetryMaven / Gradle
并发模型goroutine + channelpthread / 无asyncio / 多进程Thread / Executor
编译编译为本机代码(无 VM)编译为本机代码解释执行(CPython)编译为 JVM 字节码

常见"肌肉记忆"错误 ​

go
// ❌ C 习惯:类型在前
int x = 1;

// ✅ Go: 类型在后(更接近人类语言:"变量 x 的类型是 int")
var x int = 1

// ❌ Python 习惯:用缩进做条件
if x > 0
    fmt.Println("positive")

// ✅ Go: 必须花括号
if x > 0 {
    fmt.Println("positive")
}

// ❌ Java/C 习惯:switch 穿透
switch x {
case 1: doA()  // 不会自动执行 doB!
case 2: doB()
}

// ✅ Go: 想穿透必须显式 fallthrough
switch x {
case 1: doA(); fallthrough
case 2: doB()
}

// ❌ Java 习惯:类继承
type Dog extends Animal { ... }

// ✅ Go: 用嵌套struct + 接口组合
type Dog struct { Animal; name string }

Go 1.21 ~ 1.24 重要新特性 ​

1. log/slog 结构化日志(Go 1.21) ​

标准库终于有了结构化日志。之前要么 log.Println,要么引入 zap/zerolog 等第三方库。

go
import "log/slog"

// 基础用法
slog.Info("request processed",
    "method", "GET",
    "path", "/users",
    "latency_ms", 12.5)
// 2024/01/01 12:00:00 INFO request processed method=GET path=/users latency_ms=12.5

// JSON handler(生产推荐)
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
logger.Error("connection failed", "addr", "10.0.0.1:5432", "error", io.EOF)

// 从 context 中提取属性
ctx := context.Background()
slog.LogAttrs(ctx, slog.LevelInfo, "user login",
    slog.String("user_id", "1234"),
    slog.Int("attempt", 3))

为什么引入:log 包太原始(无日志级别),第三方库互不兼容。slog 提供统一接口,同时保持高性能(零分配设计)。

2. math/rand/v2(Go 1.22) ​

彻底告别 rand.Seed():

go
import "math/rand/v2"

// ✅ 新 API:不需要手动 Seed
r := rand.New(rand.NewPCG(42, 1024))
fmt.Println(r.IntN(100))  // 0-99

// ✅ 全局函数也是安全的(自动 Seeded)
fmt.Println(rand.IntN(100))

// ❌ 旧 API(math/rand):必须 Seed,且全局 rand 不是并发安全的
// rand.Seed(time.Now().UnixNano())

为什么引入:旧 math/rand 有两大致命问题——① 全局随机源不是并发安全的(锁竞争);② rand.Seed 是全局副作用,多个包互相干扰。v2 修复了这两个问题。

3. range over func 迭代器(Go 1.23) ​

Go 终于原生支持自定义迭代器。

Push vs Pull:两种迭代模型:

Go 1.22 及之前,实现自定义迭代器的唯一方式是 goroutine + channel(Push 模型):

go
// Push 模型:生产者通过 channel 把数据"推"给消费者
func CountPush(n int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := 1; i <= n; i++ {
            ch <- i  // 推出去
        }
        close(ch)
    }()
    return ch
}

for v := range CountPush(5) {
    fmt.Println(v)
}
// 问题:
// 1. 每次迭代都创建一个 goroutine → 内存和调度开销
// 2. break 提前退出 → goroutine 泄漏(channel 没人读了,goroutine 卡在 ch<-)
// 3. 无法在迭代中间传上下文/错误

Go 1.23 的 iter.Seq 是 Pull 模型:

go
// Pull 模型:消费者通过 yield 回调"拉"数据
// 没有 goroutine,没有 channel,纯函数调用
func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 1; i <= n; i++ {
            if !yield(i) {
                return  // 消费者 break → yield 返回 false → 干净退出
            }
        }
    }
}

for v := range Count(5) {
    fmt.Println(v)  // 1, 2, 3, 4, 5
}

Push vs Pull 核心差异:

维度Push(goroutine+channel)Pull(iter.Seq)
并发模型生产者-消费者,goroutine 分离同步调用,无 goroutine
内存开销每个迭代一个 goroutine(~2KB 栈)零额外分配
提前退出(break)goroutine 泄漏风险yield 返回 false,干净退出
错误传播需要额外 error channel直接在调用栈中返回 error
组合能力需手动编排 channel标准库 slices/maps 原生支持

Pull 模型为什么更高效:迭代器的控制权在调用方。for range 每轮循环调用一次 yield,如果调用方 break,yield 返回 false,迭代器立刻 return——没有 goroutine 被遗弃,没有 channel 等待 GC。

go
import "iter"

func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 1; i <= n; i++ {
            if !yield(i) {
                return  // 调用方 break 了
            }
        }
    }
}

// ✅ range over func
for v := range Count(5) {
    fmt.Println(v)  // 1, 2, 3, 4, 5
}

// 带单个元素的迭代器
func Lines(s string) iter.Seq[string] {
    return func(yield func(string) bool) {
        for _, line := range strings.Split(s, "\n") {
            if !yield(line) { return }
        }
    }
}

// 带两个元素的迭代器(KV)
func All[K comparable, V any](m map[K]V) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for k, v := range m {
            if !yield(k, v) { return }
        }
    }
}

为什么重要:这是 Go 自 1.0 以来最重大的语言特性变更。iter.Seq 和 iter.Seq2 成为新的标准库类型,slices、maps、strings 包都提供了对应的迭代器适配器。这意味着用户定义的容器/集合也能用 for range 语法。

标准库新增的迭代器:

go
// slices 包
slices.All(slice)         // iter.Seq2[int, E] — 索引+值
slices.Values(slice)      // iter.Seq[E] — 仅值
slices.Backward(slice)    // iter.Seq2[int, E] — 反向迭代
slices.Collect(seq)       // iter.Seq → slice

// maps 包
maps.All(m)               // iter.Seq2[K, V]
maps.Keys(m)              // iter.Seq[K]
maps.Values(m)            // iter.Seq[V]
maps.Collect(seq)         // iter.Seq2 → map
```maps.Collect(seq)         // iter.Seq2 → map

4. SwissTable Map 实验(Go 1.24) ​

bash
# 启用实验性 SwissTable map 实现
GOEXPERIMENT=swissmap go run main.go

背景:Go 的 map 一直使用传统的拉链法(bucket + overflow 链),而 C++ absl::flat_hash_map 和 Rust hashbrown 早已使用 SwissTable(开放寻址变体)。SwissTable 的优势在于:

  • 用 SIMD 指令并行比较 16 个哈希高位(一次比较 16 个!)
  • 内存完全连续(无溢出桶链表跳转)
  • Cache 友好度极高

Go 1.24 的进展:SwissTable 作为 GOEXPERIMENT 提供,预计 Go 1.25 或 1.26 成为默认实现。当前性能提升约 10-40%(因工作负载而异)。


参考:gopl.io — Alan A. A. Donovan & Brian W. Kernighan, The Go Programming Language 中文版:Go语言圣经

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记