ぺい

渋谷系アドテクエンジニアの落書き

Go言語で薄く作るAPI(go-chi/chi) #2 カスタムハンドラ

GoっぽくAPIを作る

前回

Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築 - ぺい

今回

  • カスタムハンドラ
    もっと書く予定だったのですが、ハンドラだけですごいボリュームなったので、ハンドラだけにしましたw

go-chi-api-example/02 at master · pei0804/go-chi-api-example · GitHub

$ go get github.com/pei0804/go-chi-api-example
$ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/02
$ make run
go build -o build . && ./build
2017/11/26 00:05:04 Starting app

$ make curl-members-id id=01
curl -H 'Auth:admin' http://localhost:8080/api/members/01
{
    "id": 1,
    "name": "name_1"
}

カスタムハンドラ

カスタムハンドラを導入することで、コントローラー(以降ハンドラとする)の実装が少し変わります。まずは、以下のソースを見てください。

カスタムハンドラなし

// Show endpoint
func (h *Handler) Show(w http.ResponseWriter, r *http.Request) {
    type json struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := json{ID: id, Name: fmt.Sprint("name_", id)}
    respondJSON(w, http.StatusOK, res)
}

// respondJSON レスポンスとして返すjsonを生成して、writerに書き込む
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
    response, err := json.MarshalIndent(payload, "", "    ")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    w.Write([]byte(response))
}

普通に実装すると、type HandlerFunc func(ResponseWriter, *Request)の型で実装して、ハンドラを作成しています。net/httpパッケージの実装には以下のように書かれています。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

カスタムハンドラあり

// Show endpoint
func (c *Controller) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := User{ID: id, Name: fmt.Sprint("name_", id)}
    return http.StatusOK, res, nil
}

カスタムハンドラを使った実装をしているハンドラは、type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)という型で実装しています。では、これは何に準拠された実装になるのか?また何が嬉しいのか? 実はこれも、type HandlerFunc func(ResponseWriter, *Request)に最終的に合わせて処理をしています。それを理解するには、もう少しソースを追いかける必要があります。

func (s *Server) Router() {
    c := NewController()
    s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection"))
        api.Route("/members", func(members chi.Router) {
            members.Get("/{id}", handler(c.Show).ServeHTTP) // ここでハンドラが呼ばれる
            members.Get("/", handler(c.List).ServeHTTP)
        })
    })
    s.router.Route("/api/auth", func(auth chi.Router) {
        auth.Get("/login", handler(c.Login).ServeHTTP)
    })
}

members.Get("/{id}", handler(c.Show).ServeHTTP) というのを一つずつ分解すると、

Get(pattern string, h http.HandlerFunc)

  • chiパッケージのメソッド
  • patternはエンドポイントになる文字列
  • http.HandlerFunctype HandlerFunc func(ResponseWriter, *Request)

handler(c.Show).ServeHTTP

  • type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error) という今回作成した型
  • c.Show ハンドラc.type Controller structを指し、
    Showfunc (c *Controller) Show(http.ResponseWriter, *http.Request) (int, interface{}, error)です
  • ServeHTTPfunc (handler) ServeHTTP(http.ResponseWriter, *http.Request) というメソッドを呼んでいます

つまり

Get(pattern string, h http.HandlerFunc)の型にハマるように、ハンドラを作成する。 逆にいうと、カスタムしなければ、members.Get("/{id}", h.Show)とそのまま格納しても問題なく動きます。
逆にカスタムハンドラの方は、members.Get("/{id}", handler(c.Show).ServeHTTP)というように少し書き方が変わっています。重要なのは、ここです。

ServeHTTPが重要

type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    defer func() {
        // panic対応
        if rv := recover(); rv != nil {
            debug.PrintStack()
            log.Printf("panic: %s", rv)
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
    }()
    // h(w, r)はhandler型のハンドラの実行 
    // func(http.ResponseWriter, *http.Request) (int, interface{}, error)
    status, res, err := h(w, r)
    if err != nil {
        log.Printf("error: %s", err)
        respondError(w, status, err)
        return
    }
    respondJSON(w, status, res)
    return
}

ServeHTTPhandlerという型から使えるメソッドです。
ServeHTTPfunc ServeHTTP(http.ResponseWriter, *http.Request)という型のメソッドです。 そして、status, res, err := h(w, r)で、c.Show()が実行されています。自分はここで何が起こっているのか、理解に時間がかかりました。 ちなみに、Method(method, pattern string, h http.Handler)というメソッドを使う場合は、Method("GET", "/{id}", handler(c.Show))と書くだけでいい感じに実行されます。何故か?

// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and hangs up the connection.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

net/httpパッケージには、Handlerというinterfaceがあり、これに準拠しているメソッドを定義している場合、Handlerとして使えるという記述がある。しかし、HanderFuncの場合は、そういったインターフェースは用意されていないので、最終的にHandlerFuncの型に合わせる。
そのため、members.Get("/{id}", handler(c.Show).ServeHTTP)と書くに至る。

カスタムハンドラを作ると何が嬉しい?

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 前後自由に書ける
    status, res, err := h(w, r)
    // 前後自由に書ける
}

ざっくり言うと、上みたいな感じでハンドラは前後でやりたいことを書くことが出来ます。案件に合わせた柔軟なハンドラを作れるので、かなり重宝します。 他にも、入口と出口のルールづくりが出来るので、必ずこの値がほしい、必ずこの値を返してほしいというインターフェース的なものが出来るので、忘れて本来やるべきだったものをやっていなかったということを防げます。例えば、以下のようなものです。

// Show endpoint
func (h *Handler) Show(w http.ResponseWriter, r *http.Request) {
    type json struct {
        ID   int    `json:"id"`
        Name string `json:"name"`
    }
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    res := json{ID: id, Name: fmt.Sprint("name_", id)}
    response, err := json.MarshalIndent(payload, "", "    ")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte(err.Error()))
        return
    }
    // 以下の2行を書き忘れるても、処理は正常に終わってしまうなど
    // w.Header().Set("Content-Type", "application/json")
    // w.WriteHeader(status)
    w.Write([]byte(response))
}

カスタムハンドラは非常に便利なので、必要な入り口と必要な出口を案件に合わせて定義すると良さそうです。この記事を書く上で、記事を読んだり、GitHub上のソースを見てハンドラを参考にしたり、自分なりに考えてヘルパーを作ったりしたのですが、結局使えないというドツボにハマったので、シンプルに最低限必要なもので対応するのが良さそうです。

一応解説しておくと、今回のハンドラは以下のように使います。 func(http.ResponseWriter, *http.Request) (int, interface{}, error)

次回

エラーハンドリングについて書く予定です

Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築

GoっぽくAPIを作る

Goには、様々なWAFが存在しますが、今ひとつデファクトスタンダードなものが未だにありません。その背景としてあるのは、標準パッケージで十分実装出来る。WAFは開発速度を上げてくれますが、そのWAFの開発そのものが製作者のモチベーションに左右されたり、内部実装が分かりにくくなるなど、そんなこと気にするくらいなら、小さいライブラリを組み合わせて、ある程度書いちゃうという人を結構見かけます。

でも、WAFを使わずにGoらしく実装する時ってどうやって書けばいい感じに出来るんだろうというのは、自分の中で結構気になるトピックでした。様々な実装例などから、自分なりにAPIを組んでみたので、シリーズものにして、実装を解説していきたいと思います。

go-chi/chi

今回使うのは、go-chi/chiというnet/httpのインターフェースに準拠したルーティングライブラリを使います。 採用理由としては、高速であること、ルーティングが見やすいなどと言った点です。

Goには、他にも薄いライブラリ系で有名なものがあるので紹介しておきます。

この記事で書くこと

実行

ソースコード

$ go get github.com/pei0804/go-chi-api-example
$ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/01
$ make run
go build -o build . && ./build
2017/11/26 00:05:04 Starting app

$ make curl-members-id id=01
curl -H 'Auth:admin' http://localhost:8080/api/members/01
{
    "id": 1,
    "name": "name_1"
}

コンストラクタ

// Server Server
type Server struct {
    router *chi.Mux
}

// New Server構造体のコンストラクタ
func New() *Server {
    return &Server{
        router: chi.NewRouter(),
    }
}

アプリケーション内で、保持したいものや、handlerに渡したいものなどをServerという構造体に作成して、簡単なコンストラクタを作成しています。これらをの構造体を作成せずに、アプリケーションを作成していくと、後でテストコードを書く際に辛いことになるので、基本的にはこういうものは作成した方がいいです。

ルーティング

