包丁一本さらしに巻いて

net/httpで作るGo APIサーバー #2

2017.04.02

この一連の記事ではnet/httpを主軸に据え、取替可能な部品となるライブラリを利用してAPIサーバーを作成する方法を紹介する。

今回はhttp.Handlerインターフェースについて書く。このインターフェースを上手く利用する事がhet/httpでAPIサーバーを作る時にとても重要になると思ってる。

Goのインターフェース

Goのインターフェースを網羅的に説明しようとすると長大な記事が書けてしまうので、今回はhttp.Handlerを理解していくのに必要な部分だけに絞って少し書く。以下のサンプルコードを元に話しを進める。

package main

import "fmt"

type person struct {
	Age  int
	Name string
}

func (p person) greeting() string {
	return fmt.Sprintf("hey! I'm %s", p.Name)
}

func (p person) name() string {
	return p.Name
}

type dog struct {
	Age   int
	Name  string
	Owner string
}

func (d dog) greeting() string {
	return "wan!"
}

func (d dog) name() string {
	return d.Name
}

type nameTyp string

func (s nameTyp) name() string {
	return string(s)
}

func (s nameTyp) greeting() string {
	return fmt.Sprintf("nameType: %s", s)
}

type animal interface {
	greeting() string
	name() string
}

func main() {
	p1 := person{
		Name: "achiku",
		Age:  31,
	}
	p2 := person{
		Name: "moqada",
		Age:  31,
	}
	d1 := dog{
		Name:  "taro",
		Age:   2,
		Owner: "moqada",
	}
	n1 := nameTyp("8maki")
	for _, a := range []animal{p1, p2, d1, n1} {
		fmt.Printf("%s: %s\n", a.name(), a.greeting())
	}
}

このコードの中で言いたいのはanimalインターフェースを実装しているものはどのような型でもanimalとして使えるという事。「どのような型でも」と付けたのは、structに付与したメソッドがインターフェースと一致しているかどうかに限らず、上記サンプルで示したnameTypeという実態はstringの型に付与したメソッドでもインターフェースを満たしていればanimalとして扱える、という事。言葉は乱暴かもしれないけどコンパイル時型チェックなduck typingみたいな感じに使える。

詳細は以下の記事がステップバイステップで解説していて面白い。

Goのメソッド

Goのメソッドはカスタム型になら例えそれが関数だろうと付与できる。

package main

import "fmt"

type myFunc func() string

func (f myFunc) WrapFunc() {
	fmt.Print("before msg\n")
	fmt.Printf("%s\n", f())
	fmt.Print("after msg\n")
}

func main() {
	f := myFunc(func() string {
		return "hello!!"
	})
	f.WrapFunc()
}

myFunc型にWrapFunc()メソッドを追加している。ここで面白いのはmyFuncは関数だということ。関数にメソッドを付与する、というのが最初は理解しづらかったのを覚えているんだけど、「カスタム型には元が関数だろうがstringだろうがメソッドは付与できる」という事。このサンプルコードが一番http.Handlerhttp.HandlerFuncの関係性に近い形になっている。

http.Handlerというインターフェース

やっとここまできた。いきなりだけど以下がhttp.Handlerというインターフェースの全量となる。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

ServeHTTP(http.ResponseWriter, *http.Request)というメソッド一つ持っていればそれは全てhttp.Handlerとして扱う事ができる。なんとも小さくて頼りない感じがするが、http.Requesthttp.ResponseWriterと合わさってとてもシンプルにHTTPリクエストを扱えるようになっている。以下にあまり現実的ではないけど、こういう風にも作れるという例を示す。

package main

import (
	"fmt"
	"log"
	"net/http"
)

type myString string

func (s myString) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	log.Printf("myString=%s accessed", s)
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "myString=%s", s)
	return
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/mystring/1", myString("1"))
	mux.Handle("/mystring/2", myString("2"))

	if err := http.ListenAndServe(":8080", mux); err != nil {
		log.Fatal(err)
	}
}

myString型はServeHTTP(http.ResponseWriter, *http.Request)というシグネチャを満たすのでhttp.Handlerとして利用できる。http.Handlerとして利用できるので、mux.Handle(string, http.Handler)に渡せる。そうすることでURLとHTTPリクエスト/レスポンスの処理が紐付き、サーバーとして起動できる、という流れ。

