blog

GolangでHTTPリクエストを同時に送信するための最良のテクニックを詳しく見る

Golang の世界では、HTTP リクエストを同時に送信することは、Web アプリケーションを最適化するための重要なスキルです。 この記事では、基本的な goroutine からチャネルや sync...

Mar 24, 2024 · 10 min. read


Golang の世界では、HTTP リクエストを同時に送信することは、Web アプリケーションを最適化するための重要なスキルです。 この記事では、基本的な goroutine からチャネルや sync.WaitGroup を含む高度なテクニックまで、これを実現するためのさまざまな方法を検討します。 同時実行環境でのパフォーマンスとエラー処理のベスト プラクティスを詳しく掘り下げて、Go アプリケーションの速度と信頼性を向上させる戦略を提供します。 Golang の同時 HTTP リクエストの世界に飛び込んでみましょう。

Goroutines の基本的な使用方法

Golang で同時実行性を実装する場合、最も簡単な方法はGoroutineを使用することです。 これらは Go の同時実行性の構成要素であり、関数を同時に実行するためのシンプルかつ強力な方法を提供します。

Goroutineの紹介

goroutine を開始するには、関数呼び出しの前に go キーワードを追加するだけです。 これにより、関数が goroutine として開始され、メイン プログラムが独立して実行を継続できるようになります。 タスクを開始して、完了を待たずに先に進むようなものです。

たとえば、HTTP リクエストを送信するシナリオを考えてみましょう。 通常、sendRequest() のような関数を呼び出し、プログラムはその関数が完了するまで待機します。 goroutine を使用すると、次の両方を行うことができます。

go sendRequest("http://example.com")

複数のリクエストの処理

URLのリストがあり、それぞれにHTTPリクエストを送信する必要がある場合では、 goroutine を使用しなければ、アプリケーションはこれらのリクエストを次々に送信することになり、非常に時間がかかります。 goroutine を使えば、ほぼ同時にリクエストを送ることができます:

urls := []string{"http://test.com", "http://test1.com", ...}  
for _, url := range urls {  
go sendRequest(url)  
}

このループは URL ごとに新しい goroutine を開始し、プログラムがすべてのリクエストを送信するのにかかる時間を大幅に短縮します。

同時HTTPリクエストのメソッド

ここのセクションでは、Go で HTTP リクエストを同時に処理するさまざまな方法について詳しく説明します。 各方法には独自の特徴があり、これらを理解することで、特定の目的に適した方法を選択することができます。

Goroutine のベーシック

GoでHTTPリクエストを同時に送信する最も簡単な方法は、Goランタイムが管理する軽量スレッドであるgoroutinesを使うことです。 基本的な例を示します:

requester := insrequester.NewRequester().Load()  

urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}  
for _, url := range urls {  
go requester.Get(insrequester.RequestEntity{Endpoint: url})  
}  

time.Sleep(time.Second) // goroutine 終了待ち

このアプローチは単純ですが、一度開始されたgoroutine を制御することはできません。この方法ではGetメソッドの戻り値を取得することはできません。すべてのgoroutine を待つために、しばらくsleepする必要があります。sleepを実行しても、すべてのgoroutine が完了したかどうかはまだわからない場合があります。

WaitGroup

基本的なゴルーチンを改善するために、sync.WaitGroup を使用して同期を改善できます。この関数によりgoroutine のコレクションの実行が完了するまで待機することができます。

requester := insrequester.NewRequester().Load()  
wg := sync.WaitGroup{}  

urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}  
wg.Add(len(urls))  

for _, url := range urls {  
go requester.Get(insrequester.RequestEntity{Endpoint: url})  
}  

wg.Wait() // すべてのgoroutine が完了するまで待つ

これにより、main 関数はすべての HTTP リクエストが完了するまで待機するようになります。

Channels

Channels は、goroutine 間の通信のための Go の強力な機能です。これらは、複数の HTTP リクエストからデータを収集するために使用できます。

req := insrequester.NewRequester().Load()  

urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}  
channel := make(chan string, len(urls))  

for _, url := range urls {  
go func() {  
res, _ := req.Get(insrequester.RequestEntity{Endpoint: url})  
channel <- fmt.Sprintf("%s: %d", url, res.StatusCode)  
}()  
}  

for range urls {  
res := <-channel  
fmt.Println(res)  
}

チャネルはゴルーチンを同期するだけでなく、ゴルーチン間のデータ転送も容易にします。

Worker Pools

Worker Pool は、可変数のタスクを処理するために固定数のワーカー (ゴルーチン) が作成されるパターンです。これにより、同時 HTTP リクエストの数が制限され、リソースの枯渇を防ぐことができます。

続いて、ワーカープールを達成する方法は次のとおりです。

// URLフィールドを含むジョブ構造を定義する
type Work struct {
	target string
}

