tnakata's blog

programming :-)

golangでpanicをrecoverしたときにスタックトレースを表示する

golangではそもそもpanicしないようにコードを書くべきだしpanicが発生するならここで発生するのだと把握した上でコードを書くべきだ、という認識はある。とはいえ、golang書き始めてまもないので「念のために」recoverをコードに書いておきたかった。

具体的なコードは以下の通り。

https://play.golang.org/p/CxiR4j5Q6F_a

package main

import (
    "fmt"
    "log"
    "runtime"
)

func main() {
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    defer func() {
        if err := recover(); err != nil {
            log.Printf("[ERROR] %s\n", err)
            for depth := 0; ; depth++ {
                _, file, line, ok := runtime.Caller(depth)
                if !ok {
                    break
                }
                log.Printf("======> %d: %v:%d", depth, file, line)
            }
        }
    }()

    a := []int{1, 2}
    fmt.Println(a[2])
}

結果は以下の通り。

2009/11/10 23:00:00 main.go:13: [ERROR] runtime error: index out of range
2009/11/10 23:00:00 main.go:19: ======> 0: /tmp/sandbox464635953/main.go:15
2009/11/10 23:00:00 main.go:19: ======> 1: /usr/local/go/src/runtime/asm_amd64p32.s:472
2009/11/10 23:00:00 main.go:19: ======> 2: /usr/local/go/src/runtime/panic.go:491
2009/11/10 23:00:00 main.go:19: ======> 3: /usr/local/go/src/runtime/panic.go:28
2009/11/10 23:00:00 main.go:19: ======> 4: /tmp/sandbox464635953/main.go:25
2009/11/10 23:00:00 main.go:19: ======> 5: /usr/local/go/src/runtime/proc.go:195
2009/11/10 23:00:00 main.go:19: ======> 6: /usr/local/go/src/runtime/asm_amd64p32.s:1040

2017年を振り返る

2017年はWebエンジニア1年目の年だった。つらつらとやったことを振り返る。

最初の半年はRailsを書いていた。初めてのWeb開発だったので最初はさすがに覚えることが多く大変だったが、ビジネスロジックが中心だったので1ヶ月もするとだいたい慣れてきた。ビジネスロジックを単に実装しているだけだとフレームワークの昨日の暗記ゲームみたいな気がしてきた。半年くらいたって、プログラマーとしてもっと本質的なことを勉強しないといけないと思うようになった。ただこの時点では、本質的なことを学ぶといっても何が本質的なことかわからなかった。

7月くらいから数ヶ月、PHPを書いていた。既存サービスの改修。PHP書いたことは正直あまり印象に残っていないが、アプリケーション以外の部分も担当するようになった。開発環境が用意されてないからVagrantスクリプト書いたり、Apache JMeterで簡単な負荷検証やったりした。この辺りから本やブログを片っ端から読むようになって、プログラマーとして本質的なことがぼんやり見えてきた気がした。とりあえずコンピュータサイエンスかなと。

9月。仕事ではまだPHPを書いていた。コンピュータサイエンスわからんことばっかりだったからとりあえずOSの教科書読もうと思って、Operating System Conceptsを読もうとした。英語で1000ページくらい、気合いで読んだ。OSがやってることがなんとなくわかるようになった。プロセスやらスケジューリングやら。HTTP以下のレイヤーもよくわからんと思って、ハイパフォーマンスブラウザネットワーキングを読んだ。こういった本を読むと、不思議なことに今まで書いたコードとの繋がりが少し見えてきて楽しくなってきた。一方で、本を読んでばかりだったのでなかなか上手いコードを書けるようにならないのが気がかりだった。

10月。GoでAPIを書くことになった。フレームワークを使用しないことになってログまわりやらミドルウェアやらを自分で書くことになった。これが楽しくて、今までフレームワークに任せていた部分を理解できている気がした。CIまわりを整備したりもした。開発のチームリーダー的なポジションとなったが、他に議論できる知り合いもいなかったので悩んだ時はOSSのコードをよく参考にするようになった。ちょっとコード書けるようになってきたかもと浮かれた。コンピュータサイエンスの勉強では、引き続きOSまわりを勉強していたがやはりコードに落とし込めないことが気がかりだった。アルゴリズムの勉強も始めた。

