2018-07-20#Go言語のInterfaceGo言語の優れた特徴の一つとして、Interfaceが挙げられることがあります。Interfaceを持つJavaやC#のような他言語と比べ、GoのInterfaceの言語機能における主な違いは静的言語のように静的にチェックされるメソッドの羅列を宣言できるinterfaceでありながらダックタイピングや型アサーションによって動的言語のように柔軟な使い方ができることです。これだけでは単に、ケースに合わせて使い分けることができる選択肢を得ただけのように聞こえます。実際にはGo言語のパッケージ、型システム、そして文化的特徴によってGo言語特有の軽量でシンプル、そして有機的なInterfaceを表現できます。Go特有でGoらしいGoのInterfaceです。structs"***というイディオムが比較的浸透していると思います。インターフェースを受け入れて構造体を返しなさい、もう少し具体的に言うとパッケージでは処理する側でinterfaceを定義し引数として受け取り、New関数の戻り値など外部にわたすものはstructのように具体的な型にすべきとのことです。これは伝わりやすくするためにやや誇大で、実際にはInterfaceの使い方の一つであり絶対的な指標ではありません。Go言語にも実装する側が定義するInterfaceがあります。hash.Hashなどがそうです。多様な暗号化形式のそれを仮想化したHashのInterfaceです。上記の文は間違いなくGo言語のInterfaceの特徴をよく表しています。wikiのコードレビューコメントページにあるInterfaceの項目にAPIの実装側でInterfaceを定義すべきでないとの記述があります。2017年初めごろに追加されたようなので比較的最近のようですね。JSON-RPCの記事も実際のリファクタリングで行った話で参考になります。runners{runner.Run()}}上記のように、実装する側でなく処理を行う側でInterfaceを定義し、構造体を直接使います。これによって例えばロボでもレースに参加することができますし話相手がほしいのであればSpeakerとしても宣言し扱うことができます。走れないイルカさんのような例外も許容できます。キュイキュイ言えますからね。このイディオムは、必要な機能のみを宣言することで動物以外の走れる存在に対する拡張性、すでにあるインターフェースと競合することなく独立したSpeakerを宣言できる直交性、その他依存の少なさやインターフェースの小ささ、正確さなどに繋がります。上記の例はいささか抽象的すぎるのでもう少し具体的な例でいきます。errorしかしos.Fileしか受け取らないため、テストも難しく実ファイルを要求します。interfaceによってこれを解決するわけですが仮にos.Fileがinterfaceだとしてもまだ問題があります。error今回のSaveでは書き込み(Write関数)しか必要ないとすると上記はSOLID原則のインターフェース分離の原則を満たしていません。それによってstringのようなReadしかできない型、bytesのように読み書きしかできない型と互換性がなくos.Fileの機能すべてが必要な場合は問題ありませんがbytes.Buffer型を代替にしたテストを行うような柔軟性に欠けます。他の言語であれば通常、単にインターフェースを分けますがこれにも問題があります。pike氏から実際に提案されてはいます。go2の話なので特に結論は出ていませんが色々問題があり、個人的にBcmills氏の意見に賛成です。error今回のSaveのようなWriterやReader単体ごとの機能のみ必要な場合は問題ありませんがダックタイピングのない継承形式の言語ではReadWriterやReadWriteSeekCloserなどの組み合わせとなってくると継承が一気に複雑になります。ReadWriterとSeekWriterとReadCloserと...といったような継承は見たくないですね。os.Readerとか突っ込みたいところはあるかもしれませんが少なくともinterfaceを継承する他言語では問題があることはわかってもらえたと思います。Go言語でもまだ問題があります。これは実際に使われることになるioパッケージでも同じことです。ReadWriter,SeekWriter...どう使われるかわからない実装側がインターフェースを定義することには無理も無駄もありかといってすべて網羅すればあまりにも醜いです。ioパッケージでもReadWriteSeeker,ReadWriteCloserはありますがReadWriteSeekCloserはありません。interfacesioのような汎用インターフェースパッケージであればあまりによく使われるインターフェースはパッケージ側が使わずとも定義する価値はありますがGo言語では、必要であれば必要な側が定義すべきです。仮にosにインターフェースがあればインターフェース以外のosの実装との依存関係が発生しますし(かつてerrorがos.Errorだった時と同じ)必要とした側が定義すれば自然に最小限かつパッケージが名前空間となることも合わさってインターフェースの意味がより明示的になりやすいです。errorもちろん本当はioパッケージがWriterを持っているのでWriterでなくio.Writerを使うことになるでしょう。これではinterfaceを必要とする側で定義していないのではないかと思われるかもしれませんがioパッケージはファイルの実装でも文字列の実装でもありません、汎用的な入出力処理そのものに対するツールセットでありReaderを引数にしてReaderを返す類のラップ関数を除いて、引数で受け取る関数ばかりでReaderを生成するようなことはありません。実際にReadを実装する型を返すのはstringsやbytes,osなどの具体的な内容を持つパッケージです。重要なのは必要なインターフェースのみに依存出来ることです。その関数を満たしていればそのインターフェースが自分のものであってもサードパーティのものであっても構いません。提供しないことではなく要求できることに価値があります。詳細(実装)は抽象(インターフェース)に依存すべきとの言葉がありますがJavaやC#のインターフェースに依存するとき、それはある意味では同時にインターフェースの出処にも依存していました。Go言語ではユーザーが定義したインターフェースを標準ライブラリが満たすことも可能です。Goにおいて他パッケージのインターフェースを使う場合、その多くはioのようなユーティリティの範囲か、そのパッケージがそのInterfaceを必要としてるかがほとんどです。他言語では本当に単純で普遍的なインターフェースでしかインターフェース分離の原則を綺麗に守ることは難しいものでした。Goではそれが容易です。処理する側が必要なものを定義するだけなので、メソッドは少なく、より正確で、そこに依存関係そして階層はありません。structsの部分はもう少しシンプルです。インターフェースが最小限で正確なAPIでなければいけないなら処理を簡略化するためのユーティリティ関数は定義すべきでありません。JavaのListインターフェースにはaddとaddAllがありますがaddがあればfor文でaddを回せばaddAllと同等のことが出来るので効率の事情があってもユーティリティ関数の類です。いった本質的でない関数はインターフェースには追加すべきでなく、構造体の関数として持ったり、または関数化することで対応するべきです。よって多くのケースでは構造体を返したほうが理にかなっています。bytes.Readerには各Read関数だけでなく初期化やサイズ情報などの関数を持っています。オブジェクト指向ではカプセル化とよく聞く言葉ですがGoのインターフェースは実装を隠すのでなく実装に依存しないというべきかもしれません。(*Impl)(nil)これでできます。それにGoの言語仕様自体は全くではありませんが誤ったコードを咎めるようなことはしません。appendなどの組み込み関数は容易くオーバーライド出来るので勝手にappendを変な関数にされてしまうと問題ですがそういったプログラマが非常に稀であることを考慮し、言語のシンプルさを天秤にかけるとそれらは教育やツールの役割として扱った方がメリットが大きいと考えています。上記のインターフェースの実装の保証も、プロジェクトレベルの話で標準ライブラリでは使用されません。インターフェースにおいてもダックタイピングが生んだメリットはコストを上回るものだと思います。次に、結局関数のシグネチャが一致しないといけない以上実装を知っているようなもので意味がなく、実際にはインターフェースを意識しないといけないことについてですがこれは確かにそのとおりで似たようなことをやっていてもEqualがEqだったり,fmtではPrintfなのにtesting.TではLogfだったりします。標準にあるレベルであればLoggingライブラリのように大体サードパーティも合わせてくれますが保証はされません。実践的なレベルではある程度ラップして使うケースも多いです。それでもソースレベルでの依存が無いこと、そしてなにより型階層のない構成指向のGoの気風を巧みに表現していること、表現できることには価値があります。#GoのインターフェースGoのインターフェースは型の境界です。Goにとってインターフェースは汎用的とは限りません。ReaderやWriterのような汎用的なものはしばしばioパッケージのようにユーティリティを集約したパッケージとして提供されますがComparatorでなくsort.InterfaceSerializableでなくjson.Unmarshaler,gob.Unmarshalerといったようにそれぞれのパッケージが必要なものをインターフェースとして定義しています。Playgroundなどのもう少し実践的なソースではLoggerをインターフェースにするなどしています。Go言語らしいロギングについてGoのインターフェース、いいですね。