// ワーカー関数はジョブの処理に使用され、リクエスタ、ジョブ チャネル、結果チャネル、待機グループをパラメータとして受け取ります。
func worker(works <-chan Work, req *insrequester.Request, wg *sync.WaitGroup, output chan<- *http.Response) {
	for work := range works {
		// URLに対応するレスポンスを取得するためにrequesterを使用する
		res, _ := req.Get(insrequester.RequestEntity{Endpoint: job.target})
		// 結果を結果チャンネルに送信し、待機グループ数を減らす
		output <- res
		wg.Done()
	}
}

func main() {
	// リクエスターの作成とロード
	req := insrequester.NewRequester().Load()

	// 処理するURLのリストを定義する
	urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}
	// ワークプールの労働者数を定義する
	workerNum := 3

	// ジョブ・チャンネルと結果チャンネルの作成
	works := make(chan Work, len(urls))
	output := make(chan *http.Response, len(urls))
	var wg sync.WaitGroup

	// ワーカーを始動する
	for i := 0; i < workerNum; i++ {
		go worker(requester, works, output, &wg)
	}

	// ワーカープールにジョブを送る
	wg.Add(len(urls))
	for _, url := range urls {
		works <- Work{URL: url}
	}
	close(works)
	wg.Wait()

	// 結果の収集とエクスポート
	for i := 0; i < len(urls); i++ {
		fmt.Println(<-output)
	}
}

ワークプールを使用することで、大量の同時HTTPリクエストを効率的に管理できます。 ワークプールはスケーラブルなソリューションであり、ワークロードやシステム容量に応じてリソースの利用を最適化し、全体的なパフォーマンスを向上させます。

チャネルを使用してGoroutine を制限する

このメソッドは、チャネル作成セマフォのようなメカニズムを使用して、同時goroutine の数を制限します。これは、サーバーに負荷がかかることやレート制限に達することを避けるために HTTP リクエストを抑制する必要がある状況でうまく機能します。

その方法を紹介します:

// リクエスターを作成して構成をロードする
req := insrequester.NewRequester().Load()

// 処理する URL のリストを定義する
urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}
max := 3 // 同時リクエスト数の制限

// 同時リクエストを制限するチャンネルを作る
limit := make(chan struct{}, max)

// URLのリストを反復処理する
for _, url := range urls {
    limit <- struct{}{} // トークンを取得。 トークンが解除されるまでここで待つ
    go func(url string) {
        defer func() { <-limit }() // トークンを解放する
        // POSTリクエストを行う
        req.Post(insrequester.RequestEntity{Endpoint: url})
    }(url)
}

// すべてのゴルーチンが完了するまで待つ
for i := 0; i < cap(limit); i++ {
    limit <- struct{}{}
}

セマフォによるGoroutines の制限

sync/semaphoreパッケージは、同時に実行されるgoroutine の数を制限するクリーンで効率的な方法を提供します。 これは、リソースの割り当てをより体系的に管理したい場合に特に便利です。

// リクエスターを作成してコンフィグをロードする
req := insrequester.NewRequester().Load()

// 処理する URL のリストを定義する
urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}
max := 3 // 同時リクエスト数の制限

// 重み付きセマフォを作成する
sem := semaphore.NewWeighted(max)
ctx := context.Background()

// URLのリストを反復処理する
for _, url := range urls {
    // goroutine を開始する前にセマフォの重みを取得する。
    if err := sem.Acquire(ctx, 1); err != nil {
       fmt.Printf("セマフォを取得できません:%v\n", err)
       continue
    }

    go func(url string) {
       defer sem.Release(1) // 完了時にセマフォの重みを解放する
       // リクエスタを使用して URL に対応するレスポンスを取得する
       res, _ := req.Get(insrequester.RequestEntity{Endpoint: url})
       fmt.Printf("%s: %d\n", url, res.StatusCode)
    }(url)
}

// すべてのゴルーチンがセマフォの重みを解放するのを待つ
if err := sem.Acquire(ctx, max); err != nil {
    fmt.Printf("待機中にセマフォを取得できません:%v\n", err)
}

セマフォを使用するこのアプローチは、チャンネルを手動で管理するよりも構造的で読みやすい並行処理の方法を提供する。 これは、複雑な同期要件を扱う場合や、同時実行レベルをより細かく制御する必要がある場合に特に有用である。

では、最善のアプローチは何でしょうか?

Goで同時HTTPリクエストを処理するさまざまな方法を探った後、疑問が生じます。 ソフトウェアエンジニアリングではよくあることですが、答えはアプリケーションの特定の要件と制約に依存します。 最も適切なアプローチを決定する重要な要素について考えてみましょう:

ニーズを特定する

  • 大量のリクエストを処理する場合、ワークプールやセマフォベースのアプローチの方が、リソースの使用量をうまくコントロールできる。
  • 強力なエラー処理が重要な場合は、チャネルまたはセマフォ を使用すると、より構造化されたエラー管理が可能になります。
  • レート制限を守る必要があるアプリケーションでは、チャネルやセマフォのパケット制限goroutine を使うのが効果的かもしれません。
  • それぞれのアプローチの複雑さを考慮してください。チャネルはより詳細な制御を提供しますが、複雑さも増やします。一方、セマフォ パッケージは、より単純なソリューションを提供します。

エラー処理

Goの並行実行の性質上、goroutines でのエラー処理は厄介なトピックです。 goroutines は独立して実行されるため、エラーの管理と伝播は困難ですが、堅牢なアプリケーションの構築には不可欠です。 ここでは、Goの同時実行プログラムでエラーを効果的に処理するための戦略をいくつか紹介します:

集中エラーチャネル

func worker(errChan chan<- error) {
    // タスクを実行する
    if err := doWork(); err != nil {
        errChan <- err // エラー チャネルに送信する
    }
}

func main() {
    errChan := make(chan error, 1) // エラーを保存するためのバッファリングされたチャネル

    go worker(errChan)

    if err := <-errChan; err != nil {
        // エラーの処理
        log.Printf("エラー発生:%v", err)
    }
}

または、別のゴルーチンで errChan をリッスンすることもできます。

func worker(errChan chan<- error, work Work) {
 // タスクを実行する
 if err := doWork(work); err != nil {
  errChan <- err // エラー チャネルに送信する
 }
}

func listenErrors(done chan struct{}, errChan <-chan error) {
 for {
  select {
  case err := <-errChan:
   // エラーの処理
  case <-done:
   return
  }
 }
}

func main() {
 errChan := make(chan error, 1000) // エラーを保存するチャネル
 doneChan := make(chan struct{})       // goroutine の停止を通知するためのチャネル。

 go listenErrors(doneChan, errChan)

 for _, work := range works {
   go worker(errChan, work)
 }

 // すべてのゴルーチンの完了を待つ(正確な方法はコードによって実装する必要がある)
 doneChan <- struct{}{} // エラーのリッスンを停止するように goroutine に通知します。
}

Error Group

golang.org/x/sync/errgroup パッケージは、複数の goroutine をグループ化し、それらが生成するエラーを処理する便利な方法を提供します。 errgroup.Group は、ゴルーチンでエラーが発生すると、後続のすべての操作がキャンセルされることを確保します。

import "golang.org/x/sync/errgroup"

func main() {
    g, ctx := errgroup.WithContext(context.Background())

    urls := []string{"https://test1.com", "https://test2.com", "https://test3.com"}
    for _, url := range urls {
        // URLごとにgoroutine を開始する
        g.Go(func() error {
            // 実際のHTTPリクエストロジックに置き換え
            _, err := request(ctx, url)
            return err
        })
    }

    // すべてのリクエストが完了するまで待つ
    if err := g.Wait(); err != nil {
        log.Printf("エラー発生:%v", err)
    }
}

このアプローチにより、特に多数のゴルーチンを処理する場合のエラー処理が簡素化されます。

goroutine のラッピング

もう1つの方法は、エラーを処理する関数で各ゴルーチンをラップすることになります。 このラッピングには、パニックからの回復やその他のエラー管理ロジックを含めることができます。

func work() error {
  // タスクを実行する
  return err
}

func main() {
 go func() {
   err := work()
   if err != nil {
     // エラーの処理
   }
 }()

 // 作業が完了するのを待つ何らかの方法
}

まとめると、Go並行プログラミングにおけるエラー処理戦略の選択は、アプリケーションの特定の要件とコンテキストに依存します。 集中エラーチャネル、専用のエラー処理ゴルーチン、エラーグループの使用、エラー管理関数でのゴルーチンのラッピングなど、それぞれのアプローチには利点とトレードオフがあります。

まとめ

まとめると、この記事ではGolangでHTTPリクエストを同時に送信する様々な方法について説明しました。 基本的なゴルーチン、sync.WaitGroup、チャネル、ワークプール、ゴルーチンを制限する方法について説明しました。 それぞれのメソッドには独自の特徴があり、特定のアプリケーションの要件に基づいて選択することができます。

さらに、同時実行 Go プログラムにおけるエラー処理の重要性も強調されています。同時環境でのエラーの管理は困難な場合がありますが、堅牢なアプリケーションを構築するためには不可欠です。開発者が効率的にエラーを処理できるようにするために、一元化されたエラー チャネル、errgroup パッケージの使用、エラー処理ロジックでgoroutine をラップするなどの方法が議論されてきました。

最終的に、Goで同時HTTPリクエストを処理する最良の方法の選択は、リクエストのサイズ、エラー処理の要件、レートの制限、コードの全体的な複雑さと保守性などの要因に依存します。 開発者は、アプリケーションに並行処理を実装する際に、これらの要因を注意深く考慮する必要があります。

Read next

KingbaseES JSON_REMOVE関数のKingbaseデータベースへの使用方法の紹介

KingbaseES、Kingbaseのデータベース KES V9におけるJSON_REMOVE関数の使用方法の紹介 キーワード JSON_REMOVE関数の紹介 KES では、データベースの機能を拡張する仕組みとして拡張機能があります。拡張機能により、以下のことが可能になります。

Mar 19, 2024 · 4 min read