- 原文地址:Why context.Value matters and how to improve it
- 原文做者:Axel Wagner
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:星辰
- 校對者:lsvih,leviding
以爲文章太長能夠看這裏:我認爲 context.Value 解決了描寫無狀態這個重要用例 - 並且它的抽象仍是可擴展的。我相信
dynamic scoping#Dynamic_scoping) 能夠提供一樣的好處,同時解決對當前實現的大多數爭議。 所以,我將試圖從其具體實施到它的潛在問題進行討論。html
這篇博文有點長。我建議你跳過你以爲無聊的部分前端
最近這篇博文已經在幾個 Go 論壇上被探討過。它提出了幾個很好的論據來反對 context-package:react
context.Context
。這引發了 API 的混亂的同時還須要普遍的深刻修改 API,好比,ctx context.Context
會在入參中重複出現屢次。context.Value
不是靜態類型安全的,老是須要類型斷言。然而,在對 context 被設計來解決的問題的探討中,我認爲它作的不夠好,它主要探討的是取消機制,而對於 Context.Value
只進行了簡單的說明。android
[…] 設計你的 API,而不考慮 ctx.Value,可讓你永遠有選擇的餘地。ios
我認爲這個問題提的很不公正。想要關於 context.Value 的論證是理性的,須要雙方都參與進來考慮。不管你對當前 API 的見解如何:經驗豐富且智慧的工程師們在鄭重思考後以爲 Context.Value
是須要的,這意味着這個問題值得被關注。git
我將嘗試描述我對 content 包在嘗試解決什麼樣的問題的見解,目前存在哪些替代方案,以及爲何我找到了它們的不足之處,同時我正在爲一種將來的語言演進描述一種替代設計。它將解決相同的問題,同時避免一些學習 content 包時的負面影響。但這並非意味着它將會是 Go 2 的一個具體方案(我在這的考慮還爲時過早),只是爲了表現一種平衡的觀點,使得語言設計界有更多可能,更容易考慮到所有可能。程序員
這些 context 要去解決的問題是將問題抽象爲獨立執行的、由系統的不一樣部分處理的單元,以及如何將數據做用域應用到這些單元的某一個上。很難清楚的定義我說的這些抽象,因此我會給出一些例子。github
全部這些狀況的想法都是想經過減小共享狀態的同時保持資源的共享來增長擴展性(不管是分佈在機器之間,線程之間或者只是代碼中)。golang
Go 採起了一個措施來靠近這個特性。但它不會像某些函數式編程語言那樣禁止或者阻礙可變狀態。它容許在線程之間共享內存並與互斥體進行同步,而不徹底依賴於通道。可是它也絕對想成爲一種(或惟一)編寫現代可擴展服務的語言。所以,它須要成爲一種很好的語言來編寫無狀態的服務,它須要至少在必定程度上可以達到請求隔離級別而不是進程隔離。web
(附註:這彷佛是上述文章做者的聲明,他聲稱上下文主要對服務做者有用。我不一樣意。通常抽象發生在不少層面。好比 GUI 的一次點擊就像這個請求的抽象同樣,做爲一個 HTTP 請求。)
這帶來了能在請求級別存儲一些數據的需求。一個簡單的例子就是RPC 框架中的身份驗證。不一樣的請求將具備不一樣的功能。若是一個請求來自於管理員,它應該比未認證用戶擁有更高的權限。這是從根本上的請求做用域內的數據而不是過程,服務或者應用做用域。RPC 框架應該將這些數據視爲不透明的。它是應用程序特指的,不只是數據看起來有多詳細,還有什麼樣的數據是須要的。
就像一個 HTTP 代理或者框架不須要知道它不適用的請求參數和頭同樣,RPC 框架不該該知道應用程序所須要的請求做用域的數據。
讓咱們來試試在不引入上下文的狀況下解決(可能)這個問題,例如,咱們來看看編寫 HTTP 中間件的問題。咱們但願以裝飾一個 http.Handler(或其變體)的方式來容許裝飾器附加數據給請求。
爲了得到靜態類型安全性,咱們能夠試着添加一些類型給咱們的 handlers。咱們能夠有一個包含咱們想要保留請求做用域內全部數據的類型,並經過咱們的 handler 傳遞:
type Data struct {
Username string
Log *log.Logger
// …
}
func HandleA(d Data, res http.ResponseWriter, req *http.Request) {
// …
d.Username = "admin"
HandleB(d, req, res)
// …
}
func HandleB(d Data, res http.ResponseWriter, req *http.Request) {
// …
}複製代碼
可是,這將阻止咱們編寫可重用的中間件。任何這樣的中間件都須要用 HandleA
包好。可是由於它將是可重用的,因此它不該該知道參數的類型。有能夠將 Data
參數設置爲 interface{}
類型,並須要類型斷言。但這不容許中間件注入本身的數據。你可能以爲接口類型斷言能夠解決這個問題,可是它們還有它們本身的一堆問題沒解決。因此結果是,這種方法不能帶給你真正的類型安全。
咱們能夠存儲由請求鍵入的狀態。例如身份驗證中間件能夠實現
type Authenticator struct {
mu sync.Mutex
users map[*http.Request]string
wrapped http.Handler
}
func (a *Authenticator) ServeHTTP(res http.ResponseWriter, req *http.Request) {
// …
a.mu.Lock()
a.users[req] = "admin"
a.mu.Unlock()
defer func() {
a.mu.Lock()
delete(a.users, req)
a.mu.Unlock()
}()
a.wrapped.ServeHTTP(res, req)
}
func (a *Authenticator) Username(req *http.Request) string {
a.mu.Lock()
defer a.mu.Unlock()
return a.users[req]
}複製代碼
這與上下文相比有一些好處:
然而,咱們已經認同它的共享可變狀態和相關的鎖爭用。若是其中一箇中間處理程序決定建立一個新的請求,那麼可使用一種很微妙的方式破解,好比 http.StripPrefix 將要作的那樣。
最後咱們可能會考慮將這些數據存儲在 *http.Request 自己中,例如經過將其添加爲字符串的 URL parameter,但這也有幾個缺點。事實上,它基本檢測到了 context.Context
的每一個單獨 item 的缺點。表達式是一個鏈表。即便有那樣的優勢,它的線程安全也沒法忽略,若是該請求被傳遞給不一樣的 goroutine 中的程序處理,咱們會遇到麻煩。
(附註:全部的這一切也使咱們瞭解了爲何 context 包被使用鏈表的方式實現。它容許存儲在其中的全部數據都是隻讀的,所以確定線程安全,在上下文中保存的共享狀態永遠不會出現鎖爭用,由於壓根不須要鎖。)
因此咱們看到,解決這個問題是很是困難的(若是能夠解決),實如今獨立執行的處理程序附加數據給請求時,也是優於 context.Value
的。不管是否相信這個問題值得解決,它都是有爭議的。可是若是你想得到這種可擴展的抽象,你將不得不依賴於相似於 context.Value
的東西。
不管你如今相信 context.Value
確實無用,或者你仍有疑慮:在這兩種狀況下,這些缺點顯然都不能被忽略。可是咱們能夠試着找到一些方法去改進它。消除一些缺點,同時保持其有用的屬性。
一種方法(在 Go 2 中)將是引入動態做用域#Dynamic_scoping)變量。語義上,每一個動態做用域變量表示一個單獨的棧,每次你改變它的值,新的值被推入棧。在你方法返回以後它會再次出棧。好比:
// 讓咱們創造點語法,只一點點哦。
dyn x = 23
func Foo() {
fmt.Println("Foo:", x)
}
func Bar() {
fmt.Println("Bar:", x)
x = 42
fmt.Println("Bar:", x)
Baz()
fmt.Println("Bar:", x)
}
func Baz() {
fmt.Println("Baz:", x)
x = 1337
fmt.Println("Baz:", x)
}
func main() {
fmt.Println("main:", x)
Foo()
Bar()
Baz()
fmt.Println("main:", x)
}
// 輸出:
main: 23
Foo: 23
Bar: 23
Bar: 42
Baz: 42
Baz: 1337
Bar: 42
Baz: 23
Baz: 1337
main: 23複製代碼
我想到這裏的語義有一些須要注意的地方。
dyn
這個類型。鑑於沒有辦法引用不一樣功能的本地標識符,這彷佛是合乎邏輯的。context.Context
同樣),共享的數據將是隻讀的。頭指針須要儲存在某種類型的 goroutine-local 的存儲中。這樣,寫入只會修改此本地存儲(和全局堆),所以不須要特地的同步本次修改。foo.A
修改了一個動態的 bar.X
,那麼這個修改對後來的 foo.A
的被調用者都是不可見的,無論它們是否在 bar
內。dyn x *int
來讓可變狀態傳遞。defer
的機制。foo.X = Y
來看,你沒法判斷 foo.X
是否有動態做用域。就我我的而言,我會經過從語言中移除包做用域變量來解決此問題。它們仍然能夠經過聲明一個動態做用域指針,而不修改它來模仿。那麼它的指針就是一個共享變量。可是,大多數包做用域變量的用法就僅僅是使用動態做用域變量。將此設計同 context
的一系列缺點進行比較是頗有啓發性的。
dyn
聲明都有一個明確的類型。dyn
聲明都有它本身的鏈,只有頭指針須要被操做。最後,我想提一下取消機制。索然在上述文章中,做者提到了不少關於取消機制的內容,但我迄今爲止都忽略了它。那是由於我相信取消機制在好的 context.Value
實現之上是能夠實現的。好比:
// $GOROOT/src/done
package done
// 噹噹前執行的上下文(好比請求)被取消時,C 被關閉。
dyn C <-chan struct{}
// 當 C 被關閉或者取消被調用時,CancelFunc 返回一個關閉的通道。
func CancelFunc() (c <-chan struct, cancel func()) {
// 咱們不能在這改變 C,應爲它的做用域是動態的,這就是爲何咱們返回一個調用者應該儲存的新通道。
ch := make(chan struct)
var o sync.Once
cancel = func() { o.Do(close(ch)) }
if C != nil {
go func() {
<-C
cancel()
}()
}
return ch, cancel
}
// $GOPATH/example.com/foo
package foo
func Foo() {
var cancel func()
done.C, cancel = done.CancelFunc()
defer cancel()
// Do things
}複製代碼
這種取消機制如今能夠從任何想要的庫中使用,而不須要確認其 API 明確支持。這讓它能夠很簡單的追加取消的能力。
不管你喜不喜歡這個設計,至少咱們不該該急於要求刪除 context
包。刪除它只是一種可能解決它缺點的方法之一。
若是移除 context.Context
的這一天真的來了,咱們應該問的問題是『咱們是否想要有一個規範的方法去管理請求做用域的值,其代價又是什麼』。只有這樣咱們才能開始探討最佳實現會是什麼樣的,或者是否移除它。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。