👁

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つの値を渡したため、len4 を返しました。

コードを解説します。

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、続けて 56 が追加され、[1 2 3 4 5 6] になりました。

コードを解説します。

nums = append(nums, 4)

append(スライス, 追加する値) の形で、既存のスライスの末尾に値を追加した新しいスライスを返します。返された結果を元の nums に代入し直すことで、nums が更新されます。

nums = append(nums, 5, 6)

第2引数以降にカンマ区切りで複数の値を渡すと、まとめて追加できます。ここでは 56 の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 fruitsfruits の要素を先頭から順に取り出します。取り出すたびに、左辺の 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"]85scores["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" は登録されているので 85true が返りました。

コードを解説します。

v, ok := scores["Jiro"]

マップから値を取り出すとき、v, ok := マップ名[キー] の形で2つの戻り値を受け取れます。v に値、ok に「キーが存在したかどうか」の真偽値が入ります。

oktrue のときのみ「キーが存在する」ことを示します。ゼロ値と「存在しない」を区別する慣用パターンとして頻出です。

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の扱いを体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000