Skip to content

Goの実行モデルと前提

Goの実行モデルと、実務で事故を防ぐための前提条件を詳しく解説します。

実行モデルとリソースの物理的制約

Section titled “実行モデルとリソースの物理的制約”

コンピュータ資源は有限であり、性能ではなく制約を前提に設計することが基本です。

CPU・メモリよりも先に枯渇するリソース:

  1. DB・外部APIのコネクション数

    • 接続プールの上限(例: SetMaxOpenConns(10)
    • 接続リークは数時間後にシステム全体を停止させる
  2. Goroutineのリーク

    • Goroutineが終了しないと、メモリが枯渇する
    • 数千から数百万のgoroutineがリークすると、システムがクラッシュする
  3. ファイル記述子

    • OSレベルの制限(通常1024〜65536)
    • ファイルやソケットを適切にクローズしないと枯渇
  4. Channelのデッドロック

    • Channelの送信と受信がブロックされると、Goroutineが停止する

実際の事故例:

10:00:00 - アプリケーション起動(Goroutine数: 10)
10:00:01 - リクエスト1受信(Goroutine1を開始、終了しない)
10:00:02 - リクエスト2受信(Goroutine2を開始、終了しない)
...
10:30:00 - リクエスト1000受信(Goroutine1000を開始、終了しない)
10:30:01 - メモリ使用量: 2GB
10:30:02 - OutOfMemoryError発生
10:30:03 - アプリケーションがクラッシュ

実行モデル:

Goプログラム
├─ メインgoroutine
├─ goroutine1(並行実行)
├─ goroutine2(並行実行)
└─ goroutine3(並行実行)

重要な特徴:

  1. Goroutine: 軽量スレッド、数千から数百万のgoroutineを実行可能(ただし、リークに注意)
  2. Channel: goroutine間の通信(デッドロックに注意)
  3. GOMAXPROCS: 並行実行されるgoroutineの数
  4. ガベージコレクション: 自動メモリ管理(ただし、参照が保持されている場合は動作しない)

Goのトランザクション管理:

// GORMでのトランザクション管理
func CreateOrder(orderData OrderData) (*Order, error) {
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// トランザクション内の処理
order := &Order{UserID: orderData.UserID, Amount: orderData.Amount}
if err := tx.Create(order).Error; err != nil {
tx.Rollback()
return nil, err
}
// 在庫を更新
for _, item := range orderData.Items {
if err := tx.Model(&Inventory{}).
Where("product_id = ?", item.ProductID).
Update("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
tx.Rollback()
return nil, err
}
}
if err := tx.Commit().Error; err != nil {
return nil, err
}
return order, nil
}

特徴:

  • 明示的なトランザクション境界: Begin()Commit()で明示
  • 手動ロールバック: エラー時に手動でロールバック
  • defer文: エラー時のロールバックを保証

他言語との比較:

// Java: 宣言的トランザクション管理
@Transactional
public Order createOrder(OrderData orderData) {
Order order = orderRepository.save(new Order(orderData));
return order;
}

Goの非同期処理:

// Goroutineを使用
func processOrder(orderID int64) {
go func() {
// 非同期処理
paymentService.ChargePayment(orderID)
}()
}

特徴:

  • 信頼できる非同期: Goroutineによる制御
  • エラーハンドリング: 手動で実装する必要がある
  • 再実行: 手動で実装する必要がある

他言語との比較:

// Node.js: Promise/async-await
async function processOrder(orderId: number) {
await paymentService.chargePayment(orderId);
}
環境特徴主なリスク
Serverless (Lambda/Vercel)短寿命・自動スケールコールドスタート、接続バースト、DBパンク、実行時間制限(Lambda: 15分、Vercel: 300秒)
常駐プロセス (Go HTTP Server)長寿命・安定動作Goroutineリーク、メモリリーク、接続リーク、Channelデッドロック

制約:

// ❌ 悪い例: Serverless環境で問題のあるコード
func Handler(w http.ResponseWriter, r *http.Request) {
// 問題: 長時間実行される可能性がある
// 問題: トランザクションが長時間保持される
// 問題: 接続プールが適切に管理されない
order := createOrder(orderData)
json.NewEncoder(w).Encode(order)
}

問題点:

  • 実行時間の制限: Lambdaは最大15分、Vercelは最大300秒
  • コールドスタート: Goの起動に時間がかかる(50-200ms)
  • メモリ制限: メモリ使用量に制限がある(Lambda: 128MB〜10GB)
  • 接続バースト: スケールアウト時に接続プールが急増し、DBがパンクする可能性

解決策:

// ✅ 良い例: Serverless環境に適したコード
func Handler(w http.ResponseWriter, r *http.Request) {
// 1. バリデーション(短時間)
if err := validateOrderData(orderData); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 2. 注文を作成(短時間)
order, err := createOrder(orderData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 3. 非同期処理をキューに投入
messageQueue.Send("order.created", map[string]interface{}{
"orderId": order.ID,
"amount": order.Amount,
})
// 4. 即座にレスポンスを返す
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"orderId": order.ID,
"status": "PROCESSING",
})
}

特徴:

// 常駐プロセス環境での実行
func main() {
http.HandleFunc("/orders", createOrderHandler)
http.ListenAndServe(":8080", nil)
}

メリット:

  • 長時間実行可能: 実行時間の制限がない
  • 接続プール: データベース接続プールを保持
  • Goroutine: 並行処理が可能

実装例:

// ✅ 良い例: 常駐プロセス環境に適したコード
func createOrderHandler(w http.ResponseWriter, r *http.Request) {
orderData := parseOrderData(r)
// トランザクション内で処理
order, err := createOrder(orderData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// バックグラウンド処理をGoroutineで実行
go func() {
paymentService.ChargePayment(order.ID, order.Amount)
}()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(order)
}

Goの実行モデルと前提のポイント:

  • リソースの物理的制約: CPU・メモリよりも先に枯渇するのは、DB接続数・Goroutineリーク・ファイル記述子・Channelデッドロック
  • Goroutine: 軽量スレッド、並行処理が可能(リークに注意)
  • トランザクション境界: Begin()Commit()で明示、手動ロールバック
  • 非同期処理: Goroutineによる制御、エラーハンドリングが必要
  • Serverless環境: 実行時間制限、コールドスタート、メモリ制限、接続バースト
  • 常駐プロセス環境: 長時間実行可能、接続プール、Goroutine(Goroutineリーク・接続リークに注意)

重要な原則: 性能ではなく制約を前提に設計する。Goroutineリークや接続リークは数時間後にシステム全体を停止させる。