一、選擇GO的原因 作為一個後端開發,日常工作中接觸最多的兩門語言就是PHP和GO了。無可否認,PHP確實是最好的語言(手動狗頭哈哈),寫起來真的很舒爽,沒有任何心智負擔,字元串和整型壓根就不用區分,開發速度真的是比GO快很多。現在工作中也還是有一些老項目在使用PHP,但21年之後的新項目基本上就都 ...
一、選擇GO的原因
作為一個後端開發,日常工作中接觸最多的兩門語言就是PHP和GO了。無可否認,PHP確實是最好的語言(手動狗頭哈哈),寫起來真的很舒爽,沒有任何心智負擔,字元串和整型壓根就不用區分,開發速度真的是比GO快很多。現在工作中也還是有一些老項目在使用PHP,但21年之後的新項目基本上就都是用GO了。那為什麼PHP那麼香,還要轉戰使用GO呢,下麵就給大家講解一下我們新項目從PHP轉GO的原因,有幾個比較重要的點:
1、PHP不能滿足我們的高併發業務,這是最主要的原因了,(PS:我這裡所說的PHP是指官方的php-fpm模式下的開發,是一個請求一個進程的那種模式,而不是類似於swoole常駐進程的那種。那麼為什麼不去使用swoole呢,當然也是有的,但swoole畢竟太小眾了,且之前有很多bug,使用起來心智負擔太高了),而我們部門所負責的是直播業務,每天都和高併發打交道啊,所以只能將目光轉向了併發小王子GO的懷抱。
2、GO語言當時在市面上很火,像騰訊、百度、滴滴、好未來這些大廠都在陸陸續續地從PHP轉向GO,這也是一個訊號吧,跟著大佬們走總不會錯。
3、GO語言的簡單簡潔,相比較於JAVA,上手是很快的(但真正學好還是沒那麼容易的),我當時就學了兩個禮拜左右語法就跟著一起寫項目了。
二、GO解決的併發問題
說到併發,是GO最基本的功能了,但是在傳統的PHP中是比較困難的,如果不藉助其它一些擴展的話,是做不到併發的。舉個場景:每個用戶進入直播間,都要獲取很多信息,有版本服務信息、直播基礎信息、用戶信息、直播關聯權益信息、直播間信息統計等等。如果是PHP的寫法,就得按照下麵串列的流程去做,這個介面耗時就是所有操作的時間之和,嚴重影響用戶體驗啊。
但如果換成GO去做這件事,那就非常清爽了,這個用戶請求耗時就只需要時間最長的那個操作耗時,如下圖:
那麼我們如何用去實現這個併發邏輯呢?
方法1:使用sync.WaitGroup
//請求入口
func main() {
var (
VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
)
ctx := context.Background()
GoNoErr(ctx, func() {
VersionDetail = 1 //版本服務信息
time.Sleep(1 * time.Second)
fmt.Println("執行第一個任務")
}, func() {
LiveDetail = 2 //直播基礎信息
time.Sleep(2 * time.Second)
fmt.Println("執行第二個任務")
}, func() {
UserDetail = 3 //用戶信息
time.Sleep(3 * time.Second)
fmt.Println("執行第三個任務")
}, func() {
EquityDetail = 4 //直播關聯權益信息
time.Sleep(4 * time.Second)
fmt.Println("執行第四個任務")
}, func() {
StatisticsDetail = 5 //直播間信息統計
time.Sleep(5 * time.Second)
fmt.Println("執行第五個任務")
})
fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}
//併發方法
func GoNoErr(ctx context.Context, functions ...func()) {
var wg sync.WaitGroup
for _, f := range functions {
wg.Add(1)
// 每個函數啟動一個協程
go func(function func()) {
function()
wg.Done()
}(f)
}
// 等待執行完
wg.Wait()
}
方法2:使用ErrGroup庫
//請求入口
func main() {
var (
VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail int
err error
)
ctx := context.Background()
err = GoErr(ctx, func() error {
VersionDetail = 1 //版本服務信息
time.Sleep(1 * time.Second)
fmt.Println("執行第一個任務")
return nil //返回實際執行的錯誤
}, func() error {
LiveDetail = 2 //直播基礎信息
time.Sleep(2 * time.Second)
fmt.Println("執行第二個任務")
return nil //返回實際執行的錯誤
}, func() error {
UserDetail = 3 //用戶信息
time.Sleep(3 * time.Second)
fmt.Println("執行第三個任務")
return nil //返回實際執行的錯誤
}, func() error {
EquityDetail = 4 //直播關聯權益信息
time.Sleep(4 * time.Second)
fmt.Println("執行第四個任務")
return nil //返回實際執行的錯誤
}, func() error {
StatisticsDetail = 5 //直播間信息統計
time.Sleep(5 * time.Second)
fmt.Println("執行第五個任務")
return nil //返回實際執行的錯誤
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(VersionDetail, LiveDetail, UserDetail, EquityDetail, StatisticsDetail)
}
func GoErr(ctx context.Context, functions ...func() error) error {
var eg errgroup.Group
for i := range functions {
f := functions[i] //請註意這裡的寫法,下麵有講解
eg.Go(func() (err error) {
err = f()
if err != nil {
//記日誌
}
return err
})
}
// 等待執行完
return eg.Wait()
}
上面就是使用ErrGroup庫的併發執行任務的方法,可以直接拿來使用,ErrGroup這是GO官方提供的一個同步擴展庫,可以很好地將⼀個通⽤的⽗任務拆成⼏個⼩任務併發執⾏。
上面有一點需要特別註意的寫法,就是下麵這段代碼的寫法,寫法1:
for i := range functions {
f := functions[i]
eg.Go(func() (err error) {
err = f()
也可以這樣寫,寫法2:
for _, f := range functions {
fs := f
eg.Go(func() (err error) {
err = fs()
但如果這樣寫就會有問題,寫法3:
for _, f := range functions {
eg.Go(func() (err error) {
err = f()
你們可以改一下,實際跑一下。會發現 (寫法3) 會出現類似這樣的錯誤結果
正確預期的結果(寫法1、寫法2)應該是這樣的
這是因為在 Go 語言中,當使用閉包(匿名函數)時,如果閉包引用了外部的變數,閉包實際上會捕獲這些變數的引用。在迴圈中創建閉包時,如果直接將迴圈變數作為閉包的參數或在閉包中引用該變數,會導致所有生成的閉包都引用相同的變數,即最後一次迭代的值。
為了避免這個問題,常見的做法是在迴圈內部創建一個新的變數,將迴圈變數的值賦給這個新變數,然後在閉包中引用該新變數。這樣,每次迴圈迭代都會創建一個新的變數,閉包捕獲的是不同的變數引用,而不是相同變數的引用。
在給定的代碼中,fs := f 就是為了創建一個新的變數 f,並將迴圈變數 f 的值賦給它。這樣,在閉包中就可以安全地引用這個新變數 f,而不會受到迴圈迭代的影響。這個技巧非常有用,可以在迴圈中創建多個獨立的閉包,並確保它們捕獲的是預期的變數值,而不會受到迴圈迭代的干擾。
當然,還有一些第三方庫也實現了上面的併發分組操作,大家感興趣的可以去GitHub上看看,但功能和實現基本都大同小異。以上就是GO併發的基礎,將一個父任務拆分成多個子任務去執行,提高程式的併發度,節省程式耗時。我們平時在工作中,兩種方法都可以直接拿來使用,可以說這兩個GO併發方法幾乎貫穿了我的GO職業生涯,也是最基礎最實用的併發操作方法。
一個人可以被毀滅,但不可以被打敗。