標準httpパッケージの応用

2022/01/25に公開されました。
2022/01/25に更新されました。

他ライブラリを使用しなくても標準httpパッケージできること


author: komem3

モチベーション

httpパッケージなんて最低限の機能だけでいいんだ、標準パッケージに近いinterfaceのものを使用したい、 という気持ちで、chi を使用する人が増えているように思えます。 ginecho はリッチすぎて、ライラブリに振り回される危険があるため、 chiのような軽量なライラブリというのは魅力的に思えます。 しかし、そもそもchiなどの他ライブラリを使用する必要があるのでしょうか。Goの標準httpだってできることは沢山あるんです!

ということで、今回は標準httpでできる応用を紹介していきたいです。

Chi との比較

何ができると嬉しいかということで、まずはchiでできることを見ていきましょう。 以下がchiのMuxのinterfaceと関数です。

type Router interface {
	http.Handler
	Routes

	// Use appends one or more middlewares onto the Router stack.
	Use(middlewares ...func(http.Handler) http.Handler)

	// With adds inline middlewares for an endpoint handler.
	With(middlewares ...func(http.Handler) http.Handler) Router

	// Group adds a new inline-Router along the current routing
	// path, with a fresh middleware stack for the inline-Router.
	Group(fn func(r Router)) Router

	// Route mounts a sub-Router along a `pattern`` string.
	Route(pattern string, fn func(r Router)) Router

	// Mount attaches another http.Handler along ./pattern/*
	Mount(pattern string, h http.Handler)

	// Handle and HandleFunc adds routes for `pattern` that matches
	// all HTTP methods.
	Handle(pattern string, h http.Handler)
	HandleFunc(pattern string, h http.HandlerFunc)

	// Method and MethodFunc adds routes for `pattern` that matches
	// the `method` HTTP method.
	Method(method, pattern string, h http.Handler)
	MethodFunc(method, pattern string, h http.HandlerFunc)

	// HTTP-method routing along `pattern`
	Connect(pattern string, h http.HandlerFunc)
	Delete(pattern string, h http.HandlerFunc)
	Get(pattern string, h http.HandlerFunc)
	Head(pattern string, h http.HandlerFunc)
	Options(pattern string, h http.HandlerFunc)
	Patch(pattern string, h http.HandlerFunc)
	Post(pattern string, h http.HandlerFunc)
	Put(pattern string, h http.HandlerFunc)
	Trace(pattern string, h http.HandlerFunc)

	// NotFound defines a handler to respond whenever a route could
	// not be found.
	NotFound(h http.HandlerFunc)

	// MethodNotAllowed defines a handler to respond whenever a method is
	// not allowed.
	MethodNotAllowed(h http.HandlerFunc)
}

func RegisterMethod(method string)
func URLParam(r *http.Request, key string) string
func URLParamFromCtx(ctx context.Context, key string) string
func Walk(r Routes, walkFn WalkFunc) error

これをざっと見ると以下の機能があることが分かります。

種別chi の関数http で実装可能か
Middleware の追加Use, With
Notfound の handler 追加MethodNotFound
handler のグループ化Group, Mount, Route
Method ごとの handler 追加Get, Post, MethodFunc
パスパラメータURLParam, URLParamFromCtx

※ △ は完全ではないけど実装可能なものです。

上記の表で実装可能としたものの実装例を今回はやっていきます。

http での実装

Middleware の追加

一番よくあるやつです!簡単です!

まず、middlewareの定義はよく目にします。

type Middleware func(http.Handler) http.Handler

これをどうやって使うかという話しです。 この関数を見る限り、handlerを引数にhandlerを返してくれることが分かります。試しにmiddlewareを一個作ってみて、それにhandlerを入れてみます。

以下のような、リクエストの情報とレスポンスのステータスコードをログに出力するmiddlewareを作成します。

type response struct {
	http.ResponseWriter
	status int
}

func (r *response) WriteHeader(statusCode int) {
	r.ResponseWriter.WriteHeader(statusCode)
	r.status = statusCode
}

var RequestLog Middleware = func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writer := &response{ResponseWriter: w}
		next.ServeHTTP(writer, r)

		log.Printf("[DBG] %s(%s): %d", r.URL.Path, r.Method, writer.status)
	})
}

これにhandlerを渡してhanlderを生成します。

func main() {
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Hello World"))
	})

	http.Handle("/", RequestLog(helloHandler))

	log.Printf("http serve")
	panic(http.ListenAndServe(":8080", nil))
}

出力は以下のようになります。

2022/01/25 18:39:44 http serve
2022/01/25 18:40:16 [DBG] /(GET): 200
2022/01/25 18:40:20 [DBG] /api(GET): 200

