20200226のGoに関する記事は6件です。

GoでRace Conditionを発見する

goroutine を使う選択をしたときに、あなたはRace Conditionに注意しなければならない。

countを5000回加算するコード(直列処理)

main.go
package main

import (
    "fmt"
)

func main() {
    var count int

    for i := 0; i < 10000; i++ {
        count++
    }

    fmt.Printf("count: %d\n", count)
}

結果

$ go run ./main.go
count: 5000

愚直に並行処理にする

この変更はRace Conditionを引き起こし問題になる

 import (
    "fmt"
+   "sync"
 )

 func main() {
    var count int

+   var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
-       count++
+       wg.Add(1)
+       go func() {
+           defer wg.Done()
+           count++
+       }()
    }
+   wg.Wait()

    fmt.Printf("count: %d\n", count)
 }

Bad: countを5000回加算するコード(並行処理)

これではいけない

package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int

    var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            count++
        }()
    }
    wg.Wait()

    fmt.Printf("count: %d\n", count)
}

結果

5000よりも少なくなる。 それぞれの goroutine が count を読んだタイミングが異なるため。

$ go run ./main.go     
count: 4354

Race Conditionを発見する -race オプション

go run -race main.go のように -race オプションを付けるとRace Conditionを検出できる。このオプションは、 go testgo build でも有効。

-race をつければRace Conditionを検出し、レポートを出力することができるが、メモリ使用量が増したり、実行時間が増えたりするので注意が必要。ベンチマークテストや負荷試験をするときに、CIで回すことが推奨される。

次のようにどこで Race Condition が起きたかレポートしてくれる。

$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000ac008 by goroutine 8:
  main.main.func1()
      /Users/hogehoge/workspace/go-test/main.go:16 +0x6c

Previous write at 0x00c0000ac008 by goroutine 7:
  main.main.func1()
      /Users/hogehoge/workspace/go-test/main.go:16 +0x82

Goroutine 8 (running) created at:
  main.main()
      /Users/hogehoge/workspace/go-test/main.go:14 +0xe7

Goroutine 7 (finished) created at:
  main.main()
      /Users/hogehoge/workspace/go-test/main.go:14 +0xe7
==================
count: 5000
Found 1 data race(s)
exit status 66

排他制御を導入してRace Conditionが起こらないように変更する

 func main() {
    var count int
+   var mu sync.Mutex

    var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
+           mu.Lock()
+           defer mu.Unlock()
            count++
        }()
    }

Good: countを5000回加算するコード(並行処理)

main.go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var count int
    var mu sync.Mutex

    var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            defer mu.Unlock()
            count++
        }()
    }
    wg.Wait()

    fmt.Printf("count: %d\n", count)
}

結果

$ go run ./main5.go
count: 5000

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go get でプライベートリポジトリをダウンロードできない時

公開リポジトリはダウンロード出来るけど・・・

go get github.com/motemen/gore

プライベートリポジトリはダウンロードできない!

pi@raspberrypi:~ $ go get github.com/CreatorsLab/test-repository
# cd .; git clone -- https://github.com/CreatorsLab/test-repository /home/pi/go/src/github.com/CreatorsLab/test-repository
Cloning into '/home/pi/go/src/github.com/CreatorsLab/test-repository'...
remote: Invalid username or password.
fatal: Authentication failed for 'https://c61axxxxxxxxxxxxxxxxxxxxxxxxx:x-oauth-basic@github.com/CreatorsLab/test-repository/'
package github.com/CreatorsLab/test-repository: exit status 128

結論 c61aいらないわ

以下URLには
https://github.com/k8sp/sextant/issues/29

git config --global url."https://c61axxxxxxxxxxxxxxx:x-oauth-basic@github.com/".insteadOf "https://github.com/"

こうしろって書いてあるけど、これだと動かなかった。
でも、

git config --global url."https://xxxxxxxxxxxxxxx:x-oauth-basic@github.com/".insteadOf "https://github.com/"しろ

こうしたら動いた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プライベートリポジトリをgo getする方法。

結論

①トークンを発行してね。
参考サイト
https://qiita.com/kz800/items/497ec70bff3e555dacd0

②トークンを.gitconfigに書き込んでね

