go channel && Context
在Go语言中,每一个并发的执行单元叫作一个goroutine;
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。
f() // call f(); wait for it to return go f() // create a new goroutine that calls f(); don't wait
是不是有点像脚本语言里面的&作用

package main import "fmt" import "time" func spinner(delay time.Duration) { for { for _, r := range `-\|/` { fmt.Printf("\r%c", r) time.Sleep(delay) } } } func fib(x int) int { if x < 2 { return x } return fib(x-1) + fib(x-2) } func main() { go spinner(1 * time.Millisecond) const n = 30 fibN := fib(n) // slow fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) } 结果为: - \ | / - \ Fibonacci(30) = 832040
Channels
一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan in
使用内置的make函数,我们可以创建一个channel:
ch := make(chan int) // ch has type 'chan int'
和map类似,channel也对应一个make创建的底层数据结构的引用。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-
运算符。在发送语句中,<-
运算符分割channel和要发送的值。在接收语句中,<-
运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。
ch <- x // a send statement x = <-ch // a receive expression in an assignment statement <-ch // a receive statement; result is discarded
Channel支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。
对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。
不带缓存的Channels
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。当通过一个无缓存Channels发送数据时,接收者收到数据发生在再次唤醒唤醒发送者goroutine之前;在讨论并发编程时,当我们说x事件在y事件之前发生(happens before),我们并不是说x事件在时间上比y时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了。
ch = make(chan int) // unbuffered channel ch = make(chan int, 0) // unbuffered channel
串联的Channels
Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)
func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; ; x++ { naturals <- x } }() // Squarer go func() { for { x := <-naturals squares <- x * x } }() // Printer (in main goroutine) for { fmt.Println(<-squares) } }
上述是一个永久发送逻辑;但是如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现:
当一个channel被关闭后,再向该channel发送数据将导致panic异常。当一个被关闭的channel中已经发送的数据都被成功接收后,后续的接收操作将不再阻塞,它们会立即返回一个零值;但是关闭上面例子中的naturals变量对应的channel并不能终止循环,它依然会收到一个永无休止的零值序列
有没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收。使用这个特性,我们可以修改squarer函数中的循环代码,当naturals对应的channel被关闭并没有值可接收时跳出循环,并且也关闭squares对应的channel.
// Squarer go func() { for { x, ok := <-naturals if !ok { break // channel was closed and drained } squares <- x * x } close(squares) }()
Go语言的range循环可直接在channels上面迭代。使用range循环是上面处理模式的简洁语法,它依次从channel接收数据,当channel被关闭并且没有值可接收时跳出循环。
func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; x < 100; x++ { naturals <- x } close(naturals) }() // Squarer go func() { for x := range naturals { squares <- x * x } close(squares) }() // Printer (in main goroutine) for x := range squares { fmt.Println(x) } }
带缓存的Channels
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的
ch = make(chan string, 3)
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素
ch <- "A" ch <- "B" ch <- "C"
如果有第四个发送操作将发生阻塞;
Channel和goroutine的调度器机制是紧密相连的,如果没有其他goroutine从channel接收,发送者——或许是整个程序——将会面临永远阻塞的风险。如果你只是需要一个简单的队列,使用slice就可以了。
var ch1 chan int // ch1是一个正常的channel,是双向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读int数据
chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:
chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
<-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
send <- 1
//<-send //invalid operation: <-send (receive from send-only type chan<- int)
<-recv
//recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)
//不能将单向 channel 转换为普通 channel
d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
Go channel
channel 是 Go 提供的语言级协程通讯方式,是 Go 推荐的多协程协调通讯方式。
- 可以理解成阻塞消息队列
- channel 是并发安全的,支持多生产多消费
(1)通道初始化方式
ch := make(chan 消息类型)
无缓冲通道ch := make(chan 消息类型, 缓冲大小)
有缓冲通道
(2)通道的读写
- 读
data := <-ch
阻塞读出(等待直到通道有数据)data, ok := <-ch
阻塞读出 ok 表示通道是否关闭- 通道未关闭时
ok = true
data != 零值
- 通道关闭时
ok = false
data = 零值
- 通道未关闭时
- 多路复用
select
- 利用
select
+time.Timer
实现读超时 - 利用
select
+default
实现非阻塞读 - 语法糖:循环读
for data := range ch
通道关闭退出循环 - 读取并丢弃
<- ch
- 写
ch <- data
阻塞写入(直到通道有空间)- 多路复用
select
- 利用
select
+time.Timer
实现写超时 - 利用
select
+default
实现非阻塞写
注意:读写 nil channel
将永远阻塞
(3)单向通道与双向通道
var readOnlyChannel <-chan 消息类型 = ch
只读通道,向只写通道读将抛出编译错误var writeOnlyChannel chan<- 消息类型 = ch
只写通道,向只读通道写将抛出编译错误
(4)关闭通道
close(ch)
只能执行一次,再次关闭将触发panic
- 获取是否关闭
data, ok := <-ch
阻塞读出 ok 表示通道是否关闭- 通道未关闭时
ok = true
data != 零值
- 通道关闭时
ok = false
data = 零值
- 通道未关闭时
- 写入关闭的
channel
将触发panic
- 读取关闭的
channel
立即返回零值 for range ch
将推出循环
(5)无缓冲通道和缓冲通道
- 无缓冲通道
- 创建方式
ch := make(chan 消息类型)
或ch := make(chan 消息类型, 0)
- 特点,对于一个未关闭的通道
- 某协程向该通道发送一个消息将阻塞到,另一个协程取出该消息
- 某协程从该通道读取一个消息将阻塞到,另一个协程发送一个消息
cap
和len
函数返回均为 0
- 创建方式
-
缓冲通道
- 创建方式
ch := make(chan 消息类型, 缓冲大小)
且 缓冲大小 > 0 - 特点,对于一个未关闭的通道
- 某协程向该通道发送一个消息
- 若该通道缓冲区已满,将阻塞到缓冲区有空间为止
- 否则,直接返回
- 某协程从该通道接收一个消息
- 若该通道缓冲区是空的,将阻塞到缓冲区有数据为止
- 否则,返回数据
- 某协程向该通道发送一个消息
-
cap
和len
函数可以获取 缓冲通道的容量和数据长度a3 := make(chan string, 2) a3 <- "" fmt.Println(cap(a3), len(a3))
- 创建方式
(6)实现超时和非阻塞
- 通过
select
和time.Timer
实现 - 通过
select
和default
实现
(7)通道多路复用 select
go channel 类似于阻塞 IO,也存在阻塞,操作系统提供了 IO 多路复用功能,类似的 Go 语言为 channel 提供了多路复用功能
(多路复用:将多个 阻塞 聚合在一个阻塞点)
基本语法
select {
case 读写channel:
操作
case 读写channel:
操作
default: // 可选
操作
}
select 只会执行一次,如果需要多次,一般需要使用 for 循环包裹
for {
select {
// ...
}
}
如果 select
包含 default
分支,则实现非阻塞效果,即,当其他 case
分支没有数据时,执行 default
分支
select {
case ad := <- a:
fmt.Println(ad)
default:
fmt.Println("default")
}
如果 select
中同时有多个 channel 就绪,则只会处理最上面就绪的那一个(可以理解成 每个 case
都加了 break
)
func selectMultiChannel(a <-chan string, b <-chan string){
for i := 0; i< 10; i++ {
select {
case ad := <- a:
fmt.Println(i, ad)
case bd := <- b:
fmt.Println(i, bd)
}
}
}
func Chanvar() {
a := make(chan string, 1)
b := make(chan string, 1)
b <- "b"
a <- "a"
go selectMultiChannel(a, b)
// 输出
// 0 a
// 1 b
time.Sleep(1000 * time.Microsecond)
}
利用 time.Timer
可以实现等待超时效果
func channelTimeout(a <-chan string) {
select {
case ad := <- a:
fmt.Println(ad)
case <- time.After(500 * time.Millisecond):
fmt.Println("Timeout")
}
}
func Chanvar() {
a2 := make(chan string, 1)
go channelTimeout(a2)
time.Sleep(600 * time.Millisecond)
// 输出 Timeout
a2 <- "message a"
time.Sleep(100 * time.Millisecond)
}
select 还允许 多路复用 写入消息
func selectWriteChannel(a chan<- string) {
select {
case a <- "从 select中写入 a":
fmt.Println("写入 a 成功")
default:
fmt.Println("Default 写入 a 失败")
}
}
a3 := make(chan string, 2)
go selectWriteChannel(a3)
time.Sleep(100 *time.Millisecond)
fmt.Println(cap(a3), len(a3))
go selectWriteChannel(a3)
time.Sleep(100 *time.Millisecond)
fmt.Println(cap(a3), len(a3))
go selectWriteChannel(a3)
time.Sleep(100 *time.Millisecond)
fmt.Println(cap(a3), len(a3))
// 输出
// 写入 a 成功
// 2 1
// 写入 a 成功
// 2 2
// Default 写入 a 失败
// 2 2
(8)实现原理
- 对于有缓冲的 Channel,数据队列是通过数组实现的循环队列,内存预分配 (
mallocgc
分配内存)
3、标准库提供并发工具
主要位于 sync
和 atomic
包
4、Context 控制协程
context.Context
是 Go 中用来控制协程树的接口,具有如下功能
- 使用
withXxx(parent)
系列函数可以构造一颗Context
树 - 在对某个
Context
调用cancel
函数,则可以结束当前Context
及子孙Context
- 可以在创建
Context
时,为该Context
绑定一对 KV (少用),所有子孙Context
可以通过 Key 查询自己及祖宗节点的 Value - 标准库提供,存在多种单一功能的 Context,通过
withXxx(parent)
创建- 具有超时或者定时取消功能的
Context
- 绑定一对 KV
- 具有超时或者定时取消功能的
context.Context
相关API
- 根 Context
context.Background()
是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来context.TODO()
应该只在不确定应该使用哪种上下文时使用
- 传递一个 父 Context 创建一个新的
Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
创建一个可取消的Context
调用cancel
返回值即可取消子孙 Contextfunc WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
创建一个可取消的Context
,调用cancel
返回值即可取消子孙 Context,同时在 deadline 时刻自动取消func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
创建一个可取消的Context
,调用cancel
返回值即可取消子孙 Context,同时在 timeout 后自动取消func WithValue(parent Context, key, val interface{}) Context
创建一个Context
附加一个值
context.Context
接口函数语义Deadline() (deadline time.Time, ok bool)
获取设置的截止时间(如果当前 Context 和 祖宗 Context 设置了)。- 如果设置了截止时间,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值
ok == true
- 如果没有设置截止时间,返回零值(
ok == false
)
- 如果设置了截止时间,第一个返回值是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值
Done() <-chan struct{}
获取一个只读 Channel,调用多次返回一样的值,如果当前 Context 或 祖宗 Context 取消了,则该 Channel 将被关闭,直接返回(结合 select 即可实现协程树控制)Err() error
返回当前 Context 或 祖宗 Context 是否取消- 未取消:返回 nil
- 到达Deadline而取消:返回
context deadline exceeded
- 手动调用 Cancel:返回
context canceled
Value(key interface{}) interface{}
当前 Context 或 祖宗 Context 上绑定的值
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2019-07-04 tcp 输入 prequeue以及backlog队列