つまり、middlewareでラップし続ければいいってことです。

http.Handle("/", Middleware1(Middleware2(...)))

これをやると以下のようになります。

func WithMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
	// 後から適用したものから実行されるため、逆から適用する。
	for i := len(middlewares) - 1; i >= 0; i-- {
		handler = middlewares[i](handler)
	}
	return handler
}

以下のように使用します。

// 説明の都合で recover を行なう middleware を追加
var Recover Middleware = func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("recover: %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte("internal server error"))
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		panic("panic occur")
	})

	http.Handle("/", WithMiddlewares(helloHandler,
		RequestLog,
		Recover,
	))

	log.Printf("http serve")
	panic(http.ListenAndServe(":8080", nil))
}

以下のように出力されます。単純な仕組みですね。

2022/01/25 18:59:44 http serve
2022/01/25 19:00:00 recover: panic occur
2022/01/25 19:00:00 [DBG] /api(GET): 500

よくある Use 関数で実装してみると以下のようになります。

type Mux struct {
	mux         *http.ServeMux
	middlewares []Middleware
}

func NewMux() *Mux {
	return &Mux{mux: http.NewServeMux()}
}

func (m *Mux) Use(middlewares ...Middleware) {
	m.middlewares = append(m.middlewares, middlewares...)
}

func (m *Mux) Handle(pattern string, handler http.Handler) {
	m.mux.Handle(pattern, WithMiddlewares(handler, m.middlewares...))
}

func (m *Mux) HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) {
	m.Handle(pattern, http.HandlerFunc(f))
}

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.mux.ServeHTTP(w, r)
}

func main() {
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		panic("panic occur")
	})

	mux := NewMux()

	mux.Use(
		RequestLog,
		Recover,
	)
	mux.Handle("/", helloHandler)

	log.Printf("http serve")
	panic(http.ListenAndServe(":8080", mux))
}

結構簡単に実装できましたね。

Notfound の handler 追加

これはhttpのルーティングを理解していれば簡単です。

標準httpは ServeMux に以下のように記載がある通り、最も近いパスにルーティングされます。

ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.

そのため、//images//images/magic というパスにhandlerが登録されている場合、/images/1 というリクエストは /images/ に、/favicon.co というリクエストは / に飛ばされます。

つまり、以下のように書けば404の挙動が得られます。

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	log.Printf("404 not found")
	http.NotFound(w, r)
})
2022/01/26 22:56:04 http serve
2022/01/26 22:56:10 404 not found
2022/01/26 22:57:08 [DBG] /bad-path(GET): 404

/ に反応させたいhandlerがある場合は、以下のようにします。

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		log.Printf("404 not found")
		http.NotFound(w, r)
	}
	// '/' の処理
	// ...
})

handler のグループ化

URLのグループ化に関しては http.StripPrefixを使用することで実現可能です。

ドキュメントのサンプルでは /tempfiles/ を取り除く例が書かれています。

http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp"))))

これをラップして、MountRoute を実装すると以下のようになります。

func (m *Mux) Mount(pattern string, handler http.Handler) {
	prefix := pattern
	if pattern[len(pattern)-1] != '/' {
		pattern += "/"
	} else {
		prefix = strings.TrimRight(pattern, "/")
	}
	m.Handle(pattern, http.StripPrefix(prefix, handler))
}

func (m *Mux) Route(pattern string, f func(*Mux)) {
	subMux := NewMux()
	f(subMux)
	m.Mount(pattern, subMux)
}

使用するときはこんな感じです。

mux.Route("/user", func(mux *Mux) {
	mux.HandleFunc("/bob", func(w http.ResponseWriter, _ *http.Request) {
		log.Printf("my name is bob")
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("my name is bob"))
	})
})
2022/01/27 13:14:28 http serve
2022/01/27 13:14:40 my name is bob
2022/01/27 13:14:40 [DBG] /user/bob(GET): 200

Group は仕組み的に難しいです。頑張ればできなくはないですが、そこまでするなら大人しく他のものを使用した方が健全です。

Method ごとの handler 追加

これは関数単位では実装しません。やるとしたら大変です。 一般的なやり方としては以下のやり方が知られています。

mux.HandleFunc("/method", func(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		w.Write([]byte("get request"))
	case http.MethodPost:
		w.Write([]byte("post request"))
	default:
		header := w.Header()
		header.Add("Allow", http.MethodGet)
		header.Add("Allow", http.MethodPost)
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
})

どのhandlerでもこれを書くのは大変なので共通化してみます。

まずは、呼び出されるhandlerを作成します。

type methodHandler struct {
	get    http.Handler
	post   http.Handler
	put    http.Handler
	patch  http.Handler
	delete http.Handler

	allowed []string
}

