0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goでソースコードをディレクトリ分けする方法のメモ

Last updated at Posted at 2025-01-27

自分用メモ

Go便利でよく使ってるけど長く育ててるとプロジェクトが大きくなってコードが読みにくくなるので整理できるといいですよねって話。

難しい話(スキップしてもOK)

関心の分離でぐぐるといろんな記事が出てくるので時間があれば読んでみてもいいかも。

サンプルコード

サンプルとしてGo Echoで社員情報を返すAPIサーバを作った。3つのエンドポイントがあって情報を返すことができる。それぞれの関数の構造は以下の図のようになっている。以下3段階でファイルを分割していくがこの関数と呼び出しの関係は変わらない。

Qiita - goファイル整理の図.png

実行する場合は以下のようにすればいい

$ cd levelN
$ go run ./*.go

レベル0: すべてmain.goにベタ書き

Qiita - goファイル整理の図 (2).png

ディレクトリ構造
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が長くなって可読性が低くなってきたという仮定で、機能ごとにファイルを分割する。

Qiita - goファイル整理の図 (3).png

ディレクトリ構造
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: サブディレクトリを切ってさらにファイルを分割

さらにサブディレクトリを切ってファイルを分割する。こんな感じになる。

Qiita - goファイル整理の図 (6).png

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側から外部に公開されていないといけないので関数の先頭を大文字にした。

(変更前)level1/web.go
package main
()
func web_get_shain(e *echo.Echo) {}
(変更後)level2-1/src/web/shain.go
package web
()
func Get_shain(e *echo.Echo) {}

参照する側では <モジュール名>+<パス> でパッケージにアクセスできる。ディレクトリ構造とパッケージ名が異なる場合はimport文の先頭にインポートするパッケージ名を書けばいい。

level2-1/main.go
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: マルチモジュール構成

マルチモジュール構成で書くこともできる。
Qiita - goファイル整理の図 (7).png

./
├── 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.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の制約にひっかかりやすくなるし、型宣言は全部このパッケージにあると決めておくとわかりやすいと思った。

参考にしたところ

最後に

公式ドキュメントが難しいんだよな・・・

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?