【容易踩坑】go channel解析

274 阅读13分钟

1 channel介绍

在Go语言中,channel(通道)是一种用于在goroutine之间进行通信的机制。它可以用于在不同goroutine之间传递数据,并且保证了并发安全。

channel可以被看作是一个队列,goroutine可以向channel发送数据,也可以从channel接收数据。发送和接收操作是阻塞的,即如果没有对应的接收或发送操作,goroutine会被阻塞在该操作上,直到有对应的操作为止。

channel的声明和初始化如下:

  make(chan Type)  //等价于make(chan Type, 0)
  make(chan Type, capacity)

其中,数据类型表示通道中传递的数据类型,可以是任意类型。使用make函数初始化一个channel,返回的是一个指向channel的引用。

channel的发送和接收操作分别使用<-操作符:

channel <- value      //发送value到channel
<-channel             //接收并将其丢弃

x := <-channel        //从channel中接收数据,并赋值给x
x, ok := <-channel    //功能同上,同时检查通道是否已关闭或者是否为空

1.1 channel的特性如下:

  1. channel是类型安全的:channel在声明时需要指定传递的数据类型,只能传递指定类型的数据,避免了类型错误。
  2. channel是并发安全的:多个goroutine可以同时操作一个channel,而不会发生数据竞争(data race)。
  3. channel是有容量限制的:在创建channel时,可以指定其容量,如果不指定容量,或者容量为0,则表示该channel是无缓冲的,即发送操作和接收操作是同步的;如果指定了容量,则表示该channel是有缓冲的,可以缓存一定数量的数据,当缓冲区满时,发送操作会阻塞,直到有空间可用。
  4. channel是同步的:发送操作和接收操作都是阻塞的,发送操作会阻塞直到数据被接收,接收操作会阻塞直到有数据可用。

channel在Go语言中被广泛应用于并发编程,可以用于协调不同goroutine之间的工作,并实现数据的安全传递。

image.png

1.2 举例

下面是一个使用channel进行并发通信的例子:

package main

import (
	"fmt"
	"time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Println("worker", id, "started job", j)
		time.Sleep(time.Second)
		fmt.Println("worker", id, "finished job", j)
		results <- j * 2
	}
}

func main() {
	jobs := make(chan int, 5)
	results := make(chan int, 5)

	// 启动3个goroutine来处理工作
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, results)
	}

	// 发送5个工作到jobs通道
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)

	// 从results通道接收结果
	for a := 1; a <= 5; a++ {
		<-results
	}
}

在这个例子中,我们创建了两个channel:jobsresultsjobs通道用于发送工作任务,results通道用于接收处理结果。

我们启动了3个goroutine来处理工作,每个goroutine都从jobs通道接收工作任务,处理完后将结果发送到results通道。

main函数中,我们向jobs通道发送了5个工作任务,然后关闭了jobs通道。接着,我们从results通道接收了5个处理结果。

通过使用channel,我们实现了多个goroutine之间的并发通信和协调,每个工作任务都会被分配给一个可用的goroutine进行处理,并且结果会被发送到results通道中供主goroutine接收。

// 执行结果如下(执行结果不唯一)
worker 3 started job 1
worker 1 started job 2
worker 2 started job 3
worker 2 finished job 3
worker 2 started job 4
worker 3 finished job 1
worker 3 started job 5
worker 1 finished job 2
worker 2 finished job 4
worker 3 finished job 5

1.3 channel操作注意事项

在使用channel进行通信时,有一些注意事项和易错地方需要注意。下面是一些示例说明:

  1. 避免在没有接收方的情况下发送数据:
ch := make(chan int)
ch <- 42 // 这里会导致阻塞,因为没有goroutine在接收数据

在这个例子中,我们创建了一个无缓冲的channel ch,并尝试向其发送数据。由于没有goroutine在接收数据,发送操作会导致阻塞,从而导致程序无法继续执行。为了避免这种情况,我们应该确保在发送数据之前有一个接收方。

  1. 避免在没有发送方的情况下接收数据:
ch := make(chan int)
result := <-ch // 这里会导致阻塞,因为没有goroutine在发送数据

在这个例子中,我们创建了一个无缓冲的channel ch,并尝试从其接收数据。由于没有goroutine在发送数据,接收操作会导致阻塞,从而导致程序无法继续执行。为了避免这种情况,我们应该确保在接收数据之前有一个发送方。

  1. 避免向已关闭的channel发送数据:
ch := make(chan int)
close(ch)
ch <- 42 // 这里会导致panic,因为channel已经关闭

在这个例子中,我们创建了一个无缓冲的channel ch,并在发送数据之前关闭了channel。由于channel已经关闭,再向其发送数据会导致panic。为了避免这种情况,我们应该在发送数据之前检查channel是否已经关闭。

  1. 避免重复关闭channel:
