Effective Go 学习笔记
本文档基于 Go 官方文档 Effective Go 整理编写。 每个知识点配有可运行的示例代码,帮助理解和实践。
注意:Effective Go 最初编写于 2009 年(Go 发布时),未涵盖泛型(Generics)等后续新增的语言特性。如需了解最新变化,请参考 Go Release Notes。
1. 引言(Introduction)
Effective Go 是学习编写地道 Go 代码的指南。Go 是一种注重简洁、可靠、高效的语言。要写好 Go 代码,需要理解其特性和惯用法——不能简单地把 C++ 或 Java 的写法翻译成 Go。
核心理念:Go 程序应该在结构、命名、格式等方面遵循社区约定,这样其他 Go 程序员能轻松理解你的代码。
示例:Go 的"从问题出发"思维
package main
import "fmt"
// 传统 OOP 风格(不推荐)
type Printer struct{}
func (p Printer) Print(s string) {
fmt.Println(s)
}
// Go 风格:用函数/接口组合(推荐)
func Print(s string) {
fmt.Println(s)
}
func main() {
// Go 不推荐过度封装,简单的功能直接用函数
Print("Hello, Effective Go!")
}
2. 格式(Formatting)
Go 的格式由 gofmt(或 go fmt)自动处理。格式化问题交给机器,不再需要团队争论代码风格。
关键点:
- 缩进使用 Tab(gofmt 默认)
- 行长度无限制——太长时手动折行,用额外 Tab 缩进
- 控制结构(
if、for、switch)没有括号 - 运算符优先级更清晰
示例:gofmt 自动对齐
package main
import "fmt"
type T struct {
name string // 对象名称
value int // 它的值
}
// gofmt 会自动对齐注释
func main() {
// 左大括号必须与 if/for 在同一行
if true {
fmt.Println("大括号不能另起一行")
}
// 运算符优先级符合直觉
x, y := 2, 3
// x<<8 + y<<16 含义与间距一致
result := x<<8 + y<<16
fmt.Printf("结果: %d\n", result) // 2*256 + 3*65536 = 512 + 196608 = 197120
}
练习:运行 gofmt
# 格式化当前目录所有 .go 文件
gofmt -w *.go
# 或使用 go fmt
go fmt ./...
3. 注释(Commentary)
Go 支持两种注释风格:
//行注释(推荐,大多数情况使用)/* */块注释(主要用于包注释或临时禁用大段代码)
文档注释:出现在顶级声明之前、没有空行分隔的注释,即为该声明的文档注释。
package main
import "fmt"
// Package 级别的文档注释
// 这个包演示了 Go 的注释风格
// Greet 返回一个问候语。
// 这是函数的文档注释,会在 godoc 中显示。
func Greet(name string) string {
// 这是行内注释,不会被 godoc 收录
return "Hello, " + name
}
/*
这是块注释风格,
主要用于包注释或临时禁用代码。
*/
func main() {
msg := Greet("世界")
fmt.Println(msg)
/*
块注释可以禁用大段代码
fmt.Println("这段不会执行")
fmt.Println("这段也不会")
*/
}
4. 命名(Names)
4.1 包命名(Package Names)
- 小写、单个单词,不要下划线或驼峰
- 简洁、有描述性
- 包名是导入时的默认访问器
package main
import (
"bufio" // 包名是 bufio,不是 buf_io 或 bufIo
"container/ring" // 包名是 ring,不是 container_ring
"fmt"
)
func main() {
// 包中的类型通过"包名.类型名"访问
r := ring.New(5) // 不叫 ring.NewRing,因为包名 ring 已经说明了含义
fmt.Println(r.Len())
// bufio.Reader 而不是 bufio.BufReader
// io.Reader 和 bufio.Reader 通过包名区分,不会冲突
_ = bufio.NewReaderSize(nil, 1024)
}
4.2 Getter 方法
Go 不自动提供 getter/setter。如果你需要,getter 方法名不应该包含 Get。
package main
import "fmt"
// Owner 是一个演示类型
type Owner struct {
owner string // 小写、未导出
age int
}
// Getter: 直接叫 Owner,不叫 GetOwner
func (o *Owner) Owner() string {
return o.owner
}
// Setter: 可以叫 SetOwner
func (o *Owner) SetOwner(name string) {
o.owner = name
}
func main() {
obj := &Owner{owner: "张三"}
fmt.Println(obj.Owner()) // 不叫 GetOwner
obj.SetOwner("李四")
fmt.Println(obj.Owner())
}
4.3 接口命名(Interface Names)
单方法接口按惯例以方法名 + -er 后缀命名:
package main
import "fmt"
// 按照惯例定义自己的单方法接口
type Stringer interface {
String() string
}
// Greeter 是 -er 接口的典型命名
type Greeter interface {
Greet() string
}
type Person struct {
Name string
}
// Person 实现了 Greeter 接口
func (p Person) Greet() string {
return "你好,我是" + p.Name
}
func main() {
var g Greeter = Person{Name: "小明"}
fmt.Println(g.Greet())
// 不要给方法起冲突的名字
// 如果你的类型要实现"字符串转换",叫 String() 而不是 ToString()
}
4.4 MixedCaps
Go 使用 MixedCaps 或 mixedCaps(驼峰式),不使用下划线。
package main
import (
"encoding/base64"
"fmt"
)
// 导出的 → 大写开头
// 未导出的 → 小写开头
type UserProfile struct { // 不是 user_profile
UserName string // 不是 user_name
LoginTime int64 // 不是 login_time
}
func main() {
// 标准库也都是驼峰
encoding := base64.StdEncoding
fmt.Println(encoding.EncodeToString([]byte("hello")))
}
5. 分号(Semicolons)
Go 的词法分析器会自动插入分号。规则很简单:
如果换行前的最后一个 token 是
标识符、字面量、break、continue、fallthrough、return、++、--、)或},则自动插入分号。
重要推论:左大括号 { 不能放在下一行。
package main
import "fmt"
func main() {
// ✅ 正确:左大括号在同一行
if true {
fmt.Println("正确写法")
}
// ❌ 错误:左大括号在下一行(不会编译)
// if true
// {
// fmt.Println("错误写法")
// }
// 分号只在 for 循环的初始/条件/递增之间有明确需要
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// 一行写多条语句时需要显式分号
a := 1; b := 2; fmt.Println(a, b)
}
6. 控制结构(Control Structures)
6.1 If
if 可以包含初始化语句;当 if 体以 return、break、continue、goto 结束时,省略 else。
package main
import (
"errors"
"fmt"
"os"
"strconv"
)
func main() {
// 基本 if(无括号)
x := 10
if x > 5 {
fmt.Println("x 大于 5")
}
// if 带初始化语句
if err := validateAge(-1); err != nil {
fmt.Println("验证失败:", err)
}
// 省略 else 的惯用风格:成功路径沿页面下行
content, err := readFile("test.txt")
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println("内容:", content)
// 多个错误检查链
numStr := "42"
num, err := strconv.Atoi(numStr)
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println("数字:", num)
}
func validateAge(age int) error {
if age < 0 {
return errors.New("年龄不能为负数")
}
return nil
}
func readFile(name string) (string, error) {
data, err := os.ReadFile(name)
if err != nil {
return "", err
}
return string(data), nil
}
6.2 重复声明与重新赋值(Redeclaration and Reassignment)
:= 短声明中,只要至少有一个变量是新声明的,就可以重复使用已有变量(如 err)。
package main
import (
"errors"
"fmt"
)
func main() {
// 首次声明 f 和 err
f, err := openFile("test.txt")
if err != nil {
fmt.Println("第一次打开失败:", err)
}
_ = f
// 再次使用 :=,d 是新变量,err 是重新赋值
d, err := openFile("config.txt")
if err != nil {
fmt.Println("第二次打开失败:", err)
}
_ = d
// 重要:err 只被重新赋值(同一作用域),d 是新声明的
// 如果 err 在外层作用域已存在,这里会创建新的 err
fmt.Println("成功")
}
func openFile(name string) (string, error) {
if name == "config.txt" {
return "", errors.New("文件不存在")
}
return "文件内容", nil
}
6.3 For
Go 的 for 统一了 for 和 while,没有 do-while。
package main
import "fmt"
func main() {
// 形式1:完整的 for 语句
sum := 0
for i := 0; i < 5; i++ {
sum += i
}
fmt.Println("sum =", sum) // 10
// 形式2:类似 while
n := 1
for n < 100 {
n *= 2
}
fmt.Println("n =", n) // 128
// 形式3:无限循环(类似 for(;;))
count := 0
for {
count++
if count >= 3 {
break
}
}
fmt.Println("count =", count)
// range 遍历数组/切片
fruits := []string{"苹果", "香蕉", "橙子"}
for i, fruit := range fruits {
fmt.Printf("%d: %s\n", i, fruit)
}
// 只需要索引/键
m := map[string]int{"a": 1, "b": 2}
for key := range m {
fmt.Println("key:", key)
}
// 只需要值
nums := []int{10, 20, 30}
for _, v := range nums {
fmt.Println("value:", v)
}
// range 遍历字符串(自动解析 UTF-8)
for pos, char := range "世界" {
fmt.Printf("字符 %c 在字节位置 %d\n", char, pos)
}
// 并行赋值反转数组
arr := []int{1, 2, 3, 4, 5}
for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
arr[i], arr[j] = arr[j], arr[i]
}
fmt.Println("反转后:", arr) // [5 4 3 2 1]
}
6.4 Switch
Go 的 switch 比 C 更灵活:
- 表达式不必是常量或整数
- case 从上到下匹配,匹配即停(无需
break) switch后无表达式等同于switch true
package main
import "fmt"
// switch 代替 if-else-if 链
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
// 逗号分隔多个匹配值
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
func main() {
fmt.Println(unhex('A')) // 10
fmt.Println(unhex('3')) // 3
fmt.Println(shouldEscape(' ')) // true
fmt.Println(shouldEscape('a')) // false
// switch + break label
src := []byte{0x01, 0x02, 0x81, 0x82}
outer:
for n := 0; n < len(src); n++ {
switch {
case src[n] < 0x80:
fmt.Printf("单字节: %d\n", src[n])
default:
if n+1 >= len(src) {
fmt.Println("数据不完整")
break outer
}
fmt.Printf("多字节: %d, %d\n", src[n], src[n+1])
n++
}
}
// 用 switch 比较字节切片
a := []byte{1, 2, 3}
b := []byte{1, 2, 4}
fmt.Println(compareBytes(a, b)) // -1
}
func compareBytes(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
6.5 类型 Switch(Type Switch)
通过 switch v := x.(type) 判断接口的动态类型。
package main
import "fmt"
func typeDescriptor(v interface{}) string {
switch t := v.(type) {
case nil:
return "nil"
case bool:
return fmt.Sprintf("布尔值: %t", t)
case int:
return fmt.Sprintf("整数: %d", t)
case float64:
return fmt.Sprintf("浮点数: %f", t)
case string:
return fmt.Sprintf("字符串: %q", t)
default:
return fmt.Sprintf("未知类型: %T", t)
}
}
func main() {
values := []interface{}{
nil,
true,
42,
3.14,
"hello",
[]int{1, 2, 3},
}
for _, v := range values {
fmt.Println(typeDescriptor(v))
}
}
7. 函数(Functions)
7.1 多返回值(Multiple Return Values)
Go 的函数和方法可以返回多个值。常用于返回结果和错误。
package main
import (
"errors"
"fmt"
"unicode"
)
func isDigit(c byte) bool {
return '0' <= c && c <= '9'
}
// nextInt 从字节切片中解析下一个整数,返回数字和下一个位置
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
// 文件和错误一起返回——Go 的惯用风格
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
// 多返回值:解析数字
b := []byte("价格: 42 个, 数量: 10 个")
for i := 0; i < len(b); {
x, i := nextInt(b, i)
if x > 0 {
fmt.Printf("找到数字: %d (下一个位置: %d)\n", x, i)
}
}
// 多返回值:结果和错误
result, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
// 忽略错误(不推荐,仅作演示)
result, _ = divide(10, 2)
fmt.Println("忽略错误:", result)
}
7.2 命名返回值(Named Result Parameters)
返回值可以命名,作为文档,并允许裸 return。
package main
import (
"errors"
"fmt"
"io"
)
// ReadFull 精确读取 len(buf) 字节
func ReadFull(r io.Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return // 裸 return,返回 n 和 err 的当前值
}
// 命名返回值作为文档
func divideWithRemainder(a, b int) (quotient, remainder int, err error) {
if b == 0 {
err = errors.New("除数不能为零")
return // 等价于 return 0, 0, err
}
quotient = a / b
remainder = a % b
return // 等价于 return quotient, remainder, nil
}
func main() {
// ReadFull 示例
r := NewStringReader("hello world")
buf := make([]byte, 5)
n, err := ReadFull(r, buf)
if err != nil {
fmt.Println("读取错误:", err)
} else {
fmt.Printf("读取了 %d 字节: %s\n", n, buf) // 5, "hello"
}
// 命名返回值示例
q, rem, err := divideWithRemainder(17, 5)
if err == nil {
fmt.Printf("17 / 5 = %d 余 %d\n", q, rem) // 3 余 2
}
}
// 辅助类型:将字符串包装为 io.Reader
type stringReader struct {
s string
n int
}
func NewStringReader(s string) io.Reader {
return &stringReader{s: s}
}
func (r *stringReader) Read(b []byte) (int, error) {
if r.n >= len(r.s) {
return 0, io.EOF
}
n := copy(b, r.s[r.n:])
r.n += n
return n, nil
}
7.3 Defer
defer 调度一个函数调用,在包含它的函数返回前执行。用于资源清理(关闭文件、解锁等)。
关键特性:
- 延迟调用的参数在
defer执行时立即求值(不是调用时) - 多个
defer按 LIFO(后进先出)顺序执行 - 常用于关闭资源和释放锁
package main
import (
"fmt"
"os"
)
// Contents 读取文件内容并返回
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // 确保函数返回时关闭文件
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf)
result = append(result, buf[:n]...)
if err != nil {
break
}
}
return string(result), nil
}
// defer 参数立即求值
func deferArgs() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i) // 打印: 4 3 2 1 0(LIFO且i在defer时已求值)
}
}
// 使用 defer 跟踪函数执行
func trace(s string) string {
fmt.Println("进入:", s)
return s
}
func un(s string) {
fmt.Println("离开:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("在 a 里面")
}
func b() {
defer un(trace("b"))
fmt.Println("在 b 里面")
a()
}
func main() {
// 读取自己的源代码
content, err := Contents("effective_go_study.md")
if err != nil {
fmt.Println("文件不存在,演示跳过:", err)
} else {
fmt.Printf("文件大小: %d 字节\n", len(content))
}
// defer 参数立即求值
fmt.Println("\ndefer 参数求值演示:")
deferArgs()
// defer 跟踪函数
fmt.Println("\n函数跟踪演示:")
b()
}
8. 数据(Data)
8.1 new 分配
new(T) 分配零值内存,返回 *T。适用于所有类型。
Go 1.26 新增:new 现在支持 new(expr) 语法,可以直接指定初始值。
package main
import (
"fmt"
"sync"
)
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
func main() {
// new 返回零值指针
p := new(int) // *int,值为 0
fmt.Println(*p) // 0
// Go 1.26+: new 可以指定初始值
// p2 := new(int64(300))
// fmt.Println(*p2) // 300(需要 Go 1.26)
// 零值可用的设计
var mu sync.Mutex
mu.Lock() // 零值 Mutex 是未锁定状态,可直接使用
mu.Unlock()
// new 复合类型
buf := new(bytes.Buffer) // 零值 Buffer 是空缓冲区,可直接使用
buf.WriteString("hello")
fmt.Println(buf.String()) // "hello"
// SyncedBuffer 的零值也立即可用
sb := new(SyncedBuffer)
sb.lock.Lock()
sb.buffer.WriteString("hello")
sb.lock.Unlock()
}
8.2 构造函数与复合字面量(Constructors and Composite Literals)
当零值不够时,使用构造函数。复合字面量可以大大简化构造过程。
package main
import "fmt"
type File struct {
fd int
name string
dirinfo string
nepipe int
}
// 构造函数:手动初始化
func NewFileV1(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = ""
f.nepipe = 0
return f
}
// 改进:复合字面量
func NewFileV2(fd int, name string) *File {
if fd < 0 {
return nil
}
return &File{fd: fd, name: name}
}
// 数组和 map 的复合字面量
func main() {
// 结构体字面量
f1 := NewFileV2(1, "test.txt")
fmt.Printf("文件: %+v\n", f1)
// 带字段标签的字面量(推荐)
f2 := &File{fd: 2, name: "config.txt"}
fmt.Printf("文件: %+v\n", f2)
// 数组字面量
arr := [...]int{0: 10, 2: 30, 1: 20} // 按索引指定
fmt.Println(arr) // [10 20 30]
// 切片字面量
slice := []string{1: "第二", 0: "第一"}
fmt.Println(slice) // ["第一" "第二"]
// map 字面量
m := map[int]string{
200: "OK",
404: "Not Found",
500: "Server Error",
}
fmt.Printf("HTTP 状态码: %+v\n", m)
}
8.3 make 分配
make(T, args) 只用于创建切片、map 和 channel。它返回初始化后的值(不是指针)。
| 特性 | new |
make |
|---|---|---|
| 适用类型 | 所有类型 | 仅 slice/map/channel |
| 返回 | *T(指针) |
T(值) |
| 初始化 | 零值 | 初始化内部数据结构 |
package main
import "fmt"
func main() {
// make: 创建切片(返回已初始化的值)
v := make([]int, 10, 100) // 底层数组 100 个 int,切片长度 10,容量 100
fmt.Printf("len=%d cap=%d\n", len(v), cap(v))
// new: 创建切片(返回指向 nil 切片的指针,很少有用)
p := new([]int) // p 是 *[]int
fmt.Printf("new切片: %v (len=%d)\n", *p, len(*p))
// 👆 new 切片后用 make 初始化(不必要地复杂)
*p = make([]int, 100)
fmt.Printf("初始化后 len=%d\n", len(*p))
// 👇 惯用写法
v2 := make([]int, 100)
fmt.Printf("惯用写法 len=%d\n", len(v2))
// make map
scores := make(map[string]int)
scores["张三"] = 95
fmt.Println("分数:", scores)
// make channel
ch := make(chan int, 10)
ch <- 42
fmt.Println("从 channel 接收:", <-ch)
}
8.4 数组(Arrays)
Go 中数组是值类型(赋值和传参都复制整个数组),数组大小是类型的一部分。
package main
import "fmt"
// 传递数组指针(类似 C 风格,但 Go 中不推荐)
func SumByPtr(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
func main() {
// 数组是值类型
a := [3]int{1, 2, 3}
b := a // 复制整个数组
b[0] = 100
fmt.Println(a, b) // [1 2 3] [100 2 3]
// 数组大小是类型的一部分
fmt.Printf("类型: %T\n", [3]int{}) // [3]int
fmt.Printf("类型: %T\n", [5]int{}) // [5]int(不同大小 = 不同类型)
// 传递数组指针
arr := [...]float64{7.0, 8.5, 9.1}
sum := SumByPtr(&arr) // 必须显式取地址
fmt.Println("求和:", sum)
// 但一般情况下,用切片代替数组
arr2 := []float64{7.0, 8.5, 9.1}
sum2 := 0.0
for _, v := range arr2 {
sum2 += v
}
fmt.Println("切片求和:", sum2)
}
8.5 切片(Slices)
切片是对底层数组的引用(类似胖指针:ptr + len + cap)。切片传参时,函数对元素的修改对调用方可见。
package main
import "fmt"
// Append 手动实现 append 的功能
func Append(slice []byte, data []byte) []byte {
l := len(slice)
if l+len(data) > cap(slice) {
// 容量不足,重新分配(通常分配 2 倍)
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
copy(slice[l:], data)
return slice
}
func main() {
// 切片持有对底层数组的引用
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // 引用 arr[1], arr[2], arr[3]
s1[0] = 99
fmt.Println(arr) // [1 99 3 4 5] — 原数组被修改
// 函数修改切片元素
modifyFirst(s1)
fmt.Println(arr) // [1 100 3 4 5]
// 切片长度和容量
s2 := []int{0, 1, 2, 3, 4, 5}
s3 := s2[1:4] // len=3, cap=5
fmt.Printf("len=%d cap=%d\n", len(s3), cap(s3))
// 读文件时使用切片限制读取量
buf := make([]byte, 1024)
_ = buf
// f.Read(buf[0:32]) // 只读前 32 字节
// 自定义 Append
slice := []byte{1, 2, 3}
slice = Append(slice, []byte{4, 5})
fmt.Println("Append 结果:", slice)
// append 内建函数
s := []int{1, 2, 3}
s = append(s, 4, 5, 6)
fmt.Println("append 内建:", s)
// append slice to slice
x := []int{1, 2, 3}
y := []int{4, 5, 6}
x = append(x, y...) // ... 展开
fmt.Println("展开追加:", x)
}
func modifyFirst(s []int) {
if len(s) > 0 {
s[0] = 100
}
}
8.6 二维切片(Two-dimensional Slices)
package main
import "fmt"
func main() {
// 方式1:逐行分配(每行长度可不同)
picture1 := make([][]uint8, 4)
for i := range picture1 {
picture1[i] = make([]uint8, 6)
}
fmt.Printf("逐行分配: %d x %d\n", len(picture1), len(picture1[0]))
// 方式2:一次分配再切片(每行长度相同,性能更优)
picture2 := make([][]uint8, 4) // 顶层切片
pixels := make([]uint8, 4*6) // 一次分配所有像素
for i := range picture2 {
picture2[i], pixels = pixels[:6], pixels[6:]
}
fmt.Printf("单次分配: %d x %d\n", len(picture2), len(picture2[0]))
// 每行可以不同长度
text := [][]byte{
[]byte("第一行"),
[]byte("第二行,更长一些"),
[]byte("第三行"),
}
for i, line := range text {
fmt.Printf("行 %d: len=%d, 内容=%s\n", i+1, len(line), line)
}
}
8.7 Map
Map 是 Go 的内建关联类型。slice 不能做 key(没有相等性定义)。
package main
import "fmt"
var timeZone = map[string]int{
"UTC": 0 * 60 * 60,
"EST": -5 * 60 * 60,
"CST": -6 * 60 * 60,
"MST": -7 * 60 * 60,
"PST": -8 * 60 * 60,
}
// 使用 map 实现集合
func main() {
// 访问 map
offset := timeZone["EST"]
fmt.Println("EST 偏移:", offset) // -18000
// 不存在的 key 返回零值
fmt.Println("未知键:", timeZone["XYZ"]) // 0
// 逗号 ok 惯用法(区分"存在但为零"和"不存在")
seconds, ok := timeZone["UTC"]
fmt.Printf("UTC: seconds=%d, ok=%v\n", seconds, ok) // 0, true
seconds, ok = timeZone["XYZ"]
fmt.Printf("XYZ: seconds=%d, ok=%v\n", seconds, ok) // 0, false
// 用 map 实现集合
attended := map[string]bool{
"张三": true,
"李四": true,
}
check := []string{"张三", "王五"}
for _, person := range check {
if attended[person] {
fmt.Printf("%s 参加了会议\n", person)
} else {
fmt.Printf("%s 未参加\n", person)
}
}
// 只检查存在性(忽略值)
_, present := timeZone["PST"]
fmt.Println("PST 存在:", present)
// 删除 map 条目
delete(timeZone, "PST")
_, present = timeZone["PST"]
fmt.Println("删除后 PST 存在:", present)
}
// offset 函数:查找时区偏移,带友好错误
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
fmt.Println("未知时区:", tz)
return 0
}
9. 打印(Printing)
fmt 包的格式化打印比 C 的 printf 更强大和通用。
package main
import "fmt"
type MyString string
// 自定义类型的 String() 方法
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // 转换为普通 string 避免递归
}
func (m MyString) Fun() string {
return "fun " + string(m)
}
type T struct {
a int
b float64
c string
}
func main() {
// Printf / Println / Sprint
fmt.Printf("Hello %d\n", 23)
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
// %d 自动根据类型选择格式
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
// %v 万能格式(任意类型)
timeZone := map[string]int{
"CST": -6 * 60 * 60,
"EST": -5 * 60 * 60,
}
fmt.Printf("%v\n", timeZone) // map[键值对](按键排序)
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t) // &{7 -2.35 abc def}
fmt.Printf("%+v\n", t) // &{a:7 b:-2.35 c:abc def}
fmt.Printf("%#v\n", t) // &main.T{a:7, b:-2.35, c:"abc\tdef"}
// %T 打印类型
fmt.Printf("%T\n", timeZone) // map[string]int
// %q 打印带引号的字符串
fmt.Printf("%q\n", "hello\t世界") // "hello\t世界"
// String() 方法在自定义类型上的效果
var ms MyString = "hello"
fmt.Println(ms) // MyString=hello
// ... 参数传递
logPrintln("这是日志:", "key=", "value")
// ...int 参数
fmt.Println(minInt(3, 7, 2, 9)) // 2
}
func logPrintln(v ...interface{}) {
// 把 v 当作参数列表传递给另一个可变参数函数
fmt.Println(v...)
}
func minInt(a ...int) int {
min := int(^uint(0) >> 1) // 最大的 int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
10. Append
append 是内建函数,因为它的泛型特性无法用普通 Go 函数实现。
package main
import "fmt"
func main() {
// 基本用法
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
fmt.Println(x) // [1 2 3 4 5 6]
// 追加另一个切片(使用 ... 展开)
y := []int{7, 8, 9}
x = append(x, y...)
fmt.Println(x) // [1 2 3 4 5 6 7 8 9]
// append 字符串到字节切片
var b []byte
b = append(b, "hello"...)
b = append(b, " world"...)
fmt.Println(string(b)) // "hello world"
// nil 切片也可以 append
var s []int
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
// append 返回值必须赋值回原变量(底层数组可能改变)
nums := make([]int, 3, 5) // len=3, cap=5
nums2 := append(nums, 10, 20)
nums[0] = 99
fmt.Println(nums, nums2) // [99 0 0] [99 0 0 10 20](共享底层数组)
}
11. 初始化(Initialization)
11.1 常量与 iota
package main
import "fmt"
type ByteSize float64
const (
_ = iota // 忽略第一个值
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
// 为 ByteSize 实现 String 方法(自动格式化打印)
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
func main() {
fmt.Println(YB) // 1.00YB
fmt.Println(ByteSize(1e13)) // 9.09TB
fmt.Println(KB, MB, GB) // 1.00KB 1.00MB 1.00GB
}
11.2 变量初始化
package main
import (
"fmt"
"os"
)
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
func main() {
fmt.Printf("HOME=%s, USER=%s, GOPATH=%s\n", home, user, gopath)
}
11.3 init 函数
每个文件可以有多个 init 函数,在包初始化时自动执行。
package main
import "fmt"
// 预检查
var configMode string
func init() {
fmt.Println("第一个 init 函数执行")
if user == "" {
fmt.Println("警告: $USER 未设置")
}
if home == "" {
fmt.Println("警告: $HOME 未设置,使用默认值")
home = "/home/" + user
}
}
func init() {
fmt.Println("第二个 init 函数执行")
// 验证配置
configMode = "production"
fmt.Printf("配置模式: %s\n", configMode)
}
func main() {
fmt.Println("main 函数执行")
fmt.Printf("最终值: home=%s, user=%s\n", home, user)
}
12. 方法(Methods)
指针接收者 vs 值接收者
规则:值方法可以在指针和值上调用;指针方法只能在指针上调用(但可寻址的值会自动取地址)。
package main
import (
"fmt"
"io"
)
type ByteSlice []byte
// 值接收者:返回新切片(不修改原值)
func (slice ByteSlice) Append(data []byte) []byte {
l := len(slice)
if l+len(data) > cap(slice) {
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
copy(slice[l:], data)
return slice
}
// 指针接收者:直接修改接收者
func (p *ByteSlice) AppendInPlace(data []byte) {
slice := *p
l := len(slice)
if l+len(data) > cap(slice) {
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
copy(slice[l:], data)
*p = slice
}
// 实现 io.Writer 接口
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
l := len(slice)
if l+len(data) > cap(slice) {
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0 : l+len(data)]
copy(slice[l:], data)
*p = slice
return len(data), nil
}
func main() {
// 值接收者
var b ByteSlice
b = b.Append([]byte("hello"))
fmt.Println(string(b)) // "hello"
// 指针接收者
b.AppendInPlace([]byte(" world"))
fmt.Println(string(b)) // "hello world"
// 实现 io.Writer 后,可以用 fmt.Fprintf
var buf ByteSlice
fmt.Fprintf(&buf, "This hour has %d days\n", 7)
fmt.Println(string(buf)) // "This hour has 7 days\n"
}
13. 接口与其他类型(Interfaces and other types)
13.1 接口基础
接口定义了行为:如果某物能做这个,那它就能用在这里。
package main
import (
"fmt"
"sort"
)
type Sequence []int
// 实现 sort.Interface(Len, Less, Swap)
func (s Sequence) Len() int { return len(s) }
func (s Sequence) Less(i, j int) bool { return s[i] < s[j] }
func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// Copy 返回副本
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// String — 排序后格式化打印
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s)) // 转换为 []int 避免递归
}
func main() {
seq := Sequence{3, 1, 4, 1, 5, 9, 2, 6}
fmt.Println(seq) // [1 1 2 3 4 5 6 9]
// 使用 sort.IntSlice 简化
seq2 := Sequence{9, 8, 7, 6}
s := seq2.Copy()
sort.IntSlice(s).Sort() // 转换类型获取方法
fmt.Println(s)
}
13.2 类型转换(Conversions)
Go 的类型转换可以在相关类型之间进行,用于访问不同的方法集。
package main
import (
"fmt"
"sort"
)
type Sequence2 []int
func (s Sequence2) String() string {
s = s.Copy()
sort.IntSlice(s).Sort() // 转换为 sort.IntSlice 访问 .Sort()
return fmt.Sprint([]int(s))
}
func (s Sequence2) Copy() Sequence2 {
copy := make(Sequence2, 0, len(s))
return append(copy, s...)
}
func main() {
s := Sequence2{5, 3, 1, 4, 2}
fmt.Println(s)
}
13.3 接口转换与类型断言(Type Assertions)
package main
import "fmt"
type Stringer interface {
String() string
}
func toString(value interface{}) string {
// 类型断言 + 逗号 ok
if str, ok := value.(string); ok {
return str
}
if str, ok := value.(Stringer); ok {
return str.String()
}
return fmt.Sprintf("未知类型: %T", value)
}
type User struct {
Name string
}
func (u User) String() string {
return "用户: " + u.Name
}
func main() {
values := []interface{}{
"直接字符串",
User{Name: "张三"},
42,
}
for _, v := range values {
fmt.Println(toString(v))
/*
输出:
直接字符串
用户: 张三
未知类型: int
*/
}
// 类型断言的 panic 风险
var x interface{} = "hello"
// str := x.(int) // 这会 panic!
// 安全写法:使用逗号 ok
str, ok := x.(string)
if ok {
fmt.Println("string:", str)
}
}
13.4 接口与方法
几乎任何类型都可以拥有方法,从而几乎任何类型都能实现接口。
package main
import (
"fmt"
"net/http"
"os"
)
// 整数作为 HTTP handler
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
// Channel 作为 HTTP handler
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
// 函数作为 HTTP handler(http.HandlerFunc 模式)
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
// 演示:用函数类型实现接口
type GreetFunc func(name string) string
func (f GreetFunc) Greet(name string) string {
return f(name)
}
func main() {
// 函数类型实现接口
var greeter GreetFunc = func(name string) string {
return "你好, " + name
}
fmt.Println(greeter.Greet("世界"))
}
14. 空标识符(The blank identifier)
空标识符 _ 是一个写入即丢弃的值。用于需要变量但不需要值的地方。
14.1 多赋值中丢弃值
package main
import (
"fmt"
"os"
)
func main() {
// 丢弃错误(⚠️ 不推荐,仅作示例)
fi, _ := os.Stat("test.txt")
if fi != nil {
fmt.Printf("文件大小: %d\n", fi.Size())
}
// ✅ 正确的做法:检查错误
if _, err := os.Stat("test.txt"); os.IsNotExist(err) {
fmt.Println("文件不存在")
}
// range 中丢弃索引
s := []string{"a", "b", "c"}
for _, v := range s {
fmt.Println(v)
}
// 检查 map 中键是否存在
m := map[string]int{"a": 1, "b": 2}
if _, exists := m["c"]; !exists {
fmt.Println("键 'c' 不存在")
}
}
14.2 抑制未使用导入和变量的编译错误
package main
import (
"fmt" // 使用中
"io" // 临时抑制,开发完成后删除
"log"
"os"
)
// 临时抑制未使用导入
var _ = io.Reader // 调试使用,完成后删除
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// 临时抑制未使用变量
_ = fd // TODO: 后续使用 fd
fmt.Println("编译通过")
}
14.3 为副作用导入
// 导入 net/http/pprof 仅为其副作用(注册 HTTP handlers)
// import _ "net/http/pprof"
14.4 接口检查
package main
import (
"encoding/json"
"fmt"
)
// 检查类型是否实现接口
// 编译时验证:确保 *MyType 实现了 json.Marshaler
type MyType struct {
Value string
}
func (m *MyType) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Value string `json:"value"`
}{m.Value})
}
// 编译时接口检查(全局声明,仅用于类型检查)
var _ json.Marshaler = (*MyType)(nil)
// 运行时接口检查
func checkMarshaler(v interface{}) {
if _, ok := v.(json.Marshaler); ok {
fmt.Printf("%T 实现了 json.Marshaler\n", v)
} else {
fmt.Printf("%T 未实现 json.Marshaler\n", v)
}
}
func main() {
jsonData := json.RawMessage(`{"key": "value"}`)
checkMarshaler(&jsonData)
checkMarshaler("plain string")
}
15. 嵌入(Embedding)
Go 不提供子类化,但通过嵌入实现"借用"实现。
15.1 接口嵌入
package main
import (
"fmt"
"io"
"strings"
)
// 嵌入 Reader 和 Writer 组合成 ReadWriter
type ReadWriter interface {
io.Reader
io.Writer
}
func main() {
// 一个 strings.Reader 实现了 io.Reader
var r io.Reader = strings.NewReader("hello")
// 但不能同时满足 ReadWriter(需要同时有 Read 和 Write)
// 这里只是演示接口嵌入的概念
var rw ReadWriter
fmt.Printf("ReadWriter type: %T\n", rw)
_ = r
}
15.2 结构体嵌入
package main
import (
"fmt"
"io"
"log"
"os"
"strings"
)
// 嵌入 bufio.Reader 和 bufio.Writer
// type ReadWriter struct {
// *bufio.Reader
// *bufio.Writer
// }
// Job 类型嵌入了 *log.Logger
type Job struct {
Command string
*log.Logger // 嵌入 Logger
}
// 自定义 Printf 覆盖默认行为
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
func main() {
// 创建嵌入了 Logger 的 Job
job := &Job{
Command: "deploy.sh",
Logger: log.New(os.Stdout, "JOB: ", log.Ldate|log.Ltime),
}
// 直接调用嵌入类型的方法
job.Println("开始执行...")
job.Print("工作进行中...\n")
// 调用覆盖后的方法
job.Printf("完成 (code=%d)", 0)
// 如果需要访问原始嵌入字段
job.Logger.Println("这是通过原始 Logger 打印的")
// 嵌入带方法的类型:满足接口
// 写一个 ReadWriter
var buf strings.Builder
_ = buf
// 嵌入的 ReadWriter 示例
// 使用组合
type MyReadWriter struct {
reader io.Reader
writer io.Writer
}
_ = MyReadWriter{
reader: strings.NewReader("read"),
writer: os.Stdout,
}
}
15.3 嵌入的冲突规则
package main
import "fmt"
type Base struct {
Name string
}
func (b Base) SayHello() string {
return "你好, " + b.Name
}
type Derived struct {
Base // 嵌入
Extra string
Name string // 覆盖 Base.Name
}
func main() {
d := Derived{
Base: Base{Name: "内部"},
Extra: "额外",
Name: "外部",
}
// 外部 Name 覆盖了 Base.Name
fmt.Println("Name:", d.Name) // "外部"
// 可以通过类型名访问内部字段
fmt.Println("Base.Name:", d.Base.Name) // "内部"
// 嵌入类型的方法被提升
fmt.Println(d.SayHello()) // "你好, 外部"(使用外部 Name)
}
16. 并发(Concurrency)
核心理念
不要通过共享内存来通信;而应该通过通信来共享内存。
16.1 Goroutines
Goroutine 是轻量级线程,由 Go 运行时管理。
package main
import (
"fmt"
"time"
)
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}()
}
func main() {
// 启动 goroutine
go fmt.Println("这是并发执行的")
go func() {
fmt.Println("匿名函数 goroutine")
}()
// 用 goroutine 执行排序(模拟)
list := []int{3, 1, 4, 1, 5}
c := make(chan int)
go func() {
// 模拟排序
time.Sleep(100 * time.Millisecond)
c <- 1 // 发送信号
}()
fmt.Println("等待排序完成...")
<-c // 等待信号
fmt.Println("排序完成", list)
// 延迟打印
Announce("延迟消息", 50*time.Millisecond)
time.Sleep(200 * time.Millisecond) // 等待 goroutine 完成
}
16.2 Channels
Channel 是 goroutine 之间的通信机制。可以带缓冲或无缓冲。
package main
import (
"fmt"
"time"
)
func main() {
// 无缓冲 channel
ci := make(chan int)
go func() {
ci <- 42
}()
fmt.Println("从 channel 接收:", <-ci)
// 带缓冲 channel
ch := make(chan string, 3)
ch <- "a"
ch <- "b"
ch <- "c"
fmt.Println("缓冲 channel:", <-ch, <-ch, <-ch)
// channel 用作信号
done := make(chan bool)
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("工作完成")
done <- true
}()
<-done // 等待
// 用 channel 做信号量控制并发
const MaxOutstanding = 3
sem := make(chan int, MaxOutstanding)
workItems := []string{"任务1", "任务2", "任务3", "任务4", "任务5"}
for _, item := range workItems {
sem <- 1 // 获取信号量;满时阻塞
go func(task string) {
fmt.Println("处理:", task)
time.Sleep(50 * time.Millisecond) // 模拟工作
<-sem // 释放信号量
}(item)
}
time.Sleep(300 * time.Millisecond)
fmt.Println("所有任务完成")
}
16.3 Channel 的 Channel
Channel 是一等公民,可以像其他值一样传递。
package main
import (
"fmt"
"time"
)
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
func sum(a []int) int {
s := 0
for _, v := range a {
s += v
}
return s
}
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
func main() {
// 创建客户端请求 channel
clientRequests := make(chan *Request)
// 启动服务器 handler
go handle(clientRequests)
// 客户端创建请求
resultChan := make(chan int)
req := &Request{
args: []int{3, 4, 5},
f: sum,
resultChan: resultChan,
}
// 发送请求
clientRequests <- req
// 等待结果
result := <-resultChan
fmt.Println("计算结果:", result) // 12
// 更多测试
go handle(clientRequests)
for i := 0; i < 3; i++ {
req := &Request{
args: []int{i, i*2, i*3},
f: sum,
resultChan: make(chan int),
}
clientRequests <- req
fmt.Printf("请求 %d 结果: %d\n", i, <-req.resultChan)
}
time.Sleep(10 * time.Millisecond)
}
16.4 并行计算(Parallelization)
package main
import (
"fmt"
"runtime"
"time"
)
type Vector []float64
// Op 应用一个操作
func (v Vector) Op(x float64) float64 {
return x * 2
}
// DoSome 对向量的一部分执行操作
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // 信号:这部分完成
}
// DoAll 并行执行操作
func (v Vector) DoAll(u Vector) {
numCPU := runtime.GOMAXPROCS(0) // 使用用户指定的 CPU 数
c := make(chan int, numCPU)
for i := 0; i < numCPU; i++ {
start := i * len(v) / numCPU
end := (i + 1) * len(v) / numCPU
go v.DoSome(start, end, u, c)
}
// 等待所有部分完成
for i := 0; i < numCPU; i++ {
<-c
}
}
func main() {
// 创建向量
v := Vector{1, 2, 3, 4, 5, 6, 7, 8}
u := Vector{10, 10, 10, 10, 10, 10, 10, 10}
fmt.Println("原始:", v)
v.DoAll(u)
fmt.Println("处理后:", v) // 每个元素: x + (x*2) = x*3
// runtime.NumCPU 和 GOMAXPROCS
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
time.Sleep(10 * time.Millisecond)
}
16.5 泄漏桶缓冲区(Leaky Buffer)
package main
import (
"fmt"
"time"
)
type Buffer struct {
data []byte
}
func (b *Buffer) String() string {
return string(b.data)
}
var freeList = make(chan *Buffer, 5)
var serverChan = make(chan *Buffer, 10)
// client 模拟从网络读取数据并发送到服务端
func client() {
counter := 0
for {
var b *Buffer
select {
case b = <-freeList:
// 有空闲缓冲区
default:
// 没有空闲,分配新的
b = &Buffer{}
}
// 模拟加载数据
data := fmt.Sprintf("消息 #%d", counter)
b.data = []byte(data)
counter++
serverChan <- b
time.Sleep(20 * time.Millisecond)
}
}
// server 处理消息并回收缓冲区
func server() {
for {
b := <-serverChan
fmt.Println("处理:", b.String())
// 尝试放回空闲列表
select {
case freeList <- b:
// 缓冲区放回空闲列表
default:
// 空闲列表满了,丢弃(GC 会回收)
}
}
}
func main() {
go client()
go server()
time.Sleep(200 * time.Millisecond)
fmt.Println("泄漏桶演示完成")
}
17. 错误(Errors)
Go 中的错误是一个内建接口:
type error interface {
Error() string
}
package main
import (
"fmt"
"os"
"syscall"
)
// 自定义错误类型
type PathError struct {
Op string // "open", "unlink" 等
Path string // 关联的文件
Err error // 系统调用返回的错误
}
func (e *PathError) Error() string {
return e.Op + " " + e.Pat h + ": " + e.Err.Error()
}
func Open(path string) error {
// 模拟打开文件失败
return &PathError{
Op: "open",
Path: path,
Err: syscall.ENOENT,
}
}
func main() {
// 错误是值
err := Open("/etc/passwx")
if err != nil {
fmt.Println("错误:", err)
// 输出: open /etc/passwx: no such file or directory
}
// 使用类型断言获取详细错误信息
_, err = os.Open("/nonexistent")
if err != nil {
if e, ok := err.(*os.PathError); ok {
fmt.Printf("操作: %s, 文件: %s, 错误: %v\n", e.Op, e.Path, e.Err)
}
}
// 重试模式:检查特定错误
for try := 0; try < 2; try++ {
_, err := os.Create("/dev/full") // 模拟磁盘满
if err == nil {
break // 成功
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
fmt.Println("磁盘满,清理空间后重试...")
// deleteTempFiles() // 清理空间
break
}
fmt.Println("其他错误:", err)
break
}
}
18. Panic 与 Recover
panic:停止当前函数执行,开始展开栈(运行所有 defer)recover:在延迟函数中调用,可以恢复 panic 的控制权
package main
import (
"errors"
"fmt"
"log"
)
// 安全的 goroutine 包装器
func safelyDo(work func()) (err error) {
defer func() {
if r := recover(); r != nil {
log.Println("work panic:", r)
err = fmt.Errorf("panic: %v", r)
}
}()
work()
return nil
}
// 使用 panic/recover 做错误处理的模式(包内部使用)
type Error string
func (e Error) Error() string { return string(e) }
type Regexp struct{}
func (r *Regexp) error(err string) {
panic(Error(err))
}
func (r *Regexp) parse(str string) {
if len(str) == 0 {
r.error("空模式")
}
// ... 正常解析
}
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
defer func() {
if e := recover(); e != nil {
regexp = nil
err = e.(Error) // 如果不是 Error 类型,会继续 panic
}
}()
regexp.parse(str)
return
}
func main() {
// 安全的 goroutine
err := safelyDo(func() {
panic("发生严重错误")
})
if err != nil {
fmt.Println("捕获到错误:", err)
}
// 使用 panic/recover 实现错误处理
re, err := Compile("valid")
fmt.Printf("有效模式: re=%v, err=%v\n", re, err)
re, err = Compile("")
fmt.Printf("空模式: re=%v, err=%v\n", re, err)
// 验证整数(非 panic 方式)
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("除零错误:", err)
}
_ = result
}
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("不能除以零")
}
return a / b, nil
}
什么时候该用 panic?
- 真正的不可恢复错误(如初始化失败、不可能发生的情况)
- 库函数内部使用 panic,但对外通过 error 返回
- 绝大多数情况应该返回
error,而不是 panic
19. Web 服务器示例
完整的 Go Web 服务器——QR 码生成器。
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var addr = flag.String("addr", ":1718", "HTTP 服务地址")
var templ = template.Must(template.New("qr").Parse(templateStr))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
log.Printf("启动服务器在 %s", *addr)
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}
const templateStr = `
<html>
<head>
<title>QR 码生成器</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
<input maxLength=1024 size=70 name=s value="" title="要编码为 QR 码的文本">
<input type=submit value="生成 QR 码" name=qr>
</form>
</body>
</html>
`
简化版:可运行的 Web 服务器
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "世界"
}
fmt.Fprintf(w, "你好, %s!", name)
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
})
log.Println("服务器启动在 :8080")
log.Println("访问 http://localhost:8080/hello?name=Go")
// 用 20 行代码启动一个完整的 HTTP 服务器
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
/*
运行:
go run effective_go_study.go
然后在浏览器访问:
http://localhost:8080/hello?name=Go
http://localhost:8080/health
*/
附录:学习要点速查
| 主题 | 核心要点 |
|---|---|
| 格式化 | 用 gofmt,不要自己手动排版 |
| 命名 | 包名简洁,getter 不加 Get,单方法接口用 -er,多用 MixedCaps |
| 控制结构 | if/for/switch 不加括号,if 体以 return 结尾时省略 else |
| 多返回值 | 函数返回 (result, error) 是 Go 的惯用风格 |
| defer | 资源清理,参数立即求值,LIFO 顺序 |
| new vs make | new 分配零值指针;make 初始化 slice/map/channel |
| 切片 | 引用底层数组,传参时元素修改可见 |
| map | 访问不存在的 key 返回零值,用"逗号 ok"区分 |
| 空标识符 | 用于丢弃不需要的值、抑制未使用变量/导入错误 |
| 嵌入 | 替代继承,类型的方法被提升到外层结构体 |
| 接口 | 隐式实现,不声明"implements",组合接口优于继承 |
| 并发 | 不要共享内存,而是通过 channel 传递数据 |
| 错误 | error 是接口,用类型断言获取详细错误 |
| panic/recover | 真正的异常用 panic,库对外暴露 error,不要滥用 |
| goroutine | 轻量级线程,用 channel 同步,用 select 做多路复用 |
本文档基于 Effective Go 整理 文档最后更新:2026-07-22