Go-Martini源码浅析

上面两章我们重点介绍了Golang的基础语法以及高级特性部分,接下来我们通过这些所学习到的基础内容,去学习研究一些开源的产品,从而让我们对整个Golang的体系有更多更深的理解。

在整个go-web生态中,其实有很多的开源产品助力大家日常的系列工作,这边列举如下:

Beego

此产品由国人开发,其整个架构基于MVC的思想设计,同时提供了Dao层的一个统一的ORM框架、统一的http请求的处理、页面模板引擎的统一解析等,所以整体上来说,算是一个完成度比较高的一个产品。

Gin

Gin是一款相对比较简单的产品,其主要解决的问题还是go原生对于http请求不是很方便,所以研发了一个基于Router模式的web端框架。

Martini

此框架比Gin还比较简单,也非常建议通过Martini去入手去研究一款Go-web的开源产品,其本质上来说也是基于Router模式的web端开发框架。

因为Beego的整个生态虽然完整但是比较复杂,而写这篇文章的目的其实就是希望能简单去看一下,如何用golang去研发一款产品,所以选择分析Martini这款简单的基于Router模式的产品。

对Martini代码感兴趣的也可以移步。

首先,我们可以到Github克隆整个项目下来并导入到IDE中。

我们先来分析一下Martini的整个的架构设计。

个人觉得Martini主要分为两个大的部分,一部分就是对于http请求的通用化处理,另外一部分就是基于这个内核的所衍生出来的各个中间件。

首先,我们先来看一下如何使用。

package main
 
import "github.com/go-martini/martini"
 
func main() {
    m := martini.Classic()
    m.Get("/", func() string {
        return "Hello world!"
    })
    m.Run()
}

打开浏览器运行localhost:3000我们可以看到输出Hello world。

我们对如上的代码做一个深层次的猜想。

1、首先Martini会做一个初始化,这里面可能会包括初始化相关的服务器的信息;

2、对请求做一个定义,比如是Post还是Get,请求的上下文是什么,返回的内容又是什么;

所以,我们从大的层面来分析的话,无非就是服务端定义+请求解析。

再深入源码之前,我们再去看一下Martini中的一些特性介绍,从而对于以后关于golang产品化提供更多的灵感。

package main
 
import "github.com/go-martini/martini"
 
func main() {
    m := martini.Classic()
    m.Get("/index", func() (int, string) {
        return 418, "i'm a teapot" // HTTP 418 : "i'm a teapot"
    })
    m.Run()
}   

我们可以看到,还可以给返回结果定义返回编码。

package main
 
import "github.com/go-martini/martini"
 
func main() {
    m := martini.Classic()
    m.Get("/hello/:name", func(params martini.Params) string {
        return "Hello " + params["name"]
    })
    m.Run()
}    

如上是我们看到的一个restful请求。

那我们接下来简单分析下Martini的源码,对于Martini的大概运行过程做一个了解。

首先先做一个初始化。

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}
}   

我们来看一下路由的新建。

func NewRouter() Router {
    return &router{notFounds: []Handler{http.NotFound}, groups: make([]group, 0)}
}
 
func (r *router) Group(pattern string, fn func(Router), h ...Handler) {
    r.groups = append(r.groups, group{pattern, h})
    fn(r)
    r.groups = r.groups[:len(r.groups)-1]
}
 
func (r *router) Get(pattern string, h ...Handler) Route {
    return r.addRoute("GET", pattern, h)
}
 
func (r *router) Patch(pattern string, h ...Handler) Route {
    return r.addRoute("PATCH", pattern, h)
}
 
func (r *router) Post(pattern string, h ...Handler) Route {
    return r.addRoute("POST", pattern, h)
}
 
func (r *router) Put(pattern string, h ...Handler) Route {
    return r.addRoute("PUT", pattern, h)
}
 
func (r *router) Delete(pattern string, h ...Handler) Route {
    return r.addRoute("DELETE", pattern, h)
}
 
func (r *router) Options(pattern string, h ...Handler) Route {
    return r.addRoute("OPTIONS", pattern, h)
}
 
func (r *router) Head(pattern string, h ...Handler) Route {
    return r.addRoute("HEAD", pattern, h)
}
 
func (r *router) Any(pattern string, h ...Handler) Route {
    return r.addRoute("*", pattern, h)
}
 
func (r *router) AddRoute(method, pattern string, h ...Handler) Route {
    return r.addRoute(method, pattern, h)
}     

我们可以看到在Martini的初始化的结果中返回的是一个总的Router,而我们看Router的定义其实是一个Interface,所以以上代码其实就是实现接口中的方法,比如Get请求如何处理,Post请求如何处理等。详细如下:

type Router interface {
    Routes
 
    Group(string, func(Router), ...Handler)
    Get(string, ...Handler) Route
    Patch(string, ...Handler) Route
    Post(string, ...Handler) Route
    Put(string, ...Handler) Route
    Delete(string, ...Handler) Route
    Options(string, ...Handler) Route
    Head(string, ...Handler) Route
    Any(string, ...Handler) Route
    AddRoute(string, string, ...Handler) Route
 
    NotFound(...Handler)
 
    Handle(http.ResponseWriter, *http.Request, Context)
}    

我们再来看一下是如何将路由信息进行添加的。代码如下:

