【Go言語】`implements` 宣言は不要?「ダックタイピング」なインターフェースの衝撃

【Go言語】implements 宣言は不要?「ダックタイピング」なインターフェースの衝撃

JavaC#でインターフェースを使う時、まずは「契約(Contract)」としてインターフェースを定義し、それをクラスで「実装します!」と明示的に宣言(implements / :)しますよね。

しかし、Go言語には implements キーワードが存在しません。 では、どうやって「このクラスはこのインターフェースを満たしている」と判断するのでしょうか?

1. ダックタイピング:「アヒルみたいに歩くなら、それはアヒルだ」

Goのアプローチは 「構造的部分型(Structural Subtyping)」 、通称ダックタイピングと呼ばれるものです。

「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」

つまり、「あるインターフェースが要求するメソッドをすべて持っていれば、明示的な宣言がなくても、自動的にそのインターフェースを実装したとみなす」 というルールです。


2. コードで比較:Java/C# vs Go

「動物(Animal)」インターフェースと、それを実装する「犬(Dog)」クラスで比較してみましょう。

Java / C# の場合(明示的)

「私は Animal です!」と名乗る必要があります。

// C#
public interface IAnimal {
    void Speak();
}

public class Dog : IAnimal // ← ここで明示的に宣言が必要!
{
    public void Speak() {
        Console.WriteLine("ワン!");
    }
}

Go の場合(暗黙的)

Dog 構造体は Animal インターフェースのことなど知らなくても構いません。ただメソッドを持っていればいいのです。

// Go
// 1. インターフェース定義
type Animal interface {
    Speak()
}

// 2. 構造体定義(Animalについては何も言及しない)
type Dog struct {}

// 3. メソッド実装(これで自動的に Animal を満たしたことになる)
func (d Dog) Speak() {
    fmt.Println("ワン!")
}

// 使い方
func main() {
    var a Animal
    a = Dog{} // コンパイルOK! DogはSpeak()を持ってるから。
    a.Speak()
}

もし Dog から Speak() メソッドを削除したり、引数を変えたりすると、a = Dog{} の代入箇所でコンパイルエラーになります。「DogAnimal インターフェースを満たしていません」と怒られます。


3. なぜこの仕様なのか?(Java/C#エンジニアへのメリット)

「明示しないと分かりにくくない?」と不安になるかもしれません。しかし、この設計には強力なメリットがあります。それは「利用する側(Consumer)がインターフェースを定義できる」という点です。

Java/C# の悩み

外部ライブラリのクラス SomeLibClass をテストでモックしたい時、そのクラスが ISomeLibClass を実装していなければ、モックを作るのは大変です(ラッパークラスを作ったりする必要があります)。

Go の解決策

外部ライブラリがインターフェースを提供していなくても構いません。あなたが自分のコードの中で、「このメソッドさえあればいい」というインターフェースを勝手に作ればいいのです。

// 外部ライブラリ(変更できない)
type ExternalLogger struct { ... }
func (l ExternalLogger) Log(msg string) { ... }

// 自分のコード
// ExternalLoggerに合わせて、勝手にインターフェースを作る
type MyLogger interface {
    Log(msg string)
}

// この関数は、ExternalLoggerでも、自作のMockLoggerでも受け取れる!
func DoSomething(logger MyLogger) {
    logger.Log("Hello")
}

これにより、依存関係の結合度(Coupling) が劇的に下がります。


4. interface{}any :究極の抽象型

Goには java.lang.ObjectC#object 型に相当する、「あらゆる型が入る型」があります。 それが interface{}(空のインターフェース) です。

メソッドの要件が何もない(空っぽ)ということは、「どんな型でもこの要件(虚無)を満たしている」 と言えるからです。

Go 1.18 からは、この interface{}エイリアスとして any というキーワードが導入されました。Java/C#エンジニアにはこちらの方が馴染みやすいでしょう。

func PrintAnything(v any) {
    fmt.Println(v)
}

PrintAnything(100)      // OK
PrintAnything("hello")  // OK
PrintAnything(Dog{})    // OK

5. ダウンキャスト(型アサーション

any 型に入れた値を、元の型に戻したい時はどうするでしょうか? Java(String)objC#obj as string に相当するのが アサーション です。

var i any = "hello"

// Java: String s = (String) i;
s := i.(string) // 成功すれば s に "hello" が入る

// 安全なキャスト(C#の as に近い)
// s, ok := i.(string)
s, ok := i.(string)
if ok {
    fmt.Println("文字列です:", s)
} else {
    fmt.Println("文字列ではありません")
}

ここでも、チャネルの時に出てきた「Comma-ok イディオム」が登場しますね!

taglibrary.hatenablog.com


まとめ

  1. 宣言不要: メソッドシグネチャが一致すれば、自動的に実装したとみなされる。
  2. 後出しジャンケンOK: クラス(構造体)を作った後に、それに合わせたインターフェースを定義しても機能する。
  3. any (interface{}): JavaObject のように何でも入る型だが、使う時は型アサーションが必要。

JavaC#では「設計段階でインターフェースをきっちり決める」ことが重要でしたが、Goでは「必要になったらインターフェースを切り出す」という、よりプラグマティック(実用的)なスタイルが好まれます。