
前回のおさらい
前回は、関数とパッケージについて学びました。今回は、複数のデータをひとまとめにして扱うための 構造体(こうぞうたい) と、Goらしいプログラムを書く上で欠かせない インターフェース という考え方を学びます。少し難しく感じる回ですが、1つずつ確認していけば大丈夫です。
構造体ってなに?
これまでは、name := "太郎" や age := 14 のように、1つの変数に1つのデータだけを入れてきました。しかし実際のプログラムでは、「1人の人物に関する、名前・年齢・身長といった複数のデータを、まとめて扱いたい」ということがよくあります。
こういうとき、それぞれをバラバラの変数として管理するのは大変です。
name1 := "太郎"
age1 := 14
name2 := "花子"
age2 := 13
これだと、「name1とage1は同じ人物のデータだ」ということが、名前からしかわかりません。人数が増えるとさらに管理が大変になります。
そこで登場するのが構造体です。構造体は、「複数の、種類が違うデータをひとまとめにした、オリジナルの型」 です。
学級名簿をイメージするとわかりやすいかもしれません。名簿の1行には、「名前」「年齢」「身長」といった、複数の項目がセットになって書かれていますよね。構造体は、まさにこの「1行分の項目セット」を、プログラムの中で定義するためのものです。
構造体を作ってみよう
「人物」を表す構造体を作ってみましょう。
package main
import "fmt"
type Person struct {
Name string
Age int
Height float64
}
func main() {
taro := Person{
Name: "太郎",
Age: 14,
Height: 160.5,
}
fmt.Println(taro.Name)
fmt.Println(taro.Age)
fmt.Println(taro.Height)
}
実行結果は次のようになります。
太郎
14
160.5
1つずつ見ていきましょう。
type Person struct {
Name string
Age int
Height float64
}
これは、「Person(パーソン、人物)という名前の、新しい型を定義しますよ」という宣言です。type 型名 struct { ... } という形が決まった書き方です。{ } の中には、この構造体が持つ項目(フィールドと呼びます)を、「フィールド名 型」の形で並べます。ここでは、Name(文字列)、Age(整数)、Height(小数)という3つのフィールドを持つ、Person型を定義しています。
taro := Person{
Name: "太郎",
Age: 14,
Height: 160.5,
}
こちらは、実際にPerson型のデータ(インスタンスと呼ぶこともあります)を1つ作って、taroという変数に入れている部分です。「学級名簿のフォーマット(Person型)に沿って、太郎さんの実際のデータを1行分書いた」というイメージです。
fmt.Println(taro.Name)
構造体の中の、それぞれのフィールドの値を取り出したいときは、変数名.フィールド名 という書き方をします。taro.Name で「taroさんのNameフィールドの値」、つまり "太郎" を取り出せます。
このように構造体を使うことで、関連する複数のデータを、意味のあるまとまりとして管理できるようになります。
構造体にメソッドをつける
構造体には、その構造体専用の関数を持たせることができます。これをメソッドと呼びます。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) Introduce() string {
return fmt.Sprintf("私は%sです。%d歳です。", p.Name, p.Age)
}
func main() {
taro := Person{Name: "太郎", Age: 14}
fmt.Println(taro.Introduce())
}
実行結果は次のようになります。
私は太郎です。14歳です。
func (p Person) Introduce() string { ... } の、(p Person) の部分がポイントです。これはレシーバーと呼ばれ、「この関数は、Person型専用の関数(メソッド)ですよ。関数の中では、対象のPersonのデータをpという名前で扱えますよ」という意味になります。
呼び出す側では、taro.Introduce() のように、変数名.メソッド名() という形で呼び出します。第3回で学んだ普通の関数とは違い、「Person型のデータに対して行う専用の処理」として、Person型のデータにくっつけて呼び出せる、というのがメソッドの特徴です。
なお、fmt.Sprintf という新しい関数が出てきました。これは fmt.Println の仲間で、「画面に表示する」代わりに「文字列を組み立てて返す」関数です。%s の部分に文字列を、%d の部分に整数を、それぞれ順番にあてはめて、1つの文字列を作ってくれます。
インターフェースってなに?
最後に、この回でいちばん理解が難しいインターフェースについて説明します。
インターフェースを一言で表すと、「これができれば、なんという型でもOKですよ、という約束事」 です。
たとえるなら、「電気製品の、コンセントの規格」をイメージしてください。日本の家庭用コンセントの形さえ合っていれば、それが掃除機であろうと、テレビであろうと、ドライヤーであろうと、同じコンセントに挿すことができますよね。コンセント側は、「挿すものが掃除機かテレビか」を気にしていません。「コンセントの形(規格)さえ合っていればOK」というルールになっています。
インターフェースも同じ考え方です。「このメソッドさえ持っていれば、型が何であっても、この処理の中で扱えますよ」というルールを定義するものです。
実際にコードで見てみましょう。「鳴く」というSound()メソッドを持つ動物であれば、なんでも扱える、というプログラムです。
package main
import "fmt"
type Animal interface {
Sound() string
}
type Dog struct{}
func (d Dog) Sound() string {
return "ワン!"
}
type Cat struct{}
func (c Cat) Sound() string {
return "ニャー"
}
func introduce(a Animal) {
fmt.Println(a.Sound())
}
func main() {
dog := Dog{}
cat := Cat{}
introduce(dog)
introduce(cat)
}
実行結果は次のようになります。
ワン!
ニャー
順番に確認しましょう。
type Animal interface {
Sound() string
}
これが、インターフェースの定義です。「Animalという名前のインターフェースは、Sound() stringというメソッド(引数なしで、string型を返すメソッド)を持っていることを約束します」という意味です。中身の処理は書かず、「どんなメソッドを持っているべきか」というルールだけを定義します。
type Dog struct{}
func (d Dog) Sound() string {
return "ワン!"
}
Dogという構造体(中身のフィールドはなし)に、Sound()メソッドを実装しています。Catも同様です。
ここで重要なポイントがあります。Goでは、「この構造体はAnimalインターフェースです」と明示的に宣言する必要が ありません。Sound() string というメソッドを持ってさえいれば、DogもCatも、自動的にAnimalインターフェースとして扱えるようになります。これを、Goでは「ダックタイピング」ならぬ構造的部分型と呼んだりします。「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだとみなそう」という考え方です。
func introduce(a Animal) {
fmt.Println(a.Sound())
}
このintroduce関数は、「Animalインターフェースを満たしている型なら、なんでも受け取れる」関数です。中身ではa.Sound()を呼び出しているだけで、「渡されたのがDogなのかCatなのか」を気にしていません。
introduce(dog)
introduce(cat)
Dog型のdogも、Cat型のcatも、どちらもSound()メソッドを持っているので、同じintroduce関数に渡すことができます。それぞれのSound()の中身が違うので、実行結果も「ワン!」「ニャー」と、それぞれの動物らしい結果になる、というわけです。
インターフェースは何がうれしいの?
「型ごとに専用のintroduceDog関数、introduceCat関数を作ればいいのでは?」と思うかもしれません。ですが、動物の種類が増えるたびに専用の関数を作っていては、キリがありません。
インターフェースを使うと、「Sound()メソッドさえ持っていれば、将来Bird(鳥)やCow(牛)が増えても、introduce関数はそのまま使い回せる」という、柔軟で拡張しやすいプログラムを書けるようになります。この考え方は、少し規模の大きいプログラムを書くようになると、非常によく使うことになります。
まとめ
今回は、以下のことを学びました。
- 構造体は「複数の、種類が違うデータをひとまとめにした型」で、
type 名前 struct { ... }で定義する - 構造体には、専用の関数である「メソッド」を持たせることができる
- インターフェースは「特定のメソッドさえ持っていれば、型を問わず同じように扱える、という約束事」
- Goでは、明示的な宣言なしに、必要なメソッドを持っている型は自動的にインターフェースを満たしたことになる
構造体とインターフェースは、Goで少し本格的なプログラムを書くようになると、必ずと言っていいほど登場する重要な考え方です。最初は難しく感じても大丈夫なので、何度もコードを書いて動かしながら、少しずつ慣れていきましょう。
次回は、Goの大きな特徴の1つである、複数の処理を同時に進める仕組み「ゴルーチン」と「チャネル」について学んでいきます。
次回:「Go言語入門⑤ゴルーチン・チャネル(並行処理)編」お楽しみに。


