Go-Channel

144 阅读3分钟

引发长时间的阻塞的情况

✅ 缓冲通道(Buffered Channel)

  • 通道已满:发送操作会阻塞,直到有元素被接收。

    • 阻塞的发送 goroutine 会进入一个发送等待队列,是 FIFO 的。
    • 一旦有空间释放,最早被阻塞的发送方会被唤醒。
  • 通道已空:接收操作会阻塞,直到有新元素写入。

    • 阻塞的接收 goroutine 进入接收等待队列,也是 FIFO 的。
    • 有元素写入时,会优先通知最早等待的接收方
  • 优化场景:若发送时刚好有接收者阻塞在等,那数据直接从发送方拷贝到接收方,绕过缓冲区(称为“同步发送”),提高效率。

✅ 1. 缓冲通道已满,发送操作阻塞

func main() {
	ch := make(chan int, 2) // 缓冲容量为2

	ch <- 1
	ch <- 2
	fmt.Println("Sent 2 values to ch")

	// 第三个发送会阻塞,直到有一个值被接收
	ch <- 3 // 阻塞在这里
	fmt.Println("This line will NOT print unless another goroutine receives from ch")
}

✅ 2. 缓冲通道已空,接收操作阻塞

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

	// 立即进行接收会阻塞,因为通道为空
	val := <-ch // 阻塞在这里
	fmt.Println("Received:", val)
}

✅ 3. 优化:缓冲通道为空时刚好有接收者,发送数据直接传递给接收者(绕过缓冲区)


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

	go func() {
		val := <-ch
		fmt.Println("Receiver got:", val)
	}()

	time.Sleep(time.Millisecond * 100) // 确保接收方先阻塞

	ch <- 10 // 此时会直接将 10 传给接收方,不放入缓冲区
	fmt.Println("Sender sent 10")
}

✅ 非缓冲通道(Unbuffered Channel)

  • 本质是同步机制:收发双方必须同时准备好,才能进行一次传输。
  • 没有缓冲区,数据是直接从发送方拷贝到接收方

✅ 非缓冲通道,收发需同步配对

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

	go func() {
		ch <- 42 // 发送操作会阻塞,直到有接收方
		fmt.Println("Sent 42")
	}()

	val := <-ch // 接收操作开始执行后,发送方才被唤醒
	fmt.Println("Received:", val)
}

输出:

Received: 42
Sent 42

⚠️ 错误使用通道的阻塞场景

  • 未初始化的通道(nil channel)

    • 发送和接收操作会永久阻塞

    • nil 通道通常是由于 var ch chan int 声明后未调用 make 初始化

      var ch chan int // ch == nil
      ch <- 1         // 永久阻塞
      

❌ nil 通道,发送和接收都会永久阻塞

func main() {
	var ch chan int // 没有初始化,ch == nil

	ch <- 1 // 永久阻塞
	fmt.Println("Will never be printed")
}

或者:

func main() {
	var ch chan int // ch 为 nil

	val := <-ch // 永久阻塞
	fmt.Println("Received:", val)
}

引发 panic 的情况

  • 发送到已关闭的通道:

    • 如果尝试向已经关闭的通道发送数据,会引发 panic
  • 关闭已经关闭的通道:

    • 如果尝试关闭一个已经关闭的通道,会引发 panic

✅ 示例代码:发送到已关闭的通道

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    // 发送数据的 goroutine
    go func() {
        ch <- 42 // 发送数据
        close(ch) // 关闭通道
    }()

    // 接收数据的 goroutine
    go func() {
        data := <-ch // 从通道接收数据
        fmt.Println("Received:", data)
    }()
    
    // 主 goroutine 尝试发送到已关闭的通道
    ch <- 100 // 这里会引发 panic
}

解释:

  • 在这个例子中,ch <- 100 会引发 panic,因为通道 ch 在之前已经关闭了。关闭后的通道不能再进行发送操作。

✅ 示例代码:关闭已经关闭的通道

package main

import "fmt"

func main() {
    ch := make(chan int)
    
    // 关闭通道的 goroutine
    go func() {
        close(ch)
        close(ch) // 再次关闭通道会引发 panic
    }()

    // 其他 goroutine
    go func() {
        data := <-ch
        fmt.Println("Received:", data)
    }()
}

解释:

  • 在上面的例子中,尝试两次关闭通道 ch。第一次关闭是合法的,但第二次关闭时会引发 panic,因为通道已经被关闭。

Note

如果通道关闭且没有剩余数据,接收操作会返回该类型的零值,而不是引发 panic

package main

import "fmt"

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

    // 发送数据的 goroutine
    go func() {
        ch <- 42
        close(ch) // 关闭通道
    }()

    // 接收数据的 goroutine
    go func() {
        data, ok := <-ch
        fmt.Println("Received:", data, "Channel open:", ok)
        
        data, ok = <-ch // 通道已经关闭,再次接收
        fmt.Println("Received:", data, "Channel open:", ok)
    }()

    // 主 goroutine 等待
    fmt.Scanln()
}

输出解释:

Received: 42 Channel open: true
Received: 0 Channel open: false