LoginSignup
4
1

More than 1 year has passed since last update.

Go言語入門 学習メモ 4 ポインタ、コンポジット型(配列、構造体、スライス、マップ)

Last updated at Posted at 2022-10-19

はじめに

船井総研デジタルのoswです。業務でGo言語を使うことになったのでこれから学習していきます。その備忘録です。参考になる方がいらっしゃれば幸いです。

対象読者

  • これからGo言語を学習する方
  • 既に他の言語で基本構文を学習されている方

学習環境

学習環境は次のようになっています。この環境の構築メモは下記記事にまとめてあります。ご興味がある方はご参照ください。

  • Windows 11 Home / 22H2
  • VSCode / 1.72.2
  • go version go1.19.2 windows/amd64
  • git version 2.38.0.windows.1

前回までの学習

前回は繰り返し、ラベルの働きなどを学習しました。

ポインタ

GoではC, C++のようにポインタを扱うことができるようです。ただし、ポインタの演算はできないとのこと。ポインタ自体を扱ったことがない方は次の記事を参考にするとわかりやすいかと思います。

ざっくり言えばポインタは変数で、中身を「メモリアドレス」として解釈します。中身をアドレスとして解釈した結果、メモリの該当アドレスに見える値をプログラムで参照、書き換えたりすることができます。

そのため、ポインタを使うには必ずアドレスを格納した上で参照する必要があります。未初期化のポインタは「nil」(どのアドレスも参照していない状態)が入るようです。C, C++で言うところのNULL、nullptrでしょうか。

アドレスを取得、参照する演算子は次の記事で学習しています。

構文
// "*"を付け、*int型でポインタを宣言。アドレスを格納
var ポインタ *int
ポインタ = &変数

// ポインタを変数のアドレスで初期化
var ポインタ *int = &変数

// := を使った初期化も可能
ポインタ := &変数
サンプルコード
package main

import "fmt"

func main() {

	num := 100
	// ポインタの初期化
	pNum := &num

	fmt.Println("アドレスの確認")
	fmt.Printf("numのメモリアドレス: %p\n", &num)
	fmt.Printf("%-12s: %p\n", "ポインタpNumの中身", pNum)

	fmt.Println("\n値の参照")
	fmt.Println(" numの値:", num)
	fmt.Println("pNumの値:", *pNum)

	fmt.Println("\npNum経由で値の上書き")
	*pNum = 1

	fmt.Println("\n値の参照")
	fmt.Println(" numの値:", num)
	fmt.Println("pNumの値:", *pNum)


	fmt.Println("\n未初期化時の挙動確認")
	var pError *int
	
	if pError == nil {
		fmt.Println("ポインタはnilです")
	} else {
		fmt.Println("ポインタはnilではありません")
	}

	// nilを参照しているのでランタイムエラー発生: panic: runtime error: invalid memory address or nil pointer dereference
	// fmt.Printf("%d\n", *pError)
}
実行結果
アドレスの確認
numのメモリアドレス : 0xc0000180b8
ポインタpNumの中身: 0xc0000180b8

値の参照
 numの値: 100
pNumの値: 100

pNum経由で値の上書き

値の参照
 numの値: 1
pNumの値: 1

未初期化時の挙動確認
ポインタはnilです

確認するとダブルポインタも使えるようです。ただ、今回は学習領域から外します。

ポインタの使いどころですが、構造体など大きなサイズのモノを関数などに値渡ししてしまうと重いコピー処理が発生し、オーバーヘッドも無視できなくなってくると思います。

アドレスを渡すだけ(参照渡し)にすれば重いコピーは発生しなくなり処理は速くなる、これがポインタの利点だと思います。ところが使い方によっては値渡しの方が速くなるケースもあるようで、Goでポインタの乱用は注意が必要なようです。

コンポジット型

Goでは複数のデータをまとめ、1つの型として扱うものを「コンポジット型」と呼び、下記が該当するようです。

  • 配列
  • 構造体
  • スライス
  • マップ

コンポジット型のゼロ値は下表のようになるようです。ゼロ値は未初期化時にコンパイラが自動的に挿入する値のようです。

ゼロ値
配列 全ての要素を0で初期化
構造体 全てのフィールドを0で初期化
スライス nil
マップ nil

配列

同じ型のデータをまとめる固定長のデータ構造です。長さも型の一部として保持されているようで、長さは変更できません。そのため、異なる長さの配列は別の型として認識されるようです。

構文
// 宣言
var array [要素数]

// 未初期化は全ての要素にゼロ値が入る
var array [要素数]

// 要素数に合わせて値を指定
var array [要素数]{値1, 値2, ...}

