在 context.Context 中存儲數(shù)據(jù),或者說使用上下文變量值(context values)是在 Go 中最有爭議的設(shè)計模式之一顿颅。在上下文中存儲值似乎看起來不錯默垄,但是應(yīng)該將什么東西存儲為上下文變量值引起了廣泛的討論岛杀。
誠實地說躯嫉,當(dāng)我第一次使用上下文變量的時候, 顯得有點天真,使用的方式有點不合適稳诚,會讓每個人都會抱怨的哗脖。我曾經(jīng)使用他們只是來存儲每個請求相關(guān)的片段數(shù)據(jù),以便我的 Web 應(yīng)用的處理器(handlers)能夠訪問到這些值扳还。這種方式有些缺點才避,但是總的來說這樣很有效并且允許我快速寫出我的應(yīng)用來。
過去幾個月氨距,我試圖深入研究更多關(guān)于上下文變量值的使用方式桑逝,我已經(jīng)閱讀了很多文章、Reddit 評論俏让、郵件列表的回復(fù)楞遏,以及一切關(guān)于這個話題的討論,但是這仍然困擾著我首昔。無論我多么深挖這個話題寡喝,仍然沒有人有意愿討論真正可行的解決方案。
當(dāng)然沙廉,每個人都可以提出為什么使用上下文變量值不好的理由,但是沒有一個替代方案能完全取代它臼节。相反撬陵,這些替代方案仍然很粗糙,像“自定義 structs” 或 “閉包(closures)”的方案并沒有深入研究他們在復(fù)雜的應(yīng)用中如何實現(xiàn)网缝,或?qū)χ虚g件的可重用性可能如何影響巨税。
現(xiàn)在我會對此問題給出自己的見解。在這篇文章中我們會討論為什么使用上下文變量值會有問題粉臊、一些沒有使用上下文變量值的替代方案和其適用場景草添,以及最終我們會討論如何正確使用上下文變量值以避免或減輕其潛在不足。但是扼仲,首先我想通過為什么開發(fā)者總是輕易使用上下文變量值作出解釋远寸,正如我認(rèn)為理解問題如何被解決的和問題的解決方案同樣重要。
開始之前屠凶,讓我們制定下基本準(zhǔn)則
我盡力是我的例子清晰易懂驰后,但是盡管我想要顯式強(qiáng)調(diào)那些并不是在請求的生命周期內(nèi)創(chuàng)建和銷毀的變量值 應(yīng)該從來不通過 context.Value()
管理。不應(yīng)該存儲一個日志接收器(logger)在 context.Value()
里矗愧,如果它并不是專門創(chuàng)建出來只作用于這個請求的灶芝;同樣,不應(yīng)該在上下文變量值里存儲通用數(shù)據(jù)庫連接。
有可能下面這些是與單一請求相關(guān)的:例如夜涕,你可能創(chuàng)建一個日志接收器用于預(yù)先在消息里加上請求ID(request ID)犯犁;或者對于每個需要訪問數(shù)據(jù)庫連接的請求你可能創(chuàng)建單獨的數(shù)據(jù)庫事務(wù),正好可以關(guān)聯(lián)到上下文中女器。上面兩個例子很接近我認(rèn)為的正確使用上下文變量值的場景酸役,但是關(guān)鍵是他們都只存活于請求的生命周期之內(nèi)。
為什么人們總是輕易使用上下文變量值
在解決這個問題之前晓避,我們需要知道為什么開發(fā)者會覺得需要存一些數(shù)據(jù)到上下文變量中簇捍,當(dāng)然如果有其他方式更為容易他們也會使用的,因此使用未標(biāo)識類型的 context.WithValue()
函數(shù)和 context.Value()
方法有哪些好處呢俏拱?
簡要回答就是通過使用上下文變量暑塑,我們能輕易地創(chuàng)建可重用和可互換的中間件函數(shù)。換句話說锅必,我們可以定義一個中間件事格,接收 http.Handler
作為參數(shù),然后返回一個 http.Handler
搞隐,這種方式允許我們使用任何含有路由庫驹愚、中間件庫或任何其他功能庫的中間件的結(jié)果幫助我們處理 HTTP 請求,并且符合 http.Handler
接口劣纲。這也意味著如果想要測試不同的中間件實現(xiàn)或增加不同的函數(shù)功能逢捺,我們能輕易更換中間件函數(shù)(來做這件事)。
下面的例子更強(qiáng)有力地說明了這個問題癞季。想象你正在構(gòu)建一個 Web 服務(wù)器劫瞳,然后你需要對每一個請求增加一個唯一 ID,這是一個很普遍的需求绷柒,滿足這個需求的一個實現(xiàn)是寫一個生成唯一ID的函數(shù)志于,然后把它存儲在關(guān)聯(lián)這個請求的上下文中。
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)利用這個函數(shù)伺绽,或者我們能用標(biāo)準(zhǔn)庫中的 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)在你可能會問自己嗜湃,"如果我們需要一個請求 ID 的話奈应,難道我們不能在代碼中調(diào)用下 nextRequestID()
? 這個上下文變量看起來毫無必要"购披。
從技術(shù)角度來說钥组,這是正確的。我們可以直接調(diào)用今瀑,如果你正在寫一個相對簡單的應(yīng)用我也建議你直接調(diào)用程梦,但是如果邏輯突然變得更復(fù)雜了或者我們的應(yīng)用規(guī)模增大了的話會怎樣呢点把?如果我們不是需要一個請求ID而是需要驗證用戶是否登錄,如果沒有登錄的話重定向到登錄頁屿附,如果登錄了的話查找用戶對象并且存儲下來以備之后使用我們該如何處理呢郎逃?
一個非常簡單的認(rèn)證邏輯可能會是如下版本:
user := lookupUser(r)
if user == nil {
// No user so redirect to login
http.Redirect(w, r, "/login", http.StatusFound)
return
}
現(xiàn)在不再只是在我們所有的處理器中加入一行代碼了,我們需要5行代碼挺份。這看起來并不糟糕褒翰,但是如果我們想要在處理器中進(jìn)行四或五種不同的中間處理的時候會怎樣呢?就像生成一個唯一的請求 ID匀泊,創(chuàng)建一個日志接收器利用這個請求 ID优训,驗證用戶是否登陸,驗證用戶是否是管理員各聘?
那挺起來像是在多個處理器中不斷重復(fù)的糟糕代碼揣非,也非常容易出錯。不合理的訪問權(quán)限控制一次又一次地出現(xiàn)在各種榜單上躲因,比如 OWASP TOP 10早敬,最終也更容易出錯。一個開發(fā)者可能會忘記在一個處理器中驗證一個用戶是否是管理員大脉,我們突然就有了一個只能管理員訪問的頁面暴露給普通用戶搞监,當(dāng)然誰也不希望發(fā)生這種事。
與其產(chǎn)生這種缺陷镰矿,許多開發(fā)者更喜歡在他們的路由函數(shù)中使用中間件來避免這樣的錯誤琐驴。這也幫助應(yīng)用更易于清晰地理解是否需要認(rèn)證。最終秤标,這也易于解釋他們的代碼绝淡,因為你能輕易判斷出是否用戶對象會預(yù)期出現(xiàn)。
下面的例子展示了你可能使用上面的認(rèn)證邏輯驗證當(dāng)訪問 /dashboard/
前綴的路徑時抛杨,用戶是否登錄够委。一個相似的方法可能被用于當(dāng)訪問 /admin/
前綴的路徑時荐类, 用戶是否具有管理員權(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))
}
你只能本地運行-- Web 服務(wù)器不允許在 Go Playground 運行
上下文變量適合在哪引入到我們的認(rèn)證中間件呢?當(dāng)認(rèn)證用戶的時候(取決于你的認(rèn)證策略)你可能最終會找出這個用戶對象來玉罐,盡管你已經(jīng)知道這個用戶了但可能會不得不再查一遍數(shù)據(jù)庫屈嗤,因此我們能使用上下文變量存儲這個用戶對象以備未來之用。
很干凈吊输,不是嗎饶号?因此如果上下文變量允許我們做像讓一個用戶在我們的處理器中可用這種如此酷的操作時它怎么又讓人難以接受了呢?
使用上下文變量的缺點
使用 context.WithValue()
和 context.Value()
最大的缺點時你正在主動選擇放棄一些信息和編譯時類型檢查季蚂。你可能利用這種方法寫出了通用型代碼茫船,但是也有一個值得思考的問題琅束。我們處于某種原因在函數(shù)中使用顯式類型參數(shù),因此任何時候我們選擇放棄放棄一些信息算谈,這些信息可能值得考慮是否有那么大的收益涩禀。
我無法回答你這個問題,因為對于不同的項目結(jié)果可能不一樣然眼,但是在做決定之前艾船,你應(yīng)該確保真正理解了你要放棄的是什么。
函數(shù)需要的數(shù)據(jù)被隱藏了
當(dāng)使用上下文變量的時候高每,我最大的關(guān)切是難以確定函數(shù)需要處理的數(shù)據(jù)屿岂。我們不會寫接收任意的 maps 并且期望用戶放入使我們的函數(shù)能夠工作的各種變量的函數(shù),同樣我們一半不應(yīng)該為自己的 Web 應(yīng)用寫這樣的處理器鲸匿。
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.
}
對于一些像 editUser()
這樣的函數(shù)爷怀,很明顯像用戶對象的數(shù)據(jù)要呈現(xiàn)出來,但是大部分時候晒骇,函數(shù)定義不足夠霉撵,因此作為開發(fā)者,我們不能期望別人根據(jù)函數(shù)的名字就能識別出哪些參數(shù)是必要的洪囤。相反徒坡,我們應(yīng)該明確地在代碼中指出來以更易于閱讀和維護(hù)。我們的 Web 應(yīng)用瘤缩,尤其是哪些處理器函數(shù)和中間件函數(shù)喇完,也不應(yīng)該有任何的不同。我們不應(yīng)該傳遞個 context
對象剥啤,期望他們從中取出他們需要的所有數(shù)據(jù)锦溪。
我們失去了編譯時類型安全保障
上下文變量值本質(zhì)上是一個 interface{}, interface{}
對(請查看源碼)。這也是為什么我們允許存儲任意數(shù)據(jù)而不會產(chǎn)生編譯時警告的原因--鍵值都被定義為空類型府怯,接收任何字面量刻诊。
這種做法的好處是 context.Context
任意的實現(xiàn)都能存儲適用于特定應(yīng)用的各種類型數(shù)據(jù)。缺點是我們無法指望編譯器能替我們分辨是否產(chǎn)生了錯誤牺丙。尤其是在我們的程序中當(dāng)我們存儲字符串代替 User
對象時则涯,程序仍然能編譯通過,除非我們使用類型推斷然后就崩潰了冲簿。有幾種最小化風(fēng)險的方式粟判,但是開發(fā)者總是免不了出錯,而這只會在運行時出現(xiàn)峦剔。
有什么方法避免嗎档礁?對于初學(xué)者,不要根據(jù)我們在以上例子中的方式使用上下文變量吝沫,而是使用特定類型呻澜。除此之外递礼,“packages should define keys as an unexported type to avoid collisons.” --來自 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 和非到處鍵宰衙,確保總是使用類型檢查的較長的方式(有兩個返回值的形式)。這會幫你避免在代碼中產(chǎn)生不必要的崩潰睹欲,并且給你處理異常結(jié)果的機(jī)會供炼。
如果你遵循以上建議,一些源于類型安全的缺陷將會被組織窘疮,因此我們不會在文章的剩余部分討論太多這個特殊的問題袋哼,但是一定要警惕這個問題。這并不是編譯器會幫你解決的問題闸衫,而是作為開發(fā)者涛贯、測試人員和代碼審查人員應(yīng)該要處理的錯誤。
context.Value()
的替代方案
我猜有很多人會說 "我使用方案 X 并且運行得不錯蔚出。為什么你要寫這篇文章弟翘?"。我不會試圖辯論你的方案時錯的骄酗,但是我并不真的相信有一個放之四海而皆準(zhǔn)的解決方案稀余,因此本文的剩余部分將專注于幾個我認(rèn)為有用的替代方案。我也會盡量談下他們覆蓋不到的方面/領(lǐng)域趋翻,以便你能了解到適用于自己使用場景的合適方案睛琳。
代碼復(fù)制-需要時再查抄數(shù)據(jù)
我們簡要討論了什么時候和為什么開發(fā)者會使用上下文變量,但是我想在這里也談?wù)勚皼]談的內(nèi)容踏烙。當(dāng)你寫一個相對簡單的額應(yīng)用時师骗,或者及時你在建一個復(fù)雜的應(yīng)用時,你也會幾乎總是從查找你需要的數(shù)據(jù)開始讨惩。
這正是這本書所談的內(nèi)容 -- 使用 Go 進(jìn)行 Web 開發(fā)辟癌。在這本書中,我們一開始直接在處理器內(nèi)部寫所需邏輯荐捻,然后將邏輯外移到可能每個處理器都需要調(diào)用的可重用函數(shù)中黍少。例如,與其使用之前討論過的 requireUser()
中間件靴患,我們不如寫一個函數(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
}
這將會產(chǎn)生一些代碼復(fù)制鸳君,但是還能接受。我們限制了復(fù)制代碼的行數(shù)患蹂,只有一點復(fù)制要比增加額外的復(fù)雜度搖號或颊。使這個產(chǎn)生問題的情況是它可能會演變成大量的代碼復(fù)制砸紊,比如可能在許多不同的處理器中需要調(diào)用五到六個函數(shù)。那經(jīng)常意味著你可能需要放棄這個方案并尋找新的方法囱挑。
閉包和自定義函數(shù)說明
另一個普遍的解決方案是寫一些函數(shù)醉顽,這些函數(shù)能夠查找必要的數(shù)據(jù),然后利用這些數(shù)據(jù)調(diào)用你自定義的函數(shù)平挑。為了讓這個方法淺顯易懂游添,我們經(jīng)常使用閉包,包裝相似的處理器來創(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ù)期需要一個用戶對象,通過使用 requireUser()
函數(shù)我們能見任何函數(shù) func(http.ResponseWriter, *http.Request, *User)
輕松轉(zhuǎn)變?yōu)?http.Handler
唇辨。
我發(fā)現(xiàn)這個方案意外適用于在所有的處理器中你需要相似的特定于上下文的數(shù)據(jù)的場景廊酣。例如,如果你需要請求 ID赏枚,一個使用請求 ID 和用戶對象的日志接收器時亡驰,你能使用這個方案將所有的函數(shù)轉(zhuǎn)變?yōu)?http.Handler
。
一個人為的案例如下:
// 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)
}
這個方法的不足是當(dāng)你需要在每個處理器中需要不同的數(shù)據(jù)饿幅,這種方法會隨著應(yīng)用規(guī)模的增加而變得越來越復(fù)雜凡辱。同時,這種方法消除了在路由代碼引入前運行中間件的能力栗恩,使得類似“所有起于 /dashboard/
的路徑必須要求用戶登錄”的方案更難以表達(dá)煞茫。
盡管有這些缺陷,我仍然認(rèn)為這種方案值得考慮摄凡,除非它確實本身成為了一個問題续徽。但是這并不是說,”我們最終需要特定路由的中間件“亲澡,然后放棄這種方案钦扭;而是,除非你確實遇到了它不適宜的場景否則你應(yīng)該盡量使用它床绪。
當(dāng)不適宜的場景最終發(fā)生時客情,我有一個想談?wù)劦姆桨浮?/p>
處理上下文變量的模糊性
最終我轉(zhuǎn)向的方案是在剛才回顧的方案和上下文變量的融合處理癞己。基本思想是使用上下文變量和 http.Handler
函數(shù)仰担,如本文開始的示例,但是在我們確實需要上下文變量提供的數(shù)據(jù)之前绩社,我們獻(xiàn)血一個函數(shù)從上下文變量中拉取數(shù)據(jù)摔蓝,傳遞給需要它的函數(shù)。昨晚這些之后拌滋,我們調(diào)用的函數(shù)應(yīng)該永不需從上下文變量中拉去額外的數(shù)據(jù),否則會影響到應(yīng)用的流程猜谚。
通過以上做法败砂,我們幫助消除了使用 context.Value()
獲取數(shù)據(jù)所帶來的模糊性魏铅。我們不必去考慮這個問題,“一些嵌套函數(shù)調(diào)用會預(yù)期上下文中要預(yù)設(shè)某些變量嗎祭隔?”路操,因為所有的數(shù)據(jù)總是將從上下文變量中抽取出來。
最好將此情況用案例的方式描述屯仗,因此我們再一次使用了 addRequestID()
中間件函數(shù)和一個簡單的 home
處理器魁袜,在這個案例中并不明顯的是,logger
也是被設(shè)計為作用于單個請求的日志接收器店量。
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òu)那些之前使用了上下文變量值的代碼蚁吝,并充分利用這個特性。你不必剝離很多代碼怀伦,也不必一次重構(gòu)一切骨架山林,相反,可以通過一分為二的形式來分離原來的單個函數(shù)--一個 http.Handler
獲得數(shù)據(jù)吴攒,另一個函數(shù)使用這些數(shù)據(jù)砂蔽,處理原來的函數(shù)的那些業(yè)務(wù)邏輯。
這真的和一開始的例子有所不同镣隶?
最終诡右,這個方案并沒有和我們回顧的其他方案有很大不同帆吻。最值得注意的是,這似乎和我們一開始使用上下文變量值的例子近乎一致次员,但是兩者之間還是又一些微小但是非常重要的不同之處的王带。
通過總是使用非導(dǎo)出上下文鍵值和其 getter、setter 函數(shù)刹衫,我們有效避免了分配給上下文變量錯誤類型的風(fēng)險搞挣,限制了我們的數(shù)據(jù)無法被設(shè)置的風(fēng)險囱桨。及時數(shù)據(jù)沒有被設(shè)置,我們的 getter 函數(shù)仍然可以試圖去處理它婶肩,當(dāng)他們需要將處理邏輯延遲交由處理器處理時貌夕,能夠選擇返回一個錯誤。
第二個變化更為微妙险毁;通過將我們的函數(shù)一分為二,代碼更為清晰地展示了我們預(yù)期要設(shè)置的數(shù)據(jù)鲸鹦。最終跷跪,任何查看 home
函數(shù)的人將無需通過閱讀代碼就知道我們需要設(shè)置數(shù)據(jù)吵瞻。這是一個對于預(yù)期能夠從 context.Value()
中抽取數(shù)據(jù)方案顯著的改善,這個方案無需再給其他人任何這種期望的暗示(而不是明示)眯停。
簡而言之卿泽,只要簡單地將我們的處理器和中間件劃分成兩個函數(shù)就可以將我們模糊的需求轉(zhuǎn)變?yōu)榍逦揖唧w签夭,幫助新人更快熟悉代碼,也使代碼更易于維護(hù)侄旬。
結(jié)論...
本文沒有討論到一個最終方案煌妈,那就是在你的應(yīng)用和中間件中創(chuàng)建一個屬于自己的自定義 Context
。這最終看起來像某些類似于 “閉包和自定義函數(shù)說明” 的部分汰蜘,但是我們有一個定義好的中等大小的上下文之宿,將其傳遞給每個處理器比被。
這個巨型上下文(我喜歡這樣叫它)有自己的優(yōu)缺點,可能經(jīng)常有所幫助枷莉,但是我并沒有在這兒討論它因為我想在梳理它之前試驗更多的可能性尺迂。我懷疑最終會在接下來幾周再寫一篇文章討論其細(xì)節(jié)冒掌。
同時股毫,請牢記上面的任何方案都有缺陷召衔。一些可能會導(dǎo)致代碼復(fù)制薄嫡,另一些會將類型檢查延遲到運行時處理颗胡,一些限制了你在不同的多處理器中簡單插入中間件的能力。最終哑蔫,你需要自己決定最適合于自己的方案弧呐。
無關(guān)于你選用的路由組件俘枫,請記住在代碼審查中保持警惕,確保其他人也要關(guān)注上下文變量值今阳。
參考資料
我的博客即將同步至騰訊云開發(fā)者社區(qū)茅信,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=ix804iofhkd6