🔧

Goで社内ツールを作るならこんなふうに

2022/04/24に公開

バックグラウンド

最近企業でにわかにGoの採用が増えているようですね。この流れを受けて実際にうちでも採用してみようと考えている方も多いかもしれません。しかし、Goに限った話ではないのですが、いきなり大きなプロジェクトに未経験の技術を投入するのはリスクが伴います。ですから最初は技術検証とGoの教育を兼ねて、小さな社内ツールなどから導入するのがよいでしょう。幸い、Goは非常に習得が容易な言語ですから、既に他言語の経験があるプログラマーがじゅうぶんに開発できるようになるまでには、数日と掛からないことがほとんどです。

この記事は、そんな小さなツールの導入フェーズにあたって、自らの経験から少し役に立つかもしれない情報をシェアします。

リポジトリの構成

自らの経験上、初めてGoを触れるときによく出る質問の一つは「リポジトリ構成はどうすればいいのか?」です。

結論から申し上げると、 テンプレもルールもない。自分で考えて決める。必要なければパッケージ(ディレクトリ)を分けずフラットにする。ディレクトリ配下のファイル数は増えるが、それを恐れない。 です。

他言語をやってきた方は、フラットな構造が不安でディレクトリを分けたがる人も少なくないですが、是非ここは勇気を出してフラットな構造で書いてみていただきたいです。おそらく思ったよりも困ることは少ないはずです。

ただし、例外的にパッケージをどうしても分けなければならないケースがあり、それが「リポジトリ内に複数のコマンドが存在する場合」です。ご存知の通り特殊なパッケージ package main を宣言することで、そのパッケージ内にmain関数を書け、コマンドとしてビルドすることができます。そしてリポジトリ内に複数のコマンドが存在する場合、それらは別ディレクトリの package main として宣言することで、別のコマンドとしてビルドすることができます。

こういった場合、以下のようなディレクトリ構造にするのをお勧めします。 (<> 囲みの名前は適宜置き換えて考えてください)

<myproduct>
├── go.mod
├── go.sum
├── <env.go>    <- package <myproduct>
├── <github.go> <- package <myproduct>
├── <slack.go>  <- package <myproduct>
├── <myapp-1>
│   └── main.go <- package main
└── <myapp-2>
    └── main.go <- package main

コマンドごとにディレクトリを分けつつ、リポジトリ直下には package <myproduct> を作ります。このパッケージは唯一つの「共通で利用されるパッケージ」であり、各コマンドに共通の処理や情報を置く場所とします。

さらに、各コマンドのディレクトリには main.go だけを置き、main関数と最低限の処理だけを書くことをお勧めします。これは

  • 後からコマンドを増やしたときに、既存の処理を流用しやすくするため。(各コマンドのディレクトリにある処理を package <myproduct> に移すのはちょっとだけ面倒)
  • 社内ツールでは、何かと共通処理/情報が多くなるため。(GitHubやSlackのAPIを叩いたり、社内特有のサーバー情報を記載したり、など)

というのが理由です。参考にしてみてください。

ちなみに、以下のようなディレクトリ構造にすることを推奨している記事もおそらく多いでしょう。

<myproduct>
├── cmd
│   ├── <myapp-1>
│   │   └── main.go
│   └── <myapp-2>
│       └── main.go
├── go.mod
├── go.sum
└── <env.go>

先程の構造との違いは cmd ディレクトリが一個挟まっていることです。これでも悪くないのですが、挟む意味も特にないので、自分は cmd ディレクトリなしの、より単純な方法をお勧めします。

cmd ディレクトリが挟まっているだけならあまり問題はないのですが、往々にして pkg とか config とかのディレクトリを cmd と同じ階層に生やしたくなってしまいがちです。それらも cmd ディレクトリと同様になくても機能するものなのに、今後機能を追加するたびにどのパッケージを利用するか、または追加するか頭を悩ませることになってしまいます。ですから、将来保守する人のことを考えれば、「共通パッケージを1つだけ切って、それ以外は極力何もしない」のがベストな選択肢であると筆者は考えており、実際今のところそれで上手く行っています。

import path について

コマンド (package main) から共通パッケージを利用する際は、以下のようにimport文を書きます。

import "github.com/mycompany/myproduct"

リポジトリ内の別パッケージについて、たまに相対パスで短く書くことを試みる人がいますが、必ず絶対パスで書いてください。これはGoを書くときのお約束です。

polyglot な構成

ここまでは、リポジトリ内のコードがすべてGoである前提でお話していましたが、実際に筆者が運用しているリポジトリはシェルスクリプトやJS等複数の言語によって書かれたツールを同じリポジトリに乗せています。こうなるとGoだけの場合以上に正解のない世界にはなってしまうのですが、以下のような構成を試みて今のところ問題ないので、参考にしてみてください。

<myproduct>
├── go
│   ├── <myapp-1>
│   │   └── main.go
│   ├── <myapp-2>
│   │   └── main.go
│   └── <myproduct>
│       ├── <env.go>
│       ├── <github.go>
│       └── <slack.go>
├── go.mod
├── go.sum
├── node
│   └── <build.js>
├── package.json
└── shell
    └── <install.sh>

