【Golang】空结构体、通道与context——从控制goroutine说起(一)
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)
}
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-10-27-go-channel-close-empty-struct-1/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。