はじめに
みなさん、Go言語やってますか?
毎年じわじわ人気を伸ばしてきており、2019年にいよいよメジャーバージョンアップを控えていますね。
これからGo言語やっていきたい方も多いのではないでしょうか。
この記事では@muff1225が生産自動化システムの運用から見つけた、Go言語をはじめたい/まだはじめたばかりという方向けに、ツール開発で始めるGo言語のススメを記載しています。
これからGo言語を始める方、まだまだはじめたばかりの方、ツール開発からやっていきましょう!
なぜツールなのか
ツールをオススメしている理由は、ツールの利用者が限定的かつ運用がコントロールしやすいため、新しい技術の導入や挑戦のハードルが低いからです。
開発におけるハードルの低さ、と、あればみんなハッピーになるがそんなに急いでない、そんな領域がツール開発にはあります。
新しいことをやっていると遊んでると思われがちですが新しい技術への挑戦も、成果も出せてwin-winなんですね。
つまりツールは新しい技術に挑戦するのに最適!
なぜGo言語なのか
これはGo言語の特徴とも被るのですが、Go言語を採用すると以下のメリットを享受できます。
- ツールを動かすための環境のセットアップが不要
- どの環境でも動くことが保証される
- 大量データ処理をさせる場合の並列処理がサクッと作れる
実際のところツールを使う運用業務では運用者がエンジニアじゃない場合や動作環境が統一できない場合がほとんどだと思います。
Go言語はこういった課題を簡単に解消してくれるためツール開発に余計なことを考えずに済みます。
つまりGo言語はツール開発に最適!
ツール開発の共有思想
ツールを開発する際に特に意識したいところは、使用者が誰であってもツールを自由に、かつ安全に使用できるかどうか、ですね。
開発者に依存しないツールこそが会社の中で神ツールとして継承されていきます。
Go言語でのツール開発でもこの設計思想をもとに開発が可能です!
Go言語でこの思想を実装するためのアプローチが以下になります。
WindowsやMacなどの複数環境に提供したい
アプリケーションのビルドについて
Go言語が提供するgo build
コマンドで複数環境へのサポートが実現できます。
例えばMacOSで開発しているけど、ツールの動作環境はWindowsの32bitでも動かしたいときはgo build
コマンド実行前にGOOS
とGOARCH
を設定すると環境別にアプリケーションをビルドできます。
# Windows 32bit用
GOOS=windows GOARCH=386 go build -o application.exe
こうすることでWindows 32bit版にビルドされたアプリケーションを作成できます。
毎回ビルド時に環境変数を設定するのは大変なので、以下のようにシェルを用意しておくと良いでしょう。
#!/bin/sh
bin="toolname"
windows64path="./bin/windows64/"
windows32path="./bin/windows32/"
mac32path="./bin/mac32/"
mac64path="./bin/mac64/"
# Windows 32bit用
rm -r ${windows32path}
GOOS=windows GOARCH=386 go build -o ${windows32path}${bin}.exe
# Windows 64bit用
rm -r ${windows64path}
GOOS=windows GOARCH=amd64 go build -o ${windows64path}${bin}.exe
# MacOS 32bit用
rm -r ${mac32path}
GOOS=darwin GOARCH=386 go build -o ${mac64path}${bin}
# MacOS 64bit用
rm -r ${mac64path}
GOOS=darwin GOARCH=amd64 go build -o ${mac64path}${bin}
GOOSとGOARCHについては以下をご参照ください。
GOOSとGOARCHについて
フォルダ指定の実装について
環境が異なるところにツールを提供する場合、よく躓くところでいくとフォルダの指定の実装です。
Linux系は/a/b/c
のスラッシュ区切りに対して、WindowsはC:\a\b\c
が正しいパス指定になります。
Macで開発している場合に素直に/a/b/c
でパスを指定するとWindows版でpanicを起こします。
ファルダを指定するケースではいかなる場合でもフォルダ指定の場合はGo言語のパッケージpath/filepath
を使いましょう。環境に合わせてパス指定を変換してくれます。
package main
import(
"fmt"
"path/filepath"
)
func main() {
fmt.println(filepath.Join("a", "b", "c"))
}
プログラマーがいなくても運用したい
定数用ファイルに定数を記載する
例えばツールのフラグを運用者に変更させて挙動を変えたいというシチュエーションがあったとします。
例えば処理するフォルダのパスを変更したい、名前を変更したい・・などですね。
こういったケースではGo製のライブラリViperを使用することをオススメします。
Viperはアプリケーション内で使用する定数やフラグをyamlファイルとして管理することができるライブラリで、yamlファイルの設定変更検知と再読み込みを簡単に実装できるライブラリです。
使い方はシンプルで、Viper用の設定ファイルを特定の構成に置き、Viperで呼び出すだけです。
#App Config File
file:
name: "FileName"
package main
import (
"log"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
func loadConfig() error {
// Viperのセットアップ
viper.SetConfigName("config") // 設定ファイルの名前 (拡張子は書かない)
viper.AddConfigPath(".") // 設定ファイルの場所
err := viper.ReadInConfig() // 設定ファイルの読み込み
return err
}
func main() {
conferr := loadConfig()
if conferr != nil {
log.Panic(conferr.Error())
os.Exit(1)
}
viper.WatchConfig()
// EventTriggerを設定すると設定ファイルの変更の検知とアクションをしてくれる
viper.OnConfigChange(func(e fsnotify.Event) {
conferr := loadConfig()
if conferr != nil {
log.Panic(conferr.Error())
os.Exit(1)
}
fmt.Println("Succeeded to reload config.yaml.")
})
fmt.Println(viper.GetString("file.name")) // FileName
}
viper.GetString("file.name")
でyamlファイル内にあるfile
のname
をプログラム内で参照することができます。
viper.WatchConfig()
を使うことでyaml更新に伴うイベントを処理でき、プログラム実行前に変更するのはもちろんのこと、プログラム実行中に変更しても動的に変更を反映してくれます。
サンプルの中ではStringだけですが、その他にもInteger, Booleanなど様々な定数型の指定が可能です。
詳細はViperのReadmeをご参照ください。
もっと処理を早くしたい
goroutineを使う
例えば大量のテキストファイルを処理したり、ストリーミング処理を施す場合に処理時間がすごく重要になってきます。
ここはGo言語の魅せどころ並行処理を使いましょう。
package main
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sync"
)
var wg sync.WaitGroup
var limit chan struct{}
func init() {
cpus := runtime.NumCPU() // CPUの数を取得する
limit = make(chan struct{}, cpus) // CPUの数 = 並列数
}
func main() {
inputpath := filepath.Join("a", "b", "c")
files, err := ioutil.ReadDir(inputpath)
if err != nil {
panic(err)
}
for _, file := range files {
wg.Add(1) // 並列処理の登録
go func(path string, filename string) {
// 並列処理の中身
defer wg.Done()
limit <- struct{}{}
input, err := os.Open(filepath.Join(path, filename))
if err != nil {
panic(err)
}
defer input.Close()
ioutil.ReadAll(input) // テキストファイルを読み込む
// テキスト処理を何かする
<-limit
}(inputpath, file.Name())
}
wg.Wait() //処理完了まで待機
}
ここではgoroutine数が爆発しないように並行処理のMAX数をCPU数で制限しています。
まとめ
ツール開発で必要となる要素を実装ベースでまとめていきました。
- フォルダパス指定は必ず標準パッケージの
path/filepath
を使用する。 - 複数環境に対応する場合は
go build
で対応する。 - ソフトウェアの外に定数を設定したい場合はViperを使用する。
- 大量処理にはgoroutineを使用する。
ここだけ押さえておけばあとは筋力
#さいごに
ここまでツール開発を推していきましたが、当然Go言語で開発できるのはツールだけではありません。
ツール開発をはじめ、APIサーバをGo言語で作るケースも多くなっていますね。
来たるメジャーアップデート前に、圧倒的なGo力をつけていきましょう!