<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                在并發程序中,由于連接超時,用戶取消或系統故障,往往需要執行搶占操作。我們之前使用done通道來在程序中取消所有阻塞的并發操作,雖然取得了不錯的效果,但同樣也存在局限。 如果我們可以給取消通知添加額外的信息:例如取消原因,操作是否正常完成等,這對我們進一步處理會起到非常大的作用。 在社區的不斷推動下,Go開發組決定創建一個標準模式,以應對這種需求。在Go 1.7中,context包被引入標準庫。 如果我們瀏覽一下context包,會發現其包含的內容非常少: ``` var Canceled = errors.New("context canceled") var Canceled = errors.New("context canceled") type CancelFunc type Context func Background() Context func TODO() Context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context ``` 我們稍后會討論這些類型和函數,現在讓我們把關注點放到Context類型上。這個類型會貫穿你的整個系統,就跟done通道一樣。如果你使用context包,從上游衍生出來的每個下游函數都可以使用Context作為參數。其類型定義是這樣的: ``` type Context interface { // Deadline 返回任務完成時(該 context 被取消)的時間。 // 如果deadline 未設置,則返回的ok值為false。 // 連續調用該函數將返回相同的結果。 Deadline() (deadline time.Time, ok bool) // Done 返回任務完成時(該 context 被取消)一個已關閉的通道。 // 如果該context無法被取消,Done 將返回nil。 // 連續調用該函數將返回相同的結果。 // // 當cancel被調用時,WithCancel 遍歷 Done以執行關閉; // 當deadline即將到期時,WithDeadline 遍歷 Done以執行關閉; // 當timeout時,WithTimeout 遍歷 Done以執行關閉。 // // Done 主要被用于 select 語句: // // // Stream 使用DoSomething生成值,并將值發送出去 // // 直到 DoSomething 返回錯誤或 ctx.Done 被關閉 // func Stream(ctx context.Context, out chan<- Value) error { // for { // v, err := DoSomething(ctx) // if err != nil { // return err // } // select { // case <-ctx.Done(): // return ctx.Err() // case out <- v: // } // } // } // // 查看 https://blog.golang.org/pipelines更多示例以了解如何使用 // Done通道執行取消操作。 Done() <-chan struct{} // 如果 Done 尚未關閉, Err 返回 nil. // 如果 Done 已關閉, Err 返回值不為nil的error以解釋為何關閉: // 因 context 的關閉導致 // 或 context 的 deadline 執行導致。 // 在 Err 返回值不為nil的error之后, 連續調用該函數將返回相同的結果。 Err() error // Value 根據 key 返回與 context 相關的結果, // 如果沒有與key對應的結果,則返回nil。 // 連續調用該函數將返回相同的結果。 // // 該方法僅用于傳輸進程和API邊界的請求數據, // 不可用于將可選參數傳遞給函數。 // // 鍵標識著上Context中的特定值。 // 在Context中存儲值的函數通常在全局變量中分配一個鍵, // 然后使用該鍵作為context.WithValue和Context.Value的參數。 // 鍵可以是系統支持的任何類型; // 程序中各包應將鍵定義為未導出類型以避免沖突。 // // 定義Context鍵的程序包應該為使用該鍵存儲的值提供類型安全的訪問器: // // // user包 定義了一個User類型,該類型存儲在Context中。 // package user // // import "context" // // // User 類型的值會存儲在 Context中。 // type User struct {...} // // // key是位于包內的非導出類型。 // // 這可以防止與其他包中定義的鍵的沖突。 // type key int // // // userKey 是user.User類型的值存儲在Contexts中的鍵。 // // 它是非導出的; clients use user.NewContext and user.FromContext // // 使用 user.NewContext 和 user.FromContext來替代直接使用鍵。 // var userKey key // // // NewContext 返回一個新的含有值 u 的 Context。 // func NewContext(ctx context.Context, u *User) context.Context { // return context.WithValue(ctx, userKey, u) // } // // // FromContext 返回存儲在 ctx中的 User類型的值(如果存在的話)。 // func FromContext(ctx context.Context) (*User, bool) { // u, ok := ctx.Value(userKey).(*User) // return u, ok // } Value(key interface{}) interface{} ``` 這看起來挺簡單。有一個Done方法返回當我們的函數被搶占時關閉的通道。還有一些新鮮的但不難理解的方法:一個Deadline函數,用于指示在一定時間之后goroutine是否會被取消,以及一個Err方法,如果goroutine被取消,將返回非零值。 但Value方法看起來有點奇怪。它是干嘛用的? goroutines的主要用途之一是為請求提供服務。通常在這些程序中,除了搶占信息之外,還需要傳遞特定于請求的信息。這是Value函數的意義。我們會稍微談一談這個問題,但現在我們只需要知道context包有兩個主要目的: * 提供取消操作。 * 提供用于通過調用傳輸請求附加數據的數據包。 讓我們看看第一個目的:取消操作。 正如我們在“防止Goroutine泄漏”中所學到的,函數中的取消有三個方面: * goroutine的生成者可能想要取消它。 * goroutine可能需要取消其衍生出來的goroutine。 * goroutine中的任何阻塞操作都必須是可搶占的,以便將其取消。 Context包可以幫助我們處理這三個方面的需求。 前面提到,Context類型將是函數的第一個參數。如果你查看了Context接口的方法,會發現沒有任何東西可以改變底層結果的狀態。更進一步的說,沒有任何東西被系統允許把Context本身干掉。這保護了Context調用堆棧的功能。因此,結合接口中的Done方法,Context類型可以安全的管理取消操作。 這就產生了一個問題:如果Context是一成不變的,那我們如何影響調用堆棧中當前函數的子函數中的取消行為? context包提供的一些函數回答了這個問題: ``` func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) ``` 你會發現,這些函數都傳入了Context類型的值,同時返回了Context類型的值,它們都使用與這些函數相關的選項生成Context的新實例。 WithCancel返回一個新的Context,它在調用返回的cancel函數時關閉done通道。 WithDeadline返回一個新的Context,當機器的時鐘超過給定的最后期限時,它關閉done通道。 WithTimeout返回一個新的Context,它在給定的超時時間后關閉done通道。 如果你的函數需要以某種方式在調用中取消它的子函數,可以調用這三個函數中的一個并傳遞給它的上下文,然后將返回的上下文傳遞給它的子函數。 如果你的函數不需要修改取消行為,那么函數只傳遞給定的上下文。 通過這種方式,調用者可以創建符合其需求的上下文,而不會影響其創建者。這為管理調用分支提供了一個可組合的優雅的解決方案。 通過這樣的方式,Context的實例可以貫穿你的整個程序。在面向對象的范例中,通常將對經常使用的數據的引用存儲為成員變量,但重要的是不要使用context.Context的實例來執行此操作。context.Context的實例可能與外部看起來相同,但在內部它們可能會在每個堆棧幀處發生變化。出于這個原因,總是將Context的實例傳遞給你的函數是很重要的。通過這種方式,函數具有用于它的上下文,而不是把堆棧里的上下文隨意取出來用。 在異步調用鏈的頂部,你的代碼可能不會傳遞Context。要啟動鏈,context包提供了兩個函數來創建Context的空實例。 我們來看一個使用done通道模式的例子,并比較下切換到使用context文包獲得什么好處。 這是一個同時打印問候和告別的程序: ``` func main() { var wg sync.WaitGroup done := make(chan interface{}) defer close(done) wg.Add(1) go func() { defer wg.Done() if err := printGreeting(done); err != nil { fmt.Printf("%v", err) return } }() wg.Add(1) go func() { defer wg.Done() if err := printFarewell(done); err != nil { fmt.Printf("%v", err) return } }() wg.Wait() } func printGreeting(done <-chan interface{}) error { greeting, err := genGreeting(done) if err != nil { return err } fmt.Printf("%s world!\n", greeting) return nil } func printFarewell(done <-chan interface{}) error { farewell, err := genFarewell(done) if err != nil { return err } fmt.Printf("%s world!\n", farewell) return nil } func genGreeting(done <-chan interface{}) (string, error) { switch locale, err := locale(done); { case err != nil: return "", err case locale == "EN/US": return "hello", nil } return "", fmt.Errorf("unsupported locale") } func genFarewell(done <-chan interface{}) (string, error) { switch locale, err := locale(done); { case err != nil: return "", err case locale == "EN/US": return "goodbye", nil } return "", fmt.Errorf("unsupported locale") } func locale(done <-chan interface{}) (string, error) { select { case <-done: return "", fmt.Errorf("canceled") case <-time.After(5 * time.Second): } return "EN/US", nil } ``` 這會輸出: ``` hello world! goodbye world! ``` 忽略競爭條件,我們可以看到程序有兩個分支同時運行。通過創建done通道并將其傳遞給我們的調用鏈來設置標準搶占方法。如果我們在main的任何一點關閉done頻道,那么兩個分支都將被取消。 我們可以嘗試幾種不同且有趣的方式來控制該程序。也許我們希望genGreeting如果花費太長時間就會超時。也許我們不希望genFarewell調用locale——在其父進程很快就會被取消的情況下。在每個堆棧框架中,一個函數可以影響其下的整個調用堆棧。 使用done通道模式,我們可以通過將傳入的done通道包裝到其他done通道中,然后在其中任何一個通道啟動時返回,但我們不會獲得上下文給的deadline和錯誤的額外信息。 為了將done通道模式與使用context包進行比較,我們將該程序表示為樹狀圖。 樹中的每個節點代表一個函數的調用。 :-: ![](https://box.kancloud.cn/bbdad8419e533c12d5e6488d1a3d4364_544x724.png) 讓我們使用context包來修改該程序。由于現在可以使用context.Context的靈活性,所以我們引入一個有趣的場景。 假設genGreeting在放棄調用locale之前等待一秒——超時時間為1秒。如果printGreeting不成功,我們想取消對printFare的調用。 畢竟,如果我們不打聲招呼,說再見就沒有意義了: ``` func main() { var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) //1 defer cancel() wg.Add(1) go func() { defer wg.Done() if err := printGreeting(ctx); err != nil { fmt.Printf("cannot print greeting: %v\n", err) cancel() //2 } }() wg.Add(1) go func() { defer wg.Done() if err := printFarewell(ctx); err != nil { fmt.Printf("cannot print farewell: %v\n", err) } }() wg.Wait() } func printGreeting(ctx context.Context) error { greeting, err := genGreeting(ctx) if err != nil { return err } fmt.Printf("%s world!\n", greeting) return nil } func printFarewell(ctx context.Context) error { farewell, err := genFarewell(ctx) if err != nil { return err } fmt.Printf("%s world!\n", farewell) return nil } func genGreeting(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) //3 defer cancel() switch locale, err := locale(ctx); { case err != nil: return "", err case locale == "EN/US": return "hello", nil } return "", fmt.Errorf("unsupported locale") } func genFarewell(ctx context.Context) (string, error) { switch locale, err := locale(ctx); { case err != nil: return "", err case locale == "EN/US": return "goodbye", nil } return "", fmt.Errorf("unsupported locale") } func locale(ctx context.Context) (string, error) { select { case <-ctx.Done(): return "", ctx.Err() //4 case <-time.After(1 * time.Minute): } return "EN/US", nil } ``` 1. 在main函數中使用context.Background()建立個新的Context,并使用context.WithCancel將其包裹以便對其執行取消操作。 2. 在這一行上,如果從 printGreeting返回錯誤,main將取消context。 3. 這里genGreeting用context.WithTimeout包裝Context。這將在1秒后自動取消返回的context,從而取消它傳遞context的子進程,即語言環境。 4. 這一行返回為什么Context被取消的原因。 這個錯誤會一直冒泡到main,這會導致注釋2處的取消操作被調用。 這會輸出: ``` cannot print greeting: context deadline exceeded cannot print farewell: context canceled ``` 下面的圖中數字對應例子中的代碼標注。 :-: ![](https://box.kancloud.cn/7bc061feabfbe82b725e2ac92b27eead_447x592.png) 我們可以看到系統輸出工作正常。由于local設置至少需要運行一分鐘,因此genGreeting將始終超時,這意味著main會始終取消printFarewell下面的調用鏈。 請注意,genGreeting如何構建自定義的Context.Context以滿足其需求,而不必影響父級的Context。如果genGreeting成功返回,并且printGreeting需要再次調用,則可以在不泄漏genGreeting相關操作信息的情況下進行。這種可組合性使你能夠編寫大型系統,而無需在整個調用鏈中費勁心思解決這樣的問題。 我們可以在這個程序上進一步改進:因為我們知道locale需要大約一分鐘的時間才能運行,所以可以在locale中檢查是否給出了deadline。下面這個例子演示了如何使用context.Context的Deadline方法: ``` func main() { var wg sync.WaitGroup ctx, cancel := context.WithCancel(context.Background()) defer cancel() wg.Add(1) go func() { defer wg.Done() if err := printGreeting(ctx); err != nil { fmt.Printf("cannot print greeting: %v\n", err) cancel() } }() wg.Add(1) go func() { defer wg.Done() if err := printFarewell(ctx); err != nil { fmt.Printf("cannot print farewell: %v\n", err) } }() wg.Wait() } func printGreeting(ctx context.Context) error { greeting, err := genGreeting(ctx) if err != nil { return err } fmt.Printf("%s world!\n", greeting) return nil } func printFarewell(ctx context.Context) error { farewell, err := genFarewell(ctx) if err != nil { return err } fmt.Printf("%s world!\n", farewell) return nil } func genGreeting(ctx context.Context) (string, error) { ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() switch locale, err := locale(ctx); { case err != nil: return "", err case locale == "EN/US": return "hello", nil } return "", fmt.Errorf("unsupported locale") } func genFarewell(ctx context.Context) (string, error) { switch locale, err := locale(ctx); { case err != nil: return "", err case locale == "EN/US": return "goodbye", nil } return "", fmt.Errorf("unsupported locale") } func locale(ctx context.Context) (string, error) { if deadline, ok := ctx.Deadline(); ok { //1 if deadline.Sub(time.Now().Add(1*time.Minute)) <= 0 { return "", context.DeadlineExceeded } } select { case <-ctx.Done(): return "", ctx.Err() case <-time.After(1 * time.Minute): } return "EN/US", nil } ``` 1. 我們在這里檢查是否Context提供了deadline。如果提供了,而且我們的程序時間已經越過這個時間線,這時簡單的返回一個context包預設的錯誤——DeadlineExceeded。 雖然修改的部分很小,但它允許locale函數快速失敗,而不必像之前那樣等待一分鐘。在調用資源耗費較高的程序中,這樣做會節省大量時間。唯一的問題是,你得考慮deadline設置多久合適——這需要不斷的嘗試。 接下來我們討論context包的另一個用處:存儲和檢索附加于請求的數據包。請記住,當一個函數創建一個goroutine和Context時,它通常會啟動一個為請求提供服務的進程,并且子函數可能需要相關的請求信息。下面是一個示例: ``` func main() { ProcessRequest("jane", "abc123") } func ProcessRequest(userID, authToken string) { ctx := context.WithValue(context.Background(), "userID", userID) ctx = context.WithValue(ctx, "authToken", authToken) HandleResponse(ctx) } func HandleResponse(ctx context.Context) { fmt.Printf("handling response for %v (%v)", ctx.Value("userID"), ctx.Value("authToken"), ) } ``` 這會輸出: ``` handling response for jane (abc123) ``` 很簡單的用法。不過也是有限制的: * 你使用的key必須在Go中是可比較的,也就是說,== 和 != 必須能返回正確的結果。 * 返回值必須是并發安全的,這樣才能從多個goroutine訪問。 由于Context的鍵和值都被定義為interface{},所以當試圖檢索值時,我們會失去其類型安全性。基于此,Go建議在context中存儲和檢索值時遵循一些規則。 首先,推薦你在包中自行定義key的類型,這樣無論是否其他包執行相同的操作都可以防止context中的沖突。看下面這個例子: ``` type foo int type bar int m := make(map[interface{}]int) m[foo(1)] = 1 m[bar(1)] = 2 fmt.Printf("%v", m) ``` 這會輸出: ``` map[1:2 1:1] ``` 可以看到,雖然基礎值是相同的,但不同類型的信息會在map中區分它們。由于你為包定義的key類型未導出,因此其他包不會與你在包中生成的key沖突。 由于用于存儲數據的key是非導出的,因此我們必須導出執行檢索數據的函數。這很容易做到,因為它允許這些數據的使用者使用靜態的,類型安全的函數。 當你把所有這些放在一起時,你會得到類似下面的例子: ``` func main() { ProcessRequest("jane", "abc123") } type ctxKey int const ( ctxUserID ctxKey = iota ctxAuthToken ) func UserID(c context.Context) string { return c.Value(ctxUserID).(string) } func AuthToken(c context.Context) string { return c.Value(ctxAuthToken).(string) } func ProcessRequest(userID, authToken string) { ctx := context.WithValue(context.Background(), ctxUserID, userID) ctx = context.WithValue(ctx, ctxAuthToken, authToken) HandleResponse(ctx) } func HandleResponse(ctx context.Context) { fmt.Printf( "handling response for %v (auth: %v)", UserID(ctx), AuthToken(ctx), ) } ``` 這會輸出: ``` handling response for jane (auth: abc123) ``` 在本例中,我們使用類型安全的方法來從Context獲取值,如果消費者在不同的包中,他們不會知道或關心用于存儲信息的key。 但是,這種技術會造成隱患。 在前面的例子中,我們假設HandleResponse存在于另一個名為response的包中,假設ProcessRequest包位于名為pross的包中。 pross包必須導入response包才能調用HandleResponse,但HandleResponse無法訪問pross包中定義的訪問函數,因為導入會形成循環依賴關系。由于用于Context中存儲的key類型對于process包來說是私有的,所以response包無法檢索這些數據! 這迫使我們創建以從多個位置導入的數據類型為中心的包。 雖然這不是一件壞事,但它是需要注意的。 context包非常簡潔,但依然褒貶不一。在Go社區中一直存在爭議。該包的取消操作功能相當受歡迎,但是在Context中存儲任意數據的能力以及存儲數據的類型不安全的造成了一些分歧。 雖然我們已經部分減緩了訪問函數缺乏類型安全性的問題,但是仍然可以通過存儲不正確的類型來引入錯誤。然而,更大的問題在于,開發人員到底應該在Context的實例中存儲什么樣的數據。 在context包的文檔中這樣寫到: >使用context存儲值僅適用于傳輸進程和API的請求附加數據,而不用于將可選參數傳遞給函數。 該說明十分含糊,“傳輸進程和API的請求”實在太過寬泛。我認為最好的解讀方法是與開發組一起提出一些約定,并在代碼評審中檢查它們: 1. **數據應該是由進程傳遞的或與API相關**。如果你在進程的內存中生成數據,那么除非你也通過API傳遞數據,否則可能不是一個很好的候選應用程序。 2. **數據應該是不可變的**。如果可變,那么根據定義,你存儲的內容肯定不是來自請求。 3. **數據應指向系統簡單類型。**我們在上面已經討論了關于使用安全類型的包導入問題,這個結論是很明顯的。 4. **數據應該是純粹的數據,而不是某種類型的函數。**消費者的邏輯應該是消耗這些數據。 5. **數據應該有助于操作,而不是驅動操作。**如果你的算法根據context中包含或不包含的內容而有所不同,那么就違背了“不用于將可選參數傳遞”的初衷。 這些不是硬性規定,但如果你發現自己的程序與以上約定由沖突,則可能需要考慮是否存在隱患或使用context是否必要。 另一個需要考慮的方面是該數據在使用之前可能需要經過多少層。如果在接受數據的位置和使用位置之間有幾個框架和幾十個函數,你可以考慮使用日志,并將數據添加為參數;或者你更愿意將它放在Context中,從而創建一個不可見的依賴關系。每種方法都有優點,最終這由你和你的團隊做出決定。 我將這5條約定做成了表格,你可以將之作為參考: :-: ![](https://box.kancloud.cn/03fa98563db15fb9ea105f420da62a0d_681x234.jpg) 對于是否有必要使用context存儲值,這里并沒有簡單的答案,具體取決于你的業務、算法和團隊。 我留給你的最后建議是Context提供的取消功能非常有用,這樣輕便的功能如果不用實在是太可惜了。 * * * * * 學識淺薄,錯誤在所難免。我是長風,歡迎來Golang中國的群(211938256)就本書提出修改意見。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看