個人的に最強なGolang開発用テンプレート
Golangを1年弱ほど触り、いろいろな開発をしているうちに結構よく使う構造とライブラリが顕著なことがわかってきました。
そこで今回は自分が作成したテンプレートを載せてみようと思います。自分が立ち上げたGoのプロジェクトだと殆ど利用していると思います。
なお、今回のテンプレートはこちらで公開されています。
概要
引数解析にはCobraとCobraUtilsを利用します。
エラーハンドリングにはcockroachdb/errorsを、ロギングにはlog/slogとm-mizutani/clogを用います。
Cobraはサブコマンドを実装するためのライブラリです。cobra.Command
という構造体のインスタンスを生成することで各サブコマンドを作成します。
公式で用意されているサンプルは可読性が低い上にテストを実装しにくいため、様々な試行錯誤を行った結果現在では自前のラッパーを利用した形式に落ち着いています。
ロギングは標準ライブラリであるlog/slog
と、slog
を見やすい形式にしてくれるライブラリであるm-mizutani/clog
を利用します。
全体
例えば、hello
コマンドとbye
を実装しする場合、概ね以下のような形式になります。
今回はモジュール名をmyproject
とします。
├── cmd
│ ├── hello
│ │ └── cmd.go
│ ├── bye
│ │ └── cmd.go
│ ├── hello.go
│ ├── bye.go
│ ├── exe.go
│ └── root.go
├── log
│ └── log.go
└── main.go
main
main
に配置するのはmain.go
のみです。
/main.go
main.go
は主にCobraをラップしたcmd
パッケージの起動と最終的なエラーハンドリング、ログ設定を行います。
エラーの出力をこのように行っている理由については後述します。
package main
import (
"fmt"
"os"
"myproject/cmd"
_ "myproject/log"
)
func main() {
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
os.Exit(-1)
}
}
myproject/log
ここではロギングに関する設定を行いますが、main
以外からこのパッケージを呼び出すことはありません。
また、パブリックな関数や変数も一切作成しません。代わりにimportされたときにlog/slog
の初期設定を行います。
/log/log.go
package log
import (
"log/slog"
"github.com/m-mizutani/clog"
)
func init() {
handler := clog.New(clog.WithColor(true))
logger := slog.New(handler)
slog.SetDefault(logger)
}
slog.Info
やslog.Debug
のような関数は内部ではデフォルトに設定されたロガーを呼び出しているという実装になっています。
その内包されたロガーはslog.SetDefault
で自由に設定できます。
m-mizutani/clog
は、色がついたいい感じのログハンドラを作成してくれるライブラリです。clog
によって作成されたハンドラを用いてロガーを作成し、それをslog
のデフォルトロガーに設定することで、slog.Info
等の呼び出しをclog
のハンドラで処理することができるようになります。
更に自前のハンドラを追加したりログレベルを変更したりするなど、slog
の設定の変更は全てこの中で行います。
init
関数は、そのパッケージがimportされた際に自動で実行される関数で、このモジュールをmain.go
から呼び出すことでこの初期化処理を行います。
myproject/cmd
各サブコマンドを実装するパッケージです。
Cobraのテンプレートでは各コマンドをグローバル変数として実装していますが、この方法だとテストを書くのが苦しいのでcobra.Command
のインスタンスを返す関数という形でサブコマンドを実装します。
Cobraにおいてはサブコマンドはcobra.Command
を入れ子にするという形で実装します。そのためまずは全体を統括するroot
を実装します。
/cmd/root.go
package cmd
import (
"github.com/Hayao0819/nahi/cobrautils"
"github.com/spf13/cobra"
)
var subCmds = cobrautils.Registory{}
func rootCmd() *cobra.Command {
root := cobra.Command{
Use: "go-cobra",
Short: "go-cobra command",
SilenceUsage: true,
SilenceErrors: true,
}
subCmds.Bind(&root)
return &root
}
/cmd/exe.go
cmdパッケージで唯一パブリックになるのがこのExecute() error
関数です。
rootCmd() *cobra.Command
のインスタンスを生成し、Execute()
メソッドを実行します。
package cmd
func Execute() error {
return rootCmd().Execute()
}
/cmd/hello/cmd.go, /cmd/bye/cmd.go
これらのファイルでは各コマンドの実体を定義します。
package hello
import "github.com/spf13/cobra"
func Cmd() *cobra.Command {
cmd := cobra.Command{
Use: "hello",
Short: "hello command",
RunE: func(cmd *cobra.Command, args []string) error {
// Do something
cmd.Println("Hello, hoge!")
return nil
},
}
return &cmd
}
どちらのファイルでもCmd() *cobra.Command
をエクスポートしておきます。
サブコマンドを各パッケージに分割することで、cobra.Command.RunE
の肥大化を避け、そのサブコマンドでしか利用できないプライベート関数や変数を定義できるようにします。
/cmd/hello.go, /cmd/bye.go
各サブコマンドをrootコマンドに結びつけるグルーコードです。
package cmd
import "github.com/Hayao0819/scaffold/go-cobra/cmd/hoge"
func init() {
subCmds.Add(hoge.Cmd())
}
エラーハンドリング
各コマンドでos.Exit
を呼び出すようなことは絶対にせず、必ず全てのエラーがrootに返されるように実装します。
具体的には各cobra.Command
に実装される処理はcobra.Command.Run
ではなくcobra.Command.RunE
にします。
各RunEでは、エラーが出た時にそれをWrapしつつreturnします。具体的には以下のようになります。
package hoge
import (
"os"
"github.com/cockroachdb/errors"
"github.com/spf13/cobra"
)
func Cmd() *cobra.Command {
cmd := cobra.Command{
Use: "hoge",
Short: "hoge command",
RunE: func(cmd *cobra.Command, args []string) error {
// Do something
cmd.Println("Hello, hoge!")
// Handle error
pwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "failed to get current directory")
}
cmd.Println(pwd)
return nil
},
}
return &cmd
}
そしてエラーを全てcmd/Execute() error
に帰結させます。
最終的にmain()
まで返されたエラーは
fmt.Fprintf(os.Stderr, "%+v\n", err)
で標準エラー出力に出力されます。
cockroachdb/errorsでは%+v
でerrを渡すことでスタックトレースが出力されるようになります。
唯一の課題は、スタックトレースとslog
を現状併用できないという点です。この点は気が向いたらcockroachdb/errors
にIssueを出してみようと思います。
samber/lo
今回のテンプレートには登場しませんが、頻繁に使用するライブラリがsamber/loです。
ジェネリクスを利用してForEachやMap、Filter等が実装されているユーティリティ系のライブラリです。
自分が一時期Tsをたくさん書いてた時期があるせいか、Go特有のなんでもforを愚直に回すコードよりもloを使ったほうが好きです。
終わり
Goのエラーハンドリングは少々特殊ですが、スタックトレースが表示されるようになると劇的に実装が楽になります。
また、Cobraの実装も人によって差があるようでQiitaやZenn等にいろいろな人のサンプルが実装されています。
私のこの現状の結論も、いろいろな人のものを参考に試行錯誤をしてたどり着いたものです。今後また新しいライブラリやデザインパターン等で変化があった際にはこの記事を更新しようと思います。