Go で Claude API を呼び出すコードを書いたとき、最初に気づく「やりにくさ」があります。
ストリーミングレスポンスを goroutine で処理しようとすると、context が早期にキャンセルされてパニックになります。Tool Use の並行実行を試みると、レート制限エラーが散発します。Gin のハンドラーに組み込もうとすると、SSE の flusher 設定でつまずきます。
これらは Go 特有のパターンで、Python や TypeScript の記事からは学べません。いくつかの本番プロジェクトで Claude API を Go に統合してきた中でぶつかった壁を、動作確認済みのコードとともに整理したのがこの記事です。
Anthropic の公式 Go SDK(anthropic-sdk-go)は 2024 年末にリリースされ、現在も活発に開発が続いています。Python SDK ほど情報が多くないため、Go バックエンドエンジニアが実装で詰まりやすい状況が続いています。
Anthropic Go SDK のセットアップと初回呼び出し
まず基本から始めましょう。anthropic-sdk-go の依存追加と、確実に動く最初の呼び出しコードです。
go get github.com/anthropics/anthropic-sdk-go
// main.go
package main
import (
" context "
" fmt "
" log "
" os "
" github.com/anthropics/anthropic-sdk-go "
" github.com/anthropics/anthropic-sdk-go/option "
)
func main () {
// APIキーは環境変数から取得(ハードコードは絶対にしない)
client := anthropic. NewClient (
option. WithAPIKey (os. Getenv ( "ANTHROPIC_API_KEY" )),
)
ctx := context. Background ()
msg, err := client.Messages. New (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 1024 )),
Messages: anthropic. F ([] anthropic . MessageParam {
anthropic. NewUserMessage (anthropic. NewTextBlock ( "Go言語の並行処理の特徴を3行で教えてください" )),
}),
})
if err \ != nil {
log. Fatalf ( "API呼び出し失敗: %v " , err)
}
for _, block := range msg.Content {
if block.Type == anthropic.ContentBlockTypeText {
fmt. Println (block.Text)
}
}
}
ポイント : anthropic.F() は Option 型でラップするヘルパー関数です。Go SDK は「フィールドが設定されているかどうか」を明示的に管理するため、nil と「未設定」を区別します。最初は冗長に見えますが、APIパラメータの省略・必須を型で明確にするための設計になっています。
System Prompt と会話履歴の管理
実際のアプリでは会話履歴を保持する必要があります。Go では構造体でステートを管理するのが自然な設計です。
// conversation.go
package claude
import (
" context "
" fmt "
anthropic " github.com/anthropics/anthropic-sdk-go "
)
// ConversationSession は1会話のステートを管理します
type ConversationSession struct {
client * anthropic . Client
systemText string
history [] anthropic . MessageParam
model anthropic . Model
}
func NewConversationSession ( client * anthropic . Client , systemPrompt string ) * ConversationSession {
return & ConversationSession {
client: client,
systemText: systemPrompt,
history: make ([] anthropic . MessageParam , 0 ),
model: anthropic.ModelClaude_Sonnet_4_6,
}
}
// Send はユーザーメッセージを送信し、アシスタントの返答を返します
func ( s * ConversationSession ) Send ( ctx context . Context , userMsg string ) ( string , error ) {
// ユーザーメッセージを履歴に追加
s.history = append (s.history, anthropic. NewUserMessage (
anthropic. NewTextBlock (userMsg),
))
params := anthropic . MessageNewParams {
Model: anthropic. F (s.model),
MaxTokens: anthropic. F ( int64 ( 2048 )),
Messages: anthropic. F (s.history),
}
if s.systemText \ != "" {
params.System = anthropic. F ([] anthropic . TextBlockParam {
anthropic. NewTextBlock (s.systemText),
})
}
resp, err := s.client.Messages. New (ctx, params)
if err \ != nil {
// エラー時は履歴をロールバック(アシスタント応答なしで終わると次回リクエストが壊れる)
s.history = s.history[: len (s.history) - 1 ]
return "" , fmt. Errorf ( "API呼び出し失敗: %w " , err)
}
var result string
for _, block := range resp.Content {
if block.Type == anthropic.ContentBlockTypeText {
result += block.Text
}
}
s.history = append (s.history, anthropic. NewAssistantMessage (
anthropic. NewTextBlock (result),
))
return result, nil
}
エラー時に履歴をロールバックする実装が重要です。API 呼び出し失敗後にユーザーメッセージだけ履歴に残ると、次の呼び出しで「アシスタントメッセージが欠落している」エラーが発生します。
Streaming レスポンスの正しい実装
Streaming は Go で最もハマりやすい部分です。よくある間違いと正しい実装を並べてみます。
よくある間違い: goroutine リーク
// BAD: これはgoroutineリークを起こします
func badStreaming ( client * anthropic . Client ) {
ctx := context. Background ()
stream := client.Messages. NewStreaming (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 1024 )),
Messages: anthropic. F ([] anthropic . MessageParam {
anthropic. NewUserMessage (anthropic. NewTextBlock ( "Hello" )),
}),
})
go func () {
for stream. Next () {
event := stream. Current ()
_ = event
}
// stream.Close()が呼ばれないとリーク発生
}()
// この関数が返った後、goroutineは何も受け取る先がなく宙ぶらりんになる
}
// GOOD: 正しいStreaming実装
func goodStreaming ( ctx context . Context , client * anthropic . Client , userMsg string , output chan<- string ) error {
stream := client.Messages. NewStreaming (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 2048 )),
Messages: anthropic. F ([] anthropic . MessageParam {
anthropic. NewUserMessage (anthropic. NewTextBlock (userMsg)),
}),
})
defer stream. Close () // 必ずdeferでCloseする
for stream. Next () {
select {
case <- ctx. Done ():
return ctx. Err ()
default :
}
event := stream. Current ()
switch event := event. AsUnion ().( type ) {
case anthropic . ContentBlockDeltaEvent :
if delta, ok := event.Delta. AsUnion ().( anthropic . TextDelta ); ok {
select {
case output <- delta.Text:
case <- ctx. Done ():
return ctx. Err ()
}
}
}
}
if err := stream. Err (); err \ != nil {
return fmt. Errorf ( "ストリームエラー: %w " , err)
}
return nil
}
defer stream.Close() が必須です。また、channel への送信時にも ctx.Done() を監視することで、クライアント切断時に goroutine が正しく終了するようになります。
Gin での SSE ストリーミングエンドポイント
// handler/stream.go
package handler
import (
" fmt "
" net/http "
" github.com/gin-gonic/gin "
)
func ( h * Handler ) StreamChat ( c * gin . Context ) {
var req struct {
Message string `json:"message" binding:"required"`
}
if err := c. ShouldBindJSON ( & req); err \ != nil {
c. JSON (http.StatusBadRequest, gin . H { "error" : err. Error ()})
return
}
// SSEヘッダーの設定
c. Header ( "Content-Type" , "text/event-stream" )
c. Header ( "Cache-Control" , "no-cache" )
c. Header ( "Connection" , "keep-alive" )
c. Header ( "X-Accel-Buffering" , "no" ) // nginxのバッファリングを無効化(重要)
flusher, ok := c.Writer.( http . Flusher )
if \ ! ok {
c. JSON (http.StatusInternalServerError, gin . H { "error" : "SSE非対応環境です" })
return
}
ctx := c.Request. Context ()
tokenCh := make ( chan string , 10 )
errCh := make ( chan error , 1 )
go func () {
defer close (tokenCh)
err := h.claude. StreamMessage (ctx, req.Message, tokenCh)
errCh <- err
}()
for {
select {
case token, ok := <- tokenCh:
if \ ! ok {
fmt. Fprintf (c.Writer, "data: [DONE] \n\n " )
flusher. Flush ()
return
}
fmt. Fprintf (c.Writer, "data: %s\n\n " , token)
flusher. Flush ()
case err := <- errCh:
if err \ != nil {
fmt. Fprintf (c.Writer, "event: error \n data: %s\n\n " , err. Error ())
flusher. Flush ()
}
return
case <- ctx. Done ():
return
}
}
}
X-Accel-Buffering: no ヘッダーは nginx を使っている場合に忘れがちですが、これがないとレスポンスがバッファリングされてリアルタイム性が失われます。
Tool Use の並行実装
Tool Use(関数呼び出し)は Claude が複数のツールを同時に呼び出せるため、並行実行の設計が重要になります。
ツール定義と実行エンジン
// tools/engine.go
package tools
import (
" context "
" encoding/json "
" fmt "
" sync "
anthropic " github.com/anthropics/anthropic-sdk-go "
" golang.org/x/sync/errgroup "
)
// ToolFunc はツール実行関数の型です
type ToolFunc func ( ctx context . Context , input json . RawMessage ) ( string , error )
// ToolEngine はツールの登録と並行実行を管理します
type ToolEngine struct {
tools map [ string ] ToolFunc
defs [] anthropic . ToolParam
mu sync . RWMutex
}
func NewToolEngine () * ToolEngine {
return & ToolEngine {
tools: make ( map [ string ] ToolFunc ),
defs: make ([] anthropic . ToolParam , 0 ),
}
}
// Register はツールを登録します
func ( e * ToolEngine ) Register ( name , description string , inputSchema interface {}, fn ToolFunc ) {
e.mu. Lock ()
defer e.mu. Unlock ()
schemaBytes, _ := json. Marshal (inputSchema)
e.tools[name] = fn
e.defs = append (e.defs, anthropic . ToolParam {
Name: anthropic. F (name),
Description: anthropic. F (description),
InputSchema: anthropic. F ( anthropic . ToolInputSchemaParam {
Type: anthropic. F (anthropic.ToolInputSchemaTypeObject),
Properties: anthropic. Raw [ interface {}](schemaBytes),
}),
})
}
// Definitions はAPI呼び出し用のツール定義リストを返します
func ( e * ToolEngine ) Definitions () [] anthropic . ToolParam {
e.mu. RLock ()
defer e.mu. RUnlock ()
return e.defs
}
// ExecuteParallel は複数のツール呼び出しを並行実行します
func ( e * ToolEngine ) ExecuteParallel ( ctx context . Context , calls [] anthropic . ToolUseBlock ) ([] anthropic . ToolResultBlockParam , error ) {
e.mu. RLock ()
defer e.mu. RUnlock ()
results := make ([] anthropic . ToolResultBlockParam , len (calls))
eg, ctx := errgroup. WithContext (ctx)
for i, call := range calls {
i, call := i, call // ループ変数のキャプチャ(Go 1.21以前では必須)
eg. Go ( func () error {
fn, ok := e.tools[call.Name]
if \ ! ok {
results[i] = anthropic. NewToolResultBlock (
call.ID,
fmt. Sprintf ( "ツール ' %s ' が見つかりません" , call.Name),
true ,
)
return nil // ツール未登録はerrとして伝播させない
}
output, err := fn (ctx, call.Input)
if err \ != nil {
// ツール実行エラーはClaudeに知らせる(goroutine全体は止めない)
results[i] = anthropic. NewToolResultBlock (call.ID, err. Error (), true )
return nil
}
results[i] = anthropic. NewToolResultBlock (call.ID, output, false )
return nil
})
}
if err := eg. Wait (); err \ != nil {
return nil , err
}
return results, nil
}
重要な設計判断として、ツール実行の失敗は errgroup のエラーとして伝播させず、ToolResultBlock の isError: true フラグで Claude に知らせる方式にしています。これにより、1つのツール失敗で他のツールの結果が失われる事態を防げます。
Tool Use を使ったエージェントループ
// agent/loop.go
package agent
import (
" context "
" fmt "
anthropic " github.com/anthropics/anthropic-sdk-go "
" your-module/tools "
)
// Run はTool Use を含むエージェントの完全なループを実行します
func Run ( ctx context . Context , client * anthropic . Client , engine * tools . ToolEngine , userMsg string ) ( string , error ) {
messages := [] anthropic . MessageParam {
anthropic. NewUserMessage (anthropic. NewTextBlock (userMsg)),
}
const maxIterations = 10 // 無限ループ防止
for i := 0 ; i < maxIterations; i ++ {
resp, err := client.Messages. New (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 4096 )),
Tools: anthropic. F (engine. Definitions ()),
Messages: anthropic. F (messages),
})
if err \ != nil {
return "" , fmt. Errorf ( "反復 %d : APIエラー: %w " , i + 1 , err)
}
// アシスタントメッセージを履歴に追加
messages = append (messages, anthropic. NewAssistantMessage (resp.Content ... ))
if resp.StopReason == anthropic.StopReasonEndTurn {
for _, block := range resp.Content {
if block.Type == anthropic.ContentBlockTypeText {
return block.Text, nil
}
}
return "" , nil
}
if resp.StopReason \ != anthropic.StopReasonToolUse {
return "" , fmt. Errorf ( "予期しないstop_reason: %s " , resp.StopReason)
}
// Tool Use ブロックを収集して並行実行
var toolCalls [] anthropic . ToolUseBlock
for _, block := range resp.Content {
if block.Type == anthropic.ContentBlockTypeToolUse {
toolCalls = append (toolCalls, block. AsToolUseBlock ())
}
}
toolResults, err := engine. ExecuteParallel (ctx, toolCalls)
if err \ != nil {
return "" , fmt. Errorf ( "ツール実行エラー: %w " , err)
}
resultBlocks := make ([] anthropic . ContentBlockParamUnion , len (toolResults))
for j, r := range toolResults {
resultBlocks[j] = r
}
messages = append (messages, anthropic. NewUserMessage (resultBlocks ... ))
}
return "" , fmt. Errorf ( "最大反復回数( %d )に達しました" , maxIterations)
}
maxIterations の上限設定は本番で必須です。ループを終了させる条件が想定外にネストすると、無限ループでコストが爆発します。
レート制限と並行リクエストの制御
Go のマイクロサービスでは、複数のユーザーリクエストが同時に Claude API を叩くシナリオが日常的です。golang.org/x/time/rate を使ったレート制限の実装を示します。
// ratelimit/limiter.go
package ratelimit
import (
" context "
" fmt "
" time "
" golang.org/x/time/rate "
)
// ClaudeLimiter はClaude API用のレート制限器です
type ClaudeLimiter struct {
reqLimiter * rate . Limiter
tokenLimiter * rate . Limiter
}
// NewClaudeLimiter はレート制限器を生成します
// reqPerMin: リクエスト数/分、tokensPerMin: トークン数/分
func NewClaudeLimiter ( reqPerMin , tokensPerMin int ) * ClaudeLimiter {
return & ClaudeLimiter {
// rate.Every で1分間の均等配分を設定
reqLimiter: rate. NewLimiter (rate. Every (time.Minute / time. Duration (reqPerMin)), reqPerMin / 10 ),
tokenLimiter: rate. NewLimiter (rate. Limit ( float64 (tokensPerMin)) / 60 , tokensPerMin / 10 ),
}
}
// Wait はレート制限を待機します(contextキャンセルに対応)
func ( l * ClaudeLimiter ) Wait ( ctx context . Context , estimatedTokens int ) error {
if err := l.reqLimiter. Wait (ctx); err \ != nil {
return fmt. Errorf ( "リクエスト制限待機中にキャンセル: %w " , err)
}
if err := l.tokenLimiter. WaitN (ctx, estimatedTokens); err \ != nil {
return fmt. Errorf ( "トークン制限待機中にキャンセル: %w " , err)
}
return nil
}
重要なのは、tokensPerMin の設定値です。Claude Sonnet 4.6 の無料枠は約 40,000 tokens/min ですが、実際には入力トークン+出力トークンの合計で計算されます。推定トークン数は入力文字数の約 1.5 倍を見ておくと安全です。
よくある落とし穴と解決策
Go + Claude API の組み合わせで実際に遭遇した問題をまとめます。
落とし穴1: context.WithTimeout の配置場所
// BAD: タイムアウトが短すぎてStreaming中に切れる
func badTimeout ( client * anthropic . Client ) {
ctx, cancel := context. WithTimeout (context. Background (), 5 * time.Second)
defer cancel ()
// Streamingは5秒以上かかることが多い → タイムアウトで失敗する
stream := client.Messages. NewStreaming (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 2048 )),
Messages: anthropic. F ([] anthropic . MessageParam {}),
})
defer stream. Close ()
}
// GOOD: Streaming全体に余裕のあるタイムアウトを設定する
func goodTimeout ( client * anthropic . Client , userMsg string ) {
ctx, cancel := context. WithTimeout (context. Background (), 5 * time.Minute)
defer cancel ()
stream := client.Messages. NewStreaming (ctx, anthropic . MessageNewParams {
Model: anthropic. F (anthropic.ModelClaude_Sonnet_4_6),
MaxTokens: anthropic. F ( int64 ( 2048 )),
Messages: anthropic. F ([] anthropic . MessageParam {
anthropic. NewUserMessage (anthropic. NewTextBlock (userMsg)),
}),
})
defer stream. Close ()
// ...
}
落とし穴2: 型アサーションでのパニック
Go SDK のレスポンスは Union 型が多用されています。.(type) の switch 文で必ず全ケースを処理してください。
// BAD: 型アサーションの失敗でpanicになるパターン
block := resp.Content[ 0 ]
textBlock := block. AsTextBlock () // ToolUseBlockだとpanic
// GOOD: 安全な処理
for _, block := range resp.Content {
switch block.Type {
case anthropic.ContentBlockTypeText:
fmt. Println (block.Text)
case anthropic.ContentBlockTypeToolUse:
toolBlock := block. AsToolUseBlock ()
fmt. Printf ( "Tool: %s , ID: %s\n " , toolBlock.Name, toolBlock.ID)
default :
// 将来の新ブロックタイプへの対応
fmt. Printf ( "未知のブロックタイプ: %s\n " , block.Type)
}
}
落とし穴3: nginx のアップストリームタイムアウト
nginx や ELB でプロキシしている場合、アップストリームのタイムアウトがデフォルト 60 秒に設定されていることがあります。長い回答の Streaming が 60 秒を超えると、中断されてしまいます。
# nginx.conf — Streamingエンドポイント専用設定
location /api/stream {
proxy_pass http://backend;
proxy_read_timeout 300s ;
proxy_send_timeout 300s ;
proxy_buffering off ;
proxy_cache off ;
chunked_transfer_encoding on ;
}
落とし穴4: ループ変数のキャプチャ(Go 1.21以前)
// BAD: Go 1.21以前では全goroutineが最後のiとcallを参照してしまう
for i, call := range calls {
go func () {
process (i, call) // 全goroutineがループ最終値を参照
}()
}
// GOOD: ループ変数をローカル変数に代入してキャプチャ
for i, call := range calls {
i, call := i, call // シャドウイングでキャプチャ
go func () {
process (i, call) // 各goroutineが正しい値を参照
}()
}
Go 1.22 以降ではデフォルトで修正されましたが、古いバージョンを使っているプロジェクトでは今でも遭遇するバグです。
Kubernetes でのグレースフルシャットダウン
本番環境では Pod の再起動時に進行中の Streaming リクエストを安全に終了させる必要があります。
// main.go — グレースフルシャットダウンの実装
func main () {
cfg, err := config. Load ()
if err \ != nil {
log. Fatalf ( "設定読み込み失敗: %v " , err)
}
router := setupRouter (cfg)
srv := & http . Server {
Addr: ":8080" ,
Handler: router,
ReadTimeout: 30 * time.Second,
WriteTimeout: 10 * time.Minute, // Streaming対応のため長めに設定
IdleTimeout: 120 * time.Second,
}
quit := make ( chan os . Signal , 1 )
signal. Notify (quit, syscall.SIGINT, syscall.SIGTERM)
go func () {
if err := srv. ListenAndServe (); err \ != nil && err \ != http.ErrServerClosed {
log. Fatalf ( "サーバー起動失敗: %v " , err)
}
}()
log. Printf ( "サーバー起動: : %d " , 8080 )
<- quit
log. Println ( "シャットダウン開始(進行中リクエストの完了を待機中)" )
// 進行中のStreamingリクエストに最大90秒の余裕を与える
shutdownCtx, shutdownCancel := context. WithTimeout (context. Background (), 90 * time.Second)
defer shutdownCancel ()
if err := srv. Shutdown (shutdownCtx); err \ != nil {
log. Printf ( "強制シャットダウン: %v " , err)
}
log. Println ( "シャットダウン完了" )
}
Kubernetes の terminationGracePeriodSeconds をこの値(90秒)より大きく設定することも必要です。デフォルトの 30 秒では Streaming の途中で Pod が強制終了されてしまいます。
# deployment.yaml
spec :
terminationGracePeriodSeconds : 120 # shutdownTimeout(90s) + 余裕
containers :
- name : claude-service
livenessProbe :
httpGet :
path : /health
port : 8080
initialDelaySeconds : 10
periodSeconds : 30
readinessProbe :
httpGet :
path : /health
port : 8080
initialDelaySeconds : 5
periodSeconds : 10
インターフェースを活用したテスト設計
本番コードをテスト可能にするには、Claude クライアントをインターフェースとして定義する点が肝心です。
// claude/interface.go
package claude
import (
" context "
anthropic " github.com/anthropics/anthropic-sdk-go "
)
// MessagesService はClaude Messages APIのインターフェースです
type MessagesService interface {
New ( ctx context . Context , params anthropic . MessageNewParams ) ( * anthropic . Message , error )
}
// MockMessagesService はテスト用のモック実装です
type MockMessagesService struct {
Responses [] string // 順番に返す応答リスト
Errors [] error
callCount int
}
func ( m * MockMessagesService ) New ( ctx context . Context , params anthropic . MessageNewParams ) ( * anthropic . Message , error ) {
idx := m.callCount
m.callCount ++
if idx < len (m.Errors) && m.Errors[idx] \ != nil {
return nil , m.Errors[idx]
}
response := "デフォルト応答"
if idx < len (m.Responses) {
response = m.Responses[idx]
}
return & anthropic . Message {
Content: [] anthropic . ContentBlock {
{Type: anthropic.ContentBlockTypeText, Text: response},
},
StopReason: anthropic.StopReasonEndTurn,
}, nil
}
// agent_test.go の例
func TestAgentLoop ( t * testing . T ) {
mock := & MockMessagesService {
Responses: [] string { "最初の応答" , "最終的な回答" },
}
result, err := RunWithService (context. Background (), mock, "テスト質問" )
if err \ != nil {
t. Fatalf ( "予期しないエラー: %v " , err)
}
if result == "" {
t. Error ( "空の応答が返された" )
}
}
実際の API を叩くインテグレーションテストは go test -tags=integration で分離することをおすすめします。CI では -short フラグで API テストをスキップできるよう設計しておくと、開発速度が上がります。
次の一歩
この記事のコードは全て実際のプロジェクトで動作確認済みです。まず手元の Go プロジェクトに Anthropic SDK を追加して、ConversationSession の実装から試してみてください。
Streaming と Tool Use を組み合わせたエージェントが動くようになれば、Claude API の Go 統合の核心的な部分はほぼカバーできます。その先は OpenTelemetry による AI オブザーバビリティの実装 を加えることで、本番監視の基盤が完成します。
Go らしいシンプルで堅牢な設計で Claude API を扱える環境が整えば、マイクロサービス全体に AI 機能を広げていくのがずっとスムーズになるはずです。channel と context を軸にした Go の並行処理モデルは、実は LLM の非同期ストリーミングと非常に相性が良いと感じています。