HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于

Go 硬核笔试学习笔记 - 代码驱动理解版

本笔记用大量可运行的示例代码,帮你彻底理解 Go 并发核心概念


第一章:Goroutine 的真相 - 打破你的错误认知

1.1 Goroutine 没有父子关系 - 用代码证明

很多人以为 goroutine 有父子关系,实际上 所有 goroutine 都是平级的。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 你以为:main 是"父",下面这个是"子"
    go func() {
        fmt.Println("goroutine A 启动")

        // 你以为:A 是"父",B 是"子"
        go func() {
            fmt.Println("goroutine B 启动")
            time.Sleep(2 * time.Second)
            fmt.Println("goroutine B 结束") // 这行会执行吗?
        }()

        time.Sleep(500 * time.Millisecond)
        fmt.Println("goroutine A 结束")
        // A 结束了,B 还在运行吗?
    }()

    time.Sleep(3 * time.Second)
    fmt.Println("main 结束")
}

运行结果:

goroutine A 启动
goroutine B 启动
goroutine A 结束
goroutine B 结束    <-- A 退出后,B 依然正常结束!
main 结束

结论:A 退出不影响 B,因为它们是平级的!


1.2 main 退出 = 全部死亡

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        for i := 0; ; i++ {
            fmt.Println("我还活着:", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()

    time.Sleep(500 * time.Millisecond)
    fmt.Println("main 要退出了,bye~")
    // main 一退出,上面的 goroutine 立即被杀死,没有任何通知
}

运行结果:

我还活着: 0
我还活着: 1
我还活着: 2
我还活着: 3
我还活着: 4
main 要退出了,bye~

goroutine 没有机会执行任何清理逻辑,直接被杀!


1.3 函数 return 不会杀死里面启动的 goroutine

这是一个超级常见的误解:

package main

import (
    "fmt"
    "time"
)

func startWorker() {
    go func() {
        for i := 0; ; i++ {
            fmt.Println("worker 在干活:", i)
            time.Sleep(200 * time.Millisecond)
        }
    }()
    fmt.Println("startWorker 函数要 return 了")
    // 你以为这个 goroutine 会跟着死?错!
}

func main() {
    startWorker()
    fmt.Println("startWorker 已经 return 了")

    time.Sleep(1 * time.Second)
    fmt.Println("main 结束")
}

运行结果:

startWorker 函数要 return 了
startWorker 已经 return 了
worker 在干活: 0
worker 在干活: 1
worker 在干活: 2
worker 在干活: 3
worker 在干活: 4
main 结束

startWorker 早就 return 了,但 goroutine 活得好好的!


第二章:Context 深度理解 - 面试必考

2.1 为什么需要 Context?

没有 Context 的世界:

package main

import (
    "fmt"
    "time"
)

// 问题:这个 goroutine 怎么停?
func badWorker() {
    go func() {
        for {
            fmt.Println("我停不下来啊!")
            time.Sleep(500 * time.Millisecond)
        }
    }()
}

func main() {
    badWorker()
    time.Sleep(2 * time.Second)
    fmt.Println("我想让 worker 停下来,但我做不到...")
    // 只能等 main 退出强杀
}

这就是 goroutine 泄漏!Context 就是为了解决这个问题。


2.2 Context 四种类型速查

类型用途典型场景
Background()根 contextmain 函数、测试入口
TODO()占位符老代码重构时临时使用
WithCancel()手动取消用户点取消、优雅关闭服务
WithTimeout()超时取消HTTP 请求、DB 查询、RPC 调用
WithDeadline()截止时间取消定时任务必须在某时间前完成

2.3 Background vs TODO - 什么时候用哪个?

// ✅ Background:程序入口,明确是根 context
func main() {
    ctx := context.Background()
    startServer(ctx)
}

// ✅ Background:单元测试
func TestSomething(t *testing.T) {
    ctx := context.Background()
    result := doSomething(ctx)
}

// ✅ TODO:老代码改造,暂时不知道用什么 context
func legacyFunction() {
    // 这个函数原来没有 context 参数
    // 现在要加 context,但不确定从哪传入
    // 先用 TODO 占位,后面重构时再改
    ctx := context.TODO()
    doSomething(ctx)
}

本质区别:功能完全一样,TODO() 是语义标记,告诉代码审查者"这里需要后续处理"


2.4 WithCancel - 手动取消

📌 使用场景:取消时机由业务逻辑决定,不是固定时间

场景 1:用户点击取消上传

func handleUpload(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithCancel(r.Context())
    defer cancel()

    // 启动上传
    uploadDone := make(chan error, 1)
    go func() {
        uploadDone <- uploadToS3(ctx, file)
    }()

    select {
    case err := <-uploadDone:
        // 上传完成
        if err != nil {
            http.Error(w, err.Error(), 500)
        }
    case <-r.Context().Done():
        // 用户关闭了浏览器/取消了请求
        cancel() // 取消上传
        return
    }
}

场景 2:启动可停止的后台 Worker

func startWorker(parentCtx context.Context) (stop func()) {
    ctx, cancel := context.WithCancel(parentCtx)

    go func() {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-ctx.Done():
                fmt.Println("worker 停止")
                return
            case <-ticker.C:
                doWork()
            }
        }
    }()

    return cancel // 返回停止函数
}

// 使用
func main() {
    stopWorker := startWorker(context.Background())

    time.Sleep(10 * time.Second)
    stopWorker() // 10 秒后停止
}

场景 3:并发搜索,找到第一个结果就取消其他

func searchFirst(ctx context.Context, query string) *Result {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // 函数退出时取消所有未完成的搜索

    resultCh := make(chan *Result, 3)

    // 同时搜索 3 个引擎
    go func() { resultCh <- searchGoogle(ctx, query) }()
    go func() { resultCh <- searchBing(ctx, query) }()
    go func() { resultCh <- searchBaidu(ctx, query) }()

    return <-resultCh // 返回第一个结果,其他自动取消
}

2.5 WithTimeout - 超时取消

📌 使用场景:知道"最多等多久",超时是相对于"现在"的

场景 1:HTTP 接口调用外部 API(最常用!)

func callPaymentAPI(ctx context.Context, order *Order) (*PayResult, error) {
    // 支付接口最多等 3 秒
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "POST", "https://pay.example.com/api", body)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("支付接口超时")
        }
        return nil, err
    }
    defer resp.Body.Close()
    // ...
}

场景 2:数据库查询超时

func getUser(ctx context.Context, userID string) (*User, error) {
    // DB 查询最多等 1 秒
    ctx, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()

    row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", userID)

    var user User
    if err := row.Scan(&user.ID, &user.Name); err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("数据库查询超时")
        }
        return nil, err
    }
    return &user, nil
}

场景 3:gRPC 调用超时

func getUserProfile(ctx context.Context, userID string) (*Profile, error) {
    // gRPC 调用最多等 2 秒
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    return userClient.GetProfile(ctx, &pb.GetProfileRequest{UserId: userID})
}

场景 4:整个 HTTP Handler 的总超时

func handleOrder(w http.ResponseWriter, r *http.Request) {
    // 整个订单处理最多 10 秒
    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
    defer cancel()

    // 步骤 1:查库存(最多 2 秒)
    stockCtx, stockCancel := context.WithTimeout(ctx, 2*time.Second)
    stock, err := checkStock(stockCtx, productID)
    stockCancel()
    if err != nil { /* 处理超时 */ }

    // 步骤 2:调支付(最多 5 秒)
    payCtx, payCancel := context.WithTimeout(ctx, 5*time.Second)
    payResult, err := callPayment(payCtx, order)
    payCancel()
    if err != nil { /* 处理超时 */ }

    // 步骤 3:创建订单(用剩余时间,约 3 秒)
    order, err := createOrder(ctx, orderInfo)
}

2.6 WithDeadline - 截止时间取消

📌 使用场景:知道"必须在什么时间点之前完成",是绝对时间

WithTimeout vs WithDeadline 本质区别:

// 这两行功能等价:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Second))

// 但语义不同:
// WithTimeout: "从现在开始,最多等 5 秒"
// WithDeadline: "必须在 14:30:05 之前完成"

场景 1:电商秒杀活动,必须在活动结束前完成

func handleFlashSale(w http.ResponseWriter, r *http.Request) {
    // 活动截止时间:2024-12-12 00:00:00
    deadline := time.Date(2024, 12, 12, 0, 0, 0, 0, time.Local)

    ctx, cancel := context.WithDeadline(r.Context(), deadline)
    defer cancel()

    order, err := createFlashSaleOrder(ctx, orderInfo)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "秒杀活动已结束", 400)
            return
        }
        http.Error(w, err.Error(), 500)
        return
    }
    // ...
}

场景 2:定时任务,必须在某个时间点前完成

// 每天凌晨跑的报表任务,必须在 6:00 前完成
func generateDailyReport() error {
    // 截止时间:今天 6:00
    now := time.Now()
    deadline := time.Date(now.Year(), now.Month(), now.Day(), 6, 0, 0, 0, time.Local)

    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    for _, task := range reportTasks {
        if err := task.Run(ctx); err != nil {
            if errors.Is(err, context.DeadlineExceeded) {
                // 记录警报:任务未能在 6:00 前完成
                alertOps("报表任务超时,未能在 6:00 前完成")
                return err
            }
            return err
        }
    }
    return nil
}

场景 3:跨服务调用,deadline 从上游传递

// 上游服务设置了 deadline,下游服务要遵守
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 从请求头解析上游传来的 deadline
    deadlineStr := r.Header.Get("X-Request-Deadline")
    deadline, err := time.Parse(time.RFC3339, deadlineStr)
    if err != nil {
        // 没有 deadline,使用默认超时
        deadline = time.Now().Add(30 * time.Second)
    }

    ctx, cancel := context.WithDeadline(r.Context(), deadline)
    defer cancel()

    // 所有下游调用都用这个 ctx,自动遵守 deadline
    result := processRequest(ctx)
}

场景 4:消息队列消费,消息有过期时间

func consumeMessage(msg *Message) error {
    // 消息的过期时间
    expireAt := msg.CreatedAt.Add(msg.TTL)

    ctx, cancel := context.WithDeadline(context.Background(), expireAt)
    defer cancel()

    if err := processMessage(ctx, msg); err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            // 消息已过期,跳过处理
            log.Printf("消息 %s 已过期,跳过", msg.ID)
            return nil
        }
        return err
    }
    return nil
}

2.7 Context 传播取消 - 父取消子也取消

func main() {
    // 父 context
    parentCtx, parentCancel := context.WithCancel(context.Background())

    // 子 context(从父派生)
    childCtx, _ := context.WithTimeout(parentCtx, 10*time.Second)

    // 孙 context(从子派生)
    grandchildCtx, _ := context.WithCancel(childCtx)

    go func() {
        <-grandchildCtx.Done()
        fmt.Println("孙 context 收到取消信号")
    }()

    time.Sleep(500 * time.Millisecond)
    parentCancel() // 只取消父,子和孙都会收到信号!
}

重要规则:

  • 父取消 → 所有子孙都取消
  • 子取消 → 不影响父
  • 子的 timeout 到了 → 不影响父

实际场景:HTTP 请求链路

// HTTP Handler
func handleAPI(w http.ResponseWriter, r *http.Request) {
    // 整体超时 10 秒
    ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
    defer cancel()

    // 调用服务 A(子 context,最多 3 秒)
    user, _ := serviceA.GetUser(ctx, userID)  // 内部可能再派生子 context

    // 调用服务 B(子 context,最多 5 秒)
    orders, _ := serviceB.GetOrders(ctx, userID)

    // 如果整体 10 秒到了,所有子调用都会被取消
}

2.8 Context 使用的 3 个必须遵守的规则

规则 1:Context 作为第一个参数传递

// ✅ 正确
func GetUser(ctx context.Context, userID string) (*User, error)

// ❌ 错误
func GetUser(userID string, ctx context.Context) (*User, error)

规则 2:不要存储 Context 到结构体

// ❌ 错误
type Service struct {
    ctx context.Context  // 不要这样做!
}

// ✅ 正确:每次调用传入 context
type Service struct {}

func (s *Service) DoSomething(ctx context.Context) error {
    // 使用传入的 ctx
}

规则 3:创建的 cancel 必须调用

// ✅ 正确:defer cancel()
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel()

// ❌ 错误:忘记调用 cancel 会导致资源泄漏
ctx, cancel := context.WithTimeout(parent, time.Second)
// 忘记 cancel()...

第三章:并发控制 - 笔试编程题重灾区

3.0 并发控制选型速查

需求选什么场景
等待多个 goroutine 完成WaitGroup并发下载、并发请求
等待 + 错误处理 + 自动取消errgroup并发调用多个 API
保护共享数据Mutex计数器、状态变更
读多写少的共享数据RWMutex配置缓存、用户信息缓存
只执行一次sync.Once单例初始化、懒加载
简单计数器atomic请求计数、在线人数
等待某条件满足sync.Cond生产者-消费者、资源池
并发安全的 mapsync.Map 或 RWMutex+map缓存(看下面详细对比)

3.1 WaitGroup 基础

📌 场景:并发执行多个任务,等待全部完成

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // 启动前 Add
        go func(id int) {
            defer wg.Done() // 结束时 Done
            fmt.Printf("worker %d 开始\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("worker %d 结束\n", id)
        }(i) // 注意:传参!不传参就是闭包陷阱
    }

    wg.Wait() // 等待所有 goroutine 完成
    fmt.Println("所有 worker 完成")
}

3.2 WaitGroup 经典陷阱 - 闭包问题

错误写法(笔试必考):

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(i) // 错!闭包捕获的是变量 i,不是值
        }()
    }

    wg.Wait()
}

输出(几乎总是):

3
3
3

正确写法 1 - 传参:

for i := 0; i < 3; i++ {
    wg.Add(1)
    go func(n int) { // 通过参数传入
        defer wg.Done()
        fmt.Println(n)
    }(i) // 这里传入当前的 i 值
}

正确写法 2 - 循环内新变量(Go 1.22+ 默认行为):

for i := 0; i < 3; i++ {
    i := i // 创建新变量,shadowing
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println(i)
    }()
}

3.3 WaitGroup 另一个陷阱 - Add 位置

错误写法:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 3; i++ {
        go func(n int) {
            wg.Add(1) // 错!在 goroutine 里面 Add
            defer wg.Done()
            fmt.Println(n)
        }(i)
    }

    wg.Wait() // 可能在 Add 之前就执行了!
    fmt.Println("完成")
}

正确写法:Add 必须在 go 之前!

for i := 0; i < 3; i++ {
    wg.Add(1) // 正确:在 go 之前 Add
    go func(n int) {
        defer wg.Done()
        fmt.Println(n)
    }(i)
}

3.4 Channel 实现信号量(限制并发数)

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    // 信号量:最多允许 2 个 goroutine 同时执行
    semaphore := make(chan struct{}, 2)
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            semaphore <- struct{}{} // 获取信号量(可能阻塞)
            defer func() { <-semaphore }() // 释放信号量

            fmt.Printf("[%s] worker %d 开始\n", time.Now().Format("15:04:05"), id)
            time.Sleep(1 * time.Second) // 模拟工作
            fmt.Printf("[%s] worker %d 结束\n", time.Now().Format("15:04:05"), id)
        }(i)
    }

    wg.Wait()
    fmt.Println("全部完成")
}

运行结果(注意时间):

[10:00:00] worker 1 开始
[10:00:00] worker 2 开始
[10:00:01] worker 1 结束
[10:00:01] worker 3 开始
[10:00:01] worker 2 结束
[10:00:01] worker 4 开始
[10:00:02] worker 3 结束
[10:00:02] worker 5 开始
[10:00:02] worker 4 结束
[10:00:03] worker 5 结束
全部完成

同一时刻最多只有 2 个 worker 在工作!


3.5 Worker Pool 完整实现

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // jobs 关闭后自动退出循环
        fmt.Printf("worker %d 处理 job %d\n", id, job)
        time.Sleep(100 * time.Millisecond)
        results <- job * 2
    }
    fmt.Printf("worker %d 退出\n", id)
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)
    var wg sync.WaitGroup

    // 启动 3 个 worker
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 发送 5 个任务
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // 关闭 jobs,worker 会自动退出

    // 等待所有 worker 完成,然后关闭 results
    go func() {
        wg.Wait()
        close(results)
    }()

    // 收集结果
    for result := range results {
        fmt.Println("结果:", result)
    }

    fmt.Println("全部完成")
}

3.6 Mutex vs RWMutex

📌 选择原则:读写比例决定用哪个

场景选择原因
读写比例 1:1MutexRWMutex 开销更大,没必要
读多写少(如 10:1)RWMutex读锁可以并发,提升吞吐量
写多读少MutexRWMutex 优势发挥不出来
纯写,无读MutexRWMutex 没必要

场景 1:用户信息缓存(读多写少,用 RWMutex)

type UserCache struct {
    mu    sync.RWMutex
    users map[string]*User
}

// 读操作:每秒可能被调用 10000 次
func (c *UserCache) Get(userID string) (*User, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    u, ok := c.users[userID]
    return u, ok
}

// 写操作:每秒可能只被调用 10 次
func (c *UserCache) Set(userID string, user *User) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.users[userID] = user
}

场景 2:订单状态更新(读写频率相近,用 Mutex)

type OrderManager struct {
    mu     sync.Mutex
    orders map[string]*Order
}

// 读写都频繁,直接用 Mutex 更简单
func (m *OrderManager) UpdateStatus(orderID string, status int) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if order, ok := m.orders[orderID]; ok {
        order.Status = status
    }
}

func (m *OrderManager) GetOrder(orderID string) *Order {
    m.mu.Lock()
    defer m.mu.Unlock()
    return m.orders[orderID]
}

场景 3:配置管理(读极多写极少,用 RWMutex)

type ConfigManager struct {
    mu     sync.RWMutex
    config *Config
}

// 每个请求都要读配置
func (m *ConfigManager) Get() *Config {
    m.mu.RLock()
    defer m.mu.RUnlock()
    return m.config
}

// 配置热更新,可能一天才触发一次
func (m *ConfigManager) Reload() error {
    newConfig, err := loadConfigFromFile()
    if err != nil {
        return err
    }
    m.mu.Lock()
    m.config = newConfig
    m.mu.Unlock()
    return nil
}

完整示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

type SafeCounter struct {
    mu    sync.RWMutex
    count int
}

func (c *SafeCounter) Read() int {
    c.mu.RLock() // 读锁:多个 goroutine 可以同时持有
    defer c.mu.RUnlock()
    return c.count
}

func (c *SafeCounter) Write(val int) {
    c.mu.Lock() // 写锁:独占
    defer c.mu.Unlock()
    c.count = val
}

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup

    // 10 个读 goroutine
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 5; j++ {
                val := counter.Read()
                fmt.Printf("reader %d 读到: %d\n", id, val)
                time.Sleep(10 * time.Millisecond)
            }
        }(i)
    }

    // 1 个写 goroutine
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 1; i <= 5; i++ {
            counter.Write(i)
            fmt.Printf("writer 写入: %d\n", i)
            time.Sleep(50 * time.Millisecond)
        }
    }()

    wg.Wait()
}

3.7 RWMutex 死锁陷阱

必考知识点:Lock 内部不能再 RLock!

package main

import (
    "fmt"
    "sync"
)

type BadStruct struct {
    mu sync.RWMutex
}

func (b *BadStruct) Method1() {
    b.mu.Lock()
    defer b.mu.Unlock()
    fmt.Println("Method1 获得写锁")
    b.Method2() // 错!会死锁
}

func (b *BadStruct) Method2() {
    b.mu.RLock() // 死锁!因为 Method1 已经持有写锁
    defer b.mu.RUnlock()
    fmt.Println("Method2 获得读锁")
}

func main() {
    b := &BadStruct{}
    b.Method1() // 永远卡住,死锁
}

正确做法:提取公共逻辑到内部无锁方法

func (b *BadStruct) Method1() {
    b.mu.Lock()
    defer b.mu.Unlock()
    fmt.Println("Method1 获得写锁")
    b.method2Internal() // 调用无锁版本
}

func (b *BadStruct) Method2() {
    b.mu.RLock()
    defer b.mu.RUnlock()
    b.method2Internal()
}

func (b *BadStruct) method2Internal() {
    // 无锁逻辑
    fmt.Println("内部方法执行")
}

第四章:Channel 深度理解

4.1 无缓冲 vs 有缓冲 - 什么时候用哪个?

无缓冲 channel:同步通信,发送方必须等接收方

📌 场景 1:等待 goroutine 完成

func main() {
    done := make(chan struct{}) // 无缓冲

    go func() {
        doWork()
        done <- struct{}{} // 发送信号
    }()

    <-done // 阻塞,直到收到信号
    fmt.Println("worker 完成了")
}

📌 场景 2:请求-响应模式(必须收到响应再继续)

func getResult() Result {
    respCh := make(chan Result) // 无缓冲

    go func() {
        result := doExpensiveWork()
        respCh <- result // 必须有人接收才能继续
    }()

    return <-respCh // 阻塞等待响应
}

有缓冲 channel:异步通信,发送方不用等接收方

📌 场景 1:任务队列(生产者快,消费者慢)

func main() {
    tasks := make(chan Task, 100) // 缓冲 100 个任务

    // 生产者:快速生产
    go func() {
        for _, task := range getTasks() {
            tasks <- task // 缓冲没满就不阻塞
        }
        close(tasks)
    }()

    // 消费者:慢慢处理
    for task := range tasks {
        processSlowly(task)
    }
}

📌 场景 2:日志收集(发送日志不能阻塞业务)

var logCh = make(chan string, 1000)

func init() {
    go func() {
        for log := range logCh {
            writeToFile(log)
        }
    }()
}

func Log(msg string) {
    select {
    case logCh <- msg:
        // 发送成功
    default:
        // 缓冲满了,丢弃(不阻塞主流程)
    }
}

📌 场景 3:限流/信号量

func limitConcurrency(tasks []Task, limit int) {
    sem := make(chan struct{}, limit) // 缓冲大小 = 并发数

    for _, task := range tasks {
        sem <- struct{}{} // 获取信号量
        go func(t Task) {
            defer func() { <-sem }() // 释放信号量
            process(t)
        }(task)
    }
}

选型速查:

场景用哪种原因
等待 goroutine 完成无缓冲需要同步确认
取消/停止信号无缓冲立即响应
生产者-消费者有缓冲解耦速度差异
日志/监控收集有缓冲不能阻塞业务
限流/信号量有缓冲控制并发数
Worker Pool 任务分发有缓冲任务排队

4.2 Channel 三种状态的行为

// 1. nil channel:读写都永久阻塞
var nilCh chan int
// <-nilCh      // 永久阻塞
// nilCh <- 1   // 永久阻塞

// 2. 已关闭 channel:读返回零值,写 panic
closedCh := make(chan int, 1)
close(closedCh)
val, ok := <-closedCh // val=0, ok=false
// closedCh <- 1      // panic!

