2章ではstringやintなど、基本的な組み込み型を紹介しましたが、本章では独自の型の宣言方法と使い方について解説します。
type
次のような、IDと優先度を取得してタスクを処理する関数を考えてみます。2つの情報は両方とも数値で表すため、intとして宣言しています。
この関数を呼び出す側は次のようになります。
正しく呼び出せていますが、もし次のように順番を間違えたらどうなるでしょうか。
引数の型が合っているため、コンパイルは通ってしまいます。こうしたミスはテストによって発見することもできますが、それぞれが単なるintではなく、意味に応じた型を持っていれば、コンパイル時に間違いを知ることができます。
このような場合、Goではtype
を用いて既存の型を拡張した独自の型を定義できます。
type
のあとには、型の名前、その型の定義が続きます。上記では、intを拡張してIDと優先度それぞれに型を定義し、この型を用いて関数の定義を書き換えています。
呼び出す際には、型が適合していないとコンパイルエラーになります。
このように適切な型を用意することで、型レベルの整合性をコンパイル時にチェックでき、堅牢なプログラムを記述できます。IDE(Integrated Development Environment、統合開発環境)のサポートも得やすくなり、リファクタリング時のリグレッションなども防ぎやすくなります。
構造体(struct)
Goには、構造体というデータ構造があります。構造体は複数のデータを1つにまとめることが基本的な役割ですが、後述するメソッドを持つことができ、RubyやJavaでのクラスに近い役割も担います。
構造体型の宣言
ここでは、id、detail
(タスクの詳細)、done
(完了フラグ)の3つのフィールドを持つ、Task
という型を定義してみます。
構造体型もtype
を用いて宣言し、構造体名のあとにそのフィールドを記述します。各フィールドの可視性は名前で決まり、大文字で始まる場合はパブリック、小文字の場合はパッケージ内に閉じたスコープとなります。
この型から値を生成するには、次のように各フィールドに値を割り当てます。
変数task
には、生成された構造体が格納され、各フィールドにはドットでアクセスできます。
構造体に定義した順でパラメータを渡すことで、フィールド名を省略することもできます。
構造体の生成時に値を明示的に指定しなかった場合は、ゼロ値で初期化されます。
ポインタ型
構造体型もアドレスを取得し、ポインタ型で扱うことができます。構造体から値を生成するときに、構造体の名前の前に&
を付けると、変数には構造体の値ではなくアドレスが格納されます。Task
のポインタ型は*Task
という型になります。
たとえば、関数に対して構造体を値渡しするとデータはコピーされるため、関数内での構造体への変更は呼び出し側には反映されません。
この関数の引数をポインタ型にするには、引数の型を*Task
とします。ポインタを渡すことで、渡した側に関数内での変更が反映されます。
このように、Goでは値とポインタを用途に応じて使い分けることができます。
new()
構造体は、組込みの関数new()
を用いて初期化することもできます。new()
は、構造体のフィールドをすべてゼロ値で初期化し、そのポインタを返します。
コンストラクタ
Goには構造体のコンストラクタにあたる構文がありません。代わりにNew
で始まる関数を定義し、その内部で構造体を生成するのが通例です。たとえばTask
をNew
する関数はNewTask()
という関数にし、内部でTask
を生成し、そのポインタを返します。
メソッド
型にはメソッドを定義できます。メソッドは、そのメソッドを実行した対象の型をレシーバとして受け取り、メソッドの内部で使用できます。たとえば、Task
の文字列表現を返すString()
というメソッドをTask
に定義してみます。
このメソッドは、レシーバのフィールド値から文字列を生成し、それを戻り値として呼び出し側に返します。このとき、Task
のコピーがレシーバとして渡されるため、もしメソッド内部でレシーバの中身を変更していても、呼び出し側には反映されません。
呼び出し側に変更を反映したい場合は、レシーバをポインタとして受け取るようにします。たとえば、タスクを終了済みにするFinish()
というメソッドをTask
に定義してみます。
今回は、Finish()
が実行されたTask
のポインタを受け取り、メソッド内でその内部を書き換えています。ポインタを経由して書き換えているため、呼び出し側にもその変更が反映されています。
インタフェース
Goのインタフェースは、その型がどのようなメソッドを実装するべきかを規定する役割を持ちます。
インタフェースの宣言
たとえば、先ほどTask
に実装したString()
という振る舞いが規定されていることを表すインタフェースは、次のように定義します。
インタフェースの名前は、実装すべき関数名が単純な場合は、その関数名にer
を加えた名前を付ける慣習があります。よってString()
を実装するインタフェースはStringer
となります。
インタフェースの実装
Goでは、Javaのimplements構文のように、インタフェースを実装していることを明示的に宣言する構文はありません。Goは、型がインタフェースに定義されたメソッドを実装していれば、インタフェースを満たしているとみなします。たとえば先ほどTask
にはString()
メソッドを実装しているため、Stringer
を引数に取る次のような関数に渡すことができます。
実は、このStringer
インタフェースはGoのfmtパッケージに標準で定義されており、レシーバの文字列表現を取得するためのAPIになっています。
ほかにも、代表的な標準インタフェースとしては表1のようなものがあります。
表1 Goの主な標準インタフェース
インタフェース名 | 定義 | 説明 |
io.Reader | Read(p []byte) (n int,err error) | リソースからデータの読み出しを行う |
io.Writer | Write(p []byte) (n int,err error) | リソースへのデータの書き込みを行う |
io.Closer | Close()error | リソースのクローズ処理を行う |
http.Handler | ServeHTTP (ResponseWriter,*Request) | HTTPリクエストに対するレスポンスを行う |
json.Marshaler | MarshalJSON() ([]byte,error) | 構造体やスライスなどをJSONに変換する |
json.Unmarshaler | UnmarshalJSON ([]byte)error | JSONを構造体やスライスなどに変換する |
interface{}
次のようなインタフェースを考えてみます。
このインタフェースは、実装すべきメソッドを指定していません。つまり、すべての型はこのインタフェースを実装していることになります。
これを利用すると、次のようにどんな型も受け取ることができる関数を定義できます。
また、Anyのような型を定義しなくても、次のように直接記述することもできます。
たとえば、fmt.Println()
などのいわゆるプリント関数は、これを用いて次のように定義されているため、型を気にせずに複数の値を渡すことができるのです。
型の埋め込み
Goでは、継承はサポートされていません。代わりにほかの型を「埋め込む」(Embed)という方式で、構造体やインタフェースの振る舞いを拡張できます。
構造体の埋め込み
例として、先ほどのTask
に対して、User
の情報を埋め込んでみましょう。
User
構造体の定義は以下とし、メソッドとしてFullName()
とコンストラクタ関数を実装します。
これをTask
に埋め込みます。Task
の構造体型宣言時に、フィールドではなく型のみを記述することで、その型を埋め込むことができます。
埋め込まれたUser
のフィールドやメソッドは、Task
が実装しているかのように振る舞います。また、埋め込まれた型の実体にもアクセスできます。
インタフェースの埋め込み
インタフェースも埋め込み可能です。主な用途は、複数のインタフェースを埋め込んで新たなインタフェースを定義することです。
たとえばioパッケージでは、Reader
、Writer
といったインタフェースが定義されています。
このとき、Read()
とWrite()
を両方定義したインタフェースであるReadWriter
は、2つのインタフェースを埋め込んで次のように定義できます。
ioパッケージには同様に、Closer
インタフェースを埋め込んだReadCloser
、WriteCloser
、ReadWrite Closer
なども定義されています。このように既存のインタフェースをさらに拡張したインタフェースを定義するのにも、埋め込みの概念が使われます。
型の変換
Goでは、暗黙的な型変換が起こることはありません。しかし型を変換できないわけではなく、明示的に変換する方法がいくつか提供されています。
キャスト
キャストは、キャストしたい型を指定して次のように行います。
キャストに失敗した場合はパニックが発生します。
Type Assertion――型の検査
あるインタフェース値が指定した型であるかを調べるには、Type Assertion(型の検査)を使用します。以下では引数をinterface{}
型で受け取る関数内で、Type Assertionによって文字列であることを判定しています。
第一戻り値には判定が成功した場合にその型に変換された値が返り、第二戻り値には判定が成功したかどうかが真偽値で返ります。もし第二戻り値をとらなかった場合は、判定に失敗したときにパニックが発生します。
Type Switch――型による分岐
Type Assertionは単一の型に対する検査しかできませんが、Type Switch(型での分岐)を使うと複数の型に対する検査を実行できます。そのインタフェース値がどの型なのかによって処理を分岐したい場合は、switch
と組み合わせることで型ごとに処理を分岐できます。
まとめ
本章では、Goの型システムと構造体などについて解説しました。型の扱いは堅牢なプログラムにするために重要ですので、きちんと押さえておきましょう。