// 要素数を指定した値の数で推論
array := [...]{値1, 値2, ...}

// 複数行で初期化
array := [...]{
    値1,
    値2,
    ..., // 複数行で初期化する場合は、最後のフィールドにも","が必須
}

// 要素を指定して初期化する。指定されていない要素はゼロ値が入り、長さは指定した要素のインデックスから推論してくれる
// この例ではインデックス2 == 値1, インデックス5 == 値2 となる
array := [...]{2: 値1, 5: 値2}

配列の要素へは[インデックス]でアクセス可能です。インデックスは値でも式でも良いようです。また、配列の長さは組み込み関数のlen()で取得が可能です。

サンプルコード
package main

import "fmt"

func main() {
	// 未初期化の配列の中身を表示する
	fmt.Print("array1: ")
	var array1 [10]int
	for i := 0; i < len(array1); i++ {
		fmt.Print(array1[i])
	}
	fmt.Println()

	// 要素数を推論した配列の中身を表示する
	fmt.Print("array2: ")
	array2 := [...]int{0, 1, 2, 3, 4}
	for i := 0; i < len(array2); i++ {
		fmt.Print(array2[i])
	}
	fmt.Println()

	// 指定された要素を初期化した配列の中身を表示する
	fmt.Print("array3: ")
	array3 := [...]int{2: 2, 4: 4}
	for i := 0; i < len(array3); i++ {
		fmt.Print(array3[i])
	}
}
実行結果
array1: 0000000000
array2: 01234
array3: 00204

構造体

構造体は複数の型をまとめたデータ構造。Goはクラスがない代わりに構造体が使えるようです。中の各データはフィールドと呼ばれ、"."でアクセスします。

初期化する際、{}に渡すデータは上から順にフィールドへ格納されていきます。

構文
// 定義
type 型名 struct {
    フィールド1 
    フィールド2 
    ......
}

// 初期化
// 1行で初期化
変数 := 型名 {値1, 値2}

// 複数行で初期化
変数 := 型名 {
    値1,
    値2, // 複数行で初期化する場合は、最後のフィールドにも","が必須
}

// フィールドを":"で指定して初期化
// 指定されないフィールドは各フィールドの型に合わせたゼロ値で初期化される
変数 := 型名 {
    フィールド2: 値2,
}

// 構造体初期化時に"&"を付けるとそのままポインタを初期化できる
変数 := &型名 {}

構造体にポインタを通してアクセスする場合、Cならアロー演算子でアクセスしますが、Goではアロー演算子は使えず"."でアクセスするようです。(*pointer).fieldもアクセスはできますが、この方法は通常使わないかと思います。ポインタであることを明示したいときに使う?ユースケースが今の段階だとちょっと浮かびません。

サンプルではポインタ経由で値を書き換えていますが、きちんと上書きされているのが確認できます。

サンプルコード
package main

import "fmt"

type Person struct {
	Name    string
	Age     int
	Address string
}

func main() {
	// 初期化が1行なら最後の値に","は不必要
	taro := Person{"田中太郎", 99, "地球"}

	// 初期化するフィールドを指定することもできる
	hanako := Person{
		Name   : "田中花子",
		Age    : 99,
		// 最後のフィールドに","を付けないとコンパイルエラー
        // syntax error: unexpected newline in composite literal; possibly missing comma or }
		Address: "火星",
	}

	// 一部のフィールドだけを初期化すると、残りのフィールドにはゼロ値が入る
	jiro := Person{
		Address: "金星",
	}
	
	// 各フィールドには"."を使ってアクセスする
	fmt.Println("田中太郎")
	fmt.Println("Name   :", taro.Name)
	fmt.Println("Age    :", taro.Age)
	fmt.Println("Address:", taro.Address)

	fmt.Println("\n田中花子")
	fmt.Println("Name   :", hanako.Name)
	fmt.Println("Age    :", hanako.Age)
	fmt.Println("Address:", hanako.Address)

	fmt.Println("\n田中次郎")
	fmt.Println("Name   :", jiro.Name)
	fmt.Println("Age    :", jiro.Age)
	fmt.Println("Address:", jiro.Address)

	/*
	 * ポインタでアクセスする場合
	 */
	pSaburo := &Person{
		"田中三郎",
		100,
		"木星",
	}

	// ポインタの場合も"."でアクセス可能。Cのようにアロー演算子は使わない
	fmt.Println("\n田中三郎")
	fmt.Println("Name   :", pSaburo.Name)
	fmt.Println("Age    :", pSaburo.Age)
	fmt.Println("Address:", pSaburo.Address)

	// ポインタの間接参照でもアクセスできるが、通常は"."でアクセスする
	// fmt.Println("Name   :", (*pSaburo).Name)
	// fmt.Println("Age    :", (*pSaburo).Age)
	// fmt.Println("Address:", (*pSaburo).Address)

	// データ変更
	pSaburo.Name    = "saburo Tanaka"
	pSaburo.Age     = 120
	pSaburo.Address = "土星"

	fmt.Println("\n田中三郎: 修正後")
	fmt.Println("Name   :", pSaburo.Name)
	fmt.Println("Age    :", pSaburo.Age)
	fmt.Println("Address:", pSaburo.Address)
}
実行結果
田中太郎
Name   : 田中太郎
Age    : 99
Address: 地球

