こんにちは。Go入門ブログの第11回です。
本記事では、構造体の基本について解説していきます。
また、構造体とあわせて使用されることが多いポインタについても本章であわせて説明します。
Javaなどのオブジェクト指向言語においては、クラスやオブジェクトの設計と実装がプログラミングの中心作業となります。
これと同様に、非オブジェクト指向言語であるGoでは、構造体とインターフェースを設計・実装することが非常に重要になります。
ポインタ
ポインタとは何かを簡潔に表すと、変数や関数のメモリ上のアドレス(格納場所)を指し示す変数と言えます。
ポインタを取り扱う言語としてはC言語が有名です。
しかし、ポインタの操作や演算を誤ると、プログラムを暴走させる原因となってしまいます。
このことから、ポインタはC言語における最大の障壁とも言われます。
GoでもCと同様にポインタを扱うことができますが、C言語と比較すれば複雑な利用を求められるケースは限定的ですので、さほど構えなくても大丈夫です。
ポインタの定義とアドレス演算子
型名の前にアスタリスク(*)を記述することでポインタ型を定義できます。
なお、初期化されていないポインタ型の値(ゼロ値)はnilになります。
また、演算子&を使用することで、任意の型の被演算子からポインタを生成することができます。
以下は実際にポインタ型の変数を定義し、他の変数のメモリアドレスを代入するサンプルです。
package main import "fmt" func main() { i := 100 s := "ポインタ" // ポインタを定義し初期値を代入 var pi *int = &i // :=演算子による代入も可能 ps := &s fmt.Println(pi) fmt.Println(ps) }
アドレス演算子とデリファレンス
演算子&を使用することで、ポインタ型の被演算子に対してデリファレンスを行うことができます。
デリファレンスとは、ポインタ型が保持するメモリアドレスを経由してデータを参照する仕組みです。
次の例では、int型の変数iと、そのメモリアドレスを指すポインタ型変数のpiが定義されています。
*piによるデリファレンスで、変数iの値を参照・更新しています。
package main import "fmt" func main() { i := 100 var pi *int = &i // ポインタ経由でデータの値を参照する fmt.Println(*pi) // ポインタ経由でデータの値を書き換える *pi = 200 fmt.Println(i) }
このサンプルの実行結果は以下のようになります。
100
200
構造体
構造体とは、複数の任意の値をひとつにまとめたものです。
数値や文字列などの基本型だけでなく、配列型や参照型、ポインタ型などの様々なデータ構造をまとめて、1つの型として取り扱うことができます。
Goにおいて複雑なデータを扱うプログラミングを行うには、様々な構造体を設計し、定義する必要があります。
構造体の定義
構造体の定義には、typeステートメントを使用します。
複数ののデータを構造体に格納し、typeを使ってそれに新しい型名を付ける、というのが構造体の一般的な使用方法です。
フィールド名1 型1
フィールド名2 型2
. . . .
}
構造体に含まれる要素のことをフィールドといいます。
フィールドはそれぞれが名前と型を持っています。
構造体の生成とフィールドへのアクセス
定義した構造体を生成するには、変数の定義と同様にvar [変数名] [構造体名]の形式で記述します。
生成した構造体の各フィールドには、ドット(.)を用いてアクセスします。
実例を見てみましょう。
package main import ( "fmt" ) type Nums struct { x int y int z int } func main() { // 構造体を生成 var nums Nums // 構造体のフィールドを参照 fmt.Println(nums.x) // 構造体のフィールドに値を代入 nums.y = 10 nums.z = 100 fmt.Println(nums) }
このサンプルの実行結果は以下のようになります。
0
{0 10 100}
構造体のリテラル
先ほどの例では、構造体の生成時に初期値を指定していなかったため、各フィールドにはゼロ値が代入されていました。
構造体の生成時に初期値を代入する場合、構造体のリテラルを記述する必要があります。
構造体のリテラルは、各フィールドの値を{ }で囲ってカンマ区切りで列挙します。
具体的には2通りの記述方法があります。実例を見ながら説明していきましょう。
package main import ( "fmt" ) type Nums struct { x int y int z int } func main() { // フィールド名を省略してリテラルを記述 var nums2 Nums = Nums{10, 20, 30} // フィールド名を明示してリテラルを記述 var nums3 Nums = Nums{x: 100, y: 200} fmt.Println(nums1) fmt.Println(nums2) }
このサンプルの実行結果は以下のようになります。
{10 20 30}
{100 200 0}
15行目では、フィールド名を指定せずに値を列挙しています。
このように記述すると、構造体の中で最初に定義されているフィールドxに1つめの値が、2番目に定義されているフィールドyに2つめの値が…というように、リテラルに記述した値が順番に代入されていきます。
このように記述した場合、構造体のフィールド数とリテラルに記述された値の数が一致していないとコンパイルエラーが発生します。記述が簡略的である一方で、値の数と順序をフィールドと一致させる必要があるということです。
17行目では、フィールド名を明記し、コロン(:)で区切る形で値を記述しています。
この記述方法だと、リテラル内で指定されなかったフィールド(この例ではz)にはゼロ値が初期値として代入され、コンパイルエラーは発生しません。また、記述する順序も任意です。
特別な理由がない限り、フィールド名を明記する記述方法を採用すべきでしょう。
その方が、後から構造体の定義変更なども行いやすく、また可動性も高くなります
構造体を含む構造体
構造体には任意の型のフィールドを含めることができます。
したがって、構造体のフィールドとして定義済みの他の構造体を埋め込むことも可能です。
package main import "fmt" type Nums struct { x int y int z int } // 構造体Numsを埋め込む構造体 type embed struct { z int Nums // フィールド名を省略した埋め込み構造体 Nums2 Nums // フィールド名を明記した埋め込み構造体 } func main() { // 構造体embedを生成 emb := embed{ z: 100, Nums: Nums{ // 埋め込み構造体の生成 x: 1, y: 2, z: 3, }, Nums2: Nums{ // 埋め込み構造体の生成 x: 10, y: 20, z: 30, }, } // フィールド名を指定した場合は参照時にもフィールド名の指定が必要 fmt.Println(emb.Nums2.x) fmt.Println(emb.Nums2.y) fmt.Println(emb.Nums2.z) // フィールド名を省略した場合は参照時も構造体フィールド名を省略できる fmt.Println(emb.x) fmt.Println(emb.y) fmt.Println(emb.Nums.x) fmt.Println(emb.Nums.y) // フィールド名が一意に定まらない場合は参照時に埋め込み構造体のフィールド名指定が必要 fmt.Println(emb.z) fmt.Println(emb.Nums.z) }
このサンプルの実行結果は以下のようになります。
10
20
30
1
2
1
2
100
3
構造体を埋め込む際、14行目のようにフィールド名を省略する方法と、15行目のようにフィールド名を明記する方法があります。
フィールド名を省略した場合、フィールド名は暗黙的に埋め込んだ構造体と同一の名称になります。
埋め込まれた構造体内のフィールドへは、上記サンプルのemb.Nums2.xのように、段階的に辿ってアクセスすることができます。
また、フィールド名を省略しており、かつフィールド名が一意に定まる場合に限り、上記サンプルの40-41行目のように中間の構造体フィールド名を省略することができます。
構造体とポインタ
さて、最初に説明したポインタを構造体とどのように組み合わせて使用するかをみてみましょう。
構造体は値型です。
関数の引数として構造体を渡した場合、引数は値渡しとなり、関数内でのみ処理されて元の構造体には影響を与えません。
そこで、構造体を参照渡しするために、ポインタを使用します。
ポインタを引数で渡すという事は、メモリアドレスを渡すことになります。
したがって値渡しのように構造体のコピーが作成されることはなく、呼び出し元の構造体が直接渡されます。
これにより、関数内の処理が元の構造体に反映されることになります。
それでは実例をみてみましょう。
package main import "fmt" type Nums struct { x int y int z int } // 構造体を引数として受け取る関数 func double(nums Nums) { // 引数で受け取った構造体の各フィールドを2倍 nums.x = nums.x * 2 nums.y = nums.y * 2 nums.z = nums.z * 2 fmt.Println("関数double内における構造体の値", nums) } // 構造体のポインタを引数として受け取る関数 func triple(nums *Nums) { // 引数で受け取った構造体の各フィールドを3倍 nums.x = nums.x * 3 nums.y = nums.y * 3 nums.z = nums.z * 3 fmt.Println("関数triple内における構造体の値", *nums) } func main() { // 構造体Numsを生成 nums := Nums{ x: 1, y: 2, z: 3, } // 構造体を引数で取る関数の実行(値渡し) double(nums) fmt.Println("関数double実行後の構造体の値", nums) // 構造体のポインタを引数で取る関数の実行(参照渡し) triple(&nums) fmt.Println("関数triple実行後の構造体の値", nums) }
このサンプルの実行結果は以下のようになります。
関数double内における構造体の値 {2 4 6}
関数double実行後の構造体の値 {1 2 3}
関数triple内における構造体の値 {3 6 9}
関数triple実行後の構造体の値 {3 6 9}
関数doubleは値渡しのため、関数内の更新が呼び出し元に反映されていません。
対して、関数tripleは参照渡しになっており、関数内の更新が呼び出し元にも反映されています。
終わりに
本記事では、Goの最重要要素のひとつである構造体の基本を、ポインタとあわせて解説しました。
本記事の内容で、構造体の定義と生成、基本的な操作ができるようになったかと思います。
次回の記事では、構造体と手続き処理を結びつける為の仕組みであるメソッドについて解説します。