Goデータ構造
この章では、Goのデータ構造について学び、ハンズオン形式で学習します。これにより、配列・スライス・マップを使い分ける基本が身につきます。
1. 本章の概要
1.1 本章の目的
Goでは配列・スライス・マップを組み合わせてデータを扱います。それぞれの向き不向きを押さえておかないと、実務のコードでパフォーマンスや可読性の面で不利な書き方をしがちです。本章では、固定長の配列と可変長のスライスの違い、append によるスライスの拡張に加えて、マップの基本操作、for-range によるスライス・マップの反復も扱います。
1.2 ハンズオンの流れ
配列・スライスを実際に宣言・操作し、要素追加や容量の変化を出力しながら挙動を確認します。マップの基本操作も動かして、実務で使う書き方に慣れます。
1.3 事前準備
必要なツール
この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。
| ツール名 | 関連箇所 | 理由 |
|---|---|---|
| Visual Studio Code | Visual Studio Codeのインストール | Goのコードを記述するエディタとして使用する |
| Go | Goのインストール | 本章のサンプルコードを実行する言語処理系として使用する |
2. 配列
複数のデータをまとめて処理したい場面は多くあります。たとえば100人分のスコアを扱うために変数を100個宣言していては、コードが冗長になるうえに、追加や削除も扱いづらくなります。こうした場面のために、同じ型のデータを1つの名前でまとめて扱う仕組みが用意されています。
Goには、この仕組みとして配列とスライスの2種類があります。長さが固定か可変かの違いがあり、実際にはほぼスライスを使いますが、配列を知っておかないとスライスの動きが理解しにくくなるため、まずは配列から見ていきます。
2.1 配列の宣言
配列は、宣言時に長さを指定し、以降は長さが変わらないデータ構造です。基本の書き方は以下のとおりです。
var 変数名 [長さ]型
宣言した配列の要素は、変数名[インデックス] の形で個別に読み書きします。
実際に試してみましょう。main.go を以下の内容に書き換えて保存し、go run main.go で実行します。これは、int 型を3つ持つ配列を宣言し、それぞれに値を代入して表示するコードです。
package main
import "fmt"
func main() {
var nums [3]int
nums[0] = 10
nums[1] = 20
nums[2] = 30
fmt.Println(nums)
fmt.Println(len(nums))
}
以下のような実行結果が表示されます。
[10 20 30]
3
3つの要素が入った配列と、その要素数 3 が出力されました。
コードを解説します。
var nums [3]int
[3]int は「int 型を3つ持つ配列」の型です。この時点では要素はすべてゼロ値(int の場合は 0)で初期化されます。
nums[0] = 10
nums[1] = 20
nums[2] = 30
インデックス(0 から始まる)を使って各要素に値を代入しています。配列や後述のスライスは、配列名[インデックス] の形で個別要素を読み書きできます。
fmt.Println(len(nums))
len(配列) で要素数を取得できます。配列は長さも型の一部として扱われるため、[3]int と [4]int は別の型として区別されます。
2.2 宣言と同時に初期化
配列は、宣言と同時に初期値を並べて初期化することもできます。書き方は以下のとおりです。
変数名 := [長さ]型{要素1, 要素2, ...}
たとえば、int 型3つの配列を 10, 20, 30 で初期化するには以下のように書きます。
nums := [3]int{10, 20, 30}
要素数を ... で書くと、初期化子({...} の中に並べた初期値の並び)の数から自動で決まります。次のコードは、初期化子の数から要素数を推定して配列を作る例です。
nums := [...]int{10, 20, 30, 40}
fmt.Println(len(nums))
以下のような実行結果が表示されます。
4
4つの値を渡したため、len は 4 を返しました。
コードを解説します。
nums := [...]int{10, 20, 30, 40}
[...]int{...} の ... は「初期化子の数だけ要素を持つ配列にする」という指定です。ここでは4つの値を渡しているので、自動的に [4]int の配列になります。
2.3 スライシング
配列から、一部を切り出すことができます。書き方は 変数名[start:end] で、start のインデックスから始まって end の直前までの要素が取り出されます(end のインデックスの要素は含まれません)。次のコードは、要素5つの配列から3つの異なる範囲を切り出して表示する例です。
package main
import "fmt"
func main() {
nums := [5]int{10, 20, 30, 40, 50}
fmt.Println(nums[1:4])
fmt.Println(nums[:3])
fmt.Println(nums[2:])
}
以下のような実行結果が表示されます。
[20 30 40]
[10 20 30]
[30 40 50]
それぞれ、インデックス 1〜3、先頭〜インデックス 2、インデックス 2〜末尾 の範囲が切り出されました。
コードを解説します。
fmt.Println(nums[1:4])
nums[1:4] は「インデックス 1 から 4 の直前まで」を切り出します。4 は含まれない点に注意してください。結果として [20 30 40](インデックス 1、2、3 の要素)が取り出されます。
fmt.Println(nums[:3])
start を省略すると先頭から始まります。nums[:3] は「先頭からインデックス 3 の直前まで」なので [10 20 30] になります。
fmt.Println(nums[2:])
end を省略すると末尾までを表します。nums[2:] は「インデックス 2 から末尾まで」なので [30 40 50] になります。
3. スライス
スライスは、配列とよく似た型です。宣言や要素の取り出しは配列と同じ書き方で行える一方、長さを自由に変えられる点が特徴です。
3.1 スライスの宣言
スライスは、配列と同じく var で宣言できます。ただし配列と違い、[] の中に長さは書きません。
var 変数名 []型
実際に試してみましょう。これは、int 型のスライス nums を宣言して、その状態と長さを表示するコードです。
package main
import "fmt"
func main() {
var nums []int
fmt.Println(nums)
fmt.Println(len(nums))
}
以下のような実行結果が表示されます。
[]
0
空のスライスと、その長さ 0 が出力されました。
コードを解説します。
var nums []int
[]int は「int 型のスライス」を表す型です。配列と違い、[] の中に長さを書かないのが特徴で、長さは可変です。
var で宣言しただけのスライスは、要素が何も入っていない空の状態で、長さも 0 です。値を入れるには、次項の初期化子や、後の節で扱う append / make を使います。
3.2 宣言と同時に初期化
スライスも配列と同じく、宣言と同時に要素を並べて初期化できます。書き方は以下のとおりです。
変数名 := []型{要素1, 要素2, ...}
実際に試してみましょう。これは、3つの文字列を要素に持つスライスを作って表示するコードです。
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana", "cherry"}
fmt.Println(fruits)
fmt.Println(len(fruits))
}
以下のような実行結果が表示されます。
[apple banana cherry]
3
3つの文字列を持つスライスと、その要素数 3 が出力されました。
コードを解説します。
fruits := []string{"apple", "banana", "cherry"}
[]string は「文字列のスライス」を表す型です。配列と違い、[] の中に長さが入らないのがポイントです。長さが可変なので型に含まれません。初期化子の {"apple", "banana", "cherry"} によって、要素数3のスライスとして作られます。
3.3 appendによる要素の追加
スライスの末尾に要素を追加するには append 関数を使います。書き方は以下のとおりです。
変数名 = append(変数名, 追加する値, ...)
戻り値を元の変数に代入し直す点に注意します。次のコードは、要素3つのスライスに、1つずつ・複数個ずつと追加していく例です。
package main
import "fmt"
func main() {
nums := []int{1, 2, 3}
nums = append(nums, 4)
nums = append(nums, 5, 6)
fmt.Println(nums)
}
以下のような実行結果が表示されます。
[1 2 3 4 5 6]
元の [1 2 3] に 4、続けて 5 と 6 が追加され、[1 2 3 4 5 6] になりました。
コードを解説します。
nums = append(nums, 4)
append(スライス, 追加する値) の形で、既存のスライスの末尾に値を追加した新しいスライスを返します。返された結果を元の nums に代入し直すことで、nums が更新されます。
nums = append(nums, 5, 6)
第2引数以降にカンマ区切りで複数の値を渡すと、まとめて追加できます。ここでは 5 と 6 の2つを一度に追加しています。
append は必ず戻り値を代入し直す必要があります。append(nums, 4) だけを書いて代入を忘れると、追加した結果が nums に反映されません。
3.4 スライシング
配列で扱ったスライシング(変数名[start:end])は、スライスでも同じ書き方で使えます。
nums := []int{10, 20, 30, 40, 50}
fmt.Println(nums[1:4]) // [20 30 40]
配列でもスライスでも、スライシングで切り出した結果はスライスとして得られます。
| 📝 スライスと内部配列の共有 |
|---|
スライシングで取り出したスライスは、元の配列・スライスと同じ内部配列を参照しています。片方の要素を書き換えるともう片方にも影響することがあります。独立させたい場合は copy 関数で新しいスライスに複製します。 |
3.5 makeによる長さ・容量の指定
make 関数を使うと、長さと容量を明示してスライスを作れます。書き方は以下のとおりです。
変数名 := make([]型, 長さ)
次のコードは、要素数3のスライスを make で作り、ゼロ値で初期化された状態を表示する例です。
nums := make([]int, 3)
fmt.Println(nums)
fmt.Println(len(nums))
以下のような実行結果が表示されます。
[0 0 0]
3
int のゼロ値である 0 が3つ入ったスライスが作られました。
コードを解説します。
nums := make([]int, 3)
make([]int, 3) は「int 型3つ分の場所を確保し、ゼロ値で初期化する」という指定です。int のゼロ値は 0 なので、[0 0 0] のスライスが作られます。
4. for-range
前章では文字列を対象に for-range を扱いました。この書き方は、配列やスライスの反復にも同じ形で使えます。書き方は以下のとおりです。
for インデックス, 要素 := range 変数名 {
// 各要素に対する処理
}
ここではスライスに対する使い方を見ていきます。次のコードは、3つの文字列を持つスライス fruits の各要素を、インデックスと値のペアで順に表示するものです。
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana", "cherry"}
for index, value := range fruits {
fmt.Printf("[%d] %s\n", index, value)
}
}
以下のような実行結果が表示されます。
[0] apple
[1] banana
[2] cherry
fruits の3つの要素が、それぞれインデックス付きで出力されました。
コードを解説します。
for index, value := range fruits {
fmt.Printf("[%d] %s\n", index, value)
}
range fruits で fruits の要素を先頭から順に取り出します。取り出すたびに、左辺の index にインデックス(0, 1, 2)、value に要素("apple", "banana", "cherry")が入り、続く fmt.Printf で表示されます。
配列でも同じ書き方で、for index, value := range 配列名 の形で反復できます。
インデックスが不要な場合は、アンダースコア _ で受けます。次は同じスライスを、インデックスを無視して値だけ表示する例です。
for _, value := range fruits {
fmt.Println(value)
}
コードを解説します。
for _, value := range fruits {
_ はGoで「使わない値を捨てる」ときに使う変数名で、ブランク識別子と呼ばれます。受け取っておきながら参照しないと Go はコンパイルエラーにしてしまうため、_ で明示的に無視します。ここではインデックスを使わないので、_ で受けて value だけ利用しています。
5. マップ
マップは、キーと値のペアを保持するデータ構造です。キーを指定するだけで対応する値を素早く取り出せるため、スライスをループで探すよりも効率的に検索できます。他言語では「辞書型」や「連想配列」と呼ばれるものに相当します。
5.1 マップの初期化
マップは、キーと値のペアを並べて初期化できます。書き方は以下のとおりです。
変数名 := map[キー型]値型{
キー1: 値1,
キー2: 値2,
...
}
次のコードは、名前と得点のペアを持つマップを初期化して、特定の名前で値を取り出す例です。
package main
import "fmt"
func main() {
scores := map[string]int{
"Taro": 85,
"Jiro": 72,
"Hanako": 90,
}
fmt.Println(scores["Taro"])
fmt.Println(scores["Hanako"])
}
以下のような実行結果が表示されます。
85
90
"Taro" と "Hanako" のキーに対応する得点がそれぞれ出力されました。
コードを解説します。
scores := map[string]int{
"Taro": 85,
"Jiro": 72,
"Hanako": 90,
}
map[string]int は「string 型のキーと int 型の値を持つマップ」を意味します。{"Taro": 85, ...} の形でキーと値のペアを初期化子に書きます。
fmt.Println(scores["Taro"])
マップ名[キー] の形で、キーに対応する値を取得できます。scores["Taro"] は 85、scores["Hanako"] は 90 を返します。
5.2 makeによる空マップの作成
空のマップを作りたい場合は make を使います。書き方は以下のとおりです。
変数名 := make(map[キー型]値型)
作った後は 変数名[キー] = 値 の形でキーと値のペアを追加できます。次のコードは、空のマップを作ってから2件追加して表示するコードです。
package main
import "fmt"
func main() {
scores := make(map[string]int)
scores["Taro"] = 85
scores["Jiro"] = 72
fmt.Println(scores)
}
以下のような実行結果が表示されます。
map[Jiro:72 Taro:85]
作った空のマップに2件追加した結果が表示されました。
コードを解説します。
scores := make(map[string]int)
make(map[string]int) で、空のマップを作ります。初期化子で値を渡さずに、あとから追加したい場合に使います。
scores["Taro"] = 85
scores["Jiro"] = 72
マップ名[キー] = 値 の形で、キーと値のペアを追加します。
5.3 存在チェック
存在しないキーで取得すると、値の型のゼロ値が返ります。単なる取得と、キーが存在するかの確認を区別するには、2つ目の戻り値(ok)を受け取ります。書き方は以下のとおりです。
値, ok := 変数名[キー]
ok は真偽値で、キーが存在すれば true、しなければ false になります。次のコードは、存在しないキーと存在するキーの両方を2値受け取りで扱う例です。
package main
import "fmt"
func main() {
scores := map[string]int{"Taro": 85}
v, ok := scores["Jiro"]
fmt.Println(v, ok)
v2, ok2 := scores["Taro"]
fmt.Println(v2, ok2)
}
以下のような実行結果が表示されます。
0 false
85 true
"Jiro" は登録されていないので 0(ゼロ値)と false が返り、"Taro" は登録されているので 85 と true が返りました。
コードを解説します。
v, ok := scores["Jiro"]
マップから値を取り出すとき、v, ok := マップ名[キー] の形で2つの戻り値を受け取れます。v に値、ok に「キーが存在したかどうか」の真偽値が入ります。
ok が true のときのみ「キーが存在する」ことを示します。ゼロ値と「存在しない」を区別する慣用パターンとして頻出です。
5.4 削除
delete 関数でキーを削除します。書き方は以下のとおりです。
delete(変数名, キー)
次のコードは、マップから "Taro" を削除して結果を表示する例です(前のコードの続き、func main() 内に書いて実行します)。
delete(scores, "Taro")
fmt.Println(scores)
以下のような実行結果が表示されます。
map[Jiro:72]
"Taro" の要素が消え、"Jiro" のみが残ったマップが出力されました。
コードを解説します。
delete(scores, "Taro")
delete(マップ, キー) の形で、指定したキーとその値をマップから削除します。
5.5 for-rangeでの反復
前のセクションで扱った for-range は、マップにも同じ形で使えます。書き方は以下のとおりです。
for キー, 値 := range 変数名 {
// 各要素に対する処理
}
次のコードは、3人分の得点マップを range で走査し、名前と点数のペアを表示するコードです。
package main
import "fmt"
func main() {
scores := map[string]int{
"Taro": 85,
"Jiro": 72,
"Hanako": 90,
}
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
}
以下のような実行結果が表示されます(順序は実行ごとに変わります)。
Hanako: 90
Taro: 85
Jiro: 72
3人分のペアが取り出されて表示されました。マップの反復順序は保証されないため、表示順は実行のたびに変わります。
コードを解説します。
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
マップに range を使うと、左辺の name にキー、score に値が入り、要素の数だけループします。
| 📝 マップの反復順序について |
|---|
Goのマップは、range で反復するたびに順序が変わることがあります。特定の順序で処理したい場合は、キーをスライスに集めて sort パッケージでソートしてから使います。 |
6. まとめ
この章では、Goのデータ構造を学びつつ、実際に配列・スライス・マップの扱いを体験しました。
- 配列は長さが型の一部であり、実際にはほぼスライスを使う
- スライスは可変長で、
appendで末尾に要素を追加できる(結果を代入し直す必要がある) - スライシング
[start:end]は元の内部配列を共有するため、独立させたいときはcopyを使う - マップは
map[キー型]値型で宣言し、存在チェックはv, ok := m[key]の形で行う for-rangeを使うと、スライス・マップ・文字列の要素を順に取り出せる
次の章では、Goの関数と型定義を学びつつ、実際に関数定義・interfaceの扱いを体験します。