ch := make(chan int)
close(ch)
close(ch) // 这里会导致panic,因为channel已经关闭

在这个例子中,我们创建了一个无缓冲的channel ch,并多次关闭了channel。由于channel只能关闭一次,再次关闭channel会导致panic。为了避免这种情况,我们可以使用len函数来检查channel是否已经关闭。

2 无缓冲的channel

无缓冲的channel是指在创建channel时,没有指定容量(或容量为0),即不能缓存任何数据。发送操作和接收操作都是同步的,即发送操作会阻塞直到有goroutine接收数据,接收操作会阻塞直到有goroutine发送数据。

下面是一个使用无缓冲的channel进行并发通信的例子:

package main

import (
	"fmt"
	"time"
)

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

	go func() {
		fmt.Println("Goroutine started")
		time.Sleep(time.Second)
		ch <- 42 // 发送数据到无缓冲的channel
		fmt.Println("Goroutine finished")
	}()

	fmt.Println("Waiting for data...")
	data := <-ch // 从无缓冲的channel接收数据
	fmt.Println("Received data:", data)
}

在这个例子中,我们创建了一个无缓冲的channel ch。在一个新的goroutine中,我们向ch通道发送了数据42,然后打印了一条消息表示goroutine已完成。

main函数中,我们打印了一条消息表示正在等待数据。然后,我们从ch通道接收数据,并将其赋值给变量data。最后,我们打印了接收到的数据。

由于无缓冲的channel是同步的,所以在接收操作之前,主goroutine会一直阻塞,直到有goroutine向通道发送数据。因此,在这个例子中,主goroutine会一直等待直到新的goroutine发送数据到通道中。一旦数据被发送,主goroutine会继续执行,并接收到发送的数据。

无缓冲的channel适用于需要确保发送和接收操作同时进行的场景,可以用于协调不同goroutine之间的工作。

// 执行结果
Waiting for data...
Goroutine started
Goroutine finished
Received data: 42

2.1 无缓冲的复杂例子

当然,下面是一个更复杂的例子,演示了如何使用无缓冲的channel来实现一个生产者-消费者模型:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func producer(ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()

	for i := 0; i < 5; i++ {
		data := rand.Intn(100)
		fmt.Println("Producer produced:", data)
		ch <- data // 发送数据到无缓冲的channel
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
	}

	close(ch)
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for data := range ch {
		fmt.Println("Consumer consumed:", data)
		time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
	}
}

func main() {
	ch := make(chan int)
	wg := sync.WaitGroup{}

	wg.Add(2)
	go producer(ch, &wg)
	go consumer(ch, &wg)

	wg.Wait()
}

在这个例子中,我们创建了一个无缓冲的channel ch,并使用sync.WaitGroup来等待生产者和消费者goroutine完成。

生产者goroutine使用一个循环来生成随机数,并将其发送到ch通道中。每个随机数表示一个生产的数据项。然后,生产者goroutine会随机等待一段时间。

消费者goroutine使用一个range循环来从ch通道接收数据。每个接收到的数据项表示一个消费的数据项。然后,消费者goroutine会随机等待一段时间。

main函数中,我们启动了生产者和消费者goroutine,并使用sync.WaitGroup等待它们完成。

通过使用无缓冲的channel,我们实现了生产者-消费者模型,生产者goroutine会生产数据并发送到通道中,消费者goroutine会从通道中接收数据并进行消费。由于无缓冲的channel是同步的,所以生产者和消费者goroutine会进行有效的协调,确保每个数据项都会被消费。

// 执行结果
Producer produced: 48
Consumer consumed: 48
Producer produced: 17
Consumer consumed: 17
Producer produced: 33
Consumer consumed: 33
Producer produced: 9
Consumer consumed: 9
Producer produced: 57
Consumer consumed: 57

2.2 无缓冲channel应用场景

无缓冲的channel在以下情况下特别有用:

  1. 同步操作:无缓冲的channel是同步的,发送操作会阻塞直到有goroutine接收数据,接收操作会阻塞直到有goroutine发送数据。这使得无缓冲的channel非常适合用于协调不同goroutine之间的工作,确保发送和接收操作同时进行。

  2. 传递数据:无缓冲的channel可以用于在goroutine之间传递数据。由于无缓冲的channel没有存储空间,发送操作会等待接收操作,这意味着发送的数据会直接传递给接收操作,而不会在通道中存储。

  3. 顺序保证:无缓冲的channel可以用于确保goroutine之间的顺序执行。当一个goroutine向无缓冲的channel发送数据时,它会阻塞直到数据被接收。这样,可以确保发送操作在接收操作之前发生,从而保证了顺序执行。

  4. 限制资源使用:无缓冲的channel可以用于限制资源的使用。例如,如果有多个goroutine需要访问某个共享资源,可以使用无缓冲的channel来限制同时访问该资源的goroutine数量。