$ nano ~/.gitconfig
~/.gitconfig
[url "https://xxxxxxxxxxxxxxx:x-oauth-basic@github.com/"]
insteadOf = https://github.com/

以下エラーが発生した場合トークンが間違ってるかも

# cd .; git clone -- https://github.com/CreatorsLab/test_repo /home/pi/go/src/github.com/CreatorsLab/test_repo
Cloning into '/home/pi/go/src/github.com/CreatorsLab/test_repo'...
remote: Invalid username or password.
fatal: Authentication failed for 'https://c61adxxxxxxxxxxxxxxxxxxxxxxxx:x-oauth-basic@github.com/CreatorsLab/test_repo/'
package github.com/CreatorsLab/test_repo: exit status 128

参考サイト

https://github.com/k8sp/sextant/issues/29

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goのアプリケーションをテンプレートから自動生成するCLIツール

作成したもの


gogenerとは、テンプレートからGoのプロジェクトを自動生成するCLIツールです!
Chefのtemplateっぽく使えたらと思って作りました。

動作

test


モチベーション

新しくWebのAPIを作成する時に、
一から書くと時間かかって面倒臭いので、
テンプレートだけ作っておいて、いつでもプロジェクトを使えるようにしようとました

どんなテンプレートがあるの?

テンプレートの取得には、gogener自身が持っているテンプレートと、外部に設置しているテンプレートがあります。

今のところ、

local
- ryomak-app(ddd + clean architecture構成のWebAPI)
reomte
- ryomak/go-deep-util-example (GoでDeepLearningするexample)
- ryomak/grpc-vue-go-example (I/Fがgrpcで作成したGoとvueのWebサイト)
- ryomak/go-p2pchat (P2P型のgoのチャットCLIツール)

があります。


ryomak-app

クリーンアーキテクチャで作成しています。WebAPIを作成するときは、このパッケージを再利用しています。


ryomak/go-deep-util-example


go-deepというディープラーニングするライブラリを利用して、画像分類するプログラムです。
手っ取り早くGoでディープラーニングやってみたい人にオススメです。


ryomak/grpc-vue-go-example


スクリーンショット 2020-02-26 8.25.18.png

grpcを使ってみたくて、go(clean architecture)の構成でVue.jsのWebサイトを作りました。grpcのexampleで使えます。

GPSを取得して
参加したユーザの位置情報をレーダーのように表示します。
レーダーの実装で力尽きてしまい、見た目がまだまだなのと、部屋の管理がまだ上手くないので、こちら修正も別途していこうと思います。


ryomak/go-p2pchat

https://github.com/ryomak/go-p2pchat
スクリーンショット 2020-02-22 23.11.52.png

これはP2P型のチャットCLIです。
一度一つのノードに接続すれば、ブロックチェーンのように、他の接続ノードへ伝搬します。


gogenerの使い方

$ go get github.com/ryomak/gogener/cmd/gogener 
$ gogener create -app (アプリ名) -mod (module名) ryomak-app 

最後に

今回は初めてガッツリCLIを作ってみました。
Go言語はCLIが作りやすくていいですね。
それぞれのテンプレートの元になったプロジェクトの修正等あれば、プルリクお願いしますmm

余談

テンプレートはGithubPagesで公開すれば、CLIツールで使えるようになります。是非作ってプルリク投げて欲しいです。

方法は、https://ryomak.github.io/gogener/
にまとめていますが、例で説明します。


プロジェクトを作成

基本的には生成するアプリと同じディレクトリ構成でテンプレートを配置します。配置するテンプレートはyamlファイルで記述しておきます。このファイルをgogenerに登録しておくと、このyamlに従って、プロジェクトを作成してくれます。

app_template.yaml
name: "ryomak/go-p2pchat"
bg-file-path: "bg.txt"
templates:
  - "go.mod"
  - "util/userInput.go"
  - "util/util.go"
  - "Makefile"
  - "peer/util.go"
  - "peer/peer.go"
  - "README.md"
  - "chat.go"
  - "control/control.go"
  - "app_template.yaml"

テンプレートを生成

main.goを例に見てみます。

main.go
package main
import (
    "fmt"
)