2018年は以下のことを軸に自分のリソースを割り当てたい。

Goを書いていく

自分はまだ何も武器がないプログラマなので、Goの人、と言われるくらいになる。

コンピュータサイエンスを学ぶ

ここを勉強しておかないとプログラマとして死ぬ気がするのでやる。2018年前半はアルゴリズムを継続的にやる。後半はまた考える。

negroniの実装をちゃんと理解できていなかったので自分で書き直してみる

以前、ミドルウェアをうまく扱うパッケージとしてnegroniを紹介したが、改めてコードを読むとちゃんと理解できていなかったので自分で書き直してみる。車輪の再発明GitHubに上げるまでもないのでここに残す。

GitHub - urfave/negroni: Idiomatic HTTP Middleware for Golang

package chain

import (
    "net/http"
)

// Handler is an interface.
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
}

// HandlerFunc is an adapter to allow the use of ordinary functions as HTTP handlers.
type HandlerFunc func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)

// ServeHTTP simply calls f(w, r, next).
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    f(w, r, next)
}

// Middleware is a stack of middlwares.
type Middleware struct {
    handler Handler
    next    *Middleware
}

// ServeHTTP calls ServeHTTP of next middleware.
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    m.handler.ServeHTTP(w, r, m.next.ServeHTTP)
}

// Wrap converts http.Handler into chain.Handler
func Wrap(h http.Handler) Handler {
    return HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
        h.ServeHTTP(w, r)
        next(w, r)
    })
}

// New is the constructor of Middlewre
func New(handlers ...Handler) Middleware {
    return *build(handlers)
}

func build(handlers []Handler) *Middleware {
    var next *Middleware

    if len(handlers) == 0 {
        return voidMiddleware()
    } else if len(handlers) > 1 {
        next = build(handlers[1:])
    } else {
        next = voidMiddleware()
    }

    return &Middleware{handlers[0], next}
}

func voidMiddleware() *Middleware {
    return &Middleware{
        handler: HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {}),
        next:    &Middleware{},
    }
}

メモ。

  • ミドルウェアスタックをlinked listで実装している。
  • 配列からlinked listを構成するときに再帰している。
  • インターフェースの使い方うまいなー(なんかパターンがありそうな)。

使い方は以下の通り。

package main

import (
    "./chain"
    "net/http"
)

func hello1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello1\n"))
    next(w, r)
}

func hello2(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello2\n"))
    next(w, r)
}

func hello3(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    w.Write([]byte("Hello3\n"))
    next(w, r)
}

// MyHandler is a sample handler.
type MyHandler struct{}

// ServeHTTP greet to world.
func (mh MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!\n"))
}

func main() {
    n := chain.New(
        chain.HandlerFunc(hello1),
        chain.HandlerFunc(hello2),
        chain.HandlerFunc(hello3),
        chain.Wrap(MyHandler{}),
    )
    http.ListenAndServe(":8888", n)
}

参考

takayukinakata.hatenablog.com

supervisorの起動スクリプト

supervisorの最新版をeasy_installでインストールしたら起動スクリプトがなかったので簡単なやつを書いた。centos6.7で /etc/init.d/supervisors として配置する。

#!/bin/sh
#
# supervisord - this script starts and stops supervisord
#
# chkconfig: 35 80 20
# description: Autostart supervisord.

# Source function library.
. /etc/rc.d/init.d/functions

supervisorctl="/usr/bin/supervisorctl"
supervisord="/usr/bin/supervisord"
name="supervisor"

[ -f $supervisord ] || exit 1
[ -f $supervisorctl ] || exit 1

RETVAL=0

start() {
    echo -n "Starting $name: "
    $supervisord -c /etc/supervisord.conf
    RETVAL=$?
    if [ $RETVAL -eq 0 ]
    then
        echo -e "SUCCESS"
    else
        echo -e "FAILED"
    fi
    return $RETVAL
}

