關(guān)于WEB框架
由于現(xiàn)在編程的語(yǔ)言變成go了娶靡,所以拆輪子系列牧牢,拆的輪子也是go方面的了,其實(shí)也不要緊,因?yàn)樘幚淼乃悸肥呛驼Z(yǔ)言無(wú)關(guān)的塔鳍。gin是go的輕量級(jí)的web框架伯铣,輕量級(jí)意味著僅僅提供web框架應(yīng)有的基礎(chǔ)功能。我覺得看源碼最好就是要有目標(biāo)轮纫,看gin這個(gè)web框架腔寡,我的目標(biāo)是:
- gin這個(gè)web框架是怎么實(shí)現(xiàn)web框架應(yīng)有的基礎(chǔ)功能的
- 代碼上實(shí)現(xiàn)上有什么值得學(xué)習(xí)的地方。
一次請(qǐng)求處理的大體流程
如何找到入口
要知道一次請(qǐng)求處理的大體流程蜡感,只要找到web框架的入口即可蹬蚁。先看看gin文檔當(dāng)中最簡(jiǎn)單的demo。Run方法十分耀眼郑兴,點(diǎn)擊去可以看到關(guān)鍵的http.ListenAndServe犀斋,這意味著Engine這個(gè)結(jié)構(gòu)體,實(shí)現(xiàn)了ServeHTTP這個(gè)接口情连。入口就是Engine實(shí)現(xiàn)的ServeHTTP接口叽粹。
//我是最簡(jiǎn)單的demo
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin")
})
r.Run() // listen and serve on 0.0.0.0:8080
}
//我是Run方法
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
ServeHTTP
大體流程就如注釋那樣,那么的簡(jiǎn)單却舀。這里值得關(guān)注的是虫几,Context這個(gè)上下文對(duì)象是在對(duì)象池里面取出來的,而不是每次都生成挽拔,提高效率辆脸。可以看到螃诅,真正的核心處理流程是在handleHTTPRequest方法當(dāng)中啡氢。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 從上下文對(duì)象池中獲取一個(gè)上下文對(duì)象
c := engine.pool.Get().(*Context)
// 初始化上下文對(duì)象,因?yàn)閺膶?duì)象池取出來的數(shù)據(jù)术裸,有臟數(shù)據(jù)倘是,故要初始化。
c.writermem.reset(w)
c.Request = req
c.reset()
//處理web請(qǐng)求
engine.handleHTTPRequest(c)
//將Context對(duì)象扔回對(duì)象池了
engine.pool.Put(c)
}
handleHTTPRequest
下面的代碼省略了很多和核心邏輯無(wú)關(guān)的代碼袭艺,核心邏輯很簡(jiǎn)單:更具請(qǐng)求方法和請(qǐng)求的URI找到處理函數(shù)們搀崭,然后調(diào)用。為什么是處理函數(shù)們猾编,而不是我們寫的處理函數(shù)瘤睹?因?yàn)檫@里包括了中間層的處理函數(shù)。
func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method
var path string
var unescape bool
// 省略......
// tree是個(gè)數(shù)組答倡,里面保存著對(duì)應(yīng)的請(qǐng)求方式的默蚌,URI與處理函數(shù)的樹。
// 之所以用數(shù)組是因?yàn)槲郏趥€(gè)數(shù)少的時(shí)候,數(shù)組查詢比字典要快
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod {
root := t[i].root
// 找到路由對(duì)應(yīng)的處理函數(shù)們
handlers, params, tsr := root.getValue(path, context.Params, unescape)
// 調(diào)用處理函數(shù)們
if handlers != nil {
context.handlers = handlers
context.Params = params
context.Next()
context.writermem.WriteHeaderNow()
return
}
// 省略......
break
}
}
// 省略......
}
值得欣賞學(xué)習(xí)的地方
路由處理
關(guān)鍵需求
先拋開gin框架不說,路由處理的關(guān)鍵需求有哪些设江?個(gè)人認(rèn)為有以下兩點(diǎn)
- 高效的URI對(duì)應(yīng)的處理函數(shù)的查找
- 靈活的路由組合
gin的處理
核心思路
- 每一個(gè)路由對(duì)應(yīng)的都有一個(gè)獨(dú)立的處理函數(shù)數(shù)組
- 中間件與處理函數(shù)是一致的
- 利用樹提供高效的URI對(duì)應(yīng)的處理函數(shù)數(shù)組的查找
有趣的地方
RouterGroup對(duì)路由的處理
靈活的路由組合是通過將每一個(gè)URI都應(yīng)用著一個(gè)獨(dú)立的處理函數(shù)數(shù)組來實(shí)現(xiàn)的锦茁。對(duì)于路由組合的操作抽象出了RouterGroup結(jié)構(gòu)體來應(yīng)對(duì)。它的主要作用是:
- 將路由與相關(guān)的處理函數(shù)關(guān)聯(lián)起來
- 提供了路由組的功能叉存,這個(gè)是由于關(guān)聯(lián)前綴的方式實(shí)現(xiàn)的
- 提供了中間件自由組合的功能:1. 總的中間件 2. 路由組的中間件 3.處理函數(shù)的中間件
路由組和處理函數(shù)都可以添加中間件這比DJango那種只有總的中間件要靈活得多码俩。
中間件的處理
中間件在請(qǐng)求的時(shí)候需要處理,在返回時(shí)也可能需要做處理歼捏。如下圖(圖是django的)稿存。
問題來了在gin中間件就是一個(gè)處理函數(shù),怎么實(shí)現(xiàn)返回時(shí)的處理呢瞳秽。仔細(xì)觀察瓣履,上面圖的調(diào)用,就是后進(jìn)先出练俐,是的每錯(cuò)答案就是:利用函數(shù)調(diào)用棧后進(jìn)先出的特點(diǎn)袖迎,巧妙的完成中間件在自定義處理函數(shù)完成的后處理的操作。django它的處理方式是定義個(gè)類腺晾,請(qǐng)求處理前的處理的定義一個(gè)方法燕锥,請(qǐng)求處理后的處理定義一個(gè)方法。gin的方式更靈活悯蝉,但django的方式更加清晰归形。
//調(diào)用處理函數(shù)數(shù)組
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
// 中間件例子
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// before request
c.Set("example", "12345")
c.Next()
// 返回后的處理
latency := time.Since(t)
log.Print("latency: ", latency)
status := c.Writer.Status()
log.Println("status: ", status)
}
}
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// it would print: "12345"
log.Println("example", example)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8081")
}
請(qǐng)求內(nèi)容的處理與返回內(nèi)容的處理
需求
- 獲取路徑當(dāng)中的參數(shù)
- 獲取請(qǐng)求參數(shù)
- 獲取請(qǐng)求內(nèi)容
- 將處理好的結(jié)果返回
Gin框架的實(shí)現(xiàn)思路
自己包裝一層除了能提供體驗(yàn)一致的處理方法之外,如果對(duì)官方實(shí)現(xiàn)的不爽鼻由,可以替換掉暇榴,甚至可以加一層緩存處理(其實(shí)沒必要,因?yàn)檎5氖褂梦嗣遥瑑H僅只會(huì)處理一次就夠了)跺撼。
- 如果官方的http庫(kù)能提供的,則在官方的http庫(kù)只上包裝一層讨彼,提供體驗(yàn)一致的接口歉井。
- 官方http庫(kù)不能提供的,則自己實(shí)現(xiàn)
關(guān)鍵結(jié)構(gòu)體
type Context struct {
writermem responseWriter
Request *http.Request
// 傳遞接口哈误,使用各個(gè)處理函數(shù)哩至,更加靈活,降低耦合
Writer ResponseWriter
Params Params // 路徑當(dāng)中的參數(shù)
handlers HandlersChain // 處理函數(shù)數(shù)組
index int8 // 目前在運(yùn)行著第幾個(gè)處理函數(shù)
engine *Engine
Keys map[string]interface{} // 各個(gè)中間件添加的key value
Errors errorMsgs
Accepted []string
}
值得學(xué)習(xí)的點(diǎn)
在數(shù)量少的情況下用數(shù)組查找值蜜自,比用字典查找值要快
在上面對(duì)Context結(jié)構(gòu)體的注釋當(dāng)中菩貌,可以知道Params其實(shí)是個(gè)數(shù)組。本質(zhì)上可以說是key值的對(duì)應(yīng)重荠,為啥不用字典呢箭阶,而是用數(shù)組呢? 實(shí)際的場(chǎng)景,獲取路徑參數(shù)的參數(shù)個(gè)數(shù)不會(huì)很多仇参,如果用字典性能反而不如數(shù)組高嘹叫。因?yàn)樽值湟业綄?duì)應(yīng)的值,大體的流程:對(duì)key進(jìn)行hash —> 通過某算法找到對(duì)應(yīng)偏移的位置(有好幾種算法诈乒,有興趣的可以去查查看) —> 取值罩扇。一套流程下來,數(shù)組在量少的情況下怕磨,已經(jīng)遍歷完了喂饥。
router.GET("user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is222 " + action
c.String(http.StatusOK, message)
})
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
通過接口處理有所同,有所不同的場(chǎng)景
獲取請(qǐng)求內(nèi)容
對(duì)于獲取請(qǐng)求內(nèi)容這個(gè)需求面臨著的場(chǎng)景肠鲫。對(duì)于go這種靜態(tài)語(yǔ)言來說员帮,如果要對(duì)請(qǐng)求內(nèi)容進(jìn)行處理,就需要對(duì)內(nèi)容進(jìn)行反序列化到某個(gè)結(jié)構(gòu)體當(dāng)中滩届,然而請(qǐng)求內(nèi)容的形式多種多樣集侯,例如:JSON,XML帜消,ProtoBuf等等棠枉。因此這里可以總結(jié)出下面的非功能性需求。
- 不同的內(nèi)容需要不同的反序列化機(jī)制
- 允許用戶自己實(shí)現(xiàn)反序列化機(jī)制
共同點(diǎn)都是對(duì)內(nèi)容做處理泡挺,不同點(diǎn)是對(duì)內(nèi)容的處理方式不一樣辈讶,很容易讓人想到多態(tài)這概念,異種求同娄猫。多態(tài)的核心就是接口贱除,這時(shí)候需要抽象出一個(gè)接口。
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
將處理好的內(nèi)容返回
請(qǐng)求內(nèi)容多種多樣媳溺,返回的內(nèi)容也是一樣的月幌。例如:返回JSON,返回XML悬蔽,返回HTML扯躺,返回302等等。這里可以總結(jié)出以下非功能性需求蝎困。
- 不同類型的返回內(nèi)容需要不同的序列化機(jī)制
- 允許用戶實(shí)現(xiàn)自己的序列化機(jī)制
和上面的一致的录语,因此這里也抽象出一個(gè)接口。
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
接口定義好之后需要思考如何使用接口
思考如何優(yōu)雅的使用這些接口
對(duì)于獲取請(qǐng)求內(nèi)容禾乘,在模型綁定當(dāng)中澎埠,有以下的場(chǎng)景
- 綁定失敗是用戶自己處理還是框架統(tǒng)一進(jìn)行處理
- 用戶需是否需要關(guān)心請(qǐng)求的內(nèi)容選擇不同的綁定器
在gin框架的對(duì)于這些場(chǎng)景給出的答案是:提供不同的方法,滿足以上的需求始藕。這里的關(guān)鍵點(diǎn)還是在于使用場(chǎng)景是怎樣的蒲稳。
// 自動(dòng)更加請(qǐng)求頭選擇不同的綁定器對(duì)象進(jìn)行處理
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
}
// 綁定失敗后氮趋,框架會(huì)進(jìn)行統(tǒng)一的處理
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind)
}
return
}
// 用戶可以自行選擇綁定器,自行對(duì)出錯(cuò)處理弟塞。自行選擇綁定器凭峡,這也意味著用戶可以自己實(shí)現(xiàn)綁定器。
// 例如:嫌棄默認(rèn)的json處理是用官方的json處理包决记,嫌棄它慢,可以自己實(shí)現(xiàn)Binding接口
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
對(duì)于實(shí)現(xiàn)的結(jié)構(gòu)體構(gòu)造不一致的處理
將處理好的內(nèi)容返回倍踪,實(shí)現(xiàn)的類構(gòu)造參數(shù)都是不一致的系宫。例如:對(duì)于文本的處理和對(duì)于json的處理。面對(duì)這種場(chǎng)景祭出的武器是:封裝多一層建车,用于構(gòu)造出相對(duì)于的處理對(duì)象扩借。
//對(duì)于String的處理
type String struct {
Format string
Data []interface{}
}
//對(duì)于String處理封裝多的一層
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values})
}
//對(duì)于json的處理
JSON struct {
Data interface{}
}
//對(duì)于json的處理封裝多的一層
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
//核心的一致的處理
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil {
panic(err)
}
}
總結(jié)
這個(gè)看代碼的過程是在有目標(biāo)之后,按照官方文檔的例子缤至,一步一步的看的潮罪。然后再慢慢欣賞,這框架對(duì)于一些web框架常見的場(chǎng)景领斥,它是怎么處理嫉到。這框架的代碼量很少,而且寫得十分的優(yōu)雅,非常值得一看。