需要注意的是,由于无缓冲的channel是同步的,发送和接收操作会导致goroutine阻塞,因此在使用无缓冲的channel时,需要确保有足够的goroutine来处理发送和接收操作,以避免死锁情况的发生。

3 有缓冲channel

有缓冲的channel是一种可以在其中存储一定数量的元素的channel。与无缓冲的channel不同,有缓冲的channel在发送操作时,只有当channel已满时才会导致发送方阻塞;在接收操作时,只有当channel为空时才会导致接收方阻塞。

有缓冲的channel可以提供一定的异步性,因为发送方可以继续发送数据,而不需要等待接收方立即接收。这可以用于解耦发送方和接收方的速度,以及平衡负载。

下面是一个使用有缓冲的channel的示例:

ch := make(chan int, 3) // 创建一个有缓冲大小为3的channel

ch <- 1 // 发送数据到channel
ch <- 2
ch <- 3

fmt.Println(<-ch) // 从channel接收数据
fmt.Println(<-ch)
fmt.Println(<-ch)

在这个例子中,我们创建了一个有缓冲大小为3的channel ch。我们向channel发送了3个数据,并从channel接收了这3个数据。由于channel是有缓冲的,发送操作不会立即导致发送方阻塞,只有当channel已满时才会阻塞。同样地,接收操作也只有在channel为空时才会导致接收方阻塞。

需要注意的是,在使用有缓冲的channel时,需要确保发送操作不会超过channel的缓冲大小,否则会导致发送方阻塞。同样地,需要确保接收操作不会超过channel中的元素数量,否则会导致接收方阻塞。

3.1 有缓冲channel复杂一点例子

下面是一个更复杂的例子,展示了如何使用有缓冲的channel来实现生产者-消费者模型:

package main

import (
	"fmt"
	"time"
)

func producer(ch chan<- int) {
	for i := 1; i <= 5; i++ {
		fmt.Println("Producer sending:", i)
		ch <- i // 发送数据到channel
		time.Sleep(500 * time.Millisecond)
	}
	close(ch) // 关闭channel
}

func consumer(ch <-chan int) {
	for num := range ch { // 从channel接收数据,直到channel关闭
		fmt.Println("Consumer received:", num)
		time.Sleep(1 * time.Second)
	}
}

func main() {
	ch := make(chan int, 3) // 创建一个有缓冲大小为3的channel

	go producer(ch) // 启动生产者goroutine
	go consumer(ch) // 启动消费者goroutine

	time.Sleep(5 * time.Second) // 等待一段时间,让生产者和消费者完成

	fmt.Println("Done")
}

在这个例子中,我们创建了一个有缓冲大小为3的channel ch。我们启动了一个生产者goroutine producer 和一个消费者goroutine consumer,它们会并发地执行。

生产者goroutine会向channel发送数据,并在每次发送后等待500毫秒。消费者goroutine会从channel接收数据,并在每次接收后等待1秒。

通过使用有缓冲的channel,生产者和消费者之间的速度可以解耦。生产者可以继续发送数据,即使消费者的处理速度较慢,只有当channel已满时才会导致生产者阻塞。同样地,消费者可以继续接收数据,即使生产者的生成速度较快,只有当channel为空时才会导致消费者阻塞。

通过在main函数中等待一段时间,我们确保生产者和消费者有足够的时间完成它们的工作。最后,我们打印出"Done"表示程序执行完毕。

3.2 有缓冲的应用场景

有缓冲的channel在以下场景中特别有用:

  1. 异步任务处理:有缓冲的channel可以用于实现异步任务处理。发送方可以将任务发送到channel,而不需要等待任务完成。接收方可以从channel接收任务,并在后台处理这些任务。

  2. 限制并发数量:有缓冲的channel可以用于限制并发数量。发送方可以将任务发送到channel,而不需要等待任务完成。接收方可以从channel接收任务,并在后台处理这些任务。由于channel有缓冲,发送方可以继续发送任务,直到channel已满,这样可以限制并发数量。

  3. 事件通知:有缓冲的channel可以用于事件通知。发送方可以将事件发送到channel,而不需要等待事件处理完成。接收方可以从channel接收事件,并进行相应的处理。

  4. 数据传输:有缓冲的channel可以用于数据传输。发送方可以将数据发送到channel,而不需要等待接收方立即接收。接收方可以从channel接收数据,并进行相应的处理。

需要注意的是,使用有缓冲的channel时,需要确保发送操作不会超过channel的缓冲大小,否则会导致发送方阻塞。同样地,需要确保接收操作不会超过channel中的元素数量,否则会导致接收方阻塞。

接下来会将 select 和 锁 的应用。。。(待更新)