万字长文带你吃透golang channel

Go语言是一门支持并发编程的语言,它提供了一种特殊的数据类型:channel,用于在不同的goroutine之间传递数据和同步。channel可以让我们编写出简洁、高效、安全的并发程序。本文将介绍channel的基本概念和语法、特性和原理、最佳实践和高级用法,希望能够帮助你更好地理解和使用channel。

一、channel的基本概念和语法

channel是一种引用类型,它可以用来在两个或多个goroutine之间传递数据。

1. 声明一个channel

要使用channel,首先需要声明,channel的声明方式如下:

var ch chan int // 声明一个int类型的channel,零值为nil
ch = make(chan int) // 创建一个int类型的channel,返回一个channel值

ch := make(chan int) // 声明并创建一个int类型的channel

channel的创建方式是使用内置函数make,它接受一个channel类型作为参数,并返回一个对应类型的channel值。我们可以指定一个可选的容量参数,表示channel能够缓存的数据个数。如果不指定容量参数,或者指定为0,那么就是创建一个无缓冲的channel。

2. 缓冲

上面说到声明channel的时候,make的第二个参数如果是正整数,则表明是一个有缓冲的channel,我们再来创建一个无缓冲和有缓冲的channel来直接对比一下声明上有什么差异:

ch1 := make(chan int) // 创建一个无缓冲的int类型的channel
ch2 := make(chan int, 10) // 创建一个有缓冲的int类型的channel,容量为10

如何理解缓冲两个字呢,其实就是发送数据的时候,无论是否有接收方,都可以成功发送,但channel中最多缓存容量个元素,如果容量达到最大值,发送将会阻塞(暂且认为是阻塞,不同的场景会导致出现的结果不同)

ch1 := make(chan int)     // 创建一个无缓冲的int类型的channel
ch2 := make(chan int, 10) // 创建一个有缓冲的int类型的channel,容量为10
ch2 <- 1 // 正常运行,
ch1 <- 1 // panic,fatal error: all goroutines are asleep - deadlock!

3.发送和接收操作

channel的操作主要有两种:发送和接收。发送操作是使用<-运算符将一个值发送到channel中,接收操作是使用<-运算符从channel中接收一个值。发送和接收操作都是阻塞的,也就是说,如果没有对应的接收方或发送方,那么操作就会等待,直到有匹配的操作出现。

ch := make(chan int) // 创建一个无缓冲的int类型的channel
go func() {
   
    n := <-ch // 从ch中接收一个值,赋给n
    fmt.Println("Received:", n)
}()
ch <- 42 // 向ch中发送一个值42
fmt.Println("Sent:", 42)

上面的代码中,我们创建了一个无缓冲的int类型的channel,并启动了一个匿名goroutine来从channel中接收数据。然后我们向channel中发送了一个值42,并打印出"Sent: 42"。注意,如果没有启动匿名goroutine来接收数据,那么主goroutine在发送数据时就会阻塞,导致死锁。

4.关闭channel

我们可以使用close函数来关闭一个channel。关闭一个channel表示不再向它发送任何数据。关闭一个已经关闭的channel或者从一个已经关闭的channel中发送数据都会导致panic。我们可以使用ok-idiom来判断一个channel是否已经关闭。

ch := make(chan int) // 创建一个无缓冲的int类型的channel
go func() {
   
    for {
   
        n, ok := <-ch // 从ch中接收一个值,并判断是否成功
        if !ok {
   
            fmt.Println("Channel closed")
            break
        }
        fmt.Println("Received:", n)
    }
}()
ch <- 1 // 向ch中发送一个值1
ch <- 2 // 向ch中发送一个值2
close(ch) // 关闭ch
fmt.Println("Channel closed")

上面的代码中,我们创建了一个无缓冲的int类型的channel,并启动了一个匿名goroutine来循环从channel中接收数据。我们使用n, ok := <-ch的语法来接收数据,并判断ok是否为true。如果为true,表示接收成功;如果为false,表示channel已经关闭,那么就跳出循环。然后我们向channel中发送了两个值,并关闭了channel。注意,关闭channel并不会影响从channel中接收已经发送的数据,只是不能再发送新的数据。

5.channel的方向

最后,我们还可以指定channel的类型和方向。类型表示channel能够传递的数据的类型,方向表示channel能够进行的操作。默认情况下,channel是双向的,既能发送也能接收。但是我们可以将一个双向的channel转换为一个单向的channel,只能发送或只能接收。

var ch1 chan int // 声明一个双向的int类型的channel
var ch2 chan<- int // 声明一个只能发送的int类型的channel
var ch3 <-chan int // 声明一个只能接收的int类型的channel