// Router ルーティング設定
func (s *Server) Router() {
    h := NewHandler()
    s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア
        api.Route("/members", func(members chi.Router) {  // /api/members/* でグループ化
            members.Get("/{id}", h.Show)  // /api/members/1  などで受け取るハンドラ
            members.Get("/", h.List) // /api/members で受け取るハンドラ
        })
    })
        // Authする何かのエンドポイントという想定
    s.router.Route("/api/auth", func(auth chi.Router) {
        auth.Get("/login", h.Login)
    })
}

ルーティング情報はchiを使うと自動生成することが出来たので、貼っておきます。

/api/*/members/*

/api/*/members/*/{id}

/api/auth/*/login

ルーティングの考えかたやGET POSTなどのメソッドについて分からない場合は、過去にAPI設計についてのワークショップ開催した時の資料あるので、そちらを見てください。

www.slideshare.net

ミドルウェア

ルーティングで登場したミドルウェアについて軽く触れます。 サーバーサイドの開発を何度かしたことがある人なら知っているとは思いますが、認証を通したい、処理を通る前にRequestIDのようなものを発行しておきたいなど、APIをラップする機能の総称です。

http://stackphp.com/img/onion.png 引用元:Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com

よくある具体例として、panicのrecoverが分かりやすいです。

// Recoverer is a middleware that recovers from panics, logs the panic (and a
// backtrace), and returns a HTTP 500 (Internal Server Error) status if
// possible. Recoverer prints a request ID if one is provided.
//
// Alternatively, look at https://github.com/pressly/lg middleware pkgs.
func Recoverer(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            // panicがあればrvrに入っているエラー内容をかき出す
            // 注意点として遅延関数内で実行する必要がある
            if rvr := recover(); rvr != nil { //https://golang.org/pkg/builtin/#recover

                logEntry := GetLogEntry(r)
                if logEntry != nil {
                    logEntry.Panic(rvr, debug.Stack())
                } else {
                    fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr)
                    debug.PrintStack()
                }
            // 500エラーを返す
            http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            }
        }()
        //  ハンドラを実行する
        next.ServeHTTP(w, r)
    }
    // 上記の処理をhandlerFuncに渡す
    return http.HandlerFunc(fn)
}

上記のような処理を本来は全てのエンドポイントに書く必要がありますが、それは大変過ぎますし、また同じようなコードを全箇所に書くのはナンセンスです。そういった問題を解決してくれたのがミドルウェアです。 ちなみに、使い方は以下のような感じです。

// Middleware ミドルウェア
func (s *Server) Middleware() {
    s.router.Use(middleware.RequestID)
    s.router.Use(middleware.Logger)
    s.router.Use(middleware.Recoverer)
    s.router.Use(middleware.CloseNotify)
    s.router.Use(middleware.Timeout(time.Second * 60))
}

// ~~~~
s.router.Route("/api", func(api chi.Router) {
        api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア
        api.Route("/members", func(members chi.Router) {
            // ~~~
        })
    })
       // ~~~
})

今回作成したものは、データベースのコネクションを渡して、何かデータベースと通信して〜〜ということを想定したようなものを作っています。ミドルウェアに何か引数を渡したい時は、以下のような感じで出来ます。

func Auth(db string) (fn func(http.Handler) http.Handler) { // 引数名を指定してるのでreturnのみでおk
    fn = func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Auth") // Authというヘッダの値を取得する
            if token != "admin" { // adminという文字列か見る
                // エラーレスポンスを返す
                // この関数については後で書きます
                respondError(w, http.StatusUnauthorized, fmt.Errorf("利用権限がありません"))
                return
            }
            // 何も無ければ次のハンドラを実行する
            h.ServeHTTP(w, r)
        })
    }
    return
}

ハンドラ

ここまで何度も登場してきたハンドラというワードですが、GoのAPIではこのハンドラによって様々な処理を可能にしています。分かりやすい記事があったのでリンクを貼っておきます。

// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
//
// Depending on the HTTP client software, HTTP protocol version, and
// any intermediaries between the client and the Go server, it may not
// be possible to read from the Request.Body after writing to the
// ResponseWriter. Cautious handlers should read the Request.Body
// first, and then reply.
//
// Except for reading the body, handlers should not modify the
// provided Request.
//
// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes
// that the effect of the panic was isolated to the active request.
// It recovers the panic, logs a stack trace to the server error log,
// and hangs up the connection.
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ServeHTTPというのが重要で、これに最終的に合わせてキャストすると、http.Handlerが使えます。便利過ぎる! なので、今回は以下のような記述になっています。

