Arrays, Slices, Maps, Structs, make() - Golang learning step 1-4

公開
注意最終更新から 2 年以上経過しています。最新の情報もあわせてご確認ください。

roadmap.sh > Go > Learn the Basics > Arrays, Slices, Maps, Structs, make() の学習を進めていきます。

学習箇所

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

配列(Array)

固定長の要素を格納するデータ型。すべての要素は同じ型でなければなりません。

  • 配列を別の変数に割り当てると、配列全体がコピーされる。
  • 関数に引数として配列を渡すと、アドレスだけを渡すのではなく、配列全体のコピーが作成される。
  • 配列の長さは型の一部であり、異なる長さの配列は異なる型として扱われる。
  • 配列の要素には、インデックスを使用してアクセスできる。インデックスはゼロから始まる。
    • 範囲外のインデックスにアクセスするとコンパイルエラー、またはランタイムパニックが発生
  • 要素数が固定されているため、要素数が動的に変化する場合にはスライスの使用が推奨される。
  • 配列の初期化にはリテラルを使うことができる(例: arr := [3]int{1, 2, 3})。
  • len関数を使って配列の長さを取得できる。
package main

import "fmt"

func main() {
    // var を使った配列の宣言(ゼロ値で初期化)
    var arr1 [3]int
    fmt.Println("var で宣言した配列 (初期状態):", arr1)

    // 要素を代入
    arr1[0] = 10
    arr1[1] = 20
    arr1[2] = 30
    fmt.Println("var で宣言した配列 (代入後):", arr1)

    // 配列の宣言と初期化
    arr := [3]int{1, 2, 3}
    fmt.Println("配列:", arr)

    // 配列の長さを取得
    fmt.Println("配列の長さ:", len(arr))

    // インデックスを使って要素にアクセス
    fmt.Println("インデックス 0 の要素:", arr[0])

    // 配列のコピー
    arrCopy := arr
    arrCopy[0] = 100
    fmt.Println("元の配列:", arr) // コピーされるため、元の配列は変更されない
    fmt.Println("コピーされた配列:", arrCopy)

    // 配列を関数に渡す
    modifyArray(arr)
    fmt.Println("関数呼び出し後の元の配列:", arr) // コピーされるため、元の配列は変更されない
}

// 配列を受け取る関数(配列全体がコピーされる)
func modifyArray(a [3]int) {
    a[0] = 999
    fmt.Println("関数内での配列:", a)
}

出力結果:

var で宣言した配列 (初期状態): [0 0 0]
var で宣言した配列 (代入後): [10 20 30]
配列: [1 2 3]
配列の長さ: 3
インデックス 0 の要素: 1
元の配列: [1 2 3]
コピーされた配列: [100 2 3]
関数内での配列: [999 2 3]
関数呼び出し後の元の配列: [1 2 3]

すべての要素は同じ型である必要があります。

func main() {
	// すべての要素は同じ型でなければならない
	arr := [3]int{1, "2"}
	// => cannot use "2" (untyped string constant) as int value in array or slice literal
}

範囲外のインデックスにアクセスするとエラーとなります。

func main() {
	// -- コンパイルエラーの例 --
	arr := [3]int{1, 2, 3}
	// 範囲外のインデックスにアクセスするとコンパイルエラーが発生
	fmt.Println("範囲外のインデックス 5 の要素:", arr[5])
	// => invalid argument: index 5 out of bounds [0:3]
	
	// -- ランタイムパニックの例 --
	var arr [3]int
	index := 3 // 動的にインデックスを指定
	fmt.Println("動的インデックス:", index)

	// 動的なインデックスによりランタイムパニックが発生
	fmt.Println(arr[index])
	// => panic: runtime error: index out of range [3] with length 3
}

スライス(Slice)

可変長の配列のようなデータ型。Go では非常に頻繁に使用される。

slice 型は、可変長のシーケンス(要素の数が固定されていないデータの並び)であり、配列のように要素を格納しますが、配列と異なり、サイズを動的に変更できるという利点があります。また、スライスは、配列の一部または全体を参照するデータ型で、スライス自体は配列の要素を保持していません。