func (h *methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch {
	case r.Method == http.MethodGet && h.get != nil:
		h.get.ServeHTTP(w, r)
	case r.Method == http.MethodPost && h.post != nil:
		h.post.ServeHTTP(w, r)
	case r.Method == http.MethodPut && h.put != nil:
		h.put.ServeHTTP(w, r)
	case r.Method == http.MethodPatch && h.patch != nil:
		h.patch.ServeHTTP(w, r)
	case r.Method == http.MethodDelete && h.delete != nil:
		h.delete.ServeHTTP(w, r)
	default:
		header := w.Header()
		for _, method := range h.allowed {
			header.Add("Allow", method)
		}
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

これを登録する用の関数を作成する感じです。

type MethodHandler interface {
	apply(*methodHandler)
}

type applyFunc func(*methodHandler)

func (a applyFunc) apply(m *methodHandler) {
	a(m)
}

func GetHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodGet)
		m.get = h
	})
}

func PostHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPost)
		m.post = h
	})
}

func PutHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPut)
		m.put = h
	})
}

func PatchHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPatch)
		m.patch = h
	})
}

func DeleteHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodDelete)
		m.delete = h
	})
}

func (m *Mux) MethodFunc(pattern string, handlers ...MethodHandler) {
	h := new(methodHandler)
	for _, f := range handlers {
		f.apply(h)
	}
	m.Handle(pattern, h)
}

使用感はchiと異なりますが、やりたいことは満たせています。

mux.Route("/user", func(mux *Mux) {
	mux.MethodFunc("/tea",
		GetHandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			log.Printf("I'm tea")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("I'm tea"))
		}),
		DeleteHandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			log.Printf("bob is deleted")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("bob is deleted"))
		}),
	)
})
2022/01/27 12:44:31 I'm tea
2022/01/27 12:44:31 [DBG] /user/tea(GET): 200
2022/01/27 12:44:40 bob is deleted
2022/01/27 12:44:40 [DBG] /user/tea(DELETE): 200
2022/01/27 12:44:58 [DBG] /user/tea(POST): 405

ただ、この方法の欠点として、Method ごとに異なる middleware を設定できない点が上げられます。以下のようなことがしたい場合はchiの出番です。(chiのGroupが便利すぎる)


r.Get("/", getHandler)

r.Group(func(r chi.Router){
	r.Use(basicAuth()))
	r.Post("/", postHandler)
})

パスパラメータ

簡易的なものなら可能です。パスマッチの挙動として、/images/ のような / が最後に付くパスは他のパスにも反応するという挙動を利用できます。 そのため、/images/ を登録しておけば /images/1/images/2 もマッチできます。

雰囲気としては以下のようにsplitすれば取れます。

mux.HandleFunc("/images/", func(w http.ResponseWriter, r *http.Request) {
	paths := strings.Split(r.URL.Path, "/")
	if len(paths) == 3 {
		imageID := paths[2]
		log.Printf("id %s", imageID)
	}
	w.WriteHeader(http.StatusOK)
})
2022/01/27 12:59:09 id 1
2022/01/27 12:59:09 [DBG] /images/1(POST): 200

ただ、/user/{userID}/images/{imageID} のような途中にあるものを取り出すことはできないので、本当に簡易的なものしかできないということです。

コードの全体像

最後に今回のコードの全体像です。

package main

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

type Middleware func(http.Handler) http.Handler

type response struct {
	http.ResponseWriter
	status int
}

func (r *response) WriteHeader(statusCode int) {
	r.ResponseWriter.WriteHeader(statusCode)
	r.status = statusCode
}

var RequestLog Middleware = func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writer := &response{ResponseWriter: w}
		next.ServeHTTP(writer, r)

		log.Printf("[DBG] %s(%s): %d", r.URL.Path, r.Method, writer.status)
	})
}

var Recover Middleware = func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("recover: %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte("internal server error"))
			}
		}()
		next.ServeHTTP(w, r)
	})
}

type methodHandler struct {
	get    http.Handler
	post   http.Handler
	put    http.Handler
	patch  http.Handler
	delete http.Handler

	allowed []string
}

func (h *methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch {
	case r.Method == http.MethodGet && h.get != nil:
		h.get.ServeHTTP(w, r)
	case r.Method == http.MethodPost && h.post != nil:
		h.post.ServeHTTP(w, r)
	case r.Method == http.MethodPut && h.put != nil:
		h.put.ServeHTTP(w, r)
	case r.Method == http.MethodPatch && h.patch != nil:
		h.patch.ServeHTTP(w, r)
	case r.Method == http.MethodDelete && h.delete != nil:
		h.delete.ServeHTTP(w, r)
	default:
		header := w.Header()
		for _, method := range h.allowed {
			header.Add("Allow", method)
		}
		w.WriteHeader(http.StatusMethodNotAllowed)
	}
}