stop() {
    echo -n "Stopping $name: "
    $supervisorctl shutdown
    RETVAL=$?
    if [ $RETVAL -eq 0 ]
    then
        echo -e "SUCCESS"
    else
        echo -e "FAILED"
    fi
    return $RETVAL
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        stop
        start
        ;;
    status)
        $supervisorctl status
        ;;
esac

exit $RETVAL

参考

takayukinakata.hatenablog.com

deferでのrecoverのよくわからない挙動がよくわからない

deferで呼び出した関数でrecoverした場合にはpanicは発生しない。

https://play.golang.org/p/Z0oJ2I26wq8

package main

import "fmt"

func main() {
    defer recoverFunc()
    panic("Panic happens!")
}

func recoverFunc() {
    if err := recover(); err != nil {
        fmt.Printf("Recover: %v¥n", err)
    }
}

しかし、deferで呼び出した関数からさらに呼び出した関数でrecoverした場合にはpanicが発生する(recoverが働かない)。

https://play.golang.org/p/FTqz736dHD9

package main

import "fmt"

func main() {
    defer deferredFunc()
    panic("Panic happens!")
}

func deferredFunc() {
    recoverFunc()
}

func recoverFunc() {
    if err := recover(); err != nil {
        fmt.Printf("Recover: %v¥n", err)
    }
}

直感的に後者の場合でもpanicが発生しないかと思ったんだけどな。

Goでgraceful restartをミニマムに書く

前回の記事でgraceful shutdownをミニマムに実装した。その続きでgraceful restartをミニマムに標準パッケージのみで実装してみる。

takayukinakata.hatenablog.com

参考にしたのは以下のパッケージ。

github.com

実装

前回と同様、理解のために最低限の実装を行なっている。エラー処理もところどころ真面目にやっていない。

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "strconv"
    "sync"
    "syscall"
    "time"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    w.Write([]byte("Hello, middleware!\n"))
}

func main() {
    http.HandleFunc("/", indexHandler)
    srv := &http.Server{Addr: ":8888", Handler: http.DefaultServeMux}

    var wg sync.WaitGroup
    srv.ConnState = func(conn net.Conn, state http.ConnState) {
        switch state {
        case http.StateActive:
            log.Println("StateActive!!!")
            wg.Add(1)
        case http.StateIdle:
            log.Println("StateIdle!!!")
            wg.Done()
        }
    }

    if isMaster() {
        log.Printf("master pid: %d\n", os.Getpid())
        laddr, _ := net.ResolveTCPAddr("tcp", "localhost:8888")
        l, _ := net.ListenTCP("tcp", laddr)
        log.Println(l)
        supervise(l)
    }

    log.Printf("worker pid: %d\n", os.Getpid())
    fdStr := os.Getenv("__MASTER__")
    fd, _ := strconv.Atoi(fdStr)
    file := os.NewFile(uintptr(fd), "listen socket")
    defer file.Close()
    l, _ := net.FileListener(file)
    waitSignal(l)
    err := srv.Serve(l)
    wg.Wait()
    if err != nil {
        log.Fatal(err)
    }
}

func waitSignal(l net.Listener) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT)
    go func() {
        sig := <-c
        switch sig {
        case syscall.SIGINT:
            signal.Stop(c)
            l.Close()
        }
    }()
}

func isMaster() bool {
    return os.Getenv("__MASTER__") == ""
}

func supervise(l *net.TCPListener) {
    p, _ := forkExec(l)
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGUSR2)
    for {
        switch sig := <-c; sig {
        case syscall.SIGUSR2:
            new, _ := forkExec(l)
            p.Signal(syscall.SIGINT)
            p.Wait()
            p = new
        }
    }
}

func forkExec(l *net.TCPListener) (*os.Process, error) {
    progName, err := exec.LookPath(os.Args[0])
    if err != nil {
        return nil, err
    }
    pwd, err := os.Getwd()
    if err != nil {
        return nil, err
    }
    f, err := l.File()
    if err != nil {
        return nil, err
    }
    defer f.Close()
    files := []*os.File{os.Stdin, os.Stdout, os.Stderr, f}
    fdEnv := fmt.Sprintf("%s=%d", "__MASTER__", len(files)-1)
    return os.StartProcess(progName, os.Args, &os.ProcAttr{
        Dir:   pwd,
        Env:   append(os.Environ(), fdEnv),
        Files: files,
    })
}

