简介
今天介绍一下Go语言的通道(Channel),也是Go自带的、唯一一个并发安全的类型。Channel是一种用来在不同 Go 协程之间传递数据的通信机制。通道提供了一种安全、同步的方式,用来避免多个协程访问共享数据时可能发生的竞态条件(race condition)问题。
示例:
func TestChannel(t *testing.T) {
ch := make(chan int, 3) //创建一个容量为3的缓存通道
ch <- 1 // 发送数据进入通道
receivedData := <-ch // 从通道接受数据
fmt.Println("Received data:", receivedData) // 输出:Received data: hello world
}
基本特性
- 并发安全
在并发环境下多个goroutine对通道进行发送或者接收操作,有且仅有一个goroutine会被还行,在这个协程发送或者接收数据完成之前,其他的协程都将阻塞在通道的等待队列和接收队列里。
这里提一下通道的两个等待队列:
发送等待队列:当多个协程往通道发送数据,只有一个会执行,其他的都会阻塞进入等待队列,队列是一个FIFO类型,也就是发送完成之后会首先通知最先进入队列的那个协程去执行发送。
接受等待队列:和发送等待队列类似,当多个协程进行接受操作的时候,没有抢到资源的协程会进入接收等待队列,待接收完成会通知最先进入队列的那个协程去执行接受操作。
- 完整性
数据的完整性,针对通道的发送操作其实存在两个步骤,第一步生成数据的副本,第二步将副本放入通道中,接受操作也会一样,不是直接取出通道的数据,而是先生成数据的副本,然后将通道的数据移除。通道的完整性保证这两个步骤一定是都完成的,才算执行一次发送或者接收操作。有了这个保证,就不用担心发送的数据还没发送完就被接收导致产生脏数据。
这里有个点需要注意下,放入通道的数据是拷贝过去的,而Go里面的拷贝都是浅拷贝,也就是只是拷贝了一个原数据的引用,底层的数据还是共享的,所以我们接收到数据通道的数据如果发生修改,原来的数据也会跟着修改。
示例:import ( "fmt" "runtime" "sync" "sync/atomic" "testing" ) type Singleton struct { Data string } func TestChannel(t *testing.T) { ch := make(chan *Singleton, 1) s1 := new(Singleton) s1.Data = "hello" ch <- s1 // 发送切片到通道 receivedData := <-ch // 从通道接收切片 receivedData.Data = "hello world" fmt.Println("Original data:", s1.Data) // 输出:Original data: hello world fmt.Println("Received data:", receivedData.Data) // 输出:Received data: hello world }
通道使用
无缓冲通道
无缓冲通道可以认为是一个串行执行的通道,发送方发送一个数据,在没有接收方准备就绪的情况下会一直阻塞,直到数据被接受。
func TestChannel(t *testing.T) {
// 创建一个不带缓冲的通道
ch := make(chan int)
// 启动一个Go协程,向通道发送数据
go func() {
fmt.Println("Sending data to the channel...")
ch <- 42 // 发送数据到通道,等待被接受
fmt.Println("Data has been sent to the channel.") //接收完被执行
}()
// 主协程等待一段时间,然后从通道接收数据
time.Sleep(time.Second) // 等待1秒钟,确保发送协程有足够的时间来执行
fmt.Println("Receiving data from the channel...")
data := <-ch // 从通道接收数据
fmt.Println("Received data from the channel:", data)
time.Sleep(time.Second)
}
//执行结果
Sending data to the channel...
Receiving data from the channel...
Received data from the channel: 42
Data has been sent to the channel.
有缓冲通道
有缓冲通道是一个异步执行的通道,跟我们平时用的消息队列比较类似,不过有个容量的问题,针对发送发,除非达到通道的容量,否则不会阻塞发送操作。针对接受方,只要通道可用数据不为0就不会阻塞接收操作,同样用上面的示例:
func TestChannel(t *testing.T) {
// 创建一个容量为3的缓冲通道
ch := make(chan int, 3)
// 启动一个Go协程,向通道发送数据
go func() {
fmt.Println("Sending data to the channel...")
ch <- 42 // 发送数据到通道,不用等待,会继续往下执行
fmt.Println("Data has been sent to the channel.")
}()
// 主协程等待一段时间,然后从通道接收数据
time.Sleep(time.Second) // 等待1秒钟,确保发送协程有足够的时间来执行
fmt.Println("Receiving data from the channel...")
data := <-ch // 从通道接收数据
fmt.Println("Received data from the channel:", data)
time.Sleep(time.Second)
}
//执行结果
Sending data to the channel...
Data has been sent to the channel.
Receiving data from the channel...
Received data from the channel: 42
单向通道
单向通道顾明意思就是只能发送或者只能接收,这里有些人可能有些疑问?那这样的通道可以用来干什么,只能发送那谁来消费呢?其实所谓的单向通道并不是通道本身的结构性质,而是一种行为性质。如果我们定义一个函数,希望这个函数的只能针对某个通道进行发送操作,另外一个函数只能对通道进行接收操作,从而限制通道的操作权限,在使用这两个函数的时候,传入还是同一个通道,看看下这个示例:
//参数是一个单向发送通道
func sendData(ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
ch <- 42 // 发送数据到通道
}
//参数是一个单向接收通道
func processAndConsumeData(sendCh <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
data := <-sendCh // 从通道接收数据
fmt.Println("Received data:", data)
// 进行数据处理或消费操作
}
func TestOnlyChannel(t *testing.T) {
ch := make(chan int) // 发送数据的通道
wg := &sync.WaitGroup{}
// 启动一个协程发送数据
wg.Add(1)
go sendData(ch, wg)
// 启动另一个协程处理和消费数据
wg.Add(1)
go processAndConsumeData(ch, wg) //接收传入的通道和发送是同一个
// 等待所有协程执行完成
wg.Wait()
}
单向通道的一大作用就是约束通道操作的行为
多路复用
提到多路复用是不是想到了Java里面的sokcet的多路复用模式,其实还真有点相似,连关键字都是一样的。在Go中通过select和通道联合使用可以达到多路复用的效果。
func TestChannel(t *testing.T) {
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
//intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
}
示例中通过select订阅在这几个通道上,哪个通道上有数据即满足条件执行,如果都没有满足就执行default,不会阻塞。这里有个点需要注意一下就是和java中的switch case不同,执行了case中的操作不需要break也不会执行后面的case操作。
通过select和channel实现一个等待:
func TestChannel(t *testing.T) {
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
go func() {
select { //阻塞通道中有数据发生
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
}
println("receive data")
}()
time.Sleep(time.Second)
intChannels[index] <- index
println(111)
}
//执行结果
The index: 2
111
The third candidate case is selected, the element is 2.
receive data
通过select和channel实现一个超时等待:
func TestChannel(t *testing.T) {
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
go func() {
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
case <-time.After(time.Millisecond * 100): //增加一个超时等待,超过100ms没有满足条件即执行超时操作,执行后续流程
fmt.Println("time out")
}
println("receive data")
}()
time.Sleep(time.Second)
intChannels[index] <- index
println(111)
}
//执行结果
The index: 2
time out
receive data
111