// 3. 正常 channel:按预期工作
normalCh := make(chan int, 1)
normalCh <- 100
fmt.Println(<-normalCh) // 100

总结表(必须背下来!):

操作nil channel已关闭 channel正常 channel
读 <-永久阻塞零值, false正常
写 <-永久阻塞panic正常
closepanicpanic正常

4.3 close(ch) 的正确使用

📌 场景 1:广播停止信号(通知所有 goroutine)

func main() {
    stop := make(chan struct{})

    // 启动 10 个 worker
    for i := 0; i < 10; i++ {
        go func(id int) {
            <-stop // 所有 worker 都在等
            fmt.Printf("worker %d 停止\n", id)
        }(i)
    }

    time.Sleep(time.Second)
    close(stop) // 一次 close,所有 worker 都收到!
}

// 为什么不用 stop <- struct{}{} 发 10 次?
// 因为发送只能被一个 goroutine 接收
// close 可以同时通知所有等待者

📌 场景 2:通知 range 循环结束

func main() {
    jobs := make(chan int, 10)

    // 消费者
    go func() {
        for job := range jobs {
            process(job)
        }
        fmt.Println("所有任务完成")
    }()

    // 生产者
    for i := 0; i < 100; i++ {
        jobs <- i
    }
    close(jobs) // 告诉消费者没有更多任务了
}

close 的 3 个 panic:

// ❌ 关闭 nil channel → panic
var ch chan int
close(ch)

// ❌ 关闭已关闭的 channel → panic
ch := make(chan int)
close(ch)
close(ch)

// ❌ 向已关闭的 channel 发送 → panic
ch := make(chan int)
close(ch)
ch <- 1

原则:谁发送,谁 close


4.4 只读/只写 channel - 为什么要限制方向?

📌 场景:限制函数职责,防止误操作

// 生产者:只能写
func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch) // 生产者负责 close
}

// 消费者:只能读
func consumer(ch <-chan int) {
    for val := range ch {
        process(val)
    }
}

func main() {
    ch := make(chan int, 10)
    go producer(ch)
    consumer(ch)
}

📌 场景:返回只读 channel,防止调用方误写

// 订阅事件流
func subscribe(topic string) <-chan Event {
    ch := make(chan Event, 100)

    go func() {
        for {
            event := fetchEvent(topic)
            ch <- event
        }
    }()

    return ch // 返回只读 channel
}

func main() {
    events := subscribe("orders")
    for event := range events {
        process(event)
    }
    // events <- Event{}  // 编译错误!
}

4.5 select 多种模式

模式 1:多路监听

📌 场景:worker 同时监听任务和退出信号

func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("收到退出信号")
            return
        case job := <-jobs:
            process(job)
        }
    }
}

模式 2:非阻塞操作

📌 场景:尝试发送,满了就丢弃

func tryLog(msg string) {
    select {
    case logCh <- msg:
        // 成功
    default:
        // 满了,丢弃
    }
}

模式 3:超时控制

📌 场景:最多等 3 秒

func fetchWithTimeout(ch <-chan Result) (Result, error) {
    select {
    case result := <-ch:
        return result, nil
    case <-time.After(3 * time.Second):
        return Result{}, errors.New("timeout")
    }
}

模式 4:定时任务

📌 场景:每秒执行,支持退出

func startTicker(ctx context.Context) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            doPeriodicWork()
        }
    }
}

模式 5:优先级处理

📌 场景:高优先级任务优先

func priorityWorker(high, low <-chan Job) {
    for {
        select {
        case job := <-high:
            process(job)
        default:
            select {
            case job := <-high:
                process(job)
            case job := <-low:
                process(job)
            }
        }
    }
}

第五章:ErrGroup - 现代 Go 并发最佳实践

5.1 基础用法

package main

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())

    // 任务 1
    g.Go(func() error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(100 * time.Millisecond):
            fmt.Println("任务 1 完成")
            return nil
        }
    })

    // 任务 2
    g.Go(func() error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(200 * time.Millisecond):
            fmt.Println("任务 2 完成")
            return nil
        }
    })

    // 等待所有任务完成
    if err := g.Wait(); err != nil {
        fmt.Println("出错:", err)
    } else {
        fmt.Println("全部成功")
    }
}

5.2 任一出错取消全部

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func main() {
    g, ctx := errgroup.WithContext(context.Background())

    // 任务 1:会失败
    g.Go(func() error {
        time.Sleep(100 * time.Millisecond)
        return errors.New("任务 1 失败了")
    })

    // 任务 2:本来要 2 秒,但会被取消
    g.Go(func() error {
        select {
        case <-ctx.Done():
            fmt.Println("任务 2 被取消了")
            return ctx.Err()
        case <-time.After(2 * time.Second):
            fmt.Println("任务 2 完成")
            return nil
        }
    })

    if err := g.Wait(); err != nil {
        fmt.Println("出错:", err)
    }
}

输出:

任务 2 被取消了
出错: 任务 1 失败了

第六章:Panic 和 Recover

6.1 goroutine 里的 panic

重要:一个 goroutine 的 panic 会导致整个程序崩溃!

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        panic("goroutine panic!")
    }()

    time.Sleep(1 * time.Second)
    fmt.Println("这行永远不会执行")
}

6.2 正确处理 goroutine 中的 panic

package main

import (
    "fmt"
    "time"
)

func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("捕获到 panic: %v\n", r)
            }
        }()
        fn()
    }()
}

func main() {
    safeGo(func() {
        panic("我要 panic 了!")
    })

    safeGo(func() {
        fmt.Println("我正常执行")
    })

    time.Sleep(1 * time.Second)
    fmt.Println("main 继续运行")
}

输出:

我正常执行
捕获到 panic: 我要 panic 了!
main 继续运行

第七章:GPM 调度模型(面试必问)

7.1 什么是 GPM?

G (Goroutine): Go 协程,轻量级线程
P (Processor): 逻辑处理器,执行 G 的上下文
M (Machine):   OS 线程,真正执行代码

关系:
┌─────────────────────────────────────────────┐
│                   Go Runtime                 │
│  ┌───────┐   ┌───────┐   ┌───────┐          │
│  │   P   │   │   P   │   │   P   │  (GOMAXPROCS)
│  └───┬───┘   └───┬───┘   └───┬───┘          │
│      │           │           │               │
│  ┌───▼───┐   ┌───▼───┐   ┌───▼───┐          │
│  │   M   │   │   M   │   │   M   │  (OS线程) │
│  └───────┘   └───────┘   └───────┘          │
│                                              │
│  Local Queue: [G][G][G]  [G][G]  [G][G][G]  │
│                                              │
│  Global Queue: [G][G][G][G][G]...            │
└─────────────────────────────────────────────┘

7.2 GOMAXPROCS

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
    fmt.Printf("当前 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))

    // 设置为 1(单核模式)
    old := runtime.GOMAXPROCS(1)
    fmt.Printf("旧值: %d, 新值: %d\n", old, runtime.GOMAXPROCS(0))

    // 恢复
    runtime.GOMAXPROCS(old)
}

第八章:常见面试编程题

8.1 交替打印奇偶数

package main

import (
    "fmt"
    "sync"
)

func main() {
    ch := make(chan struct{})
    var wg sync.WaitGroup
    wg.Add(2)

    // 打印奇数
    go func() {
        defer wg.Done()
        for i := 1; i <= 10; i += 2 {
            <-ch
            fmt.Println("奇数:", i)
            ch <- struct{}{}
        }
    }()

    // 打印偶数
    go func() {
        defer wg.Done()
        for i := 2; i <= 10; i += 2 {
            ch <- struct{}{}
            <-ch
            fmt.Println("偶数:", i)
        }
    }()

    wg.Wait()
}

8.2 实现生产者消费者

package main

import (
    "fmt"
    "sync"
    "time"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 5; i++ {
        fmt.Printf("生产: %d\n", i)
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
}

func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for val := range ch {
        fmt.Printf("消费者 %d 消费: %d\n", id, val)
    }
}

func main() {
    ch := make(chan int, 3)
    var wg sync.WaitGroup

    // 启动生产者
    wg.Add(1)
    go producer(ch, &wg)

    // 启动消费者
    for i := 1; i <= 2; i++ {
        wg.Add(1)
        go consumer(i, ch, &wg)
    }

    // 等生产者完成后关闭 channel
    go func() {
        wg.Wait()
    }()

    // 简单处理:等待一段时间
    time.Sleep(2 * time.Second)
}

8.3 实现超时控制

package main

import (
    "context"
    "fmt"
    "time"
)

func doWork(ctx context.Context) error {
    select {
    case <-time.After(2 * time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    start := time.Now()
    err := doWork(ctx)
    elapsed := time.Since(start)

    if err != nil {
        fmt.Printf("任务失败: %v (耗时 %v)\n", err, elapsed)
    } else {
        fmt.Printf("任务成功 (耗时 %v)\n", elapsed)
    }
}

第九章:黄金法则总结

1. Goroutine 没有父子关系,都是平级
2. main() 退出 = 杀死所有 goroutine
3. 函数 return 不会杀死里面启动的 goroutine
4. Context 是控制 goroutine 生命周期的唯一正确方式
5. select + ctx.Done() 是优雅退出的标准模式
6. WaitGroup.Add() 必须在 go 之前
7. 闭包捕获变量要小心,推荐传参
8. Lock 内部不能再 Lock/RLock,会死锁
9. 关闭 nil channel 会 panic
10. 向已关闭的 channel 发送会 panic
11. 每个 goroutine 都应该有退出机制,否则泄漏
12. 使用 defer recover 防止 goroutine panic 导致程序崩溃

附录:快速对照表

Channel 操作对照表

操作nil空非空满已关闭
读阻塞阻塞成功成功零值
写阻塞成功成功阻塞panic
closepanic成功成功成功panic

锁对照表

场景使用
只有写sync.Mutex
读多写少sync.RWMutex
原子操作sync/atomic
只执行一次sync.Once

Context 对照表

函数用途
Background()根 context
TODO()占位符
WithCancel()手动取消
WithTimeout()超时取消
WithDeadline()截止时间取消
WithValue()传递值

第二部分:Go 语言核心机制(笔试高频)


第十章:Slice 底层原理 - 必考重灾区

10.1 Slice 的内部结构

// runtime/slice.go 中的定义
type slice struct {
    array unsafe.Pointer // 指向底层数组
    len   int            // 长度
    cap   int            // 容量
}

用代码验证:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := make([]int, 3, 5)
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
    fmt.Printf("slice 结构体大小: %d 字节\n", unsafe.Sizeof(s)) // 24 字节 = 8+8+8
}

10.2 Slice 扩容机制(面试必问)

package main

import "fmt"

func main() {
    s := make([]int, 0)
    prevCap := 0

    for i := 0; i < 20; i++ {
        s = append(s, i)
        if cap(s) != prevCap {
            fmt.Printf("len=%2d, cap=%2d (扩容了!)\n", len(s), cap(s))
            prevCap = cap(s)
        }
    }
}

输出:

len= 1, cap= 1 (扩容了!)
len= 2, cap= 2 (扩容了!)
len= 3, cap= 4 (扩容了!)
len= 5, cap= 8 (扩容了!)
len= 9, cap=16 (扩容了!)
len=17, cap=32 (扩容了!)

扩容规则(Go 1.18+):

  • cap < 256:翻倍
  • cap >= 256:增长 (cap + 3*256) / 4,约 1.25 倍

10.3 Slice 共享底层数组(超级大坑)

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}

    // 创建切片,共享底层数组
    slice1 := original[1:3] // [2, 3]
    slice2 := original[2:4] // [3, 4]

    fmt.Println("修改前:")
    fmt.Println("original:", original)
    fmt.Println("slice1:", slice1)
    fmt.Println("slice2:", slice2)

    // 修改 slice1
    slice1[1] = 999 // 修改的是 original[2]

    fmt.Println("\n修改 slice1[1] = 999 后:")
    fmt.Println("original:", original) // [1, 2, 999, 4, 5]
    fmt.Println("slice1:", slice1)     // [2, 999]
    fmt.Println("slice2:", slice2)     // [999, 4] 也被影响了!
}

输出:

修改前:
original: [1 2 3 4 5]
slice1: [2 3]
slice2: [3 4]

修改 slice1[1] = 999 后:
original: [1 2 999 4 5]
slice1: [2 999]
slice2: [999 4]

10.4 append 可能导致底层数组分离

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    slice1 := original[1:3] // len=2, cap=4

    fmt.Printf("slice1: len=%d, cap=%d\n", len(slice1), cap(slice1))

    // append 不触发扩容(cap 够用)
    slice1 = append(slice1, 100)
    fmt.Println("append 后 original:", original) // [1, 2, 3, 100, 5] 被修改!

    // 继续 append 触发扩容
    slice1 = append(slice1, 200, 300, 400)
    fmt.Printf("扩容后 slice1: len=%d, cap=%d\n", len(slice1), cap(slice1))

    // 现在修改 slice1 不影响 original
    slice1[0] = 999
    fmt.Println("最终 original:", original) // [1, 2, 3, 100, 5] 不变
    fmt.Println("最终 slice1:", slice1)     // [999, 3, 100, 200, 300, 400]
}

10.5 如何安全复制 Slice

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}

    // 方法 1: copy 函数
    copied1 := make([]int, len(original))
    copy(copied1, original)

    // 方法 2: append 到 nil slice
    copied2 := append([]int(nil), original...)

    // 方法 3: 使用 slices.Clone (Go 1.21+)
    // copied3 := slices.Clone(original)

    // 验证独立性
    copied1[0] = 999
    copied2[0] = 888

    fmt.Println("original:", original) // [1, 2, 3, 4, 5] 不变
    fmt.Println("copied1:", copied1)   // [999, 2, 3, 4, 5]
    fmt.Println("copied2:", copied2)   // [888, 2, 3, 4, 5]
}

10.6 经典笔试题:Slice 作为函数参数

package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 999        // 会影响原 slice
    s = append(s, 4)  // 不会影响原 slice(如果触发扩容)
    s[0] = 888        // 可能影响也可能不影响
}

func main() {
    s := []int{1, 2, 3}
    fmt.Println("调用前:", s)

    modifySlice(s)
    fmt.Println("调用后:", s) // [999, 2, 3] - 只有第一行生效
}

原因:slice 是值传递,但底层数组是共享的。append 可能创建新数组。


第十一章:Map 底层原理

11.1 Map 的基本特性

package main

import "fmt"

func main() {
    // 1. nil map 可以读,不能写
    var nilMap map[string]int
    fmt.Println(nilMap["key"]) // 0,不 panic
    // nilMap["key"] = 1       // panic!

    // 2. 必须用 make 初始化才能写
    m := make(map[string]int)
    m["key"] = 1

    // 3. 检查 key 是否存在
    val, ok := m["key"]
    fmt.Printf("val=%d, exists=%v\n", val, ok)

    val, ok = m["not_exist"]
    fmt.Printf("val=%d, exists=%v\n", val, ok) // val=0, exists=false
}

11.2 Map 并发读写 panic(面试必考)

package main

import (
    "sync"
)

func main() {
    m := make(map[int]int)
    var wg sync.WaitGroup

    // 并发写 - 会 panic: concurrent map writes
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m[n] = n // panic!
        }(i)
    }

    wg.Wait()
}

解决方案 1: sync.Mutex

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (sm *SafeMap) Set(key string, val int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = val
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    val, ok := sm.m[key]
    return val, ok
}

func main() {
    sm := &SafeMap{m: make(map[string]int)}
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Set(fmt.Sprintf("key%d", n), n)
        }(i)
    }

    wg.Wait()
    fmt.Println("完成")
}

解决方案 2: sync.Map(适合读多写少)

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var wg sync.WaitGroup

    // 并发写
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("key%d", n), n)
        }(i)
    }

    wg.Wait()

    // 遍历
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%s: %d\n", key, value)
        return true // 返回 false 停止遍历
    })
}

11.3 Map 遍历顺序是随机的

package main

import "fmt"

func main() {
    m := map[string]int{
        "a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
    }

    // 多次遍历,顺序不同
    for i := 0; i < 3; i++ {
        fmt.Printf("第 %d 次遍历: ", i+1)
        for k := range m {
            fmt.Printf("%s ", k)
        }
        fmt.Println()
    }
}

Go 故意设计成随机,防止开发者依赖顺序。


第十二章:Interface 与类型系统

12.1 Interface 的内部结构

// 空接口 interface{}
type eface struct {
    _type *_type         // 类型信息
    data  unsafe.Pointer // 数据指针
}

// 非空接口
type iface struct {
    tab  *itab           // 类型+方法表
    data unsafe.Pointer  // 数据指针
}

12.2 nil interface vs nil 值(超级大坑)

package main

import "fmt"

type MyError struct{}

func (e *MyError) Error() string {
    return "my error"
}

func returnsError() error {
    var err *MyError = nil
    return err // 返回的是 (*MyError)(nil),不是 nil!
}

func main() {
    err := returnsError()

    if err == nil {
        fmt.Println("err is nil")
    } else {
        fmt.Println("err is NOT nil!") // 这行会执行!
        fmt.Printf("type=%T, value=%v\n", err, err)
    }
}

输出:

err is NOT nil!
type=*main.MyError, value=<nil>

原因:interface 有两个字段(类型+值),只有两个都是 nil 时,interface 才等于 nil。

正确写法:

func returnsError() error {
    var err *MyError = nil
    if err == nil {
        return nil // 直接返回 nil
    }
    return err
}

12.3 类型断言

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // 方式 1: 不安全的断言(可能 panic)
    s := i.(string)
    fmt.Println(s)

    // 方式 2: 安全的断言
    s, ok := i.(string)
    if ok {
        fmt.Println("是 string:", s)
    }

    // 断言失败
    n, ok := i.(int)
    fmt.Printf("n=%d, ok=%v\n", n, ok) // n=0, ok=false

    // 方式 3: type switch
    switch v := i.(type) {
    case string:
        fmt.Println("string:", v)
    case int:
        fmt.Println("int:", v)
    default:
        fmt.Printf("unknown type: %T\n", v)
    }
}

12.4 空接口 vs any

package main

import "fmt"

func main() {
    // Go 1.18+ any 是 interface{} 的别名
    var a any = 42
    var b interface{} = "hello"

    fmt.Printf("a: %T = %v\n", a, a)
    fmt.Printf("b: %T = %v\n", b, b)
}

第十三章:defer 执行顺序与陷阱

13.1 defer 是 LIFO(后进先出)

package main

import "fmt"

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}

输出:

3
2
1

13.2 defer 参数在声明时求值

package main

import "fmt"

func main() {
    x := 10
    defer fmt.Println("defer x =", x) // x 在这里就被求值了!
    x = 20
    fmt.Println("normal x =", x)
}

输出:

normal x = 20
defer x = 10

13.3 defer 闭包捕获变量

package main

import "fmt"

func main() {
    x := 10
    defer func() {
        fmt.Println("defer x =", x) // 闭包捕获的是变量,不是值
    }()
    x = 20
    fmt.Println("normal x =", x)
}

输出:

normal x = 20
defer x = 20

13.4 defer 与 return 的执行顺序(面试必考)

package main

import "fmt"

func f1() int {
    x := 5
    defer func() {
        x++ // 修改的是局部变量,不影响返回值
    }()
    return x
}

func f2() (x int) { // 命名返回值
    x = 5
    defer func() {
        x++ // 修改的是返回值!
    }()
    return x
}

func f3() (x int) {
    defer func() {
        x++ // 修改返回值
    }()
    return 5 // return 先把 5 赋给 x,然后执行 defer
}

func main() {
    fmt.Println("f1():", f1()) // 5
    fmt.Println("f2():", f2()) // 6
    fmt.Println("f3():", f3()) // 6
}

执行顺序:return 赋值 → defer 执行 → 函数返回


13.5 defer 在循环中的陷阱

package main

import (
    "fmt"
    "os"
)

// 错误写法:defer 在循环中会积累,直到函数结束才执行
func badExample() {
    for i := 0; i < 1000; i++ {
        f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
        defer f.Close() // 所有 Close 都在函数结束时执行!
        // 1000 个文件句柄被占用
    }
}

// 正确写法 1:立即关闭
func goodExample1() {
    for i := 0; i < 1000; i++ {
        f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
        // 处理文件...
        f.Close() // 立即关闭
    }
}

// 正确写法 2:使用匿名函数
func goodExample2() {
    for i := 0; i < 1000; i++ {
        func() {
            f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
            defer f.Close() // 每次循环结束时执行
            // 处理文件...
        }()
    }
}

第十四章:GC 垃圾回收

14.1 三色标记法

白色:未被访问的对象(待回收)
灰色:已被访问但子对象未全部访问
黑色:已被访问且子对象全部访问完成(存活)

GC 过程:
1. 所有对象初始为白色
2. 从根对象开始,标记为灰色
3. 遍历灰色对象的子对象,子对象标记为灰色,当前对象标记为黑色
4. 重复步骤 3,直到没有灰色对象
5. 回收所有白色对象

14.2 GC 触发条件

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // 查看 GC 信息
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)
    fmt.Printf("GC 次数: %d\n", stats.NumGC)
    fmt.Printf("堆内存: %d MB\n", stats.HeapAlloc/1024/1024)

    // 手动触发 GC
    runtime.GC()

    runtime.ReadMemStats(&stats)
    fmt.Printf("GC 后次数: %d\n", stats.NumGC)
}

GC 触发时机:

  1. 堆内存达到阈值(GOGC 控制,默认 100%)
  2. 手动调用 runtime.GC()
  3. 2 分钟内没有 GC

14.3 减少 GC 压力的技巧

package main

import (
    "bytes"
    "sync"
)

// 技巧 1: 使用 sync.Pool 复用对象
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func usePool() {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()

    buf.WriteString("hello")
    // 使用 buf...
}

// 技巧 2: 预分配 slice
func preallocate() {
    // 不好:多次扩容
    var s1 []int
    for i := 0; i < 1000; i++ {
        s1 = append(s1, i)
    }

    // 好:一次分配
    s2 := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        s2 = append(s2, i)
    }
}

// 技巧 3: 避免在循环中创建对象
func avoidAllocation() {
    // 不好
    for i := 0; i < 1000; i++ {
        s := make([]byte, 1024) // 每次都分配
        _ = s
    }

    // 好
    s := make([]byte, 1024) // 只分配一次
    for i := 0; i < 1000; i++ {
        // 复用 s
        _ = s
    }
}

第十五章:sync 包其他重要类型

15.1 sync.Once(只执行一次)

📌 使用场景:需要保证某个初始化操作只执行一次,无论多少 goroutine 同时调用

场景 1:单例模式(最经典)

var once sync.Once
var dbConn *sql.DB

