主頁(yè) > 知識(shí)庫(kù) > Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解

Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解

熱門標(biāo)簽:400手機(jī)電話免費(fèi)辦理 上海企業(yè)外呼系統(tǒng)排名 開通400電話申請(qǐng)流程 揚(yáng)州電銷外呼系統(tǒng)軟件 如何利用高德地圖標(biāo)注家 百度地圖標(biāo)注位置網(wǎng)站 電腦外呼系統(tǒng)輻射大嗎 智能語(yǔ)音電銷的機(jī)器人 武漢百應(yīng)人工智能電銷機(jī)器人

WaitGroup 用于線程同步,很多場(chǎng)景下為了提高并發(fā)需要開多個(gè)協(xié)程執(zhí)行,但是又需要等待多個(gè)協(xié)程的結(jié)果都返回的情況下才進(jìn)行后續(xù)邏輯處理,這種情況下可以通過(guò) WaitGroup 提供的方法阻塞主線程的執(zhí)行,直到所有的 goroutine 執(zhí)行完成。
本文目錄結(jié)構(gòu):

WaitGroup 不能被值拷貝
Add 需要在 Wait 之前調(diào)用
使用 channel 實(shí)現(xiàn) WaitGroup 的功能
Add 和 Done 數(shù)量問(wèn)題
WaitGroup 和 channel 控制并發(fā)數(shù)
WaitGroup 和 channel 實(shí)現(xiàn)提前退出
WaitGroup 和 channel 返回錯(cuò)誤
使用 ErrGroup 返回錯(cuò)誤
使用 ErrGroup 實(shí)現(xiàn)提前退出
改善版的 Errgroup

WaitGroup 不能被值拷貝

wg 作為一個(gè)參數(shù)傳遞的時(shí)候,我們?cè)诤瘮?shù)中操作的時(shí)候還是操作的一個(gè)拷貝的變量,對(duì)于原來(lái)的 wg 是不會(huì)改變。
這一點(diǎn)可以從 WaitGroup 實(shí)現(xiàn)的源碼定義的 struct 能能看出來(lái),WaitGroup 的 struct 就兩個(gè)字段,第一個(gè)字段就是 noCopy,表明這個(gè)結(jié)構(gòu)體是不希望直接被復(fù)制的。noCopy 是的實(shí)現(xiàn)是一個(gè)空的 struct{},主要的作用是嵌入到結(jié)構(gòu)體中作為輔助 vet 工具檢查是否通過(guò) copy 賦值這個(gè) WaitGroup 實(shí)例,如果有值拷貝的情況,會(huì)被檢測(cè)出來(lái),我們一般的 lint 工具也都能檢測(cè)出來(lái)。
在某些情況下,如果 WaitGroup 需要作為參數(shù)傳遞到其他的方法中,一定需要使用指針類型進(jìn)行傳遞。

type WaitGroup struct {
    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state, and the other 4 as storage
    // for the sema.
    state1 [3]uint32
}

可以用以下一個(gè)例子來(lái)說(shuō)明:

