Go: Goのマップと構造体:ハッシュテーブルでのO(1)検索、structタグ、ネスト、および構成
最終更新:2026-08-26
Goにおけるデータモデリングの2大柱はマップと構造体です。マップは動的なクエリを処理し、構造体は固定された構造を記述します。これらを組み合わせることで、ビジネスオブジェクトの90%を記述することができます。
マップは O(1) のキー・値検索を実現するのに対し、構造体はフィールドを用いてデータを集約します。このレッスンでは、Go におけるデータモデリングのすべての基本概念を習得し、それらを活用して完全な e コマース在庫管理システムを構築できるようになります。
1. 学習内容
- マップの CRUD(作成/読み取り/更新/削除)
- comma-ok: キーが存在するかどうかを確認します
- マップの範囲内の要素を反復処理して削除する
- 構造体内のフィールドの定義、初期化、およびアクセス
- struct tag(JSONシリアライゼーション用のキー)
- 入れ子構造と構成
- マップとスライスのどちらを選ぶか
- マップと構造体(Struct)を用いたEC用SKU在庫管理システムの構築
2. あるEコマースエンジニアの実話
(1) 課題:スライスの検索が遅すぎる
ボブは、あるECプラットフォームのバックエンドエンジニアです。「ダブル11」の先行販売キャンペーンが進行中ですが、在庫照会APIの応答が信じられないほど遅くなっています:
「当社では10万点のSKUを在庫として保有しており、これらは
[]Productスライスを使用して格納されています。すべてのクエリで線形スキャンが行われています。QPSが増加すると、平均応答時間は800ミリ秒となり、CPU使用率は100%に達します。上司から、これを100ミリ秒未満に最適化するよう、3日間の猶予を与えられました。」
彼は自分が書いたコードを開いた:
// Version 1: Store all products using a slice
var products []Product
func findProduct(sku string) *Product {
for _, p := range products { // O(n) linear scan
if p.SKU == sku {
return &p
}
}
return nil
}
10万個の製品 × 1,000 QPS = 1億回のトラバーサル/秒。CPU使用率が100%になるのは避けられません。
(2) Goでの解決策:O(1)の検索にはmapを使用する
// inventory.go
package main
import "fmt"
type Product struct {
SKU string // Product unique identifier
Name string // Product name
Price float64 // Price
Stock int // Stock count
Category string // Classification
}
// Using map to create a hash table
type Inventory struct {
products map[string]Product // SKU -> Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// O(1) query
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// O(1) update
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
p.Stock += delta
inv.products[sku] = p
return nil
}
func main() {
inv := NewInventory()
// Bulk import of 100,000 SKUs
for i := 0; i < 100000; i++ {
sku := fmt.Sprintf("SKU-%05d", i)
inv.products[sku] = Product{
SKU: sku,
Name: fmt.Sprintf("Product-%d", i),
Price: 99.99,
Stock: 100,
Category: "electronics",
}
}
// Query performance comparison
p, ok := inv.Find("SKU-50000")
if ok {
fmt.Printf("Found: %s, price=%.2f\n", p.Name, p.Price)
}
// Update inventory
inv.UpdateStock("SKU-50000", -5)
}
出力:
Found: Product-50000, price=99.99
(3) パフォーマンス:slice と map のクエリパフォーマンスの比較
| データサイズ | スライス内の線形スキャン | マップ内のハッシュ検索 | パフォーマンスの差 |
|---|---|---|---|
| 100 SKU | 50 ns | 50 ns | 同等 |
| 1,000 SKU | 500 ns | 50 ns | 10倍 |
| 10,000 SKU | 5 µs | 50 ns | 100倍 |
| 100,000 SKU | 50 µs | 50 ns | 1,000倍 |
3. マップの基礎
(1) 地図を作成する3つの方法
package main
import "fmt"
func main() {
// Method 1: make (Recommended)
m1 := make(map[string]int) // empty map, writable
// Method 2: make + pre-allocated capacity
m2 := make(map[string]int, 100) // pre-allocate 100 capacity, reduce resizing
// Method 3: Literal Initialization
m3 := map[string]int{
"Alice": 28,
"Bob": 32,
}
// Method 4: nil map (read-only, cannot be written to)
var m4 map[string]int // == nil, cannot do m4["a"] = 1
_ = m4
}
(2) マップのCRUD
package main
import "fmt"
func main() {
ages := make(map[string]int)
// Create
ages["Alice"] = 28
ages["Bob"] = 32
// Read
fmt.Println(ages["Alice"]) // 28
// Update
ages["Alice"] = 29
// Delete
delete(ages, "Bob")
// Length
fmt.Printf("len=%d\n", len(ages))
fmt.Println(ages) // map[Alice:29]
}
出力:
28
len=1
map[Alice:29]
▶ サンプル:マップの反復処理(順序はランダム)
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28, "Bob": 32, "Charlie": 45}
// for range: key + value
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Only the key
for name := range ages {
fmt.Printf("name: %s\n", name)
}
// Only the value (use _ to ignore the key)
for _, age := range ages {
fmt.Printf("age: %d\n", age)
}
}
出力(順不同):
Alice is 28 years old
Charlie is 45 years old
Bob is 32 years old
name: Bob
...
4. comma-ok の構文(要点)
(1) 「値がゼロ」と「存在しない」を区別する
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28}
// Incorrect approach: Cannot distinguish between "key does not exist" and "value = 0"
age := ages["Bob"]
fmt.Printf("Bob's age: %d\n", age) // 0 (but is Bob really 0 years old?)
// Correct approach: comma-ok
age, ok := ages["Bob"]
if ok {
fmt.Printf("Bob's age: %d\n", age)
} else {
fmt.Println("Bob not found")
}
// Just check if it exists: discard the value
_, exists := ages["Bob"]
fmt.Printf("Bob exists: %v\n", exists)
}
出力:
Bob's age: 0
Bob not found
Bob exists: false
(2) 実践における「comma-ok」:クエリのキャッシュ
package main
import "fmt"
// Cache: key → value
var cache = make(map[string]string)
func getCached(key string) (string, bool) {
val, ok := cache[key]
return val, ok
}
func main() {
// Set cache
cache["user:1"] = "Alice"
cache["user:2"] = "Bob"
// Query cache
if val, ok := getCached("user:1"); ok {
fmt.Printf("Hit: %s\n", val)
} else {
fmt.Println("Miss")
}
if _, ok := getCached("user:999"); !ok {
fmt.Println("user:999 not in cache, fetching from DB...")
}
}
出力:
Hit: Alice
user:999 not in cache, fetching from DB...
(3) カンマ可の一般的なパターン
| 文脈 | 構文 | 意味 |
|---|---|---|
| 地図検索 | v, ok := m[key] |
キーは存在しますか? |
| 型のアサーション | v, ok := x.(T) |
型の一致? |
| チャンネルからの受信 | v, ok := <-ch |
チャンネルが閉じられましたか? |
5. 構造体の基礎
(1) 構造体の定義と初期化
package main
import "fmt"
// define struct
type User struct {
Name string
Age int
City string
}
func main() {
// Method 1: By field order (not recommended; poor readability)
u1 := User{"Alice", 28, "Shanghai"}
// Method 2: Initializing Field Names (Recommended)
u2 := User{
Name: "Bob",
Age: 32,
City: "Beijing",
}
// Method 3: Partial Initialization (with the remainder set to zero)
u3 := User{Name: "Charlie"} // Age=0, City=""
// Method 4: new() returns a pointer
u4 := new(User)
u4.Name = "Dave"
fmt.Println(u1, u2, u3, u4)
}
出力:
{Alice 28 Shanghai} {Bob 32 Beijing} {Charlie } &{Dave 0 }
(2) フィールドへのアクセスと変更
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Alice", Age: 28}
// Read field
fmt.Println(u.Name) // Alice
// Write to a field
u.Age = 29
// Pointer access (automatic dereferencing)
p := &u
fmt.Println(p.Name) // Alice (equivalent to (*p).Name)
p.Age = 30 // automatic dereferencing
}
▶ サンプル:構造体のコピーとポインタの比較
package main
import "fmt"
type Counter struct {
Value int
}
// Pass-by-value: Copy the entire struct
func incrementByValue(c Counter) {
c.Value++ // modifies the copy
fmt.Printf("Inside function: %d\n", c.Value)
}
// Pointer passing: pass by reference
func incrementByPointer(c *Counter) {
c.Value++ // modifies the original
fmt.Printf("Inside function: %d\n", c.Value)
}
func main() {
c := Counter{Value: 10}
incrementByValue(c)
fmt.Printf("After value passing: %d\n", c.Value) // 10 (unchanged)
incrementByPointer(&c)
fmt.Printf("After pointer passing: %d\n", c.Value) // 11 (modified)
}
出力:
Inside function: 11
After value passing: 10
Inside function: 11
After pointer passing: 11
6. struct tag(JSONシリアライゼーションの鍵)
(1) struct tag の構文
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" validate:"min=0,max=150"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // - means JSON ignores this field
}
(2) JSONシリアライゼーションの実践
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // password not serialized
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Password: "secret123",
}
// Serialization: struct → JSON
data, _ := json.Marshal(u)
fmt.Printf("JSON: %s\n", data)
// Deserialization: JSON → struct
jsonStr := `{"name":"Bob","age":32,"email":"bob@example.com"}`
var u2 User
json.Unmarshal([]byte(jsonStr), &u2)
fmt.Printf("Deserialization: %+v\n", u2)
}
出力:
JSON: {"name":"Alice","age":28,"email":"alice@example.com"}
Deserialization: {Name:Bob Age:32 Email:bob@example.com Password:}
(3) よく使われるstructタグライブラリ
| ライブラリ | タグ名 | 用途 |
|---|---|---|
| encoding/json | json:"name" |
JSONフィールド名 |
| gorm | gorm:"primaryKey" |
ORMのフィールド制約 |
| バリデータ | validate:"required" |
フィールド検証 |
| yaml | yaml:"name" |
YAMLのシリアライズ |
7. ネストされた構造
(1) ネストされた構造体(継承ではなく構成)
Goには継承という概念はありません。代わりに、ネストされた構造体を用いて構成を実現しています:
package main
import "fmt"
type Address struct {
City string
Country string
}
type User struct {
Name string
Age int
Address Address // nested Address
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// Accessing nested fields
fmt.Println(u.Address.City) // Shanghai
fmt.Println(u.Address.Country) // China
}
(2) 匿名ネスト(フィールド昇格)
package main
import "fmt"
type Address struct {
City string
Country string
}
// Anonymous nesting: fields are "promoted"
type User struct {
Name string
Age int
Address // equivalent to Address Address, but without the field name
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// Field promotion: direct access, without u.Address.City
fmt.Println(u.City) // Shanghai
fmt.Println(u.Country) // China
// You can also use the full path
fmt.Println(u.Address.City)
}
▶ サンプル:ネストされたタグとJSONタグの実践
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Address Address `json:"address"`
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Address: Address{
City: "Shanghai",
Country: "China",
},
}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))
}
出力:
{
"name": "Alice",
"age": 28,
"email": "alice@example.com",
"address": {
"city": "Shanghai",
"country": "China"
}
}
8. map と slice のどちらを選ぶか
(1) モデル選択の参照表
| 次元 | スライス []T |
マップ map[K]V |
|---|---|---|
| 検索方法 | 線形スキャン O(n) | ハッシュ O(1) |
| 注文済みですか? | ✅ 注文済み | ❌ 未注文 |
| メモリ使用量 | コンパクト (24 + len*size) | コンパクトではない (ハッシュテーブル + バケット) |
| 要素の削除 | 手動での移動が必要 | delete(m, k) O(1) |
| 代表的なシナリオ | リスト/キュー/スタック/ソート | 辞書/キャッシュ/インデックス |
(2) モデル選択の決定木
graph TB
A[Need to store multiple elements] --> B{Need to look up by key?}
B -->|Yes| C[Use map<br/>O(1) lookup]
B -->|No| D{Need to maintain order?}
D -->|Yes| E[Use slice]
D -->|No| F{Element count < 100?}
F -->|Yes| G[Either slice or map works]
F -->|No| C
(3) 実例:学生の成績管理
package main
import "fmt"
// Store student grades in a map (look up by student ID)
type GradeBook struct {
scores map[string]int // Student ID → Score
}
func (gb *GradeBook) Set(id string, score int) {
gb.scores[id] = score
}
func (gb *GradeBook) Get(id string) (int, bool) {
score, ok := gb.scores[id]
return score, ok
}
// Use a slice to store a list of scores (for sorting or calculating the average)
func average(scores []int) float64 {
if len(scores) == 0 {
return 0
}
sum := 0
for _, s := range scores {
sum += s
}
return float64(sum) / float64(len(scores))
}
func main() {
gb := &GradeBook{scores: make(map[string]int)}
gb.Set("S001", 95)
gb.Set("S002", 82)
gb.Set("S003", 67)
if s, ok := gb.Get("S001"); ok {
fmt.Printf("S001: %d\n", s)
}
// Collect all scores and calculate the average
allScores := []int{}
for _, s := range gb.scores {
allScores = append(allScores, s)
}
fmt.Printf("Average Score: %.2f\n", average(allScores))
}
出力:
S001: 95
Average Score: 81.33
9. 完全な例:EコマースSKU在庫管理システム
マップと構造体のすべての機能を組み合わせて、完全なEコマース在庫照会システムを構築します:
// inventory_system.go
package main
import (
"encoding/json"
"fmt"
"sort"
)
// Product
type Product struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
Tags []string `json:"tags,omitempty"` // Tags: new/hot/discount
}
// Inventory System
type Inventory struct {
products map[string]Product // SKU → Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// Add product
func (inv *Inventory) Add(p Product) {
inv.products[p.SKU] = p
}
// Query (comma-ok)
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// Update stock
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
newStock := p.Stock + delta
if newStock < 0 {
return fmt.Errorf("insufficient stock for %s: have %d, need %d",
sku, p.Stock, -delta)
}
p.Stock = newStock
inv.products[sku] = p
return nil
}
// Sort by price (unordered map → convert to slice)
func (inv *Inventory) ListByPrice() []Product {
list := make([]Product, 0, len(inv.products))
for _, p := range inv.products {
list = append(list, p)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Price < list[j].Price
})
return list
}
// Filter by category
func (inv *Inventory) FindByCategory(category string) []Product {
var result []Product
for _, p := range inv.products {
if p.Category == category {
result = append(result, p)
}
}
return result
}
func main() {
inv := NewInventory()
// Initialize 100 SKUs
categories := []string{"electronics", "clothing", "food"}
for i := 0; i < 100; i++ {
inv.Add(Product{
SKU: fmt.Sprintf("SKU-%05d", i),
Name: fmt.Sprintf("Product-%d", i),
Price: float64(i%50 + 10),
Stock: 100 - i%30,
Category: categories[i%3],
})
}
// 1. Look up a single SKU
if p, ok := inv.Find("SKU-0050"); ok {
fmt.Printf("Found: %s, price=%.2f, stock=%d\n", p.Name, p.Price, p.Stock)
}
// 2. Update inventory
if err := inv.UpdateStock("SKU-0050", -10); err == nil {
fmt.Println("Stock updated successfully")
}
// 3. Filter by category
electronics := inv.FindByCategory("electronics")
fmt.Printf("\nElectronics products: %d\n", len(electronics))
// 4. Sort by price (top 3)
sorted := inv.ListByPrice()
fmt.Println("\nTop 3 cheapest:")
for i, p := range sorted[:3] {
fmt.Printf(" %d. %s: %.2f\n", i+1, p.Name, p.Price)
}
// 5. JSON Serialization (API Response)
if p, ok := inv.Find("SKU-0050"); ok {
data, _ := json.MarshalIndent(p, "", " ")
fmt.Printf("\nJSON output:\n%s\n", data)
}
}
期待される出力:
Found: Product-50, price=10.00, stock=70
Stock updated successfully
Electronics products: 34
Top 3 cheapest:
1. Product-0: 10.00
2. Product-30: 10.00
3. Product-60: 10.00
JSON output:
{
"sku": "SKU-0050",
"name": "Product-50",
"price": 10,
"stock": 60,
"category": "food"
}
ListByPrice のメソッドでは、sort.Slice が比較関数としてクロージャを使用しています。このクロージャは list 変数をキャプチャし、比較が行われるたびにそれを呼び出します。
10. その他の例題セット
▶ サンプル:マップリテラルによる初期化と make の使用とのパフォーマンス比較
package main
import (
"fmt"
"time"
)
func main() {
// Literal initialization
start1 := time.Now()
m1 := map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
"f": 6, "g": 7, "h": 8, "i": 9, "j": 10,
}
fmt.Printf("Literals: %v, %v\n", len(m1), time.Since(start1))
// make pre-allocation
start2 := time.Now()
m2 := make(map[string]int, 10)
m2["a"] = 1
m2["b"] = 2
m2["c"] = 3
m2["d"] = 4
m2["e"] = 5
m2["f"] = 6
m2["g"] = 7
m2["h"] = 8
m2["i"] = 9
m2["j"] = 10
fmt.Printf("make: %v, %v\n", len(m2), time.Since(start2))
}
出力:
Literals: 10, 1.2µs
make: 10, 850ns
▶ サンプル:構造体の値を受け取るメソッドと、ポインタを受け取るメソッド
package main
import "fmt"
type Counter struct {
Value int
}
// Value receiver: operates on a copy; does not affect the original
func (c Counter) IncrementValue() Counter {
c.Value++
return c
}
// Pointer receiver: directly modifies the original
func (c *Counter) IncrementPointer() {
c.Value++
}
func main() {
c := Counter{Value: 10}
// Value receiver: must use the return value
c = c.IncrementValue()
fmt.Printf("After value receiver: %d\n", c.Value) // 11
// Pointer receiver: direct modification
c.IncrementPointer()
fmt.Printf("After pointer receiver: %d\n", c.Value) // 12
}
出力:
After value receiver: 11
After pointer receiver: 12
▶ サンプル:map における並行処理の落とし穴の実演
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[string]int)
// Writing goroutine
go func() {
for i := 0; i < 1000; i++ {
m["key"] = i // write operation
}
}()
// Reading goroutine
go func() {
for i := 0; i < 1000; i++ {
_ = m["key"] // read operation
}
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Concurrent reads and writes may cause a panic (fatal error: concurrent map read and map write)")
}
出力(実行時にパニックが発生する可能性があります):
Concurrent reads and writes may cause a panic (fatal error: concurrent map read and map write)
sync.Map または sync.Mutex で保護する必要があります。これについては第16課で詳しく解説しています。
❓ よくある質問
reflect.DeepEqual() を使用してください。json:"name"に余分なスペースが入っている)や、引用符の種類が間違っている(二重引用符を使用する必要があります)などが挙げられます。sync.Map(第16課)を使用するか、ミューテックスを追加してください(第16課)。📖 まとめ
- マップは O(1) のキー・値の検索を可能にし、Go のコレクション型の核となるものです
- 「comma-ok」構文は、「ゼロ値」と「存在しないこと」を区別するため、マップクエリにおいて安全なアプローチとなります
- 構造体は、フィールドを用いてデータをまとめるほか、(継承に代わる手段として)ネストされた構成をサポートしている
- structタグはJSON/ORMのシリアライズにおいて重要な役割を果たします。最も一般的な形式は
json:"name"です。 - マップとスライスの選択:キーによる検索 → マップ;順序の維持/ソート → スライス;多くの場合、両方が組み合わせて使用される
- mapもstructも値セマンティクスで扱われますが、mapは参照型(基になるデータを共有する)であるのに対し、structは値型(引数として渡される際にコピーされる)です。
- 大きな構造体や、変更が必要な構造体にはポインタを使用しなければならない (
func (s *Struct) Method())
📝 練習問題
-
基本問題(難易度 ⭐):
map[string]intを使用して、あるテキスト中に各単語が何回出現するかを数えてください。入力として"the quick brown fox jumps over the lazy dog the"が与えられた場合、出力はmap[the:3 quick:1 brown:1 ...]となるようにしてください。 -
上級問題(難易度 ⭐⭐):
Student構造体 (Name string, Scores []int) を定義し、以下のメソッドを実装してください:(1) 平均スコアを計算するAverage() float64;(2) 成績(A/B/C/D/F)を返すGrade() string; (3) 少なくとも3人の生徒を対象にテストを行う。 -
チャレンジ問題(難易度 ⭐⭐⭐):連絡先リストアプリを実装してください。
map[string]Contactを使用して連絡先を保存します(Contactには名前、電話番号、メールアドレス、グループが含まれます)。(1) 追加、削除、検索機能、(2) グループ(家族/友人/仕事)によるフィルタリング機能を実装してください。(3) JSONファイルへのエクスポート(os.WriteFileを使用)。要件:完全なエラー処理+JSONタグ+テスト用連絡先を少なくとも10件。