動作確認すると、

// server
$ go run test.go // server起動

// handlerを修正: "Hello, middleware!\n" => "Hello, middleware!!!!!!\n"

// client1
$ curl localhost:8888

// server master process にUSR2シグナルを送る
$ kill -USR2 xxxxx

// client2
$ curl localhost:8888

// client1
$ curl localhost:8888
Hello, middleware! // 修正前のレスポンスが返る

// client2
$ curl localhost:8888
Hello, middleware!!!!!! // 修正後のレスポンスが返る

// server
// 停止することなく動いたまま

感想

ポイントは

  • 再起動のシグナルを受け取るmaster processと実際のサーバ処理を行うworker processを用意すること
  • master processは最初に作成したリスナーをworker process作成時に渡し、masterとworkerでリスナーを共有すること
  • master processは再起動のシグナルを受け取ったら新しいworker processを作成してから古いworker processにシグナルを送りgraceful shutdownをさせること

だろうか。ソケットまわりを勉強して「ソケットもファイルも同じインターフェースで処理できる」ということを知っていたが理解はしていなかった。しかし、実際に標準netパッケージのfunc (*TCPListener) Filefunc FileListenerを使用してリスナーとファイルの相互変換を実装したことで少しは理解できたかなと感じた。あ、リスナーとソケットは同じ意味くらいの認識でいます。 また、masterとworkerでソケットを共有するために、func StartProcessをうまく利用する実装方法を知ることができて勉強になった。

なお、masterとworkerでソケットを共有するというのは以下のブログにある図が非常にわかりやすかった。

blog.shibayu36.org

Goでgraceful shutdownをミニマムに書く

graceful shutdownの仕組みがわかってなかったので自分でミニマムに実装してみる。

コード量が小さく読みやすかったので以下のパッケージを参考にした。

github.com

github.com

実装

自分でミニマムに実装したコードは以下の通り。アクティブな接続が返ってこなかった場合とかも考えなくてはいけないが、ここでは理解のため手元で動かして簡単に試す程度の実装をしている。

package main

import (
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    time.Sleep(10 * time.Second)
    w.Write([]byte("Hello, middleware!\n"))
}

func main() {
    http.HandleFunc("/", indexHandler)

    srv := &http.Server{Addr: ":8888", Handler: http.DefaultServeMux}

    var wg sync.WaitGroup
    srv.ConnState = func(conn net.Conn, state http.ConnState) {
        switch state {
        case http.StateActive:
            log.Println("StateActive!!!")
            wg.Add(1)
        case http.StateIdle:
            log.Println("StateIdle!!!")
            wg.Done()
        }
    }
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        log.Fatal(err)
    }
    waitSignal(l)
    err = srv.Serve(l)
    wg.Wait()
    if err != nil {
        log.Fatal(err)
    }
}

func waitSignal(l net.Listener) {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT)
    go func() {
        sig := <-c
        switch sig {
        case syscall.SIGINT:
            signal.Stop(c)
            l.Close()
        }
    }()
}

動作確認すると、

// server
$ go run test.go // サーバ起動

// client
$ curl localhost:8888 // curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!! // 特に何もしない

// client
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888 // 再度curl実行

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C // Ctrl+CでSIGINTを送る

// client
$ curl localhost:8888
Hello, middleware!
$ curl localhost:8888
Hello, middleware! // 正常にレスポンスが返る

// server
$ go run test.go 
2017/12/20 11:10:03 StateActive!!!
2017/12/20 11:10:13 StateIdle!!!
2017/12/20 11:11:59 StateActive!!!
^C2017/12/20 11:12:09 StateIdle!!! // 既に確立されたコネクションは正常にレスポンスを返している
2017/12/20 11:12:09 accept tcp [::]:8888: use of closed network connection
exit status 1 // 確立されたコネクションが全てレスポンスを返してからサーバシャットダウンしている

感想

