chiachan
chiachan
Published on 2025-01-19 / 4 Visits
0

go语言学习笔记 - channal

#go

Go语言笔记 - Channel

1. 什么是channel

Channel 是 Go 语言中用来在 goroutine 之间传递数据的通信机制,本质上像一个管道(队列),可以安全地在线程之间传数据,避免加锁。

通俗点说,Channel 就是 goroutine 之间用来**发送(send)接收(receive)**数据的桥梁。

2. 并发模型:Goroutine + Channel

CSP(Communicating Sequential Processes)是一种“用通信代替共享内存”的并发模型
Go 就是这么设计的,用 channel 通信,不用锁、不共享数据。

3. 如何使用channel

这里将举例如何使用channel进行消息产出、消费等。

创建channel

ch := make(chan int) // 创建一个传 int 的 channel

发送数据

ch <- 10 // 把 10 发送到 ch

接受数据

value := <-ch // 从 ch 接收一个值

完整实例

package main

import "fmt"

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

    go func() {
        ch <- "Hello from goroutine" // 子 goroutine 发送
    }()

    msg := <-ch // 主 goroutine 接收
    fmt.Println(msg)
}

4. channel分类

类型 特点 用途示例
无缓冲 channel 发送/接收必须同时进行 严格同步通信
有缓冲 channel 缓冲没满可继续发送 缓解同步压力
close + range 自动读取直到结束 读取所有数据,像读切片
select 多路复用 哪个 channel 来就处理谁 并发处理多个任务,超时控制等

无缓冲channel(默认)

发送与接收必须同时进行,否则会阻塞

package main

import "fmt"

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

    go func() {
        ch <- 1 // 没有接收者会卡在这
    }()

    val := <-ch
    fmt.Println("收到:", val)
}

有缓冲channel

缓冲区没满时,发送不会阻塞。
缓冲区满时,发送阻塞。
接收可以在发生之后再进行。

package main

import "fmt"

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

    ch <- 1
    ch <- 2
    // ch <- 3 // 再发就会阻塞或报错(缓冲满了)

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

关闭channel和range

range会一直读直到channel关闭

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch) // 表示不再发送数据了

    for val := range ch {
        fmt.Println("收到:", val)
    }
}

select多路复用

同时监听多个channel

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "来自 ch1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "来自 ch2"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    case <-time.After(3 * time.Second):
        fmt.Println("超时")
    }
}

5. select高级用法

非阻塞操作

// 发送
select {
case ch <- 1:
    fmt.Println("发送成功")
default:
    fmt.Println("channel 满,发送失败")
}

// 接收
select {
case val := <-ch:
    fmt.Println("收到:", val)
default:
    fmt.Println("没有数据可收")
}

超时控制

select {
case res := <-ch:
    fmt.Println("收到:", res)
case <-time.After(2 * time.Second):
    fmt.Println("超时退出")
}

同时监听多个 channel

select {
case v1 := <-ch1:
    fmt.Println("来自 ch1:", v1)
case v2 := <-ch2:
    fmt.Println("来自 ch2:", v2)
default:
    fmt.Println("两个 channel 都没准备好")
}

检测 channel 是否关闭

v, ok := <-ch
if !ok {
    fmt.Println("channel 已关闭")
}

6. channel死锁

死锁就是当 goroutine 被​永久阻塞​,比如等不到数据或者收不到数据,就会死锁。

场景 1:没有接收方

func main() {
    ch := make(chan int)
    ch <- 1 // 阻塞等待,但没人接收,程序卡死,panic: deadlock
}

场景 2:channel 关闭后还发送数据

func main() {
    ch := make(chan int, 1)
    close(ch)
    ch <- 1 // panic: send on closed channel
}

7. 注意事项

禁止向已关闭的 channel 发送数据

func main() {
    ch := make(chan int)
    close(ch)
    ch <- 1 // ❗panic: send on closed channel
}

禁止重复关闭channel

func main() {
    ch := make(chan int)
    close(ch)
    close(ch) // ❗panic: close of closed channel
}

禁止关闭或操作nil channel

nil channel 没法被关闭、也不能发送接收,否则会一直阻塞,或直接 panic

func main() {
    var ch chan int // nil
    // ch <- 1 // ❗panic: send on closed channel
    // <-ch // ❗panic: send on closed channel
    close(ch) // ❗panic: close of nil channel
}

从关闭的 channel 接收数据(合法,但可能要判断)

从关闭的 channel 中读取数据是​安全的​,只是会读到“零值”,并且 ok == false

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

    v, ok := <-ch
    fmt.Println(v, ok) // 👉 0 false(ok=false 说明 channel 关闭了)
}

接收使用range,必须关闭channel

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    // close(ch) // 关闭channel

    for val := range ch {
        fmt.Println("收到:", val)  // 死循环
    }
}

原则

谁创建、谁关闭;谁发送、谁不关闭;谁接收、谁不关心关闭。

什么情况下会触发资源泄漏

channel是有可能引发goroutine泄漏的。

goroutine使用操作channel后,会处于发送或接收的阻塞状态,如果此时channel一直处于满或空状态不改变,goroutine就会一直阻塞,不会结束也不会垃圾回收器回收

避坑小技巧

  1. 写入时,确保channel未关闭
  2. 确保关闭操作只有唯一操作者且只能执行一次
  3. 操作前确保channel已完成初始化
  4. 接收方使用range,必须关闭channel