2020-11-30-golang併發模式context
https://blog.golang.org/context
Go的併發模式:Context (上下文)
在Go的伺服器中,每個的進來的請求都是由獨立的goroutine來處理的。處理請求的goroutine中通常會啟動額外的goroutines去訪問資料庫和RPC服務。處同一個請求的這群goroutines通常需要訪問特定的請求值,例如:終端使用者的身份識別,授權令牌和請求的截止日期。當一個請求被取消或超時時,這個請求所有相關的goroutines都應該快速退出,以便系統回收相關資源。
谷歌開發了一個context的包,使得在一個處理請求中的所有goroutine中跨API邊界傳遞請求範圍內的值,取消訊號,截止時間很方便(makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.)。
Context
// 一個Context包含一個 Deadline, cancelation signal, 和 request-scoped values
// across API boundaries. 它的方法是可供多個goroutines安全地同時使用
type Context interface {
// Done返回一個channel,當Context取消時或超時時,它會被closed。
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}
Done方法返回一個channel,它作為Context中執行的相關函式的一個取消訊號,當channel關閉時,相關函式需要放棄它們的工作並返回。Err方法返回一個錯誤,代表Context為什麼取消。這時有篇文章介紹Done channel的更多細節。
Context沒有一個Cancel的方法,與Done channel是隻接收的原因是一樣的:接收取消訊號的函式通常不是傳送訊號函式。特別是,當父操作為子操作啟動goroutines時,這些子操作應該不能取消父操作。相反,WithCancel函式為取消一個新的Context值提供了一個途徑。
Context是多goroutine安全的。程式碼中可以將一個Context傳遞給多個goroutine,和取消Context以通知goroutine。
派生Derived contexts
context包提供了從現有context值中派生新的Context值的功能。它們形成一顆樹。當一個Context取消時,所有從它派生的Contexts都會取消。
// Background 返回一個空的(empty) Context. 它永遠不會被取消,沒有deadline,沒有values。
// Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
Backgroud是任何的Context樹的根,永遠不會被取消。
WithCancel 和WithTimeout返回的派生的context可以比父context早點取消。當請求處理返回時,與這個請求相關的那個Context通常會被取消。WithCancel對於取消冗餘請求也很有用。WithTimeout對於設定對後端伺服器的請求的deadline很有用:
// WithCancel 返回父Context的一個拷貝,當父Context的Done被 關閉或cancel被呼叫時,子Context的Done也會盡可能快地被關閉。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// A CancelFunc cancels a Context.
type CancelFunc func()
// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
例子:Google網頁搜尋
我們這個例子中是一個http服務端,處理 /search?q=golang&timeout=1s
這樣的URLs,通過轉發查詢關鍵詞"golang"給 Google Web Search API ,然後渲染結果。超時引數告訴伺服器在該持續時間過去之後取消請求。
程式碼跨三個包(The code is split across three packages:)
-
server 提供main函式和/search的處理函式(handler)
-
userip 提供從請求中提取使用者IP地址並將其與上下文關聯的功能。
-
google 提供Search函式,將查詢發給Google。
Server程式
package main
import (
"context"
"html/template"
"log"
"net/http"
"time"
"golang.org/x/blog/content/context/google"
"golang.org/x/blog/content/context/userip"
)
func main() {
http.HandleFunc("/search", handleSearch)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
// query to google.Search. If the query param includes timeout, the search is
// canceled after that duration elapses.
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the Context for this handler. Calling cancel closes the
// ctx.Done channel, which is the cancellation signal for requests
// started by this handler.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a context that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.
// Check the search query.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
// Store the user IP in ctx for use by code in other packages.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}
}
var resultsTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
<ol>
{{range .Results}}
<li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
{{end}}
</ol>
<p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
</body>
</html>
`))
userip包
package userip
import (
"context"
"fmt"
"net"
"net/http"
)
// FromRequest 從req中提取使用者IP地址, 如果有的話
func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
return userIP, nil
}
// key的型別不匯出,通過這樣防止其它包中使用同樣的key而引起衝突。got it
type key int
// userIPkey 是使用者IP地址的context key, 它的零值是任意的。
// 如果當前包內定義其它context keys, 它們會有不同的整形值。
const userIPKey key = 0
// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}
// FromContext extracts the user IP address from ctx, if present.
func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
google包
package google
import (
"context"
"encoding/json"
"net/http"
"golang.org/x/blog/content/context/userip"
)
// Results is an ordered list of search results.
type Results []Result
// A Result contains the title and URL of a search result.
type Result struct {
Title, URL string
}
// Search sends query to Google search and returns the results.
func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)
// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
// Issue the HTTP request and handle the response. The httpDo function
// cancels the request if ctx.Done is closed.
var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()
// Parse the JSON search result.
// https://developers.google.com/web-search/docs/#fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo waits for the closure we provided to return, so it's safe to
// read results here.
return results, err
}
// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
// closed while the request or f is running, httpDo cancels the request, waits
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
總結
在Google,我們要求Go程式設計師,在傳入和傳出請求之間的呼叫路徑上,將Context作為每個函式中的第一個引數傳遞。這使許多不同團隊開發的Go程式碼可以很好地進行互操作。它提供對超時和取消的簡單控制,並確保諸如安全性憑證之類的關鍵值正確地傳遞Go程式。
相關文章
- 九. Go併發程式設計--context.ContextGo程式設計Context
- Go高效併發 10 | Context:多執行緒併發控制神器GoContext執行緒
- 【Go進階—併發程式設計】ContextGo程式設計Context
- Go高效併發 11 | 併發模式:Go 語言中即學即用的高效併發模式Go模式
- 「Golang成長之路」併發之併發模式Golang模式
- Golang 高效實踐之併發實踐context篇GolangContext
- 「Golang成長之路」併發之併發模式篇Golang模式
- go併發-工作池模式Go模式
- Golang常見的併發模式Golang模式
- Go語言中的併發模式Go模式
- Context真正的實現與Context設計模式Context設計模式
- Java併發設計模式--不可變模式(immutable)Java設計模式
- React 併發功能體驗-前端的併發模式已經到來。React前端模式
- Java 併發模式之Master-WorkerJava模式AST
- Java併發指南9:AQS共享模式與併發工具類的實現JavaAQS模式
- 【併發程式設計】Future模式新增Callback及Promise 模式程式設計模式Promise
- 併發設計模式---生產者/消費者模式設計模式
- 使用Fan-Out模式併發處理模式
- Go併發模式:管道和顯式取消Go模式
- Go 併發 2.2:錯誤處理模式Go模式
- 設計模式(八)Context中的裝飾者模式設計模式Context
- 淺析Java併發中的單例模式Java單例模式
- 併發請求:統計資料收集模式模式
- Java併發程式設計---java規範與模式下的併發程式設計1.1Java程式設計模式
- 從裝飾者模式到 Context 類族模式Context
- Doug Lea併發設計模式(JUC學習前奏)設計模式
- 說一說併發設計模式—Future(非同步)設計模式非同步
- PHP 併發程式設計之 Master-Worker 模式PHP程式設計AST模式
- GO 語言的併發模式你瞭解多少?Go模式
- WCF 的 Service Instance模式和併發處理模式
- Android開發 - Context解析AndroidContext
- 多執行緒設計模式-高併發請求快取模式(Guarded Suspension)執行緒設計模式快取
- 網際網路高併發架構設計模式架構設計模式
- 實戰Java高併發程式設計模式視訊Java程式設計設計模式
- Java併發---併發理論Java
- 併發-1-併發模型模型
- Guava併發:使用Monitor控制併發Guava
- 【併發程式設計】Future模式及JDK中的實現程式設計模式JDK