假如有一个方法一直在打印提示信息,现在想要在main方法中经过一段的时间主动的去停止,这个时候我们可以选择使用共享变量的方式(注意加读写锁),也可以使用channel的方式。例如:

共享变量:

var stop bool
var rwlock sync.RWMutex
var wg sync.WaitGroup
func g1() {
    for {
        rwlock.RLock()
        if stop {
            fmt.Println("退出了捏")
            wg.Done()
            return
        }
        time.Sleep(time.Second)
        fmt.Println("欸嘿!")
        rwlock.RUnlock()
    }
}

func main() {
    wg.Add(1)
    go g1()
    
    time.Sleep(3*time.Second)
    rwlock.Lock()
    stop = true
    rwlock.Unlock()
    wg.Wait()
    fmt.Println("退出程序")
}

运行结果:

欸嘿!
欸嘿!
欸嘿!
退出了捏
退出程序

使用channel:

var wg sync.WaitGroup

func g1(stop chan struct{}) {
    for {
        select {
        case <- stop:
            fmt.Println("退出了捏")
            wg.Done()
            return
        default:
            time.Sleep(time.Second)
            fmt.Println("欸嘿!")
        }
    }
}

func main() {
    stop := make(chan struct{})
    wg.Add(1)
    go g1(stop)
    time.Sleep(3*time.Second)
    stop <- struct{}{}
    wg.Wait()
    fmt.Println("退出程序")
}
因为channel是协程安全的所以不用加锁

运行结果:

欸嘿!
欸嘿!
欸嘿!
退出了捏
退出程序

除此之外,还有一种更常用的方式,就是使用context,例如:

var wg sync.WaitGroup

func g1(ctx context.Context) {
    for {
        select {
        case <- ctx.Done():
            fmt.Println("退出了捏")
            wg.Done()
            return
        default:
            time.Sleep(time.Second)
            fmt.Println("欸嘿!")
        }
    }
}

func main() {
    wg.Add(1)
    ctx, cancel := context.WithCancel(context.Background())
    go g1(ctx)
    time.Sleep(3*time.Second)
    cancel()
    wg.Wait()
    fmt.Println("退出程序")
}

运行结果:

欸嘿!
欸嘿!
欸嘿!
退出了捏
退出程序
WithCancel() 方法返回值是一个 context.Context 类型的interface和一个cancel方法
ctx(context.Context)有一个Done方法,返回值是一个channel
当使用cancel方法之后,ctx会向Done()返回的channel中发送一个信号
此时接受到信号,select跳转到退出的分支,由此完成对goroutine的信息传递实现主动退出

假如现在有两个方法,分别有对应的channel,每当他们运行完成之后会向channel中发送一个信号。现在想要知道是哪一个方法先完成,可以如何做?

此时可以使用select方法,来对channel进行监控。

select的语法和switch很像,例如:

func g1(ch chan struct{}) {
    time.Sleep(time.Second)
    ch <- struct{}{}
}

func g2(ch chan struct{}) {
    time.Sleep(2*time.Second)
    ch <- struct{}{}
}

func main() {
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})

    go g1(ch1)
    go g2(ch2)
    select {
    case <-ch1:
        fmt.Println("g1 done")
    case <-ch2:
        fmt.Println("g2 done")
    }
}

运行结果为:

g1 done

假如我现在将这个select放到一个for循环中,不断地监控两个channel,并且想要当超过一定的时间之后就自动退出,也就是设定超时时间,这个时候可以使用 time.NewTimer() 这个方法接收一个时间做为参数,并返回一个timer对象,这个对象有一个成员 C 是一个channel,当时间到达时timer会往这个timer中发送一个信号。我们可以根据这个原理来完成超时时间的设置比如:

func g1(ch chan struct{}) {
    time.Sleep(time.Second)
    ch <- struct{}{}
}

func g2(ch chan struct{}) {
    time.Sleep(2*time.Second)
    ch <- struct{}{}
}