// 数据库连接单例
func GetDB() *sql.DB {
    once.Do(func() {
        var err error
        dbConn, err = sql.Open("mysql", "user:pass@tcp(localhost:3306)/db")
        if err != nil {
            panic(err)
        }
    })
    return dbConn
}

场景 2:配置懒加载

var configOnce sync.Once
var appConfig *Config

func GetConfig() *Config {
    configOnce.Do(func() {
        data, _ := os.ReadFile("config.yaml")
        yaml.Unmarshal(data, &appConfig)
    })
    return appConfig
}

场景 3:日志初始化

var logOnce sync.Once
var logger *zap.Logger

func GetLogger() *zap.Logger {
    logOnce.Do(func() {
        logger, _ = zap.NewProduction()
    })
    return logger
}

完整示例:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once
var instance *Singleton

type Singleton struct {
    Name string
}

func GetInstance() *Singleton {
    once.Do(func() {
        fmt.Println("初始化 Singleton")
        instance = &Singleton{Name: "唯一实例"}
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            s := GetInstance()
            fmt.Printf("goroutine %d: %s\n", id, s.Name)
        }(i)
    }
    wg.Wait()
}

输出:

初始化 Singleton
goroutine 0: 唯一实例
goroutine 1: 唯一实例
goroutine 2: 唯一实例
goroutine 3: 唯一实例
goroutine 4: 唯一实例

"初始化 Singleton" 只打印一次!

sync.Once vs init() 区别:

特性sync.Onceinit()
执行时机第一次调用时(懒加载)程序启动时(饥饿加载)
适用场景可能用不到的资源必须启动时就准备好的资源
错误处理可以在运行时处理只能 panic
依赖其他资源可以等依赖就绪后再初始化必须在启动时就具备所有依赖

15.2 sync.Cond(条件变量)

📌 使用场景:多个 goroutine 需要等待某个条件满足才能继续

场景 1:生产者-消费者,队列为空时消费者等待

type Queue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    items []int
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu)
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    q.items = append(q.items, item)
    q.cond.Signal() // 通知一个等待的消费者
    q.mu.Unlock()
}

func (q *Queue) Get() int {
    q.mu.Lock()
    for len(q.items) == 0 { // 用 for 不用 if,防止虚假唤醒
        q.cond.Wait() // 等待生产者放入数据
    }
    item := q.items[0]
    q.items = q.items[1:]
    q.mu.Unlock()
    return item
}

场景 2:连接池,所有连接用完时等待

type Pool struct {
    mu      sync.Mutex
    cond    *sync.Cond
    conns   []*Conn
    maxSize int
}

func (p *Pool) Get() *Conn {
    p.mu.Lock()
    defer p.mu.Unlock()

    for len(p.conns) == 0 {
        p.cond.Wait() // 等待有连接归还
    }

    conn := p.conns[len(p.conns)-1]
    p.conns = p.conns[:len(p.conns)-1]
    return conn
}

func (p *Pool) Put(conn *Conn) {
    p.mu.Lock()
    p.conns = append(p.conns, conn)
    p.cond.Signal() // 通知一个等待的 goroutine
    p.mu.Unlock()
}

场景 3:广播通知所有 goroutine(用 Broadcast)

type GameRoom struct {
    mu      sync.Mutex
    cond    *sync.Cond
    started bool
}

func (r *GameRoom) WaitForStart(playerID int) {
    r.mu.Lock()
    for !r.started {
        fmt.Printf("玩家 %d 等待游戏开始...\n", playerID)
        r.cond.Wait()
    }
    r.mu.Unlock()
    fmt.Printf("玩家 %d 开始游戏!\n", playerID)
}

func (r *GameRoom) Start() {
    r.mu.Lock()
    r.started = true
    r.cond.Broadcast() // 通知所有等待的玩家
    r.mu.Unlock()
}

基础示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    ready := false

    // 消费者
    go func() {
        mu.Lock()
        for !ready { // 用 for 不用 if,防止虚假唤醒
            cond.Wait() // 等待信号,会自动释放锁
        }
        fmt.Println("消费者:收到信号,开始工作")
        mu.Unlock()
    }()

    time.Sleep(1 * time.Second)

    // 生产者
    mu.Lock()
    ready = true
    cond.Signal() // 唤醒一个等待的 goroutine
    // cond.Broadcast() // 唤醒所有等待的 goroutine
    mu.Unlock()

    time.Sleep(1 * time.Second)
}

Signal vs Broadcast:

方法作用适用场景
Signal()唤醒一个等待的 goroutine任务队列、连接池(只需一个消费者处理)
Broadcast()唤醒所有等待的 goroutine游戏开始、配置更新(所有人都要知道)

15.3 atomic 原子操作

📌 使用场景:对简单类型(int64/uint64/pointer)进行无锁的并发安全操作

场景 1:请求计数器(性能最优!)

type RequestCounter struct {
    total   int64
    success int64
    failed  int64
}

func (c *RequestCounter) RecordSuccess() {
    atomic.AddInt64(&c.total, 1)
    atomic.AddInt64(&c.success, 1)
}

func (c *RequestCounter) RecordFail() {
    atomic.AddInt64(&c.total, 1)
    atomic.AddInt64(&c.failed, 1)
}

func (c *RequestCounter) GetStats() (total, success, failed int64) {
    return atomic.LoadInt64(&c.total),
           atomic.LoadInt64(&c.success),
           atomic.LoadInt64(&c.failed)
}

场景 2:在线人数统计

var onlineUsers int64

func UserLogin() {
    atomic.AddInt64(&onlineUsers, 1)
}

func UserLogout() {
    atomic.AddInt64(&onlineUsers, -1)
}

func GetOnlineCount() int64 {
    return atomic.LoadInt64(&onlineUsers)
}

场景 3:配置热更新(atomic.Value)

type ConfigManager struct {
    config atomic.Value // 存储 *Config
}

func (m *ConfigManager) Get() *Config {
    return m.config.Load().(*Config)
}

func (m *ConfigManager) Update(newConfig *Config) {
    m.config.Store(newConfig) // 原子替换,无锁
}

// 使用:读写分离,读操作完全无锁
cfg := manager.Get()
fmt.Println(cfg.ServerName)

场景 4:状态标记(bool 替代)

type Service struct {
    running int32 // 用 int32 模拟 bool,0=false, 1=true
}

func (s *Service) Start() bool {
    // CAS: 如果是 0(未运行),设为 1(运行中)
    return atomic.CompareAndSwapInt32(&s.running, 0, 1)
}

func (s *Service) Stop() {
    atomic.StoreInt32(&s.running, 0)
}

func (s *Service) IsRunning() bool {
    return atomic.LoadInt32(&s.running) == 1
}

基础示例:

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1) // 原子加 1
        }()
    }

    wg.Wait()
    fmt.Println("counter:", counter) // 一定是 1000

    // 其他原子操作
    atomic.StoreInt64(&counter, 0)           // 原子存储
    val := atomic.LoadInt64(&counter)        // 原子读取
    atomic.CompareAndSwapInt64(&counter, 0, 100) // CAS
    fmt.Printf("val=%d, counter=%d\n", val, counter)
}

atomic vs Mutex 对比:

特性atomicMutex
性能非常高(无锁)相对较低(需要加锁解锁)
适用类型int32/int64/uint32/uint64/pointer任意类型
适用操作简单读写、加减、CAS复杂操作(多步骤修改)
代码复杂度低稍高(需要 Lock/Unlock)

选择原则:

  • 简单计数器 → atomic
  • 复杂结构保护 → Mutex
  • 读多写少的配置 → atomic.Value

第十六章:字符串与 []byte

16.1 字符串是不可变的

package main

import "fmt"

func main() {
    s := "hello"
    // s[0] = 'H' // 编译错误!字符串不可变

    // 必须转换为 []byte
    b := []byte(s)
    b[0] = 'H'
    s = string(b)
    fmt.Println(s) // Hello
}

16.2 字符串与 []byte 转换的性能

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    s := "hello"

    // 普通转换:会复制数据
    b1 := []byte(s)
    s1 := string(b1)

    fmt.Println(b1, s1)

    // 零拷贝转换(危险!仅用于只读场景)
    // Go 1.20+ 可以用 unsafe.StringData 和 unsafe.SliceData
    b2 := unsafe.Slice(unsafe.StringData(s), len(s))
    _ = b2 // 只能读,不能写!
}

16.3 字符串遍历

package main

import "fmt"

func main() {
    s := "hello世界"

    // 按字节遍历
    fmt.Println("按字节:")
    for i := 0; i < len(s); i++ {
        fmt.Printf("%d: %x\n", i, s[i])
    }

    // 按 rune 遍历(正确处理中文)
    fmt.Println("\n按 rune:")
    for i, r := range s {
        fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
    }

    // 字符串长度
    fmt.Printf("\nlen(s)=%d, rune数=%d\n", len(s), len([]rune(s)))
}

输出:

按字节:
0: 68
1: 65
...
5: e4
6: b8
7: 96
...

按 rune:
0: h (U+0068)
1: e (U+0065)
...
5: 世 (U+4E16)
8: 界 (U+754C)

len(s)=11, rune数=7

第十七章:内存逃逸分析

17.1 什么是逃逸

package main

// 逃逸到堆
func escape() *int {
    x := 42
    return &x // x 必须分配到堆,因为函数返回后还要用
}

// 不逃逸,留在栈
func noEscape() int {
    x := 42
    return x
}

func main() {
    _ = escape()
    _ = noEscape()
}

查看逃逸分析:

go build -gcflags="-m" main.go

输出类似:

./main.go:5:2: moved to heap: x

17.2 常见逃逸场景

package main

import "fmt"

// 1. 返回局部变量指针 → 逃逸
func case1() *int {
    x := 1
    return &x
}

// 2. interface{} 参数 → 逃逸
func case2() {
    x := 1
    fmt.Println(x) // Println 参数是 interface{},x 逃逸
}

// 3. 闭包引用 → 逃逸
func case3() func() int {
    x := 1
    return func() int {
        return x // x 被闭包引用,逃逸
    }
}

// 4. slice 扩容 → 可能逃逸
func case4() {
    s := make([]int, 0)
    for i := 0; i < 100; i++ {
        s = append(s, i) // 扩容时可能逃逸
    }
}

// 5. 发送到 channel → 逃逸
func case5(ch chan *int) {
    x := 1
    ch <- &x // x 逃逸
}

17.3 如何减少逃逸

package main

// 1. 传值而不是传指针(如果数据不大)
type SmallStruct struct {
    A, B int
}

func goodPass(s SmallStruct) {}  // 值传递,不逃逸
func badPass(s *SmallStruct) {}  // 可能逃逸

// 2. 预分配足够容量的 slice
func preallocSlice() {
    s := make([]int, 0, 100) // 一次性分配,减少逃逸
    for i := 0; i < 100; i++ {
        s = append(s, i)
    }
}

// 3. 使用 sync.Pool 复用对象
// 见前面 sync.Pool 的例子

func main() {}

第十八章:更多面试高频题

18.1 如何判断两个 slice 是否相等

package main

import (
    "fmt"
    "reflect"
    "slices" // Go 1.21+
)

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    s3 := []int{1, 2, 4}

    // 方法 1: reflect.DeepEqual(慢)
    fmt.Println(reflect.DeepEqual(s1, s2)) // true
    fmt.Println(reflect.DeepEqual(s1, s3)) // false

    // 方法 2: slices.Equal (Go 1.21+,快)
    fmt.Println(slices.Equal(s1, s2)) // true
    fmt.Println(slices.Equal(s1, s3)) // false

    // 方法 3: 手动比较
    equal := len(s1) == len(s2)
    if equal {
        for i := range s1 {
            if s1[i] != s2[i] {
                equal = false
                break
            }
        }
    }
    fmt.Println(equal) // true
}

18.2 实现 LRU 缓存

package main

import (
    "container/list"
    "fmt"
)

type LRUCache struct {
    capacity int
    cache    map[int]*list.Element
    list     *list.List
}

type entry struct {
    key, value int
}

func NewLRUCache(capacity int) *LRUCache {
    return &LRUCache{
        capacity: capacity,
        cache:    make(map[int]*list.Element),
        list:     list.New(),
    }
}

func (c *LRUCache) Get(key int) int {
    if elem, ok := c.cache[key]; ok {
        c.list.MoveToFront(elem)
        return elem.Value.(*entry).value
    }
    return -1
}

func (c *LRUCache) Put(key, value int) {
    if elem, ok := c.cache[key]; ok {
        elem.Value.(*entry).value = value
        c.list.MoveToFront(elem)
        return
    }

    if c.list.Len() >= c.capacity {
        // 删除最久未使用的
        back := c.list.Back()
        if back != nil {
            c.list.Remove(back)
            delete(c.cache, back.Value.(*entry).key)
        }
    }

    elem := c.list.PushFront(&entry{key, value})
    c.cache[key] = elem
}

func main() {
    cache := NewLRUCache(2)
    cache.Put(1, 1)
    cache.Put(2, 2)
    fmt.Println(cache.Get(1)) // 1
    cache.Put(3, 3)           // 淘汰 key=2
    fmt.Println(cache.Get(2)) // -1
    fmt.Println(cache.Get(3)) // 3
}

18.3 实现一个简单的协程池

package main

import (
    "fmt"
    "sync"
    "time"
)

type Pool struct {
    work chan func()
    wg   sync.WaitGroup
}

func NewPool(size int) *Pool {
    p := &Pool{
        work: make(chan func(), 100),
    }

    p.wg.Add(size)
    for i := 0; i < size; i++ {
        go func(id int) {
            defer p.wg.Done()
            for fn := range p.work {
                fn()
            }
            fmt.Printf("worker %d 退出\n", id)
        }(i)
    }

    return p
}

func (p *Pool) Submit(fn func()) {
    p.work <- fn
}

func (p *Pool) Shutdown() {
    close(p.work)
    p.wg.Wait()
}

func main() {
    pool := NewPool(3)

    for i := 0; i < 10; i++ {
        n := i
        pool.Submit(func() {
            fmt.Printf("执行任务 %d\n", n)
            time.Sleep(100 * time.Millisecond)
        })
    }

    pool.Shutdown()
    fmt.Println("所有任务完成")
}

终极速查表

零值表

类型零值
int/float0
string""
boolfalse
pointernil
slicenil
mapnil
channelnil
interfacenil
struct各字段零值

make vs new

函数用途返回值
makeslice/map/chan值
new任何类型指针

常见 panic 场景

1. 空指针解引用
2. 数组/slice 越界
3. 向已关闭 channel 发送
4. 关闭 nil/已关闭 channel
5. 类型断言失败(不带 ok)
6. 并发读写 map

面试答题模板

Q: 描述一下 XXX 的原理?
A:
1. 核心数据结构是...
2. 基本工作流程是...
3. 关键优化/设计点是...
4. 常见坑/最佳实践是...

第三部分:性能对比 & 值传递深度理解


第十九章:性能对比(面试常问)

19.1 数组 vs 切片

package main

import (
    "fmt"
    "testing"
)

// 数组:值类型,传参时完整复制
func sumArray(arr [1000000]int) int {
    sum := 0
    for _, v := range arr {
        sum += v
    }
    return sum
}

// 切片:引用类型(header 24字节),传参只复制 header
func sumSlice(s []int) int {
    sum := 0
    for _, v := range s {
        sum += v
    }
    return sum
}

// Benchmark 对比
func BenchmarkArray(b *testing.B) {
    var arr [1000000]int
    for i := 0; i < b.N; i++ {
        sumArray(arr) // 每次复制 8MB!
    }
}

func BenchmarkSlice(b *testing.B) {
    s := make([]int, 1000000)
    for i := 0; i < b.N; i++ {
        sumSlice(s) // 只复制 24 字节
    }
}

func main() {
    fmt.Println("数组传参:复制整个数组(可能很慢)")
    fmt.Println("切片传参:只复制 header(24字节,很快)")
}

性能对比结果:

BenchmarkArray-8     100    15234567 ns/op    // 慢!每次复制 8MB
BenchmarkSlice-8   10000      123456 ns/op    // 快!只复制 24 字节

结论:

特性数组切片
传参开销O(n) 复制全部O(1) 只复制 header
内存布局连续,栈上(小数组)header 栈上,数据堆上
适用场景固定小数组、性能敏感动态大小、绝大多数场景

19.2 string vs []byte

package main

import (
    "bytes"
    "strings"
    "testing"
)

// 字符串拼接对比
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 1000; j++ {
            s += "a" // 每次都创建新字符串!
        }
    }
}

func BenchmarkStringsBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 1000; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

func BenchmarkBytesBuffer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var buf bytes.Buffer
        for j := 0; j < 1000; j++ {
            buf.WriteString("a")
        }
        _ = buf.String()
    }
}

func BenchmarkByteSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        bs := make([]byte, 0, 1000) // 预分配
        for j := 0; j < 1000; j++ {
            bs = append(bs, 'a')
        }
        _ = string(bs)
    }
}

性能对比结果:

BenchmarkStringConcat-8       1000   1500000 ns/op   530000 B/op  // 最慢!
BenchmarkStringsBuilder-8   200000      8000 ns/op     2000 B/op  // 快
BenchmarkBytesBuffer-8      150000     10000 ns/op     3000 B/op  // 快
BenchmarkByteSlice-8        200000      7000 ns/op     1000 B/op  // 最快

结论:

方法性能适用场景
s += "x"最差永远别用
strings.Builder优秀推荐,大量拼接
bytes.Buffer良好需要 io.Writer 接口
[]byte + append最优知道大概长度时

19.3 Map vs Slice 查找

package main

import "testing"

const size = 10000

func BenchmarkMapLookup(b *testing.B) {
    m := make(map[int]bool, size)
    for i := 0; i < size; i++ {
        m[i] = true
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = m[size/2] // O(1) 查找
    }
}

func BenchmarkSliceLookup(b *testing.B) {
    s := make([]int, size)
    for i := 0; i < size; i++ {
        s[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        target := size / 2
        for _, v := range s { // O(n) 查找
            if v == target {
                break
            }
        }
    }
}

性能对比结果:

BenchmarkMapLookup-8     50000000    25 ns/op   // O(1)
BenchmarkSliceLookup-8     500000  3000 ns/op   // O(n)

19.4 sync.Mutex vs sync.RWMutex vs atomic

package main

import (
    "sync"
    "sync/atomic"
    "testing"
)

func BenchmarkMutexWrite(b *testing.B) {
    var mu sync.Mutex
    var count int
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            mu.Lock()
            count++
            mu.Unlock()
        }
    })
}

func BenchmarkRWMutexWrite(b *testing.B) {
    var mu sync.RWMutex
    var count int
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            mu.Lock()
            count++
            mu.Unlock()
        }
    })
}

func BenchmarkAtomic(b *testing.B) {
    var count int64
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            atomic.AddInt64(&count, 1)
        }
    })
}

性能对比结果:

BenchmarkMutexWrite-8      20000000    80 ns/op
BenchmarkRWMutexWrite-8    15000000   100 ns/op  // 写操作 RWMutex 更慢
BenchmarkAtomic-8          50000000    30 ns/op  // 最快

结论:

场景推荐
简单计数器atomic
读多写少sync.RWMutex
写多或读写均衡sync.Mutex

19.5 channel vs mutex

package main

import (
    "sync"
    "testing"
)

func BenchmarkChannelSync(b *testing.B) {
    ch := make(chan int, 1)
    ch <- 0
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            v := <-ch
            v++
            ch <- v
        }
    })
}

func BenchmarkMutexSync(b *testing.B) {
    var mu sync.Mutex
    var count int
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            mu.Lock()
            count++
            mu.Unlock()
        }
    })
}

性能对比结果:

BenchmarkChannelSync-8   5000000   300 ns/op  // 较慢
BenchmarkMutexSync-8    20000000    80 ns/op  // 较快

结论:

  • mutex 更快:适合保护共享数据
  • channel 更优雅:适合 goroutine 间通信、任务分发
  • Go 谚语:"不要通过共享内存来通信,而要通过通信来共享内存"

19.6 interface{} vs 泛型 (Go 1.18+)

package main

import "testing"

// interface{} 版本
func SumInterface(nums []interface{}) int {
    sum := 0
    for _, n := range nums {
        sum += n.(int) // 类型断言有开销
    }
    return sum
}

// 泛型版本
func SumGeneric[T int | int64 | float64](nums []T) T {
    var sum T
    for _, n := range nums {
        sum += n
    }
    return sum
}

