Goを学びたての人が誤解しがちなtypeと構造体について #golang
December 6, 2016
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
タイトルをキャッチーかつ若干煽り気味にしたのは、そもそも記事を見てもらう確率を上げるためで他意はありません。なぜ読んでほしいのかというと、type
とstruct
について、一部の機能に着目して、それがtype
とstruct
の全てだと誤解されるとマズいなと感じることが最近多いためです。
この記事で書いてあることは過去にもインタフェースの実装パターンや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
は、identifier
とQualifiedIdent
なので、識別子、つまりはすでに名前の付いている型を表します。ちなみに、QualifiedIdent
は、以下のように定義されているので、別のパッケージの型を参照する際に用いられる記法です(この場合は型だが、変数や関数の場合もある)。
QualifiedIdent = PackageName "." identifier .
さて、ここまででtype
で新しく名前が付けられる型として、構造体だけではなく、既存の型にも別名が付けられることが、言語仕様を見ることで分かりました。つまり、以下のような記述ができるということです。
type Hex int
これは、int
型にHex
という別名を付けています。なお、int
とHex
のように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を斜め読みするだけでも十分効果はあると思います。