http.ListenAndServe(fmt.Sprint(":", *port), s.router)

今回はハンドラをカスタムをしていませんが、ハンドラに渡したい、返してほしいものを変えることが出来ます。以下に分かりやすいサンプルがあったので貼っておきます。 次回の記事では、カスタムハンドラの一例を示す予定です。

type Handler func(w http.ResponseWriter, r *http.Request) error

// 共通の処理などを受け口を作るとロギングにも良い
// WAFのここらへんの設計は結構面白い
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h(w, r); err != nil {
        // handle returned error here.
        w.WriteHeader(503)
        w.Write([]byte("bad"))
    }
}

func main() {
    r := chi.NewRouter()
    r.Method("GET", "/", Handler(customHandler))
    http.ListenAndServe(":3333", r)
}

func customHandler(w http.ResponseWriter, r *http.Request) error {
    q := r.URL.Query().Get("err")

    if q != "" {
        return errors.New(q)
    }

    w.Write([]byte("foo"))
    return nil
}

コマンドラインのフラグからの動作変更

ちょっとした動作変更などは、フラグを使うと便利です。ケースによっては設定ファイルでやるべきこともありますが、簡単なアプリケーションなら、フラグパッケージでサクッとやると便利です。絶対にやるべきでないのは、変える可能生の高いものをハードコーディングしてしまうことです。それをするくらいなら、flagパッケージを使ってください。

var (
    port   = flag.String("port", "8080", "addr to bind") // -port=9000 などにするとポート変更が出来る
    env    = flag.String("env", "develop", "実行環境 (production, staging, develop)") // 実行環境変えたり
    gendoc = flag.Bool("gendoc", true, "ドキュメント自動生成")
)
flag.Parse() <- これを必ず書く

まとめ

もう少し具体例にしたいので、段々アプリケーションっぽいものにしていきます。

次回やること

開発環境をDockerにして良かったこと

f:id:tikasan0804:20171101212036p:plain

Dockerいいぞ

Vagrantすげー便利ーー!!」から、「Docker最高!」になったのですが、個人的にもっと早く知りたかった系の話だったので、記事にしておこうと思います。 ちなみに、筆者は学生です。プロダクトに関わるタイミングとして、個人で作っているものとか、アルバイトや学校の作品などです。

プロジェクトを抱えれば抱えるほどDockerは神

私の場合、色んなところでモノ作ったりしていることもあって、Dockerの恩恵を得ることが出来ています。 逆に言うと、単一プロジェクトしか関わっていない人なら、Vagrantのが本番環境の再現性が高いので、Dockerに頑張って移行しなくても良いと思います。

ぶっ壊して、すぐ作り直すが出来る

壊して作り直す面で、本当に素晴らしいパフォーマンスを発揮します。最初のビルドは構成によっては、時間がかかりますが、二回目からはキャッシュされたイメージから作り直しが行われるので、驚く程、短時間で終わらせることが出来ます。一方で、Vagrantは作り直すことは出来ますが、環境が壊れた時に作り直しに時間を要するので萎えます。(気長に待てば終わりますが)

容量が小さい

まともに環境を用意するよりは容量を小さく抑えることが出来ます。なので、複数のプロジェクトを抱えていたとして、容量で死ぬという問題は起きにくいです。また容量が問題になったとして、一時的に削除すれば良いだけで、必要になった時に、また環境を戻せば良いだけです。

データストア系をクリーンな状態で使える

私は現在の学校に入ってからプログラムを勉強し始めたのですが、当初は「XAMPP」で開発をしていました。そして、色んな開発をしていく中で起きたのが、MySQL周りがカオスになるという問題でした。また、これがチーム制作になるとさらに悲惨を極める状態になります。色々ありすぎたので、以下に箇条書きします。

  • 指定されたユーザーが存在しません
  • 文字コードが違います
  • XAMPP動かない
  • MySQLの謎のエラー
  • XAMPPのバージョン違い
  • 設定などをコードにして残しにくい

Dockerにすると、こういった問題は一切起きません。例えば、以下の要件でデータベースを用意する必要があったとします。

  • mysqlのバージョンは5.7
  • 専用ユーザー user: sample_user password: password
  • 3306ポートでアクセス出来るようにしたい
  • mysqlというフォルダ名でデータを永続化させたい
  • 文字コードはutf8