func BenchmarkInterface(b *testing.B) {
    nums := make([]interface{}, 1000)
    for i := range nums {
        nums[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumInterface(nums)
    }
}

func BenchmarkGeneric(b *testing.B) {
    nums := make([]int, 1000)
    for i := range nums {
        nums[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        SumGeneric(nums)
    }
}

性能对比结果:

BenchmarkInterface-8   500000   3000 ns/op  // 类型断言开销
BenchmarkGeneric-8    2000000    700 ns/op  // 无额外开销

第二十章:值传递 vs 引用传递 - 彻底搞清楚!

20.1 Go 只有值传递!但效果不同

核心概念:Go 所有传参都是值传递,但有些类型本身就包含指针

┌─────────────────────────────────────────────────────────────────┐
│                    Go 类型传参行为总表                            │
├─────────────┬─────────────┬─────────────┬─────────────────────────┤
│    类型     │  传递的是   │ 修改会影响  │        原因              │
│             │             │   原数据?  │                         │
├─────────────┼─────────────┼─────────────┼─────────────────────────┤
│   int       │    值       │    ❌ 不会  │ 复制整个值               │
│   string    │    值       │    ❌ 不会  │ 复制 header,但不可变    │
│   bool      │    值       │    ❌ 不会  │ 复制整个值               │
│   float     │    值       │    ❌ 不会  │ 复制整个值               │
│   array     │    值       │    ❌ 不会  │ 复制整个数组             │
│   struct    │    值       │    ❌ 不会  │ 复制整个结构体           │
├─────────────┼─────────────┼─────────────┼─────────────────────────┤
│   *T        │   指针值    │    ✅ 会    │ 复制指针,指向同一内存   │
│   slice     │  header值   │    ✅ 会*   │ 复制 header,共享底层数组│
│   map       │  指针值     │    ✅ 会    │ 复制指针,指向同一 map   │
│   channel   │  指针值     │    ✅ 会    │ 复制指针,指向同一 chan  │
│   func      │  指针值     │    -        │ 函数是引用类型           │
│   interface │  双指针     │    视情况   │ 取决于底层类型           │
└─────────────┴─────────────┴─────────────┴─────────────────────────┘

* slice 的 append 可能导致底层数组分离,需要特别注意

20.2 基本类型:修改不影响原值

package main

import "fmt"

func modifyInt(x int) {
    x = 100
    fmt.Println("函数内 x:", x) // 100
}

func modifyString(s string) {
    s = "modified"
    fmt.Println("函数内 s:", s) // modified
}

func main() {
    a := 42
    modifyInt(a)
    fmt.Println("函数外 a:", a) // 42 (没变!)

    b := "hello"
    modifyString(b)
    fmt.Println("函数外 b:", b) // hello (没变!)
}

输出:

函数内 x: 100
函数外 a: 42
函数内 s: modified
函数外 b: hello

20.3 指针:修改会影响原值

package main

import "fmt"

func modifyByPointer(x *int) {
    *x = 100 // 通过指针修改
}

func main() {
    a := 42
    modifyByPointer(&a)
    fmt.Println("a:", a) // 100 (变了!)
}

20.4 Slice:最容易混淆的类型!

package main

import "fmt"

// 情况 1:修改元素 → 影响原 slice
func modifyElement(s []int) {
    s[0] = 999
}

// 情况 2:append 不扩容 → 可能影响原 slice
func appendNoGrow(s []int) []int {
    return append(s, 100)
}

// 情况 3:append 扩容 → 不影响原 slice
func appendWithGrow(s []int) []int {
    // 添加足够多元素触发扩容
    return append(s, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}

// 情况 4:重新赋值 → 不影响原 slice
func reassign(s []int) {
    s = []int{100, 200, 300}
    fmt.Println("函数内:", s)
}

func main() {
    // 情况 1
    s1 := []int{1, 2, 3}
    modifyElement(s1)
    fmt.Println("情况1 - 修改元素:", s1) // [999, 2, 3] ✅ 影响了

    // 情况 2
    s2 := make([]int, 2, 10) // len=2, cap=10
    s2[0], s2[1] = 1, 2
    original := s2
    _ = appendNoGrow(s2)
    fmt.Println("情况2 - append不扩容, 原slice:", original) // [1, 2]
    fmt.Println("情况2 - 底层数组第3个元素:", original[:3])   // [1, 2, 100] ⚠️ 底层被改了

    // 情况 3
    s3 := []int{1, 2, 3}
    appendWithGrow(s3)
    fmt.Println("情况3 - append扩容:", s3) // [1, 2, 3] ❌ 没影响

    // 情况 4
    s4 := []int{1, 2, 3}
    reassign(s4)
    fmt.Println("情况4 - 重新赋值:", s4) // [1, 2, 3] ❌ 没影响
}

输出:

情况1 - 修改元素: [999 2 3]
情况2 - append不扩容, 原slice: [1 2]
情况2 - 底层数组第3个元素: [1 2 100]
情况3 - append扩容: [1 2 3]
函数内: [100 200 300]
情况4 - 重新赋值: [1 2 3]

Slice 传参规则总结:

┌────────────────────────┬───────────────────┐
│        操作            │   影响原 slice?   │
├────────────────────────┼───────────────────┤
│ s[i] = x (修改元素)    │      ✅ 是        │
│ append 不扩容          │ ⚠️ 底层数组被改   │
│ append 扩容            │      ❌ 否        │
│ s = newSlice (重新赋值)│      ❌ 否        │
└────────────────────────┴───────────────────┘

20.5 Map:修改总是影响原值

package main

import "fmt"

func modifyMap(m map[string]int) {
    m["new"] = 100      // 添加
    m["existing"] = 999 // 修改
    delete(m, "delete") // 删除
}

func reassignMap(m map[string]int) {
    m = make(map[string]int) // 重新赋值
    m["inside"] = 1
    fmt.Println("函数内:", m)
}

func main() {
    m := map[string]int{
        "existing": 1,
        "delete":   2,
    }

    modifyMap(m)
    fmt.Println("修改后:", m) // map[existing:999 new:100] ✅ 全部生效

    m2 := map[string]int{"a": 1}
    reassignMap(m2)
    fmt.Println("重新赋值后:", m2) // map[a:1] ❌ 重新赋值不影响
}

Map 传参规则总结:

┌─────────────────────────┬───────────────────┐
│        操作             │   影响原 map?     │
├─────────────────────────┼───────────────────┤
│ m[k] = v (添加/修改)    │      ✅ 是        │
│ delete(m, k)            │      ✅ 是        │
│ m = newMap (重新赋值)   │      ❌ 否        │
└─────────────────────────┴───────────────────┘

20.6 Struct:值传递 vs 指针传递

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 值传递:不影响原 struct
func modifyByValue(p Person) {
    p.Name = "Modified"
    p.Age = 100
}

// 指针传递:影响原 struct
func modifyByPointer(p *Person) {
    p.Name = "Modified"
    p.Age = 100
}

func main() {
    p1 := Person{Name: "Alice", Age: 30}
    modifyByValue(p1)
    fmt.Println("值传递后:", p1) // {Alice 30} ❌ 没变

    p2 := Person{Name: "Bob", Age: 25}
    modifyByPointer(&p2)
    fmt.Println("指针传递后:", p2) // {Modified 100} ✅ 变了
}

20.7 Struct 中包含引用类型(超级大坑!)

package main

import "fmt"

type Team struct {
    Name    string
    Members []string // slice 是引用类型!
}

func modifyTeam(t Team) {
    t.Name = "New Name"        // 不影响原值
    t.Members[0] = "Modified"  // ⚠️ 会影响原值!
}

func main() {
    team := Team{
        Name:    "Original",
        Members: []string{"Alice", "Bob"},
    }

    modifyTeam(team)
    fmt.Println("Name:", team.Name)       // Original (没变)
    fmt.Println("Members:", team.Members) // [Modified Bob] (变了!)
}

原因图解:

值传递复制了什么?

原 Team                    复制的 Team
┌──────────────────┐      ┌──────────────────┐
│ Name: "Original" │      │ Name: "Original" │ ← 复制了值
│ Members: ────────┼──┐   │ Members: ────────┼──┐
└──────────────────┘  │   └──────────────────┘  │
                      │                         │
                      └────────┬────────────────┘
                               ▼
                        底层数组 (共享!)
                        ["Alice", "Bob"]

20.8 深拷贝 vs 浅拷贝

package main

import (
    "encoding/json"
    "fmt"
)

type Data struct {
    Values []int
    Info   map[string]string
}

// 浅拷贝:只复制顶层,内部引用共享
func shallowCopy(d Data) Data {
    return d
}

// 深拷贝方法 1:手动复制
func deepCopyManual(d Data) Data {
    newData := Data{
        Values: make([]int, len(d.Values)),
        Info:   make(map[string]string),
    }
    copy(newData.Values, d.Values)
    for k, v := range d.Info {
        newData.Info[k] = v
    }
    return newData
}

// 深拷贝方法 2:JSON 序列化(简单但慢)
func deepCopyJSON(d Data) Data {
    b, _ := json.Marshal(d)
    var newData Data
    json.Unmarshal(b, &newData)
    return newData
}

func main() {
    original := Data{
        Values: []int{1, 2, 3},
        Info:   map[string]string{"key": "value"},
    }

    // 浅拷贝测试
    shallow := shallowCopy(original)
    shallow.Values[0] = 999
    shallow.Info["key"] = "modified"
    fmt.Println("浅拷贝后原数据:", original) // Values 和 Info 都被改了!

    // 重置
    original = Data{
        Values: []int{1, 2, 3},
        Info:   map[string]string{"key": "value"},
    }

    // 深拷贝测试
    deep := deepCopyManual(original)
    deep.Values[0] = 999
    deep.Info["key"] = "modified"
    fmt.Println("深拷贝后原数据:", original) // 原数据不变!
}

输出:

浅拷贝后原数据: {[999 2 3] map[key:modified]}
深拷贝后原数据: {[1 2 3] map[key:value]}

20.9 接口的值传递陷阱

package main

import "fmt"

type Counter interface {
    Add()
    Value() int
}

// 值接收者
type ValueCounter struct {
    count int
}

func (c ValueCounter) Add()      { c.count++ }
func (c ValueCounter) Value() int { return c.count }

// 指针接收者
type PointerCounter struct {
    count int
}

func (c *PointerCounter) Add()      { c.count++ }
func (c *PointerCounter) Value() int { return c.count }

func main() {
    // 值接收者:修改不生效
    var vc Counter = ValueCounter{}
    vc.Add()
    vc.Add()
    fmt.Println("ValueCounter:", vc.Value()) // 0 (没变!)

    // 指针接收者:修改生效
    var pc Counter = &PointerCounter{}
    pc.Add()
    pc.Add()
    fmt.Println("PointerCounter:", pc.Value()) // 2 (变了!)
}

20.10 终极决策树:什么时候用指针?

                    需要传参吗?
                        │
            ┌───────────┴───────────┐
            ▼                       ▼
        需要修改原值?            只读?
            │                       │
    ┌───────┴───────┐               ▼
    ▼               ▼           类型是什么?
   是              否               │
    │               │       ┌───────┼───────┐
    ▼               │       ▼       ▼       ▼
  用指针           │    slice    map     其他
    *T             │   channel
                   │       │       │       │
                   │       ▼       ▼       ▼
                   │   直接传   直接传   看大小
                   │  (已含指针)(已含指针)   │
                   │                   ┌───┴───┐
                   │                   ▼       ▼
                   │              小(<64B)  大(>64B)
                   │                   │       │
                   │                   ▼       ▼
                   │               直接传   用指针
                   │                          *T
                   │
                   └─────────────────────────────┐
                                                 │
                            类型是什么?         │
                                 │               │
                    ┌────────────┼────────────┐  │
                    ▼            ▼            ▼  │
               slice/map     struct         基本类型
               channel          │               │
                    │       ┌───┴───┐           ▼
                    ▼       ▼       ▼       直接传
                直接传  小(<64B)  大(>64B)
                       直接传    用指针

简化版规则:

// 1. 需要修改原值 → 用指针
func modify(p *Person) { p.Age = 100 }

// 2. 大 struct (>64字节) → 用指针避免复制开销
func process(big *BigStruct) { ... }

// 3. slice/map/channel → 直接传(它们本身就含指针)
func handle(s []int, m map[string]int, ch chan int) { ... }

// 4. 小 struct + 只读 → 直接传
func read(p Person) string { return p.Name }

// 5. 实现修改状态的方法 → 指针接收者
func (p *Person) SetAge(age int) { p.Age = age }

第二十一章:隐式指针类型详解

21.1 哪些类型是"隐式指针"?

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // Slice: 内部是 (指针, len, cap)
    s := []int{1, 2, 3}
    fmt.Printf("slice 大小: %d 字节\n", unsafe.Sizeof(s)) // 24

    // Map: 内部是指向 hmap 的指针
    m := make(map[string]int)
    fmt.Printf("map 大小: %d 字节\n", unsafe.Sizeof(m)) // 8 (一个指针)

    // Channel: 内部是指向 hchan 的指针
    ch := make(chan int)
    fmt.Printf("channel 大小: %d 字节\n", unsafe.Sizeof(ch)) // 8 (一个指针)

    // String: 内部是 (指针, len)
    str := "hello"
    fmt.Printf("string 大小: %d 字节\n", unsafe.Sizeof(str)) // 16

    // Interface: 内部是 (类型指针, 数据指针)
    var i interface{} = 42
    fmt.Printf("interface 大小: %d 字节\n", unsafe.Sizeof(i)) // 16
}

内部结构图解:

Slice (24 字节)              Map (8 字节)           Channel (8 字节)
┌─────────────────┐         ┌─────────┐            ┌─────────┐
│ ptr  → [1,2,3]  │         │ ptr ────┼──→ hmap    │ ptr ────┼──→ hchan
│ len = 3         │         └─────────┘            └─────────┘
│ cap = 3         │
└─────────────────┘

String (16 字节)             Interface (16 字节)
┌─────────────────┐         ┌─────────────────┐
│ ptr  → "hello"  │         │ type → *_type   │
│ len = 5         │         │ data → 实际数据  │
└─────────────────┘         └─────────────────┘

21.2 为什么 nil slice 和空 slice 不同?

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var nilSlice []int          // nil slice: ptr=nil, len=0, cap=0
    emptySlice := []int{}       // 空 slice: ptr=有值, len=0, cap=0
    makeSlice := make([]int, 0) // 空 slice: ptr=有值, len=0, cap=0

    fmt.Println("nilSlice == nil:", nilSlice == nil)   // true
    fmt.Println("emptySlice == nil:", emptySlice == nil) // false
    fmt.Println("makeSlice == nil:", makeSlice == nil)   // false

    // 但功能上它们一样
    fmt.Println("len:", len(nilSlice), len(emptySlice), len(makeSlice)) // 0 0 0

    // JSON 序列化不同!
    // nilSlice  → null
    // emptySlice → []

    // 判断是否为"空"的正确方式
    fmt.Println("正确判断空:", len(nilSlice) == 0) // true
}

21.3 nil map 可以读不能写

package main

import "fmt"

func main() {
    var nilMap map[string]int

    // 读取:返回零值,不 panic
    val := nilMap["key"]
    fmt.Println("读取 nil map:", val) // 0

    // 写入:panic!
    // nilMap["key"] = 1 // panic: assignment to entry in nil map

    // 正确做法:先初始化
    nilMap = make(map[string]int)
    nilMap["key"] = 1
    fmt.Println("写入后:", nilMap)
}

性能对比速查表

传参开销对比

类型传参开销是否影响原值
int/float/bool4-8 字节否
string16 字节否(不可变)
slice24 字节是(修改元素)
map8 字节是
channel8 字节是
[100]int800 字节否
struct{A,B int}16 字节否
*struct{...}8 字节是

什么时候用指针?

场景建议
需要修改原值用指针
struct > 64 字节用指针
实现修改方法指针接收者
slice/map/channel直接传
小 struct 只读直接传

字符串拼接性能

方法性能场景
+最差永远别用
fmt.Sprintf差格式化需求
strings.Builder优推荐
[]byte + append最优知道长度时

并发原语性能

原语相对性能场景
atomic最快简单计数
sync.Mutex快写多
sync.RWMutex中读多写少
channel较慢通信

第四部分:其他高频考点


第二十二章:init 函数执行顺序

22.1 init 函数基本规则

package main

import "fmt"

// 1. 一个包可以有多个 init 函数
// 2. 同一个文件可以有多个 init 函数
// 3. init 函数不能被调用,不能有参数和返回值

func init() {
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2")
}

func main() {
    fmt.Println("main")
}

输出:

init 1
init 2
main

22.2 多包的 init 执行顺序

执行顺序:
1. 按 import 顺序,递归初始化依赖包
2. 每个包内:const → var → init()
3. 最后执行 main 包的 main()

import 依赖图:
main → A → C
     → B → C

执行顺序:C.init → A.init → B.init → main.init → main.main

注意:C 只初始化一次!
// === pkg/c/c.go ===
package c

import "fmt"

var C = initC()

func initC() string {
    fmt.Println("C: var 初始化")
    return "C"
}

func init() {
    fmt.Println("C: init()")
}

// === pkg/a/a.go ===
package a

import (
    "fmt"
    _ "pkg/c" // 导入 C
)

func init() {
    fmt.Println("A: init()")
}

// === main.go ===
package main

import (
    _ "pkg/a"
    _ "pkg/b"
)

func init() {
    fmt.Println("main: init()")
}

func main() {
    fmt.Println("main: main()")
}

输出:

C: var 初始化
C: init()
A: init()
B: init()
main: init()
main: main()

22.3 init 的常见用途

// 1. 注册驱动
import _ "github.com/go-sql-driver/mysql" // 只执行 init,不使用其他导出

// 2. 初始化配置
var config Config

func init() {
    config = loadConfig()
}

// 3. 检查环境
func init() {
    if os.Getenv("API_KEY") == "" {
        panic("API_KEY not set")
    }
}

第二十三章:for range 陷阱大全

23.1 循环变量复用(Go 1.21 前的大坑)

package main

import "fmt"

func main() {
    nums := []int{1, 2, 3}
    var funcs []func()

    // 错误写法(Go 1.21 之前)
    for _, n := range nums {
        funcs = append(funcs, func() {
            fmt.Println(n) // 闭包捕获的是同一个变量!
        })
    }

    for _, f := range funcs {
        f() // 3 3 3
    }
}

Go 1.22+ 已修复此问题,每次迭代创建新变量

Go 1.21 及之前的正确写法:

// 方法 1: 传参
for _, n := range nums {
    n := n // 创建新变量
    funcs = append(funcs, func() {
        fmt.Println(n)
    })
}

// 方法 2: 通过参数传递
for _, n := range nums {
    funcs = append(funcs, func(x int) func() {
        return func() { fmt.Println(x) }
    }(n))
}

23.2 range 指针陷阱

package main

import "fmt"

type Item struct {
    Name string
}

func main() {
    items := []Item{{"a"}, {"b"}, {"c"}}
    var ptrs []*Item

    // 错误!所有指针指向同一个地址
    for _, item := range items {
        ptrs = append(ptrs, &item) // item 是循环变量,地址不变
    }

    for _, p := range ptrs {
        fmt.Println(p.Name) // c c c
    }
}

正确写法:

// 方法 1: 使用索引
for i := range items {
    ptrs = append(ptrs, &items[i])
}

// 方法 2: 创建局部变量
for _, item := range items {
    item := item // 创建新变量
    ptrs = append(ptrs, &item)
}

23.3 range 遍历时修改

package main

import "fmt"

func main() {
    // 1. 遍历时添加元素(slice)- 不会遍历新元素
    s := []int{1, 2, 3}
    for i, v := range s {
        if i == 0 {
            s = append(s, 4) // 不会遍历到 4
        }
        fmt.Println(v)
    }
    // 输出: 1 2 3
    fmt.Println("最终 s:", s) // [1 2 3 4]

    // 2. 遍历时删除元素(map)- 可能遍历到也可能遍历不到
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    for k := range m {
        if k == "a" {
            delete(m, "b") // b 可能已经遍历过,也可能没有
        }
        fmt.Println(k)
    }
}

23.4 range 值是复制的

package main

import "fmt"

type BigStruct struct {
    Data [1000000]int
}

func main() {
    items := []BigStruct{{}, {}}

    // 不好:每次迭代复制整个结构体
    for _, item := range items {
        _ = item // 复制了 1000000 * 8 = 8MB
    }

    // 好:使用索引
    for i := range items {
        _ = items[i] // 不复制
    }
}

23.5 range channel

package main

import "fmt"

func main() {
    ch := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            ch <- i
        }
        close(ch) // 必须 close,否则 range 会永久阻塞
    }()

    // range channel 会一直读取直到 channel 关闭
    for v := range ch {
        fmt.Println(v)
    }
}

第二十四章:方法集规则(接口实现)

24.1 值接收者 vs 指针接收者

package main

import "fmt"

type Animal interface {
    Speak() string
    SetName(string)
}

type Dog struct {
    Name string
}

// 值接收者
func (d Dog) Speak() string {
    return "Woof!"
}

// 指针接收者
func (d *Dog) SetName(name string) {
    d.Name = name
}

func main() {
    // 值类型:只能调用值接收者方法
    // 指针类型:可以调用值接收者 + 指针接收者方法

    var d1 Dog = Dog{Name: "Buddy"}
    d1.Speak()           // OK
    d1.SetName("Max")    // OK - Go 自动取地址 (&d1).SetName()

    var d2 *Dog = &Dog{Name: "Buddy"}
    d2.Speak()           // OK - Go 自动解引用 (*d2).Speak()
    d2.SetName("Max")    // OK

    // 但接口赋值时规则不同!
    var a1 Animal = &Dog{} // OK - *Dog 实现了所有方法
    // var a2 Animal = Dog{} // 错误!Dog 没有实现 SetName

    _ = a1
}

24.2 方法集规则表

┌───────────────┬─────────────────────────────────────┐
│   接收者类型   │            方法集                   │
├───────────────┼─────────────────────────────────────┤
│   T (值)      │ 只包含值接收者方法                   │
│   *T (指针)   │ 包含值接收者 + 指针接收者方法        │
└───────────────┴─────────────────────────────────────┘

实际调用时:
┌───────────────┬───────────────────┬───────────────────┐
│   变量类型    │ 值接收者 func(T)  │ 指针接收者 func(*T)│
├───────────────┼───────────────────┼───────────────────┤
│   T           │      ✅           │   ✅ (自动 &T)    │
│   *T          │   ✅ (自动 *T)    │      ✅           │
└───────────────┴───────────────────┴───────────────────┘

接口实现时:
┌───────────────┬───────────────────┬───────────────────┐
│   实现类型    │ 值接收者 func(T)  │ 指针接收者 func(*T)│
├───────────────┼───────────────────┼───────────────────┤
│   T           │      ✅           │      ❌           │
│   *T          │      ✅           │      ✅           │
└───────────────┴───────────────────┴───────────────────┘

24.3 为什么有这个规则?

package main

type Counter struct {
    count int
}

// 指针接收者:会修改原值
func (c *Counter) Increment() {
    c.count++
}

func main() {
    // 问题:如果允许值类型实现指针接收者接口...
    var c Counter = Counter{count: 0}

    // Go 可以自动取地址调用方法
    c.Increment() // 等价于 (&c).Increment(),这没问题

    // 但是接口呢?
    // var i SomeInterface = c // 如果允许...
    // i.Increment() // 这里的 i 内部存储的是 c 的副本!
    // 调用 Increment 修改的是副本,不是原来的 c
    // 这会导致困惑,所以 Go 不允许
}

第二十五章:new 与 make 的区别

25.1 对比表

┌─────────┬────────────────┬─────────────┬─────────────────────┐
│  函数   │    适用类型    │   返回值    │       作用          │
├─────────┼────────────────┼─────────────┼─────────────────────┤
│  new    │    任意类型    │   *T        │ 分配零值内存,返回指针│
│  make   │ slice/map/chan │   T         │ 初始化内部结构,返回值│
└─────────┴────────────────┴─────────────┴─────────────────────┘

25.2 代码示例

package main

import "fmt"

func main() {
    // new: 返回指针,值是零值
    p := new(int)
    fmt.Printf("new(int): type=%T, value=%d\n", p, *p) // *int, 0

    s := new([]int)
    fmt.Printf("new([]int): type=%T, value=%v, nil=%v\n", s, *s, *s == nil)
    // *[]int, [], true - 是 nil slice!

    // make: 返回初始化后的值
    s2 := make([]int, 0)
    fmt.Printf("make([]int): type=%T, nil=%v\n", s2, s2 == nil)
    // []int, false - 不是 nil!

    m := make(map[string]int)
    fmt.Printf("make(map): type=%T, nil=%v\n", m, m == nil)
    // map[string]int, false

    ch := make(chan int, 1)
    fmt.Printf("make(chan): type=%T, nil=%v\n", ch, ch == nil)
    // chan int, false
}

25.3 何时用 new,何时用 make

// new 的使用场景(较少)
p := new(int)        // 需要 *int 类型
s := new(MyStruct)   // 等价于 &MyStruct{}

// 通常直接用字面量更清晰
p := &MyStruct{}     // 比 new(MyStruct) 更常用
var i int            // 比 new(int) 更简洁