スライスの構造

スライスは内部的に以下の3つの情報を持っています

  • ポインタ:スライスが参照する配列の最初の要素へのポインタ
  • 長さ(len):スライスの要素数
  • 容量(cap):スライスが参照する配列の要素数

スライスの作成例

  1. リテラルを使ったスライスの宣言
  2. 配列からスライスを作成
  3. make 関数を使ったスライスの宣言
    • make([]TYPE, length, capacity)
    • 配列の型、長さ、容量を指定してスライスを作成できる。長さと容量の指定はオプション。長さが指定されていて容量が指定されていない場合、容量は長さと同じになる。
  4. append 関数を使った要素の追加
  5. copy 関数を使ってスライスをコピー
package main

import "fmt"

func main() {
	// --- リテラルを使ったスライスの宣言 --- //
	s := []int{1, 2, 3, 4, 5} // 配列を作らずに直接スライスを宣言
	fmt.Println(s)            // [1 2 3 4 5]

	// --- 配列からスライスを作成 --- //
	arr := [5]int{1, 2, 3, 4, 5}
	s1 := arr[1:4]  // 配列の一部をスライスとして取り出す
	fmt.Println(s1) // [2 3 4]

	// -- make 関数を使ったスライスの宣言 -- //
	// make([]TYPE, length, capacity)
	s2 := make([]int, 5) // 長さ 5, 要素が全て 0 のスライスを作成
	fmt.Println(s2)      // [0 0 0 0 0]

	// --- append 関数を使った要素の追加 --- //
	s3 := []int{1, 2, 3}
	s3 = append(s3, 4, 5) // スライスに新しい要素を追加
	fmt.Println(s3)       // [1 2 3 4 5]

	// -- copy 関数を使ってスライスをコピー -- //
	src := []int{1, 2, 3}
	dst := make([]int, len(src))
	copy(dst, src)   // srcの内容をdstにコピー
	fmt.Println(dst) // [1 2 3]
}

スライスと配列の違い

特徴 配列 スライス
サイズ 固定 可変
作成方法 [n]T{} []T{} または make([]T, len, cap)
長さの変更 不可 可能(appendなどで追加)
参照かコピーか 値としてコピー 配列への参照

スライスの利点

  • メモリ効率:スライスは大きな配列を直接コピーせずに一部を参照するため、メモリを効率よく使える。
  • 柔軟性:スライスは容量を動的に増減できるため、可変長のデータに適している。

スライスの注意点

  • スライスの長さが容量を超えると、新しい配列が作成され、古いスライスの要素が新しい配列にコピーされる。このため、スライスを大きくするたびにパフォーマンスに影響を与える場合がある。
    • 小さなスライスであればコピーのコストは目立たないが、非常に大きなスライスでは、コピーする要素数が多くなるため、パフォーマンスに影響が出ることがある。
    • 新しい配列が作成されるたびに、メモリの再確保が発生するため、メモリの断片化や不必要なメモリ移動が起こる可能性があり、メモリの管理に負荷がかかる。
    • 普段はスライスの拡張によって影響を受けないコードでも、特定のサイズや操作で突然パフォーマンスが低下することがあるため、これが予測しにくい点で問題となることがある。
    • スライスを多くの要素で拡張することが予想される場合、あらかじめ十分な容量を確保し、容量が超えた際に発生する再確保やコピーの頻度を減らすことがパフォーマンス改善の一つの方法。
  • スライスは配列の参照を保持しているため、スライス内の要素を変更すると、それが元の配列にも影響を与えることがある。
package main

import "fmt"