func (r *router) addRoute(method string, pattern string, handlers []Handler) *route {
    if len(r.groups) > 0 {
        groupPattern := ""
        h := make([]Handler, 0)
        for _, g := range r.groups {
            groupPattern += g.pattern
            h = append(h, g.handlers...)
        }
 
        pattern = groupPattern + pattern
        h = append(h, handlers...)
        handlers = h
    }
 
    route := newRoute(method, pattern, handlers)
    route.Validate()
    r.appendRoute(route)
    return route
}    

如上代码的核心就是,对于每一个web的请求,在Martini中其实都是被定义成一个个的Handler,每一个Handler配合web请求的方法(比如是Get、Post等),同时使用路径请求上下文信息,就构成了一个完整的web请求处理。而在Martini中,其将之定义为router。

到这里,我们可能要思考一个问题了,这个Handler是什么?为什么这么定义后就能够对web的请求做处理?

探究这个,我们在回顾一下之前我们如何来设计一个小型的web服务器的程序:

package main
 
import (
    "io"
    "log"
    "net/http"
)
 
func main() {
    http.HandleFunc("/index", func(writer http.ResponseWriter, request *http.Request) {
        writer.WriteHeader(200)
        io.WriteString(writer,"Hello World")
    })
    error := http.ListenAndServe(":8080",nil)
    if error!=nil {
        log.Fatal(error)
    }
}

如上程序就是原生定义一款web服务器的方式,那么Martini中是如何定义的呢?

func (m *Martini) Run() {
    port := os.Getenv("PORT")
    if len(port) == 0 {
        port = "3000"
    }
 
    host := os.Getenv("HOST")
 
    m.RunOnAddr(host + ":" + port)
}    

在启动的入口中我们会看到在Martini中会定义一个RunOnAddr的方法,我们点进去看一下。

func (m *Martini) RunOnAddr(addr string) {
    logger := m.Injector.Get(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
    logger.Printf("listening on %s (%s)\n", addr, Env)
    logger.Fatalln(http.ListenAndServe(addr, m))
}    

我们可以看到http.ListenAndServe(addr, m)就是开启一个web服务。

这里我们会发现传入的参数除了addr外还有一个Martini的实例,同时我们知道http.ListenAndServe(addr string, handler Handler)是原生的Golang的Http方法,第二个参数是一个Handler的接口,如下:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}    

所以我们猜想在Martini的实例中实现了ServeHttp的方法,才让Martini的Handler可以去处理各项web请求。我们再继续往下看:

func (m *Martini) ServeHTTP(res http.ResponseWriter, req *http.Request) {
    m.createContext(res, req).run()
}    

我们看到其定义了一个Context:

func (c *context) run() {
    for c.index <= len(c.handlers) {
        _, err := c.Invoke(c.handler())
        if err != nil {
            panic(err)
        }
        c.index += 1
 
        if c.Written() {
            return
        }
    }
}    

再run方法中我们就可以看到所定义的各个handler是如何来映射与使用的了。

对于一款web框架,我们肯定也需要去访问一些前端的页面资源,那么Martini是如何去做的呢?

其实很简单,还是Handler。

Martini支持你扩展各种进行http请求的解析插件,比如静态资源处理插件:

func Static(directory string, staticOpt ...StaticOptions) Handler {
    if !filepath.IsAbs(directory) {
        directory = filepath.Join(Root, directory)
    }
    dir := http.Dir(directory)
    opt := prepareStaticOptions(staticOpt)
 
    return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
        if req.Method != "GET" && req.Method != "HEAD" {
            return
        }
        if opt.Exclude != "" && strings.HasPrefix(req.URL.Path, opt.Exclude) {
            return
        }
        file := req.URL.Path
        // if we have a prefix, filter requests by stripping the prefix
        if opt.Prefix != "" {
            if !strings.HasPrefix(file, opt.Prefix) {
                return
            }
            file = file[len(opt.Prefix):]
            if file != "" && file[0] != '/' {
                return
            }
        }
        f, err := dir.Open(file)
        if err != nil {
            // try any fallback before giving up
            if opt.Fallback != "" {
                file = opt.Fallback // so that logging stays true
                f, err = dir.Open(opt.Fallback)
            }
 
            if err != nil {
                // discard the error?
                return
            }
        }
        defer f.Close()
 
        fi, err := f.Stat()
        if err != nil {
            return
        }
 
        // try to serve index file
        if fi.IsDir() {
            // redirect if missing trailing slash
            if !strings.HasSuffix(req.URL.Path, "/") {
                dest := url.URL{
                    Path:     req.URL.Path + "/",
                    RawQuery: req.URL.RawQuery,
                    Fragment: req.URL.Fragment,
                }
                http.Redirect(res, req, dest.String(), http.StatusFound)
                return
            }
 
            file = path.Join(file, opt.IndexFile)
            f, err = dir.Open(file)
            if err != nil {
                return
            }
            defer f.Close()
 
            fi, err = f.Stat()
            if err != nil || fi.IsDir() {
                return
            }
        }
 
        if !opt.SkipLogging {
            log.Println("[Static] Serving " + file)
        }
 
        // Add an Expires header to the static content
        if opt.Expires != nil {
            res.Header().Set("Expires", opt.Expires())
        }
 
        http.ServeContent(res, req, file, fi.ModTime(), f)
    }
}    

如上其实就是定义了一个静态资源处理的handler插件。

所以对于后面有相关需求的插件我们也可以去尝试定制。

以上就是针对于Martini这款go-web框架的简单分析,有兴趣的同学可以线下与我交流。

—————–EOF——————



Previous     Next
mjgao /