// make 是必须的(slice/map/chan)
s := make([]int, 10)       // 创建长度为 10 的 slice
m := make(map[string]int)  // 创建可用的 map
ch := make(chan int, 5)    // 创建缓冲区为 5 的 channel

// 常见错误
var m map[string]int
m["key"] = 1  // panic! nil map 不能写

var s []int
s = append(s, 1) // OK,append 可以处理 nil slice

第二十六章:内存泄漏场景

26.1 Goroutine 泄漏

package main

import (
    "fmt"
    "time"
)

// 泄漏场景 1:channel 无人读取
func leak1() {
    ch := make(chan int)
    go func() {
        ch <- 1 // 永久阻塞!没人读
        fmt.Println("这行永远不会执行")
    }()
}

// 泄漏场景 2:channel 无人写入
func leak2() {
    ch := make(chan int)
    go func() {
        <-ch // 永久阻塞!没人写
        fmt.Println("这行永远不会执行")
    }()
}

// 泄漏场景 3:无限循环没有退出
func leak3() {
    go func() {
        for {
            time.Sleep(time.Second)
            // 没有退出条件!
        }
    }()
}

// 正确做法:使用 context 控制
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
            return // 可以退出
        }
    }()
}

26.2 Slice 导致的内存泄漏

package main

import "fmt"

func main() {
    // 场景:从大 slice 中截取小部分
    bigSlice := make([]byte, 1000000) // 1MB

    // 错误:smallSlice 持有 bigSlice 的底层数组引用
    // 即使只用 10 字节,整个 1MB 都无法释放
    smallSlice := bigSlice[:10]

    // 正确:复制需要的部分
    smallSlice2 := make([]byte, 10)
    copy(smallSlice2, bigSlice[:10])

    // bigSlice 可以被 GC 回收
    bigSlice = nil

    _ = smallSlice
    _ = smallSlice2
}

// 另一个场景:slice 中的指针
type User struct {
    Name string
    Data []byte // 大数据
}

func processUsers(users []*User) []*User {
    result := users[:0] // 复用底层数组
    for _, u := range users {
        if u.Name != "" {
            result = append(result, u)
        }
    }
    // 问题:被删除的 User 仍然被底层数组引用
    // 正确做法:
    for i := len(result); i < len(users); i++ {
        users[i] = nil // 清除引用
    }
    return result
}

26.3 time.Ticker 泄漏

package main

import (
    "fmt"
    "time"
)

// 错误:Ticker 不停止会泄漏
func leak() {
    ticker := time.NewTicker(time.Second)
    // ticker 永远不会停止,资源泄漏

    for i := 0; i < 3; i++ {
        <-ticker.C
        fmt.Println("tick")
    }
    // 函数返回,但 ticker goroutine 还在运行
}

// 正确:使用 defer Stop
func noLeak() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop() // 重要!

    for i := 0; i < 3; i++ {
        <-ticker.C
        fmt.Println("tick")
    }
}

26.4 HTTP Body 未关闭

package main

import (
    "io"
    "net/http"
)

// 错误:Body 未关闭导致连接泄漏
func leak() {
    resp, err := http.Get("https://example.com")
    if err != nil {
        return
    }
    // 忘记 resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    _ = body
}

// 正确写法
func noLeak() {
    resp, err := http.Get("https://example.com")
    if err != nil {
        return
    }
    defer resp.Body.Close() // 必须关闭!

    body, _ := io.ReadAll(resp.Body)
    _ = body
}

26.5 闭包持有大对象

package main

func main() {
    var funcs []func()

    for i := 0; i < 100; i++ {
        bigData := make([]byte, 1000000) // 1MB

        // 错误:闭包持有 bigData 引用
        funcs = append(funcs, func() {
            _ = bigData[0] // bigData 无法释放
        })
    }

    // 100MB 内存被占用!
}

// 正确:只捕获需要的部分
func correct() {
    var funcs []func()

    for i := 0; i < 100; i++ {
        bigData := make([]byte, 1000000)
        firstByte := bigData[0] // 只取需要的值

        funcs = append(funcs, func() {
            _ = firstByte // 只持有 1 字节
        })
    }
}

第二十七章:常见编程题

27.1 实现一个并发安全的单例

package main

import (
    "fmt"
    "sync"
)

type Singleton struct {
    Name string
}

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{Name: "singleton"}
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := GetInstance()
            fmt.Printf("%p\n", s) // 所有地址相同
        }()
    }
    wg.Wait()
}

27.2 实现超时的 HTTP 请求

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

func fetchWithTimeout(url string, timeout time.Duration) ([]byte, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func main() {
    body, err := fetchWithTimeout("https://example.com", 5*time.Second)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Body length:", len(body))
}

27.3 使用 channel 实现 Fan-out/Fan-in

package main

import (
    "fmt"
    "sync"
)

// Fan-out: 一个输入分发到多个 worker
// Fan-in: 多个 worker 的输出合并到一个 channel

func fanOut(input <-chan int, workers int) []<-chan int {
    outputs := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        outputs[i] = worker(input)
    }
    return outputs
}

func worker(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for n := range input {
            output <- n * n // 处理:计算平方
        }
    }()
    return output
}

func fanIn(inputs ...<-chan int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup

    for _, ch := range inputs {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for n := range c {
                output <- n
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(output)
    }()

    return output
}

func main() {
    // 创建输入
    input := make(chan int)
    go func() {
        for i := 1; i <= 10; i++ {
            input <- i
        }
        close(input)
    }()

    // Fan-out 到 3 个 worker
    outputs := fanOut(input, 3)

    // Fan-in 合并结果
    result := fanIn(outputs...)

    // 收集结果
    for r := range result {
        fmt.Println(r)
    }
}

27.4 实现限流器 (Rate Limiter)

package main

import (
    "fmt"
    "time"
)

// 令牌桶算法
type RateLimiter struct {
    tokens   chan struct{}
    interval time.Duration
}

func NewRateLimiter(rate int, interval time.Duration) *RateLimiter {
    rl := &RateLimiter{
        tokens:   make(chan struct{}, rate),
        interval: interval,
    }

    // 定期添加令牌
    go func() {
        ticker := time.NewTicker(interval / time.Duration(rate))
        defer ticker.Stop()
        for range ticker.C {
            select {
            case rl.tokens <- struct{}{}:
            default: // 桶满了,丢弃
            }
        }
    }()

    // 初始填满令牌桶
    for i := 0; i < rate; i++ {
        rl.tokens <- struct{}{}
    }

    return rl
}

func (rl *RateLimiter) Allow() bool {
    select {
    case <-rl.tokens:
        return true
    default:
        return false
    }
}

func (rl *RateLimiter) Wait() {
    <-rl.tokens
}

func main() {
    // 每秒 5 个请求
    limiter := NewRateLimiter(5, time.Second)

    for i := 0; i < 10; i++ {
        if limiter.Allow() {
            fmt.Printf("Request %d: allowed at %v\n", i, time.Now().Format("15:04:05.000"))
        } else {
            fmt.Printf("Request %d: rate limited\n", i)
        }
    }
}

27.5 实现优雅关闭 (Graceful Shutdown)

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{
        Addr: ":8080",
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            time.Sleep(2 * time.Second) // 模拟慢请求
            w.Write([]byte("Hello"))
        }),
    }

    // 启动服务器
    go func() {
        fmt.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Printf("Server error: %v\n", err)
        }
    }()

    // 等待中断信号
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    fmt.Println("\nShutting down server...")

    // 给正在处理的请求 30 秒时间完成
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        fmt.Printf("Server shutdown error: %v\n", err)
    }

    fmt.Println("Server gracefully stopped")
}

终极面试速查卡

必背知识点

1. Goroutine 平级,无父子关系
2. main 退出杀死所有 goroutine
3. Go 只有值传递,但 slice/map/channel 含指针
4. nil interface ≠ nil(类型+值都为 nil 才等于 nil)
5. defer 是 LIFO,参数在声明时求值
6. init 顺序:依赖包 → const → var → init → main
7. 方法集:T 只有值方法,*T 有值+指针方法
8. make 用于 slice/map/chan,new 返回指针
9. for range 循环变量复用(Go 1.22 前)
10. channel: nil 阻塞,关闭后读零值写 panic

性能优化口诀

1. 字符串拼接用 Builder
2. slice 预分配容量
3. 小对象用 sync.Pool
4. 简单计数用 atomic
5. 读多写少用 RWMutex
6. 大 struct 传指针
7. 避免循环内分配

并发口诀

1. 通过通信共享内存(channel)
2. 不通过共享内存通信(mutex)
3. select 处理多路 channel
4. context 控制 goroutine 生命周期
5. WaitGroup 等待完成
6. errgroup 任务组合+错误传播

第五部分:进阶知识


第二十八章:reflect 反射

28.1 反射三大法则

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.14

    // 法则 1: 从 interface{} 到反射对象
    v := reflect.ValueOf(x)
    t := reflect.TypeOf(x)
    fmt.Println("Type:", t)           // float64
    fmt.Println("Value:", v)          // 3.14
    fmt.Println("Kind:", v.Kind())    // float64

    // 法则 2: 从反射对象到 interface{}
    y := v.Interface().(float64)
    fmt.Println("Back to float64:", y)

    // 法则 3: 要修改反射对象,值必须可设置(settable)
    // v.SetFloat(2.0) // panic! v 不可设置

    // 正确:传指针
    p := reflect.ValueOf(&x)
    pv := p.Elem() // 获取指针指向的值
    pv.SetFloat(2.0)
    fmt.Println("Modified x:", x) // 2.0
}

28.2 反射常用操作

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string `json:"name" validate:"required"`
    Age  int    `json:"age"`
}

func (p Person) SayHello() string {
    return "Hello, " + p.Name
}

func (p *Person) SetName(name string) {
    p.Name = name
}

func main() {
    p := Person{Name: "Alice", Age: 30}

    // 1. 获取类型信息
    t := reflect.TypeOf(p)
    fmt.Println("Type:", t.Name())        // Person
    fmt.Println("Kind:", t.Kind())        // struct
    fmt.Println("NumField:", t.NumField()) // 2

    // 2. 遍历字段
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s, Type: %s, Tag: %s\n",
            field.Name, field.Type, field.Tag.Get("json"))
    }

    // 3. 获取和设置值
    v := reflect.ValueOf(&p).Elem()
    nameField := v.FieldByName("Name")
    if nameField.CanSet() {
        nameField.SetString("Bob")
    }
    fmt.Println("Modified:", p.Name) // Bob

    // 4. 调用方法
    method := reflect.ValueOf(p).MethodByName("SayHello")
    result := method.Call(nil)
    fmt.Println("Method result:", result[0].String()) // Hello, Bob

    // 5. 获取 Tag
    nameFieldType, _ := t.FieldByName("Name")
    fmt.Println("validate tag:", nameFieldType.Tag.Get("validate")) // required
}

28.3 反射性能对比

package main

import (
    "reflect"
    "testing"
)

type Data struct {
    Value int
}

// 直接访问
func BenchmarkDirect(b *testing.B) {
    d := Data{Value: 42}
    for i := 0; i < b.N; i++ {
        _ = d.Value
    }
}

// 反射访问
func BenchmarkReflect(b *testing.B) {
    d := Data{Value: 42}
    v := reflect.ValueOf(d)
    for i := 0; i < b.N; i++ {
        _ = v.FieldByName("Value").Int()
    }
}

// 缓存反射信息
func BenchmarkReflectCached(b *testing.B) {
    d := Data{Value: 42}
    v := reflect.ValueOf(d)
    field := v.FieldByName("Value")
    for i := 0; i < b.N; i++ {
        _ = field.Int()
    }
}

性能对比:

BenchmarkDirect-8          1000000000    0.3 ns/op
BenchmarkReflect-8         10000000    150 ns/op   // 慢 500 倍!
BenchmarkReflectCached-8   100000000    10 ns/op   // 缓存后好很多

反射性能优化建议:

  1. 缓存 reflect.Type 和 reflect.Value
  2. 避免在热路径使用反射
  3. 考虑使用代码生成替代反射

28.4 反射实现简单的 JSON 序列化

package main

import (
    "fmt"
    "reflect"
    "strings"
)

func SimpleJSON(v interface{}) string {
    val := reflect.ValueOf(v)
    typ := reflect.TypeOf(v)

    if val.Kind() != reflect.Struct {
        return fmt.Sprintf("%v", v)
    }

    var parts []string
    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        value := val.Field(i)

        // 获取 json tag
        jsonTag := field.Tag.Get("json")
        if jsonTag == "" {
            jsonTag = field.Name
        }

        // 简单处理
        var valStr string
        switch value.Kind() {
        case reflect.String:
            valStr = fmt.Sprintf(`"%s"`, value.String())
        default:
            valStr = fmt.Sprintf("%v", value.Interface())
        }

        parts = append(parts, fmt.Sprintf(`"%s":%s`, jsonTag, valStr))
    }

    return "{" + strings.Join(parts, ",") + "}"
}

type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email"`
}

func main() {
    u := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
    fmt.Println(SimpleJSON(u))
    // {"name":"Alice","age":30,"email":"alice@example.com"}
}

第二十九章:unsafe 包

29.1 unsafe.Pointer 基础

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // unsafe.Pointer 是通用指针,可以转换任何指针类型
    var x int64 = 42

    // int64 指针 → unsafe.Pointer → *byte
    p := unsafe.Pointer(&x)
    bp := (*byte)(p)
    fmt.Printf("第一个字节: %d\n", *bp) // 42 (小端序)

    // 获取类型大小和对齐
    fmt.Println("int64 大小:", unsafe.Sizeof(x))    // 8
    fmt.Println("int64 对齐:", unsafe.Alignof(x))   // 8
}

29.2 突破私有字段限制

package main

import (
    "fmt"
    "unsafe"
)

type secret struct {
    public  int
    private int // 小写,私有字段
}

func main() {
    s := secret{public: 1, private: 42}

    // 正常无法访问 s.private

    // 使用 unsafe 访问
    // private 字段偏移 = public 的大小 = 8 字节
    privatePtr := (*int)(unsafe.Pointer(
        uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.private),
    ))

    fmt.Println("private value:", *privatePtr) // 42

    // 修改私有字段
    *privatePtr = 100
    fmt.Println("modified:", *privatePtr) // 100
}

29.3 零拷贝 string 和 []byte 转换

package main

import (
    "fmt"
    "unsafe"
)

// 零拷贝 string → []byte(危险!只能读不能写)
func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

// 零拷贝 []byte → string
func BytesToString(b []byte) string {
    return unsafe.String(unsafe.SliceData(b), len(b))
}

func main() {
    s := "hello"
    b := StringToBytes(s)
    fmt.Println("bytes:", b) // [104 101 108 108 111]

    b2 := []byte{119, 111, 114, 108, 100}
    s2 := BytesToString(b2)
    fmt.Println("string:", s2) // world

    // 警告:不要修改 StringToBytes 返回的 []byte!
    // b[0] = 'H' // 可能导致未定义行为
}

29.4 unsafe 使用规则

/*
unsafe.Pointer 转换规则:

1. *T → unsafe.Pointer → *U
   任何指针可以转换为 unsafe.Pointer,再转换为其他指针

2. unsafe.Pointer → uintptr → 计算 → uintptr → unsafe.Pointer
   可以进行指针运算,但必须在同一表达式中完成

3. 错误示例(GC 可能移动对象):
   u := uintptr(unsafe.Pointer(&x))  // 不要这样!
   // ... 其他代码 ...
   p := unsafe.Pointer(u)  // x 可能已经被移动

4. 正确示例:
   p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + offset)  // 同一表达式
*/

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    A int32
    B int64
    C int32
}

func main() {
    e := Example{A: 1, B: 2, C: 3}

    // 查看内存布局
    fmt.Println("Sizeof Example:", unsafe.Sizeof(e))   // 24 (有填充)
    fmt.Println("Offset A:", unsafe.Offsetof(e.A))     // 0
    fmt.Println("Offset B:", unsafe.Offsetof(e.B))     // 8 (填充了 4 字节)
    fmt.Println("Offset C:", unsafe.Offsetof(e.C))     // 16

    // 内存布局:
    // [A:4字节][填充:4字节][B:8字节][C:4字节][填充:4字节]
}

第二十九章B:错误处理 - error 接口与 errors 包(面试高频)

29B.1 error 接口的本质

📌 error 是 Go 中最简单的接口

// 标准库定义
type error interface {
    Error() string
}

// 任何实现了 Error() string 方法的类型都是 error

场景:为什么 Go 选择返回 error 而不是 throw 异常?

  • 显式处理:调用方必须处理错误,不能忽略
  • 控制流清晰:不会被意外的异常打断
  • 性能:无需栈展开,开销小

29B.2 创建错误的 4 种方式

方式 1:errors.New(最简单)

import "errors"

// 场景:简单的错误信息
err := errors.New("用户不存在")

方式 2:fmt.Errorf(带格式化)

// 场景:需要包含动态信息
userID := "123"
err := fmt.Errorf("用户 %s 不存在", userID)

方式 3:fmt.Errorf + %w(错误包装,Go 1.13+)

// 场景:需要保留原始错误,同时添加上下文
func getUser(id string) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        // 包装错误:保留原始 err,添加上下文
        return nil, fmt.Errorf("查询用户 %s 失败: %w", id, err)
    }
    return user, nil
}

方式 4:自定义 error 类型(最灵活)

// 场景:需要携带额外信息、需要类型判断
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s (id=%s) 不存在", e.Resource, e.ID)
}

// 使用
err := &NotFoundError{Resource: "用户", ID: "123"}

29B.3 errors.Is vs errors.As - 什么时候用哪个?

📌 核心区别:Is 比较值,As 提取类型

errors.Is - 判断错误链中是否包含特定错误值

// 场景:检查是否是某个已知的 sentinel error
import (
    "errors"
    "io"
    "os"
)

func readFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 检查是否是"文件不存在"错误
        if errors.Is(err, os.ErrNotExist) {
            return fmt.Errorf("配置文件不存在,请先创建: %w", err)
        }
        return err
    }
    return nil
}

// 常用的 sentinel errors:
// - io.EOF           读到文件末尾
// - os.ErrNotExist   文件不存在
// - os.ErrPermission 权限不足
// - context.Canceled      context 被取消
// - context.DeadlineExceeded  context 超时
// - sql.ErrNoRows    查询无结果

errors.As - 从错误链中提取特定类型的错误

// 场景:需要获取错误的详细信息
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("字段 %s 校验失败: %s", e.Field, e.Message)
}

func handleRequest(data string) error {
    if err := validate(data); err != nil {
        var validErr *ValidationError
        if errors.As(err, &validErr) {
            // 可以访问具体字段
            log.Printf("校验失败 - 字段: %s, 原因: %s", validErr.Field, validErr.Message)
            return fmt.Errorf("参数错误: %s", validErr.Field)
        }
        return err
    }
    return nil
}

选择指南:

需求用什么例子
判断是否是某个特定错误errors.Iserrors.Is(err, os.ErrNotExist)
判断是否是某个错误类型并获取信息errors.Aserrors.As(err, &myErr)
只打印错误信息err.Error() 或 fmt.Println(err)-

29B.4 错误包装与解包(Go 1.13+ 核心知识)

为什么要包装错误?

// 不包装:丢失调用链信息
func getUser(id string) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, err // 丢失了"是 getUser 里出的错"这个信息
    }
    return user, nil
}

// 包装:保留完整调用链
func getUser(id string) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%s) 失败: %w", id, err)
    }
    return user, nil
}

// 调用链:getUser -> db.Query -> net/dial
// 包装后的错误: "getUser(123) 失败: db query error: connection refused"

errors.Unwrap - 解包错误

// 场景:获取被包装的原始错误
wrappedErr := fmt.Errorf("外层错误: %w", originalErr)
unwrapped := errors.Unwrap(wrappedErr) // 返回 originalErr

// 注意:errors.Is 和 errors.As 会自动递归解包,通常不需要手动调用 Unwrap

29B.5 实战:HTTP 服务的错误处理最佳实践

// 定义业务错误类型
type AppError struct {
    Code    int    // HTTP 状态码
    Message string // 用户看到的消息
    Err     error  // 原始错误(用于日志)
}

func (e *AppError) Error() string {
    return e.Message
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 预定义的错误
var (
    ErrNotFound     = &AppError{Code: 404, Message: "资源不存在"}
    ErrUnauthorized = &AppError{Code: 401, Message: "未授权"}
    ErrBadRequest   = &AppError{Code: 400, Message: "请求参数错误"}
)

// 包装业务错误
func NewNotFoundError(resource string, err error) *AppError {
    return &AppError{
        Code:    404,
        Message: fmt.Sprintf("%s 不存在", resource),
        Err:     err,
    }
}

// Handler 中使用
func getUserHandler(w http.ResponseWriter, r *http.Request) {
    userID := r.URL.Query().Get("id")

    user, err := userService.GetUser(userID)
    if err != nil {
        var appErr *AppError
        if errors.As(err, &appErr) {
            // 返回业务错误
            http.Error(w, appErr.Message, appErr.Code)
            log.Printf("业务错误: %v, 原因: %v", appErr.Message, appErr.Err)
        } else {
            // 未知错误,返回 500
            http.Error(w, "服务器内部错误", 500)
            log.Printf("未知错误: %v", err)
        }
        return
    }

    json.NewEncoder(w).Encode(user)
}

29B.6 error vs panic 选择指南

场景用 error用 panic
文件不存在✅❌
网络超时✅❌
用户输入非法✅❌
数据库查询失败✅❌
配置文件解析失败(启动时)❌✅
程序逻辑 bug(数组越界)❌✅
必要依赖不存在❌✅

记忆口诀:

  • 可恢复的 → error(让调用方决定怎么处理)
  • 不可恢复的 → panic(程序无法继续运行)

第三十章:panic 和 recover 机制详解

30.1 panic 的传播

package main

import "fmt"

func a() {
    fmt.Println("a: start")
    b()
    fmt.Println("a: end") // 不会执行
}

func b() {
    fmt.Println("b: start")
    c()
    fmt.Println("b: end") // 不会执行
}

func c() {
    fmt.Println("c: start")
    panic("something went wrong")
    fmt.Println("c: end") // 不会执行
}

func main() {
    a()
    fmt.Println("main: end") // 不会执行
}

// 输出:
// a: start
// b: start
// c: start
// panic: something went wrong

30.2 defer + recover 捕获 panic

package main

import "fmt"

func a() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("a: recovered from:", r)
        }
    }()

    fmt.Println("a: start")
    b()
    fmt.Println("a: end") // 不会执行
}

func b() {
    fmt.Println("b: start")
    panic("something went wrong")
    fmt.Println("b: end") // 不会执行
}

func main() {
    a()
    fmt.Println("main: end") // 会执行!
}

// 输出:
// a: start
// b: start
// a: recovered from: something went wrong
// main: end

