はじめに
会社の先輩が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)
}
}