用Go语言写了7年HTTP服务以后【译】

趁着元旦休假+春节,尝试把2018年期间让我受益的一些文章、问答,翻译一下。
欢迎指正、讨论,但愿对你也有所帮助。
原文:How I write Go HTTP services after seven yearshtml

如下,开始正文
我从r59(1.0版本以前的版本)便开始使用Go,过去7年里一直用Go来编写API和HTTP服务。在Machine Box(译者注:做者公司),写各式各样的API是个人主要工做。咱们是作机器学习的,机器学习自己又很复杂,我编写的API就是为了让开发者更容易理解和接入机器学习。目前为止,收到的反馈还都不错。git

若是你还没尝试过Machine Box,请赶忙试一试,并给我一些反馈吧。github

多年以来,我写服务端程序的方式发生了不少变化,我想把我编写服务端程序的方式分享给你,但愿能对你有所帮助。golang

server structjson

我写的组件基本都包含一个相似这样的server结构体:api

type server struct {
    db     *someDatabase
    router *someRouter
    email  EmailSender
}

routes.go闭包

在组件里还有一个单独的文件routes.go,用来配置路由:app

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
}

routes.go很方便,由于维护代码的时候大部分都从URL和错误日志入手,看一眼routers.go,能帮咱们快速定位。框架

定义handler来处理不一样请求机器学习

func (s *server) handleSomething() http.HandlerFunc { ... }

handler能够经过s访问相关数据。

返回handler
其实handler中并不直接处理请求,而是返回一个函数,创造一个闭包环境,在handler中咱们就能这样操做了:

func (s *server) handleSomething() http.HandlerFunc {
    thing := prepareThing()
    return func(w http.ResponseWriter, r *http.Request) {
        // use thing        
    }
}

prepareThing只需调用一次,也就是你能够经过在handler初始化时,只获取一次thing变量,就能在整个handler中使用。但要保证获取的是共享数据。若是handler中更改数据,须要使用mutex或者其余方式加锁保护。

经过传参解决handler的特殊状况
若是某个handler依赖外部数据,经过传参来解决:

func (s *server) handleGreeting(format string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, format, "World")
    }
}

format参数能够被handler直接使用。

用HandlerFunc替换Handler
我如今在几乎全部地方都用http.HandlerFunc来替换http.Handler了。

func (s *server) handleSomething() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

这两个类型不少状况下均可以互换,对我来说http.HandlerFunc更易读。

用Go函数实现中间件
中间件函数的入参是http.HandlerFunc,返回值是一个新http.HandlerFunc。新http.HandlerFunc能够在原始HandlerFunc以前或者以后调用,甚至能够决定不调用原始HandlerFunc(译者注:看例子吧).

func (s *server) adminOnly(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !currentUser(r).IsAdmin {
            http.NotFound(w, r)
            return
        }
        h(w, r)
    }
}

中间件能够选择是否调用原始handler。以上面代码为例,若是IsAdmin为false,中间件直接返回404,再也不调用h(w, r);若是IsAdmin为true,h这个handler就被调用(h是传入的参数)。
我一般在routers.go中列出中间件:

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
    s.router.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex()))
}

特殊的请求类型和响应类型也能够这样处理
你要处理的特殊的请求类型和响应类型,通常也都是针对个别handler的。若是是这样,你能够在函数中直接定义使用:

func (s *server) handleSomething() http.HandlerFunc {
    type request struct {
        Name string
    }
    type response struct {
        Greeting string `json:"greeting"`
    }
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

这样作可让代码看起来更整洁,也容许你用相同名称命名这些结构体。测试时,拷贝到测试函数中便可。
或者……

建立临时测试类型让测试更简单
若是request或者response类型的定义隐藏在handler中,你能够在测试代码中声明新类型完成测试。这也是一个阐明代码历史和设计的机会,能让维护者更容易理解代码。

举例来说,咱们有一个Person类型,在不少接口中都要使用。若是咱们有个/greet接口,这个接口只关心Person类型的name字段,那咱们就能够这样来写测试用例:

func TestGreet(t *testing.T) {
    is := is.New(t)
    p := struct {
        Name string `json:"name"`
    }{
        Name: "Mat Ryer",
    }
    var buf bytes.Buffer
    err := json.NewEncoder(&buf).Encode(p)
    is.NoErr(err) // json.NewEncoder
    req, err := http.NewRequest(http.MethodPost, "/greet", &buf)
    is.NoErr(err)
    //... more test code here

这段测试代码很明显地说明了Name字段才是惟一须要关注的。

使用sync.Once

若是在预处理handler时必需要作一些耗资源的逻辑,我会把它推迟到第一次调用时处理。这么处理能让应用启动更迅速。

func (s *server) handleTemplate(files string...) http.HandlerFunc {
    var (
        init sync.Once
        tpl  *template.Template
        err  error
    )
    return func(w http.ResponseWriter, r *http.Request) {
        init.Do(func(){
            tpl, err = template.ParseFiles(files...)
        })
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        // use tpl
    }
}

sync.Once确保只执行一次,其余请求在该逻辑处理完以前都会阻塞。

  • 为了能在出错时捕获和保证日志的完整,错误检查放在了init 以外;
  • 若是handler没被调用,耗资源逻辑永远不会执行——这样作好处很是明显,固然也取决于代码部署方式。

不过我要声明,这样处理是将初始化启动时推迟到了运行时(首次访问)。由于我常用 Google App Engine,对我而言这样作优点明显。但你可能面临不一样状况,要因地制宜地考虑如何使用sync.Once

server类型方便测试
咱们的server类型很是便于测试。

func TestHandleAbout(t *testing.T) {
    is := is.New(t)
    srv := server{
        db:    mockDatabase,
        email: mockEmailSender,
    }
    srv.routes()
    req, err := http.NewRequest("GET", "/about", nil)
    is.NoErr(err)
    w := httptest.NewRecorder()
    srv.ServeHTTP(w, req)
    is.Equal(w.StatusCode, http.StatusOK)
}
  • 每一个测试用例建立一个server实例——耗资源能够延迟加载,即便对大型组件总归也浪费不了多少时间;
  • 调用srv.ServeHTTP时实际上是在测试整个调用栈了,也包括路由、中间件等等。若是想避免所有都调用,你也能够直接调用对应的handler;
  • httptest.NewRecorder记录handler都干了啥;
  • 这段代码用了我开发的一个小测试框架

总结
我但愿文章内容对你有帮助,若是不一样意本文观点或者有其余想法都欢迎在Twitter上和我讨论。

相关文章
相关标签/搜索