ch1 = ch2 // 错误,不能将一个只能发送的channel赋值给一个双向的channel
ch1 = ch3 // 错误,不能将一个只能接收的channel赋值给一个双向的channel
ch2 = ch1 // 正确,可以将一个双向的channel赋值给一个只能发送的channel
ch3 = ch1 // 正确,可以将一个双向的channel赋值给一个只能接收的channel

上面的代码中,我们声明了三个不同方向的int类型的channel,并尝试进行赋值操作。注意,只能将一个双向的channel转换为一个单向的channel,反过来是不行的。单向的channel主要用于函数参数或返回值,用来限制函数对channel的操作。

func send(ch chan<- int, n int) {
   
    ch <- n // 可以发送数据
    // n = <-ch // 错误,不能接收数据
}

func receive(ch <-chan int) int {
   
    n := <-ch // 可以接收数据
    // ch <- n // 错误,不能发送数据
    return n
}

func main() {
   
    ch := make(chan int) // 创建一个双向的int类型的channel
    go send(ch, 42) // 调用send函数,传入一个只能发送的channel
    n := receive(ch) // 调用receive函数,传入一个只能接收的channel
    fmt.Println(n) // 打印42
}

上面的代码中,我们定义了两个函数:sendreceive,它们分别接受一个只能发送和一个只能接收的int类型的channel作为参数,并进行相应的操作。然后我们在主函数中创建了一个双向的int类型的channel,并分别传入这两个函数中。这段代码完成了最基本的发送者和接收者的完整示例。

二、channel的特性和原理

1. 无缓冲和有缓冲channel

前面我们简单介绍了缓冲的含义和基本用法,无缓冲和有缓冲channel是指创建时是否指定了容量参数。无缓冲的channel没有容量参数,或者容量参数为0;有缓冲的channel有一个正整数的容量参数。无缓冲和有缓冲channel在行为上有一些区别,主要体现在以下几个方面:

  • 无缓冲的channel是同步的,也就是说,发送和接收操作必须成对出现,否则会阻塞。无缓冲的channel可以用来实现goroutine之间的同步和通信,例如等待一个goroutine完成某个任务,或者传递一个信号或一个值。
  • 有缓冲的channel是异步的,也就是说,发送操作只有在channel已满时才会阻塞,接收操作只有在channel为空时才会阻塞。有缓冲的channel可以用来实现goroutine之间的解耦和流量控制,例如实现一个worker pool或者一个消息队列。
  • 无缓冲的channel保证了发送和接收操作的顺序一致,也就是说,发送方发送的第一个值一定会被接收方第一个接收到。有缓冲的channel则不能保证这一点,因为发送方和接收方可能并发地进行操作,导致数据在channel中乱序。
  • 无缓冲的channel通常用于传递少量的数据,因为它们不能缓存数据。有缓冲的channel通常用于传递大量的数据,因为它们可以缓存数据。但是要注意,channel不是用来存储数据的,而是用来传递数据的。如果我们不及时地从channel中接收数据,那么就会导致发送方阻塞,影响程序的性能。

下面我们举一些例子来说明无缓冲和有缓冲channel的使用场景。

1.)无缓冲channel的使用场景
  • 实现goroutine之间的同步
func main() {
   
    done := make(chan struct{
   }) // 创建一个无缓冲的空结构体类型的channel
    go func() {
   
        fmt.Println("Hello, world!") // 打印一句话
        done <- struct{
   }{
   } // 向done中发送一个空结构体值
    }()
    <-done // 从done中接收一个值,表示goroutine已经完成
}

上面的代码中,我们创建了一个无缓冲的空结构体类型的channel,并启动了一个匿名goroutine来打印一句话。然后我们向done中发送了一个空结构体值,并在主函数中从done中接收了一个值。这样就实现了主函数等待匿名goroutine完成的同步效果。注意,这里我们使用空结构体类型是因为它不占用任何内存空间,只是用来传递信号。

  • 实现goroutine之间的通信
func main() {
   
    ch := make(chan int) // 创建一个无缓冲的int类型的channel
    go func() {
   
        n := <-ch // 从ch中接收一个值
        fmt.Println("Received:", n)
    }()
    ch <- 42 // 向ch中发送一个值
    fmt.Println("Sent:", 42)
}

上面的代码中,我们创建了一个无缓冲的int类型的channel,并启动了一个匿名goroutine来从channel中接收数据。然后我们向channel中发送了一个值,并打印出"Sent: 42"。这样就实现了主函数向匿名goroutine传递数据的通信效果。

2.)有缓冲channel的使用场景
  • 实现goroutine之间的解耦
func main() {
   
    ch := make(chan int, 10) // 创建一个有缓冲的int类型的channel,容量为10
    go func() 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值