在使用Go七年后我如何编写Go HTTP服务

栏目: Go · 发布时间: 5年前

内容简介:原文:翻译:devabel自从r59(一个1.0之前的版本)以来,我一直在写Go(那时还不叫Golang),并且在过去的七年里一直在Go中构建HTTP API和服务。

原文: https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831

翻译:devabel

自从r59(一个1.0之前的版本)以来,我一直在写Go(那时还不叫Golang),并且在过去的七年里一直在 Go 中构建HTTP API和服务。

在Machine Box工作时,我的大多数技术工作都涉及构建各种API。 机器学习很复杂,大多数开发人员都无法掌握,因此我的工作是通过API接口简化这个过程,到目前为止我们已经得到了很好的反馈。

如果您还没有亲眼目睹过Machine Box开发者的体验,请试一试,让我知道您的想法。

多年来,我编写服务的方式发生了变化,因此今天我想分享如何编写服务 - 也许这种模式对您的工作有帮助。

A server struct

我的所有组件都有一个服务器结构,通常看起来像这样:

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

这很方便,因为大多数代码维护都是以URL和错误报告开始的 - 所以只需浏览一下routes.go即可指示我们查看的位置。

处理程序挂起服务器

我的HTTP处理程序挂起了服务器:

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

处理程序可以通过s服务器变量访问依赖项。

返回处理程序

我的处理函数实际上并不处理请求,它们返回一个函数。

这给了我们一个闭包环境,我们的处理程序可以在其中运行

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

prepareThing仅被调用一次,因此您可以使用它来执行一次性每个处理程序初始化,然后在处理程序中使用该事物。

确保只读取共享数据,如果处理程序正在修改任何内容,请记住您需要一个互斥锁或其他东西来保护它。

获取特定于处理程序的依赖项的参数

如果特定处理程序具有依赖项,请将其作为参数。

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

格式变量可供处理程序访问。

处理程序上的HandlerFunc

我现在几乎在所有情况下都使用http.HandlerFunc,而不是http.Handler。

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

它们或多或少是可以互换的,所以只需选择更容易阅读的内容。 对我来说,这是http.HandlerFunc。

中间件只是一个Go函数

中间件函数接受一个http.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)
    }
}

处理程序内部的逻辑可以选择是否调用原始处理程序 - 在上面的示例中,如果IsAdmin为false,处理程序将返回HTTP 404 Not Found并返回(abort); 注意没有调用h处理程序。

如果IsAdmin为true,则执行将传递给传入的h处理程序。

通常我在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())
    s.router.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex()))
}

请求和响应类型也可以在那里

如果端点有自己的请求和响应类型,通常它们仅对该特定处理程序有用。

如果是这种情况,您可以在函数内定义它们。

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) {
        ...
    }
}

这会对您的包空间进行整理,并允许您将这些类型命名为相同,而不必考虑特定于处理程序的版本。

在测试代码中,您只需将类型复制到测试函数中并执行相同的操作即可。 要么…

测试类型可以帮助构建测试

如果您的请求/响应类型隐藏在处理程序中,您只需在测试代码中声明新类型即可。

这是一个为需要了解您的代码的后代做一些故事讲述的机会。

例如,假设我们的代码中有Person类型,我们在许多端点上重用它。 如果我们有一个/ greet端点,我们可能只关心他们的名字,所以我们可以在测试代码中表达:

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

从这个测试中可以清楚地看出,我们唯一关心的领域是人的名字。

sync.Once设置依赖项

如果我在准备处理程序时必须做任何昂贵的事情,我会推迟到第一次调用该处理程序时。

这改善了应用程序启动时间

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确保代码只执行一次,其他调用(其他人发出相同的请求)将一直阻塞,直到完成。

  1. 错误检查在init函数之外,所以如果出现问题我们仍然会出现错误并且不会在日志中丢失它

    2.如果未调用处理程序,则永远不会完成昂贵的工作 - 这可能会带来很大的好处,具体取决于代码的部署方式

    请记住,执行此操作时,您将初始化时间从启动时移至运行时(首次访问端点时)。 我经常使用Google App Engine,所以这对我来说很有意义,但是你的情况可能会有所不同,所以值得思考何时何地使用sync.Once这样。

服务器是可测试的

我们的服务器类型非常便于测试。

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

在每个测试中创建一个服务器实例 - 如果昂贵的东西延迟加载,这将不会花费太多时间,即使对于大组件

通过在服务器上调用ServeHTTP,我们正在测试整个堆栈,包括路由和中间件等。如果你想避免这种情况,你当然可以直接调用处理程序方法。

使用httptest.NewRecorder记录处理程序正在执行的操作

此代码示例使用我的测试迷你框架(作为Testify的迷你替代品)

结论

我希望本文中涉及的内容有意义,并帮助您完成工作。 如果您不同意或有其他想法,请发推特给我。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance Python

High Performance Python

Micha Gorelick、Ian Ozsvald / O'Reilly Media / 2014-9-10 / USD 39.99

If you're an experienced Python programmer, High Performance Python will guide you through the various routes of code optimization. You'll learn how to use smarter algorithms and leverage peripheral t......一起来看看 《High Performance Python》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具