Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Goでプロジェクトを始めたい際に楽できるツールを作った

数年ぶりに戻ってきたGoですが、環境が大きく様変わりしていて劇的に使いやすくなっていました。

とくにいいなと思ったのが go mod でした。これは Go の 1.11 に実験で入ったあとから利用できる機能のようです。

ところで、go mod init というコマンドがあります。これは Go プロジェクトを新規に始める際に、go.mod という設定ファイルを作成してくれるものです。Go はプロジェクトの内容をここから色々読み取って機能するようになっています。

このコマンドは、一度ディレクトリを作成してからそのディレクトリに入って叩く必要があります。たとえば、github.com/yuk1ty/startgo というパッケージパスでプロジェクトを始めたい際には、一度 startgo というディレクトリを作成し、cd し、その中で go mod init を叩く必要があります。

mkdir startgo
cd startgo
go mod init github.com/yuk1ty/startgo

さらに、git を使えるようにしたい場合、git init も中で打つ必要があります。加えて .gitignore も用意したいことも多いでしょう。Hello, world のために main.go もファイルを作成してエディタで用意して…といった具合にです。

git init
gibo dump go >> .gitignore
touch main.go
vim main.go
# vim で Go を編集する

最初 go mod init を触った際、Rust の cargo new のように一通りプロジェクトに必要な内容物を一気に生成してくれるといろいろ手間が省けて嬉しいなと感じたのを覚えています。git も gitignore も Hello, world 用の簡単なファイルも用意された状態でプロジェクトが始まり、go run main.go するだけで開発をスタートできると嬉しいはずです。

というわけで cargo にインスパイアされてこのニーズを満たす CLI ツールを作ってみました。

github.com

この手のツールはすでに存在していそうですが、とくに調べずにとりあえず好きに作ってみました。知り合いの Gopher に聞いてみたところとくに心当たりがなさそうでしたので[*1、この手のツールは本当に現時点ではないのかもしれません。心当たりのいる Gopher の方がいたら教えていただきたいです。

使い方と仕様

前提

  • v0.2.0 時点では Windows は動作するか保証できていないです。
  • VCS には git を想定しています。

コマンド

readygo コマンドには次のオプションが用意されています。

  • --module-path (-p): go mod init する際に使用するモジュールパス。
  • --dir-name (-n): 作成するディレクトリに使用する名前。省略可能。省略した場合は、--module-path を参考にディレクトリ名は設定される。
  • --layout (-l): Go Standard Layout か、そうではなく空っぽのディレクトリを作成するかを選べる。defaultstandard を設定可能。省略可能。省略した場合の値は default

たとえば、次のように使用することができます。

readygo --module-path github.com/yuk1ty/startgo --dir-name startgo --layout default

この結果生成されるファイル等は次のようになります。

ls -a --tree --level 1 startgo
startgo
├── .git
├── .gitignore
├── go.mod
└── main.go

もちろん短いコマンドも用意されています。

readygo -p github.com/yuk1ty/startgo -n startgo -s default

また、--dir-name オプションは省略可能です。

readygo --module-path github.com/yuk1ty/startgo

この場合、--dir-name には startgo が自動で挿入されます。これは非常にシンプルなロジックで決定されています。具体的には、スラッシュで一度 split した後に、配列の一番うしろを取るという簡単なロジックです。だいたいのケースはこれで対応できるのではないかと思い採用しています。

最後に --layoutstandard を設定すると、いわゆる Standard Go Project Layout のうち、cmd, pkg, internal の3つのディレクトリを一旦生成します。この Standard Go Project Layout はいろいろと議論の余地があるようですが[*2]OSS をいくつか眺めていると意外と利用のユースケースが見受けられたので、最小限用意するようにしています。

readygo --module-path github.com/yuk1ty/startgo --layout standard

この結果生成されるファイル等は次のようになります。

ls -a --tree --level 1 startgo
startgo
├── .git
├── .gitignore
├── cmd
├── go.mod
├── internal
├── main.go
└── pkg

なお、この --layout には所定のフォーマットの YAML ファイルを読み込む機能を導入しようかと考えています。ご自身のよく作られるパッケージの型に合わせてカスタマイズできるとよいのではないかとは考えています。他にも Go のコミュニティでよく利用されるディレクトリレイアウトなどあればご教授ください。一番多いのはフラットディレクトリではないかと思っているので、基本的には default の生成する空のディレクトリで事足りるのではないかとは思っています。

生成されるファイル

readygo コマンドを実行すると、go.mod 以外にもいくつか開発に必要なファイルを用意します。

コマンド実行後用意されるのは、具体的には次のファイルやディレクトリです。パッケージ作成後すぐに git にコミットしたり、あるいは go run して動作確認できることを目指してこのファイルやディレクトリを選んでいます。

  • .git: git init した結果生成されるディレクトリ。
  • .gitignore: Go パッケージで使用できる .gitignore が生成される。
  • main.go: Hello, world できるコードが記述された Go ファイル。

内部実装

内部実装はだいたい300行前後の比較的簡単なロジックになっています。Go で CLI ツールを作ったのは初めてでしたので、知見を少し残しておきたいと思います。

Cobra

Go で CLI ツールを使う際に使えるライブラリのようです。

github.com

とくに cobra-cli が強力で、この CLI ツールにいろいろと指示を出すと雛形を用意してくれます。この上で開発をすれば好みの CLI ツールを作成可能なので、非常に開発しやすく体験がよかったです。

今回作成した CLI ツールもこの Cobra を使い倒しています。コマンド処理の本体実装は root.go に記述されています。このディレクトリのレイアウトなどは cobra-cli の生成するものに従っています。

github.com

git や go コマンド周りの実装

git については最初 git 専用のライブラリがありそうだったので使用しようかと思いましたが、結局普通にシェルを Go から実行することにしました。これが一般的な方法なのかはよくわかりませんが、やることは git init くらいでその結果をアプリケーション側で利用することはなかったため、この形で間に合ったかなと思っています。git コマンドがお使いの環境にない場合はエラーになりそこで処理が終了します。

   git := exec.Command("git", "init")
    err = git.Run()
    if err != nil {
        return err
    }

github.com

ただ、cargo などの実装を見ていると VCS にはさまざまな種類を選択できるようです。一旦自分が使いたいために git での初期化のみに対応しましたが、今後他の VCS 対応も追加していこうかなと思っています。cargo では git 以外の VCS を使用する場合は追加でオプションをつけることで専用の初期化が走るように作られていますが、readygo もこの方式に習って新しいオプションをつけるようにしようかと考えています。

go xxx コマンドを Go から実行する際に特別なパッケージがあるかもよくわからなかったので、やはり同様に go mod init コマンドを Go から直接コマンドを実行しています。同様にとくにコマンドを実行した結果を使用したかったわけではなく、副作用だけ発生させて結果はそのまま破棄で構わなかったので、この形で間に合ったかなと思います。

   cmd := exec.Command("go", "mod", "init", *pkgName)
    err = cmd.Run()
    if err != nil {
        return err
    }

readygo -p [パッケージパス] ではじめられるので、ぜひ試してみてください。

今後のプラン

*1:というか、たった数コマンドなのでそこまで手間じゃないというのはありそうです。

*2:Russ Cox がコメントしている(https://github.com/golang-standards/project-layout/issues/117)。そもそも Go がオフィシャルに推進しているものではないことや、「多くの Go のエコシステムで使われてきた」という言説自体が誤りであること、また pkg ディレクトリがとくに余計な複雑性を持ち込むことになりよくないなどといった話が書かれている。