LoginSignup
2
1

More than 5 years have passed since last update.

Go goroutineとchannel

Last updated at Posted at 2019-04-06

はじめに

会社の先輩がGoの楽しさについて語っていました。
本当に楽しいのか試したいと思い、最近初めてGoを書いたのでそのことを書きます。
せっかくだったらおもしろい機能をつくりたかったので、
コマンドライン引数に指定したワードと画像枚数を元に、Bing Search v7 APIを叩いて、
slackに指定した枚数分の画像を表示させる処理を並列で動かす実装をしました。

使った技術やツール

  • GoLand(JetbrainsのIDE)
  • Go 1.12.1
  • Bing Search v7 API
  • Slack Incoming Webhook API

構造体

type BingJson struct {
    Type         string `json:"_type"`
    QueryContext struct {
        OriginalQuery string `json:"originalQuery"`
    } `json:"queryContext"`
    Value []struct {
        ContentUrl string `json:"contentUrl"`
    } `json:"value"`
}

Bing Search v7 APIを叩いて返ってきたjsonをparseするときの型を構造体を利用してこのように作成しました。他にも返ってくる値はありますが使わないので書いていません。

main関数

func main() {
    var searchWord string
    var count string
    flag.StringVar(&searchWord, "searchword", "", "Search word")
    flag.StringVar(&count, "count", "0", "Count")
    flag.Parse()
    searchWord = flag.Arg(0)
    count = flag.Arg(1)
    execApi(searchWord, count)
}

スクリプト実行時はmain関数から実行されるので、main関数を作成しました。
go run hoge.go -h を実行するとスクリプトの説明が出力されるようにしました。
コマンドライン引数にしていした二つの値を代入し、APIを叩く関数に移ります。

Bing Search v7 APIを叩いてparseする

func execApi(searchWord string, count string) {
    // Create new http request
    req, err := http.NewRequest("GET", bingEndpoint, nil)
    errorHandling(err)

    // Add get parameters
    params := req.URL.Query()
    params.Add("q", searchWord)
    params.Add("count", count)
    req.URL.RawQuery = params.Encode()

    // Add request header
    req.Header.Add(bingHeaderApiKey, bingApiKey)

    // Exec request with new http client
    client := new(http.Client)
    resp, err := client.Do(req)
    errorHandling(err)

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

    // Parse json
    bingJson := new(BingJson)
    err = json.Unmarshal(body, &bingJson)
    errorHandling(err)
}

func errorHandling(err error) {
    if err != nil {
        panic(err)
    }
}

それぞれコメントに記載してある処理をしています。
かなり久しぶりにポインタとアドレスについて復習しましたw

goroutineとchannelを使ってループ処理を並列化

func execApi(searchWord string, count string) {
    // ...

    // Post images to slack
    var wg sync.WaitGroup
    ch := make(chan struct{}, 10)
    for i, v := range bingJson.Value {
        wg.Add(1)
        ch <- struct{}{}
        go func(index int, url string) {
            defer func(){
                wg.Done()
                <- ch
            }()
            fmt.Printf("%d: %s ", index, url)
            postSlack(url)
        }(i, v.ContentUrl)
    }
    wg.Wait()
}

今回作った機能で一番こだわった処理なので、詳しく説明します。
goroutineの上限を10とし、slackに画像URLを飛ばす処理を並列で行うプログラムです。

sync.WaitGroupとchannel

    var wg sync.WaitGroup
    ch := make(chan struct{}, 10)

sync.WaitGroupはいくつかのgoroutineを管理するための値で、まずはこれを初期化しています。
次にchannel数の上限を初期化しています。これがgoroutineの上限となります。

goroutine

    for i, v := range bingJson.Value {
        wg.Add(1)
        ch <- struct{}{}
        go func(index int, url string) {
            defer func(){
                wg.Done()
                <- ch
            }()
            fmt.Printf("%d: %s ", index, url)
            postSlack(url)
        }(i, v.ContentUrl)
    }
    wg.Wait()

wg.Add(1)
for文の最初でAddすることで、使用するgoroutineをインクリメントしています。
ch <- struct{}{}
値を送信するための処理です。今回はstruct{}{}を送信しています。
go func(index int, url string)
関数の呼び出しgoをつけるだけで軽量のスレッド(goroutine)が立ち上がります。
defer func()
deferをつけることによって遅延実行させることができます。
「wg.Done」と「<- ch」を関数の一番最後に実行しています。
wg.Done
使用するgoroutineをデクリメントしています。
<- ch
受信したこと検知する処理です。

Slackに通知

func postSlack(text string) {
    // Create new http request
    data := url.Values{}
    data.Set("payload", "{\"text\": \""+text+"\"}")
    req, err := http.NewRequest("POST", slackWebhook, strings.NewReader(data.Encode()))
    errorHandling(err)

    // Set request header
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    // Exec request with new http client
    client := new(http.Client)
    resp, err := client.Do(req)
    errorHandling(err)
    fmt.Println(resp.Status)
}

引数のtextをIncoming Webhookを利用してSlackに通知させています。

ソースコード

全体のソースコードはこちらとなります

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
    "sync"
)

const (
    bingEndpoint     = "https://api.cognitive.microsoft.com/bing/v7.0/images/search"
    bingApiKey       = "bingApiKey"
    bingHeaderApiKey = "Ocp-Apim-Subscription-Key"
    slackWebhook     = "slackWebhook"
)

type BingJson struct {
    Type         string `json:"_type"`
    QueryContext struct {
        OriginalQuery string `json:"originalQuery"`
    } `json:"queryContext"`
    Value []struct {
        ContentUrl string `json:"contentUrl"`
    } `json:"value"`
}

func main() {
    var searchWord string
    var count string
    flag.StringVar(&searchWord, "searchword", "", "Search word")
    flag.StringVar(&count, "count", "0", "Count")
    flag.Parse()
    searchWord = flag.Arg(0)
    count = flag.Arg(1)
    execApi(searchWord, count)
}

func execApi(searchWord string, count string) {
    // Create new http request
    req, err := http.NewRequest("GET", bingEndpoint, nil)
    errorHandling(err)

    // Add get parameters
    params := req.URL.Query()
    params.Add("q", searchWord)
    params.Add("count", count)
    req.URL.RawQuery = params.Encode()

    // Add request header
    req.Header.Add(bingHeaderApiKey, bingApiKey)

    // Exec request with new http client
    client := new(http.Client)
    resp, err := client.Do(req)
    errorHandling(err)

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

    // Parse json
    bingJson := new(BingJson)
    err = json.Unmarshal(body, &bingJson)
    errorHandling(err)

    // Post images to slack
    var wg sync.WaitGroup
    ch := make(chan struct{}, 10)
    for i, v := range bingJson.Value {
        ch <- struct{}{}
        wg.Add(1)
        go func(index int, url string) {
            defer func() {
                <-ch
                wg.Done()
            }()
            fmt.Printf("%d: %s ", index, url)
            postSlack(url)
        }(i, v.ContentUrl)
    }
    wg.Wait()
}

func postSlack(text string) {
    // Create new http request
    data := url.Values{}
    data.Set("payload", "{\"text\": \""+text+"\"}")
    req, err := http.NewRequest("POST", slackWebhook, strings.NewReader(data.Encode()))
    errorHandling(err)

    // Set request header
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

    // Exec request with new http client
    client := new(http.Client)
    resp, err := client.Do(req)
    errorHandling(err)
    fmt.Println(resp.Status)
}

func errorHandling(err error) {
    if err != nil {
        panic(err)
    }
}

2
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
2
1