gRPC クライアント接続の新旧比較 – grpc.Dial と grpc.NewClient の違いと移行の経緯

## 概要
この記事では、gRPC クライアントの接続方法として利用されてきた `grpc.Dial` と、最近推奨される `grpc.NewClient` について、入門者向けに解説します。なぜ `grpc.Dial` が非推奨となったのか、その経緯と新しい API の使い方も含めて整理しました。

## 1. gRPC クライアント接続の基本

### grpc.Dial とは？

- **概要:**  
  従来の gRPC クライアント接続作成の標準APIです。  
  ```go
  conn, err := grpc.Dial("localhost:50051",
      grpc.WithTransportCredentials(insecure.NewCredentials()),
      grpc.WithBlock(), // （オプション：接続が確立するまでブロック）
  )
  ```
  このように使用し、即座に接続を確立（またはブロックして待つ）することができます。

- **特徴:**  
  - 同期的に接続を試みる（`grpc.WithBlock` を指定した場合）
  - デフォルトのスキームは、内部では `passthrough` が利用されることが多い
  - 多くの既存コードやチュートリアルで採用されています

### grpc.NewClient とは？

- **概要:**  
  最近導入された新しい gRPC クライアント接続作成の API です。  
  ```go
  conn, err := grpc.NewClient("dns:///localhost:50051",
      grpc.WithTransportCredentials(insecure.NewCredentials()),
  )
  if err != nil {
      // エラーハンドリング
  }
  // NewClient は接続を遅延開始するので、必要に応じて明示的に接続を開始
  conn.Connect()
  ```
  
- **特徴:**  
  - 非同期に接続を開始し、後から明示的に `Connect()` で接続を確立する設計になっています
  - デフォルトスキームが `dns` になっており、クラウド環境などでの名前解決を前提としています
  - 柔軟な接続管理が可能になり、将来の機能拡張や効率改善を見据えた API となっています

## 2. なぜ grpc.Dial は非推奨になったのか？

### 背景と経緯

1. **非同期接続の要求:**  
   従来の `grpc.Dial` は、`grpc.WithBlock` を指定すれば同期的に接続を試みるため、接続確立までブロックするという動作がありました。しかし、現代の多くの環境では、非同期に接続を開始し、必要なタイミングで接続状態を確認するほうが柔軟です。

2. **デフォルトスキームの変更:**  
   `grpc.Dial` は、デフォルトで `passthrough` スキームを利用するケースがありました。  
   一方、クラウド環境やコンテナ環境では、サービスディスカバリのために DNS を使うことが一般的になっているため、`grpc.NewClient` ではデフォルトスキームが `dns` に変更されています。  
   これにより、環境に合わせた接続の仕組みが提供されます。

3. **API の一貫性と将来性:**  
   gRPC チームは、より効率的で柔軟な接続管理を可能にするために、新しい API を導入しました。  
   将来的に接続の再試行や動的な接続管理の仕組みが強化されることを見据え、旧 API を段階的に非推奨とし、移行を促しています。

### 非推奨の警告

- 警告例:
  ```
  grpc.Dial is deprecated: use NewClient instead. Will be supported throughout 1.x.
  ```
  このように、将来的なサポートを前提に、新しい API への移行が推奨されています。

## 3. grpc.NewClient の使い方（入門者向けサンプル）

以下は、`grpc.NewClient` を使って gRPC サーバーに接続するサンプルコードです。  
※ サンプルコード内のターゲット URI や認証設定は適宜調整してください。

```go
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "github.com/yourusername/yourproject/proto/hello"
)

func main() {
    // NewClient の使用（デフォルトは dns スキーム）
    conn, err := grpc.NewClient("dns:///localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }
    defer conn.Close()

    // 明示的に接続開始（NewClient は接続を遅延開始するため）
    conn.Connect()

    // 接続状態を確認（例: 最大5秒待機）
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    for {
        state := conn.GetState().String()
        log.Printf("Current state: %s", state)
        if state == "READY" {
            break
        }
        if !conn.WaitForStateChange(ctx, conn.GetState()) {
            log.Fatalf("Connection failed to become READY within timeout")
        }
    }
    log.Println("Client connected successfully")

    // gRPC サービスクライアントの生成と RPC 呼び出し
    client := pb.NewGreeterClient(conn)
    res, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "Gopher"})
    if err != nil {
        log.Fatalf("RPC error: %v", err)
    }
    log.Printf("Server response: %s", res.Message)
}
```

### 補足説明

- **スキームの選択:**  
  上記の例では `dns:///localhost:50051` としています。  
  ローカル開発の場合、場合によっては `passthrough:///localhost:50051` の方がうまく動作することもあります

- **非同期接続:**  
  `grpc.NewClient` はすぐには接続しないので、`Connect()` を呼んで接続開始を促し、その後接続状態を監視しています。  
  これにより、接続が READY 状態になるまで待機できます

## 4. まとめ

- **grpc.Dial と grpc.NewClient の違い:**  
  - **grpc.Dial:** 従来の同期的な接続（必要に応じてブロック）をサポート。  
  - **grpc.NewClient:** 非同期に接続を開始し、柔軟な接続管理が可能。  
    - デフォルトスキームが `dns` になっているため、クラウド環境に適している。
- **なぜ grpc.Dial が非推奨になったのか:**  
  非同期接続の要件、デフォルトスキームの変更、将来的な API 拡張と一貫性のため、grpc チームはより柔軟な `grpc.NewClient` への移行を推奨しています。

これらのポイントを押さえて、今後のプロジェクトでは新しい API を使って、効率的で信頼性の高い gRPC クライアント接続を実現してください。

## 参考リンク
[gRPC Go のアンチパターン（英語）](https://github.com/grpc/grpc-go/blob/master/Documentation/anti-patterns.md)

---

この内容を参考に、ぜひ新しい API を試してみてください。質問や不明点があればコメントでお知らせください！