Go 语言中 Channel 的使用 | 豆包MarsCode AI刷题

159 阅读3分钟

Go 语言中的 Channel 学习笔记

在 Go 语言中,并发 (Concurrency) 是其备受喜爱的特性之一,而 Channel 则是并发编程的核心工具之一。Go 官方文档通过示例详细讲解了 Channel 的基本概念和用法。如果您对并发编程不太熟悉,可以参考官方示例逐步学习。本文主要记录 Channel 的使用场景,并通过示例理解 Channel 的作用及其经典名言:

Do not communicate by sharing memory; instead, share memory by communicating.
(不要通过共享内存来通信,而是通过通信来共享内存。)


Channel 的基本概念

Channel 用于在多个 goroutine 之间进行通信,可以分为读和写两种操作。在实际开发中,如果代码中用到了大量的 Channel,需要注意合理规范其使用场景,否则可能导致逻辑混乱或难以调试。建议在团队开发中明确 Channel 的使用规则,例如:

  • 仅用于写入:chan<- int
  • 仅用于读取:<-chan int
  • 读写通用:chan int

规范建议

为了代码可读性和可维护性,建议将 Channel 的读写逻辑分离到不同的函数中,这样便于理解和调试。例如:

// 只写入
func writeOnlyChannel(ch chan<- int, data int) {
    ch <- data
}

// 只读取
func readOnlyChannel(ch <-chan int) int {
    return <-ch
}

共享内存导致的问题

在传统编程中,多个 goroutine 共享内存变量常常引发竞争问题:

package main

import (
    "fmt"
    "sync"
)

func addByShareMemory(n int) []int {
    var ints []int
    var wg sync.WaitGroup

    wg.Add(n)
    for i := 0; i < n; i++ {
        go func(i int) {
            defer wg.Done()
            ints = append(ints, i) // 多 goroutine 共享内存
        }(i)
    }

    wg.Wait()
    return ints
}

func main() {
    foo := addByShareMemory(10)
    fmt.Println(len(foo))
    fmt.Println(foo)
}

运行结果

每次运行代码时,ints 的值都会不同,原因如下:

  • ints 是一个共享变量,多个 goroutine 会同时对其读写。
  • 由于没有同步机制,导致数据竞态问题,出现不可预测的结果。

解决共享内存问题

方法一:调整 GOMAXPROCS

通过设置 runtime.GOMAXPROCS(1) 限制程序只使用一个逻辑处理器,从而避免并发问题:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

func init() {
    runtime.GOMAXPROCS(1)
}

这种方法仅适用于测试,实际项目中不建议使用,因为它会显著降低并发能力。


方法二:使用 sync.Mutex

通过互斥锁 (Mutex) 保证对共享变量的安全访问:

func addByShareMemory(n int) []int {
    var ints []int
    var wg sync.WaitGroup
    var mux sync.Mutex

    wg.Add(n)
    for i := 0; i < n; i++ {
        go func(i int) {
            defer wg.Done()
            mux.Lock()
            ints = append(ints, i) // 加锁保护共享变量
            mux.Unlock()
        }(i)
    }

    wg.Wait()
    return ints
}

方法三:通过 Channel 共享内存

使用 Channel 实现 "通过通信来共享内存" 的理念:

func addByShareCommunicate(n int) []int {
    var ints []int
    channel := make(chan int, n) // 创建缓冲区为 n 的 Channel

    for i := 0; i < n; i++ {
        go func(ch chan<- int, val int) {
            ch <- val // 写入 Channel
        }(channel, i)
    }

    for i := range channel {
        ints = append(ints, i)
        if len(ints) == n {
            break
        }
    }

    close(channel) // 关闭 Channel
    return ints
}

在这个例子中:

  • 只写入 Channel 的 goroutine 使用 chan<- int 限制其功能,仅允许写入。
  • 主 goroutine 使用 for 循环从 Channel 中读取数据,从而避免了共享内存问题。
  • 使用 Channel 后,无需再使用 sync.WaitGroupsync.Mutex,代码更简洁,逻辑更清晰。

性能比较

对上述两种实现方式进行性能测试,结果如下:

BenchmarkAddByShareMemory-8        31131   38005 ns/op  2098 B/op  11 allocs/op
BenchmarkAddByShareCommunicate-8   22915   51837 ns/op  2936 B/op   9 allocs/op
  • 通过共享内存实现 的性能更高,操作耗时和内存分配较少。
  • 通过 Channel 实现 虽然稍慢,但代码清晰、易维护,更适合需要频繁通信的场景。

总结

适用场景

  • 如果多个 goroutine 之间不需要频繁通信,可以直接使用 sync.WaitGroupsync.Mutex,性能较优。
  • 如果需要多个 goroutine 之间交换数据,建议使用 Channel 实现共享内存,代码清晰且便于维护。

注意事项

  1. Channel 的使用场景应明确,例如:

    • 生产者-消费者模式
    • 数据流处理
  2. 不要滥用 Channel,尤其是在可以通过简单回调或普通变量实现的情况下。