func main() {
    // スライスの拡張に関する例
    s := make([]int, 3, 5) // 長さ3、容量5のスライスを作成
    fmt.Println("初期スライス:", s)         // [0 0 0]
    fmt.Println("長さ:", len(s))             // 3
    fmt.Println("容量:", cap(s))             // 5

    // 容量内で要素を追加する
    s = append(s, 1, 2)                     // 容量を超えない範囲での追加
    fmt.Println("容量内での追加後のスライス:", s)  // [0 0 0 1 2]
    fmt.Println("長さ:", len(s))             // 5
    fmt.Println("容量:", cap(s))             // 5

    // 容量を超える要素を追加する
    s = append(s, 3)                        // 容量を超えると新しい配列が確保される
    fmt.Println("容量を超えた追加後のスライス:", s)  // [0 0 0 1 2 3]
    fmt.Println("長さ:", len(s))             // 6
    fmt.Println("容量:", cap(s))             // 10(新しい配列が確保され、容量が拡大)

    // スライスは参照型に関する例
    arr := [5]int{1, 2, 3, 4, 5}             // 配列を作成
    s1 := arr[1:4]                           // 配列の部分をスライスとして取り出す
    fmt.Println("元の配列:", arr)             // [1 2 3 4 5]
    fmt.Println("スライス s1:", s1)          // [2 3 4]

    s1[0] = 100                              // スライスの要素を変更
    fmt.Println("スライス s1 の変更後:", s1)  // [100 3 4]
    fmt.Println("変更後の元の配列:", arr)     // [1 100 3 4 5] (元の配列にも影響)
}

マップ(Map)

map 型は、キーと値のペアを格納するコレクションです。他の言語ではハッシュテーブル、連想配列、辞書などと呼ばれることもあります。

  • 各要素はキーと値のペアで構成される。
  • 要素の追加や削除が可能で、サイズは動的に変更される。
  • キーと値には、ほとんどの型を使用できる(ただし、キーは比較可能な型である必要がある)。

map の宣言と初期化

map 型は次のように定義されます。

map[キーの型]値の型

map を作成するためには、make 関数を使用するか、リテラルを使って初期化することができます。

package main

import "fmt"

func main() {
	// map[キーの型]値の型 の形式で宣言します
	var m map[string]int

	// または、make関数を使用して初期化します
	m = make(map[string]int)

	// マップリテラルを使用して宣言と初期化を同時に行うこともできます
	m1 := map[string]int{
		"apple":  1,
		"banana": 2,
	}
}

map のゼロ値

map のゼロ値は nil です。nil の map に対して要素の追加やアクセスを行うと、ランタイムパニックが発生します。そのため、map を使用する前に必ず make で初期化するか、リテラルで定義する必要があります。

package main

import "fmt"

func main() {
	var m map[string]int
	
	if m == nil {
	  fmt.Println("m is nil")
	} else {
	  fmt.Println("m is not nil")
	}
	
	// => m is nil

	var m1 = make(map[string]int)

	if m1 == nil {
	  fmt.Println("m1 is nil")
	} else {
	  fmt.Println("m1 is not nil")
	}
	
	// => m1 is nil
}
package main

import "fmt"

func main() {
	var m map[string]int
	m["key"] = 1 // => panic: assignment to entry in nil map

	var m1 = make(map[string]int)
	m1["key"] = 1

	var m2 = map[string]int{
		"key": 1,
	}

	fmt.Println(m1, m2)
	// => map[key:1] map[key:1]
}

map の操作

map に新しいキーを追加する場合や既存のキーに関連付けられた値を更新する場合、次のように記述します。

map[key] = value

キーを指定して対応する値を取得します。指定したキーが存在しない場合は、値の型に応じたゼロ値が返されます。

value := map[key]

delete 関数を使って、特定のキーに関連する要素を削除することができます。

delete(map, key)

キーが map に存在するかを確認するためには、次のように 2 つ目の戻り値を利用します。

value, exists := map[key]

exists が true の場合、キーは map に存在し、value に対応する値が格納されています。false の場合は、キーが存在しません。

package main

import "fmt"