type MethodHandler interface {
	apply(*methodHandler)
}

type applyFunc func(*methodHandler)

func (a applyFunc) apply(m *methodHandler) {
	a(m)
}

func GetHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodGet)
		m.get = h
	})
}

func PostHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPost)
		m.post = h
	})
}

func PutHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPut)
		m.put = h
	})
}

func PatchHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodPatch)
		m.patch = h
	})
}

func DeleteHandlerFunc(h http.HandlerFunc) MethodHandler {
	return applyFunc(func(m *methodHandler) {
		m.allowed = append(m.allowed, http.MethodDelete)
		m.delete = h
	})
}

type Mux struct {
	mux         *http.ServeMux
	middlewares []Middleware
}

func NewMux() *Mux {
	return &Mux{mux: http.NewServeMux()}
}

func (m *Mux) MethodFunc(pattern string, handlers ...MethodHandler) {
	h := new(methodHandler)
	for _, f := range handlers {
		f.apply(h)
	}
	m.Handle(pattern, h)
}

func (m *Mux) Use(middlewares ...Middleware) {
	m.middlewares = append(m.middlewares, middlewares...)
}

func (m *Mux) Handle(pattern string, handler http.Handler) {
	m.mux.Handle(pattern, WithMiddlewares(handler, m.middlewares...))
}

func (m *Mux) HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) {
	m.Handle(pattern, http.HandlerFunc(f))
}

func (m *Mux) Mount(pattern string, handler http.Handler) {
	prefix := pattern
	if pattern[len(pattern)-1] != '/' {
		pattern += "/"
	} else {
		prefix = strings.TrimRight(pattern, "/")
	}
	m.Handle(pattern, http.StripPrefix(prefix, handler))
}

func (m *Mux) Route(pattern string, f func(*Mux)) {
	subMux := NewMux()
	f(subMux)
	m.Mount(pattern, subMux)
}

func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	m.mux.ServeHTTP(w, r)
}

func WithMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handler {
	// 後から適用したものから実行されるため、逆から適用する。
	for i := len(middlewares) - 1; i >= 0; i-- {
		handler = middlewares[i](handler)
	}
	return handler
}

func main() {
	helloHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		panic("panic occur")
	})

	mux := NewMux()

	mux.Use(
		RequestLog,
		Recover,
	)
	mux.Handle("/api", helloHandler)
	mux.Route("/user", func(mux *Mux) {
		mux.HandleFunc("/bob", func(w http.ResponseWriter, _ *http.Request) {
			log.Printf("my name is bob")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte("my name is bob"))
		})
		mux.MethodFunc("/tea",
			GetHandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				log.Printf("I'm tea")
				w.WriteHeader(http.StatusOK)
				w.Write([]byte("I'm tea"))
			}),
			DeleteHandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				log.Printf("bob is deleted")
				w.WriteHeader(http.StatusOK)
				w.Write([]byte("bob is deleted"))
			}),
		)
	})

	mux.HandleFunc("/images/", func(w http.ResponseWriter, r *http.Request) {
		paths := strings.Split(r.URL.Path, "/")
		if len(paths) == 3 {
			imageID := paths[2]
			log.Printf("id %s", imageID)
		}
		w.WriteHeader(http.StatusOK)
	})

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.NotFound(w, r)
	})

	log.Printf("http serve")
	panic(http.ListenAndServe(":9000", mux))
}

まとめ

標準のhttpパッケージでも色々なことが可能なんだよということで、いくつか紹介させて頂きました。 最初からライブラリを使用するという選択をするのも勿論ありですが、標準のものでも十分な場合もあるというこを理解して頂けたらなら嬉しいです。



GI Cloud は事業の拡大に向けて一緒に夢を追う仲間を募集しています

当社は「クラウドで日本のIT業界を変革し、世の中をもっとハッピーに」をミッションに掲げ、Google Cloudに特化した技術者集団として、お客様にコンサルティングからシステム開発、運用・保守まで一気通貫でサービスを提供しています。

まだ小規模な事業体ですが、スタートアップならではの活気と成長性に加えて、大手総合商社である伊藤忠グループの一員としてやりがいのある案件にもどんどんチャレンジできる環境が整っています。成長意欲の高い仲間と共にスキルを磨きながら、クラウドの力で世の中をもっとハッピーにしたい。そんな我々の想いに共感できる方のエントリーをお待ちしています。

採用ページ

※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。

※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。