nakaoka3の技術ブログ

2023年中に52本なにか書きます

Go言語の型 構造的型付けと名前的型付け

最近 Go言語をよく触っていたので、Goの型について調べたことをまとめます。

Goのインターフェイス

Goの インターフェイスは、構造的型付け structural typing の特性を持っています。

Goのインターフェイスはメソッドのシグネチャの集まりです。

// M() というメソッドを持つインターフェイス
type I interface {
    M()
}

ある構造体に対して、インターフェイスのメソッドをすべて実装することで、インターフェイスの型として扱うことができます。このように型の等価性や互換性をその型の持つ構造(Goの場合はメソッド)に基づいて判断することを構造的型付けといいます。

type A struct {
    s string
}

func (a A) M() {
    fmt.Println(a.s)
}

type B struct {
    s string
}

func (b B) M() {
    fmt.Println(b.s)
}


func main() {
    // M というメソッドを実装しているため、I として扱う事ができる
    var x1 I
    x1 = A{"a"}
    x1.M()
    // Bも同様
    var x2 I
    x2 = B{"b"}
    x2.M()
}

構造的型付けと比較されるのが名前的型付け nominal typing です。構造的型付けとは対象的に、名前的型付けでは型の等価性や互換性は明示的に書かれます。

以下はJavaによる名前的型付けの例です。Goとは異なり、Javaではあるクラスをあるインターフェイスとして扱うには、明示的に implements インターフェイス名 を書く必要があります。

interface I {
    void M();
}

class A implements I {
    public void M() {
        System.out.println("hello");
    }
}

I i = new A();

Goの構造体

Go の構造体は名前的型付けの特性を持っています。同じ構造であっても、違う名前がつけられた型は異なる型として扱われます。

上記で定義した AとBは同じプロパティとメソッドを持った構造体ですが、互換性はありません。Aの型としてBをアサインしようとしてもコンパイルエラーになります。

var a A
// a = B{1} // コンパイルエラー
a = A{"a"}

Go言語ではある構造体に別の名前をつけて新しい型を作ることができます。同じフィールドを持った構造体ですが、別の型として扱われます。

// 構造体の型Aから、新しく別の構造体の型A2を作る
type A2 A

// var a2 A = A2{"a"} // コンパイルエラー

このようにGoでは、インターフェイスに関しては構造的型付けの特性を持っていますが、構造体に関しては名前的型付けの特性を持っているということができます。

TypeScriptの場合

Goの型について調べていくうちに、TypeScriptの型も構造的型付けの特性を持つということを知りました。

type A = {
    x: number;
    y: number;
}
// Aと同じプロパティを持つ型
type B = {
    x: number;
    y: number;
}


let a : A = { x: 0, y: 0 };
// AとBは同じ構造なのでアサインできる
let b : B = a;

A と B は構造的に等しいので、コンパイルエラーにはなりません。

Goの構造体で異なる型の構造体同士に互換性がないのとは対照的です。

まとめ

  • Go では インターフェイス interface に関しては構造的型付けを採用している
  • Go では 構造体 struct に関しては名前的型付けを採用している
  • Goの型システムは構造的型付けと名前的型付けの両方の特性を持っている