30.3 recover 只能在 defer 中调用

package main

import "fmt"

func main() {
    // 错误:recover 不在 defer 中,无效
    if r := recover(); r != nil {
        fmt.Println("recovered:", r) // 永远不会执行
    }

    // 错误:recover 在嵌套函数中,无效
    defer func() {
        func() {
            if r := recover(); r != nil {
                fmt.Println("nested recover:", r) // 不会捕获
            }
        }()
    }()

    // 正确:recover 直接在 defer 函数中
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r) // 有效
        }
    }()

    panic("test")
}

30.4 panic 的类型

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            // r 是 interface{} 类型
            switch v := r.(type) {
            case string:
                fmt.Println("string panic:", v)
            case error:
                fmt.Println("error panic:", v.Error())
            case int:
                fmt.Println("int panic:", v)
            default:
                fmt.Printf("unknown panic: %T %v\n", v, v)
            }
        }
    }()

    // panic 可以传任何类型
    // panic("string error")
    // panic(errors.New("error type"))
    // panic(42)
    panic(struct{ msg string }{"custom type"})
}

30.5 何时使用 panic

/*
使用 panic 的场景:
1. 程序初始化失败(配置错误、必要资源不存在)
2. 不可恢复的错误(程序 bug,如数组越界)
3. 依赖违反(前置条件不满足)

不使用 panic 的场景:
1. 可预期的错误(文件不存在、网络超时)
2. 用户输入错误
3. 正常的业务逻辑分支

最佳实践:
- 库代码尽量返回 error,不要 panic
- 如果必须 panic,在包边界 recover 并转为 error
*/

package main

import (
    "errors"
    "fmt"
)

// 库函数:内部可能 panic,但对外返回 error
func SafeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = errors.New("division panic")
        }
    }()

    if b == 0 {
        panic("divide by zero")
    }
    return a / b, nil
}

func main() {
    result, err := SafeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

第三十一章:常见设计模式

31.1 函数选项模式 (Functional Options)

package main

import "fmt"

type Server struct {
    host    string
    port    int
    timeout int
    maxConn int
}

// Option 是配置函数类型
type Option func(*Server)

// 各种配置选项
func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConn(maxConn int) Option {
    return func(s *Server) {
        s.maxConn = maxConn
    }
}

// 构造函数
func NewServer(opts ...Option) *Server {
    // 默认值
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30,
        maxConn: 100,
    }

    // 应用选项
    for _, opt := range opts {
        opt(s)
    }

    return s
}

func main() {
    // 使用默认配置
    s1 := NewServer()
    fmt.Printf("Server1: %+v\n", s1)

    // 自定义配置
    s2 := NewServer(
        WithHost("0.0.0.0"),
        WithPort(9000),
        WithTimeout(60),
    )
    fmt.Printf("Server2: %+v\n", s2)
}

31.2 工厂模式

package main

import "fmt"

// 产品接口
type Animal interface {
    Speak() string
}

// 具体产品
type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }

type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }

type Bird struct{}
func (b Bird) Speak() string { return "Tweet!" }

// 简单工厂
func NewAnimal(animalType string) Animal {
    switch animalType {
    case "dog":
        return Dog{}
    case "cat":
        return Cat{}
    case "bird":
        return Bird{}
    default:
        return nil
    }
}

// 注册式工厂(更灵活)
type AnimalFactory struct {
    creators map[string]func() Animal
}

func NewAnimalFactory() *AnimalFactory {
    return &AnimalFactory{
        creators: make(map[string]func() Animal),
    }
}

func (f *AnimalFactory) Register(name string, creator func() Animal) {
    f.creators[name] = creator
}

func (f *AnimalFactory) Create(name string) Animal {
    if creator, ok := f.creators[name]; ok {
        return creator()
    }
    return nil
}

func main() {
    // 简单工厂
    dog := NewAnimal("dog")
    fmt.Println(dog.Speak())

    // 注册式工厂
    factory := NewAnimalFactory()
    factory.Register("dog", func() Animal { return Dog{} })
    factory.Register("cat", func() Animal { return Cat{} })

    animal := factory.Create("cat")
    fmt.Println(animal.Speak())
}

31.3 装饰器模式

package main

import (
    "fmt"
    "time"
)

// 原始函数类型
type Handler func(string) string

// 装饰器:添加日志
func WithLogging(h Handler) Handler {
    return func(s string) string {
        fmt.Printf("[LOG] Input: %s\n", s)
        result := h(s)
        fmt.Printf("[LOG] Output: %s\n", result)
        return result
    }
}

// 装饰器:添加计时
func WithTiming(h Handler) Handler {
    return func(s string) string {
        start := time.Now()
        result := h(s)
        fmt.Printf("[TIME] Duration: %v\n", time.Since(start))
        return result
    }
}

// 装饰器:添加重试
func WithRetry(h Handler, times int) Handler {
    return func(s string) string {
        var result string
        for i := 0; i < times; i++ {
            result = h(s)
            if result != "" {
                return result
            }
            fmt.Printf("[RETRY] Attempt %d failed\n", i+1)
        }
        return result
    }
}

// 原始处理函数
func process(s string) string {
    time.Sleep(100 * time.Millisecond)
    return "processed: " + s
}

func main() {
    // 组合装饰器
    handler := WithLogging(WithTiming(process))

    result := handler("hello")
    fmt.Println("Final result:", result)
}

31.4 中间件模式(HTTP)

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

// 中间件类型
type Middleware func(http.Handler) http.Handler

// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// 认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// 恢复中间件
func RecoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// 链式调用中间件
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

func main() {
    // 业务处理器
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, World!")
    })

    // 应用中间件
    finalHandler := Chain(handler,
        RecoverMiddleware,
        LoggingMiddleware,
        AuthMiddleware,
    )

    http.Handle("/", finalHandler)
    log.Println("Server starting on :8080")
    // http.ListenAndServe(":8080", nil)
}

31.5 观察者模式

package main

import "fmt"

// 观察者接口
type Observer interface {
    Update(event string)
}

// 被观察者
type Subject struct {
    observers []Observer
}

func (s *Subject) Register(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Unregister(o Observer) {
    for i, obs := range s.observers {
        if obs == o {
            s.observers = append(s.observers[:i], s.observers[i+1:]...)
            return
        }
    }
}

func (s *Subject) Notify(event string) {
    for _, o := range s.observers {
        o.Update(event)
    }
}

// 具体观察者
type EmailNotifier struct {
    email string
}

func (e *EmailNotifier) Update(event string) {
    fmt.Printf("Email to %s: %s\n", e.email, event)
}

type SMSNotifier struct {
    phone string
}

func (s *SMSNotifier) Update(event string) {
    fmt.Printf("SMS to %s: %s\n", s.phone, event)
}

func main() {
    subject := &Subject{}

    email := &EmailNotifier{email: "user@example.com"}
    sms := &SMSNotifier{phone: "123-456-7890"}

    subject.Register(email)
    subject.Register(sms)

    subject.Notify("Order created")
    subject.Notify("Order shipped")
}

第三十二章:Go 模块和依赖管理

32.1 go.mod 文件解析

// go.mod 示例
module github.com/myuser/myproject

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/go-redis/redis/v8 v8.11.5
    golang.org/x/sync v0.3.0
)

require (
    // indirect 表示间接依赖
    github.com/bytedance/sonic v1.9.1 // indirect
    github.com/go-playground/validator/v10 v10.14.0 // indirect
)

replace (
    // 替换依赖(本地开发或 fork)
    github.com/old/package => github.com/new/package v1.0.0
    github.com/another/package => ../local/path
)

exclude (
    // 排除特定版本
    github.com/bad/package v1.2.3
)

32.2 常用 go mod 命令

# 初始化模块
go mod init github.com/myuser/myproject

# 下载依赖
go mod download

# 整理依赖(添加缺失的,删除无用的)
go mod tidy

# 查看依赖图
go mod graph

# 查看为什么需要某个依赖
go mod why github.com/some/package

# 验证依赖
go mod verify

# 创建 vendor 目录
go mod vendor

# 编辑 go.mod
go mod edit -require github.com/some/package@v1.0.0
go mod edit -replace github.com/old=github.com/new@v1.0.0

32.3 版本选择规则

Go 使用 MVS (Minimal Version Selection) 算法:
选择满足所有依赖要求的最小版本

示例:
- A 依赖 C v1.1.0
- B 依赖 C v1.2.0
- 结果:选择 C v1.2.0(满足两者的最小版本)

版本格式:
v1.2.3
│ │ └── 补丁版本(bug 修复)
│ └──── 次版本(新功能,向后兼容)
└────── 主版本(破坏性变更)

v0.x.x - 不稳定版本
v1.x.x - 稳定版本
v2+ 需要在 import path 中加版本后缀:
  import "github.com/user/repo/v2"

第三十三章:测试

33.1 单元测试基础

// math.go
package math

func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}
// math_test.go
package math

import (
    "testing"
)

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

// 表格驱动测试(推荐)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -1, 2, 1},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// 测试错误情况
func TestDivide(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("Divide(10, 0) should return error")
    }
}

33.2 Benchmark 性能测试

// math_test.go
package math

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// 重置计时器(排除初始化时间)
func BenchmarkWithSetup(b *testing.B) {
    // 初始化代码
    data := make([]int, 1000)
    for i := range data {
        data[i] = i
    }

    b.ResetTimer() // 重置计时器

    for i := 0; i < b.N; i++ {
        // 实际测试代码
        _ = data[500]
    }
}

// 并行基准测试
func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(2, 3)
        }
    })
}

// 内存分配统计
func BenchmarkAlloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = make([]int, 100)
    }
}
# 运行基准测试
go test -bench=.
go test -bench=BenchmarkAdd
go test -bench=. -benchmem  # 显示内存分配
go test -bench=. -count=5   # 运行 5 次
go test -bench=. -benchtime=3s  # 每个测试运行 3 秒

33.3 Example 示例测试

// math_test.go
package math

import "fmt"

func ExampleAdd() {
    result := Add(2, 3)
    fmt.Println(result)
    // Output: 5
}

func ExampleAdd_negative() {
    result := Add(-1, -2)
    fmt.Println(result)
    // Output: -3
}

// 无序输出
func ExampleMultipleLines() {
    fmt.Println("line 1")
    fmt.Println("line 2")
    // Unordered output:
    // line 2
    // line 1
}

33.4 Mock 和接口测试

// service.go
package service

type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}
// service_test.go
package service

import (
    "errors"
    "testing"
)

// Mock 实现
type MockUserRepo struct {
    users map[int]*User
    err   error
}

func (m *MockUserRepo) GetUser(id int) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return nil, errors.New("user not found")
}

func (m *MockUserRepo) SaveUser(user *User) error {
    return m.err
}

func TestGetUserName(t *testing.T) {
    // 准备 Mock
    mockRepo := &MockUserRepo{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }

    service := &UserService{repo: mockRepo}

    // 测试正常情况
    name, err := service.GetUserName(1)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if name != "Alice" {
        t.Errorf("GetUserName(1) = %s; want Alice", name)
    }

    // 测试错误情况
    _, err = service.GetUserName(999)
    if err == nil {
        t.Error("GetUserName(999) should return error")
    }
}

33.5 测试覆盖率

# 运行测试并生成覆盖率
go test -cover

# 生成覆盖率文件
go test -coverprofile=coverage.out

# 查看覆盖率报告
go tool cover -func=coverage.out

# HTML 报告
go tool cover -html=coverage.out -o coverage.html

# 显示哪些代码没被覆盖
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

33.6 TestMain

// main_test.go
package main

import (
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // 测试前的全局设置
    setup()

    // 运行所有测试
    code := m.Run()

    // 测试后的清理
    teardown()

    os.Exit(code)
}

func setup() {
    // 初始化数据库连接、创建测试数据等
    println("Setting up...")
}

func teardown() {
    // 清理测试数据、关闭连接等
    println("Tearing down...")
}

附加速查表

反射速查

操作代码
获取类型reflect.TypeOf(v)
获取值reflect.ValueOf(v)
获取 Kindv.Kind()
获取字段数t.NumField()
获取字段t.Field(i) / v.FieldByName("Name")
设置值v.Elem().SetInt(10)
调用方法v.MethodByName("Foo").Call(args)
获取 Tagfield.Tag.Get("json")

测试命令速查

命令说明
go test运行测试
go test -v详细输出
go test -run TestXxx运行特定测试
go test -bench=.运行基准测试
go test -cover覆盖率
go test -race竞态检测
go test -short跳过长测试
go test -timeout 30s设置超时

panic/recover 规则

1. panic 会沿调用栈向上传播
2. recover 只能在 defer 中直接调用才有效
3. recover 捕获后,程序从 defer 后继续执行
4. 嵌套的 recover 无效
5. panic(nil) 也能被 recover 捕获(Go 1.21+行为变化)

第六部分:最后 5% 进阶内容


第三十四章:泛型 (Go 1.18+)

34.1 泛型函数

package main

import "fmt"

// 类型参数用 [] 声明
func Min[T int | int64 | float64](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// 使用类型约束
func Sum[T ~int | ~int64 | ~float64](nums []T) T {
    var sum T
    for _, n := range nums {
        sum += n
    }
    return sum
}

func main() {
    // 显式指定类型
    fmt.Println(Min[int](3, 5))       // 3

    // 类型推断(推荐)
    fmt.Println(Min(3.14, 2.71))      // 2.71

    fmt.Println(Sum([]int{1, 2, 3}))  // 6
}

34.2 类型约束 (Constraints)

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// 自定义约束
type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64
}

// ~ 表示底层类型(包括自定义类型)
type MyInt int

func Double[T Number](n T) T {
    return n * 2
}

// 使用标准库约束 (golang.org/x/exp/constraints)
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// comparable 约束:可比较类型
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// any 约束:任意类型
func Print[T any](v T) {
    fmt.Println(v)
}

func main() {
    var x MyInt = 5
    fmt.Println(Double(x))                    // 10
    fmt.Println(Max("apple", "banana"))       // banana
    fmt.Println(Contains([]int{1, 2, 3}, 2))  // true
}

34.3 泛型结构体

package main

import "fmt"

// 泛型栈
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

// 泛型 Map(键值对)
type Pair[K comparable, V any] struct {
    Key   K
    Value V
}

func main() {
    // int 栈
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    val, _ := intStack.Pop()
    fmt.Println(val) // 2

    // string 栈
    strStack := Stack[string]{}
    strStack.Push("hello")
    strStack.Push("world")
    s, _ := strStack.Pop()
    fmt.Println(s) // world
}

34.4 泛型接口

package main

import "fmt"

// 泛型接口
type Container[T any] interface {
    Add(T)
    Get(int) T
    Len() int
}

// 实现泛型接口
type List[T any] struct {
    items []T
}

func (l *List[T]) Add(item T) {
    l.items = append(l.items, item)
}

func (l *List[T]) Get(i int) T {
    return l.items[i]
}

func (l *List[T]) Len() int {
    return len(l.items)
}

// 使用泛型接口
func PrintAll[T any](c Container[T]) {
    for i := 0; i < c.Len(); i++ {
        fmt.Println(c.Get(i))
    }
}

func main() {
    list := &List[string]{}
    list.Add("a")
    list.Add("b")
    PrintAll[string](list)
}

34.5 泛型最佳实践

/*
何时使用泛型:
1. 通用数据结构(Stack、Queue、Set、Tree)
2. 通用算法(Sort、Filter、Map、Reduce)
3. 减少 interface{} 的类型断言

何时不使用泛型:
1. 只有一两种类型,直接写多个函数更清晰
2. 不同类型需要不同的实现逻辑
3. 为了泛型而泛型(保持简单)
*/

// 实用泛型函数
func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

func Filter[T any](slice []T, f func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, init U, f func(U, T) U) U {
    result := init
    for _, v := range slice {
        result = f(result, v)
    }
    return result
}

// 使用示例
func main() {
    nums := []int{1, 2, 3, 4, 5}

    // Map: 每个元素乘 2
    doubled := Map(nums, func(n int) int { return n * 2 })
    // [2, 4, 6, 8, 10]

    // Filter: 偶数
    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    // [2, 4]

    // Reduce: 求和
    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    // 15
}

第三十五章:pprof 性能分析

35.1 CPU 分析

package main

import (
    "os"
    "runtime/pprof"
)

func main() {
    // 创建 CPU profile 文件
    f, _ := os.Create("cpu.prof")
    defer f.Close()

    // 开始 CPU 分析
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    // 你的代码
    doWork()
}

func doWork() {
    // 模拟工作
    for i := 0; i < 1000000; i++ {
        _ = i * i
    }
}
# 分析 CPU profile
go tool pprof cpu.prof

# 常用命令
(pprof) top        # 显示最耗时的函数
(pprof) list doWork # 显示函数的逐行分析
(pprof) web        # 生成 SVG 图(需要 graphviz)

35.2 内存分析

package main

import (
    "os"
    "runtime"
    "runtime/pprof"
)

func main() {
    // 执行代码
    doAllocations()

    // 写入内存 profile
    f, _ := os.Create("mem.prof")
    defer f.Close()

    runtime.GC() // 先 GC,获得准确数据
    pprof.WriteHeapProfile(f)
}

func doAllocations() {
    var data [][]byte
    for i := 0; i < 1000; i++ {
        data = append(data, make([]byte, 1024))
    }
    _ = data
}
# 分析内存
go tool pprof mem.prof

# 查看分配
(pprof) top
(pprof) alloc_space  # 按分配空间排序
(pprof) inuse_space  # 按使用中的空间排序

35.3 HTTP pprof(生产环境推荐)

package main

import (
    "net/http"
    _ "net/http/pprof" // 自动注册 /debug/pprof 路由
)

func main() {
    // 业务处理
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello"))
    })

    // 启动服务(pprof 自动可用)
    http.ListenAndServe(":8080", nil)
}
# 访问 pprof 页面
http://localhost:8080/debug/pprof/

# 命令行分析
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30  # CPU
go tool pprof http://localhost:8080/debug/pprof/heap                # 内存
go tool pprof http://localhost:8080/debug/pprof/goroutine           # goroutine

# 查看 goroutine 泄漏
curl http://localhost:8080/debug/pprof/goroutine?debug=1

35.4 Trace 分析

package main

import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()

    trace.Start(f)
    defer trace.Stop()

    // 你的代码
    doWork()
}
# 分析 trace
go tool trace trace.out

# 浏览器打开,可以看到:
# - Goroutine 调度
# - GC 事件
# - 系统调用
# - 阻塞事件

35.5 Benchmark + pprof

# 运行 benchmark 并生成 profile
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof

# 分析
go tool pprof cpu.prof
go tool pprof mem.prof

第三十六章:CGO 基础

36.1 基本用法

package main

/*
#include <stdio.h>
#include <stdlib.h>

void sayHello(const char* name) {
    printf("Hello, %s!\n", name);
}

int add(int a, int b) {
    return a + b;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // 调用 C 函数
    name := C.CString("World")
    defer C.free(unsafe.Pointer(name))
    C.sayHello(name)

    // 调用 C 函数并获取返回值
    result := C.add(3, 5)
    fmt.Println("3 + 5 =", result)
}

36.2 类型转换

package main

/*
#include <stdlib.h>
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    // Go string → C string
    goStr := "hello"
    cStr := C.CString(goStr)
    defer C.free(unsafe.Pointer(cStr))

    // C string → Go string
    backToGo := C.GoString(cStr)
    fmt.Println(backToGo)

    // Go []byte → C 数组
    goBytes := []byte{1, 2, 3, 4}
    cBytes := C.CBytes(goBytes)
    defer C.free(cBytes)

    // C 数组 → Go []byte
    backToBytes := C.GoBytes(cBytes, C.int(len(goBytes)))
    fmt.Println(backToBytes)

    // 数值类型
    var goInt int = 42
    var cInt C.int = C.int(goInt)
    backToInt := int(cInt)
    fmt.Println(backToInt)
}

36.3 CGO 注意事项

/*
CGO 性能开销:
- 每次 CGO 调用约 100-200ns
- 不要在热路径频繁调用 CGO
- 批量处理数据,减少调用次数

CGO 内存管理:
- C.CString 分配的内存必须手动 C.free
- C.CBytes 分配的内存必须手动 C.free
- Go 指针不能直接传给 C(可能被 GC 移动)

CGO 限制:
- 不能交叉编译(需要目标平台的 C 编译器)
- 增加编译时间
- 二进制文件变大
*/

// 正确的内存管理
func correctUsage() {
    cStr := C.CString("hello")
    defer C.free(unsafe.Pointer(cStr)) // 必须释放!

    // 使用 cStr...
}

// 错误:内存泄漏
func wrongUsage() {
    cStr := C.CString("hello")
    // 忘记 free,内存泄漏!
    _ = cStr
}

第三十七章:编译和链接

37.1 编译命令

# 基本编译
go build main.go

# 指定输出文件名
go build -o myapp main.go

# 编译并安装到 $GOPATH/bin
go install

# 交叉编译
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac

# 常用 GOOS/GOARCH 组合
# linux/amd64, linux/arm64
# darwin/amd64, darwin/arm64
# windows/amd64

37.2 编译优化

# 去除调试信息(减小二进制体积)
go build -ldflags="-s -w" -o myapp

# -s: 去除符号表
# -w: 去除 DWARF 调试信息

# 注入版本信息
go build -ldflags="-X main.Version=1.0.0 -X main.BuildTime=$(date)" -o myapp

# 在代码中使用
var Version string
var BuildTime string
// main.go
package main

import "fmt"

var (
    Version   string = "dev"
    BuildTime string = "unknown"
)

func main() {
    fmt.Printf("Version: %s, Built: %s\n", Version, BuildTime)
}

37.3 查看编译信息

# 查看编译过程
go build -x main.go

# 查看依赖
go list -m all

# 查看二进制大小分析
go build -o myapp
go tool nm myapp | head -20

# 查看汇编
go build -gcflags="-S" main.go 2>&1 | head -50

# 查看内联决策
go build -gcflags="-m" main.go

# 查看逃逸分析
go build -gcflags="-m -m" main.go

37.4 构建标签 (Build Tags)

// +build linux
// 或 Go 1.17+:
//go:build linux

package main

// 这个文件只在 Linux 上编译
// file_windows.go
//go:build windows

package main

func platformSpecific() string {
    return "Windows"
}
// file_linux.go
//go:build linux

package main

func platformSpecific() string {
    return "Linux"
}
# 使用自定义标签
go build -tags="debug" main.go

# 代码中
//go:build debug

package main

func debugLog(msg string) {
    fmt.Println("[DEBUG]", msg)
}

第三十八章:常见算法题 Go 实现

38.1 两数之和

func twoSum(nums []int, target int) []int {
    m := make(map[int]int)
    for i, n := range nums {
        if j, ok := m[target-n]; ok {
            return []int{j, i}
        }
        m[n] = i
    }
    return nil
}

