Google言語で7年間HTTPサービスを書いた後【訳】

6380 ワード

元旦の休暇+春節を利用して、2018年の間に私に利益をもたらしたいくつかの文章、問答を試して、翻訳してみます.指摘、討論を歓迎して、あなたにも役に立つことを望んでいます.原文:How I write Go HTTPサービスafter seven years
以下、本稿ではr 59(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変数を1回だけ取得することで、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.HandlerFunchttp.Handlerを置き換えています.
func (s *server) handleSomething() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

この2つのタイプは多くの場合交換可能で、私にとって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は、1回のみ実行され、他の要求が論理処理が完了する前にブロックされることを保証する.
  • エラー時にログの完全性をキャプチャおよび保証するために、エラーチェックは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で私と議論することを歓迎します.