func main() {
	ages := make(map[string]int)

	// キーと値の追加
	ages["Alice"] = 25
	ages["Bob"] = 30

	fmt.Println(ages)
	// => map[Alice:25 Bob:30]

	// 値の取得
	aliceAge := ages["Alice"]
	fmt.Println(aliceAge)

	// 値の更新
	ages["Alice"] = 26

	// 値の削除
	delete(ages, "Bob")

	fmt.Println(ages)
	// => map[Alice:26]

	// キーが存在するかを確認する
	age, exists := ages["Alice"]
	if exists {
		fmt.Println("Aliceの年齢:", age)
	} else {
		fmt.Println("Aliceは存在しません")
	}
}

map のループ

map の全要素を反復処理するには、range を使ったループが便利です。

package main

import "fmt"

func main() {
	ages := map[string]int{
		"Alice": 25,
		"Bob":   30,
	}

	for key, value := range ages {
		fmt.Println(key, value)
		// => Bob 30
		// => Alice 25
	}
}

map は参照型のデータ構造

Go において、map 変数はマップデータ構造への参照を保持します。2 つのマップ変数が同じマップを参照している場合、一方の変数の内容を変更すると、もう一方の変数の内容にも影響します。

次の例では、a と b が同じ map を参照しているため、a に変更を加えると b の内容にも影響が出ます。

package main

import "fmt"

func main() {
	a := map[string]int{
		"Alice": 25,
		"Bob":   30,
	}

	b := a

	a["Alice"] = 26

	fmt.Println(a, b)
	// => map[Alice:26 Bob:30] map[Alice:26 Bob:30]
}

構造体(Struct)

構造体(struct)は、複数のフィールドを持つカスタムデータ型を定義するための基本的なデータ構造です。構造体を使用すると、異なる型のデータを 1 つのまとまりとして扱うことができます。たとえば、名前、年齢、住所といった複数の関連するデータを一緒に格納したい場合に便利です。

Goの構造体の特徴
  • 構造体は複数のフィールドを持つことができ、各フィールドは異なる型にすることが可能。
  • 構造体を使うことで、自分で定義した型を作成し、その型を通じてデータを効率的に扱うことができる。
  • 構造体はデフォルトでは値型であり、コピーされます。ただし、ポインタを使って参照渡しにすることもできる。

構造体の宣言と使用方法

構造体の定義

構造体は、struct キーワードを使用して定義します。次のように、フィールド名と型を組み合わせて定義します。

type Person struct {
    Name  string
    Age   int
    Email string
}

ここでは、Person という構造体を定義しています。この構造体には Name、Age、Email というフィールドがあり、それぞれ string 型や int 型を持ちます。

構造体のインスタンス化とフィールドへのアクセス、変更

定義した構造体を使ってインスタンスを作成し、フィールドにデータを格納します。省略されたフィールドはゼロ値になります。

構造体のゼロ値

Go言語では、変数を宣言したときにその変数に明示的に値を割り当てない場合、その変数はその型のゼロ値で初期化されます。構造体も例外ではありません。

構造体のゼロ値は、すべてのフィールドがそれぞれの型のゼロ値で初期化された状態です。

  • 数値型(int, float, etc.): 0
  • ブール型: false
  • 文字列型: "" (空文字列)
  • ポインタ型: nil
  • スライス、マップ、チャンネル: nil

以下の例で、Name 以外を渡さずに構造体 Person を初期化したとき、Age フィールドは 0、Email フィールドは空文字列、Active フィールドは false としてゼロ値で初期化されていることがわかります。

package main

import "fmt"

type Person struct {
	Name   string
	Age    int
	Email  string
	Active bool
}

func main() {
	// Name 以外のフィールドは初期化されない
	p := Person{
		Name: "Alice",
	}

	// フィールドのゼロ値が自動的に適用される
	fmt.Printf("%#v\n", p)
	// => main.Person{Name:"Alice", Age:0, Email:"", Active:false}
}
フィールドへのアクセスと変更

構造体のフィールドにアクセスするには、ドット(.)を使います。

構造体のフィールドは通常の変数と同じように変更可能です。

package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