docker-compose.yml にすると以下のような感じになります。あとは、これをメンバーに配布すれば終了です。最高に楽ちん。

version: '3'
services:
  mysql:
    image: mysql:5.7
    environment:
        - MYSQL_ROOT_PASSWORD=password
        - MYSQL_DATABASE=sample
        - MYSQL_USER=sample_user
        - MYSQL_PASSWORD=password
    ports:
        - 3306:3306
    volumes:
        - "./mysql:/var/lib/mysql"
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci

これと同じ要件を、XAMPPで実現するのは辛いですし。ローカルでMySQLインストールしたとして、一体どんな設定がされているかも分かったもんじゃありませんし、Vagrantで用意するにしては手間がかかり過ぎます。こういった小規模なレベルだと、Dockerはとても手軽で便利です。

設定をコードに残せる

Dockerはコマンドだけで起動することは出来ますが、基本的に Dockerfiledocker-compose.yml などを作成して環境構築する場合がほとんどです。これらをgit管理に含めておくと、環境がコードで把握することが出来るので便利です。 ansibleなどのプロビジョニングツールを使えば、同様のことは出来ますが。。。

デプロイが簡単に出来る

デプロイに必要な環境が出来上がっているので、それをそのまま使ってデプロイ出来るので、とても便利です。
GKEなどを使うとそのままデプロイ出来ます。

クラスタリングが簡単に出来る

私はあまりやったことがないのですが、スケーリングなどが比較的簡単に行えるようです。 qiita.com

Dockerで出来ないこと・イケてないところ

良いことばかりではありません!

Windowsは10以外は快適ではなさそう

qiita.com

  • Docker for Windows Windows 10 のみ対応。今後、対応OSを増やしていく予定とのこと。
  • Windows Machine上のLinux VMでDocker

上記を見た限りだるそう。 自分はMacなので、よく分かりませんが、windows10じゃないと辛そうです。

カーネルの設定を細かくいじれない

Dockerはある程度の制限の中でしか設定出来ないので、こういった案件がある場合は、Vagrant案件ですということになりそうです。

まとめ

  • さくっと作って壊せる
  • MySQLだけがほしいみたいな小規模な案件にもすぐ使える
  • 容量が軽い + 一杯になれば一時的に消せば良い
  • 色んな案件抱える程ありがたい

direnvを使ってGo開発をいい感じにする

GOPATH以下が汚くなってません?

以下のような感じで、GOPATHを設定して開発をしていると・・・

export GOPATH=$HOME/go
export PATH=$PATH::$GOPATH/bin

ghqリポジトリを一括で管理しているので、探すのに困ることはありませんが f:id:tikasan0804:20171009150638p:plain

352個もソースがありました。こういうことをずっとしていると、自分の環境だけで動くみたいなソースになっていることに気づけなかったりします。あと、Goglandのindexing終わらない事案とか・・・w

また、仕様としてimportが以下のなるのですが、リポジトリ名やユーザーが変わると全滅するソースになったりします。一応GOPATHをプロジェクトごとに設定すれば解決出来ますが、それは正直面倒ですよね?それを解決してくれるのが、direnvです

package main

import "github.com/pei0804/direnv-example/sample"

func main() {
    sample.Sample()
}

やりたいこと

  • importを必要最低限だけ記述したい
  • 必要なパッケージだけをGOPATH以下に入れたい
  • GOPATHを動的にいい感じに設定したい
package main

import "app/sample"

func main() {
    sample.Sample()
}
package sample

import "fmt"

func Sample() {
    fmt.Println("Hello World")
}

最終的に上記のようなコードで動作させます。

direnvを使ってGOPATHをいい感じにする

github.com

direnv is an environment switcher for the shell. It knows how to hook into bash, zsh, tcsh and fish shell to load or unload environment variables depending on the current directory. This allows project-specific environment variables without cluttering the ~/.profile file.

つまり、シェルの環境変数いい感じ適用するぜ!

では、READMEにあるように、シェルそれぞれに合った設定をしてください。 自分はzshなので、以下の記述を追加しました。

# ~/.zshrc
eval "$(direnv hook zshrc)"

具体例

とりあえず動かす

$ git clone https://github.com/pei0804/direnv-example.git <--- あえてgit cloneで適当に配置する
$ cd direnv-example
$ make direnv                                                                                                                                                                                                                                                    
direnv allow
direnv: loading .envrc                                                                                                                                                                                                                                                      
direnv: export ~GOPATH
$ make run
go run main.go
Hello World

