一、协程

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

WaitGroupGo 语言中的一个同步工具,用于等待一组协程或 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

ChannelGo 语言中用于协程间通信和同步的重要机制。通过使用 Channel,我们可以在协程之间进行数据传递和协调工作。

常见的Channel报错:

  1. send on closed channel: 当向一个已经关闭的channel发送数据时会报错。

  2. receive on closed channel: 当从一个已经关闭的channel接收数据时会报错。

  3. deadlock: 当goroutine在发送或接收数据时发生阻塞,而没有其他goroutine来处理这个channel时会导致死锁。

  4. invalid memory address or nil pointer dereference: 当向一个nil的channel发送或接收数据时会导致空指针异常。

  5. panic: send on nil channel: 当向一个nil的channel发送数据时会导致panic。

  6. 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,分别向c1c2发送数据。然后,使用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语句中,等待超时计时器或协程的结束。如果超时计时器触发,将输出"超时"。如果协程在超时时间内完成,将输出"协程正常结束"。

通过这种方式,可以在协程中处理超时情况,避免协程无限期阻塞。根据具体的需求,可以调整超时时间和协程的逻辑。