この構造は以下のようなロジックで導出されました。

  • まず、go.mod や package.json のような一般的にリポジトリルートに置くべきファイルはリポジトリルートに置きます。ここはかなり諸説ありうる部分だとは思います。
  • 次に、言語ごとにディレクトリを分けます。
  • <myproduct>/go ディレクトリ直下に素直にパッケージを作るとパッケージ名が go になって不便なので、共通パッケージはその下にまた別に切るようにします。パスが <myproduct>/go/<myproduct> となってしまいますが許容します。それ以外はGoだけの場合と同じで、共通パッケージは唯一つであり、コマンドはそれぞれ別のディレクトリを切っています。

Go以外の言語の構造については割愛します。繰り返しになりますが諸説ありうる内容ですので、参考程度にとどめておいていただければと思います。

社内のプライベートリポジトリの利用

ご存知の通りGoはgo.modに記載された依存モジュールを自動でダウンロードしてくれます。npmなどと違って、ライブラリはGitHub上にアップロードすればすぐに使えるようになって大変便利です。(npmの方針も一つの選択であり、悪いわけではありません。念のため。)

既定では、パブリックリポジトリは proxy.golang.org 経由でダウンロードされ、そこからダウンロードできないプライベートリポジトリはVCS(git など)でダウンロードされます。なお、HTTPSでアクセスしようとするので、SSHでプライベートリポジトリにアクセスしている場合は、git の設定が必要です。

# 特定のURLで始まるHTTPSアクセスをSSHアクセスに置き換えます
# URLは適宜変更してください
# SSHのURLの末尾の : や、HTTPSのURLの末尾の / をお忘れなく!

# コマンドで設定する場合
git config --add --global url."[email protected]:".insteadOf https://github.com/

# .gitconfig を直接書く場合
[url "[email protected]:"]
    insteadOf = https://github.com/

中央集権的な社内用の管理システムを立ち上げる必要はありませんから、管理システムを立ち上げた人が去ってカオスが訪れることもないですね。どんどん社内にリポジトリを作り、再利用可能にしていきましょう。

GOPRIVATE

上述したように、プライベートリポジトリは proxy.golang.org に一度アクセスを試みた後でVCSでのアクセスに切り替えるので、proxy.golang.org にプライベートリポジトリのURLを一度渡す形になります。情報統制の厳しい環境でこれが問題となる場合は、GOPRIVATE 環境変数を使います。

GOPRIVATE=github.com/my-organization

GOPRIVATEは , 区切りで複数指定でき、ワイルドカードが使え、またモジュール名と前方一致でマッチするため、以下のような設定もできます。

GOPRIVATE=*.corp.example.com,rsc.io/private

ちなみに、Goの環境変数は、上記のようにOSの環境変数としてもセットできますが、go env -w コマンドで書き換えることも可能です。この場合、環境変数と違って永続的に設定が書き換えられます。Windows環境など、こちらが役に立つ場面もあるかもしれません。お好みで選んでください。

go env -w GOPRIVATE=github.com/my-organization

参考リンク: https://goproxy.io/docs/GOPRIVATE-env.html

教育について

さてここまでは開発を始める話でしたが、チームでGoを扱っていくことになる以上、同時に教育体制を整えることも必須です。といっても既に優秀な初学者向けの資料がインターネットに十分に存在していますから、ありがたくそれを利用しましょう。筆者のおすすめは、Goの伝道師 tenntenn 氏によるスライドです。https://tenn.in/go にアクセスしてみてください。何度かに分けて読み合わせて、一通り資料を読み終われば、未経験からでも既にGoを書ける状態になっているはずです。

一応いくつかつまずきうることについて補足しておきます。

  • 今まで通ってきた言語によってはポインタの理解が難しいかもしれません。しかしあまり気にせず、軽く流して一通り見ることを優先することをお勧めします。正直ポインタへの理解が足りていなくても、Goはそれっぽく書けてしまいますので。
  • モダンな開発環境と縁遠い職場の場合、環境構築で若干戸惑うかもしれません。たとえばAtomを利用している人がいる場合、諦めてVSCodeに移行することを勧めてください。VimユーザーでLanguage Serverの存在を知らない人がいる場合は、有無を言わさず真っ先に vim-lspvim-lsp-settings の2つのプラグインのインストールをしてもらってください。また、各エディタで保存時に gofmt (または goimports) が効くように設定を済ませてください(具体的な方法は割愛します)。

こういった資料の活用に加え、メンバーに実際に日常的にツールの開発に携わってもらうことが出来たら、教育体制については概ね問題ないでしょう。

さいごに

Goの普及に合わせて、Goを採用した/採用したい企業も増加傾向が認められます。一方でGoの経験者が既に豊富にいるかと言うとまだそうとも言えない状況です。結果として、今一番求められているのは、Go未経験のプログラマがGoを書けるようになること、でしょう。小さなツールでGoを書くことを実践すれば、企業はGoを安全に導入でき、メンバーはGoの経験を積むことができ、まさにwin-winの関係がもたらされると期待しています。そういった幸せな世界の実現に向けて、筆者が現場で得た知見がいくらか役に立つことがあれば幸いです。

おまけ

あなたがもし本を読んで勉強する気力があるなら、実用Go言語 が大変おすすめです!まさにこの記事と同様に、導入を考え始めるフェーズにおいて役立つ非常に「実用的な」書籍です。※この文章は筆者が勝手に書いたものであり、依頼された広告/プロモーションではありません。

Discussion