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.0var 形式一般用于:需要显式指定类型、变量稍后会被重新赋值而初始值不重要、或包级别声明。
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 |
uint8 | 0 ~ 255(byte 是其别名) |
uint16 | 0 ~ 65535 |
uint32 | 0 ~ 2^32-1(rune 是 int32 别名,表示 Unicode 码点) |
uint64 | 0 ~ 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位精度——推荐默认用 float64complex64/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...) // 105.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 在等待时不阻塞 fibgoroutine 初始栈仅 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
| 维度 | make | new |
|---|---|---|
| 适用类型 | 仅 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{} | nil | nil | true |
var p *int = nil; var i interface{} = p | *int | nil | false |
根本原因: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/http | HTTP 客户端 / 服务端 |
encoding/json | JSON 编解码 |
encoding/xml | XML 解析 |
text/template | 文本模板 |
html/template | HTML 模板(防 XSS) |
strings / bytes | 字符串 / 字节切片操作 |
strconv | 字符串与数值转换 |
time | 时间处理 |
sync | Mutex / RWMutex / WaitGroup / Once |
sync/atomic | 原子操作 |
context | 请求上下文与超时传播 |
testing | 测试框架 |
reflect | 运行时反射 |
unsafe | 底层内存操作 |
flag | 命令行参数解析 |
image / image/gif | 图像处理 |
Go vs C / Python / Java — 语法差异速查
学习 Go 时最容易被"其他语言的肌肉记忆"坑到。以下对比帮助你快速适应 Go 的思维方式:
| 特性 | Go | C | Python | Java |
|---|---|---|---|---|
| 入口函数 | func main() 在 package main | int main() | 文件顶层代码 | public static void main |
| 分号 | 词法器自动插入,不写 | 必须 ; | 不写 | 必须 ; |
| 花括号 | func() { 同一行强制 | 任意 | 无(缩进) | 任意 |
| 变量声明 | var x int / x := 1 | int x = 1 | x = 1 | int x = 1 / var x |
| 类型位置 | 变量名在前,类型在后 | 类型在前 | 无声明 | 类型在前 |
| 循环 | for 做所有循环(无 while) | for / while / do-while | for / while | for / while / do-while |
| switch | 默认不穿透(无需 break) | 必须 break | 无 switch (3.10+) | 默认穿透 |
| 指针 | ✅ 有(无指针算术) | ✅ 有(含指针算术) | ❌ 无(引用语义) | ❌ 无(引用类型) |
| 类/继承 | ❌ 无(struct+interface 组合) | ❌ 无(struct) | ✅ class + 多重继承 | ✅ class + 单继承 |
| 构造函数 | func NewXxx() 惯例 | ❌ 无 | __init__ | 与类同名方法 |
| 异常处理 | panic/recover(不用作常规错误) | errno / longjmp | try/except | try/catch |
| 错误返回 | (result, error) 多返回值 | errno / 返回值 -1 | 异常 | 异常 + 受检异常 |
| 泛型 | ✅ 1.18+(类型参数) | ❌(宏模拟 / C11 _Generic) | 鸭子类型(动态) | ✅ 类型擦除 |
| 包管理 | go mod | C 无标准(make/cmake) | pip / poetry | Maven / Gradle |
| 并发模型 | goroutine + channel | pthread / 无 | 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 → map4. 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语言圣经
登录后即可发表评论 👇