上記の手順で動いたと思います。 これでどこでも動かせる+必要なパッケージのみのクリーンな環境が出来ました。 ちなみにファイル構成は以下のような感じです。

.
├── LICENSE
├── Makefile
├── README.md
└── go
    └── src
        └── app
            ├── Makefile
            ├── main.go
            └── sample
                └── sample.go

シェルに設定していないGOPATHが正しく動作した理由は、.envrvの記述にあります。 内容は簡単なもので、今いる場所/go で新たなGOPATHを追加しているだけです。

export GOPATH=$(pwd)/go:$GOPATH

この操作で、さっき追加したソースはGoのルールに乗っ取り正しく構成されたソースになります。自分は最近この手法で開発するケースが増えています。特にDockerベースの開発をする時は、この手法がとてもマッチしてていい感じです。(また後日記事にします) ちなみに、最近いい感じになってきているgolang/depは、こういう構成で開発することを想定しているっぽいです。

ソース

github.com

GolangのgoaでAPIをデザインしよう(ベース作成編)

f:id:tikasan0804:20170505212036p:plain

goaはいいぞ!

Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください

ベースの作成が微妙にだるい

結構goaを使う機会が増えてきて、微妙にだるいなーと思ったことがありました。 goaで何度かAPI作ったことがある人なら分かると思いますが
「使うコード大体決まってるのに、毎回作るの面倒・・・」

ということで、自分がよく使う構成をベースのテンプレートを作成しました。個人的には、これをCLIでやりたい気持ちもありましたが、goaのv2が出てからやろうかなという気持ちです。

作ったもの

github.com

上記2パターン作成しました。それぞれにREADMEを置いてます。 良い書き方や間違っている部分あれば、PRください。

builderscon 2017 Tokyo 控えめに言って最高だった

builderscon.io

行ってきました!大満足でした!

buildersconとは?

公式サイトに以下のようにあります。

buildersconは「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭りです。 buildersconではトークに関して技術的な制約はありません、特定のプログラミング言語や技術スタックによるくくりも設けません。 必要なのは技術者達に刺激を与えワクワクさせてくれるアイデアのみです。

あなたが実装したクレイジーなハックを見せて下さい。あなたの好きな言語のディープな知識をシェアしてください。あなたの直面した様々な問題と、それをどう解決したかを教えてください。未来技術のような未知の領域について教えてください。

buildersconに参加して、あなたの情熱をシェアしてください!皆様のご参加をお待ちしております!

f:id:tikasan0804:20170805200427p:plain

ノベルティのTシャツはこんな感じでした。いい感じですね。

なぜこのカンファレンスは面白いのか

このカンファレンスは、特定のテーマはあるものの、言語やフレームワークや設計思想など発表内容に、ほとんど縛りがありません。なので、多種多様な発表が凝縮されていて、常に発見があります。何かの技術をテーマとしたカンファレンスが面白いのは当然ですが、こういった全く違う趣向のものも面白いなと率直に感じました。

どんなセッションがある?

builderscon.io トップレベルで変態なセッションでした。内容はタイトルからは分からないくらい高度で、最後には会場全体が「すげえ・・・」ってなってました。

builderscon.io メルカリのバナー作成をUI含めて自動生成するというすごそうなツールのお話でした。作るのにかかる工数をどこまでに抑えるか、今後どのような展開が予想されるかなど、様々な意思決定や実際に組んだコードの意図なども含めて話があり、メルカリのエンジニアすげえ・・・って感じでした。

builderscon.io 正直僕には難しすぎて、鼻水が出ました。ただただすごいです。

builderscon.io 大規模サービスになると何が辛いんだっけみたいな話から、具体的な解決パターンなどが詳しく解説されていて、かなり参考になる話でした。これは一度見直さなければ・・・という感じのセッションです。

builderscon.io Slack社のSlackのコア部分がPHPで構成されていて、それは一体どのようにしてトラフィックに耐えているのか、またどのようにして、パフォーマンス向上を実現したのかについてのセッションでした。
主にHHVMとPHP7についての話でした。(Slackは最終的にHHVMを採用しました)このセッションは最後の質疑応答が非常に面白かったです。中でも以下の回答が秀逸でした。

セッション動画

