Go goroutineとchannel

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

使った技術やツール

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

# 構造体
```go
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関数
```go
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する
```go
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を使ってループ処理を並列化
```go
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
```go
	var wg sync.WaitGroup
	ch := make(chan struct{}, 10)
```
sync.WaitGroupはいくつかのgoroutineを管理するための値で、まずはこれを初期化しています。
次にchannel数の上限を初期化しています。これがgoroutineの上限となります。

## goroutine
```go
    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に通知
```go
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に通知させています。

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

```go
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)
	}
}

```