func main() {
    fmt.Println("hello [[.AppName]]")
}

VueやAngularを含めたプロジェクトのテンプレートだと、{{}}で,かぶってしまうので、Delimsを[[]]に変更しています。
デフォルトでテンプレートで使えるパラメータは、

  • AppName
  • ModName
  • ToCamel
  • ToLowerCamel が使えます。

gogenerにテンプレートを追加

gogenerにプルリクを投げます。修正は一行だけです。

gogener/interal/recipe/remote/templates.go
var remoteMap = map[string]string{
    "go-deep-util-example": "https://ryomak.github.io/templates-for-gogener/go-deep-util/app_template.yaml",
    "grpc-vue-go-example":  "https://ryomak.github.io/templates-for-gogener/grpc-vue-example/app_template.yaml",
  + "exapp": "{github pages url}/app_template.yaml"
}

これで以上です。

最後に

今回、初めてしっかりしたCLIを作ってみました。
すぐにAPPを作成したい時は、使えると思います。

個人的な学びとしては、CLI作成において、Github Actionで、CLIのリリースまで行いました。普段はCircleCIを用いていたのですが、GithubActionが便利で、乗り換えて行こうと思いました。
CLIのリリースにはgorelaserが簡単でした!下の記事をみたらすぐできます!
https://tellme.tokyo/post/2020/02/04/release-go-cli-tool/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ログのクラスタリング分析の処理をGoで実装し、Cloud RunとPub/Subを使ってサーバレスで処理できる仕組みを作る

先日、Firebase + GAEを使って実装したサービス「LogCrow」に新しい機能として、ログの登録を簡略化するために、ログファイルをアップロードして、ログの各行をクラスタリングし、類似ログ同士をまとめあげた上で、ログ登録できる機能を追加しました。

LogCrowのAnalyzeメニューにアクセスすると、ファイルをアップロードできるフォームがあります。ここに分析にかけたいログファイルをアップロードして実行すると分析処理がサーバサイドで走ります。

AnalyzePage.png

AnalyzePage2.png

AnalyzePage3.png

分析には多少時間がかかります。
分析が完了すると、分析結果を以下のような感じで確認できます。

AnalyzePage4.png

類似ログがまとめ上げられているので、その内容を眺め、ログの原因と対策の情報を記録しておきたいものを選択し、「Add log」を実行することで、このログメッセージをベースにした新規ログ登録画面に遷移します。

前置きが長くなりましたが、この機能の裏側でどのようなことが行われているのかを基盤のアーキテクチャの視点と分析処理の視点で紹介します。

この仕組みの裏側の流れ(基盤アーキテクチャ)

まずはこの処理を実行する基盤側の流れです。基本的に全てサーバレスで実装です。
裏側の仕組みとしてはこちらの記事で紹介したような感じで以下図の流れになります。

CloudRun_CloudPubSub.png

  1. フロントエンドのVue.jsからファイルをアップロード
  2. バックエンドのGAEのAPI側でファイルを受け取りCloud Storage上に一時的にファイルをアップロード
  3. Storageにファイルアップロードが完了したことをトリガーとしてCloud Functionsを実行
  4. Cloud Functionsの関数処理の中でCloud Pub/SubにメッセージをPush
  5. Pub/SubのSubscriberとして登録されているCloud Runを実行
  6. Cloud Runでは、Cloud Storageにアップロードされているファイルを読み込み分析処理を実行
  7. 実行結果はFirestoreに保存
  8. フロントエンドからは7.で登録された結果を確認

Cloud Runを採用した理由

当初はCloud Functionsで実行しようと考えていました。しかし、後述のログのクラスタリング分析処理をGo言語で実装しており、かつGoの1.12のバージョン以上でないと動かないライブラリに依存した実装にしていたため、Cloud FunctionsのGoランタイムの言語制約の1.11のバージョンに合致せず、コンテナベースで自由に動かせるCloud Runで実行することにしました。

Cloud Functions -> Pub/Sub -> Cloud Runの流れを採用した理由

Cloud Functionsの場合は様々なトリガーに対応しているのですが、Cloud Runは実行トリガーが以下のみ対応となります。
- HTTPSリクエストによる実行
- Pub/Subからのメッセージによる実行
- Cloud Schedulerによるスケジュール実行
- Cloud Tasksを使った実行
- Webhookによる呼び出しによる実行

