Goのマップと構造体
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) 線形スキャン
if p.SKU == sku {
return &p
}
}
return nil
}
10万個の製品 × 1,000 QPS = 1億回のトラバーサル/秒。CPU使用率が100%になるのは避けられません。
(2) Goでの解決策:O(1)の検索にはmapを使用する
// inventory.go
パッケージ main
import "fmt"
type Product struct {
SKU 文字列 // 商品の固有識別子
Name 文字列 // 商品名
Price float64 // 価格
Stock int // 在庫
Category 文字列 // Classification
}
// Using `map` to Create a Hash Table
type Inventory struct {
products map[文字列]Product // SKU -> Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[文字列]Product)}
}
// O(1) クエリ
func (inv *Inventory) Find(sku 文字列) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// O(1) update
func (inv *Inventory) UpdateStock(sku 文字列, delta int) エラー {
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 のクエリパフォーマンスの比較
| データサイズ | 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) // 空 map,書き込み可能
// Method 2: make + preallocated capacity
m2 := make(map[string]int, 100) // 事前割り当て 100 容量,容量の削減
// 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,できない m4["a"] = 1
_ = m4
}
(2) マップのCRUD
パッケージ main
import "fmt"
func main() {
ages := make(map[文字列]int)
// Create
ages["Alice"] = 28
ages["Bob"] = 32
// Read
fmt.Println(ages["Alice"]) // 28
// Update
ages["Alice"] = 29
// Delete
削除(ages, "Bob")
// Length
fmt.Printf("len=%d\n", len(ages))
fmt.Println(ages) // map[Alice:29]
}
出力:
28
len=1
map[Alice:29]
▶ サンプル:map iterate(順次、ランダム)
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)
}
// As long as 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) 「値がゼロ」と「存在しない」を区別する
パッケージ main
import "fmt"
func main() {
ages := map[文字列]int{"Alice": 28}
// 誤ったアプローチ:「キーが存在しない」ことと「値が 0」であることを区別できない
age := ages["Bob"]
fmt.Printf("Bob's age: %d\n", age) // 0(しかし Bob 本当だ 0 歳ですか?)
// Correct spelling: comma-ok
age, ok := ages["Bob"]
if ok {
fmt.Printf("Bob's age: %d\n", age)
} else {
fmt.Println("Bob not found")
}
// 存在確認だけを行う:値は破棄する
_, 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) comma-ok 一般モード
| 文脈 | 構文 | 意味 |
|---|---|---|
| 地図検索 | v, ok := m[key] |
キーは存在しますか? |
| 型のアサーション | v, ok := x.(T) |
型の一致? |
| チャンネルからの受信 | v, ok := <-ch |
チャンネルを閉じる? |
5. 構造体の基礎
(1) 構造体の定義と初期化
パッケージ main
import "fmt"
// define struct
type User struct {
Name 文字列
Age int
City 文字列
}
func main() {
// Method 1: By フィールド 順序 (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(に相当する (*p).Name)
p.Age = 30 // 自動的にデリファレンスされる
}
▶ サンプル:構造体のコピーとポインタの比較
パッケージ main
import "fmt"
type Counter struct {
Value int
}
// 値渡し:struct 全体がコピーされる
func incrementByValue(c Counter) {
c.Value++ // コピーを編集
fmt.Printf("Inside the 関数: %d\n", c.Value)
}
// Pointer Passing: Pass-by-Reference
func incrementByPointer(c *Counter) {
c.Value++ // 元のオブジェクトを修正する
fmt.Printf("Inside the 関数: %d\n", c.Value)
}
func main() {
c := Counter{Value: 10}
incrementByValue(c)
fmt.Printf("After 値 passing: %d\n", c.Value) // 10(変わらない)
incrementByPointer(&c)
fmt.Printf("After passing the pointer: %d\n", c.Value) // 11(修正済み)
}
出力:
関数内:11
値渡し後:10
関数内:11
ポインタの受け渡し後: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:"-"` // - 表す JSON 無視する
}
(2) JSONシリアライゼーションの実践
パッケージ main
import (
"encoding/json"
"fmt"
)
type User struct {
Name 文字列 `json:"name"`
Age int `json:"age"`
Email 文字列 `json:"email,omitempty"`
Password 文字列 `json:"-"` // パスワードはシリアライズされない
}
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"}
デシリアライズ: {Name:Bob Age:32 Email:bob@example.com Password:}
(3) よく使われるstructタグライブラリ
| ライブラリ | タグ名 | 用途 |
|---|---|---|
| encoding/json | json:"name" |
JSON フィールド name |
| 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 // ネスト 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) 匿名ネスト(フィールド昇格)
パッケージ main
import "fmt"
type Address struct {
City 文字列
Country 文字列
}
// Anonymous Nesting: Fields Are "Promoted"
type User struct {
Name 文字列
Age int
Address // に相当する Address Address,ただし、フィールド名は記述しない
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// Field promotion: Direct access, without using `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[複数の要素を保存する必要がある] --> B{ボタンを押して検索する必要があります?}
B -->|はい| C[で map<br/>O(1) 検索]
B -->|いいえ| D{順序を保証する必要がある?}
D -->|はい| E[で slice]
D -->|いいえ| F{元素の数 < 100?}
F -->|はい| G[で slice または map どちらでも構いません]
F -->|いいえ| C
(3) 実例:学生の成績管理
package main
import "fmt"
// Store student grades in a map (look up by student ID)
type GradeBook struct {
scores map[string]int // 学生番号 → 点数
}
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 score)
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
平均点: 81.33
map と slice を組み合わせて使用するのが一般的です。具体的には、map をクエリに、slice をソートや集計に使用します。
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"` // タグ:新商品/売れ筋/割引
}
// Inventory System
type Inventory struct {
products map[string]Product // SKU → Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// Add Item
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 Inventory
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"
}
sort.Slice はクロージャを比較関数として使用しています。このクロージャは list 変数をキャプチャし、各比較の際にそれを呼び出します。
10. その他の例題セット
▶ サンプル:map リテラルによる初期化と 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 preallocation
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))
}
出力:
リテラル: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 object
func (c Counter) IncrementValue() Counter {
c.Value++
return c
}
// Pointer receiver: Directly modifies the original object
func (c *Counter) IncrementPointer() {
c.Value++
}
func main() {
c := Counter{Value: 10}
// Value receiver: Must use a return value
c = c.IncrementValue()
fmt.Printf("After the value is received: %d\n", c.Value) // 11
// Pointer receiver: Direct modification
c.IncrementPointer()
fmt.Printf("After the pointer is received: %d\n", c.Value) // 12
}
出力:
受信者の後に:11
ポインタの受信後:12
▶ サンプル:mapにおける並行処理の落とし穴の実証
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[string]int)
// Writing a goroutine
go func() {
for i := 0; i < 1000; i++ {
m["key"] = i // 書き込み操作
}
}()
// Reading a goroutine
go func() {
for i := 0; i < 1000; i++ {
_ = m["key"] // 読み取り操作
}
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Concurrent reads and writes may cause a panic (fatal error: concurrent map read and map write)")
}
出力(実行時にパニックが発生する可能性があります):
同時読み書きの可能性 panic(fatal エラー: concurrent map read and map write)
sync.Map または sync.Mutex で保護する必要があります。これについては第16課で詳しく解説しています。
❓ よくある質問
map は参照型ですか?map変数はハッシュテーブルへのポインタであり、代入とパラメータ渡しはどちらも同じ基底となるハッシュテーブルを共有します。これはスライスに似ていますが、より根本的な違いがあります。スライスはappendを介して独立してサイズ変更をトリガーできますが、mapにはこの分離メカニズムがありません。reflect.DeepEqual() を使用してください。struct tag の形式が間違っているとどうなりますか?json:"name" に余分なスペースが入っている)や、引用符の種類が間違っている(二重引用符を使用する必要があります)などが挙げられます。sync.Map(第16課)を使用するか、ミューテックスを追加してください(第16課)。📖 まとめ
- マップは O(1) のキー・値の検索を可能にし、Go のコレクション型の核となるものです
- 「comma-ok」構文は、「ゼロ値」と「存在しないこと」を区別するため、マップクエリにおいて安全なアプローチとなります。
- フィールドを使用してデータを集約し、ネストされた構成(継承の代替手段として)をサポートする
struct tagは JSON/ORM のシリアライズにおいて重要な役割を果たしており、json:"name"が最も一般的な形式です- map と slice の選定:キーによる検索 → map;順序保持/ソート → slice;この2つはよく組み合わせて使用される
- 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件。



