golang里面,通过go关键字开启一个goroutine后,我们如何在外部能够控制goroutine的运行流转呢?

1.先看一个例子

package main

import (
	"fmt"
	"sync"

	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
  go func(){
    for {
      fmt.Println("goroutine...")
      time.Sleep(time.Second)
		}
    wg.Done()
  }()
	wg.Wait()
	fmt.Println("exit")
}

明显可以看出wg.Done()是不可达的unreachable code,且一直会输出goroutine...。我们首当其冲要解决的问题,就是使其可达。很简单,sleep后,break强制退出死循环就可以了。

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go func() {
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			break
		}
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("exit")
}

2.全局变量

没错,确实强制退出for循环就可以解决首要问题,但是在实际开发中,一个死循环绝对不是简简单单的进入循环内部,执行一次,然后break,否则干嘛要使用死循环呢?**更通用的需求:由开发者设计,自由控制。**一种情况是在goroutine内部控制,比如输出4次后退出:

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go func() {
        //计数器
		count := 0
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			count++
			if count > 3 {
				break
			}
		}
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("exit")
}

另外一种情况,就是在外部能够控制goroutine,让goroutine接收通知后,再退出,我们很容易就能想到利用全局变量作为标识:

var wg sync.WaitGroup
var exit bool

func main() {
	wg.Add(1)
	go func() {
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			if exit {
				break
			}
		}
		wg.Done()
	}()
	time.Sleep(time.Second * 3) 
	exit = true                
	wg.Wait()
	fmt.Println("exit")
}

全局变量可以实现,但是存在一些问题:

  • 跨包调用时不容易统一,比如在多个地方都使用了这个全局变量,还会出现竞态问题。
  • goroutine中再启动goroutine,就不好办了。

3.通道

var wg sync.WaitGroup
var exit bool

func main() {
	wg.Add(1)
	var exitChan = make(chan struct{})
	go func(exitChan chan struct{}) {
	LOOP:
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			select {
			case <-exitChan: 
				break LOOP
			default:
			}
		}
		wg.Done()
	}(exitChan)
	time.Sleep(time.Second * 3) 
	exitChan <- struct{}{}      
	close(exitChan)
	wg.Wait()
	fmt.Println("exit")
}

以上代码有几个重点:

  • 通道
  • select等待通道有值并接收
  • 通过通道给goroutine发送退出信号

4.空结构体

可以看到上面是使用的空结构体struct{}类型的通道。这是为什么?用其他类型行不行?

经过查阅资料:涉及到在 Go 语言中 ”宽度“ 的概念,宽度描述了一个类型的实例所占用的存储空间的字节数。宽度是一个类型的属性。在 Go 语言中的每个值都有一个类型,值的宽度由其类型定义,并且总是 8 bits 的倍数。在 Go 语言中我们可以借助 unsafe.Sizeof 方法,来获取类型对应的宽度。各种类型都至少占有一定的宽度,我们也经常说任何类型声明后,都分配了空间。唯独空结构体比较特殊:宽度为0

空结构体这一特点完美契合在上面这种场景下使用:占位符,还不占空间,满足基本输入输出就好。

至于为什么只有空结构会有这种特殊待遇,其他类型又不行?这都是编译器的功劳:

// base address for all 0-byte allocations
var zerobase uintptr

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
 ...
 if size == 0 {
  return unsafe.Pointer(&zerobase)
 }
}

当发现 size 为 0 时,会直接返回变量 zerobase 的引用,该变量是所有 0 字节的基准地址,不占据任何宽度。我们可以在大量的包中看到利用空结构体的特点,从而达到占位符的作用。

5.关闭的通道

exitChan <- struct{}{}      
close(exitChan)

上面的代码是先发送了后,再关闭的通道。但是我们去看第三方包时是只用close(exitChan)关闭的操作,并没有发送,代码如下:

var wg sync.WaitGroup
var exit bool

func main() {
	wg.Add(1)
	var exitChan = make(chan struct{})
	go func(exitChan chan struct{}) {
	LOOP:
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			select {
			case <-exitChan:
				break LOOP
			default:
			}
		}
		wg.Done()
	}(exitChan)
	time.Sleep(time.Second * 3) 
	// exitChan <- struct{}{}      
	close(exitChan)
	wg.Wait()
	fmt.Println("exit")
}

也达到了我们预想的结果,这是为什么呢?

当通道被关闭时,再往该通道发送值会引发panic,但是再从该通道取值的操作:会先取完通道中的值,然后取到的值就一直都是对应类型的零值。所以这条case一定会通过的;有时候多问几个为什么?或许有机会继续夯实我们的基础。好了,再多问一个问题:如何判断一个通道是否被关闭?

// 方法一:尝试取值
v,ok:=<-ch
if !ok {
    fmt.Println("通道已关闭")
}

// 方法二 更常用 for-range
for i := range ch2 { // 通道关闭后会退出for range循环
	fmt.Println(i)
}