func main() {
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})
    timer := time.NewTimer(4*time.Second)

    go g1(ch1)
    go g2(ch2)
    for {
        select {
        case <-ch1:
            fmt.Println("g1 done")
        case <-ch2:
            fmt.Println("g2 done")
        case <-timer.C:
            fmt.Println("time out!")
            return
        }
    }
}

运行结果如下:

g1 done
g2 done
time out!

同时和switch一样,select也支持default方法,当没有读取到值时就运行default。

关于select还有两点需要注意的:

  1. select时某一个分支就绪了(获取到了值)就执行哪一个分支
  2. 如果多个分支同时就绪,是随机执行,目的是为了防止一直读取某一个分支的值,从而造成”饥饿“现象

channel如果直接定义一般都是双向的,即我们既可以往里面写值,也可以从里面读值。

但是有的时候我们希望某一个方法里面只能是读值,不能写值、或者是只能够写值,不能够读值,此时就需要用到单项channel。

单向channel的定义和双向channel差不多,只是多了一个 <- 来标识是单向写入还是单向取出

var ch1 chan<- int // 写入
var ch2 <-chan int // 取出

对于初始化可以通过将一个双向channel赋值给单向channel来完成初始化,go会在内部自动完成类型的转换。

var ch1 chan<- int // 写入
var ch2 <-chan int // 取出

ch := make(chan int, 1)
ch1 = ch
ch2 = ch

例如:

var wg sync.WaitGroup
func producer(out chan<- int) {
    defer wg.Done()
    for i:= 0; i < 10; i ++ {
        out <- i
    }
    close(out)
}

func receiver(in <-chan int) {
    defer wg.Done()
    for num := range in {
        fmt.Println(num)
    }
}

func main() {
    wg.Add(2)
    c := make(chan int)
    go producer(c)
    go receiver(c)
    wg.Wait()
}

运行结果如下:

0
1
2
3
4
5
6
7
8
9

对于channel来说可以使用for range语法来不断地读值,但是如果当全部值已经读取完毕之后,for range就会阻塞。这个时候我们可以使用close(),来手动的关闭channel,此时for range就会退出了。

例如:

func main() {
    var msg chan int
    var wg sync.WaitGroup
    wg.Add(1)
    msg = make(chan int, 3)
    go func(msg chan int) {
        defer wg.Done()
        for data := range msg {
            fmt.Println(data)
        }
        fmt.Println("所有消息都读完啦!")
    }(msg)
    for i := 0; i < 3; i ++ {
        msg <- i;
    }
    close(msg)
    wg.Wait()
}

运行结果如下:

0
1
2
所有消息都读完啦!
有一个需要注意的地方,对于一个close了的channel来说可以继续读值,但是不能向里写值

在go语言中,两个协程之间进行通信,使用的是一种消息队列的形式。消息的生产者将消息放入消息队列,然后消费者从消息队列中获取消息。

go提供了一个数据类型和语法糖来完成这种操作 channel<-

例如:

func main() {
    var msg chan string
    msg = make(chan string, 1)
    msg <- "哈喽,哈喽!"
    data := <- msg
    fmt.Println(data)
}
channel的使用需要初始化,并且需要显性的指明缓冲区大小
chan 后面跟着的是channel中放的数据类型,这个同样需要显性的指明

运行结果应该是:

哈喽,哈喽!

当缓冲区大小为0时,不能直接的在一个协程中写入和读取

例如:

func main() {
    var msg chan string
    msg = make(chan string, 0) // 缓冲区大小设为0
    msg <- "哈喽,哈喽!"
    data := <- msg
    fmt.Println(data)
}

运行之后会报死锁:

fatal error: all goroutines are asleep - deadlock!

此时,由于go语言有一种happen-before的机制,让我们可以使用一个另外的goroutine来读取,例如:

func main() {
    var msg chan string
    var wg sync.WaitGroup
    wg.Add(1)
    msg = make(chan string, 0) // 缓冲区大小设为0
    go func(msg chan string) {
        defer wg.Done()
        data := <- msg
        fmt.Println(data)
    }(msg)
    msg <- "哈喽,哈喽!"
    wg.Wait()
}

运行结果为:

哈喽,哈喽!