Go的通道-基本用法

本文深入探讨Go语言的通道基础,包括并发安全的通道类型、通道的声明与初始化、并发特性和阻塞情况。文章还讨论了发送和接收操作的互斥、通道容量与长度的关系,以及元素值的浅拷贝特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Go的通道-基本用法

一、Go语言独有的并发编程模式和编程哲学

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)

二、通道的基础知识

2.1 通道类型的值本身是并发安全的

通道类型的值本身就是并发安全的,这也是Go语言自带的、唯一一个可以满足并发安全性的类型。

2.2 声明并初始化一个通道

像使用make初始化切片一样初始化一个通道。第一个参数代表通道的具体类型的类型字面量。

// 比如
make(chan int) // chan是通道类型的关键字,int说明该通道类型的元素类型

还可以接受第二个int类型的参数,第二个参数是可选的。指通道的容量,表示该通道最多可以缓存多少个元素值。这个值不能小于0。

当容量为0时,这个通道称为非缓冲通道,也就是不带缓冲的通道。

2.3 一个通道相当于一个先进先出(FIFO)的队列

使用接送操作符(<-)发送和接收元素值。

package main

import (
	"fmt"
)

/*
输出:
1
*/
func main() {
	ch1 := make(chan int, 3)
	ch1 <- 1
	ch1 <- 2
	ch1 <- 3
	fmt.Printf("管道ch1 接收的第一个元素:%d\n", <-ch1)
}

三、对于发送和接收操作都有哪些基本的特性

它们的基本特性如下:

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间是互斥的;
  2. 发送操作和接收操作中对元素值的处理都是不可分割的;
  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此;

3.1 对于同一个通道,发送操作之间是互斥的,接收操作之间是互斥的

在同一时刻,只会执行同一个通道的任意个发送操作中的某一个。直到这个元素值被完全复制进该通道之后,其他针对该通道的发送操作才可能被执行。

类似的,在同一时刻,运行时系统只会执行,对同一个通道的任意个接收操作中的某一个。直到这个元素完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的,也是如此。

这里要注意一个细节,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。

另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含两步,第一步时生成正在通道中的这个元素值的副本,并准备给接收方,第二步是删除在通道中的这个元素值。

3.2 发送操作和接收操作中对元素值的处理都是不可分割的

这里“不可分割”的意思是,它们处理元素值时都是一气呵成的,绝不会被打断。

3.3 发送操作在完全完成之前会被阻塞。接收操作也是如此

发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。也就是说,在它之后的代码不会有执行的机会,直到这句代码的阻塞解除。

更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。

接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止

四、发送操作和接收操作在什么时候可能被长时间的阻塞?

4.1 情况一:针对缓冲通道

(1)发送操作的阻塞

针对缓冲通道。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。

这时,通道会优先通知最早因此而等待的、那个发送操作所在的goroutine,后者会再次执行发送操作。

由于发送操作在这种情况下被阻塞后,它们所在的goroutine会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。

(2)接收操作的阻塞

相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。

这时,通道会优先通知最早等待的那个接收操作所在的goroutine,并使它再次执行接收操作。

因此而等待的所有接收操作的goroutine,都会按照先后顺序被放入通道内部的接收等待队列。

4.2 情况二:针对非缓冲通道

无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。因此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有后发双发对接上了,数据才会被传递。

并且,数据是直接从发送方复制到接收方的。中间不会有非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据。

在大多数情况下,缓冲通道会作为收发双方的中间件。正如前文所述,元素值会先从发送方复制到缓冲通道,之后再由缓冲通道复制给接收方。

但是,当发送操作在执行的时候,发现空的通道中,正好有等待的接收操作,那么它会直接把元素复制给接收方。

4.3 情况三:错误的使用通道,导致的阻塞

对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永远地处于阻塞状态。它们所属的goroutine中的任何代码,都不会再被执行。

