Go:API 併發模式
本文旨在簡潔地向您展示作者花了很長時間組裝起來的東西,從而節省您的時間和精力。希望您發現裏面的內容是一個具有啓發性的、實用的、真實場景的 API 併發案例。
API 併發模式
如果您遵循下面列出的準則,就可以以最少的錯誤和工作量來構建一個高度併發的 API。
1、使用 go 關鍵字來異步執行,可以達到併發的目的
2、創建和關閉專用通道,這樣可以最大限度地減少內存泄漏和死鎖的機會。
3、使用 context.Context,停止不再需要的掛起請求。
4、使用指針代替 channel 返回結果,可以減少需要管理的 channel 數量。
5、通過 <-chan error 返回錯誤,您可以在返回響應之前等待阻塞操作完成。
就是這些!如果你堅持這 5 條規則,就可以寫出可讀的、整潔的 Go 代碼,而且具有高度併發,不會出現死鎖或內存泄漏。
代碼示例
下面是使用這些規則的示例實現。希望可以說明如何寫出易於閱讀、測試和維護的高併發 API 代碼。
API 請求
步驟 1:異步地發出所有 API 請求和阻塞操作。
// Piece 表示部分的響應結果
type Piece struct {
ID uint `json:"id"`
}
// getPiece 調用`GET /piece/:id`
funcgetPiece(ctx context.Context, id uint, piece *Piece) <-chan error {
out := make(chan error)
go func() {
// 關閉channel,合理管理內存
defer close(out)
// NewRequestWithContext在調用者取消ctx時會自動取消請求
req, err := http.NewRequestWithContext(
ctx,
"GET",
fmt.Sprintf("api.url.com/piece/%d", id),
nil,
)
if err != nil {
out <- err
return
}
// 發起請求
rsp, err := http.DefaultClient.Do(req)
if err != nil {
out <- err
return
} else if rsp.StatusCode != http.StatusOK {
out <- fmt.Errorf("%d: %s", rsp.StatusCode, rsp.Status)
return
}
// 將響應結果解析到piece
defer rsp.Body.Close()
if err := json.NewDecoder(rsp.Body).Decode(piece); err != nil {
out <- err
return
}
}()
return out
}
API 響應
步驟 2:將多個阻塞操作和 API 請求組合到一個響應結構體中
// Result是將併發檢索的多個阻塞操作組合在一起
type Result struct {
FirstPiece *Piece `json:"firstPiece,omitempty"`
SecondPiece *Piece `json:"secondPiece,omitempty"`
ThirdPiece *Piece `json:"thirdPiece,omitempty"`
}
// GetResult提供API請求到處理程序
func GetResult(w http.ResponseWriter, r *http.Request) {
// 解析和驗證請求參數…
// getResult立即停止如果http.Request被取消
var result Result
if err := <-getResult(r.Context(), &result); err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
// 對響應結果進行序列號
bs, err := json.Marshal(&result)
if err != nil {
w.Write([]byte(err.Error()))
w.WriteHeader(http.StatusInternalServerError)
return
}
// 成功!
w.Write(bs)
w.WriteHeader(http.StatusOK)
}
// getResult 返回多個併發API調用的結果
func getResult(ctx context.Context, result *Result) <-chan error {
out := make(chan error)
go func() {
// 正確管理內存
defer close(out)
// 如果有一個請求失敗,cancel func將允許我們停止所有掛起的請求
ctx, cancel := context.WithCancel(ctx)
// Merge將所有getPieces返回的errors統一到一個“<-chan error"中
//如果沒有發生錯誤,Merge會等待所有<-chan error關閉
for err := range util.Merge(
getPiece(ctx, 1, result.FirstPiece),
getPiece(ctx, 2, result.SecondPiece),
getPiece(ctx, 3, result.ThirdPiece),
) {
if err != nil {
// 取消所有掛起的請求
cancel()
// 將錯誤傳給調用者
out <- err
return
}
}
}()
return out
}
Merge 函數
步驟 3:實現一個聚合函數。即使你非常熟悉 go,這裏也可能是最複雜和最容易出錯的部分。我建議直接複製黏貼這段代碼到你自己的 util 包中。
package util
import (
"sync"
)
// 合併多個錯誤通道到一個錯誤通道中
func Merge(errChans ...<-chan error) <-chan error {
mergedChan := make(chan error)
// 創建WaitGroup等待所有的errChans關閉
var wg sync.WaitGroup
wg.Add(len(errChans))
go func() {
// 當所有的errchan都關閉時,關閉mergedChan
wg.Wait()
close(mergedChan)
}()
for i := range errChans {
go func(errChan <-chan error) {
// 等待每個errChan關閉
for err := range errChan {
if err != nil {
// 將每個errChan內容發送到mergedChan
mergedChan <- err
}
}
//通知WaitGroup其中一個errChans關閉
wg.Done()
}(errChans[i])
}
return mergedChan
}
總結
在 Go 中有許多方法來實現併發。根據作者經驗,這是在構建 API 時實現併發的一種清晰而有效的方法,該 API 可以保持代碼的整潔性並最小化內存管理錯誤。
希望這對 Golang 中的併發提供一個實用的說明。也希望在把這些信息拼湊在一起時,它能幫你節省一些時間和煩惱。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xIWP5MHnIM6vmjVlpWTBJA