分享
  1. 首页
  2. 文章

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

alphali · · 1536 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

趁着元旦休假+春节,尝试把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 之外;
  • 如果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上和我讨论。


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:alphali

查看原文:用Go语言写了7年HTTP服务之后【译】

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1536 次点击
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