Golangへのマイグレーション!

Golangへのマイグレーション!

こんにちは、Anti-Patternの塚本です。

今回のブログは、タイトル通り、あるマイグレーションについて書きたいと思います。

そもそもマイグレーションとは?

プログラミングやソフトウェア開発で、古いプログラミング言語や開発環境で記述・開発されたソフトウェアを、別の言語や新しい環境に適合するよう変換することをマイグレーションという。

http://e-words.jp/w/%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3.html

そのものズバリ!だったので引用してます。

そして、マイグレーション対象はバッチプログラムなのですが、この様な状態でして。

マイグレーション前

  • サポートが切れそうなPHP
  • ZendFrameworkをカスタマイズした独自フレームワークを使用
  • 仕様書なし

リリースから結構年数がたっているので仕方ない部分はあります。

マイグレーション後

  • オープンソースのGolang10.x
  • フレームワーク未使用。ORMライブラリにgormを使用
  • GoDoc

Golangの採用理由ですが、オープンソースというのが大きいです。

他にも色々あるのですが、

  • 処理が軽量で早い

http://sairoutine.hatenablog.com/entry/2017/12/02/182827

  • 並行処理の仕組み(goroutine)が標準パッケージ
  • ユニットテストの仕組み(testing)が標準パッケージ
  • プロファイリングツールの仕組み(pprof)が標準パッケージ
  • プログラムの自動整形(gofmt)が標準パッケージで提供
  • 生産性の高さ

ベースプログラム

1ヶ月位検証して作ったプログラムの概要がこちらになります。(DBアクセスなど省いてます)

sync.WaitGroupとchannel利用して、並行処理を実装してます。

package main
import ("sync")
// エンドポイント
func main() {
 var wg sync.WaitGroup
 executionChannel := make(chan struct{}, 2)
 processErrorChannel := make(chan bool, 2)
 resultStatusChannel := make(chan bool)
 doneChannel := make(chan struct{})
 targets := []string{"orange","Apple","peach","watermelon"}
 go resultReceptionWorker(processErrorChannel, resultStatusChannel, doneChannel)
 for _, v := range targets {
   wg.Add(1)
   executionChannel <- struct{}{}
   go processA(&wg, executionChannel, processErrorChannel, v)
 }
 wg.Wait()
 doneChannel <- struct{}{}
 if <-resultStatusChannel {
 }
 close(executionChannel)
 close(processErrorChannel)
 close(doneChannel)
 close(resultStatusChannel)
 return
}
// 業務処理処理関数
func processA(wg *sync.WaitGroup, executionChannel chan struct{}, processErrorChannel chan bool, v string) {
 processErr := false
 defer func() {
  processErrorChannel <- processErr
  <-executionChannel
  wg.Done()
 }()
 if err := processB(); err != nil {
   processErr = true
 }
 return
}
// 業務処理処理サブ関数
func processB() (err error) {
 return
}
// 並行処理結果を受け付け
func resultReceptionWorker(processErrorChannel, resultStatusChannel chan bool, doneChannel chan struct{}) {
 allStatus := false
 for {
  select {
  case ng := <-processErrorChannel:
   if ng {
    allStatus = true
   }
  case <-doneChannel:
   resultStatusChannel <- allStatus
   return
  default:
  }
 }
}

はまりどころ

・詰まるgoroutine
Channelに対する適切なバッファサイズの設定、何故かブロックされるChannelへの送受信。色々な方がブログで書かれてますが、仕組みを理解するまでははハマりました。

・荒ぶるCPU
resultReceptionWorkerは全てのgoroutineが終わるまでループします。goroutineの処理時間に比例してここが高負荷になります。pprofで確認してsleepを入れました。

・Stackされていないerror?
github.com/pkg/errorsをimport
errors.WithStack(error)関数でerrorをラップして返却し、Stackをログに出力してます。

まとめ

Golangを使いましたが、良い言語だと思います。処理速度も問題なし。

簡易ステートメントや型推論など、直感的にコーティングできると思いますし、とてもシンプルに感じました。

個人的な意見ですが、ステップ数を減らすためのテクニカルなコード嫌いです。であれば、少し長くてもシンプルな方が可読性が良いですし、他の人にも理解しやすいと思ってます。

最後に、以下のブログの記事にとても共感しました。

非知的なプログラマのためにデザインされている (Designed for non-intelligent programmers)

https://www.yunabe.jp/docs/why_golang_is_good.html