func main() {
	// --- 構造体のインスタンス化 --- //
	p := Person{
		Name:  "Alice",
		Age:   30,
		Email: "alice@example.com",
	}
	fmt.Println(p) // {Alice 30 alice@example.com}

	// --- フィールドへのアクセス --- //
	fmt.Println(p.Name) // Alice
	fmt.Println(p.Age)  // 30

	// --- 構造体のフィールドの変更 --- //
	p.Age = 31
	fmt.Println(p.Age) // 31
}
ポインタを使った構造体の操作

構造体は値型なので、関数に渡すとコピーが作られますが、ポインタを使うことで参照渡しが可能です。

package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

func main() {
	p := Person{
		Name:  "Alice",
		Age:   30,
		Email: "alice@example.com",
	}

	updateEmail(&p, "new_email@example.com")
	fmt.Println(p.Email) // new_email@example.com
}

func updateEmail(p *Person, newEmail string) {
	p.Email = newEmail
}

構造体の組み込みメソッド

Go では、構造体にメソッドを定義することもできます。メソッドは構造体に関連する関数のようなもので、構造体の特定の操作をカプセル化できます。

package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

func (p Person) Greet() string {
	return "Hello, my name is " + p.Name
}

func main() {
	p := Person{
		Name:  "Alice",
		Age:   30,
		Email: "alice@example.com",
	}

	fmt.Println(p.Greet()) // Hello, my name is Alice
}

コンストラクタ関数

Go 言語では、構造体自体にコンストラクタがないため、構造体生成のためにコンストラクタ関数を定義して利用することが慣用的(一般的)です。

  • コンストラクタ関数により、初期化処理をカプセル化でき、誤って不完全な初期化がされるのを防げます。例えば、フィールドのデフォルト値を設定したり、パラメータに対してバリデーションを行ったりできます。
  • 構造体のポインタを返すことで、呼び出し側が構造体のコピーではなく、同じインスタンスを参照できるようになります。これは、メモリ効率やパフォーマンスの面でメリットがあります。
  • 同じコンストラクタ関数を使うことで、構造体の生成方法が一貫し、コードがより読みやすく、保守しやすくなります。
package main

import (
	"errors"
	"fmt"
)

type Person struct {
	Name  string
	Age   int
	Email string
}

func newPerson(name string, age int, email string) (*Person, error) {
	if name == "" {
		return nil, errors.New("name cannot be empty")
	}
	if age < 0 {
		return nil, errors.New("age cannot be negative")
	}
	if email == "" {
		return nil, errors.New("email cannot be empty")
	}

	return &Person{
		Name:  name,
		Age:   age,
		Email: email,
	}, nil
}

func main() {
	p, err := newPerson("John Doe", 18, "johndoe@example.com")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Name: %s, Age: %d, Email: %s\n", p.Name, p.Age, p.Email)
	// => Name: John Doe, Age: 18, Email: johndoe@example.com
}

構造体の組み込み(埋め込み)

Goの構造体は、他の構造体を埋め込むことができ、埋め込まれたフィールドやメソッドに直接アクセスできます。これにより、構造体の簡単な継承のような機能が実現できます。

package main

import "fmt"

type Person struct {
	Name  string
	Age   int
	Email string
}

type Address struct {
	City  string
	State string
}

type Employee struct {
	Person
	Position string
	Address
}

func main() {
	e := Employee{
		Person: Person{
			Name:  "Bob",
			Age:   40,
			Email: "bob@example.com",
		},
		Position: "Manager",
		Address: Address{
			City:  "New York",
			State: "NY",
		},
	}

	fmt.Println(e.Name) // Bob(Person構造体のフィールドに直接アクセス可能)
	fmt.Println(e.City) // New York(Address構造体のフィールドに直接アクセス可能)
}

匿名構造体

匿名構造体(Anonymous Structs)は、その場で定義され、通常は一度だけ使用される構造体です。匿名構造体は、通常の構造体のようにフィールドを持ちますが、型に名前がありません。そのため、名前を付けずにその場で即座に定義し、利用することができます。この特徴により、特定の一時的な目的のために使われるデータ構造に適しています。

dog := struct {
    name   string
    isGood bool
}{
    "Rex",
    true,
}

テーブル駆動型テストでの利用