38.2 反转链表

type ListNode struct {
    Val  int
    Next *ListNode
}

func reverseList(head *ListNode) *ListNode {
    var prev *ListNode
    curr := head
    for curr != nil {
        next := curr.Next
        curr.Next = prev
        prev = curr
        curr = next
    }
    return prev
}

38.3 二分查找

func binarySearch(nums []int, target int) int {
    left, right := 0, len(nums)-1
    for left <= right {
        mid := left + (right-left)/2
        if nums[mid] == target {
            return mid
        } else if nums[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1
}

38.4 快速排序

func quickSort(nums []int) {
    if len(nums) <= 1 {
        return
    }
    pivot := nums[len(nums)/2]
    left, right := 0, len(nums)-1
    for left <= right {
        for nums[left] < pivot {
            left++
        }
        for nums[right] > pivot {
            right--
        }
        if left <= right {
            nums[left], nums[right] = nums[right], nums[left]
            left++
            right--
        }
    }
    quickSort(nums[:right+1])
    quickSort(nums[left:])
}

38.5 合并两个有序链表

func mergeTwoLists(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    curr := dummy
    for l1 != nil && l2 != nil {
        if l1.Val < l2.Val {
            curr.Next = l1
            l1 = l1.Next
        } else {
            curr.Next = l2
            l2 = l2.Next
        }
        curr = curr.Next
    }
    if l1 != nil {
        curr.Next = l1
    } else {
        curr.Next = l2
    }
    return dummy.Next
}

38.6 有效的括号

func isValid(s string) bool {
    stack := []rune{}
    pairs := map[rune]rune{')': '(', ']': '[', '}': '{'}

    for _, c := range s {
        if c == '(' || c == '[' || c == '{' {
            stack = append(stack, c)
        } else {
            if len(stack) == 0 || stack[len(stack)-1] != pairs[c] {
                return false
            }
            stack = stack[:len(stack)-1]
        }
    }
    return len(stack) == 0
}

38.7 最大子数组和(Kadane算法)

func maxSubArray(nums []int) int {
    maxSum, currSum := nums[0], nums[0]
    for i := 1; i < len(nums); i++ {
        currSum = max(nums[i], currSum+nums[i])
        maxSum = max(maxSum, currSum)
    }
    return maxSum
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

38.8 二叉树遍历

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// 前序遍历(递归)
func preorder(root *TreeNode) []int {
    if root == nil {
        return nil
    }
    result := []int{root.Val}
    result = append(result, preorder(root.Left)...)
    result = append(result, preorder(root.Right)...)
    return result
}

// 中序遍历(迭代)
func inorder(root *TreeNode) []int {
    var result []int
    var stack []*TreeNode
    curr := root

    for curr != nil || len(stack) > 0 {
        for curr != nil {
            stack = append(stack, curr)
            curr = curr.Left
        }
        curr = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        result = append(result, curr.Val)
        curr = curr.Right
    }
    return result
}

// 层序遍历(BFS)
func levelOrder(root *TreeNode) [][]int {
    if root == nil {
        return nil
    }
    var result [][]int
    queue := []*TreeNode{root}

    for len(queue) > 0 {
        levelSize := len(queue)
        level := make([]int, levelSize)

        for i := 0; i < levelSize; i++ {
            node := queue[0]
            queue = queue[1:]
            level[i] = node.Val

            if node.Left != nil {
                queue = append(queue, node.Left)
            }
            if node.Right != nil {
                queue = append(queue, node.Right)
            }
        }
        result = append(result, level)
    }
    return result
}

38.9 TopK 问题(堆)

import "container/heap"

// 小顶堆
type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() any {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[:n-1]
    return x
}

// 找第 K 大的元素
func findKthLargest(nums []int, k int) int {
    h := &MinHeap{}
    heap.Init(h)

    for _, n := range nums {
        heap.Push(h, n)
        if h.Len() > k {
            heap.Pop(h)
        }
    }
    return (*h)[0]
}

38.10 LRU 缓存(链表+哈希)

// 见第 18.2 章的完整实现

最终速查:100% 覆盖清单

基础语法

  • [x] 变量、常量、类型
  • [x] 数组、切片、map
  • [x] 函数、方法、接口
  • [x] 结构体、指针
  • [x] 控制流(for、if、switch、select)

并发

  • [x] goroutine
  • [x] channel
  • [x] select
  • [x] sync 包(Mutex、RWMutex、WaitGroup、Once、Cond、Map)
  • [x] atomic
  • [x] context
  • [x] errgroup

内存与 GC

  • [x] 内存分配(栈 vs 堆)
  • [x] 逃逸分析
  • [x] GC 三色标记
  • [x] sync.Pool

运行时

  • [x] GPM 调度模型
  • [x] init 执行顺序
  • [x] defer 机制
  • [x] panic/recover

类型系统

  • [x] interface 内部结构
  • [x] 类型断言
  • [x] 反射
  • [x] 泛型

底层

  • [x] slice/map 底层结构
  • [x] string 底层结构
  • [x] unsafe 包

工程

  • [x] go mod
  • [x] 测试(单元、基准、示例)
  • [x] pprof
  • [x] 编译优化
  • [x] CGO

设计模式

  • [x] 函数选项
  • [x] 工厂
  • [x] 单例
  • [x] 装饰器
  • [x] 中间件
  • [x] 观察者

算法

  • [x] 排序
  • [x] 查找
  • [x] 链表
  • [x] 二叉树
  • [x] 堆
  • [x] 动态规划基础


Part 7:场景驱动 - 让知识点不再孤立

这一部分用真实业务场景串联所有知识点,让你知道:

  • 什么时候用?
  • 为什么这样用?
  • 不这样用会怎样?

第 39 章:一个 HTTP 请求的完整生命周期 - 串联 Context/Goroutine/超时

39.1 场景:用户请求一个需要调用多个下游服务的 API

用户请求 → API Gateway → 你的服务 → 调用 DB + 调用 Redis + 调用外部 API
                                    ↓
                              最多等 3 秒,超时就返回

问题来了:

  • 如果 DB 查询卡住了,用户要等多久?
  • 如果用户中途取消请求(关闭浏览器),后台还在查 DB 吗?
  • 调用链这么长,怎么统一控制超时?

39.2 错误写法:没有超时控制

// ❌ 错误:没有任何超时控制
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 如果 queryDB 卡住 10 分钟,用户就要等 10 分钟
    user := queryDB(r.URL.Query().Get("id"))

    // 如果这里也卡住...
    extra := callExternalAPI(user.ID)

    json.NewEncoder(w).Encode(map[string]any{
        "user":  user,
        "extra": extra,
    })
}

func queryDB(id string) *User {
    // 假设这里因为慢查询卡住了
    time.Sleep(10 * time.Minute) // 模拟慢查询
    return &User{ID: id}
}

后果:

  1. 用户请求超时,但服务器还在傻傻执行
  2. goroutine 泄漏:用户走了,goroutine 还在
  3. 资源浪费:DB 连接被无意义占用

39.3 正确写法:Context 贯穿整个调用链

// ✅ 正确:Context 传递,统一超时控制
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 从 HTTP 请求中获取 context(用户取消请求时会自动 cancel)
    ctx := r.Context()

    // 设置整体超时 3 秒
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel() // 重要:确保资源释放

    // context 传递给所有下游调用
    user, err := queryDBWithContext(ctx, r.URL.Query().Get("id"))
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "请求超时", http.StatusGatewayTimeout)
            return
        }
        if errors.Is(err, context.Canceled) {
            // 用户主动取消了(关闭浏览器)
            return // 不需要返回任何东西
        }
        http.Error(w, err.Error(), 500)
        return
    }

    extra, err := callExternalAPIWithContext(ctx, user.ID)
    if err != nil {
        // 同样处理超时和取消
        // ...
    }

    json.NewEncoder(w).Encode(map[string]any{
        "user":  user,
        "extra": extra,
    })
}

func queryDBWithContext(ctx context.Context, id string) (*User, error) {
    // 创建一个 channel 来接收结果
    resultCh := make(chan *User, 1)
    errCh := make(chan error, 1)

    go func() {
        // 实际的 DB 查询
        // 真实代码中,你应该把 ctx 传给 DB 驱动
        // 比如:db.QueryRowContext(ctx, "SELECT ...")
        time.Sleep(2 * time.Second) // 模拟查询
        resultCh <- &User{ID: id, Name: "test"}
    }()

    // 关键:用 select 同时监听结果和 context
    select {
    case <-ctx.Done():
        // 超时或被取消
        return nil, ctx.Err()
    case err := <-errCh:
        return nil, err
    case user := <-resultCh:
        return user, nil
    }
}

串联的知识点:

知识点在这个场景中的作用
r.Context()HTTP 请求自带 context,用户关闭浏览器会触发 cancel
WithTimeout设置整体超时时间
defer cancel()防止 context 泄漏
ctx.Done()监听取消/超时信号
select同时等待多个 channel(结果 or 取消)
context.DeadlineExceeded判断是超时还是其他错误

39.4 更复杂:并发调用多个服务,任一超时就返回

func handleComplexRequest(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
    defer cancel()

    // 用 errgroup 并发调用多个服务
    g, ctx := errgroup.WithContext(ctx)

    var user *User
    var orders []*Order
    var recommendations []*Product

    // 并发请求 1:查用户
    g.Go(func() error {
        var err error
        user, err = getUserWithContext(ctx, "123")
        return err
    })

    // 并发请求 2:查订单
    g.Go(func() error {
        var err error
        orders, err = getOrdersWithContext(ctx, "123")
        return err
    })

    // 并发请求 3:查推荐(这个服务不稳定,经常超时)
    g.Go(func() error {
        var err error
        recommendations, err = getRecommendationsWithContext(ctx, "123")
        if err != nil {
            // 推荐服务挂了不影响主流程,返回空即可
            recommendations = []*Product{}
            return nil // 注意:返回 nil,不让它影响其他请求
        }
        return nil
    })

    // 等待所有请求完成
    if err := g.Wait(); err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "部分服务超时", 504)
            return
        }
        http.Error(w, err.Error(), 500)
        return
    }

    // 全部成功
    json.NewEncoder(w).Encode(map[string]any{
        "user":            user,
        "orders":          orders,
        "recommendations": recommendations,
    })
}

为什么用 errgroup?

方案优点缺点
顺序调用简单3个服务各1秒 = 总共3秒
WaitGroup并发没有错误传递,没有自动cancel
errgroup并发 + 错误传递 + 自动cancel最佳选择

errgroup 的行为:

  • 任何一个 goroutine 返回 error → 自动 cancel 其他 goroutine
  • 所有 goroutine 都成功 → Wait() 返回 nil

第 40 章:一个真实的 Slice 故障案例 - 串联 Slice 底层/并发/值传递

40.1 场景:线上 bug - 数据莫名被覆盖

代码:

// 一个获取用户标签的函数
func getUserTags(userID string) []string {
    baseTags := []string{"registered", "active"}

    if isPremiumUser(userID) {
        baseTags = append(baseTags, "premium")
    }

    if isNewUser(userID) {
        baseTags = append(baseTags, "new")
    }

    return baseTags
}

// 调用方
func processUser(userID string) {
    tags := getUserTags(userID)

    // 给这个用户额外加一个临时标签
    tags = append(tags, "processing")

    // ... 处理逻辑
}

线上现象: 偶发性地,某些用户的 baseTags 里出现了 "processing" 标签

你能看出问题吗?


40.2 问题分析:Slice 底层数组共享

// 让我们追踪内存
func getUserTags(userID string) []string {
    // baseTags: len=2, cap=2, 指向数组 A [registered, active, _, _]
    baseTags := []string{"registered", "active"}

    if isPremiumUser(userID) {
        // append 时,cap 不够,会扩容
        // 新的 baseTags: len=3, cap=4, 指向新数组 B [registered, active, premium, _]
        baseTags = append(baseTags, "premium")
    }

    if isNewUser(userID) {
        // 如果已经是 premium,现在 len=3, cap=4
        // append "new" 时,cap 够用,不扩容!
        // baseTags: len=4, cap=4, 还是指向数组 B [registered, active, premium, new]
        baseTags = append(baseTags, "new")
    }

    return baseTags // 返回的 slice 指向数组 B
}

func processUser(userID string) {
    tags := getUserTags(userID)
    // 假设 tags: len=3, cap=4, 指向数组 B [registered, active, premium, _]

    // append "processing"
    // cap 够用(4 > 3),不扩容!
    // 直接写入数组 B 的第 4 个位置
    // tags: len=4, cap=4, 指向数组 B [registered, active, premium, processing]
    tags = append(tags, "processing")

    // 问题:如果另一个地方也持有指向数组 B 的 slice,它的数据被污染了!
}

根本原因:

  • append 在 cap 够用时不扩容,直接修改底层数组
  • 多个 slice 可能共享同一个底层数组
  • 一个修改,全部受影响

40.3 更恐怖的并发场景

var defaultTags = []string{"registered", "active", "verified"} // cap = 3

func getUserTagsConcurrent(userID string) []string {
    // 所有 goroutine 共享同一个 defaultTags
    tags := defaultTags

    if hasSpecialRole(userID) {
        // ⚠️ 灾难:如果 cap 够用,会直接修改 defaultTags 的底层数组!
        tags = append(tags, "special")
    }

    return tags
}

// 并发调用
func main() {
    for i := 0; i < 1000; i++ {
        go func(id int) {
            tags := getUserTagsConcurrent(strconv.Itoa(id))
            fmt.Println(tags)
        }(i)
    }
    time.Sleep(time.Second)
}

后果:

  • 数据竞争
  • 随机的数据错乱
  • 非常难复现和调试的 bug

40.4 正确写法:防御性复制

// 方案 1:返回时复制
func getUserTagsSafe(userID string) []string {
    baseTags := []string{"registered", "active"}

    if isPremiumUser(userID) {
        baseTags = append(baseTags, "premium")
    }

    // 返回一个新的 slice,完全切断与原数组的关系
    result := make([]string, len(baseTags))
    copy(result, baseTags)
    return result
}

// 方案 2:限制 cap(推荐)
func getUserTagsSafer(userID string) []string {
    baseTags := []string{"registered", "active"}

    if isPremiumUser(userID) {
        baseTags = append(baseTags, "premium")
    }

    // 用 slice[:len:len] 限制 cap = len
    // 这样后续任何 append 都会触发扩容,生成新数组
    return baseTags[:len(baseTags):len(baseTags)]
}

// 方案 3:并发安全的全局默认值
var defaultTags = []string{"registered", "active", "verified"}

func getUserTagsConcurrentSafe(userID string) []string {
    // 先复制,再操作
    tags := make([]string, len(defaultTags), len(defaultTags)+2)
    copy(tags, defaultTags)

    if hasSpecialRole(userID) {
        tags = append(tags, "special") // 安全,操作的是新数组
    }

    return tags
}

知识点串联表:

知识点在这个故障中的体现
Slice 三要素ptr, len, cap 理解清楚才能分析问题
append 扩容机制cap 够用时不扩容是 bug 根源
值传递slice 是值传递,但底层数组是共享的
并发安全共享底层数组 + 并发写 = 数据竞争
防御性编程copy 或 限制 cap 来隔离

第 41 章:选型决策 - 什么场景用什么工具

41.1 并发控制:Channel vs Mutex vs Atomic

决策树:

需要在 goroutine 间通信/传递数据吗?
├── 是 → 用 Channel
│       └── 需要多个 goroutine 消费同一个数据流? → 用 Channel
│       └── 生产者-消费者模型? → 用 Channel
│       └── 需要 select 多路复用? → 用 Channel
│
└── 否,只是保护共享数据 →
    ├── 数据是简单类型(int64, pointer)?
    │   ├── 只需要加减/读写? → 用 atomic(性能最好)
    │   └── 需要 CAS 操作? → 用 atomic
    │
    └── 数据是复杂结构(struct, map, slice)?
        ├── 读多写少? → 用 RWMutex
        └── 写操作频繁? → 用 Mutex

实际例子对比:

// 场景 1:计数器 - 用 atomic
type Counter struct {
    count int64
}

func (c *Counter) Inc() {
    atomic.AddInt64(&c.count, 1)
}

func (c *Counter) Get() int64 {
    return atomic.LoadInt64(&c.count)
}

// 场景 2:缓存 - 用 RWMutex(读多写少)
type Cache struct {
    mu   sync.RWMutex
    data map[string]any
}

func (c *Cache) Get(key string) (any, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache) Set(key string, value any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

// 场景 3:任务队列 - 用 Channel(goroutine 间通信)
func workerPool(tasks <-chan Task, results chan<- Result) {
    for task := range tasks {
        results <- process(task)
    }
}

// 场景 4:配置热更新 - 用 atomic.Value(读多写少,但需要存复杂类型)
type ConfigManager struct {
    config atomic.Value // 存储 *Config
}

func (m *ConfigManager) Get() *Config {
    return m.config.Load().(*Config)
}

func (m *ConfigManager) Update(newConfig *Config) {
    m.config.Store(newConfig)
}

41.2 sync 包选型速查表

需求选什么为什么
简单计数器atomic无锁,性能最好
保护 map/slicesync.Mutex 或 sync.RWMutexmap 不是并发安全的
读多写少的缓存sync.RWMutex读锁可以并发持有
全局唯一初始化sync.Once保证只执行一次
需要等待 N 个任务完成sync.WaitGroup简单的计数等待
等待某个条件满足sync.Cond条件变量
高并发 mapsync.Map特定场景下比 RWMutex+map 快
对象池复用sync.Pool减少 GC 压力

41.3 sync.Map 什么时候用?

常见误区: "并发 map 就用 sync.Map"

真相: sync.Map 只在特定场景下有优势

// ❌ 错误场景:频繁写入
// sync.Map 写入需要加锁,没有优势
func badUseOfSyncMap() {
    var m sync.Map
    for i := 0; i < 10000; i++ {
        m.Store(i, i) // 频繁写入,性能不如 Mutex+map
    }
}

// ✅ 正确场景 1:读多写少
// sync.Map 的 Load 是无锁的
func goodUseOfSyncMap() {
    var m sync.Map
    // 初始化时写入一次
    for i := 0; i < 100; i++ {
        m.Store(i, i)
    }
    // 后续只读
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            m.Load(id % 100) // 高并发读,无锁
        }(i)
    }
    wg.Wait()
}

// ✅ 正确场景 2:key 相对稳定,每个 key 只被一个 goroutine 写
// 比如:每个 goroutine 有自己的 key
func goodUseOfSyncMap2() {
    var m sync.Map
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // 每个 goroutine 只写自己的 key
            key := fmt.Sprintf("worker-%d", id)
            m.Store(key, time.Now())
        }(i)
    }
    wg.Wait()
}

sync.Map 适用场景总结:

  1. 读多写少(比如配置缓存)
  2. 多个 goroutine 读写不同的 key(无竞争)
  3. key 集合相对稳定,写入主要是更新已有 key

不适用场景:

  1. 需要遍历所有 key-value(Range 是 O(n) 且需要加锁)
  2. 需要知道 map 大小(没有 Len 方法)
  3. 频繁增删 key

第 42 章:内存泄漏排查实战 - 串联 pprof/goroutine/channel

42.1 场景:线上服务内存持续增长

现象:

  • 服务运行几天后,内存从 500MB 涨到 5GB
  • 重启后恢复,但很快又开始涨
  • 没有明显的大对象分配

42.2 第一步:确认是 goroutine 泄漏还是内存泄漏

// 在服务启动时加入 pprof
import _ "net/http/pprof"

func main() {
    // 暴露 pprof 端点
    go func() {
        log.Println(http.ListenAndServe(":6060", nil))
    }()

    // 你的服务代码
    startServer()
}

排查命令:

# 1. 查看 goroutine 数量
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | head -1
# 如果数量持续增长 → goroutine 泄漏

# 2. 查看 goroutine 堆栈
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 3. 查看内存分配
go tool pprof http://localhost:6060/debug/pprof/heap

# 4. 查看内存分配来源
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap

42.3 案例 1:goroutine 泄漏 - 没有退出机制

问题代码:

func startWorker(dataCh <-chan Data) {
    go func() {
        for data := range dataCh {
            process(data)
        }
        // 只有 close(dataCh) 才能退出这个 goroutine
        // 如果忘记 close,这个 goroutine 永远阻塞
    }()
}

// 调用方
func handleRequest() {
    ch := make(chan Data)
    startWorker(ch)

    // 处理完后
    // 忘记 close(ch) 了!
    // goroutine 永远阻塞在 range ch
}

pprof 现象:

goroutine profile: total 10234  ← 数量持续增长
1 @ 0x43e5e6 0x4077a7 0x407778 0x6834a0 0x45ec01
#   0x6834a0    main.startWorker.func1+0x40    /app/main.go:15

修复:

func startWorker(ctx context.Context, dataCh <-chan Data) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // 有退出机制!
            case data, ok := <-dataCh:
                if !ok {
                    return // channel 关闭也退出
                }
                process(data)
            }
        }
    }()
}

42.4 案例 2:goroutine 泄漏 - 发送到已满的 channel

问题代码:

func sendNotification(userID string, msg string) {
    // 每次调用都启动一个 goroutine
    go func() {
        notificationCh <- Notification{UserID: userID, Msg: msg}
        // 如果 notificationCh 已满,这里永远阻塞
        // 这个 goroutine 永远不会结束
    }()
}

var notificationCh = make(chan Notification, 100) // 只能缓存 100 个

修复:

func sendNotification(userID string, msg string) {
    go func() {
        select {
        case notificationCh <- Notification{UserID: userID, Msg: msg}:
            // 发送成功
        default:
            // channel 已满,记录日志或采取其他措施
            log.Warn("notification channel full, dropping message")
        }
    }()
}

// 或者用超时
func sendNotificationWithTimeout(ctx context.Context, userID string, msg string) error {
    select {
    case notificationCh <- Notification{UserID: userID, Msg: msg}:
        return nil
    case <-time.After(time.Second):
        return errors.New("send timeout")
    case <-ctx.Done():
        return ctx.Err()
    }
}

42.5 案例 3:内存泄漏 - slice 底层数组无法释放

问题代码:

var cache = make(map[string][]byte)

func processLargeFile(filename string) {
    // 读取一个 100MB 的文件
    data, _ := ioutil.ReadFile(filename)

    // 只需要前 100 字节作为标识
    header := data[:100]

    // 存入缓存
    cache[filename] = header
    // 问题:header 和 data 共享底层数组
    // 虽然我们只存了 100 字节的 slice
    // 但 100MB 的底层数组无法被 GC 回收!
}

修复:

func processLargeFile(filename string) {
    data, _ := ioutil.ReadFile(filename)

    // 复制需要的部分,切断与原数组的关系
    header := make([]byte, 100)
    copy(header, data[:100])

    cache[filename] = header
    // data 对应的 100MB 数组可以被 GC 回收了
}