ポイントは

  • プロセスを終了するシグナルをsignal.Notifyで捕まえて処理を変更すること
  • http.StateActiveな接続が存在する場合はプロセスがすぐに終了しないようにすること

だろうか。最初はリスナーをクローズすると確立されたConnも終了してしまうのかと思っていたが、ここに関してはTCPListener.Closeがうまい感じにやってくれている。このためリスナーをクローズしたタイミングで新規の接続は受け付けないといったことも実現できている。

https://golang.org/pkg/net/#TCPListener.Close

Close stops listening on the TCP address. Already Accepted connections are not closed.

Goアプリケーションをsupervisorでdaemonとして起動する

起動したGoのアプリケーションをdaemonプロセスとして動かす。なお、Centos6.7を使用している。

Supervisor: A Process Control System — Supervisor 3.3.3 documentation

Install

$ wget https://bootstrap.pypa.io/ez_setup.py -O - | python
$ easy_install supervisor
$ supervisord -v
3.3.3
  • yumでインストールすると古いバージョン(2.x.x系)がインストールされる。古いバージョンだとsupervisorctlコマンドでstartstopを実行した時にコマンドが返ってこないので注意

Configuration

設定ファイル(/etc/supervisord.conf)を作成する

設定は以下を参考にした。設定例は補足に記載。

SupervisorでGolangで書かれたアプリケーションのデーモン化をした話 - こんちゃんブログ

詳細は以下を参照

http://supervisord.org/configuration.html

How to use

各コマンドの詳細は以下を参照

http://supervisord.org/running.html

日本語だとこちらが詳しい

supervisorctlについて調べてみた - Masteries

supervisord

supervisorを起動するためのコマンド。以下のコマンドでsupervisorを起動する。なお、設定ファイルでautostart=trueと設定しているので、sueprvisordを起動するとGoアプリケーションも自動で立ち上がっている。

$ supervisord -c /etc/supervisord.conf

supervisorctl

supervisorでdaemonとして動かしたいアプリケーションの起動・停止するための主なコマンド

  • supervisorctl start <name>
  • supervisorctl stop <name>
  • supervisorctl restart <name>
    • supervisorの設定ファイルを再読込せずにアプリを再起動
  • supervisorctl update
    • supervisorの設定ファイルを再読込してアプリを再起動
  • supervisorctl status
$ supervisorctl start goapp
goapp: started
$ supervisorctl status
goapp           RUNNING   pid 2675, uptime 0:00:02

$ supervisorctl stop goapp
goapp: stopped
$ supervisorctl status
goapp           STOPPED   Dec 15 08:12 AM

$ supervisorctl restart goapp
goapp: stopped
goapp: started

$ ps aux | grep goapp
root      2681  0.0  0.2  46592  5416 ?        Sl   08:14   0:00 /go/bin/goapp

個人の感想

公式のドキュメントがしっかりしてて助かる

補足

設定ファイルの例

[unix_http_server]
file = /var/tmp/supervisor.sock
chmod = 0777
chown= root:root

[supervisord]
http_port=/var/tmp/supervisor.sock ; (default is to run a UNIX domain socket server)
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (logging level;default info; others: debug,warn)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)

[supervisorctl]
serverurl=unix:///var/tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[program:goapp]
command=/go/bin/goapp ; the program (relative uses PATH, can take args)
autostart=true              ; start at supervisord start (default: true)
autorestart=true            ; retstart at unexpected quit (default: true)

Goで始めるMiddleware その2

前回記事を書いたがもう少し詳しく書く。

takayukinakata.hatenablog.com

なぜGoでMiddlewareを書きたいのか

前回書き忘れていたところ。

おそらくだがフレームワークを使用している場合には、フレームワークがよろしくやってくれるので自分でMiddlewareをどうこうしたいというモチベーションはあまりない気がする。しかし、あくまで個人の主観だが、標準パッケージが利用しやすいGoでは、シンプルなAPIなどであればフレームワークを使用せずに標準のnet/httpを使用して書きたいケースなどは十分あり得るはずである。そして、標準のnet/httpを使用して書く場合には自分でMiddlewareまわりを実装する必要が発生するはずである(パッケージを使用して実装するのも含め)。このため、GoにおいてはMiddlewareを書きたくなるモチベーションが生まれることは十分あり得る。

