LoginSignup
7
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-06

はじめに

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

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

作ったもの

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

githubはこちら

簡単な処理フロー

  1. 実行引数にて、userIDとQiitaのアクセストークンを渡す
  2. ユーザーの記事一覧をAPIで取得
  3. 各記事ごとに、記事単体を取得するAPIを叩く
  4. 必要な情報を標準出力に出力

ゴルーチンの使い所

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

n+1とは...?

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

ソースコードで解説

  1. ユーザーの記事一覧をAPIで取得
  2. 各記事ごとに、記事単体を取得するAPIを叩く

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

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で取得 以下の部分にて、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を叩く ここからが本題です。まず、最初に記事数分のバッファを持ったチャネルを作成します。
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

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1