FunTester Go 语言常见错误——并发编程实践

FunTester · March 29, 2025 · 1265 hits

在 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 风采
视频专题
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up