在 context.Context 中存儲(chǔ)數(shù)據(jù),或者說(shuō)使用上下文變量值(context values)是在 Go 中最有爭(zhēng)議的設(shè)計(jì)模式之一。在上下文中存儲(chǔ)值似乎看起來(lái)不錯(cuò),但是應(yīng)該將什么東西存儲(chǔ)為上下文變量值引起了廣泛的討論。
誠(chéng)實(shí)地說(shuō),當(dāng)我第一次使用上下文變量的時(shí)候, 顯得有點(diǎn)天真,使用的方式有點(diǎn)不合適,會(huì)讓每個(gè)人都會(huì)抱怨的。我曾經(jīng)使用他們只是來(lái)存儲(chǔ)每個(gè)請(qǐng)求相關(guān)的片段數(shù)據(jù),以便我的 Web 應(yīng)用的處理器(handlers)能夠訪問(wèn)到這些值。這種方式有些缺點(diǎn),但是總的來(lái)說(shuō)這樣很有效并且允許我快速寫(xiě)出我的應(yīng)用來(lái)。
過(guò)去幾個(gè)月,我試圖深入研究更多關(guān)于上下文變量值的使用方式,我已經(jīng)閱讀了很多文章、Reddit 評(píng)論、郵件列表的回復(fù),以及一切關(guān)于這個(gè)話題的討論,但是這仍然困擾著我。無(wú)論我多么深挖這個(gè)話題,仍然沒(méi)有人有意愿討論真正可行的解決方案。
當(dāng)然,每個(gè)人都可以提出為什么使用上下文變量值不好的理由,但是沒(méi)有一個(gè)替代方案能完全取代它。相反,這些替代方案仍然很粗糙,像“自定義 structs” 或 “閉包(closures)”的方案并沒(méi)有深入研究他們?cè)趶?fù)雜的應(yīng)用中如何實(shí)現(xiàn),或?qū)χ虚g件的可重用性可能如何影響。
現(xiàn)在我會(huì)對(duì)此問(wèn)題給出自己的見(jiàn)解。在這篇文章中我們會(huì)討論為什么使用上下文變量值會(huì)有問(wèn)題、一些沒(méi)有使用上下文變量值的替代方案和其適用場(chǎng)景,以及最終我們會(huì)討論如何正確使用上下文變量值以避免或減輕其潛在不足。但是,首先我想通過(guò)為什么開(kāi)發(fā)者總是輕易使用上下文變量值作出解釋,正如我認(rèn)為理解問(wèn)題如何被解決的和問(wèn)題的解決方案同樣重要。
開(kāi)始之前,讓我們制定下基本準(zhǔn)則
我盡力是我的例子清晰易懂,但是盡管我想要顯式強(qiáng)調(diào)那些并不是在請(qǐng)求的生命周期內(nèi)創(chuàng)建和銷毀的變量值 應(yīng)該從來(lái)不通過(guò) context.Value() 管理。不應(yīng)該存儲(chǔ)一個(gè)日志接收器(logger)在 context.Value() 里,如果它并不是專門創(chuàng)建出來(lái)只作用于這個(gè)請(qǐng)求的;同樣,不應(yīng)該在上下文變量值里存儲(chǔ)通用數(shù)據(jù)庫(kù)連接。
有可能下面這些是與單一請(qǐng)求相關(guān)的:例如,你可能創(chuàng)建一個(gè)日志接收器用于預(yù)先在消息里加上請(qǐng)求ID(request ID);或者對(duì)于每個(gè)需要訪問(wèn)數(shù)據(jù)庫(kù)連接的請(qǐng)求你可能創(chuàng)建單獨(dú)的數(shù)據(jù)庫(kù)事務(wù),正好可以關(guān)聯(lián)到上下文中。上面兩個(gè)例子很接近我認(rèn)為的正確使用上下文變量值的場(chǎng)景,但是關(guān)鍵是他們都只存活于請(qǐng)求的生命周期之內(nèi)。
為什么人們總是輕易使用上下文變量值
在解決這個(gè)問(wèn)題之前,我們需要知道為什么開(kāi)發(fā)者會(huì)覺(jué)得需要存一些數(shù)據(jù)到上下文變量中,當(dāng)然如果有其他方式更為容易他們也會(huì)使用的,因此使用未標(biāo)識(shí)類型的 context.WithValue() 函數(shù)和 context.Value() 方法有哪些好處呢?
簡(jiǎn)要回答就是通過(guò)使用上下文變量,我們能輕易地創(chuàng)建可重用和可互換的中間件函數(shù)。換句話說(shuō),我們可以定義一個(gè)中間件,接收 http.Handler 作為參數(shù),然后返回一個(gè) http.Handler,這種方式允許我們使用任何含有路由庫(kù)、中間件庫(kù)或任何其他功能庫(kù)的中間件的結(jié)果幫助我們處理 HTTP 請(qǐng)求,并且符合 http.Handler 接口。這也意味著如果想要測(cè)試不同的中間件實(shí)現(xiàn)或增加不同的函數(shù)功能,我們能輕易更換中間件函數(shù)(來(lái)做這件事)。
下面的例子更強(qiáng)有力地說(shuō)明了這個(gè)問(wèn)題。想象你正在構(gòu)建一個(gè) Web 服務(wù)器,然后你需要對(duì)每一個(gè)請(qǐng)求增加一個(gè)唯一 ID,這是一個(gè)很普遍的需求,滿足這個(gè)需求的一個(gè)實(shí)現(xiàn)是寫(xiě)一個(gè)生成唯一ID的函數(shù),然后把它存儲(chǔ)在關(guān)聯(lián)這個(gè)請(qǐng)求的上下文中。
var requestID = 0
func nextRequestID() int {
requestID++
return requestID
}
func addRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "request_id", nextRequestID())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
警告 上述代碼只用于示例,尚不足以用于生產(chǎn)環(huán)境
然后我們能使用任何路由包(例如 chi)利用這個(gè)函數(shù),或者我們能用標(biāo)準(zhǔn)庫(kù)中的 http.Handle() 函數(shù)利用它,如下:
func main() {
http.Handle("/", addRequestID(http.HandlerFunc(printHi)))
http.ListenAndServe(":3000", nil)
}
func printHi(w http.ResponseWriter, r *http.Request) {
fmt.Println(w, "Hi! Your request ID is:", r.Context().Value("request_id"))
}
現(xiàn)在你可能會(huì)問(wèn)自己,"如果我們需要一個(gè)請(qǐng)求 ID 的話,難道我們不能在代碼中調(diào)用下 nextRequestID()? 這個(gè)上下文變量看起來(lái)毫無(wú)必要"。
從技術(shù)角度來(lái)說(shuō),這是正確的。我們可以直接調(diào)用,如果你正在寫(xiě)一個(gè)相對(duì)簡(jiǎn)單的應(yīng)用我也建議你直接調(diào)用,但是如果邏輯突然變得更復(fù)雜了或者我們的應(yīng)用規(guī)模增大了的話會(huì)怎樣呢?如果我們不是需要一個(gè)請(qǐng)求ID而是需要驗(yàn)證用戶是否登錄,如果沒(méi)有登錄的話重定向到登錄頁(yè),如果登錄了的話查找用戶對(duì)象并且存儲(chǔ)下來(lái)以備之后使用我們?cè)撊绾翁幚砟兀?/p>
一個(gè)非常簡(jiǎn)單的認(rèn)證邏輯可能會(huì)是如下版本:
user := lookupUser(r)
if user == nil {
// No user so redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
return
}
現(xiàn)在不再只是在我們所有的處理器中加入一行代碼了,我們需要5行代碼。這看起來(lái)并不糟糕,但是如果我們想要在處理器中進(jìn)行四或五種不同的中間處理的時(shí)候會(huì)怎樣呢?就像生成一個(gè)唯一的請(qǐng)求 ID,創(chuàng)建一個(gè)日志接收器利用這個(gè)請(qǐng)求 ID,驗(yàn)證用戶是否登陸,驗(yàn)證用戶是否是管理員?
那挺起來(lái)像是在多個(gè)處理器中不斷重復(fù)的糟糕代碼,也非常容易出錯(cuò)。不合理的訪問(wèn)權(quán)限控制一次又一次地出現(xiàn)在各種榜單上,比如 OWASP TOP 10,最終也更容易出錯(cuò)。一個(gè)開(kāi)發(fā)者可能會(huì)忘記在一個(gè)處理器中驗(yàn)證一個(gè)用戶是否是管理員,我們突然就有了一個(gè)只能管理員訪問(wèn)的頁(yè)面暴露給普通用戶,當(dāng)然誰(shuí)也不希望發(fā)生這種事。
與其產(chǎn)生這種缺陷,許多開(kāi)發(fā)者更喜歡在他們的路由函數(shù)中使用中間件來(lái)避免這樣的錯(cuò)誤。這也幫助應(yīng)用更易于清晰地理解是否需要認(rèn)證。最終,這也易于解釋他們的代碼,因?yàn)槟隳茌p易判斷出是否用戶對(duì)象會(huì)預(yù)期出現(xiàn)。
下面的例子展示了你可能使用上面的認(rèn)證邏輯驗(yàn)證當(dāng)訪問(wèn) /dashboard/ 前綴的路徑時(shí),用戶是否登錄。一個(gè)相似的方法可能被用于當(dāng)訪問(wèn) /admin/ 前綴的路徑時(shí), 用戶是否具有管理員權(quán)限。
func requireUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := lookupUser(r)
if user == nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
dashboard := http.NewServeMux()
dashboard.HandleFunc("/dashboard/hi", printHi)
dashboard.HandleFunc("/dashboard/bye", printBye)
mux := http.NewServeMux()
// All routes that start with /dashboard/ require that a user is authenticated using the requireUser middleware
mux.Handle("/dashboard/", requireUser(dashboard))
mux.HandleFunc("/", home)
http.ListenAndServe(":3000", addRequestID(mux))
}
你只能本地運(yùn)行-- Web 服務(wù)器不允許在 Go Playground 運(yùn)行
上下文變量適合在哪引入到我們的認(rèn)證中間件呢?當(dāng)認(rèn)證用戶的時(shí)候(取決于你的認(rèn)證策略)你可能最終會(huì)找出這個(gè)用戶對(duì)象來(lái),盡管你已經(jīng)知道這個(gè)用戶了但可能會(huì)不得不再查一遍數(shù)據(jù)庫(kù),因此我們能使用上下文變量存儲(chǔ)這個(gè)用戶對(duì)象以備未來(lái)之用。
很干凈,不是嗎?因此如果上下文變量允許我們做像讓一個(gè)用戶在我們的處理器中可用這種如此酷的操作時(shí)它怎么又讓人難以接受了呢?
使用上下文變量的缺點(diǎn)
使用 context.WithValue() 和 context.Value() 最大的缺點(diǎn)時(shí)你正在主動(dòng)選擇放棄一些信息和編譯時(shí)類型檢查。你可能利用這種方法寫(xiě)出了通用型代碼,但是也有一個(gè)值得思考的問(wèn)題。我們處于某種原因在函數(shù)中使用顯式類型參數(shù),因此任何時(shí)候我們選擇放棄放棄一些信息,這些信息可能值得考慮是否有那么大的收益。
我無(wú)法回答你這個(gè)問(wèn)題,因?yàn)閷?duì)于不同的項(xiàng)目結(jié)果可能不一樣,但是在做決定之前,你應(yīng)該確保真正理解了你要放棄的是什么。
函數(shù)需要的數(shù)據(jù)被隱藏了
當(dāng)使用上下文變量的時(shí)候,我最大的關(guān)切是難以確定函數(shù)需要處理的數(shù)據(jù)。我們不會(huì)寫(xiě)接收任意的 maps 并且期望用戶放入使我們的函數(shù)能夠工作的各種變量的函數(shù),同樣我們一半不應(yīng)該為自己的 Web 應(yīng)用寫(xiě)這樣的處理器。
func bad(m map[interface{}]interface{}) {
// we don't expect m to have the keys "user" and
// "request_id" for our code to work. If we needed those
// we would define our function like the one below.
}
func good(user User, requestID int) {
// Now it is clear that this function requires a user and
// a request ID.
}
對(duì)于一些像 editUser() 這樣的函數(shù),很明顯像用戶對(duì)象的數(shù)據(jù)要呈現(xiàn)出來(lái),但是大部分時(shí)候,函數(shù)定義不足夠,因此作為開(kāi)發(fā)者,我們不能期望別人根據(jù)函數(shù)的名字就能識(shí)別出哪些參數(shù)是必要的。相反,我們應(yīng)該明確地在代碼中指出來(lái)以更易于閱讀和維護(hù)。我們的 Web 應(yīng)用,尤其是哪些處理器函數(shù)和中間件函數(shù),也不應(yīng)該有任何的不同。我們不應(yīng)該傳遞個(gè) context 對(duì)象,期望他們從中取出他們需要的所有數(shù)據(jù)。
我們失去了編譯時(shí)類型安全保障
上下文變量值本質(zhì)上是一個(gè) interface{}, interface{} 對(duì)(請(qǐng)查看源碼)。這也是為什么我們?cè)试S存儲(chǔ)任意數(shù)據(jù)而不會(huì)產(chǎn)生編譯時(shí)警告的原因--鍵值都被定義為空類型,接收任何字面量。
這種做法的好處是 context.Context 任意的實(shí)現(xiàn)都能存儲(chǔ)適用于特定應(yīng)用的各種類型數(shù)據(jù)。缺點(diǎn)是我們無(wú)法指望編譯器能替我們分辨是否產(chǎn)生了錯(cuò)誤。尤其是在我們的程序中當(dāng)我們存儲(chǔ)字符串代替 User 對(duì)象時(shí),程序仍然能編譯通過(guò),除非我們使用類型推斷然后就崩潰了。有幾種最小化風(fēng)險(xiǎn)的方式,但是開(kāi)發(fā)者總是免不了出錯(cuò),而這只會(huì)在運(yùn)行時(shí)出現(xiàn)。
有什么方法避免嗎?對(duì)于初學(xué)者,不要根據(jù)我們?cè)谝陨侠又械姆绞绞褂蒙舷挛淖兞?,而是使用特定類型。除此之外?em>“packages should define keys as an unexported type to avoid collisons.” --來(lái)自 Go 源碼。這意味著在 context.WithValue() 或 context.Value() 中任何以自定義類型作為作為鍵的變量調(diào)用不要在定義它的包外分享它。例如:
type userCtxKeyType string
const userCtxKey userCtxKeyType = "user"
func WithUser(ctx context.Context, user *User) context.Context {
return context.WithValue(ctx, userCtxKey, user)
}
func GetUser(ctx context.Context) *User {
user, ok := ctx.Value(userCtxKey).(*User)
if !ok {
// Log this issue
return nil
}
return user
}
除了使用 getterr 和 settings 和非到處鍵,確保總是使用類型檢查的較長(zhǎng)的方式(有兩個(gè)返回值的形式)。這會(huì)幫你避免在代碼中產(chǎn)生不必要的崩潰,并且給你處理異常結(jié)果的機(jī)會(huì)。
如果你遵循以上建議,一些源于類型安全的缺陷將會(huì)被組織,因此我們不會(huì)在文章的剩余部分討論太多這個(gè)特殊的問(wèn)題,但是一定要警惕這個(gè)問(wèn)題。這并不是編譯器會(huì)幫你解決的問(wèn)題,而是作為開(kāi)發(fā)者、測(cè)試人員和代碼審查人員應(yīng)該要處理的錯(cuò)誤。
context.Value() 的替代方案
我猜有很多人會(huì)說(shuō) "我使用方案 X 并且運(yùn)行得不錯(cuò)。為什么你要寫(xiě)這篇文章?"。我不會(huì)試圖辯論你的方案時(shí)錯(cuò)的,但是我并不真的相信有一個(gè)放之四海而皆準(zhǔn)的解決方案,因此本文的剩余部分將專注于幾個(gè)我認(rèn)為有用的替代方案。我也會(huì)盡量談下他們覆蓋不到的方面/領(lǐng)域,以便你能了解到適用于自己使用場(chǎng)景的合適方案。
代碼復(fù)制-需要時(shí)再查抄數(shù)據(jù)
我們簡(jiǎn)要討論了什么時(shí)候和為什么開(kāi)發(fā)者會(huì)使用上下文變量,但是我想在這里也談?wù)勚皼](méi)談的內(nèi)容。當(dāng)你寫(xiě)一個(gè)相對(duì)簡(jiǎn)單的額應(yīng)用時(shí),或者及時(shí)你在建一個(gè)復(fù)雜的應(yīng)用時(shí),你也會(huì)幾乎總是從查找你需要的數(shù)據(jù)開(kāi)始。
這正是這本書(shū)所談的內(nèi)容 -- 使用 Go 進(jìn)行 Web 開(kāi)發(fā)。在這本書(shū)中,我們一開(kāi)始直接在處理器內(nèi)部寫(xiě)所需邏輯,然后將邏輯外移到可能每個(gè)處理器都需要調(diào)用的可重用函數(shù)中。例如,與其使用之前討論過(guò)的 requireUser() 中間件,我們不如寫(xiě)一個(gè)函數(shù),然后被 http.Handler 調(diào)用,如下所示:
func printHi(w http.ResponseWriter, r *http.Request) {
user, err := requireUser(w, r)
if err != nil {
return
}
// do stuff w/ user
}
func requireUser(w http.ResponseWriter, r *http.Request) (*User, error) {
user := lookupUser(r)
if user == nil {
// No user so redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
return nil, errors.New("User isn't logged in")
}
return user, nil
}
這將會(huì)產(chǎn)生一些代碼復(fù)制,但是還能接受。我們限制了復(fù)制代碼的行數(shù),只有一點(diǎn)復(fù)制要比增加額外的復(fù)雜度搖號(hào)。使這個(gè)產(chǎn)生問(wèn)題的情況是它可能會(huì)演變成大量的代碼復(fù)制,比如可能在許多不同的處理器中需要調(diào)用五到六個(gè)函數(shù)。那經(jīng)常意味著你可能需要放棄這個(gè)方案并尋找新的方法。
閉包和自定義函數(shù)說(shuō)明
另一個(gè)普遍的解決方案是寫(xiě)一些函數(shù),這些函數(shù)能夠查找必要的數(shù)據(jù),然后利用這些數(shù)據(jù)調(diào)用你自定義的函數(shù)。為了讓這個(gè)方法淺顯易懂,我們經(jīng)常使用閉包,包裝相似的處理器來(lái)創(chuàng)建我們的 http.Hander,這些處理器需要相同的數(shù)據(jù)。
func requireUser(fn func(http.ResponseWriter, *http.Request, *User)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user := lookupUser(r)
if user == nil {
// No user so redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
return
}
fn(w, r, user)
}
}
func printUser(w http.ResponseWriter, r *http.Request, user *User) {
fmt.Fprintln(w, "User is:", user)
}
func main() {
http.HandleFunc("/user", requireUser(printUser))
http.ListenAndServe(":3000", nil)
}
很明顯 printUser() 預(yù)期需要一個(gè)用戶對(duì)象,通過(guò)使用 requireUser() 函數(shù)我們能見(jiàn)任何函數(shù) func(http.ResponseWriter, *http.Request, *User) 輕松轉(zhuǎn)變?yōu)?http.Handler。
我發(fā)現(xiàn)這個(gè)方案意外適用于在所有的處理器中你需要相似的特定于上下文的數(shù)據(jù)的場(chǎng)景。例如,如果你需要請(qǐng)求 ID,一個(gè)使用請(qǐng)求 ID 和用戶對(duì)象的日志接收器時(shí),你能使用這個(gè)方案將所有的函數(shù)轉(zhuǎn)變?yōu)?http.Handler。
一個(gè)人為的案例如下:
// requireUser and printUser don't change
func printReqID(w http.ResponseWriter, r *http.Request, requestID int) {
fmt.Fprintln(w, "RequestID is:", requestID)
}
func printUserAndReqID(w http.ResponseWriter, r *http.Request, requestID int, user *User) {
printReqID(w, r, requestID)
printUser(w, r, user)
}
func addRequestID(fn func(http.ResponseWriter, *http.Request, int)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fn(w, r, nextRequestID())
}
}
func requireUserWithReqID(fn func(http.ResponseWriter, *http.Request, int, *User)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
addRequestID(func(w http.ResponseWriter, r *http.Request, reqID int) {
requireUser(func(w http.ResponseWriter, r *http.Request, user *User) {
fn(w, r, reqID, user)
})(w, r)
})(w, r)
}
}
func main() {
http.HandleFunc("/user", requireUser(printUser))
http.HandleFunc("/reqid", addRequestID(printReqID))
http.HandleFunc("/both", requireUserWithReqID(printUserAndReqID))
http.ListenAndServe(":3000", nil)
}
這個(gè)方法的不足是當(dāng)你需要在每個(gè)處理器中需要不同的數(shù)據(jù),這種方法會(huì)隨著應(yīng)用規(guī)模的增加而變得越來(lái)越復(fù)雜。同時(shí),這種方法消除了在路由代碼引入前運(yùn)行中間件的能力,使得類似“所有起于 /dashboard/ 的路徑必須要求用戶登錄”的方案更難以表達(dá)。
盡管有這些缺陷,我仍然認(rèn)為這種方案值得考慮,除非它確實(shí)本身成為了一個(gè)問(wèn)題。但是這并不是說(shuō),”我們最終需要特定路由的中間件“,然后放棄這種方案;而是,除非你確實(shí)遇到了它不適宜的場(chǎng)景否則你應(yīng)該盡量使用它。
當(dāng)不適宜的場(chǎng)景最終發(fā)生時(shí),我有一個(gè)想談?wù)劦姆桨浮?/p>
處理上下文變量的模糊性
最終我轉(zhuǎn)向的方案是在剛才回顧的方案和上下文變量的融合處理?;舅枷胧鞘褂蒙舷挛淖兞亢?http.Handler 函數(shù),如本文開(kāi)始的示例,但是在我們確實(shí)需要上下文變量提供的數(shù)據(jù)之前,我們獻(xiàn)血一個(gè)函數(shù)從上下文變量中拉取數(shù)據(jù),傳遞給需要它的函數(shù)。昨晚這些之后,我們調(diào)用的函數(shù)應(yīng)該永不需從上下文變量中拉去額外的數(shù)據(jù),否則會(huì)影響到應(yīng)用的流程。
通過(guò)以上做法,我們幫助消除了使用 context.Value() 獲取數(shù)據(jù)所帶來(lái)的模糊性。我們不必去考慮這個(gè)問(wèn)題,“一些嵌套函數(shù)調(diào)用會(huì)預(yù)期上下文中要預(yù)設(shè)某些變量嗎?”,因?yàn)樗械臄?shù)據(jù)總是將從上下文變量中抽取出來(lái)。
最好將此情況用案例的方式描述,因此我們?cè)僖淮问褂昧?addRequestID() 中間件函數(shù)和一個(gè)簡(jiǎn)單的 home 處理器,在這個(gè)案例中并不明顯的是,logger 也是被設(shè)計(jì)為作用于單個(gè)請(qǐng)求的日志接收器。
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
http.ListenAndServe(":3000", addRequestID(addLogger(mux)))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqID := GetRequestID(ctx)
logger := GetLogger(ctx)
home(w, r, reqID, logger)
}
func home(w http.ResponseWriter, r *http.Request, requestID int, logger *Logger) {
logger.Println("Here is a log")
fmt.Fprintln(w, "Homepage...")
}
使這個(gè)方案特別吸引我的是,這個(gè)方案非常容易重構(gòu)那些之前使用了上下文變量值的代碼,并充分利用這個(gè)特性。你不必剝離很多代碼,也不必一次重構(gòu)一切骨架,相反,可以通過(guò)一分為二的形式來(lái)分離原來(lái)的單個(gè)函數(shù)--一個(gè) http.Handler 獲得數(shù)據(jù),另一個(gè)函數(shù)使用這些數(shù)據(jù),處理原來(lái)的函數(shù)的那些業(yè)務(wù)邏輯。
這真的和一開(kāi)始的例子有所不同?
最終,這個(gè)方案并沒(méi)有和我們回顧的其他方案有很大不同。最值得注意的是,這似乎和我們一開(kāi)始使用上下文變量值的例子近乎一致,但是兩者之間還是又一些微小但是非常重要的不同之處的。
通過(guò)總是使用非導(dǎo)出上下文鍵值和其 getter、setter 函數(shù),我們有效避免了分配給上下文變量錯(cuò)誤類型的風(fēng)險(xiǎn),限制了我們的數(shù)據(jù)無(wú)法被設(shè)置的風(fēng)險(xiǎn)。及時(shí)數(shù)據(jù)沒(méi)有被設(shè)置,我們的 getter 函數(shù)仍然可以試圖去處理它,當(dāng)他們需要將處理邏輯延遲交由處理器處理時(shí),能夠選擇返回一個(gè)錯(cuò)誤。
第二個(gè)變化更為微妙;通過(guò)將我們的函數(shù)一分為二,代碼更為清晰地展示了我們預(yù)期要設(shè)置的數(shù)據(jù)。最終,任何查看 home 函數(shù)的人將無(wú)需通過(guò)閱讀代碼就知道我們需要設(shè)置數(shù)據(jù)。這是一個(gè)對(duì)于預(yù)期能夠從 context.Value() 中抽取數(shù)據(jù)方案顯著的改善,這個(gè)方案無(wú)需再給其他人任何這種期望的暗示(而不是明示)。
簡(jiǎn)而言之,只要簡(jiǎn)單地將我們的處理器和中間件劃分成兩個(gè)函數(shù)就可以將我們模糊的需求轉(zhuǎn)變?yōu)榍逦揖唧w,幫助新人更快熟悉代碼,也使代碼更易于維護(hù)。
結(jié)論...
本文沒(méi)有討論到一個(gè)最終方案,那就是在你的應(yīng)用和中間件中創(chuàng)建一個(gè)屬于自己的自定義 Context。這最終看起來(lái)像某些類似于 “閉包和自定義函數(shù)說(shuō)明” 的部分,但是我們有一個(gè)定義好的中等大小的上下文,將其傳遞給每個(gè)處理器。
這個(gè)巨型上下文(我喜歡這樣叫它)有自己的優(yōu)缺點(diǎn),可能經(jīng)常有所幫助,但是我并沒(méi)有在這兒討論它因?yàn)槲蚁朐谑崂硭霸囼?yàn)更多的可能性。我懷疑最終會(huì)在接下來(lái)幾周再寫(xiě)一篇文章討論其細(xì)節(jié)。
同時(shí),請(qǐng)牢記上面的任何方案都有缺陷。一些可能會(huì)導(dǎo)致代碼復(fù)制,另一些會(huì)將類型檢查延遲到運(yùn)行時(shí)處理,一些限制了你在不同的多處理器中簡(jiǎn)單插入中間件的能力。最終,你需要自己決定最適合于自己的方案。
無(wú)關(guān)于你選用的路由組件,請(qǐng)記住在代碼審查中保持警惕,確保其他人也要關(guān)注上下文變量值。
參考資料
我的博客即將同步至騰訊云開(kāi)發(fā)者社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=ix804iofhkd6