Go でよく使われる「テーブル駆動型テスト」では、テストケースごとに異なるデータを持たせるため、匿名構造体がよく使われます。テストの各ケースで異なる入力と期待される出力を持つ場合、それを匿名構造体で定義し、配列やスライスに格納してテストを行います。

例えば、次のような形で、入力と期待される結果を匿名構造体にまとめてテストを行います。

add.go

package main

func add(a int, b int) int {
	return a + b
}

add_test.go

package main

import "testing"

func TestAdd(t *testing.T) {
	tests := []struct {
		name     string // テスト名
		a, b     int    // テスト時に使用する値
		expected int    // 期待する結果
	}{
		{"positive numbers", 2, 3, 5},
		{"negative numbers", -1, -2, -3},
		{"mixed numbers", -1, 5, 4},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := add(tt.a, tt.b)
			if result != tt.expected {
				t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
			}
		})
	}
}
go test add_test.go add.go
# => ok   0.271s

構造体まとめ

  • 構造体(Struct)は、Goにおけるカスタムデータ型で、複数のフィールドを持てる。
  • 構造体は複数の異なる型のフィールドを保持し、それらをまとめて扱うことができる。
  • メソッドを定義して、構造体に特定の機能を持たせることが可能。
  • Goの構造体は値型で、ポインタを使って参照渡しができる。
  • 構造体の生成には、コンストラクタ関数を定義するのが慣用的。
  • 構造体の埋め込みによって、簡単な継承のような振る舞いを実現できる。
  • 匿名構造体によって型名の無い構造体を定義できる。

makeを使った割り当て

組み込み関数 make は、スライス、マップ、チャネルを作成し、初期化された(ゼロ化されていない)T型の値を返します。

これら 3 つの型に make が用意されている理由は、スライス、マップ、チャネルは内部的にはデータ構造への参照を表しており、使用前に適切に初期化する必要があるためです。

例えばスライスは、データへのポインタ(配列内部)、長さ、容量の 3 つの要素を含む記述子であり、これらの要素が初期化されるまでスライスは nil です。make は、スライス、マップ、チャネルに対して内部データ構造を初期化し、使用できるように値を準備します。これにより、これらの型を正しく、安全に使用できるようになります。

なお、make で生成された各要素の値は、初期状態でゼロ値になります。

make 関数の使用例

make 関数は、スライスを作成する際に以下の引数を取ります。

  1. 第一引数:作成するスライスの型(例:[]int)
  2. 第二引数:初期の長さ(length)。これは初期状態で利用可能な要素数を表します。
  3. 第三引数(省略可能):初期の容量(capacity)。これは将来的にスライスを拡張できる余地を考慮した、背後の配列の総要素数を表します。省略した場合、容量は長さと同じ値に設定されます。
package main

import "fmt"

func main() {
	// --- スライスの生成 --- //
	s := make([]int, 3, 10)

	fmt.Println(s)
	// => [0 0 0]

	// --- マップの生成 --- //
	m := make(map[string]int, 5)

	fmt.Println(m)
	// => map[]
	fmt.Println(len(m))
	// => 0

	// --- チャネルの生成 --- //
	ch := make(chan int, 3)

	fmt.Println(ch)
	// => 0xc000120000 (メモリアドレス。実行ごとに異なる)
	fmt.Println(cap(ch))
	// => 3

	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	// => 1
}
  • スライス: 10 個の int 型要素を持つ配列を割り当て、その後、最初の 3 要素を指す長さ 3、容量 10 のスライス構造を作成します。(容量は省略可能。詳細はスライスのセクションを参照)
  • マップ: string 型のキーと int 型の値を持つマップを生成します。第二引数の5は初期のサイズヒントで、パフォーマンス最適化のために使用されます。必須ではありません。生成直後のマップは空ですが、要素を追加できる状態です。
  • チャネル: int 型の値を扱うバッファ付きチャネルを生成します。第二引数の 3 はバッファのサイズを指定します。省略するとバッファなしのチャネルになります。チャネルの容量(バッファサイズ)は cap() 関数で確認できます。
Ritolabo