Storageへのアップロード完了を持って実行できるようにすることが最もリアルタイムに処理を後続にすすめることができると考えたので、Storageへの書き込み完了をトリガーとしてキックできるCloud Functions側の処理を間に挟むことにしました。Cloud Functionsからは上記のいずれかの方法で実行すれば良いので直接HTTPSリクエストをCloud Run宛に投げても実行できるのですが、認証情報の引き渡しのことや、連携失敗時の処理再実行のことなどを考慮し、Pub/Subを間に挟み、サービス間連携させるようにしました。
Cloud Runの呼び出しを認証済みの内部のサービスからのみ許可する仕組みにしたかったため、Cloud FunctionsからPub/Subを経由して呼び出すようにしました。

この仕組みの裏側の流れ(分析アルゴリズム)

次にCloud Runで実行させている分析処理の中身です。
具体的にはこちらのリポジトリで公開しているようなクラスタリング処理を行っています。

  1. ログに含まれる時刻表記や単純な数字のみの単語などの不要なキーワードを除去
  2. ログ文をGoのproseというライブラリを使って形態素解析を行い品詞タグ付け
  3. 品詞タグのCD(数字、序数など)、SYM(記号)などの不要なキーワード以外のものを重要キーワードとしてピックアップ
  4. ピックアップされた単語をwegoというライブラリに含まれるWord2Vecでベクトル化
  5. 1行のログに含まれる各単語のベクトルを加算平均し、ログ1行のベクトルを算出
  6. goClusteringというライブラリを使い、Ward法を用いて各行のログをクラスタリング分類

こんな流れになります。
最後のクラスタリング分類の方法としては、K-meansとかも考えたのですが、K-meansだと予め決まった数のクラスタ数に分割するというトップダウンのアプローチになってしまい、今回の要件のように件数がわからない、場合によってはそれぞれ全く別のクラスタに分類されるケースもあり得るような場合に分割しづらかったため、ボトムアップのアプローチであるWard法を採用しています。

out.png

イメージとしては上記図のような感じで、ログの各行が近いもの同士が並んでおり、どのレベルの距離以上でクラスタを区切るかを指定すればある程度の類似度以上の近しいものを同一クラスタとみなして分割することができます。上の図の場合、0.008以上の距離で分割すると、2つのクラスタに、0.004~0.007ぐらいの距離で分割すると3クラスタに分割されることになります。

まとめ

このような処理もサーバレスで実装できてしまいます。ある程度制約がある中でどのような構成が良いかを検討して組めばサーバレスでも十分に要件を満たせる実装は可能かと思います。
特にCloud Runはコンテナベースで非常に柔軟に処理実行組み込めるので非常に便利です。
LogCrowもぜひお試しいただければと思います。

https://logcrow.firebaseapp.com/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go 1.14 で defer が速くなるのは 8 個まで

TL;DR

2020/02/25 にリリースされた Go 1.14 では defer のインライン展開というランタイム高速化の改善が盛り込まれましたが、これは関数内の defer が 8 個までの場合に限り有効です。 8 個より多くの defer やループ内の defer を含む関数では従来と同じコードが生成され、パフォーマンスの改善はありません。

解説

2020/02/25 に開催された Go 1.14 Release Party in Japan の YouTube Live を視聴していて、 @tenntenn さんの defer トークで次のようなスライドを目にしました。

image.png

そこで思わず次のような質問をしてしまったのですが、

image.png

後で調べてみたところ Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case という文書の Implementation のところにずばり答えが書いてありました。

  1. We need to restrict the number of defers in a function to the size of the deferBits bitmask. To minimize code size, we currently make deferBits to be 8 bits, and don’t do open-coded defers if there are more than 8 defers in a function. If there are more than 8 defers in a function, we revert to the standard defer chain implementation.

要するに、ひとつの関数の中でインラインに展開される defer の数は 8 個までということです。その数を越える defer がある関数は、従来と同じコードが生成されます。

実際に次のようなプログラムでコードを生成して試してみました。

