Go言語でContextを活用する3つのパターン

## はじめに

Go言語では`context`パッケージを利用して、タイムアウトやキャンセル処理を実装できます。
下記で`context`を活用する3つのパターンを紹介します。

## HTTPサーバーとHTTPクライアントのタイムアウトとキャンセル
`net/http`パッケージを使って、`HTTP`リクエストにタイムアウトを設定し、リクエスト処理中にキャンセルが発生した場合に適切に処理できるようにします。

### HTTPサーバー例
リクエストにタイムアウトを設定し、リクエスト処理中にキャンセルが発生した場合に適切に処理できるようにする`HTTP`サーバーの例です。

```go
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/example", handleRequest)
	http.ListenAndServe(":8080", nil)
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
	defer cancel()

	result := make(chan string)

	go func() {
		time.Sleep(3 * time.Second)
		result <- "Success"
	}()

	select {
	case res := <-result:
		fmt.Fprintln(w, res)
	case <-ctx.Done():
		w.WriteHeader(http.StatusRequestTimeout)
		fmt.Fprintln(w, "Request timed out")
	}
}
```

### HTTPクライアント例

`HTTP`クライアントでリクエストを送信する際に、タイムアウトを設定して、適切にキャンセル処理ができるようにする例です。

```go
gopackage main

import (
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	req, err := http.NewRequest("GET", "http://localhost:8080/example", nil)
	if err != nil {
		panic(err)
	}

	req = req.WithContext(ctx)

	client := &http.Client{}
	resp, err := client.Do(req)

	if err != nil {
		fmt.Println("Request error:", err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		panic(err)
	}

	fmt.Println("Response:", string(body))
}
```

## タスクのキャンセル

タスクが進行中にキャンセルが発生した場合、すぐにタスクを中止する例です。

```go
package main

import (
	"context"
	"fmt"
	"math/rand"
	"time"
)

func student(ctx context.Context, name string) {
	preparationTime := time.Duration(rand.Intn(5)+1) * time.Second
	fmt.Printf("%s は準備に %v かかります\n", name, preparationTime)

	select {
	case <-time.After(preparationTime):
		fmt.Printf("%s が準備を完了しました\n", name)
	case <-ctx.Done():
		fmt.Printf("%s は準備を中止しました\n", name)
	}
}

func main() {
	rand.Seed(time.Now().UnixNano())
	ctx, cancel := context.WithCancel(context.Background())

	names := []string{"Alice", "Bob", "Charlie", "David", "Eva"}
	for _, name := range names {
		go student(ctx, name)
	}

	time.Sleep(3 * time.Second)
	if rand.Intn(10) < 5 { // 50% の確率で悪天候
		fmt.Println("悪天候のためお祭りを中止")
		cancel()
	}

	time.Sleep(5 * time.Second)
}
```

### 最速のタスクの結果を使用する

複数のタスクを実行し、最初に完了したタスクの結果を受け取って、残りのタスクをキャンセルする例です。

```go
package main

import (
	"context"
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func performTask(ctx context.Context, taskID int, result chan<- int) {
	duration := time.Duration(rand.Intn(5)+1) * time.Second
	fmt.Printf("Task %d will take %v to complete\n", taskID, duration)

	select {
	case <-time.After(duration):
		select {
		case result <- taskID:
		case <-ctx.Done():
		}
	case <-ctx.Done():
		fmt.Printf("Task %d was cancelled\n", taskID)
	}
}

func main() {
	rand.Seed(time.Now().UnixNano())
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	result := make(chan int)
	var wg sync.WaitGroup

	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(taskID int) {
			defer wg.Done()
			performTask(ctx, taskID, result)
		}(i)
	}

	fastestTask := <-result
	fmt.Printf("Task %d completed first\n", fastestTask)

	cancel()
	wg.Wait()
}
```

これらのパターンは、タイムアウトやキャンセル処理が必要な様々なシーンで活用できます。contextをうまく使って、効率的で柔軟なプログラムを作成しましょう。

https://go.dev/blog/context

https://www.digitalocean.com/community/tutorials/how-to-use-contexts-in-go

https://go.dev/blog/context-and-structs