自分用メモ
Go便利でよく使ってるけど長く育ててるとプロジェクトが大きくなってコードが読みにくくなるので整理できるといいですよねって話。
難しい話(スキップしてもOK)
関心の分離でぐぐるといろんな記事が出てくるので時間があれば読んでみてもいいかも。
サンプルコード
サンプルとしてGo Echoで社員情報を返すAPIサーバを作った。3つのエンドポイントがあって情報を返すことができる。それぞれの関数の構造は以下の図のようになっている。以下3段階でファイルを分割していくがこの関数と呼び出しの関係は変わらない。
実行する場合は以下のようにすればいい
$ cd levelN
$ go run ./*.go
レベル0: すべてmain.goにベタ書き
level0/
├── docs/ "<- swaggerドキュメントを生成する部分"
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod "<- module example.com/ore_no_program"
├── go.sum
└── main.go "<- package main"
moduleとして example.com/ore_no_program
と宣言していて、そのなかに含まれるpackageは main
1つだけという状態。 Goのmoduleとpackageの関係性はぐぐるといろいろ出てくる。
レベル1: ファイルを分割
main.go
が長くなって可読性が低くなってきたという仮定で、機能ごとにファイルを分割する。
level1/
├── docs/
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── main.go "<- package main"
├── web.go "<- package main"
├── ap.go "<- package main"
├── db.go "<- package main"
├── go.mod "<- module example.com/ore_no_program"
└── go.sum
ファイルが異なっていても、同じpackage mainに属していればひとまとまりと認識されて相互参照できる。
レベル2-1: サブディレクトリを切ってさらにファイルを分割
さらにサブディレクトリを切ってファイルを分割する。こんな感じになる。
level2-1/
├── docs/
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod "<- module example.com/ore_no_program"
├── go.sum
├── main.go "<- package main"
└── src/
├── app/
│ ├── listBusho.go "<- package app"
│ └── listGender.go "<- package app"
├── db/
│ ├── busho.go "<- package db"
│ └── person.go "<- package db"
└── web/
├── busho.go "<- package web"
├── gender.go "<- package web"
└── shain.go "<- package web"
src/web
, src/app
, src/db
にそれぞれコードを分割して配置し、別のpackage名を指定した。
packageまたぎで関数を参照するにはpackage側から外部に公開されていないといけないので関数の先頭を大文字にした。
package main
(略)
func web_get_shain(e *echo.Echo) {}
package web
(略)
func Get_shain(e *echo.Echo) {}
参照する側では <モジュール名>+<パス> でパッケージにアクセスできる。ディレクトリ構造とパッケージ名が異なる場合はimport文の先頭にインポートするパッケージ名を書けばいい。
package main
import (
web "example.com/ore_no_program/src/web"
)
func main() {
web.Get_shain()
}
go run するときにエラーが出たら
ファイル構造を直しているときに以下のようなエラーが出ることがある。
main.go:XX:Y: no required module provides package <module><path>: go.mod file not found in current directory or any parent directory; see 'go help modules'
project rootのgo.mod
, go.sum
を消してコマンドラインからgo mod tidy
するとうまくいくかも。vscodeのチェックはリアルタイムで追従できないようなので一回フォルダをクローズしてから開き直すといいかも。
エラーが出たときに go.mod
を作れとか、go.work
にパスがないとか言われるときがあるけど、モジュール依存関係を作り直せば必要ない。少なくとも私のプロジェクトでは必要なかった。
(非推奨)レベル2-2: マルチモジュール構成
./
├── docs/
│ ├── docs.go
│ ├── swagger.json
│ └── swagger.yaml
├── go.mod "<- module example.com/ore_no_program"
├── go.sum
├── go.work
├── go.work.sum
├── main.go "<- package main"
└── src/
├── app/
│ ├── go.mod "<- module example.com/ore_no_program/app"
│ ├── listBusho.go "<- package app"
│ └── listGender.go "<- package app"
├── db/
│ ├── go.mod "<- module example.com/ore_no_program/db"
│ ├── busho.go "<- package db"
│ └── person.go "<- package db"
└── web/
├── go.mod "<- module example.com/ore_no_program/web"
├── busho.go "<- package web"
├── gender.go "<- package web"
└── shain.go "<- package web"
module example.com/ore_no_program
から他のモジュールを見えるようにするため、go.work
は以下のように書く。
go 1.22.2
use .
use (
./src/web
./src/app
./src/db
)
各go.mod
のgoバージョンは合わせておいたほうがいい。多少ぶれがあっても許容されるようだけど。
この構成はGo 1.18以降で使用できるワークスペースモードを使っている。他にもgo.mod
の中でreplace
句を使うやり方があるようだ。動くことは動くのだけど、1つのアプリケーションを動かすだけなのに公開するつもりのないモジュールを作成するのはやりすぎ、というのが非推奨の理由。
実際にやってみて気になったところ
packageは循環してimportできない
言われてみればあたりまえなんだけど既存のコードを分け始めるとこれで怒られてどうやってわけるか工夫するようになる。関数の中を変えるほど時間的気力的余裕がない (もちろん設計するほど意識も高くない) ときは1つのpackage複数ファイルでまとめておいて、とりあえずしのぐのはありかも。
main packageの関数、typeを子packageに渡すのはやめたほうがいい
できなくもないけどトリッキーな方法になるので、違う方法を考えたほうがいい。
typeだけ切り出してひとつのpackageにするのはあり
いろいろなファイルに分けると上の循環importの制約にひっかかりやすくなるし、型宣言は全部このパッケージにあると決めておくとわかりやすいと思った。
参考にしたところ
最後に
公式ドキュメントが難しいんだよな・・・