42.6 案例 4:time.After 在循环中使用

问题代码:

func worker(tasks <-chan Task) {
    for {
        select {
        case task := <-tasks:
            process(task)
        case <-time.After(time.Minute):
            // 每次循环都创建一个新的 Timer
            // 在 task 频繁到来时,这些 Timer 堆积
            log.Println("no task for 1 minute")
        }
    }
}

修复:

func worker(tasks <-chan Task) {
    // 复用同一个 Timer
    timer := time.NewTimer(time.Minute)
    defer timer.Stop()

    for {
        select {
        case task := <-tasks:
            // 收到任务,重置定时器
            if !timer.Stop() {
                select {
                case <-timer.C:
                default:
                }
            }
            timer.Reset(time.Minute)
            process(task)
        case <-timer.C:
            log.Println("no task for 1 minute")
            timer.Reset(time.Minute)
        }
    }
}

42.7 内存泄漏排查清单

症状可能原因排查方法
goroutine 数量持续增长goroutine 泄漏pprof/goroutine
内存增长但 goroutine 不变对象泄漏pprof/heap
heap 显示大量 []byteslice 底层数组未释放检查 slice 截取
heap 显示大量 Timertime.After 在循环中使用改用 time.NewTimer
大量 HTTP Response Body未调用 resp.Body.Close()检查 HTTP 客户端代码

第 43 章:设计模式的真实落地 - 不是为了用而用

43.1 Functional Options 模式 - 解决什么问题?

问题场景:

// 你有一个客户端,需要支持各种配置
type Client struct {
    host     string
    port     int
    timeout  time.Duration
    retries  int
    logger   Logger
    tracer   Tracer
}

// ❌ 方案 1:巨大的构造函数
func NewClient(host string, port int, timeout time.Duration,
               retries int, logger Logger, tracer Tracer) *Client {
    // 调用方需要记住参数顺序
    // 新增参数需要改所有调用方
}

// ❌ 方案 2:Config 结构体
type ClientConfig struct {
    Host     string
    Port     int
    Timeout  time.Duration
    Retries  int
    Logger   Logger
    Tracer   Tracer
}

func NewClient(cfg ClientConfig) *Client {
    // 好一点,但是无法区分"用户没设置"和"用户设置为零值"
    // 比如 Timeout = 0 是用户要的还是忘记设置了?
}

✅ 正确方案:Functional Options

type Client struct {
    host     string
    port     int
    timeout  time.Duration
    retries  int
    logger   Logger
    tracer   Tracer
}

// Option 是一个函数类型
type Option func(*Client)

// 每个配置项是一个返回 Option 的函数
func WithHost(host string) Option {
    return func(c *Client) {
        c.host = host
    }
}

func WithPort(port int) Option {
    return func(c *Client) {
        c.port = port
    }
}

func WithTimeout(timeout time.Duration) Option {
    return func(c *Client) {
        c.timeout = timeout
    }
}

func WithLogger(logger Logger) Option {
    return func(c *Client) {
        c.logger = logger
    }
}

// 构造函数接收可变数量的 Option
func NewClient(options ...Option) *Client {
    // 先设置默认值
    client := &Client{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        retries: 3,
        logger:  defaultLogger,
    }

    // 应用用户的配置(会覆盖默认值)
    for _, opt := range options {
        opt(client)
    }

    return client
}

// 使用:清晰、灵活、可扩展
func main() {
    // 全部使用默认值
    c1 := NewClient()

    // 只修改需要的配置
    c2 := NewClient(
        WithHost("api.example.com"),
        WithTimeout(10 * time.Second),
    )

    // 新增选项不影响已有代码
    c3 := NewClient(
        WithHost("api.example.com"),
        WithLogger(myLogger),
        WithTracer(myTracer), // 新增的选项
    )
}

为什么这样设计:

  1. 默认值清晰:用户不传就用默认值
  2. 可选配置:只传需要改的
  3. 向后兼容:新增选项不影响已有代码
  4. 自解释:WithTimeout(10 * time.Second) 比 NewClient(..., 10*time.Second, ...) 清晰

真实使用场景:

  • grpc.Dial(target, opts...)
  • http.NewRequest + http.Client
  • 各种 SDK 的 Client 构造

43.2 中间件模式 - 为什么 Web 框架都用这个?

问题场景:

// 你有很多 handler,每个都需要:
// - 记录日志
// - 验证 token
// - 限流
// - 记录耗时

// ❌ 错误做法:每个 handler 都写一遍
func handleUser(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    log.Printf("request: %s %s", r.Method, r.URL.Path)

    token := r.Header.Get("Authorization")
    if !validateToken(token) {
        http.Error(w, "unauthorized", 401)
        return
    }

    if !rateLimiter.Allow() {
        http.Error(w, "too many requests", 429)
        return
    }

    // 真正的业务逻辑
    user := getUser(r.URL.Query().Get("id"))
    json.NewEncoder(w).Encode(user)

    log.Printf("response time: %v", time.Since(start))
}

// 每个 handler 都要重复这些代码!

✅ 正确做法:中间件

// 中间件类型定义
type Middleware func(http.Handler) http.Handler

// 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("→ %s %s", r.Method, r.URL.Path)

        next.ServeHTTP(w, r) // 调用下一个处理器

        log.Printf("← %s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// 认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if !validateToken(token) {
            http.Error(w, "unauthorized", 401)
            return // 不调用 next,请求在这里终止
        }

        // 认证通过,继续
        next.ServeHTTP(w, r)
    })
}

// 限流中间件
func RateLimitMiddleware(limiter *rate.Limiter) Middleware {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "too many requests", 429)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 业务 handler 只关心业务逻辑
func handleUser(w http.ResponseWriter, r *http.Request) {
    user := getUser(r.URL.Query().Get("id"))
    json.NewEncoder(w).Encode(user)
}

// 组装中间件
func main() {
    handler := http.HandlerFunc(handleUser)

    // 洋葱模型:请求从外到内,响应从内到外
    // RateLimit → Auth → Logging → handleUser
    wrapped := RateLimitMiddleware(rateLimiter)(
        AuthMiddleware(
            LoggingMiddleware(handler),
        ),
    )

    http.Handle("/user", wrapped)
}

// 更优雅的链式写法
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    // 从后往前包装
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    handler := Chain(
        http.HandlerFunc(handleUser),
        LoggingMiddleware,
        AuthMiddleware,
        RateLimitMiddleware(rateLimiter),
    )
    http.Handle("/user", handler)
}

执行顺序图解:

请求 → RateLimit → Auth → Logging → handleUser
                                         ↓
响应 ← RateLimit ← Auth ← Logging ← handleUser

为什么用中间件:

  1. 单一职责:每个中间件只做一件事
  2. 可复用:登录和非登录接口可以组合不同的中间件
  3. 可测试:每个中间件可以单独测试
  4. 可插拔:加减功能只需要修改组装代码

43.3 单例模式 - 为什么 Go 里要用 sync.Once?

问题场景:

// 全局唯一的 DB 连接
var db *sql.DB

// ❌ 错误做法 1:直接初始化(可能连接还没建立就被使用)
func init() {
    var err error
    db, err = sql.Open("mysql", dsn)
    if err != nil {
        panic(err)
    }
}

// ❌ 错误做法 2:懒加载但有竞争
func GetDB() *sql.DB {
    if db == nil {
        db, _ = sql.Open("mysql", dsn) // 多个 goroutine 可能同时执行
    }
    return db
}

// ❌ 错误做法 3:每次加锁(性能差)
var mu sync.Mutex

func GetDB() *sql.DB {
    mu.Lock()
    defer mu.Unlock()
    if db == nil {
        db, _ = sql.Open("mysql", dsn)
    }
    return db
}

✅ 正确做法:sync.Once

var (
    db     *sql.DB
    dbOnce sync.Once
    dbErr  error
)

func GetDB() (*sql.DB, error) {
    dbOnce.Do(func() {
        // 这个函数只会执行一次,且是线程安全的
        db, dbErr = sql.Open("mysql", dsn)
        if dbErr != nil {
            return
        }
        // 配置连接池
        db.SetMaxOpenConns(100)
        db.SetMaxIdleConns(10)
    })
    return db, dbErr
}

// 使用
func handleRequest() {
    db, err := GetDB()
    if err != nil {
        // 处理错误
    }
    // 使用 db
}

sync.Once 的特性:

  1. 只执行一次:无论多少 goroutine 调用
  2. 线程安全:内部有锁保护
  3. 执行完成才返回:其他 goroutine 会等待直到初始化完成

真实使用场景:

  • 数据库连接池
  • 配置加载
  • 日志初始化
  • 缓存客户端初始化

第 44 章:面试题背后的真实场景

44.1 "如何实现一个超时的 HTTP 请求?" - 这个问题在问什么

面试官真正想知道的:

  1. 你是否理解 Context 的作用
  2. 你是否知道资源清理
  3. 你是否考虑过错误处理

完整答案:

func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
    // 1. 创建带超时的 context
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel() // 重要:确保取消

    // 2. 创建请求并关联 context
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    // 3. 使用自定义的 HTTP Client(有连接超时等配置)
    client := &http.Client{
        Timeout: 10 * time.Second, // 整体超时(包括重定向等)
        Transport: &http.Transport{
            DialContext: (&net.Dialer{
                Timeout:   3 * time.Second,  // 连接超时
                KeepAlive: 30 * time.Second,
            }).DialContext,
            TLSHandshakeTimeout:   3 * time.Second,
            ResponseHeaderTimeout: 5 * time.Second,
            IdleConnTimeout:       90 * time.Second,
            MaxIdleConns:          100,
            MaxIdleConnsPerHost:   10,
        },
    }

    // 4. 发送请求
    resp, err := client.Do(req)
    if err != nil {
        // 区分超时和其他错误
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("request timeout: %w", err)
        }
        if errors.Is(err, context.Canceled) {
            return nil, fmt.Errorf("request canceled: %w", err)
        }
        return nil, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close() // 重要:必须关闭 Body

    // 5. 检查状态码
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    // 6. 读取响应(也可能超时)
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, fmt.Errorf("read body: %w", err)
    }

    return body, nil
}

面试延伸问题及答案:

Q: defer cancel() 为什么重要? A: 如果请求成功完成,cancel 会释放 context 相关的资源。不调用会导致 context 相关的 goroutine 泄漏(直到父 context 被 cancel)。

Q: 为什么要 defer resp.Body.Close()? A: HTTP 响应的 Body 是一个 io.ReadCloser,底层是 TCP 连接。不关闭会导致连接无法复用,最终耗尽连接池。

Q: http.Client.Timeout 和 context.WithTimeout 有什么区别? A:

  • Client.Timeout 是整体超时,包括连接、发送、重定向、接收
  • context.WithTimeout 更灵活,可以在中途取消,可以跨多个请求共享

44.2 "如何限制并发数?" - 三种方案对比

方案 1:Channel(信号量)

// 适合场景:任务数量不确定,持续有新任务
func limitedConcurrency(tasks []Task, limit int) {
    sem := make(chan struct{}, limit)
    var wg sync.WaitGroup

    for _, task := range tasks {
        wg.Add(1)
        sem <- struct{}{} // 获取信号量,如果已满则阻塞

        go func(t Task) {
            defer wg.Done()
            defer func() { <-sem }() // 释放信号量

            process(t)
        }(task)
    }

    wg.Wait()
}

方案 2:Worker Pool

// 适合场景:任务队列,需要重用 goroutine
func workerPool(tasks []Task, workerCount int) {
    taskCh := make(chan Task, len(tasks))
    var wg sync.WaitGroup

    // 启动固定数量的 worker
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for task := range taskCh {
                process(task)
            }
        }()
    }

    // 发送任务
    for _, task := range tasks {
        taskCh <- task
    }
    close(taskCh)

    wg.Wait()
}

方案 3:errgroup + SetLimit(Go 1.20+)

// 适合场景:需要错误处理和自动取消
func limitedWithErrgroup(ctx context.Context, tasks []Task, limit int) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(limit) // 设置并发限制

    for _, task := range tasks {
        task := task // 重要:捕获循环变量
        g.Go(func() error {
            return process(ctx, task)
        })
    }

    return g.Wait()
}

对比表:

方案优点缺点适用场景
Channel 信号量简单直观无错误传递简单任务
Worker Poolgoroutine 复用代码多高吞吐队列
errgroup错误处理+取消需要 Go 1.20+需要错误处理

44.3 "说说你理解的 GMP 模型" - 用图说话

          ┌─────────────────────────────────────────────┐
          │                  Go Runtime                  │
          └─────────────────────────────────────────────┘
                              ↓
     ┌──────────────────────────────────────────────────────┐
     │                    Global Queue                       │
     │        [G] [G] [G] ... (等待被调度的 goroutine)        │
     └──────────────────────────────────────────────────────┘
                              ↓ 偷取
     ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
     │       P0        │  │       P1        │  │       P2        │
     │  Local Queue    │  │  Local Queue    │  │  Local Queue    │
     │  [G][G][G]      │  │  [G][G]         │  │  [G][G][G][G]   │
     │       ↓         │  │       ↓         │  │       ↓         │
     │   Running: G    │  │   Running: G    │  │   Running: G    │
     └────────┬────────┘  └────────┬────────┘  └────────┬────────┘
              │                    │                    │
              │                    │                    │
     ┌────────┴────────┐  ┌────────┴────────┐  ┌────────┴────────┐
     │       M0        │  │       M1        │  │       M2        │
     │  (OS Thread)    │  │  (OS Thread)    │  │  (OS Thread)    │
     │                 │  │                 │  │                 │
     │  [CPU Core 0]   │  │  [CPU Core 1]   │  │  [CPU Core 2]   │
     └─────────────────┘  └─────────────────┘  └─────────────────┘

关键概念:

组件是什么数量作用
G (Goroutine)用户态协程可以有百万个执行用户代码
P (Processor)逻辑处理器默认 = CPU 核数调度 G 到 M
M (Machine)OS 线程按需创建实际执行代码

调度流程(面试必答):

  1. 正常执行:G 在 P 的 local queue 中,P 绑定一个 M,M 执行 G

  2. 阻塞处理:

    • G 执行系统调用(如文件 IO)→ M 阻塞
    • P 会与 M 解绑,找另一个 M 继续执行其他 G
    • 系统调用完成后,G 回到某个 P 的 queue
  3. 抢占式调度:

    • G 执行超过 10ms,会被标记为可抢占
    • 调度器会在安全点(函数调用、内存分配)抢占
  4. 工作窃取:

    • P 的 local queue 空了
    • 先从 global queue 拿
    • 再从其他 P 的 local queue 偷一半

面试加分点:

// 查看当前 GOMAXPROCS(P 的数量)
fmt.Println(runtime.GOMAXPROCS(0))

// 设置 P 的数量
runtime.GOMAXPROCS(4)

// 查看当前 goroutine 数量
fmt.Println(runtime.NumGoroutine())

// 让出当前 P(不常用)
runtime.Gosched()

第 45 章:总结 - 知识点之间的关系图

                              ┌─────────────┐
                              │   Go 程序   │
                              └──────┬──────┘
                                     │
            ┌────────────────────────┼────────────────────────┐
            ↓                        ↓                        ↓
     ┌──────────────┐        ┌──────────────┐        ┌──────────────┐
     │   数据结构   │        │   并发控制   │        │   内存管理   │
     └──────┬───────┘        └──────┬───────┘        └──────┬───────┘
            │                       │                       │
    ┌───────┴───────┐       ┌───────┴───────┐       ┌───────┴───────┐
    │               │       │               │       │               │
    ↓               ↓       ↓               ↓       ↓               ↓
┌───────┐      ┌───────┐ ┌───────┐    ┌───────┐ ┌───────┐    ┌───────┐
│ Slice │←────→│  Map  │ │Channel│←──→│Context│ │ Stack │←──→│ Heap  │
└───┬───┘      └───┬───┘ └───┬───┘    └───┬───┘ └───┬───┘    └───┬───┘
    │              │         │            │         │            │
    │ 底层数组共享  │         │   select   │         │  逃逸分析   │
    │              │         │   多路复用  │         │            │
    ↓              ↓         ↓            ↓         ↓            ↓
┌───────────────────────────────────────────────────────────────────┐
│                          常见问题                                  │
├───────────────────────────────────────────────────────────────────┤
│ Slice: append 不扩容导致数据污染                                   │
│ Map: 并发读写 panic                                                │
│ Channel: 阻塞导致 goroutine 泄漏                                   │
│ Context: 忘记 cancel 导致资源泄漏                                  │
│ 内存: slice 子切片持有大数组、goroutine 泄漏                       │
└───────────────────────────────────────────────────────────────────┘
                                │
                                ↓
┌───────────────────────────────────────────────────────────────────┐
│                          排查工具                                  │
├───────────────────────────────────────────────────────────────────┤
│ go tool pprof: CPU、内存、goroutine 分析                          │
│ go build -gcflags="-m": 逃逸分析                                  │
│ go test -race: 数据竞争检测                                        │
│ go test -bench: 性能基准测试                                       │
└───────────────────────────────────────────────────────────────────┘

附录:特性变体速查手册(复习专用)

这部分是各章节知识点的汇总对比,方便快速复习和查阅


附录 A:Context 四种类型速查

类型创建方式取消方式典型场景
Backgroundcontext.Background()不可取消main 函数入口、测试入口
TODOcontext.TODO()不可取消重构老代码时的临时占位
WithCancelcontext.WithCancel(parent)调用 cancel()用户取消上传、停止后台 worker、并发搜索取第一个结果
WithTimeoutcontext.WithTimeout(parent, duration)超时或调用 cancel()HTTP 调用外部 API(3秒)、DB 查询(1秒)、gRPC 调用
WithDeadlinecontext.WithDeadline(parent, time)到达截止时间或调用 cancel()秒杀活动截止、定时任务必须在6点前完成、消息队列 TTL

WithTimeout vs WithDeadline:

  • WithTimeout:从现在开始算,"最多等 N 秒"
  • WithDeadline:到某个时间点,"必须在 14:30 前完成"

附录 B:Channel 变体速查

类型声明特点典型场景
无缓冲make(chan T)发送阻塞直到有人接收同步信号、等待 goroutine 完成
有缓冲make(chan T, n)缓冲满才阻塞任务队列、限流(信号量)
只读<-chan T只能接收函数参数,防止误操作
只写chan<- T只能发送函数参数,防止误操作

Channel 操作结果:

操作nil channel已关闭 channel正常 channel
发送永久阻塞panic阻塞或成功
接收永久阻塞返回零值 + false阻塞或成功
关闭panicpanic成功

附录 C:sync 包选型速查

需求选什么场景示例
等待多个 goroutine 完成WaitGroup并发下载 10 个文件
等待 + 错误处理 + 自动取消errgroup并发调用 3 个 API,任一失败全部取消
保护共享数据(读写相近)Mutex订单状态更新
保护共享数据(读多写少)RWMutex用户缓存(读 1000 次/秒,写 10 次/秒)
只执行一次初始化sync.Once数据库连接单例、配置懒加载
简单计数器atomic请求计数、在线人数
等待条件满足sync.Cond生产者-消费者、连接池等待
并发安全 map(读多写少)sync.Map配置缓存、路由表
并发安全 map(写多)RWMutex + map频繁增删的缓存
对象池复用sync.PoolHTTP 请求对象、buffer 复用

Mutex vs RWMutex 选择:

读写比例选择原因
1:1MutexRWMutex 开销更大
10:1 或更高RWMutex读锁可并发,吞吐量更高
纯写MutexRWMutex 没优势

atomic vs Mutex 选择:

场景选择原因
int64 计数器atomic无锁,性能最好
复杂结构(map/slice/struct)Mutexatomic 不支持
多步骤原子操作Mutexatomic 只能单步原子

附录 D:错误处理速查

方式用法场景
errors.Newerrors.New("msg")简单错误信息
fmt.Errorffmt.Errorf("user %s: %v", id, err)带格式化的错误
fmt.Errorf + %wfmt.Errorf("context: %w", err)包装错误,保留原始错误链
自定义 error 类型实现 Error() string需要携带额外信息

errors.Is vs errors.As:

函数用途示例
errors.Is(err, target)判断错误链中是否包含某个值errors.Is(err, os.ErrNotExist)
errors.As(err, &target)从错误链中提取某个类型errors.As(err, &myErr)

error vs panic 选择:

场景选择
文件不存在、网络超时、用户输入非法error
启动时配置错误、程序 bug、必要依赖缺失panic

附录 E:Slice 陷阱速查

陷阱原因解决方案
子切片修改影响原 slice共享底层数组copy() 或 append([]T(nil), s...)
append 后原 slice 被污染cap 够用时不扩容返回时 copy,或限制 cap s[:len:len]
函数内 append 不影响原 sliceslice header 是值传递返回新 slice 或传指针
并发读写 slice数据竞争加锁或用 channel

附录 F:并发控制决策树

需要在 goroutine 间传递数据?
├── 是 → Channel
│   ├── 需要同步等待? → 无缓冲 channel
│   ├── 需要缓冲/限流? → 有缓冲 channel
│   └── 需要 select 多路复用? → Channel
│
└── 否,只是保护共享数据 →
    ├── 数据是简单类型(int64/pointer)?
    │   └── 是 → atomic(性能最好)
    │
    └── 数据是复杂结构?
        ├── 读多写少? → RWMutex
        └── 读写相近? → Mutex

附录 G:常见 Sentinel Error 速查

// 标准库常用 sentinel errors
io.EOF                    // 读到文件末尾
os.ErrNotExist            // 文件/目录不存在
os.ErrPermission          // 权限不足
os.ErrExist               // 文件/目录已存在
context.Canceled          // context 被取消
context.DeadlineExceeded  // context 超时
sql.ErrNoRows             // 查询无结果
http.ErrServerClosed      // 服务器已关闭

附录 H:面试高频问题速答

Q: goroutine 有父子关系吗? A: 没有,所有 goroutine 平级,由 runtime 统一调度。

Q: main 退出后其他 goroutine 会怎样? A: 立即被杀死,没有任何清理机会。

Q: WithTimeout 和 WithDeadline 区别? A: WithTimeout 是相对时间(从现在起 N 秒),WithDeadline 是绝对时间(到某个时间点)。

Q: 如何避免 goroutine 泄漏? A: 使用 context 控制退出、channel 必须 close、select 监听 ctx.Done()。

Q: sync.Map 什么时候用? A: 读多写少、或多 goroutine 读写不同 key 时。频繁写入用 RWMutex+map。

Q: 为什么 Go 不提供 goroutine ID? A: 防止滥用,避免破坏调度模型。

Q: slice 作为函数参数,修改会影响原 slice 吗? A: 修改元素会影响,append 可能不会(取决于是否扩容)。

Q: errors.Is 和 errors.As 区别? A: Is 比较值(是不是这个错误),As 提取类型(获取错误详情)。


笔记完成!