在go中假如我起了一个goroutine,在主协程中为了让子协程运行完,可以使用 time.Sleep 的方式,主动的等到子协程运行完之后再退出。

但是假如我不知道这个子协程运行的具体时间,这个时候为了让主协程可以等待协程运行完之后退出可以使用 sync.WaitGroup

sync.WaitGroup 提供了三个方法:

Add(), Done(), Wait()

第一个Add,可以添加需要等待的协程数量,第二个Done可以让等待的协程数量减一,最后一个Wait可以放在main函数中,效果是当等待的协程数量清零之后才会运行后面的内容。例如:

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i ++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(i)
        }(i)
    }
    wg.Wait()
    fmt.Print("全部协程都运行完了呢!")
}

运行之后的结果应该类似于:

0
9
3
2
4
7
6
8
5
1
全部协程都运行完了呢!

在go语言中,interface{}类型可以接收任意值,根据这个特性,可以完成动态类型传参,例如:

func add(a, b interface{}) interface{} {
    switch a.(type) {
    case int:
        ai, _ := a.(int)
        bi, _ := b.(int)
        return ai + bi
    case float64:
        ai, _ := a.(float64)
        bi, _ := b.(float64)
        return ai + bi
    default:
        return nil
    }
}

这个函数完成了一个int类型和float64类型可以同时使用的相加函数

对于一个结构体来说,可以通过同时实现多个接口的方法来实现多个接口,例如:

type Writer interface {
    Write()
}
type Closer interface {
    Close()
}
type WriteCloser struct{}

func (wc *WriteCloser) Write() {
    fmt.Println("write")
}

func (wc *WriteCloser) Close() {
    fmt.Println("close")
}

func main() {
    var It1 Writer = &WriteCloser{}
    It1.Write()
    var It2 Closer = &WriteCloser{}
    It2.Close()
}

运行之后的结果是:

write
close

对于一个接口来说,可以通过接口字段组合实现不同的行为,也叫做接口嵌入。例如:

type Writer interface {
    Write()
}
type WriteCloser struct{
    Writer
}
type writedatabase struct {}
type writefile struct {}
func (wd *writedatabase) Write() {
    fmt.Println("write to database")
}
func (wf *writefile) Write() {
    fmt.Println("write to file")
}
func main() {
    var wc = &WriteCloser {
        &writedatabase{},
    }
    wc.Write()
    var wc2 = &WriteCloser {
        &writefile{},
    }
    wc2.Write()
}

运行之后的结果是:

write to database
write to file

在go中,使用panic来抛出一个错误并将程序停止。

一般有主动panic和被动panic两种情况。

对于主动panic,可以是当一个程序启动时如果需要的依赖程序没有启动就将这个启动停掉,比如没有开etcd或mysql等,例如:

func main() {
    ismysql := false; // 模拟mysql未启动的场景
    if !ismysql {
        panic("mysql 未启动")
    }
}

运行之后会得到如下结果:

panic: mysql 未启动

goroutine 1 [running]:
main.main()
        C:/gostudy/20250704/exe1/exe1.go:6 +0x25
exit status 2

对于被动panic的情况,如果不想要让程序退出可以使用recover来接受这个panic,之后可以选择打印或者做其他处理,并且程序是保持运行的。例如:

var mp map[string]string

func A() {
    defer func() {
        r := recover()
        fmt.Println("捕获到panic", r)
    }()
    mp["heky"] = "heky" // 在不用make或者初值初始化的情况下程序会报panic
}

func main() {
    A()
    fmt.Println("程序继续运行,未退出")
}

运行之后会得到如下结果:

捕获到panic assignment to entry in nil map
程序继续运行,未退出

这里需要注意的是如果要使用recover来捕获panic,recover必须在一个defer后面跟着的匿名函数中,并且这个recover必须在panic之前(因为在panic之后就是不可达的)。

假如说有一个需求就是每次访问一个函数之后都会获得一个变量递增之后的值,并且可以随时将这个变量的值清零。

如果想要做到前半部分的需求,我们可以使用全局变量。例如

var idx int

func getidx() int {
    idx += 1
    return idx
}

func main() {
    for range 5 {
        fmt.Println(getidx())
    }
}

之后想要完成第二个需求也可以通过将idx赋值为0来实现。

除了全局变量之外,还可以使用go函数闭包的特性来实现。例如:

func getidx() func() int {
    idx := 0
    return func() int {
        idx += 1
        return idx
    }
}

func main() {
    id := getidx()
    for range 5 {
        fmt.Println(id())
    }
}

此时假如想要将值清零可以重新调用getidx,获取一个新的函数的句柄,然后调用新获取的函数的句柄。例如:

func main() {
    // ... 省略上面内容
    id2 := getidx()
    for range 3 {
        fmt.Println(id2())
    }
}

运行之后的结果应该如下所示:

1
2
3
4
5
1
2
3

通过上面的例子可以看出和使用全局变量的方式相比,函数闭包的实现可以不依赖于一个专门的变量,这对于后面有多个线程同时需要使用到idx时可以避免发生堵塞和竞争。