設計一個框架
大部分時候,我們需要實現一個 Web 應用,第一反應是應該使用哪個框架。不同的框架設計理念和提供的功能有很大的差別。比如 Python 語言的 django
和flask
,前者大而全,後者小而美。Go語言/golang 也是如此,新框架層出不窮,比如Beego
,Gin
,Iris
等。那為什麼不直接使用標準庫,而必須使用框架呢?在設計一個框架之前,我們需要回答框架核心為我們解決了什麼問題。只有理解了這一點,才能想明白我們需要在框架中實現什麼功能。
我們先看看標準庫 net/http 如何處理一個請求
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/",handler)
//http.HandlerFunc("/count",counter)
log.Fatal(http.ListenAndServe("localhost:8000",nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w,"URL.Path = %q\n",r.URL.Path)
fmt.Println("1234")
}
net/http
提供了基礎的Web功能,即監聽埠,對映靜態路由,解析HTTP報文。一些Web開發中簡單的需求並不支援,需要手工實現。
- 動態路由:例如
hello/:name
,hello/*
這類的規則。 - 鑑權:沒有分組/統一鑑權的能力,需要在每個路由對映的handler中實現。
- 模板:沒有統一簡化的HTML機制。
- …
當我們離開框架,使用基礎庫時,需要頻繁手工處理的地方,就是框架的價值所在。但並不是每一個頻繁處理的地方都適合在框架中完成。Python有一個很著名的Web框架,名叫bottle
,整個框架由bottle.py
一個檔案構成,共4400行,可以說是一個微框架。那麼理解這個微框架提供的特性,可以幫助我們理解框架的核心能力。
- 路由(Routing):將請求對映到函式,支援動態路由。例如
'/hello/:name
。 - 模板(Templates):使用內建模板引擎提供模板渲染機制。
- 工具集(Utilites):提供對 cookies,headers 等處理機制。
- 外掛(Plugin):Bottle本身功能有限,但提供了外掛機制。可以選擇安裝到全域性,也可以只針對某幾個路由生效。
- …
Gee框架
這個教程將使用 Go 語言實現一個簡單的 Web 框架,起名叫做Gee
,geektutu.com
的前三個字母。我第一次接觸的 Go 語言的 Web 框架是Gin
,Gin
的程式碼總共是14K,其中測試程式碼9K,也就是說實際程式碼量只有5K。Gin
也是我非常喜歡的一個框架,與Python中的Flask
很像,小而美。
7天實現Gee框架
這個教程的很多設計,包括原始碼,參考了Gin
,大家可以看到很多Gin
的影子。
時間關係,同時為了儘可能地簡潔明瞭,這個框架中的很多部分實現的功能都很簡單,但是儘可能地體現一個框架核心的設計原則。例如Router
的設計,雖然支援的動態路由規則有限,但為了效能考慮匹配演算法是用Trie樹
實現的,Router
最重要的指標之一便是效能。
標準庫啟動Web服務
Go語言內建了 net/http
庫,封裝了HTTP網路程式設計的基礎的介面,我們實現的Gee
Web 框架便是基於net/http
的。我們接下來通過一個例子,簡單介紹下這個庫的使用。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/",indexHandler)
http.HandleFunc("/hello",helloHandler)
log.Fatal(http.ListenAndServe(":8000",nil))
}
// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w,"URL.Path = %q\n",req.URL.Path)
}
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k,v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n",k,v)
}
}
我們設定了2個路由,/
和/hello
,分別繫結 indexHandler 和 helloHandler , 根據不同的HTTP請求會呼叫不同的處理函式。訪問/
,響應是URL.Path = /
,而/hello
的響應則是請求頭(header)中的鍵值對資訊。
用 curl 這個工具測試一下,將會得到如下的結果
$ curl localhost:8000/
URL.Path = "/"
$ curl localhost:8000/hello
Header["User-Agent"] = ["curl/7.64.1"]
Header["Accept"] = ["*/*"]
main 函式的最後一行,是用來啟動 Web 服務的,第一個引數是地址,:8000
表示在 8000 埠監聽。而第二個引數則代表處理所有的HTTP請求的例項,nil
代表使用標準庫中的例項處理。第二個引數,則是我們基於net/http
標準庫實現Web框架的入口。
實現http.Handler介面
package http
type Handler interface {
ServeHTPP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error {
}
第二個引數的型別是什麼呢?通過檢視net/http
的原始碼可以發現,Handler
是一個介面,需要實現方法 ServeHTTP ,也就是說,只要傳入任何實現了 ServerHTTP 介面的例項,所有的HTTP請求,就都交給了該例項處理了。
main.go
package main
import (
"fmt"
"log"
"net/http"
)
type Engine struct{}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
case "/hello":
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
default:
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
func main() {
engine := new(Engine)
log.Fatal(http.ListenAndServe(":8000", engine))
}
-
我們定義了一個空的結構體
Engine
,實現了方法ServeHTTP
。這個方法有2個引數,第二個引數是 Request ,該物件包含了該HTTP請求的所有的資訊,比如請求地址、Header和Body等資訊;第一個引數是 ResponseWriter ,利用 ResponseWriter 可以構造針對該請求的響應。 -
在 main 函式中,我們給 ListenAndServe 方法的第二個引數傳入了剛才建立的
engine
例項。至此,我們走出了實現Web框架的第一步,即,將所有的HTTP請求轉向了我們自己的處理邏輯。還記得嗎,在實現Engine
之前,我們呼叫 http.HandleFunc 實現了路由和Handler的對映,也就是隻能針對具體的路由寫處理邏輯。比如/hello
。但是在實現Engine
之後,我們攔截了所有的HTTP請求,擁有了統一的控制入口。在這裡我們可以自由定義路由對映的規則,也可以統一新增一些處理邏輯,例如日誌、異常處理等。 -
程式碼的執行結果與之前的是一致的。
Gee框架的雛形
程式碼結構
tree gee_demo1
gee_demo1
├── gee
│ ├── gee.go
│ └── go.mod
├── go.mod
└── main.go
1 directory, 4 files
go.mod
module gee_demo1
go 1.13
require gee v0.0.0
replace gee => ./gee
- 在
go.mod
中使用replace
將 gee 指向./gee
從 go 1.11 版本開始,引用相對路徑的 package 需要使用上述方式。
main.go
package main
import (
"fmt"
"gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
})
r.Run(":8000")
}
看到這裡,如果你使用過gin
框架的話,肯定會覺得無比的親切。gee
框架的設計以及API均參考了gin
。使用New()
建立 gee 的例項,使用 GET()
方法新增路由,最後使用Run()
啟動Web服務。這裡的路由,只是靜態路由,不支援/hello/:name
這樣的動態路由,動態路由我們將在下一次實現。
gee.go
package gee
import (
"fmt"
"log"
"net/http"
)
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)
// Engine implement the interface of ServeHTTP
type Engine struct {
router map[string]HandlerFunc
}
// New is the constructor of gee.Engine
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern
log.Printf("Route %4s - %s", method, pattern)
engine.router[key] = handler
}
// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + "-" + req.URL.Path
if handler, ok := engine.router[key]; ok {
handler(w, req)
} else {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
那麼gee.go
就是重頭戲了。我們重點介紹一下這部分的實現。
-
首先定義了型別
HandlerFunc
,這是提供給框架使用者的,用來定義路由對映的處理方法。我們在Engine
中,新增了一張路由對映表router
,key 由請求方法和靜態路由地址構成,例如GET-/
、GET-/hello
、POST-/hello
,這樣針對相同的路由,如果請求方法不同,可以對映不同的處理方法(Handler),value 是使用者對映的處理方法。 -
當使用者呼叫
(*Engine).GET()
方法時,會將路由和處理方法註冊到對映表 router 中,(*Engine).Run()
方法,是 ListenAndServe 的包裝。 -
Engine
實現的 ServeHTTP 方法的作用就是,解析請求的路徑,查詢路由對映表,如果查到,就執行註冊的處理方法。如果查不到,就返回 404 NOT FOUND 。
執行go run main.go
,再用 curl 工具訪問,結果與最開始的一致
測試
$ curl localhost:8000
URL.Path = "/"
$ curl localhost:8000/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.64.1"]
至此,整個Gee
框架的原型已經出來了。實現了路由對映表,提供了使用者註冊靜態路由的方法,包裝了啟動服務的函式。當然,到目前為止,我們還沒有實現比net/http
標準庫更強大的能力,不用擔心,很快就可以將動態路由、中介軟體等功能新增上去了。
文章絕大多數來自作者: https://geektutu.com/