在 Go 语言中,并发编程提供了强大的工具来提升程序的性能和响应能力,但实际应用时,许多开发者会在并发编程实践中犯错。这些错误包括 goroutine 管理不当、同步机制使用不当、死锁的发生以及资源竞争等,可能导致程序运行异常或性能下降。

本模块将深入探讨在并发编程实践中常见的错误,并通过具体案例分析,帮助开发者识别和解决这些问题。通过了解并发编程的最佳实践,开发者能够避免常见的坑,编写更加高效、安全且易于维护的并发代码,提升整个系统的稳定性与表现。

1. 传递不合适的 context (#61)

错误示例:

package main

import (
    "context"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    // 直接传递 context,不知道何时会被取消
    doSomething(r.Context())
}

func doSomething(ctx context.Context) {
    // 在 context 被取消后仍然继续操作
    select {
    case <-ctx.Done():
        // 错误地忽略了 context 的取消
    }
}

影响分析:
如果 context 的取消没有被正确处理,可能导致资源泄露或 Goroutine 阻塞,尤其是在 HTTP 请求完成后。

最佳实践:
始终检查 context 的生命周期,响应 ctx.Done() 信号以优雅退出。

func doSomething(ctx context.Context) {
    select {
    case <-ctx.Done():
        // 及时停止操作
        return
    default:
        // 继续处理
    }
}

2. 启动了一个 Goroutine 但不知道它何时会停止 (#62)

错误示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        for {
            fmt.Println("FunTester Goroutine")
            time.Sleep(1 * time.Second) // 无限循环,goroutine 不会停止
        }
    }()
    time.Sleep(3 * time.Second)
}

影响分析:
Goroutine 泄漏会占用内存和 CPU 资源,可能导致系统性能下降甚至崩溃。

最佳实践:
使用 context.Contextchan 机制通知并控制 Goroutine 的退出。

package main

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

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

    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Goroutine 退出")
                return
            default:
                fmt.Println("FunTester Goroutine")
                time.Sleep(1 * time.Second)
            }
        }
    }(ctx)

    time.Sleep(4 * time.Second)
}

3. 不注意处理 Goroutines 和循环中的迭代变量 (#63)

错误示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i) // i 是循环变量,可能打印非预期值
        }()
    }
    time.Sleep(1 * time.Second)
}

影响分析:
Goroutines 捕获了循环变量的地址,可能导致意外的输出。

最佳实践:
创建局部变量或将迭代变量作为参数传递给 Goroutines。

func main() {
    for i := 0; i < 5; i++ {
        i := i // 创建局部变量
        go func() {
            fmt.Println(i) // 正确打印 0 到 4
        }()
    }
    time.Sleep(1 * time.Second)
}

4. 使用 select + channels 时误以为分支选择顺序是确定的 (#64)

错误示例:

package main

import "fmt"

func main() {
    ch1, ch2 := make(chan int), make(chan int)
    go func() { ch1 <- 1 }()
    go func() { ch2 <- 2 }()

    select {
    case val := <-ch1:
        fmt.Println("ch1:", val) // 假设总是先选 ch1
    case val := <-ch2:
        fmt.Println("ch2:", val)
    }
}

影响分析:
select 在多个 channel 都就绪时,会随机选择一个分支,这种行为可能违反预期。

最佳实践:
在需要明确顺序时,可以采用优先级机制或显式判断。

select {
case val := <-ch1:
    fmt.Println("ch1:", val)
case val := <-ch2:
    fmt.Println("ch2:", val)
default:
    fmt.Println("没有数据准备好")
}

5. 不正确使用通知 channels (#65)

错误示例:

package main

func main() {
    done := make(chan bool)
    go func() {
        // 错误使用 bool 类型通知
        done <- true
    }()
    <-done
}

最佳实践:
通知 channels 应使用 chan struct{},传递数据的意义是无关的。

package main

func main() {
    done := make(chan struct{})
    go func() {
        // 正确使用 struct{} 作为通知 channel
        close(done)
    }()
    <-done
}

6. 不使用 nil channels (#66)

错误示例:

package main

func main() {
    var ch chan int
    select {
    case <-ch: // 未初始化的 channel,运行时错误
    }
}

最佳实践:
通过 nil channel 禁用某个分支。

func main() {
    var ch chan int = nil
    select {
    case <-ch: // 此分支永远不会被执行
    default:
        fmt.Println("分支被禁用")
    }
}

7. 不清楚如何确定 channel size (#67)

错误示例:

package main

func main() {
    ch := make(chan int, 0) // 无缓冲 channel 可能造成死锁
    ch <- 1
}

最佳实践:
合理设置 channel size,根据应用场景决定是否使用缓冲。

ch := make(chan int, 1) // 使用缓冲区避免阻塞

8. 使用 append 不当导致数据竞争 (#69)

错误示例:

package main

func main() {
    slice := []int{}
    go func() { slice = append(slice, 1) }()
    go func() { slice = append(slice, 2) }()
}

影响分析:
多个 Goroutine 并发修改共享 slice,导致数据竞争。

最佳实践:
使用锁保护或将 slice 替换为线程安全的结构。

package main

import (
    "sync"
)

func main() {
    var mu sync.Mutex
    slice := []int{}

    go func() {
        mu.Lock()
        defer mu.Unlock()
        slice = append(slice, 1)
    }()
}

总结

并发和数据处理是 Go 语言的核心,但也隐藏了众多陷阱。从 context 的取消、Goroutines 泄漏、到 channels 和锁的正确使用,每个错误都可能成为性能问题或 bug 的根源。在开发中,注意养成最佳实践习惯,才能让代码更加健壮和高效。

FunTester 原创精华
【免费合集】从 Java 开始性能测试
故障测试与 Web 前端
服务端功能测试
性能测试专题
Java、Groovy、Go
测试开发、自动化、白盒
测试理论、FunTester 风采
视频专题


↙↙↙阅读原文可查看相关链接,并与作者交流