大部分時候,咱們須要實現一個 Web 應用,第一反應是應該使用哪一個框架。不一樣的框架設計理念和提供的功能有很大的差異。好比 Python 語言的 django
和flask
,前者大而全,後者小而美。Go語言/golang 也是如此,新框架層出不窮,好比Beego
,Gin
,Iris
等。那爲何不直接使用標準庫,而必須使用框架呢?在設計一個框架以前,咱們須要回答框架核心爲咱們解決了什麼問題。只有理解了這一點,才能想明白咱們須要在框架中實現什麼功能。html
咱們先看看標準庫 net/http 如何處理一個請求git
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開發中簡單的需求並不支持,須要手工實現。github
hello/:name
,hello/*
這類的規則。當咱們離開框架,使用基礎庫時,須要頻繁手工處理的地方,就是框架的價值所在。但並非每個頻繁處理的地方都適合在框架中完成。Python有一個很著名的Web框架,名叫bottle
,整個框架由bottle.py
一個文件構成,共4400行,能夠說是一個微框架。那麼理解這個微框架提供的特性,能夠幫助咱們理解框架的核心能力。golang
'/hello/:name
。這個教程將使用 Go 語言實現一個簡單的 Web 框架,起名叫作Gee
,geektutu.com
的前三個字母。我第一次接觸的 Go 語言的 Web 框架是Gin
,Gin
的代碼總共是14K,其中測試代碼9K,也就是說實際代碼量只有5K。Gin
也是我很是喜歡的一個框架,與Python中的Flask
很像,小而美。web
7天實現Gee框架
這個教程的不少設計,包括源碼,參考了Gin
,你們能夠看到不少Gin
的影子。
時間關係,同時爲了儘量地簡潔明瞭,這個框架中的不少部分實現的功能都很簡單,可是儘量地體現一個框架核心的設計原則。例如Router
的設計,雖然支持的動態路由規則有限,但爲了性能考慮匹配算法是用Trie樹
實現的,Router
最重要的指標之一即是性能。算法
Go語言內置了 net/http
庫,封裝了HTTP網絡編程的基礎的接口,咱們實現的Gee
Web 框架即是基於net/http
的。咱們接下來經過一個例子,簡單介紹下這個庫的使用。shell
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)中的鍵值對信息。django
用 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框架的入口。json
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請求,擁有了統一的控制入口。在這裏咱們能夠自由定義路由映射的規則,也能夠統一添加一些處理邏輯,例如日誌、異常處理等。
代碼的運行結果與以前的是一致的。
tree gee_demo1 gee_demo1 ├── gee │ ├── gee.go │ └── go.mod ├── go.mod └── main.go 1 directory, 4 files
module gee_demo1 go 1.13 require gee v0.0.0 replace gee => ./gee
go.mod
中使用 replace
將 gee 指向 ./gee
從 go 1.11 版本開始,引用相對路徑的 package 須要使用上述方式。
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
這樣的動態路由,動態路由咱們將在下一次實現。
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
標準庫更強大的能力,不用擔憂,很快就能夠將動態路由、中間件等功能添加上去了。
路由(router)
獨立出來,方便以後加強。上下文(Context)
,封裝 Request 和 Response ,提供對 JSON、HTML 等返回類型的支持.package main import ( "fmt" "gee" "net/http" ) func main() { r := gee.New() r.GET("/", func(c *gee.Context) { c.HTML(http.StatusOK, "<h1>Hello Gee</h1>") }) r.GET("/hello", func(c *gee.Context) { // expect /hello?name=geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path) }) r.POST("/login", func(c *gee.Context) { c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), }) }) r.Run(":9999") }
Handler
的參數變成成了gee.Context
,提供了查詢Query/PostForm參數的功能。gee.Context
封裝了HTML/String/JSON
函數,可以快速構造HTTP響應。必要性
*http.Request
,構造響應http.ResponseWriter
。可是這兩個對象提供的接口粒度太細,好比咱們要構造一個完整的響應,須要考慮消息頭(Header)和消息體(Body),而 Header 包含了狀態碼(StatusCode),消息類型(ContentType)等幾乎每次請求都須要設置的信息。所以,若是不進行有效的封裝,那麼框架的用戶將須要寫大量重複,繁雜的代碼,並且容易出錯。針對經常使用場景,可以高效地構造出 HTTP 響應是一個好的框架必須考慮的點。用返回JSON數據做比較, 感覺下封裝先後的差距
封裝前
obj = map[string]interface{}{ "name": "geektutu", "password": "1234", } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) encoder := json.NewEncoder(w) if err := encoder.Encode(obj); err != nil { http.Error(w, err.Error(), 500) }
封裝後
c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), })
*http.Request
和http.ResponseWriter
的方法,簡化相關接口的調用,只是設計 Context 的緣由之一。對於框架來講,還須要支撐額外的功能。例如,未來解析動態路由/hello/:name
,參數:name
的值放在哪呢?再好比,框架須要支持中間件,那中間件產生的信息放在哪呢?Context 隨着每個請求的出現而產生,請求的結束而銷燬,和當前請求強相關的信息都應由 Context 承載。所以,設計 Context 結構,擴展性和複雜性留在了內部,而對外簡化了接口。路由的處理函數,以及將要實現的中間件,參數都統一使用 Context 實例, Context 就像一次會話的百寶箱,能夠找到任何東西。type H map[string]interface{} type Context struct { // origin objects Writer http.ResponseWriter Req *http.Request // request info Path string Method string // response info StatusCode int } func newContext(w http.ResponseWriter, req *http.Request) *Context { return &Context{ Writer: w, Req: req, Path: req.URL.Path, Method: req.Method, } } func (c *Context) PostForm(key string) string { return c.Req.FormValue(key) } func (c *Context) Query(key string) string { return c.Req.URL.Query().Get(key) } func (c *Context) Status(code int) { c.StatusCode = code c.Writer.WriteHeader(code) } func (c *Context) SetHeader(key string, value string) { c.Writer.Header().Set(key, value) } func (c *Context) String(code int, format string, values ...interface{}) { c.SetHeader("Content-Type", "text/plain") c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { c.SetHeader("Content-Type", "application/json") c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) } } func (c *Context) Data(code int, data []byte) { c.Status(code) c.Writer.Write(data) } func (c *Context) HTML(code int, html string) { c.SetHeader("Content-Type", "text/html") c.Status(code) c.Writer.Write([]byte(html)) }
map[string]interface{}
起了一個別名gee.H
,構建JSON數據時,顯得更簡潔。Context
目前只包含了http.ResponseWriter
和*http.Request
,另外提供了對 Method 和 Path 這兩個經常使用屬性的直接訪問。咱們將和路由相關的方法和結構提取了出來,放到了一個新的文件中router.go
,方便咱們下一次對 router 的功能進行加強,例如提供動態路由的支持。 router 的 handle 方法做了一個細微的調整,即 handler 的參數,變成了 Context。
type router struct { handlers map[string]HandlerFunc } func newRouter() *router { return &router{handlers: make(map[string]HandlerFunc)} } func (r *router) addRoute(method string, pattern string, handler HandlerFunc) { log.Printf("Route %4s - %s", method, pattern) key := method + "-" + pattern r.handlers[key] = handler } func (r *router) handle(c *Context) { key := c.Method + "-" + c.Path if handler, ok := r.handlers[key]; ok { handler(c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path) } }
// HandlerFunc defines the request handler used by gee type HandlerFunc func(*Context) // Engine implement the interface of ServeHTTP type Engine struct { router *router } // New is the constructor of gee.Engine func New() *Engine { return &Engine{router: newRouter()} } func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { engine.router.addRoute(method, pattern, 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) { c := newContext(w, req) engine.router.handle(c) }
將router
相關的代碼獨立後,gee.go
簡單了很多。最重要的仍是經過實現了 ServeHTTP 接口,接管了全部的 HTTP 請求。相比第一天的代碼,這個方法也有細微的調整,在調用 router.handle 以前,構造了一個 Context 對象。這個對象目前還很是簡單,僅僅是包裝了原來的兩個參數,以後咱們會慢慢地給Context插上翅膀。
如何使用,main.go
一開始就已經亮相了。運行go run main.go
,藉助 curl ,一塊兒看一看今天的成果吧。
curl -i http://localhost:9999/ HTTP/1.1 200 OK Date: Mon, 12 Aug 2019 16:52:52 GMT Content-Length: 18 Content-Type: text/html; charset=utf-8 <h1>Hello Gee</h1> $ curl "http://localhost:9999/hello?name=geektutu" hello geektutu, you're at /hello $ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234' {"password":"1234","username":"geektutu"} $ curl "http://localhost:9999/xxx" 404 NOT FOUND: /xxx
感謝大佬 geektutu.com 分享