gen.go
package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    n := 0
    if len(os.Args) > 1 {
        n, _ = strconv.Atoi(os.Args[1])
    }
    fmt.Print("package main\nimport \"os\"\nfunc main() {\n")
    for i := 0; i < n; i++ {
        fmt.Printf("if len(os.Args) == %d { defer func() {}() }\n", i)
    }
    fmt.Print("}\n")
}

このプログラムは、コマンドライン引数に渡した数字だけの条件分岐と defer を含む main() を生成します。

コマンドライン
$ go run gen.go 4 | gofmt
package main

import "os"

func main() {
    if len(os.Args) == 0 {
        defer func() {}()
    }
    if len(os.Args) == 1 {
        defer func() {}()
    }
    if len(os.Args) == 2 {
        defer func() {}()
    }
    if len(os.Args) == 3 {
        defer func() {}()
    }
}

defer が 8 個のとき、 SSA では Or8 を含む条件分岐のブロックが 8 セット生成されます。

bash
go run gen.go 8 | go tool compile -d ssa/build/dump=main /dev/stdin
main_01__build.dump(抜粋)
  b2: <- b3 b4
    v24 = Phi <mem> v22 v127
    v224 = Phi <uint8> v21 v4
    v25 = Load <[]string> v23 v24
    v26 = SliceLen <int> v25
    v28 = Eq64 <bool> v26 v27
    If v28 -> b6 b7
  b6: <- b2
    v33 = Copy <mem> v24
    v34 = Store <mem> {func()} v32 v29 v33
    v36 = Copy <uint8> v224
    v37 = Or8 <uint8> v36 v35
    v38 = Store <mem> {uint8} v5 v37 v34
    Plain -> b5
  b7: <- b2
    Plain -> b5

defer が 9 個のとき、SSA では次のような runtime.deferprocStack の呼び出しが 9 セット生成されます。この呼び出しのエラーチェックとリターンも行っているため、だいぶ長くなっています。

bash
go run gen.go 9 | go tool compile -d ssa/build/dump=main /dev/stdin
main_01__build.dump(抜粋)
  b2: <- b5 b4
    v24 = Phi <mem> v20 v1
    v25 = Load <[]string> v23 v24
    v26 = SliceLen <int> v25
    v28 = Eq64 <bool> v26 v27
    If v28 -> b8 b9
  b8: <- b2
    v30 = Copy <mem> v24
    v31 = VarDef <mem> {.autotmp_11} v30
    v32 = LocalAddr <*struct { siz uint32; started bool; heap bool; openDefer bool; sp uintptr; pc uintptr; fn uintptr; _panic uintptr; link uintptr; framepc uintptr; varp uintptr; fd uintptr; args [0]uint8 }> {.autotmp_11} v2 v31
    v33 = OffPtr <*uint32> [0] v32
    v34 = Store <mem> {uint32} v33 v14 v31
    v35 = OffPtr <**func()> [24] v32
    v36 = Store <mem> {*func()} v35 v29 v34
    v37 = Store <mem> {uintptr} v18 v32 v36
    v38 = StaticCall <mem> {runtime.deferprocStack} [8] v37
    Defer v38 -> b10 b11 (likely)
  b9: <- b2
    Plain -> b7
  b10: <- b8
    Plain -> b7
  b11: <- b8
    v39 = Copy <mem> v38
    v40 = StaticCall <mem> {runtime.deferreturn} v39
    Ret v40

ということで、 defer が 8 個と 9 個とで異なる SSA が生成されていました。また擬似コードにおける deferBits には uint8 が使われていることがわかりました。

感想

今回のイベントのトークはどれも興味深く、またためになる内容でよかったです。 Go の SSA というものを初めて読みましたが、全くわからんものではないということがわかったのが大きな収穫でした。 これなら自分もなにか作れそうな気がしてきました。

新型コロナウイルスの影響でイベント開催中止の決定が相次いでいるのは残念ですが、多くのコミュニティでオンライン開催に切り替えられ、急造ながらも質問や感想をその場で伝えられる双方向なイベントが実現できているのは大変素晴らしいことだと思います。イベントを主催される方々の熱意と努力に敬意を表します。また、今後の技術の洗練やノウハウの蓄積によるオンラインイベントの進化にも期待したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む