スライス・マップ・配列
.NET (C#) の経験がある開発者向けに、配列、スライス、マップ、range ループ、List<T> / Dictionary<K,V> との違いを解説します。
1. 配列 (Array)
Go の配列は長さが型の一部で、値型です。[3]int と [4]int は別の型で、代入や関数への受け渡しでは全要素がコピーされます。
var a [3]int // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]string{"x", "y"} // 長さを要素数から推論
d := b // 全要素のコピー
d[0] = 99
fmt.Println(b[0]) // 1
固定長が必要な特殊な場面 (ハッシュ値 [32]byte など) を除き、日常的にはスライスを使います。
2. スライス (Slice)
スライスは配列の一部を指すビューで、内部は次の3つの組 (ヘッダ) です。
| フィールド | 意味 |
|---|---|
| ポインタ | 底となる配列の先頭要素 |
len | 要素数 |
cap | 底の配列の、先頭から数えた確保済み容量 |
s := []int{1, 2, 3} // リテラル
t := make([]int, 0, 10) // len=0, cap=10 (事前確保)
var u []int // nil スライス (len=0, cap=0)
fmt.Println(len(s), cap(s)) // 3 3
append の仕組み
append は容量に余裕があれば底の配列に書き込み、足りなければより大きな配列を確保してコピーします。戻り値を必ず受け取ります。
s := make([]int, 0, 2)
s = append(s, 1, 2) // cap=2 のまま
s = append(s, 3) // 再確保: cap が増える (通常は約2倍)
事前に要素数が分かるなら、make([]T, 0, n) で容量を確保すると再確保を避けられます。
スライス式と共有
s[low:high] は元のデータをコピーせず、同じ底の配列を共有します。
a := []int{0, 1, 2, 3, 4}
b := a[1:3] // [1 2] len=2, cap=4
b[0] = 99
fmt.Println(a) // [0 99 2 3 4] 元も変わる
b = append(b, 100)
fmt.Println(a) // [0 99 2 100 4] cap に余裕があるため、元の要素を上書きする
独立したコピーが必要なときは、slices.Clone か copy を使います。
c := slices.Clone(a[1:3]) // Go 1.21+
d := make([]int, 2)
copy(d, a[1:3])
3インデックススライス a[1:3:3] で cap を制限すると、append が元を上書きしなくなります。
nil スライスと空スライス
var n []int // nil
e := []int{} // 空 (nil ではない)
fmt.Println(len(n), n == nil) // 0 true
n = append(n, 1) // nil でも append できる
nil スライスは、len、cap、range、append がそのまま使えます。「空かどうか」は len(s) == 0 で判定し、s == nil に頼りません。なお、JSON では nil スライスは null、空スライスは [] にシリアライズされる点に注意してください。
要素の削除と挿入
専用メソッドはなく、標準パッケージ slices を使います。
s := []int{0, 1, 2, 3, 4}
s = slices.Delete(s, 1, 3) // [0 3 4] (index 1 以上 3 未満を削除)
s = slices.Insert(s, 1, 10, 11) // [0 10 11 3 4]
3. マップ (Map)
map[K]V はハッシュテーブルで、Dictionary<K,V> に相当します。キーは == で比較可能な型 (文字列、数値、構造体、配列など) に限られます。スライス・マップ・関数はキーにできません。
m := map[string]int{"a": 1, "b": 2}
m2 := make(map[string]int)
m["c"] = 3
v := m["a"] // 1
v, ok := m["x"] // ok=false: キーがないとゼロ値と false
delete(m, "a") // 存在しなくてもエラーにならない
fmt.Println(len(m))
重要な注意点
- 順序は保証されない:
rangeの順序は毎回ランダム。順序が必要ならキーを取り出してソートする - nil マップへの書き込みは panic: 読み取りと
delete、rangeは nil でも安全
var m map[string]int
fmt.Println(m["a"]) // 0 (OK)
m["a"] = 1 // panic: assignment to entry in nil map
- 同時アクセスは安全ではない: 複数 goroutine から読み書きすると、fatal error になる。
sync.Mutexで保護するかsync.Mapを使う (Goroutines とチャネル) - 構造体の値を直接変更できない:
m["a"].Count++はコンパイルエラー
m := map[string]Item{}
it := m["a"]
it.Count++
m["a"] = it // 取り出して変更して戻す
mp := map[string]*Item{} // ポインタを値にすれば直接変更できる
mp["a"].Count++
順序付きで走査する
import (
"maps"
"slices"
)
for _, k := range slices.Sorted(maps.Keys(m)) { // Go 1.23+
fmt.Println(k, m[k])
}
HashSet の代わり
セットは map[T]struct{} (または map[T]bool) で表します。struct{} はメモリを使いません。
set := map[string]struct{}{}
set["a"] = struct{}{}
if _, ok := set["a"]; ok {
fmt.Println("contains")
}
4. range ループ
s := []string{"a", "b", "c"}
for i, v := range s { fmt.Println(i, v) } // インデックスと値
for i := range s { fmt.Println(i) } // インデックスのみ
for _, v := range s { fmt.Println(v) } // 値のみ
for k, v := range map[string]int{"x": 1} { fmt.Println(k, v) }
for i, r := range "日本語" { fmt.Println(i, r) } // 文字列は rune 単位 (i はバイト位置)
for i := range 3 { fmt.Println(i) } // 整数 (Go 1.22+): 0 1 2
注意点:
vは要素のコピー。構造体を変更するときはs[i].Field = ...を使う- Go 1.22 以降、ループ変数はイテレーションごとに新しい変数になる
- ループ中にマップへ追加・削除しても動作は定義されているが、追加した要素が走査されるかは不定
5. slices / maps 標準パッケージ
LINQ に相当する汎用の機能はありませんが、Go 1.21 以降は slices と maps パッケージで基本操作が揃っています。
import (
"maps"
"slices"
)
nums := []int{3, 1, 2}
slices.Sort(nums) // [1 2 3]
slices.Contains(nums, 2) // true
idx := slices.Index(nums, 3) // 2
slices.Reverse(nums) // [3 2 1]
slices.Max(nums) // 3
i, found := slices.BinarySearch(nums, 2) // ソート済みを前提とする
people := []Person{{"a", 30}, {"b", 20}}
slices.SortFunc(people, func(x, y Person) int {
return cmp.Compare(x.Age, y.Age) // import "cmp"
})
m2 := maps.Clone(m)
keys := slices.Collect(maps.Keys(m)) // Go 1.23+
Where / Select に相当するものは、for ループで書くのが Go では普通です。
var adults []Person
for _, p := range people {
if p.Age >= 20 {
adults = append(adults, p)
}
}
6. .NET との対応表
| .NET (C#) | Go |
|---|---|
T[] (配列、参照型) | [N]T (配列、値型) / 通常は []T |
List<T> | []T (slice) |
Dictionary<K,V> | map[K]V |
HashSet<T> | map[T]struct{} |
| LINQ | for ループ / slices・maps パッケージ |
list.Add(x) | s = append(s, x) |
list.Count | len(s) |
list.RemoveAt(i) | s = slices.Delete(s, i, i+1) |
list.Contains(x) | slices.Contains(s, x) |
list.Sort() | slices.Sort(s) |
dict.TryGetValue(k, out v) | v, ok := m[k] |
dict.Remove(k) | delete(m, k) |
ReadOnlyCollection<T> | なし (慣習とコピーで守る) |
ConcurrentDictionary | sync.Map / map + sync.RWMutex |
Span<T> / ArraySegment<T> | スライス式 s[a:b] |
SortedDictionary | 標準にはなし (キーをソートして走査) |
7. C# 開発者がつまずきやすいポイント
スライスは参照ではなく「ヘッダの値渡し」
関数にスライスを渡すと、ヘッダだけがコピーされます。要素の変更は呼び出し元に見えますが、append の結果は戻り値で返さないと反映されません。
func add(s []int) { s = append(s, 1) } // 呼び出し元の len は変わらない
func add2(s []int) []int { return append(s, 1) }
s = add2(s)
append の結果を同じ変数に代入する
s2 := append(s1, x) のように別の変数へ代入すると、容量次第で s1 と s2 が底の配列を共有したり、しなかったりします。s = append(s, x) の形で使うのが原則です。
他のスライスの一部を保持するとメモリが解放されない
巨大なスライスの一部 big[:10] を保持すると、底の配列全体が GC されません。必要なら slices.Clone でコピーします。
マップのイテレーション順序
C# の Dictionary は (実装上) 挿入順に近い順序で列挙されますが、Go はランダムです。テストの期待値や出力の順序をマップ順に依存させないでください。
その他のポイント
- 配列は値渡しでコピーされる (C# の配列は参照型)
- 範囲外のインデックスは panic (
index out of range) - スライスは
==で比較できない (nil との比較のみ可)。slices.Equalを使う - マップの
lenは要素数のみで、capはない。大量に追加するならmake(map[K]V, n)で事前確保する for rangeで要素を削除する場合は、後ろから回すかslices.DeleteFuncを使う- 文字列は不変のバイト列。
[]byte(s)や[]rune(s)への変換はコピーを伴う
8. コード例
C#
var users = new List<User> { new("a", 30), new("b", 17) };
var adultNames = users
.Where(u => u.Age >= 18)
.Select(u => u.Name)
.OrderBy(n => n)
.ToList();
var counts = new Dictionary<string, int>();
foreach (var u in users)
{
counts.TryGetValue(u.Name, out var c);
counts[u.Name] = c + 1;
}
Go
package main
import (
"fmt"
"maps"
"slices"
)
type User struct {
Name string
Age int
}
func main() {
users := []User{{"a", 30}, {"b", 17}}
// Where + Select + OrderBy に相当 (容量を事前確保)
adultNames := make([]string, 0, len(users))
for _, u := range users {
if u.Age >= 18 {
adultNames = append(adultNames, u.Name)
}
}
slices.Sort(adultNames)
// 集計: 存在しないキーはゼロ値なので、そのまま加算できる
counts := map[string]int{}
for _, u := range users {
counts[u.Name]++
}
// 順序付きで出力
for _, k := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(k, counts[k])
}
fmt.Println(adultNames)
}
9. まとめ
- 配列は値型で長さが型の一部。通常はスライス (
[]T) を使う - スライスは (ポインタ, len, cap) のヘッダ。
s[a:b]は底の配列を共有し、appendは必ず戻り値を受け取る - 独立したコピーは
slices.Clone/copy。サイズが分かればmake([]T, 0, n)で事前確保する - マップは順序なし、nil マップへの書き込みは panic、並行アクセスは保護が必要
- 存在確認は
v, ok := m[k]。セットはmap[T]struct{} - LINQ の代わりに
forループとslices/mapsパッケージを使う