// 錯(cuò)誤的用法,函數(shù)傳遞 wg 是值拷貝
func main() {
    wg := sync.WaitGroup{}

    wg.Add(10)

    for i := 0; i  10; i++ {
        go func(i int) {
            do(i, wg)
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

func do(i int, wg sync.WaitGroup) { // wg 值拷貝,會(huì)導(dǎo)致程序
    fmt.Println(i)
    wg.Done()
}

// 正確的用法,waitgroup 參數(shù)傳遞使用指針的形式
func main() {
    wg := sync.WaitGroup{}

    wg.Add(10)

    for i := 0; i  10; i++ {
        go func(i int) {
            do(i, wg)
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

func do(i int, wg *sync.WaitGroup) {
    fmt.Println(i)
    wg.Done()
}

Add 需要在 Wait 之前調(diào)用

WaitGroup 結(jié)構(gòu)體提供了三個(gè)方法,Add、Done、Wait,Add 的作用是用來(lái)設(shè)置WaitGroup的計(jì)數(shù)值(子goroutine的數(shù)量);Done的作用用來(lái)將 WaitGroup 的計(jì)數(shù)值減 1,其實(shí)就是調(diào)用Add(-1);Wait 的作用是檢測(cè) WaitGroup 計(jì)數(shù)器的值是否為 0,如果為 0 表示所有的 goroutine 都運(yùn)行完成,否則會(huì)阻塞等待計(jì)數(shù)器的值為0(所有的 groutine都執(zhí)行完成)之后才運(yùn)行后面的代碼。
所以在 WaitGroup 調(diào)用的時(shí)候一定要保障 Add 函數(shù)在 Wait 函數(shù)之前執(zhí)行,否則可能會(huì)導(dǎo)致 Wait 方法沒(méi)有等到所有的結(jié)果運(yùn)行完成而被執(zhí)行完。也就是我們不能在 Grountine 中來(lái)執(zhí)行 Add 和 Done,這樣可能當(dāng)前 Grountine 來(lái)不及運(yùn)行,外層的 Wait 函數(shù)檢測(cè)到滿足條件然后退出了。

func main() {
    wg := sync.WaitGroup{}
    wg.Wait() // 直接調(diào)用 Wait() 方法是不會(huì)阻塞的,因?yàn)?wg 中 goroutine 計(jì)數(shù)器的值為 0
    fmt.Println("success")
}
// 錯(cuò)誤的寫法,在 goroutine 中進(jìn)行 Add(1) 操作。
// 可能在這些 goroutine 還沒(méi)來(lái)得及 Add(1) 就已經(jīng)執(zhí)行 Wait 操作了
func main() {
    wg := sync.WaitGroup{}

    for i := 0; i  10; i++ {
        go func(i int) {
            wg.Add(1)
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

// 打印的結(jié)果,不是我們預(yù)期的打印 10 個(gè)元素之后再打印 success,而是會(huì)隨機(jī)打印其中的一部分
success
1
0
5
2

// 正確的寫法一
func main() {
    wg := sync.WaitGroup{}
    wg.Add(10) // 在 groutine 外層先把需要運(yùn)行的 goroutine 的數(shù)量設(shè)置好,保障比 Wait 函數(shù)先執(zhí)行

    for i := 0; i  10; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

// 正確的寫法二
func main() {
    wg := sync.WaitGroup{}

    for i := 0; i  10; i++ {
        wg.Add(1) // 保障比 Wait 函數(shù)先執(zhí)行
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

使用 channel 實(shí)現(xiàn) WaitGroup 的功能

如果想要實(shí)現(xiàn)主線程中等待多個(gè)協(xié)程的結(jié)果都返回的情況下才進(jìn)行后續(xù)調(diào)用,也可以通過(guò)帶緩存區(qū)的 channel 來(lái)實(shí)現(xiàn),實(shí)現(xiàn)的思路是需要先知道等待 groutine 的運(yùn)行的數(shù)量,然后初始化一個(gè)相同緩存區(qū)數(shù)量的 channel,在 groutine 運(yùn)行結(jié)束之后往 channel 中放入一個(gè)值,并在主線程中阻塞監(jiān)聽獲取 channel 中的值全部返回。

func main() {
    numGroutine := 10
    ch := make(chan struct{}, numGroutine)

    for i := 0; i  numGroutine; i++ {
        go func(i int) {
            fmt.Println(i)
            ch - struct{}{}
        }(i)
    }

    for i := 0; i  numGroutine; i++ {
        -ch
    }

    fmt.Println("success")
}

// 打印結(jié)果:
7
5
3
1
9
0
4
2
6
8
success

Add 和 Done 數(shù)量問(wèn)題

需要保障 Add 的數(shù)量和 Done 的數(shù)量一致,如果 Add 數(shù)量小于 Done 數(shù)量的情況下,調(diào)用 Wait 方法會(huì)檢測(cè)到計(jì)數(shù)器的值為負(fù)數(shù),程序會(huì)報(bào) panic;如果 Add 數(shù)量大于 Done 的數(shù)量,會(huì)導(dǎo)致 Wait 循環(huán)阻塞后面的代碼得不到執(zhí)行。
Add 數(shù)量小于 Done 數(shù)量:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(1) // Add 數(shù)量小于 Done 數(shù)量

    for i := 0; i  10; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

// 運(yùn)行結(jié)果,有兩種結(jié)果
結(jié)果一:打印部分輸出然后退出,這種情況是因?yàn)?Done 執(zhí)行了一個(gè)只會(huì),Wait 檢測(cè)到剛好滿足條件然后退出了
1
success
9
5

結(jié)果二:執(zhí)行 Wait 函數(shù)的時(shí)候,計(jì)數(shù)器的值已經(jīng)是負(fù)數(shù)了
0
9
3
panic: sync: negative WaitGroup counter

Add 數(shù)量大于 Done 數(shù)量:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(20)

    for i := 0; i  10; i++ {
        go func(i int) {
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

// 執(zhí)行結(jié)果:deadlock
0
9
3
7
8
1
4
2
6
5
fatal error: all goroutines are asleep - deadlock!

WaitGroup 和 channel 控制并發(fā)數(shù)

使用 waitgroup 可以控制一組 groutine 同時(shí)運(yùn)行并等待結(jié)果返回之后再進(jìn)行后續(xù)操作,雖然 groutine 對(duì)資源消耗比較小,但是大量的 groutine 并發(fā)對(duì)系統(tǒng)的壓力還是比較大,所以這種情況如果需要控制 waitgroup 中 groutine 并發(fā)數(shù)量控制,就可以使用緩存的 channel 控制同時(shí)并發(fā)的 groutine 數(shù)量。

func main() {
    wg := sync.WaitGroup{}
    wg.Add(200)

    ch := make(chan struct{}, 10) // 控制最大并發(fā)數(shù)是 10
 
    for i := 0; i  200; i++ {
        ch - struct{}{}
        go func(i int) {
            fmt.Println(i)
            wg.Done()
            -ch
        }(i)
    }

    wg.Wait()
    fmt.Println("success")
}

根據(jù)使用 channel 實(shí)現(xiàn) WaitGroup 的功能的思路,我們上面的代碼也可以通過(guò)兩個(gè) channel 進(jìn)行改造來(lái)實(shí)現(xiàn)。

func main() {
    numGroutine := 200 // 運(yùn)行的 groutine 總數(shù)量
    numParallel := 10  // 并發(fā)的 groutine 數(shù)量

    chTotal := make(chan struct{}, numGroutine)
    chParallel := make(chan struct{}, numParallel)

    for i := 0; i  200; i++ {
        chTotal - struct{}{}
        go func(i int) {
            fmt.Println(i)
            -chTotal
            chParallel - struct{}{}
        }(i)
    }

    for i := 0; i  numGroutine; i++ {
        -chParallel
    }
    fmt.Println("success")
}

WaitGroup 和 channel 實(shí)現(xiàn)提前退出

用 WaitGroup 協(xié)調(diào)一組并發(fā) goroutine 的做法很常見(jiàn),但 WaitGroup 本身也有其不足:
WaitGroup 必須要等待控制的一組 goroutine 全部返回結(jié)果之后才往下運(yùn)行,但是有的情況下我們希望能快速失敗,也就是這一組 goroutine 中只要有一個(gè)失敗了,那么就不應(yīng)該等到所有 goroutine 結(jié)束再結(jié)束任務(wù),而是提前結(jié)束以避免資源浪費(fèi),這個(gè)時(shí)候就可以使用 channel 配合 WaitGroup 實(shí)現(xiàn)提前退出的效果。

func main() {
    wg := sync.WaitGroup{}
    wg.Add(10)

    ch := make(chan struct{}) // 使用一個(gè) channel 傳遞退出信號(hào)

    for i := 0; i  10; i++ {
        go func(i int) {
            time.Sleep(time.Duration(i) * time.Second)
            fmt.Println(i)
            if i == 2 { // 檢測(cè)到 i==2 則提前退出
                ch - struct{}{}
            }
            wg.Done()
        }(i)
    }

    go func() {
        wg.Wait()        // wg.Wait 執(zhí)行之后表示所有的 groutine 都已經(jīng)執(zhí)行完成了,而且沒(méi)有 groutine 往 ch 傳遞退出信號(hào)
        ch - struct{}{} // 需要傳遞一個(gè)信號(hào),不然主線程會(huì)一直阻塞
    }()

    -ch // 阻塞等待收到退出信號(hào)之后往下執(zhí)行

    fmt.Println("success")
}

// 打印結(jié)果
0
1
2
success

WaitGroup 和 channel 返回錯(cuò)誤

WaitGroup 除了不能快速失敗之外還有一個(gè)問(wèn)題就是不能在主線程中獲取到 groutine 出錯(cuò)時(shí)返回的錯(cuò)誤,這種情況下就可以用到 channel 進(jìn)行錯(cuò)誤傳遞,在主線程中獲取到錯(cuò)誤。

// 案例一:groutine 中只要有一個(gè)失敗了則返回 err 并且回到主協(xié)程運(yùn)行后續(xù)代碼
func main() {
    wg := sync.WaitGroup{}
    wg.Add(10)

    ch := make(chan error) // 使用一個(gè) channel 傳遞退出信號(hào)

    for i := 0; i  10; i++ {
        go func(i int) {
            time.Sleep(time.Duration(i) * time.Second)
            if i == 2 { // 檢測(cè)到 i==2 則提前退出
                ch - fmt.Errorf("i can't be 2")
                close(ch)
                return
            }
            fmt.Println(i)
            wg.Done()
        }(i)
    }

    go func() {
        wg.Wait() // wg.Wait 執(zhí)行之后表示所有的 groutine 都已經(jīng)執(zhí)行完成了,而且沒(méi)有 groutine 往 ch 傳遞退出信號(hào)
        ch - nil // 需要傳遞一個(gè) nil error,不然主線程會(huì)一直阻塞
        close(ch)
    }()

    err := -ch
    fmt.Println(err.Error())
}

// 運(yùn)行結(jié)果:
/*
0
1
i can't be 2
*/

// 案例二:等待所有的 groutine 都運(yùn)行完成再回到主線程并捕獲所有的 error
func main() {
    wg := sync.WaitGroup{}
    wg.Add(10)

    ch := make(chan error, 10) // 設(shè)置和 groutine 數(shù)量一致,可以緩沖最多 10 個(gè) error

    for i := 0; i  10; i++ {
        go func(i int) {
            defer func() {
                wg.Done()
            }()
            time.Sleep(time.Duration(i) * time.Second)
            if i == 2 {
                ch - fmt.Errorf("i can't be 2")
                return
            }
            if i == 3 {
                ch - fmt.Errorf("i can't be 3")
                return
            }
            fmt.Println(i)
        }(i)
    }

    wg.Wait() // wg.Wait 執(zhí)行之后表示所有的 groutine 都已經(jīng)執(zhí)行完成了
    close(ch) // 需要 close channel,不然主線程會(huì)阻塞

    for err := range ch {
        fmt.Println(err.Error())
    }
}

// 打印結(jié)果:
0
1
4
5
6
7
8
9
i can't be 2
i can't be 3

使用 ErrGroup 返回錯(cuò)誤

正是由于 WaitGroup 有以上說(shuō)的一些缺點(diǎn),Go 團(tuán)隊(duì)在實(shí)驗(yàn)倉(cāng)庫(kù)(golang.org/x)增加了 errgroup.Group 的功能,相比 WaitGroup 增加了錯(cuò)誤傳遞、快速失敗、超時(shí)取消等功能,相對(duì)于通過(guò) channel 和 WaitGroup 組合實(shí)現(xiàn)這些功能更方便,也更加推薦。
errgroup.Group 結(jié)構(gòu)體也比較簡(jiǎn)單,在 sync.WaitGroup 的基礎(chǔ)之上包裝了一個(gè) error 以及一個(gè) cancel 方法,err 的作用是在 goroutine 出錯(cuò)的時(shí)候能夠返回,cancel 方法的作用是在出錯(cuò)的時(shí)候快速失敗。
errgroup.Group 對(duì)外暴露了3個(gè)方法,WithContext、Go、Wait,沒(méi)有了 Add、Done 方法,其實(shí) Add 和 Done 是在包裝在了 errgroup.Group 的 Go 方法里面了,我們執(zhí)行的時(shí)候不需要關(guān)心。

// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero Group is valid and does not cancel on error.
type Group struct {
    cancel func()

    wg sync.WaitGroup

    errOnce sync.Once
    err     error
}

func WithContext(ctx context.Context) (*Group, context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    return Group{cancel: cancel}, ctx
}

// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *Group) Wait() error {
    g.wg.Wait()
    if g.cancel != nil {
        g.cancel()
    }
    return g.err
}

// Go calls the given function in a new goroutine.
//
// The first call to return a non-nil error cancels the group; its error will be
// returned by Wait.
func (g *Group) Go(f func() error) {
    g.wg.Add(1)

    go func() {
        defer g.wg.Done()

        if err := f(); err != nil {
            g.errOnce.Do(func() {
                g.err = err
                if g.cancel != nil {
                    g.cancel()
                }
            })
        }
    }()
}

以下是使用 errgroup.Group 來(lái)實(shí)現(xiàn)返回 goroutine 錯(cuò)誤的例子:

func main() {
    eg := errgroup.Group{}

    for i := 0; i  10; i++ {
        i := i // 這里需要進(jìn)行賦值操作,不然會(huì)有閉包問(wèn)題,eg.Go 執(zhí)行的 groutine 會(huì)引用 for 循環(huán)的 i
        eg.Go(func() error {
            if i == 2 {
                return fmt.Errorf("i can't be 2")
            }
            fmt.Println(i)
            return nil
        })
    }

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }
}

// 打印結(jié)果
9
6
7
8
3
4
1
5
0
i can't be 2

需要注意的一點(diǎn)是通過(guò) errgroup.Group 來(lái)返回 err 只會(huì)返回其中一個(gè) groutine 的錯(cuò)誤,而且是最先返回 err 的 groutine 的錯(cuò)誤,這一點(diǎn)是通過(guò) errgroup.Group 的 errOnce 來(lái)實(shí)現(xiàn)的。

使用 ErrGroup 實(shí)現(xiàn)提前退出

使用 errgroup.Group 實(shí)現(xiàn)提前退出也比較簡(jiǎn)單,調(diào)用 errgroup.WithContext 方法獲取 errgroup.Group 對(duì)象以及一個(gè)可以取消的 WithCancel 的 context,并且將這個(gè) context 方法傳入到所有的 groutine 中,并在 groutine 中使用 select 監(jiān)聽這個(gè) context 的 Done() 事件,如果監(jiān)聽到了表明接收到了 cancel 信號(hào),然后退出 groutine 即可。需要注意的是 eg.Go 一定要返回一個(gè) err 才會(huì)觸發(fā) errgroup.Group 執(zhí)行 cancel 方法。

// 案例一:通過(guò) groutine 顯示返回 err 觸發(fā) errgroup.Group 底層的 cancel 方法
func main() {
    ctx := context.Background()
    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i  10; i++ {
        i := i // 這里需要進(jìn)行賦值操作,不然會(huì)有閉包問(wèn)題,eg.Go 執(zhí)行的 groutine 會(huì)引用 for 循環(huán)的 i
        eg.Go(func() error {
            select {
            case -ctx.Done():
                return ctx.Err()
            case -time.After(time.Duration(i) * time.Second):
            }
            if i == 2 {
                return fmt.Errorf("i can't be 2") // 需要返回 err 才會(huì)導(dǎo)致 eg 的 cancel 方法
            }
            fmt.Println(i)
            return nil
        })
    }

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }
}

// 打印結(jié)果:
0
1
i can't be 2

// 案例二:通過(guò)顯示調(diào)用 cancel 方法通知到各個(gè) groutine 退出
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i  10; i++ {
        i := i // 這里需要進(jìn)行賦值操作,不然會(huì)有閉包問(wèn)題,eg.Go 執(zhí)行的 groutine 會(huì)引用 for 循環(huán)的 i
        eg.Go(func() error {
            select {
            case -ctx.Done():
                return ctx.Err()
            case -time.After(time.Duration(i) * time.Second):
            }
            if i == 2 {
                cancel()
                return nil // 可以不用返回 err,因?yàn)槭謩?dòng)觸發(fā)了 cancel 方法
                //return fmt.Errorf("i can't be 2")
            }
            fmt.Println(i)
            return nil
        })
    }

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }
}

// 打印結(jié)果:
0
1
context canceled


// 案例三:
// 基于 errgroup 實(shí)現(xiàn)一個(gè) http server 的啟動(dòng)和關(guān)閉 ,以及 linux signal 信號(hào)的注冊(cè)和處理,要保證能夠 一個(gè)退出,全部注銷退出
// https://lailin.xyz/post/go-training-week3-errgroup.html
func main() {
    g, ctx := errgroup.WithContext(context.Background())

    mux := http.NewServeMux()
    mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    })

    // 模擬單個(gè)服務(wù)錯(cuò)誤退出
    serverOut := make(chan struct{})
    mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
        serverOut - struct{}{}
    })

    server := http.Server{
        Handler: mux,
        Addr:    ":8080",
    }

    // g1
    // g1 退出了所有的協(xié)程都能退出么?
    // g1 退出后, context 將不再阻塞,g2, g3 都會(huì)隨之退出
    // 然后 main 函數(shù)中的 g.Wait() 退出,所有協(xié)程都會(huì)退出
    g.Go(func() error {
        return server.ListenAndServe()
    })

    // g2
    // g2 退出了所有的協(xié)程都能退出么?
    // g2 退出時(shí),調(diào)用了 shutdown,g1 會(huì)退出
    // g2 退出后, context 將不再阻塞,g3 會(huì)隨之退出
    // 然后 main 函數(shù)中的 g.Wait() 退出,所有協(xié)程都會(huì)退出
    g.Go(func() error {
        select {
        case -ctx.Done():
            log.Println("errgroup exit...")
        case -serverOut:
            log.Println("server will out...")
        }

        timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        // 這里不是必須的,但是如果使用 _ 的話靜態(tài)掃描工具會(huì)報(bào)錯(cuò),加上也無(wú)傷大雅
        defer cancel()

        log.Println("shutting down server...")
        return server.Shutdown(timeoutCtx)
    })

    // g3
    // g3 捕獲到 os 退出信號(hào)將會(huì)退出
    // g3 退出了所有的協(xié)程都能退出么?
    // g3 退出后, context 將不再阻塞,g2 會(huì)隨之退出
    // g2 退出時(shí),調(diào)用了 shutdown,g1 會(huì)退出
    // 然后 main 函數(shù)中的 g.Wait() 退出,所有協(xié)程都會(huì)退出
    g.Go(func() error {
        quit := make(chan os.Signal, 0)
        signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

        select {
        case -ctx.Done():
            return ctx.Err()
        case sig := -quit:
            return errors.Errorf("get os signal: %v", sig)
        }
    })

    fmt.Printf("errgroup exiting: %+v\n", g.Wait())
}

改善版的 Errgroup

使用 errgroup.Group 的 WithContext 我們注意到在返回 eg 對(duì)象的同時(shí)還會(huì)返回另外一個(gè)可以取消的 context 對(duì)象,這個(gè) context 對(duì)象的功能就是用來(lái)傳遞到 eg 需要同步的 groutine 中有一個(gè)發(fā)生錯(cuò)誤時(shí)取消整個(gè)同步的 groutine,但是有不少同學(xué)可能會(huì)不經(jīng)意將這個(gè) context 傳到其他的非 eg 同步的業(yè)務(wù)代碼groutine 中,這樣會(huì)導(dǎo)致非關(guān)聯(lián)的業(yè)務(wù)代碼莫名其妙的收到 cancel 信息,類似如下的寫法:

func main() {
    ctx := context.Background()
    eg, ctx := errgroup.WithContext(ctx)

    for i := 0; i  10; i++ {
        i := i // 這里需要進(jìn)行賦值操作,不然會(huì)有閉包問(wèn)題,eg.Go 執(zhí)行的 groutine 會(huì)引用 for 循環(huán)的 i
        eg.Go(func() error {
            select {
            case -ctx.Done():
                return ctx.Err()
            case -time.After(time.Duration(i) * time.Second):
            }
            if i == 2 {
                return fmt.Errorf("i can't be 2") // 需要返回 err 才會(huì)導(dǎo)致 eg 的 cancel 方法
            }
            fmt.Println(i)
            return nil
        })
    }

    if err := eg.Wait(); err != nil {
        fmt.Println(err.Error())
    }

    OtherLogic(ctx)
}

func OtherLogic(ctx context.Context) {
    // 這里的 context 用了創(chuàng)建 eg 返回的 context,這個(gè) context 可能會(huì)往后面更多的 func 中傳遞
    // 如果在該方法或者后面的 func 中有對(duì) context 監(jiān)聽取消型號(hào),會(huì)導(dǎo)致這些 context 被取消了
}

另外不管是 WaitGroup 還是 errgroup.Group 都不支持控制最大并發(fā)限制以及 panic 恢復(fù)的功能,因?yàn)槲覀儾荒鼙U衔覀兺ㄟ^(guò)創(chuàng)建的 groutine 不會(huì)出現(xiàn)異常,如果沒(méi)有在創(chuàng)建的協(xié)程中捕獲異常,會(huì)直接導(dǎo)致整個(gè)程序退出,這是非常危險(xiǎn)的。
這里推薦一下 bilbil 開源的微服務(wù)框架 go-kratos/kratos 自己實(shí)現(xiàn)了一個(gè)改善版本的 errgroup.Group,其實(shí)現(xiàn)的的思路是利用 channel 來(lái)控制并發(fā),并且創(chuàng)建 errgroup 的時(shí)候不會(huì)返回 context 避免 context 往非關(guān)聯(lián)的業(yè)務(wù)方法中傳遞。

到此這篇關(guān)于Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解的文章就介紹到這了,更多相關(guān)Golang waitgroup內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • 解決Golang 中使用WaitGroup的那點(diǎn)坑
  • 在golang中使用Sync.WaitGroup解決等待的問(wèn)題
  • Golang中的sync包的WaitGroup操作
  • Golang中的sync.WaitGroup用法實(shí)例
  • Golang標(biāo)準(zhǔn)庫(kù)syscall詳解(什么是系統(tǒng)調(diào)用)
  • Golang的os標(biāo)準(zhǔn)庫(kù)中常用函數(shù)的整理介紹

標(biāo)簽:延邊 宜賓 黑龍江 武漢 新余 張掖 嘉峪關(guān) 江西

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解》,本文關(guān)鍵詞  Golang,標(biāo)準(zhǔn),庫(kù),tips,之,waitgroup,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問(wèn)題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解》相關(guān)的同類信息!
  • 本頁(yè)收集關(guān)于Golang 標(biāo)準(zhǔn)庫(kù) tips之waitgroup詳解的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章