www.youtube.com 今後恐らくここに順次アップされていくと思われます。他にも見たいセッションがたくさんあったので、アップされるのが楽しみです。

まとめ

次回は参加するだけではなく、何かLTでも良いので発表出来たらなと考えていたりします・・・。
主催の@lestrratさん、運営の方々、スポンサーのみなさん。ありがとうございました!本当に良かったです。

builderscon 2017 Tokyo: Building high performance push notification server in Go

builderscon.io

github.com

GoのOSSのGaurunを使ってアプリユーザーにプッシュ通知を送るための話。
※聞きながら書いたので、雑になってます

プッシュ通知の仕組み

Push通知のためのサービスを通して、ユーザーに通知が行く。
APNs GCM/FCMなどを使った話をします。

APNs

種類

  • APNs Binary Provider API(レガシー)
  • APNs Provider API これだと、JSONペイロードcurlで投げるだけでプッシュ通知を試せたりするくらい気軽になる。

GCM/FCM

  • HTTPS
  • サーバーキー

GoogleはFCM使えと言っている。
基本はJSONペイロードcurlで投げるだけでpush通知を投げれる。

ネットワークを介す処理だから遅い

APNsとGCM/FCMとの通信には、ある程度時間がかかる。これはサーバーがアメリカにあったりするからです。特にAPNsは結構遅い。

構成例

サーバー -> nginx -> Gaurun -> HTTP2 -> APNs/GCMなど -> アプリ

通知するタイミングは、お気に入りつけたや購入したコメントしたなど。
出来れば短時間で対象ユーザーに送りたい。

短時間で大量にさばくには、ネットワークレイテンシが高い問題を解消する必要があった。(これが難しい)

旧メルカリの構成

通知が遅くなる問題を見たら、処理と通知が同期的に行われていたから、ネットワークレイテンシと比例して遅くなる。またバッチ処理もあまり効率が良くなかったので、全ユーザー通知も時間がかかっていた。

通知を非同期にした

APIサーバーが通知Queueに送って、それらを順次処理していく形にした。これで劇的なパフォーマンスの改良が可能になった。QueueはQ4Mを使った。

PHPがそもそも遅いので限界があった

PHPは並行処理が苦手なので、そこまで速くならなかった。

Goと相性が良かった

ネットワークレイテンシを下げる必要がある。スループットを改善する。このような需要とマッチしていた。

Gaurun

プッシュ通知サーバーに使うGoOSS
JSON baseのAPI(HTTP)

提供しているAPI

  • プッシュ通知
  • モニタリング(errorの数やgoroutineの数)
  • コンフィグ

Goを採用した理由

  • 高い並行性
  • 高いパフォーマンス
  • 標準パッケージが使いやすい

構成

  • HTTPサーバー
  • APNs GCM/FCMのプロキシ
  • queueと並行処理が出来る

gorutineはwerkerはpusherを持っている。
werkerはpusherの数しか知らない。pusherはpushするだけ。

コネクションハンドリング

http.Clientを使っている(net/http) 設定は、http.Transportを使っている。昔は結構自力で書いてたけど、いまは標準パッケージにお任せしている。

Timeoutについて

サーバーのタイムアウトなのか?クライアントのタイムアウトなのか?接続する時のタイムアウトなのか?とにかく色んなタイムアウトがある。

net/httpパッケージ

  • net.Dial
  • http.Transport
  • http.Client
  • http.Server

上記それぞれにTimeoutが用意されている

Gaurunのパフォーマンスを上げる

TOMLで設定をチューニングする必要があります。なぜなら、初期設定のままだと、結構保守的な設定になっています。

  • core.workers ワーカーの数
  • core.queues channelのサイズ

などなど、色々な設定が出来る。

pusherの数

core.workers x core.pusher_max を調整して、メモリを無駄なく使うと良い。

Keep-Aliveの数

keep-aliveはあまりにも大きすぎる数値を指定すると、場合によってはAPNsなどに拒否されることがあったりするかも?

Bulk enqueue

JSONペイロードのnotification[]の中に複数指定して、リクエストすればおk。デフォルトでは、100件までとしている。

Device tokenのスクリーニング

無効化されたtokenを定期的に削除したい。無駄に送信するとパフォーマンスが落ちるので大事です。Gaurunでは成功、失敗が error_logで取れるので、これを使えばスクリーニングが出来ます。

メルカリでの実際の運用
JSON.log -> S3 -> Batch -> MySQL