田中花子
Name   : 田中花子
Age    : 99
Address: 火星

田中次郎
Name   : 
Age    : 0
Address: 金星

田中三郎
Name   : 田中三郎
Age    : 100
Address: 木星

田中三郎: 修正後
Name   : saburo Tanaka
Age    : 120
Address: 土星

スライス

配列は固定長で長さは変更できませんが、スライスは可変長の配列です。そのため、配列を使うよりスライスの方が一般的なようです。また、スライスは長さを型情報として含みません。

宣言は配列で使った[]に要素数や"..."を記述せず宣言します。初期化は右辺で []型{値1, 値2, 略...} とスライスを直接生成したもの、配列から生成したスライス、あるいはmakeでスライスを生成して初期化します。ここで、配列から生成したスライスを初期化に用いる場合は少々注意です。

スライス演算で切り出す範囲を指定しますが、終端のインデックスは含みません。そのため、下記構文の配列からスライスを作成する例は注意してください。

構文
// 宣言 []T は 型T のスライスを表します。
var 変数 []int
変数 = []int{0, 1, 2, 3, 4}

// 新規で生成したスライスで初期化
変数 := []int{0, 1, 2, 3, 4}

// 既存の配列からスライスを生成して初期化
// array[1 ~ 4]の部分配列をスライスとして生成。スライス演算と呼ぶ
変数 := array[1:5]

// 要素数10の配列arrayがあるとき、次の切り出しはすべて同じ意味になる
array[0:10]
array[:10]
array[0:]
array[:]

// 組み込み関数makeで作成することも可能。こちらは長さ、キャパシティも指定できるようです
変数 := make([], 長さ, キャパシティ)

// スライスに要素の追加。appendの戻り値を同じ変数で受け取ると混乱しなくて良い
同じ変数 = append(データを追加する変数, 追加するデータ)

配列同様、スライスのアクセスは[インデックス]で可能です。スライスは長さとキャパシティを管理しており、それぞれスライスの要素数、現在確保してるメモリの容量を意味しているようです。要素数が容量を超えると新しいメモリ領域を確保し、そちらにまるまるコピーします。

容量の初期値はスライスを切り出した配列の長さを基に設定されるようです。(スライスが参照する起点から終端までの長さ)

文字だけではわかりにくいので図に起こしました。

len、capのイメージ.png

lenは切り出した後、スライスに格納されている要素数で長さを表し、capは切り出す起点から終端までの長さ(切り出し元の配列)で、これがスライスが持つ容量の初期値になります。切り出し元の配列へのポインタも保持しているようです。下記記事が参考になります。

切り出し元の配列へのポインタを持っているとのことなので、それぞれのアドレスを表示し、中身を上書きした上で書き換わっているかどうかを確認してみます。長さ、キャパシティはそれぞれ組み込み関数のlen(), cap()で取得可能です。

サンプルコード
package main

import "fmt"

func main() {

	// 配列とスライスの作成
	array  := [...]int{0, 1, 2, 3, 4}
	slice1 := array[1:3]

	fmt.Println("配列、スライスの中身を表示")
	fmt.Println("array:", array)
	fmt.Println("s    :", slice1)
	fmt.Println("cap  :", cap(slice1))
	fmt.Println("len  :", len(slice1))

	fmt.Println("\n配列の切り出した起点、array[1]とスライスの先頭アドレスを表示")
	// Cの場合、配列名がそのまま先頭アドレスを表すが、Goではたとえ先頭要素でも明示的に&array[0]と表現しないといけないらしい
	fmt.Printf("&array[1]: %p\n", &array[1])
	// スライスはポインタ扱いになり、渡すとそのままアドレスが表示される
	fmt.Printf("slice    : %p\n", slice1)

	// スライスの要素数を超えるが、切り出し元の配列の長さ分表示するとどうなるか実験
	// ランタイムエラー: panic: runtime error: index out of range [2] with length 2
	// for i := 0; i < cap(slice1); i++ {
	// fmt.Print(slice1[i])
	// }

	fmt.Println("\n\nスライスを書き換えた上で配列、スライスの中身を表示")
	for i := 0; i < len(slice1); i++ {
		slice1[i] = 99
	}
	fmt.Println("array:", array)
	fmt.Println("s    :", slice1)

	fmt.Println("\nスライスの長さが容量を超えた際のアドレスの変化")
	slice2 := make([]int, 0, 5)

	for i := 0; i < 6; i++ {
		slice2 = append(slice2, i+10)
		fmt.Println("slice2:", slice2)
		fmt.Printf("address: %p\n", slice2)
		fmt.Println("len:", len(slice2))
		fmt.Println("cap:", cap(slice2))
		fmt.Println()
	}
}
実行結果
配列スライスの中身を表示
array: [0 1 2 3 4]
s    : [1 2]
cap  : 4
len  : 2

