golang開發:go併發的建議

飛翔碼農發表於2022-04-05

這個是前段時間看到Go語言的貢獻者與佈道師 Dave Cheney對Go併發的建議或者叫使用的陷阱(不是我自己的建議),結合自己最近幾年對gorotine的使用,再回頭看這幾條建議,真的會茅塞頓開,覺得特別重要。這篇文章對併發的建議的章節地址
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_concurrency

Dave Cheney

Dave Cheney 是 Go 程式語言的開源貢獻者和專案成員。David 是技術社群中備受尊敬的聲音,他就軟體設計、效能和 Go 程式語言等各種主題發表演講。David 在go語言歷程中,分享過很多關於Golang語言的正確使用的文章。這是他的部落格地址。
https://dave.cheney.net/

8.1. Keep yourself busy or do the work yourself(讓自己忙碌起來或自己做工作)

這個建議應該比較容易理解,啟動一個gorotine應該是執行程式的,自己執行或者被人呼叫執行,不應該啟動gorotine之後這個gorotine啥事都沒幹。
作者舉了一個例子

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, GopherCon SG")
	})
	go func() {
		if err := http.ListenAndServe(":8080", nil); err != nil {
			log.Fatal(err)
		}
	}()

	for {
	}
}

為了阻塞main gorotine不要直接退出,等待go func的執行,最後寫了一個for的死迴圈,這樣的話,main gorotine就是通常所說的啥事都沒幹,毫無結果地執行。我們當然可以使用WaitGroup去等待go func的結束。作者給我們的建議,既然我們只有一個任務需要做,main gorotine就可以完成,為什麼要啟動一個gorotine去做這個任務,而讓main gorotine去等待,完全可以讓main去做這個任務

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, GopherCon SG")
	})
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

許多 Go 程式設計師過度使用 goroutine,尤其是在他們剛開始的時候。與生活中的所有事物一樣,適度是成功的關鍵。

8.2. Leave concurrency to the caller(將併發留給呼叫者)

這個表述起來比較容易,平常開發中可能會被忽略,一個物件提供了啟動使用goroutine的方法,那麼就必須提供關閉goroutine的方法,而且一般得原則的是誰呼叫誰關閉。
舉一個我們專案開發中的例子

timer_go.go
package main

import (
	"fmt"
	"sync"
	"time"
)

type TimerGo struct {
	quit chan bool
}

func NewTimerGo() *TimerGo {
	timer := new(TimerGo)
	timer.quit = make(chan bool)
	return timer
}

func (this *TimerGo) Run(wg *sync.WaitGroup) {
	defer wg.Done()
	cfgTime := 3

	t := time.NewTicker(time.Duration(cfgTime) * time.Second)
	defer t.Stop()

	for {
		select {
		case <- this.quit:
			fmt.Println("quite")
			return
		case <-t.C:
			this.Sync()
		}
	}
}

func (this *TimerGo) Sync() {
	fmt.Printf("Sync")
}

func (this *TimerGo) Close() {
	close(this.quit)
}

main.go
package main
func main() {
	timergo := NewTimerGo()
	wg := new(sync.WaitGroup)
	wg.Add(1)
	go timergo.Run(wg)
	//start up
	timergo.Close()
	wg.Wait()
}

這個例子比較容易理解,我們需要每隔三秒執行一個非同步的任務,這個工作我們啟動一個goroutine去執行,所以我們在main函式執行go timergo.Run,我們也提供Close的方法,通過一個channal去關閉它。
原則就是,誰呼叫誰關閉。提供執行方法,就必須提供關閉方法。

8.3. Never start a goroutine without knowning when it will stop(永遠不要在不知道何時停止的情況下啟動 goroutine)

這個原則我覺得應該是最重要的原則,而且在開發中最容易遇到的問題。我們前期也寫過很多這樣的程式碼,而且我看大家使用的專案基本也都是在需要啟動一個goroutine去執行程式碼的時候是這樣寫的

go AAA()
go BBB()
go CCC()

很少有人去關心啟動的這三個goroutine應該在什麼情況下去關閉,應該怎麼關閉,他們得執行狀態是怎麼樣的,在服務重新啟動時候,是等待執行完畢還是強制中斷。
這個原則應該會指引我們去做一些可靠的架構和規劃。這個遇到的太多了,有必要花時間去整理這裡。

永遠不要在不知道何時停止的情況下啟動 goroutine

相關文章