はじめに
第1〜2章に続き、第3章で学んだポイントを要点+検証コードでまとめます。
前回の記事はこちら👇
配列と型:サイズが違うと別型
- 配列は長さを含めて型が決まる。
[3]int
と[4]int
は別型で、代入も代入互換も無い。
package main
func main() {
var a [3]int
var b [4]int
// ❌ cannot use b (type [4]int) as type [3]int
}
スライスの初期化・比較:nil と空、比較は不可
宣言のみ(未初期化)のスライスは nil。
一方、リテラル []T{} や make([]T, 0) は空スライス(len=0)だが nil ではない。
スライス同士の比較はできない(== は nil との比較のみOK)。
package main
import "fmt"
func main() {
var s []int // nil
t := []int{} // 空だが非nil
u := make([]int, 0) // 空だが非nil
fmt.Println(s == nil) // true
fmt.Println(t == nil) // false
fmt.Println(u == nil) // false
// fmt.Println(s == t) // ❌ slice can only be compared to nil
}
関数呼び出しは値渡し
Goは値渡し。スライスは「ヘッダ(ポインタ等)のコピーなので、
要素の変更は元にも影響(同じ配列を指すため)。
append で再割当が起きると別配列に移り、以降は元に影響しない
package main
import "fmt"
func add1(n int) { n++ }
func touchElem(s []int) { s[0] = 99 } // 要素変更 → 共有配列が書き換わる
func main() {
i := 10
add1(i)
fmt.Println(i) // 10(値渡し)
a := []int{1, 2, 3}
touchElem(a)
fmt.Println(a) // [99 2 3](値渡し)
}
make で容量(cap) を先取り
スライスの将来の append を見越して cap を大きめに。
package main
import "fmt"
func main() {
s := make([]int, 0, 5) // len=0 cap=5
fmt.Println(len(s), cap(s)) // 0 5
s = append(s, 1,2,3,4,5)
fmt.Println(len(s), cap(s)) // 5 5(ちょうど)
}
サブスライス式とフルスライス式
package main
import "fmt"
func main() {
x := make([]string, 0, 5)
x = append(x, "a","b","c","d") // len=4 cap=5
y := x[:2] // サブスライス:len=2 cap=5(共有・危険)
z := x[:2:2] // フルスライス:len=2 cap=2(cap制限・安全)
y = append(y, "Y","Y") // 共有配列に追記→xが汚染され得る
z = append(z, "Z") // cap不足→新配列に退避→xは無傷
fmt.Println("x:", x) // x: [a b Y Y]
fmt.Println("y:", y) // y: [a b Y Y]
fmt.Println("z:", z) // z: [a b Z]
}
配列⇄スライス:共有と非共有
配列→スライス:s := arr[:] は同じ配列を共有(書き換えが相互に見える)。
スライス→配列:スライスの要素を変更しても配列には影響しない。
package main
import "fmt"
func main() {
// 配列からスライスへ
arr := [4]int{1,2,3,4}
s := arr[:] // 共有
s[0] = 99
fmt.Println(arr) // [99 2 3 4]
}
copy は共有しない(ディープコピーの一種)
package main
import "fmt"
func main() {
src := []int{1,2,3}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 999
fmt.Println(src) // [1 2 3]
fmt.Println(dst) // [999 2 3]
}
文字列・rune・UTF-8
文字列はUTF-8のバイト列(不変)。len はバイト数。
rune は int32 の別名。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "こんにちは" // 1文字=3バイト、合計15バイト
fmt.Println(len(s)) // 15(バイト数)
fmt.Println(utf8.RuneCountInString(s)) // 5(rune数)
// インデックスはバイト単位
fmt.Printf("%x\n", s[0]) // e3(先頭バイト)
// range はrune単位
for i, r := range s {
fmt.Printf("i=%d r=%c\n", i, r)
} // こ→ん→に→ち→は
// 文字列⇄runeスライス
rs := []rune(s)
rs[0] = 'さ'
fmt.Println(string(rs)) // 「さんにちは」
}
文字列のインデックス/スライスは1バイト文字のみで扱うようにする。
日本語などの多バイトでは途中で切ると壊れたUTF-8になる。
推奨:日本語など多バイトを扱うときは []rune に変換してからインデックス/スライスする。
「カンマOK」イディオム
マップ参照:キーの存在確認
package main
import "fmt"
func main() {
// 在庫数を持つシンプルなマップ
stock := map[string]int{
"apple": 3,
"banana": 0, // 在庫0(キーはある)
}
// あるキー
v, ok := stock["apple"]
fmt.Println("apple:", v, ok) // 3 true
// ないキー(ゼロ値が返るが、okがfalseで判別できる)
v2, ok2 := stock["orange"]
fmt.Println("orange:", v2, ok2) // 0 false
// 在庫0と“そもそも存在しない”を区別できるのがポイント
v3, ok3 := stock["banana"]
fmt.Println("banana:", v3, ok3) // 0 true
}
集合(Set) をマップで表現
存在だけを表したいので、値型はstruct{}(ゼロサイズ)か bool が定番。
package main
import "fmt"
func main() {
// 値にboolを使ったSet(存在=true)
set := map[string]bool{}
// 追加
set["go"] = true
set["rust"] = true
// 存在確認(キーがなければ false が返る)
fmt.Println(set["go"]) // true
fmt.Println(set["java"]) // false
// 削除
delete(set, "go")
fmt.Println(set["go"]) // false
}
構造体(struct)と合成(埋め込み)
Go にはクラスや継承はない。代わりに 構造体の埋め込みやインターフェース、委譲でする。
まとめ(第3章)
-
配列は長さを含めて型。サイズ違いは別型。
-
スライスはnilと空を区別、比較はnilとのみ。
-
Goの関数は値渡しだが、スライスはヘッダのコピーなので要素変更は共有配列に影響。
-
make で cap 先取り、フルスライスで cap 制限すると安全。
-
配列→スライスは共有、copy は非共有。
-
文字列はUTF-8の不変バイト列。インデックス/スライスは1バイト文字のみで、多バイトは []rune/utf8 を使う。
-
カンマOK(map)で存在する値とゼロ値を区別する。
-
クラス/継承はなく、構造体+埋め込み+インターフェースで設計する。