昨日Twitterに書いた内容に、sivchariさんとhajimehoshiさんからリプライをいただいたので、備忘録的にまとめておきます。
- 発端
- interface型と非interface型の後方互換性について
- interface型を公開した場合
- 1. メソッドの追加
- 2. メソッドのシグニチャの変更
- 3. メソッドの削除
- 公開されたinterface型を変更する場合の後方互換性についてのまとめ
- 非interface型を公開した場合
- 1. メソッドの追加
- 2. メソッドのシグニチャの変更
- 3. メソッドの削除
- 公開された非interface型を変更する場合の後方互換性についてのまとめ
- interface型を公開した場合
- その他の後方互換性を崩さない機能拡張のパターンの紹介
- 他から実装できないinterfaceにする
- interfaceを合成する
- 内部用のinterfaceを分け、構造体型を公開する
- まとめ
発端
昨日、フューチャー技術ブログに掲載された、こちらのManoさんの記事を読みました。
内容としてはGo 1.20で追加される見込みの context.WithCancelCause
と context.Cause
に関するもので、気になったのは context.Cause
関数についての次の記述でした。
Cause()ですが、以下のように context.Context のインターフェースにCause()といった関数を追加してくれた方が利用者としては便利じゃないかと思いますよね。これはGo1互換性ポリシーに書いてあるように、パッケージエクスポートされたインターフェースに新しい関数を追加することは許可されてないということで否定されていました(そのため、context.Contextを引数にとる現在のかたちで提供されています)。
記事中で紹介されているコード例も引用させていただきます。
// 互換性をぶっ壊すAPIイメージ type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any + Cause() error // ★後方互換性を壊すためインターフェースに新規関数の追加はできない }
これはとても共感できる内容で、ここで自分が持った疑問は "なぜcontext.Contextは構造体型ではなくinterface型で定義されているのか?" と言うものでした。
context.Contextって、どうしてinterfaceで定義されてるんだろうな。Context自体をmockしたい事とかって特に無いし、ここのブログにある通りinterfaceにしちゃうとメソッドの追加だけで後方互換性を壊してしまうし、ただの構造体型にした方が柔軟性あった気がするが
— syumai (@__syumai) 2023年1月26日
それでは、 context.Context
のような公開された型に、後方互換性を破壊することなく Cause()
メソッドのような公開メソッドを追加する方法について考えてみましょう。