前回のおさらい

GoでシンプルにMiddlewareを実装すると以下のようになる。

Middlewares in Go: Best practices and examples – Nicolas Merouze

package main

import (
    "fmt"
    "net/http"
)

// indexHandler ...
func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, middleware!")
}

// aboutHandler ...
func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("This is midlleware test!!")
}

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    }
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
    mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

    http.ListenAndServe(":8888", mux)
}

このようにMiddlewareを入れ子にして実装することで、例えば複数のMiddlewareを追加したい場合も容易に実装できる。

もっといい感じにMiddlewareを書くには

GoでMiddlewareを始めることができた。Middlewareの追加もできた。

しかし、追加するMiddlewareがさらに増えるとめんどくさそうなのは容易にわかると思う。

ではこの場合はどう実装したらいいだろうか。解の一つとしてMiddlewareスタックのようなものを作るというのがある。

package main

import (
    "fmt"
    "net/http"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    }
}

// middleware2 ...
// middleware3 ...

func main() {
    middlewares := newMws(middleware1, middleware2, middleware3)

    mux := http.NewServeMux()
    mux.HandleFunc("/", middlewares.then(indexHandler))
    mux.HandleFunc("/about", middlewares.then(aboutHandler))

    http.ListenAndServe(":8888", mux)
}

type middleware func(http.HandlerFunc) http.HandlerFunc

type mwStack struct {
    middlewares []middleware
}

func newMws(mws ...middleware) mwStack {
    return mwStack{append([]middleware(nil), mws...)}
}

func (m mwStack) then(h http.HandlerFunc) http.HandlerFunc {
    for i := range m.middlewares {
        h = m.middlewares[len(m.middlewares)-1-i](h)
    }
    return h
}

比較するとこうなる。Middlewareの入れ子を何回も書く必要がなくなる。

// 前
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", middleware1(middleware2(middleware3(indexHandler))))
    mux.HandleFunc("/about", middleware1(middleware2(middleware3(aboutHandler))))

    http.ListenAndServe(":8888", mux)
}

// 後
func main() {
    middlewares := newMws(middleware1, middleware2, middleware3)

    mux := http.NewServeMux()
    mux.HandleFunc("/", middlewares.then(indexHandler))
    mux.HandleFunc("/about", middlewares.then(aboutHandler))

    http.ListenAndServe(":8888", mux)
}

パッケージを使うと

ではパッケージを使うとどうだろうか。ここではAliceとnegroniを紹介する。

Alice

github.com

Aliceを使用すると前述したMiddlewareスタックのようなものを容易に実装できる(というか元ネタ)。以下、Aliceの説明を引用する。

Alice provides a convenient way to chain your HTTP middleware functions and the app handler.

In short, it transforms

Middleware1(Middleware2(Middleware3(App)))

to

alice.New(Middleware1, Middleware2, Middleware3).Then(App)

コードを見ればわかるが考え方的には前述したMiddlewareスタックのようなものをalice.New(Middleware1, Middleware2, Middleware3)で作ってから.Then(App)でハンドラを返すようになっている。Aliceを使用すると実装は以下のようになる。

package main

import (
    "fmt"
    "net/http"

    "github.com/justinas/alice"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("[START] middleware1")
        next.ServeHTTP(w, r)
        fmt.Println("[END] middleware1")
    })
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/about", aboutHandler)

    chain := alice.New(middleware1, middleware2, middleware3)

    http.ListenAndServe(":8888", chain.Then(mux))
}

negroni

github.com

negroniも同様にMiddlewareスタックのようなものを扱うが、これまで紹介したものと違い実装する側がそのスタックを意識しなくてよい。negroni独自にHandlerインターフェースを用意しており、これに沿って実装すれば容易にMiddlewareの追加もできる。また、デフォルトでロガーやリカバリー処理(negroni.Classic())を用意してくれており、比較的リッチなパッケージとなっている。

https://github.com/urfave/negroni#negroniclassic

negroniを使用すると実装は以下のようになる。

package main

