自己紹介
普段はインターン先では Python を使って ML を用いた ソフトウェアの開発をしていますが、エンジニアとして成長するために静的型付け言語を学ぼうと思いました。その第一歩に選ばれた Go を学んでいくにあたり、interface 周りでつまづき、色々調べる中で自分なりに腑に落ちたので共有させていただきます。
TL; DR
Golang における net/http モジュールのルーティングついての紹介です。
ルーティングするとき誤魔化していたところをしっかり公式や記事を読んだことで理解につながったので共有させていただきます。
結論から言うと http で interface を実装してくれているので何も考えずに
http.HandleFunc("/endpoint", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})
といったふうに書くことができます。
この記事を読んでわかることは、
- Go ってすごい
- interface の使われ方
などです。気楽に読んでいただければ嬉しいです 🙇♂️
⭐️ interface とは
Golang におけるインターフェースとは、簡単に言うとメソッドの塊のことです。
また、とある構造体などに対してインターフェースに定義されているメソッド全てを定義していることをとインターフェースを実装していると言います。
この時点で Python しか書いてこなかった僕の頭は ??? になりました。
実際のコードを見てみましょう。
type Animal interface {
Eat()
Sleep()
}
type Dog struct {}
func (d *Dog) Eat() {
fmt.Println("mogumogu") // mogumogu と表示されます
}
func (d *Dog) Sleep() {
fmt.Println("suyasuya")
}
序盤 4 行で interface Animal を定義しています。
Dog と言う構造体を定義し、Dog に対して Animal で宣言されているメソッドたちを定義します。この時点で、Dog は Animal interface を実装したことになりました。
例えば、 Cat
と言う名前の構造体を定義し、Eat
と Sleep
を定義することで Cat も Animal interface を自動的に実装したことになります。
⭐️ http における紛らわしいやつらの違い
http における紛らわしい奴等リストは
- Handler
- HandlerFunc
- Handle
- HandleFunc
です。
実際にそれぞれ net/http/server.go に実装されているので見ていきましょう。
Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler は interface です。ただ 1 つ ServeHTTP と言う関数さえ実装してくれれば OK な interface です。
HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)
HandlerFunc は関数の型です。ResponseWriter と *Request を引数に取るような関数の型ですね。
Handle
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
Handle は実際の関数です。関数内部で何をやっているかについては長いので割愛させていただきます。簡単に言うと pattern
エンドポイントに対して handler
を紐付けています。(関数内部の Handle
は外の Handle
とは関係なく、DefaultServeMux
のメソッドであることに注意してください。)
HandlerFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
HandlerFunc も実際の関数ですね。TL; DR ではこの関数を呼び出していました。
こちらは短いので内部の関数をみてみましょう。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
最終行に全てが詰まっています。
mux.Handle(pattern, HandlerFunc(handler))
つまり、
-
HandlerFunc
メソッドはHandle
メソッドを呼び出していること -
handler
(コールバック関数) をHandlerFunc
でラップしていること
が肝になっているわけです。
これらによって、僕たちは何も考えずに TL; DR で書いたようなコードを書けるわけです。
⭐️ 実際の関数の流れ
これまでに勉強したことを実際の流れに即して見ていきましょう。きっと感動してしまうことでしょう。
まず、TL; DR で書いたように僕たちは
http.HandleFunc("/endpoint", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello")
})
エンドポイントを実装する時はこう書くだけでよかったのです。
HandleFunc
が呼ばれているので見てみましょう。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
- pattern = “/endpoint”
- handler = func(w, r)
となっていて良さそうですね。
次に呼ばれているのは DefaultServeMux.HandleFunc
です。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
- pattern = “/endpoint”
- handler = func(w, r)
ここも良さそうですね。
次に呼ばれるのが mux.Handle
です。
ここが大事なポイントです。(*1)
func (mux *ServeMux) Handle(pattern string, handler Handler) {
// ルーティング
}
mux.Handle に関しては先ほど割愛させてもらったので引数だけ見ます。(関数としては実際にルーティングしている部分を担っています。)
- pattern = “/endpoint”
- handler = Handler(func(w, r))
1 つ前の メソッドで func(w, r) を HandlerFunc
型にキャストしていましたが、今回のメソッドの第 2 引数 Handler
ではないです。。。なぜでしょう。。。
見返してみると、Handler
は interface だったので、HandlerFunc(f(w, r))
がどこかで Handler interface を実装してないといけません。つまり、HandlerFunc
が ServeHTTP
メソッドを定義していないといけないわけです。
僕たちはいちいち コールバック関数に ServeHTTP
メソッドを実装していないのできっとパッケージ側で実装してくれているのだろうと踏んで見てみると
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
2083 行目からありました!
つまり HandlerFunc は晴れて Handler interface に昇格して(*1)のメソッドが通り、無事ルーティングできるわけです。
⭐️ 実装できるのは構造体だけじゃない
「⭐️ interface とは」で見たような超具体的な構造体に対して interface を実装することが多くあると思うのですが、Golang の標準モジュールでやっていたことは、
HandlerFunc
と言う 関数の型 に対して interface を実装する
と言うより抽象化されたことでした。
これのおかげで、関数をキャストして、 HTTP サーバーのハンドラーとしてふるまうようにできたのです。(すごい 😭)
またすごいポイントとしては ServeHTTP
はその処理内容をハンドラーに完全に委譲していたことです。これのおかげでハンドラーに柔軟性が出てよしななエンドポイントが作成できます。
interface のおかげで全く裏のことを知らずにエンドポイント作成できるのは非常にいいことですね
最後に
ここまで読んでくださった方、ありがとうございました。
何も考えずに実装できるのはいいことですが、やっぱ裏側がちょっと気になって、調べて腑に落ちると感動しますね。
何か間違えたことがあれば教えていただけると幸いです。
参考
https://qiita.com/_kyamasan/items/940e575bc77e6b1f8ddb
https://pkg.go.dev/net/http
Comments
Let's comment your feelings that are more than good