はじめに
はじめまして。Go初学者です。
関わっているプロダクト的にも、自身のスキル的にも、並列処理そのものもあまり書いてこなかったこともあり、使い方も使い所もいまいちわからなかったゴルーチンですが、真面目に勉強中です。
今回は試しにゴルーチンを利用してみたので、同じ境遇の方に向けて、実装例と共にご紹介させていただきます。
作ったもの
- Qiita APIを実行し、自分自身の投稿情報を取得していいね率等を表示するCLIツール
簡単な処理フロー
- 実行引数にて、userIDとQiitaのアクセストークンを渡す
- ユーザーの記事一覧をAPIで取得
- 各記事ごとに、記事単体を取得するAPIを叩く
- 必要な情報を標準出力に出力
ゴルーチンの使い所
今回は、3の処理を直列に実行すると、記事の数だけ直列実行されるため、n+1の処理になってしまいます。
なので、3の処理は並列に実行することでパフォーマンスを上げてみました。
そもそも呼び出しの回数は変わってないのでn+1問題自体は解決していない気もしますが、
私の投稿数なんて大したことないので一旦置いておきます^q^
ソースコードで解説
- ユーザーの記事一覧をAPIで取得
- 各記事ごとに、記事単体を取得するAPIを叩く
こちらについて、ソースコードを元にみていきます。
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
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で取得
以下の部分にて、GETで該当のURLからJSONを取得し、structに変換してスライスで受け取っています。
ここは普通に一回呼び出すだけで良いので、並列処理は必要ありません。
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を叩く
ここからが本題です。まず、最初に記事数分のバッファを持ったチャネルを作成します。
pageDetailItemCh := make(chan pageDetailItem, len(pageItems))
その後、goキーワードの後に無名関数を呼び出し、記事数分、並列でリクエスト処理を呼び出します。
また、このままだとチャネルがクローズされないので、3秒でクローズされるように別のゴルーチンを利用する。
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
という関数は、以下の通り実行結果をチャネルに送信します。
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にて待ち受けます。
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