Go

Goを学びたての人が誤解しがちなtypeと構造体について #golang

More than 1 year has passed since last update.

はじめに

タイトルをキャッチーかつ若干煽り気味にしたのは、そもそも記事を見てもらう確率を上げるためで他意はありません。なぜ読んでほしいのかというと、typestructについて、一部の機能に着目して、それがtypestructの全てだと誤解されるとマズいなと感じることが最近多いためです。

この記事で書いてあることは過去にもインタフェースの実装パターン2016年度Go研修で取り上げていますが、今回は端的に分かりやすくなるようにまとめたいと思います。

構造体

構造体とは何でしょうか?言語仕様を見ると、名前と型を持つフィールドというもの集まりだと書かれています。構造体型は、structという予約語を使って定義することができます。

struct {
    Name string
    Age  int
}

上記の場合だと、string型のNameフィールドとint型のAgeフィールドを持つ構造体を表しています。
Goを勉強したことがある方は、なんでtypeと書かないんだろうと思うかもしれませんが、typeの話とstructの話は別の概念の話なので、ここでは別で扱います。

構造体型の変数は以下のように定義することができます。

var person struct {
    Name string
    Age  int
}

初期化したい場合は、以下のように書くことができます。

var person struct {
    Name string
    Age  int
}{
    Name: "tenntenn",
    Age:  30,
}

フィールドへの代入は.を使ってアクセスします。

person.Name = "Takuya Ueda"

また、構造体のフィールドにはstructタグが設けれたり、名前のない匿名フィールドの埋め込みなんかもありますが、ここでは話がややこしくなるので解説するのはやめておきます。詳しく知りたい型は、言語仕様を読むか、インタフェースの実装パターンを読んで頂けると幸いです。

さて、typeやメソッドの話は構造体の話では出てきません。なぜなら、構造体とtype、メソッドの話は別の概念だからです。
次にtypeの解説をしましょう。

type

typeという予約語を用いると、既存の型や型リテラルに別名をつけることができます。言語仕様にも同じようなことが書いてあります。

ここで注意してほしいのは、typeは決して、構造体だけのために用いられるものではないということです。初学者向けのサイトや書籍では、構造体の定義方法をtype有りきで説明しがちです。確かに間違ってはいないし、他の言語のclassの概念と対比できて、分かりやすいかもしれません。しかし、私はあまりその説明方法を好みません。なぜなら、typeは構造体型以外の型にも名前をつけることができるし、そこからつながるメソッドやインタフェースについても、構造体という制限はないからです。

言語仕様を確認することが、言語を学習する上でもっとも正しく理解できる方法だと思います。言語仕様に書いてあるtypeの文法を見てましょう。

TypeDecl     = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec     = identifier Type .

typeという予約語の後ろにTypeSpecというものが来ています。このTypeSpecは何なのかというと、identifierの後ろにTypeを記述したものです。identifierは識別子を表すので、ここでは新しくつける型名にあたります。それではTypeとは具体的に何にあたるのでしょうか?

言語仕様からTypeの定義を調べてみましょう。

Type      = TypeName | TypeLit | "(" Type ")" .
TypeName  = identifier | QualifiedIdent .

上記の定義から、TypeNameまたは、TypeLit、そして、()で括られたTypeということが分かります。TypeNameは、identifierQualifiedIdentなので、識別子、つまりはすでに名前の付いている型を表します。ちなみに、QualifiedIdentは、以下のように定義されているので、別のパッケージの型を参照する際に用いられる記法です(この場合は型だが、変数や関数の場合もある)。

QualifiedIdent = PackageName "." identifier .

さて、ここまででtypeで新しく名前が付けられる型として、構造体だけではなく、既存の型にも別名が付けられることが、言語仕様を見ることで分かりました。つまり、以下のような記述ができるということです。

type Hex int

これは、int型にHexという別名を付けています。なお、intHexのようにtypeで別名をつけた場合、キャストなしでは各型をまたぐ演算や変数へ代入ができません。

もちろん、別のパッケージの型にも新たに名前をつけることができます。

type MyReader io.Reader

文法をきちんと確認すると、普段使ってなくて知らないことに気付かされます。

自明な"(" Type ")"は置いておくとして、Typeにはもう一つ、TypeLitという記述ができることが文法上から分かっています。TypeLitの定義を見てみましょう。

TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
            SliceType | MapType | ChannelType .

どうやら色んな型の記述であることが分かります。TypeLitが何を表すのかというと、型リテラルというものを表します。型リテラルは型の情報そのものを記述する方法で、よく知られているのは以下のスライスやマップの定義でしょう。

var ns []int
var m map[string]int

上記のTypeLitの定義を見る限り、構造体や配列、ポインター、関数、インタフェース、チャネルなども同じように型リテラルで書くことができます。

ここで察しが良い方は、構造体の変数定義を

var person struct {
    Name string
    Age  int
}

のように書ける理由が分かったでしょう。文法上では、構造体の型リテラルもスライスの型リテラル([]intなど)も同じようにTypeLitで扱われ、さらにTypeでまとめられています。たとえば、上記の例で出てきている変数宣言のvarについても、以下のように定義されています。

VarDecl     = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec     = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

1つ以上の識別子の後ろにTypeを書くということが文法上で定義されています。

typeの方に話を戻すと、typeで別名を定義できるものとして、以下の4つがあることが分かりました。

  • 組み込み型(intやfloat64)など
  • パッケージ内の型(typeで定義されたもの)
  • パッケージ外の型(typeで定義され、パッケージ名を指定して記述するもの)
  • 型リテラル

なお、文法上は組み込み型とパッケージ内の型については区別がありません。ここまでの説明で、構造体とtypeはセットで覚えるものとすることが、あまり良いことではないことがお分かりいただけたかと思います。

構造体を学習する上で、他の言語のclassに関連付けてメソッドについても一緒に扱うことが多いようです。メソッドについても構造体ありきで覚えてしまうと非常にもったいので、簡単に説明します。

メソッド

メソッドは、関数定義に加えて、名前の前にレシーバというものを記述します。

func (h Hex) String() string {
    return fmt.Sprint("%x", int(h))
}

このレシーバにできる型は、以下のような決まりがあります。

  • typeで名前が付けられている型はOK
  • 上記の型のポインタ型はOK
  • 組み込み型はダメ
  • パッケージ外の型はダメ
  • 型リテラルはダメ

つまり、パッケージ内でtype定義された型のみがレシーバにできます。

ここで思い出してほしいのは、typeで別名が付けれる型についてです。typeで新たに名前が付けれるのは以下の4つでした。

  • 組み込み型(intやfloat64)など
  • パッケージ内の型(typeで定義されたもの)
  • パッケージ外の型(typeで定義され、パッケージ名を指定して記述するもの)
  • 型リテラル

つまり、上記の4種類については、typeで新たに型名さえつければ、メソッドのレシーバとして使用することができます。

そのため、以下のようなこともできます。

type Hex int
func (h Hex) String() string {
    return fmt.Sprint("%x", int(h))
}

また、もちろん関数にもメソッドを付けれます。

type Func func() string
func (f Func) String() string {
     return f()
}

スライスやチャネルなどの型リテラルにも、typeで別名を付けさえすれば、メソッドを設けることができます。
そして、ここでは詳しく触れませんが、Goのインタフェースはメソッドの集まりで、あるインタフェースのメソッドリストが、ある型のメソッドリストに内包する形になっていれば、その型はそのインタフェースを実装したことになります。つまりは、構造体以外もメソッドが設けられるのえ、インタフェースを実装することができるということです。関数にインタフェースを実装させるのは、http.HandlerFuncのように、標準パッケージでも多く使われているので、上記のtypeやメソッドの性質を理解しておくのは、インタフェースや標準パッケージを理解する上で重要です。

おわりに

いかがだったでしょうか?
typeやメソッドが構造体ありきで学習していくと、後々、http.HandlerFuncなどに出くわしたときに、きちんと理解できず、「おまじない」になってしまいがちです。言語仕様をきちんと読み、普段なんとなく書いている記述が文法上どのように定義されているのか理解することで、今まできちんと理解してなかった概念や知らなかった概念を知る機会になります。Goの言語仕様は、そんなに長くなく、少しずつ読んでいけばちゃんと読める量なので、ぜひ読んでみることをオススメします。英語が厳しい方は、文法を定義したBNFを斜め読みするだけでも十分効果はあると思います。