はじめに
こんにちは!!Google Cloudでオブザーバビリティを担当しているものです。年に一度のGoアドベントカレンダーの時期がやってきましたね!本記事は Goアドベントカレンダー 2022 の12日目の記事です。昨日は @Maki_Daisuke さんの担当でした。
Goアドベントカレンダーもついに今年で10年目です!これまでに書いた記事を見るとなかなか懐かしいトピックがあったりしますね。
- Go製アプリケーションのコンテナ化にはkoを推したい - YAMAGUCHI::weblog
- gopterでステートフルなPBT - YAMAGUCHI::weblog
- Goのハンズオン環境としてGlitchを使う - YAMAGUCHI::weblog
- golang.org/x/text/messageでI18N - YAMAGUCHI::weblog
- or-done-channelでコードの可読性を上げる - YAMAGUCHI::weblog
- GoのASTを使ってパッケージのメンテナンスを考える - YAMAGUCHI::weblog
- Goで良い感じに日時をパースするライブラリdatemakiの話とGo 1.6 - YAMAGUCHI::weblog
- Goの正規表現エンジンを使ってファジング用ツールを書いてみる - YAMAGUCHI::weblog
- GoのimageパッケージでGopherをぐるぐる回そう - YAMAGUCHI::weblog
今年は何を書こうかなあと思ったときに、久々に原点に戻って、Goの設計プラクティスに関して最近同僚と話していて面白かった「単一メソッドインターフェースと関数型」について書こうと思います。
宣伝
ところでGo Conference 2023のCfPがオープンしました。開催は来年の6月、CfPの締切は来月末です。皆様のセッションプロポーザルをお待ちしております!
単一メソッドインターフェース
単一メソッドインターフェース(Single Method Interface)というのは正式な名称ではないのですが、次のようなインターフェースを指します。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
type Reader interface { Read(p []byte) (n int, err error) }
いずれも標準パッケージの net/http
や io
から持ってきたものですが、名前の通りメソッドを1つしか持たないインターフェースです。Goにはこういう単一メソッドインターフェースが非常に多くて、これを駆使することで非常に柔軟で強力な型システムが実現されています。
関数型
一方で関数型(Function Type)と呼んでいるのは、あるシグネチャを持った関数を個別の型として宣言しているものです。同様に標準パッケージから例を持ってくると次のようなものがあります。
type HandlerFunc func(ResponseWriter, *Request)
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
これらはそれぞれ net/http
や bufio
から引用しています。関数型は単純に特定の関数シグネチャをエイリアスするというだけではなく、「特有の型を作ることで、値として渡すときに制限を設ける」ことができます。
単一メソッドインターフェースと関数型の比較
これらは「特定の関数シグネチャを型として明示的に区別する」という点では同じですが、その性質は異なります。
型アサーション
interface{}
型から型アサーションするときに、単一メソッドインターフェースのほうが挙動がわかりやすいです。たとえば次のような例の場合、結果はどうなるでしょうか。
type T struct{} func (T) ServeHTTP(http.ResponseWriter, *http.Request) {} func ServeHTTP(http.ResponseWriter, *http.Request) {} func isHandler(data interface{}) { _, ok := data.(http.Handler) if ok { fmt.Println("a http.Handler") } else { fmt.Println("NOT a http.Handler") } } func isHandlerFunc(data interface{}) { _, ok := data.(http.HandlerFunc) if ok { fmt.Println("a http.HandlerFunc") } else { fmt.Println("NOT a http.HandlerFunc") } } func main() { sh := (T{}).ServeHTTP sh2 := ServeHTTP isHandler(sh) isHandlerFunc(sh) isHandler(sh2) isHandlerFunc(sh2) }
よく訓練されたGopherの皆様であれば、なんのつまづきもなくこれらの結果を予想できると思いますが、Goまだ馴染んでいない方だと特にHandlerFuncかどうかの判定で戸惑うのではないかと思います。 sh
では、メソッドのレシーバーがあるので実際には func(T, http.ResponseWriter, *http.Request)
となっているのでだめ、ということ、後者は実装のまま func(http.ResponseWriter, *http.Request)
として見られているのでだめ、ということになります。
これを見るとまだHandlerかどうかのほうがわかりやすい結果になっているかと思います。
型変換
型変換をする場合には、関数型は関数シグネチャだけ合っていれば型変換できるのに対して、インターフェースはメソッド名まで合っていないといけません。次の例を見てみてください。
type T struct{} func (T) ServeHTTP(http.ResponseWriter, *http.Request) {} type N struct{} func (N) ServeDebugHandler(http.ResponseWriter, *http.Request) {} func main() { var ( t T n N ) var _ http.Handler = t var _ http.Handler = n // 動作しない var _ http.HandlerFunc = t.ServeHTTP var _ http.HandlerFunc = n.ServeDebugHandler }
拡張性
構造体はインターフェースで定義されたメソッドを実装しているかぎり、インターフェースを満たしているとみなされるため、状態を保持するためのフィールドを定義したり、他の補助的なメソッドを追加したりなど自由に拡張できます。一方で、関数型の場合はそもそも関数シグネチャに縛られているので、拡張性はインターフェースと比較すると著しく乏しくなります。
宣言
インターフェースの実装はトップレベルでしか宣言できないのに対して、関数型は関数内でも定義可能です。
type T struct{} func (T) ServeHTTP(http.ResponseWriter, *http.Request) {} func ServeHTTP(http.ResponseWriter, *http.Request) {} func main() { var ( _ http.Handler = new(T) _ http.HandlerFunc = ServeHTTP ) type innerT struct{} // 関数内で構造体のメソッドは実装できない // func (innerT) ServeHTTP(http.ResponseWriter, *http.Request) {} // var _ http.Handler = new(innerT) f := func(http.ResponseWriter, *http.Request) {} var _ http.HandlerFunc = f }
メソッド値と関数型
型キャストのところでも少し触れましたが、メソッド値は関数値として渡すことが可能なので便利です。
type Feature struct { DryRunMode bool } func (f *Feature) RenderDebug(w http.ResponseWriter, req *http.Request) {} func New(mux *http.ServeMux) *Feature { f := &Feature{} mux.HandleFunc("/featurez", f.RenderDebug) return f }
使い分け
以上は言語仕様から各々の特徴を確認しただけで、どちらが優れているということはありません。しかしながら、拡張しやすいという点や、総合的な挙動の理解のしやすさを考えると、一旦単一メソッドインターフェースを用意してあげるのがまずは安全なのかなあと思いました。
一方で、逆に拡張性を絞って渡す関数に制限を加えたい場合には、関数型を定義してあげるのも良いかなと思いました。
これらを考えた上で、さらに net/http
パッケージの実装を見てみると、関数型自体が単一メソッドインターフェースを実装している(そして、これは単一メソッドインターフェースでないと実装は無理だと思う)関係性が http.HandlerFunc
と http.Handler
に見られて、本当に美しいな、などと思うのでした。(おそらくこういった実装をしている例は、標準パッケージ内で公開インターフェースで行っているのはおそらく net/http
しかないと思います)
// In net/http: // type HandlerFunc func(http.ResponseWriter, *http.Request) // // type (f *HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) // // type Handler interface { // ServeHTTP(ResponseWriter, *Request) // } f := func(http.ResponseWriter, *http.Request) {} var _ http.Handler = http.HandlerFunc(f)
おわりに
今年の記事は久々にGoの設計についての考察を書きました。Goは言語仕様が軽量なこともあり、こうした考察がしやすい言語になっていると思います。来年も仕事でGoをたくさん活用したいと思います。
明日は @otiai10 さんの記事です。