24

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

 5 years ago
source link: https://studygolang.com/articles/17733?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

趁着元旦休假+春节,尝试把2018年期间让我受益的一些文章、问答,翻译一下。

欢迎指正、讨论,希望对你也有所帮助。

原文: How I write Go HTTP services after seven years

以下,开始正文

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

如果你还没尝试过Machine Box,请赶紧试一试,并给我一些反馈吧。

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

server struct

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

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

routes.go

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

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 

不过我要声明,这样处理是将初始化启动时推迟到了运行时(首次访问)。因为我经常使用 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 上和我讨论。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK