【Go】Goroutine使った並列処理サンプル - API呼び出し

# はじめに

はじめまして。Go初学者です。

関わっているプロダクト的にも、自身のスキル的にも、並列処理そのものもあまり書いてこなかったこともあり、使い方も使い所もいまいちわからなかったゴルーチンですが、真面目に勉強中です。
今回は試しにゴルーチンを利用してみたので、同じ境遇の方に向けて、実装例と共にご紹介させていただきます。

# 作ったもの

- Qiita APIを実行し、自分自身の投稿情報を取得していいね率等を表示するCLIツール

[githubはこちら](https://github.com/wataboru/aggregate-my-qiita) 

## 簡単な処理フロー

1. 実行引数にて、userIDとQiitaのアクセストークンを渡す
2. [ユーザーの記事一覧をAPI](https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems)で取得
3. 各記事ごとに、[記事単体を取得するAPI](https://qiita.com/api/v2/docs#get-apiv2itemsitem_id)を叩く
4. 必要な情報を標準出力に出力

## ゴルーチンの使い所

今回は、3の処理を直列に実行すると、記事の数だけ直列実行されるため、n+1の処理になってしまいます。
なので、3の処理は並列に実行することでパフォーマンスを上げてみました。

[n+1とは...?](https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361)

そもそも呼び出しの回数は変わってないのでn+1問題自体は解決していない気もしますが、
私の投稿数なんて大したことないので一旦置いておきます^q^

## ソースコードで解説

> 2. [ユーザーの記事一覧をAPI](https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems)で取得
> 3. 各記事ごとに、[記事単体を取得するAPI](https://qiita.com/api/v2/docs#get-apiv2itemsitem_id)を叩く

こちらについて、ソースコードを元にみていきます。

```aggregatemyqiita/aggregateMyQiita.go
func Aggregate(params Params) error {
	usersURL := "https://qiita.com/api/v2/users/" + params.UserID + "/items"

	client := client{
		token: params.Token,
	}

	res, err := client.request(usersURL)
	if err != nil {
		return err
	}

	var pageItems pageItems
	decodeBody(res, &pageItems)

	pageDetailItemCh := make(chan pageDetailItem, len(pageItems))

	go time.AfterFunc(time.Second*3, func() {
		close(pageDetailItemCh)
	})
	for _, item := range pageItems {
		go client.parallelRequest(pageDetailItemCh, "https://qiita.com/api/v2/items/"+item.ID)
	}

	for pageDetailItem := range pageDetailItemCh {
		fmt.Println("==========================================================")
		fmt.Println("ID: ", pageDetailItem.ID)
		fmt.Println("タイトル: ", pageDetailItem.Title)
		fmt.Printf("タグ: ")
		for _, tag := range pageDetailItem.Tags {
			fmt.Printf("%s, ", tag.Name)
		}
		fmt.Printf("%s", "\n")
		fmt.Println("いいね数: ", pageDetailItem.LikesCount)
		fmt.Println("閲覧数: ", pageDetailItem.PageViewsCount)
		fmt.Printf("いいね率:%.2f%%\n", pageDetailItem.likeRatio()*100)
	}
	fmt.Println("==========================================================")

	return nil
```

```aggregatemyqiita/client.go
func (client *client) request(url string) (*http.Response, error) {
	//fmt.Printf("[INFO]: %s\n", "Request to "+url)
	request, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return &http.Response{}, fmt.Errorf("[ERR] :%s", err)
	}

	request.Header.Set("Authorization", "Bearer "+client.token)

	res, err := client.do(request)
	if err != nil {
		return res, err
	}

	return res, nil
}

func (client *client) parallelRequest(pageDetailItemCh chan pageDetailItem, url string) error {
	//fmt.Printf("[INFO]: %s\n", "Request to "+url)
	request, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return fmt.Errorf("[ERR] :%s", err)
	}

	request.Header.Set("Authorization", "Bearer "+client.token)

	res, err := client.do(request)
	if err != nil {
		return err
	}

	var pageDetailItem pageDetailItem
	decodeBody(res, &pageDetailItem)

	pageDetailItemCh <- pageDetailItem
	return nil
}
```

- > [ユーザーの記事一覧をAPI](https://qiita.com/api/v2/docs#get-apiv2usersuser_iditems)で取得
以下の部分にて、GETで該当のURLからJSONを取得し、structに変換してスライスで受け取っています。
ここは普通に一回呼び出すだけで良いので、並列処理は必要ありません。

```aggregatemyqiita/aggregateMyQiita.go
	usersURL := "https://qiita.com/api/v2/users/" + params.UserID + "/items"

	client := client{
		token: params.Token,
	}

	res, err := client.request(usersURL)
	if err != nil {
		return err
	}

	var pageItems pageItems
	decodeBody(res, &pageItems)
```

- > 各記事ごとに、[記事単体を取得するAPI](https://qiita.com/api/v2/docs#get-apiv2itemsitem_id)を叩く
ここからが本題です。まず、最初に記事数分のバッファを持ったチャネルを作成します。

```aggregatemyqiita/aggregateMyQiita.go
	pageDetailItemCh := make(chan pageDetailItem, len(pageItems))
```

その後、goキーワードの後に無名関数を呼び出し、記事数分、並列でリクエスト処理を呼び出します。
また、このままだとチャネルがクローズされないので、3秒でクローズされるように別のゴルーチンを利用する。

```aggregatemyqiita/aggregateMyQiita.go
	for _, item := range pageItems {
		go client.parallelRequest(pageDetailItemCh, "https://qiita.com/api/v2/items/"+item.ID)
	}
	go time.AfterFunc(time.Second*2, func() {
		close(pageDetailItemCh)
	})
```

この`parallelRequest`という関数は、以下の通り実行結果をチャネルに送信します。

```aggregatemyqiita/client.go
func (client *client) parallelRequest(pageDetailItemCh chan pageDetailItem, url string) error {
	//fmt.Printf("[INFO]: %s\n", "Request to "+url)
	request, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return fmt.Errorf("[ERR] :%s", err)
	}

	request.Header.Set("Authorization", "Bearer "+client.token)

	res, err := client.do(request)
	if err != nil {
		return err
	}

	var pageDetailItem pageDetailItem
	decodeBody(res, &pageDetailItem)

	pageDetailItemCh <- pageDetailItem
	return nil
}
```

チャネルに送信されたレスポンスについては、以下のrangeにて待ち受けます。

```aggregatemyqiita/aggregateMyQiita.go
	for pageDetailItem := range pageDetailItemCh {
		fmt.Println("==========================================================")
		fmt.Println("ID: ", pageDetailItem.ID)
		fmt.Println("タイトル: ", pageDetailItem.Title)
		fmt.Printf("タグ: ")
		for _, tag := range pageDetailItem.Tags {
			fmt.Printf("%s, ", tag.Name)
		}
		fmt.Printf("%s", "\n")
		fmt.Println("いいね数: ", pageDetailItem.LikesCount)
		fmt.Println("閲覧数: ", pageDetailItem.PageViewsCount)
		fmt.Printf("いいね率:%.2f%%\n", pageDetailItem.likeRatio()*100)
	}
```

# おわりに

結構勉強会などでも、「並列処理の利用するシーンってどんな時でしょう？」みたいな質問が上がっていて、
自分自身もまだまだわかりませんが、こんな使い方もあるかなと思い投稿させていただきました。
内容へのご指摘も、「こんなんあるよ」みたいなお話も大歓迎です。お待ちしています。

# 参考 

プログラミング言語Go完全入門（執筆時点で2020/07/31までの期間限定で無償で公開されてます！）
上記の資料については、直接URLを貼るのは憚られますので以下よりご参照ください！
[「プログラミング言語Go完全入門」の期間限定公開のお知らせ - Mercari Enfineering Blog](https://tech.mercari.com/entry/2020/03/17/120137)