這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
martini是非常優雅的Go Web架構。他基於依賴注入的思想,仿照Sinatra的路由設計,參考Express的中介軟體設計,而且核心微小,擴充方便,非常值得學習。但是由於本身API設計簡潔,使很多細節無法從代碼理解。所以,我寫一點筆記記錄martini的工作方式。
Martini核心
我們從最簡單的官方執行個體入手:
package mainimport "github.com/go-martini/martini"func main() { m := martini.Classic() m.Get("/", func() string { return "Hello world!" }) m.Run()}
martini.Martini是內建的核心結構,負責完成依賴注入和調用的過程。martini.ClassicMartini是路由martini.Router和martini.Martini的組合,實現路由分發和邏輯調用的過程。m := martini.Classic()返回的就是martini.ClassicMartini。具體在martini.go#L104:
func Classic() *ClassicMartini {r := NewRouter()m := New()m.Use(Logger())m.Use(Recovery())m.Use(Static("public"))m.MapTo(r, (*Routes)(nil))m.Action(r.Handle)return &ClassicMartini{m, r}}
裡面的m := New()定義在martini.go#L38:
func New() *Martini {m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}m.Map(m.logger)m.Map(defaultReturnHandler())return m}
依賴注入
上面很明顯的看到兩個奇特方法:m.Map()、m.MapTo()。這裡,需要注意martini的一個最重要原則,注入的任何類型的結構,都是唯一的。即如:
type User struct{ Id int}m.Map(&User{Id:1})m.Map(&User{Id:2})
martini在尋找&User類型的時候,只能擷取到&User{Id:2}結構(最後註冊的)。Map的作用,就是向內部註冊對應類型的具體對象或者值。類型索引是reflect.Type。從而我們可以理解到m.New()的代碼中將m.Logger(*log.Logger)和defaultReturnHandler(martini.ReturnHandler)(括弧中是類型索引)注入到內部。
這裡出現了一個問題。介面這種類型,是無法用reflect.Type()直接擷取的(因為傳來的都是已經實現介面的具體結構)。解決方案就是m.MapTo()。
m.MapTo(r, (*Routes)(nil))
即將r(martini.router)按照martini.Router介面(注意大小寫)類型注入到內部。
(*Routes)(nil)
也是高明的構造。介面的預設值不是nil,無法直接new。但是指標的預設值是nil,可以直接賦值,比如var user *User; user = nil。因此他註冊一個介面指標類型的null 指標,用reflect.Type.Elem()方法就可以擷取到指標的內部類型,即介面類型,並以介面類型索引注入到內部。
路由過程
HTTP處理
martini.Martini實現了http.Handler方法,實際的HTTP執行過程在代碼martini.go#L68:
func (m *Martini) ServeHTTP(res http.ResponseWriter, req *http.Request) {m.createContext(res, req).run()}
這裡需要我們關注m.createContext,它返回*martini.context類型,代碼martini.go#L87:
func (m *Martini) createContext(res http.ResponseWriter, req *http.Request) *context {c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}c.SetParent(m)c.MapTo(c, (*Context)(nil))c.MapTo(c.rw, (*http.ResponseWriter)(nil))c.Map(req)return c}
建立*martini.context類型;然後SetParent設定尋找注入對象的時候同時從m(*martini.Martini)中尋找(*martini.context和*martini.Martini兩個獨立的inject),這樣就可以擷取m.Map注入的資料。
這裡叉出來說:從代碼看出實際上注入的資料有兩層,分別在*martini.context和*martini.Martini。*martini.context中的是當前請求可以擷取的(每個請求都會m.createContext(),都是新的對象);martini.Martini是全域的,任何請求都可以擷取到。
回到上一段,c.MapTo把*martini.context按martini.Context介面,將martini.ResponseWriter按http.ResponseWriter介面,把req(*http.Request)注入到當前上下文。
context.run方法定義在martini.go#L163:
func (c *context) run() {for c.index <= len(c.handlers) {_, err := c.Invoke(c.handler())if err != nil {panic(err)}c.index += 1if c.Written() {return}}}
它在迴圈c.handlers(來自m.handlers,createContext代碼中)。這裡想解釋三個細節。
c.Invoke是inject.Invoke方法,內部就是擷取c.hanlder()返回的martini.Handler(func)類型的傳入參數reflect.Type.In(),根據參數個數和類型去內部找對應的結構,然後拼裝成[]reflect.Value給函數的reflect.Value(func).Call()。
c.handler()的返回來自兩個方面,c.hanlders和c.action。c.handlers來自m.Use()添加,c.action來自r.Handle(*martini.router.Handle)(見上文martini.ClassicMartini.New中的m.Action(r.Handle))。因此,可以發現實際上handlers是有兩個列表,一個是c.handlers([]martini.handler)和r.handlers(martini.routerContext.handlers)。而且前者先執行。也就是說無論m.Use寫在哪兒,都要比router添加的func先執行。
c.Written判斷請求是否已經發送。他實際上是判斷martini.ResponseWriter.status是否大於0。因此只要發送了response status,handlers過程就會停止。
路由調用
從上面可以知道,路由調用過程有兩個方面:一是m.Use()添加的handlers,二是路由添加比如m.Get("/",handlers...)中的handlers。m.Use的handlers調用就是上文的*martini.context.run方法,不再贅述。路由中的handlers執行是在router.go#L218:
func (r *route) Handle(c Context, res http.ResponseWriter) {context := &routeContext{c, 0, r.handlers}c.MapTo(context, (*Context)(nil))context.run()}
和router.go#L315:
func (r *routeContext) run() {for r.index < len(r.handlers) {handler := r.handlers[r.index]vals, err := r.Invoke(handler)if err != nil {panic(err)}r.index += 1// if the handler returned something, write it to the http responseif len(vals) > 0 {ev := r.Get(reflect.TypeOf(ReturnHandler(nil)))handleReturn := ev.Interface().(ReturnHandler)handleReturn(r, vals)}if r.Written() {return}}}
如果你已經理解上文中說明,這個過程和martini.context.run是一樣的。唯一這裡要解釋的是martini.ReturnHandler。它與很上文中的m.Map(defaultReturnHandler())遙相呼應。
中介軟體
從上文不難理解,中介軟體其實就是martini.Handler被m.Use添加到m.handlers中。這裡我們來說明官方的一個中介軟體martini.Logger(),實現代碼在logger.go:
func Logger() Handler {return func(res http.ResponseWriter, req *http.Request, c Context, log *log.Logger) {start := time.Now()log.Printf("Started %s %s", req.Method, req.URL.Path)rw := res.(ResponseWriter)c.Next()log.Printf("Completed %v %s in %v\n", rw.Status(), http.StatusText(rw.Status()), time.Since(start))}}
首先看func的傳入參數,http.ResponseWriter和*http.Request來自:
c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}// ...c.MapTo(c.rw, (*http.ResponseWriter)(nil))c.Map(req)
Context來自:
context := &routeContext{c, 0, r.handlers}c.MapTo(context, (*Context)(nil))
*log.Logger來自:
m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}m.Map(m.logger)
然後看rw := res.(ResponseWriter)。實際上c.rw是NewReponseWriter(res)返回的martini.ResponseWriter類型,一次可以在這裡直接轉換(注意在外部調用,不是martini包中,要import並寫res.(martini.ResponseWriter))。
最後是c.Next()方法,源碼在martini.go#L154:
func (c *context) Next() {c.index += 1c.run()}
意思就是index自增,指向下一個handler,c.run走完所有handler,然後繼續中介軟體裡的log.Printf...。
總結
martini的對外API很簡單,但是內部實現其實比較複雜的。需要仔細的閱讀,並且有一定標準庫的基礎,才能很好的理解他代碼的用意。
我這裡只是按照自己的理解說明,如果有錯誤請在評論中指正。