由于通道类型是引用类型,所以它的零值就是nil。换句话说,当我们只声明该类型的变量,但是没有用make函数对它进行初始化时,该变量的值就会时nil。

package main

import (
	"fmt"
)

func main() {
	// 示例1
	ch1 := make(chan int, 1)
	ch1 <- 1
	// ch1 <- 2 // 通道已满,这里会造成则色
	fmt.Println(<-ch1)

	// 示例2
	ch2 := make(chan int, 1)
	// elem, ok := <-ch2 // 通道已空,这里会造成阻塞
	// fmt.Printf("%d, %t\n", elem, ok)
	ch2 <- 1

	// 示例3
	var ch3 chan int
	// ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!
	// fmt.Println(<-ch3) // 通道的值为nil,因此这里会造成永久的阻塞!
	_ = ch3
}

五、发送操作和接收操作何时会引起恐慌

(1)对于一个已经初始化,但并没有关闭的通道来说,收发操作一定不会引起恐慌。但是通道一旦关闭,还对它进行发送操作,就会引起恐慌。

(2)另外,如果我们试图关闭一个已经关闭了的通道,也会引起恐慌。注意,接收操作是可以感知通道的关闭,并能够安全的退出。

更具体的说,当我们对接收表达式的结果,同时赋给两个变量时,第二个变量就是一个bool类型。它的值如果是false就说明通道已经关闭,并且再没有元素可取。

注意,如果通道关闭时,里面还有元素未取出,那么接收表达式的第一个结果,仍会是通道里的某一个元素值,而第二个结果值一定是True。因此,通过接收表达式的第二个结果值,来判断通道是否关闭是可能有延时的。

由于通道的收发操作的上述特性,所以除非有特殊的保障措施,我们千万不要让接收方关闭通道,而应当让发送方作做件事。

package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan int, 1)
	// 发送方
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Printf("发送方:发送元素:%v\n", i)
			ch1 <- i
		}
		fmt.Println("发送方:关闭通道")
		close(ch1)
	}()

	// 接收方
	for {
		elem, ok := <-ch1
		if !ok {
			fmt.Println("接收方:元素接收完毕!")
			break
		}
		fmt.Printf("接收方:接收到元素%d\n", elem)
	}

	fmt.Println("结束。")
}

六、思考

6.1 通道的长度代表着什么?它在什么时候会与通道的容量相同?

通道的长度代表着整个通道已经存在的元素值len可以查看,类似于切片,而容量则是刚开始设定的值,整个chan可以看成一个已经长度的队列,操作也可以跟队列相类比。

6.2 元素值在经过通道传递时会被复制,那么这个复制是浅表复制还是深层复制呢?

go没有深拷贝,只有浅拷贝,通过值拷贝,而没有所谓的引用拷贝,也是因为这样,整个go语言才能使得效率很高,内存占用少。

Go中没有深层复制,都是浅层复制。数组因为是值类型的,所以即使是浅复制也能完全复制过来。

package main

import (
	"fmt"
)

type Book struct {
	name  string
	price int
}

type Student struct {
	name string
	age  int
	desc []string
	oneB Book
}

func main() {
	ch01 := make(chan Student, 1)
	oneB := Book{name: "b01", price: 2}
	d01 := []string{"a", "b"}
	s01 := Student{name: "aa", age: 12, oneB: oneB, desc: d01}
	ch01 <- s01
	// 修改前的值:{name:aa age:12 desc:[a b] oneB:{name:b01 price:2}}
	fmt.Printf("修改前的值:%+v\n", s01)
	s01.name = "bb"
	s01.oneB.name = "b02"
	d01[1] = "c"
	// 修改后的值:{name:bb age:12 desc:[a c] oneB:{name:b02 price:2}}
	fmt.Printf("修改后的值:%+v\n", s01)
	// 从通道里出来的值:{name:aa age:12 desc:[a c] oneB:{name:b01 price:2}}
	fmt.Printf("从通道里出来的值:%+v\n", <-ch01)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值