以下は良くGoのnet/httpの例で見るfunc(http.ResponseWriter, *http.Request)とURLを紐付けるタイプのやつ。

	mux.HandleFunc("/mystring/3", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("myString=%s accessed", "3")
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "myString=%s", "3")
		return
	})

これも結構面白くて、ServeMux.HandleFuncは以下のような形になってる。

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

http.HandlerFuncが何をやっているかというと。

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

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

これは関数func(http.ResponseWriter, *http.Request)http.HandlerFuncに型変換しServeHTTPメソッドを持つ形にして、ServeHTTPの中で型変換したその関数を呼ぶ、という形になっており、あくまでもシンタックスシュガーとしてURLとhttp.Handlerが実行する処理の紐付けを行っている事がわかる。たまにURL routing系のライブラリでhttp.HandlerFuncしか取らないものがあるんだけど、シンタックスシュガーだけ取り入れて肝心のhttp.Handlerを受けれないのはライブラリとしてはもったいないなという思いがある。

何が嬉しいのか

気軽に使いたいだけならhttp.HandlerFuncのシグネチャに合う関数func(http.ResponseWriter, *http.Request)を書いてサクッと使える。もし実際の処理の前後に何か処理を挟みたい、処理を共通化したい、そもそもfunc(http.ResponseWriter, *http.Request)の空returnがいまいち好きになれない、等、ということになればServerHTTP(http.ResponseWriter, *http.Request)をメソッドに持つ型を作り、それに処理をする独自シグネチャを持つ関数を渡してURLに紐付けるのが良いのではないか。

例えば独自シグネチャをステータスコード、JSONレスポンス用構造体、エラーを返せるようにfunc(http.ResponseWriter, *http.Request) (int, interface{}, error)とおき、AppHandlerというServeHTTPをメソッドとして持つ構造体をアダプターとして使うパターン。

// AppHandler application handler adaptor
type AppHandler struct {
	h func(http.ResponseWriter, *http.Request) (int, interface{}, error)
}

func (a AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	encoder := json.NewEncoder(w)
	status, res, err := a.h(w, r)
	if err != nil {
		log.Printf("error: %s", err)
		w.WriteHeader(status)
		encoder.Encode(res)
		return
	}
	w.WriteHeader(status)
	encoder.Encode(res)
	return
}

// Greeting greeting
func (app *App) Greeting(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
	res, err := HelloService(r.Context(), "", time.Now())
	if err != nil {
		app.Logger.Printf("error: %s", err)
		e := ErrorResponse{
			Code:    http.StatusInternalServerError,
			Message: "something went wrong",
		}
		return http.StatusInternalServerError, e, err
	}
	app.Logger.Printf("ok: %v", res)
	return http.StatusOK, res, nil
}

func main() {
...

	// for gorilla/mux
	router := mux.NewRouter()
	r := router.PathPrefix("/api").Subrouter()
	r.Methods("GET").Path("/hello").Handler(AppHandler{h: app.Greeting})
...

こうする事でエラー発生時のロギング、HTTP StatusCodeの値によって別URLにリダイレクト、JSONをResponseWriterへの書き込み等を共通的に扱う事ができるようになった。また、これは主観だけど空returnが無くなるのでうっかりエラー処理時にreturn書き忘れる事も減る気がする。

テストは以下のようになる。この記事で書いたものからあまり変化は無いかも。

func TestGreeting(t *testing.T) {
	app := testNewApp(t)
	req := httptest.NewRequest(http.MethodGet, "/api/hello", nil)
	w := httptest.NewRecorder()
	status, res, err := app.Greeting(w, req)
	if err != nil {
		t.Fatal(err)
	}
	if status != http.StatusOK {
		t.Fatalf("want %d got %d", http.StatusOK, status)
	}
	gt, ok := res.(*Greeting)
	if !ok {
		t.Fatalf("want type Greeting got %s", reflect.TypeOf(res))
	}
	if expected := "anonymous"; gt.Name != expected {
		t.Errorf("want %s got %s", expected, gt.Name)
	}
	if expected := "hello!"; gt.Message != expected {
		t.Errorf("want %s got %s", expected, gt.Message)
	}
}
このエントリーをはてなブックマークに追加
comments powered by Disqus