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() | 根 context | main 函数、测试入口 |
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 | 生产者-消费者、资源池 |
| 并发安全的 map | sync.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:1 | Mutex | RWMutex 开销更大,没必要 |
| 读多写少(如 10:1) | RWMutex | 读锁可以并发,提升吞吐量 |
| 写多读少 | Mutex | RWMutex 优势发挥不出来 |
| 纯写,无读 | Mutex | RWMutex 没必要 |
场景 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 | 正常 |
| close | panic | panic | 正常 |
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 |
| close | panic | 成功 | 成功 | 成功 | 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 触发时机:
- 堆内存达到阈值(GOGC 控制,默认 100%)
- 手动调用
runtime.GC() - 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.Once | init() |
|---|---|---|
| 执行时机 | 第一次调用时(懒加载) | 程序启动时(饥饿加载) |
| 适用场景 | 可能用不到的资源 | 必须启动时就准备好的资源 |
| 错误处理 | 可以在运行时处理 | 只能 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 对比:
| 特性 | atomic | Mutex |
|---|---|---|
| 性能 | 非常高(无锁) | 相对较低(需要加锁解锁) |
| 适用类型 | 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/float | 0 |
| string | "" |
| bool | false |
| pointer | nil |
| slice | nil |
| map | nil |
| channel | nil |
| interface | nil |
| struct | 各字段零值 |
make vs new
| 函数 | 用途 | 返回值 |
|---|---|---|
| make | slice/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/bool | 4-8 字节 | 否 |
| string | 16 字节 | 否(不可变) |
| slice | 24 字节 | 是(修改元素) |
| map | 8 字节 | 是 |
| channel | 8 字节 | 是 |
| [100]int | 800 字节 | 否 |
| 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 // 缓存后好很多
反射性能优化建议:
- 缓存
reflect.Type和reflect.Value - 避免在热路径使用反射
- 考虑使用代码生成替代反射
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.Is | errors.Is(err, os.ErrNotExist) |
| 判断是否是某个错误类型并获取信息 | errors.As | errors.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) |
| 获取 Kind | v.Kind() |
| 获取字段数 | t.NumField() |
| 获取字段 | t.Field(i) / v.FieldByName("Name") |
| 设置值 | v.Elem().SetInt(10) |
| 调用方法 | v.MethodByName("Foo").Call(args) |
| 获取 Tag | field.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}
}
后果:
- 用户请求超时,但服务器还在傻傻执行
- goroutine 泄漏:用户走了,goroutine 还在
- 资源浪费: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/slice | sync.Mutex 或 sync.RWMutex | map 不是并发安全的 |
| 读多写少的缓存 | sync.RWMutex | 读锁可以并发持有 |
| 全局唯一初始化 | sync.Once | 保证只执行一次 |
| 需要等待 N 个任务完成 | sync.WaitGroup | 简单的计数等待 |
| 等待某个条件满足 | sync.Cond | 条件变量 |
| 高并发 map | sync.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 适用场景总结:
- 读多写少(比如配置缓存)
- 多个 goroutine 读写不同的 key(无竞争)
- key 集合相对稳定,写入主要是更新已有 key
不适用场景:
- 需要遍历所有 key-value(Range 是 O(n) 且需要加锁)
- 需要知道 map 大小(没有 Len 方法)
- 频繁增删 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 显示大量 []byte | slice 底层数组未释放 | 检查 slice 截取 |
| heap 显示大量 Timer | time.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), // 新增的选项
)
}
为什么这样设计:
- 默认值清晰:用户不传就用默认值
- 可选配置:只传需要改的
- 向后兼容:新增选项不影响已有代码
- 自解释:
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
为什么用中间件:
- 单一职责:每个中间件只做一件事
- 可复用:登录和非登录接口可以组合不同的中间件
- 可测试:每个中间件可以单独测试
- 可插拔:加减功能只需要修改组装代码
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 的特性:
- 只执行一次:无论多少 goroutine 调用
- 线程安全:内部有锁保护
- 执行完成才返回:其他 goroutine 会等待直到初始化完成
真实使用场景:
- 数据库连接池
- 配置加载
- 日志初始化
- 缓存客户端初始化
第 44 章:面试题背后的真实场景
44.1 "如何实现一个超时的 HTTP 请求?" - 这个问题在问什么
面试官真正想知道的:
- 你是否理解 Context 的作用
- 你是否知道资源清理
- 你是否考虑过错误处理
完整答案:
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 Pool | goroutine 复用 | 代码多 | 高吞吐队列 |
| 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 线程 | 按需创建 | 实际执行代码 |
调度流程(面试必答):
正常执行:G 在 P 的 local queue 中,P 绑定一个 M,M 执行 G
阻塞处理:
- G 执行系统调用(如文件 IO)→ M 阻塞
- P 会与 M 解绑,找另一个 M 继续执行其他 G
- 系统调用完成后,G 回到某个 P 的 queue
抢占式调度:
- G 执行超过 10ms,会被标记为可抢占
- 调度器会在安全点(函数调用、内存分配)抢占
工作窃取:
- 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 四种类型速查
| 类型 | 创建方式 | 取消方式 | 典型场景 |
|---|---|---|---|
Background | context.Background() | 不可取消 | main 函数入口、测试入口 |
TODO | context.TODO() | 不可取消 | 重构老代码时的临时占位 |
WithCancel | context.WithCancel(parent) | 调用 cancel() | 用户取消上传、停止后台 worker、并发搜索取第一个结果 |
WithTimeout | context.WithTimeout(parent, duration) | 超时或调用 cancel() | HTTP 调用外部 API(3秒)、DB 查询(1秒)、gRPC 调用 |
WithDeadline | context.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 | 阻塞或成功 |
| 关闭 | panic | panic | 成功 |
附录 C:sync 包选型速查
| 需求 | 选什么 | 场景示例 |
|---|---|---|
| 等待多个 goroutine 完成 | WaitGroup | 并发下载 10 个文件 |
| 等待 + 错误处理 + 自动取消 | errgroup | 并发调用 3 个 API,任一失败全部取消 |
| 保护共享数据(读写相近) | Mutex | 订单状态更新 |
| 保护共享数据(读多写少) | RWMutex | 用户缓存(读 1000 次/秒,写 10 次/秒) |
| 只执行一次初始化 | sync.Once | 数据库连接单例、配置懒加载 |
| 简单计数器 | atomic | 请求计数、在线人数 |
| 等待条件满足 | sync.Cond | 生产者-消费者、连接池等待 |
| 并发安全 map(读多写少) | sync.Map | 配置缓存、路由表 |
| 并发安全 map(写多) | RWMutex + map | 频繁增删的缓存 |
| 对象池复用 | sync.Pool | HTTP 请求对象、buffer 复用 |
Mutex vs RWMutex 选择:
| 读写比例 | 选择 | 原因 |
|---|---|---|
| 1:1 | Mutex | RWMutex 开销更大 |
| 10:1 或更高 | RWMutex | 读锁可并发,吞吐量更高 |
| 纯写 | Mutex | RWMutex 没优势 |
atomic vs Mutex 选择:
| 场景 | 选择 | 原因 |
|---|---|---|
| int64 计数器 | atomic | 无锁,性能最好 |
| 复杂结构(map/slice/struct) | Mutex | atomic 不支持 |
| 多步骤原子操作 | Mutex | atomic 只能单步原子 |
附录 D:错误处理速查
| 方式 | 用法 | 场景 |
|---|---|---|
errors.New | errors.New("msg") | 简单错误信息 |
fmt.Errorf | fmt.Errorf("user %s: %v", id, err) | 带格式化的错误 |
fmt.Errorf + %w | fmt.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 不影响原 slice | slice 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 提取类型(获取错误详情)。
笔记完成!