一、协程
Go 的协程是一种轻量级的线程,它可以在相同的地址空间中并发执行多个任务。协程通过关键字 go 来创建,它可以在函数调用之前添加,从而在并发上下文中执行该函数。
协程之间的切换是由 Go运行时自动管理的,不需要显式的上下文切换操作。这使得协程非常高效,因为它们可以在很小的开销下进行并发执行。
Go的协程还支持通过通道进行通信,通道是一种用于在协程之间传递数据的机制。通过通道,协程可以安全地进行数据交换和同步。
以下是一个简单的协程案例
package main
import (
"fmt"
"time"
)
func sing() {
fmt.Println("唱歌")
time.Sleep(1 * time.Second)
fmt.Println("唱歌结束")
}
func main() {
go sing()
go sing()
go sing()
go sing()
time.Sleep(2 * time.Second)
}
主线程结束协程自动结束,主线程不会等待协程的结束
当我们去掉 time.Sleep(2 * time.Second) 后,你会发现程序没有任何输出就结束了。
二、WaitGroup
WaitGroup 是 Go 语言中的一个同步工具,用于等待一组协程或 goroutine 完成。它提供了一种方便的方式来确保所有的协程都执行完毕,避免程序过早退出。
使用 WaitGroup,可以在每个协程结束时调用 Add 方法增加等待计数,在主协程中调用 Wait 方法等待所有协程完成。
以下是一个使用 WaitGroup 的示例:
package main
import (
"fmt"
"sync"
"time"
)
func sing(wg *sync.WaitGroup) {
// 增加等待计数
wg.Add(1)
fmt.Println("唱歌")
time.Sleep(1 * time.Second)
fmt.Println("唱歌结束")
// 减少等待计数
wg.Done()
}
func main() {
var wg sync.WaitGroup
// 启动多个协程
for i := 0; i < 4; i++ {
go sing(&wg)
}
// 等待所有协程完成
wg.Wait()
fmt.Println("所有协程都结束了")
}
在这个示例中,我们创建了一个 WaitGroup 对象 wg,并在每个协程中调用 Add 方法增加等待计数。在协程结束时,调用 Done 方法减少等待计数。最后,在主协程中调用 Wait 方法等待所有协程完成。
通过使用 WaitGroup,我们可以确保程序在所有协程都结束后再继续执行后续操作,避免了主线程提前结束的问题。这样可以更好地控制程序的执行顺序和并发性。
三、Channel
Channel是 Go 语言中用于协程间通信和同步的重要机制。通过使用 Channel,我们可以在协程之间进行数据传递和协调工作。
常见的Channel报错:
send on closed channel: 当向一个已经关闭的channel发送数据时会报错。receive on closed channel: 当从一个已经关闭的channel接收数据时会报错。deadlock: 当goroutine在发送或接收数据时发生阻塞,而没有其他goroutine来处理这个channel时会导致死锁。invalid memory address or nil pointer dereference: 当向一个nil的channel发送或接收数据时会导致空指针异常。panic: send on nil channel: 当向一个nil的channel发送数据时会导致panic。panic: receive on nil channel: 当从一个nil的channel接收数据时会导致panic。
以下是一个使用 Channel的示例:
package main
import "fmt"
func main() {
var c chan int // 声明一个传递整形的通道
// 初始化通道
c = make(chan int, 1) // 初始化一个 有一个缓冲位的通道
c <- 1
//c <- 2 // 会报错 deadlock
fmt.Println(<-c) // 取值
//fmt.Println(<-c) // 再取也会报错 deadlock
c <- 2
n, ok := <-c
fmt.Println(n, ok)
close(c) // 关闭协程
c <- 3 // 关闭之后就不能再写或读了 send on closed channel
fmt.Println(c)
}
在同步模式下,channel没有任何意义,需要在异步模式下使用channel,在协程函数里面写,在主线程里面接收数据
package main
import (
"fmt"
"sync"
"time"
)
var moneyChan = make(chan int) // 声明并初始化一个长度为0的信道
func pay(name string, money int, wait *sync.WaitGroup) {
fmt.Printf("%s 开始购物\n", name)
time.Sleep(1 * time.Second)
fmt.Printf("%s 购物结束\n", name)
moneyChan <- money
wait.Done()
}
// 协程
func main() {
var wait sync.WaitGroup
startTime := time.Now()
wait.Add(3)
// 主线程结束,协程函数跟着结束
for i := 0; i < 3; i++ {
go pay(fmt.Sprintf("用户%d", i), 100, &wait)
}
go func() {
defer close(moneyChan)
// 在协程函数里面等待上面三个协程函数结束
wait.Wait()
}()
for {
money, ok := <-moneyChan
fmt.Println(money, ok)
if !ok { // 如果不break,会一直循环
break
}
}
fmt.Println("购买完成", time.Since(startTime))
}
四、select
select语句用于在多个通信操作(如channel操作)之间进行选择。它会阻塞执行,直到其中一个操作可以继续执行。以下是一个使用select的示例:
package main
import "fmt"
func main() {
// 创建两个 channel
c1 := make(chan int)
c2 := make(chan string)
// 发送数据到 channel
go func() {
c1 <- 1
}()
go func() {
c2 <- "hello"
}()
// 使用 select 等待数据
select {
case m := <-c1:
fmt.Println("Received from c1:", m)
case s := <-c2:
fmt.Println("Received from c2:", s)
}
}
在上述示例中,使用go关键字启动了两个 goroutine,分别向c1和c2发送数据。然后,使用select语句等待数据的到来。select会阻塞执行,直到其中一个case可以执行。如果多个case都可以执行,select会随机选择一个执行。在这个例子中,可能会输出"Received from c1:"或"Received from c2:",具体取决于哪个goroutine首先完成发送数据。
五、协程超时处理
当使用协程时,有时可能需要设置超时时间,以避免协程无限期阻塞。Go 语言提供了time.Duration类型来表示时间间隔,并可以使用time.After函数来设置超时时间。
以下是一个示例,展示了如何在协程中处理超时:
package main
import (
"fmt"
"time"
)
func main() {
// 定义超时时间
timeout := time.Second * 5
// 启动协程
go func() {
// 等待一段时间
time.Sleep(timeout + time.Second)
// 协程执行的代码
fmt.Println("协程执行完毕")
}()
// 设置超时计时器
timer := time.After(timeout)
// 等待协程或超时
select {
case <-timer:
fmt.Println("超时")
case <-make(chan struct{}):
fmt.Println("协程正常结束")
}
}
在上述示例中,定义了一个5秒的超时时间。然后,启动了一个协程,在协程中使用time.Sleep函数模拟耗时操作。同时,设置了一个超时计时器,使用time.After函数指定超时时间。
在select语句中,等待超时计时器或协程的结束。如果超时计时器触发,将输出"超时"。如果协程在超时时间内完成,将输出"协程正常结束"。
通过这种方式,可以在协程中处理超时情况,避免协程无限期阻塞。根据具体的需求,可以调整超时时间和协程的逻辑。