import (
    "fmt"
    "net/http"

    "github.com/urfave/negroni"
)

// indexHandler ...
// aboutHandler ...

// middleware1 ...
func middleware1(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
    fmt.Println("[START] middleware1")
    next(w, r)
    fmt.Println("[END] middleware1")
}

// middleware2 ...
// middleware3 ...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/about", aboutHandler)

    n := negroni.New()
    n.Use(negroni.HandlerFunc(middleware1))
    n.Use(negroni.HandlerFunc(middleware2))
    n.Use(negroni.HandlerFunc(middleware3))
    n.UseHandler(mux)
    http.ListenAndServe(":8888", n)
}

感想

Goを書き始めてから初めてMiddlewareを意識した。最初はよくわからなかったが実際に書いてみるとそんなに怖くなかった。とはいえGoを書き始めて間もないので他によさげなパッケージがあれば教えてほしいです。

書いたコードはここに置きました。

github.com

Goの標準logでやってるintをasciiにする変換処理を見てみる

Goのlogパッケージでやってるintをasciiにする変換処理が何やってるか最初わけわかんなかったけど、多少理解できたからメモしておく。対象のコードは以下の通り。標準で年月日をログの先頭に付与できる機能(2009/01/23 01:23:23 log-messageみたいな感じ)があってそこで使用されている。

src/log/log.go - The Go Programming Language

// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
    // Assemble decimal in reverse order.
    var b [20]byte
    bp := len(b) - 1
    for i >= 10 || wid > 1 {
        wid--
        q := i / 10
        b[bp] = byte('0' + i - q*10)
        bp--
        i = q
    }
    // i < 10
    b[bp] = byte('0' + i)
    *buf = append(*buf, b[bp:]...)
}

例えば、

itoa(buf, 2017, 4)

とすると、bufには'2''0''1''7'が設定される。つまり、2017とログに出力される。

まず、単純にintをasciiにする処理を見てみる。上記のコードではb[bp] = byte('0' + i - q*10)b[bp] = byte('0' + i)に当たるところ。 そもそもasciiでは10進数の数字と文字の数字は以下のように対応している。

10進数 : 文字
48 : 0
49 : 1
50 : 2
51 : 3
52 : 4
53 : 5
54 : 6
55 : 7
56 : 8
57 : 9

このため、例えばintの7をasciiの7にしたい場合にはintの55を持つ必要がある。このintからasciiへの変換処理をどのように実装するかというと、byte('0' + i)に当たる。つまり、byte('0' + 7) = byte(48 + 7) = byte(55) = '7'という具合である。'0' = 48を基準にしてここにintを足すことでintからasciiへの変換処理を実現している。

では、b[bp] = byte('0' + i - q*10)の処理はどうだろうか。なお、widに関してはここでは説明しない。例えばi = 2017とすると1回目のループで各変数は以下のようになる。

// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func itoa(buf *[]byte, i int, wid int) {
    // Assemble decimal in reverse order.
    var b [20]byte
    bp := len(b) - 1     // bp = 19
    for i >= 10 || wid > 1 {
        wid--
        q := i / 10     // q = 2017 / 10 = 201
        b[bp] = byte('0' + i - q*10)     // b[19] = byte('0' + 2017 - 201*10) = byte('0' + 2017 - 2010) = byte(48 + 7) = byte(55) =  '7'
        bp--     // bp = 18
        i = q     // i = 201
    }
    // i < 10
    b[bp] = byte('0' + i)
    *buf = append(*buf, b[bp:]...)
}

これを見るとわかるように2017の7(int)を'7'(ascii)と変換していることがわかる。また、ループを繰り返すと一の位から順にintからasciiに変換していることもわかる。

上記のようにしてintからasciiへの変換を実現している。

まとめ

標準パッケージは全て「イカした」実装していて自分じゃ思いつかないようなやり方ばっかりだろうから読んでもわからなさそう、とか勝手に思い込んでた。だけど、読んでみるとプレーンな実装箇所は案外泥臭いやり方(上記q := i / 10のところとか!)だし、理解できるところも多くて、今まで自分が勝手に持ってた心理的障壁みたいなものがちょっと取り除かれた気がする。