配列の切り出した起点array[1]とスライスの先頭アドレスを表示
&array[1]: 0xc000010338
slice    : 0xc000010338

スライスを書き換えた上で配列スライスの中身を表示
array: [0 99 99 3 4]
s    : [99 99]

スライスの長さが容量を超えた際のアドレスの変化
slice2: [10]
address: 0xc0000104b0
len: 1
cap: 5

slice2: [10 11]
address: 0xc0000104b0
len: 2
cap: 5

slice2: [10 11 12]
address: 0xc0000104b0
len: 3
cap: 5

slice2: [10 11 12 13]
address: 0xc0000104b0
len: 4
cap: 5

slice2: [10 11 12 13 14]
address: 0xc0000104b0
len: 5
cap: 5

slice2: [10 11 12 13 14 15]
address: 0xc000012230
len: 6
cap: 10

切り出し元の配列とアドレスが一致、上書きされていることが確認できました。また、スライスの要素数が容量を超えると新規でメモリを確保し、そちらにコピーされることが確認できました。

マップ

キーと値を紐づけ、キーで値を参照できるデータ構造です。他の言語でも使われる連想配列です。変数[キー]でアクセスすると紐づいた値が返される。長さの取得は組み込み関数のlen()で可能。

構文
// 宣言 未初期化状態なのでゼロ値のnilが入る
var 変数 map[キーの型]値の型

// 初期化
変数 := map[キーの型]値の型 {キー: 値1, キー: 値2, ...}

// 複数行で初期化
変数 := map[キーの型]値の型 {
    キー: 値1,
    キー: 値2, // 複数行で初期化する場合は、最後のフィールドにも","が必須
}

// makeで初期化。キャパシティを指定できる。上限を超えた場合は動的にメモリが確保される
変数 = make(map[キーの型]値の型, キャパシティ)

// mapへ指定したキーでアクセス。キーがmap内に存在しない場合はキーと値の組が新規で生成される
変数[キー] = 

// 指定したキーがmapに存在するか確認する
// 存在すれば変数vに値が入り、変数okにtrueが返る。存在しないならゼロ値、falseが返る
v, ok := 変数[キー]

// 指定したキーをmapから削除する
delete(変数, キー)

mapのキャパシティに関しては下記記事をご参照ください。

サンプルコード
package main

import "fmt"

func main() {
	m := map[int]int{
		0: 10,
		1: 11,
		2: 12,
		3: 13,
	}

	fmt.Println("mの中身を表示")
	fmt.Println("m: ", m)

	fmt.Println("\nキー:10が存在するか確認")
	if v, ok := m[10]; ok {
		fmt.Printf("キーは存在します。値は%dです\n", v)
	} else {
		fmt.Printf("キーは存在しません\n")
	}

	fmt.Println("\nキー:10を追加")
	m[10] = 100

	fmt.Println("\nキー:10が存在するか確認")
	if v, ok := m[10]; ok {
		fmt.Printf("キーは存在します。値は%dです\n", v)
	} else {
		fmt.Printf("キーは存在しません\n")
	}

	fmt.Println("\nキー:10を削除")
	delete(m, 10)

	fmt.Println("\nキー:10が存在するか確認")
	if v, ok := m[10]; ok {
		fmt.Printf("キーは存在します。値は%dです\n", v)
	} else {
		fmt.Printf("キーは存在しません\n")
	}
}
実行結果
mの中身を表示
m:  map[0:10 1:11 2:12 3:13]

キー:10が存在するか確認
キーは存在しません

キー:10を追加

キー:10が存在するか確認
キーは存在します値は100です

キー:10を削除

キー:10が存在するか確認
キーは存在しません

おわりに

今回はここまでです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1