2023年の Go での気づきに思いを馳せて構築するオレオレ Go サーバープロジェクトレイアウト
はじめに
2023 年は念願叶い業務でも趣味でも Go を触ることができました。
そんな 2023 年での Go での気づきに思いを馳せ、タスク管理をするような簡単なサーバーを構築しそのプロジェクトレイアウトをご紹介してみます。
気づきといっても読む方にとっては当たり前のことかもしれません。
また、プロジェクトレイアウトはこれが正義だとは思っていません。紹介するレイアウトは業務では使っていないです。運用などもまともにしたことはありません。私がそこそこの規模のサーバーを新規で構築するならこんな風にしようかなって記事です。
気づき
interface
を満たすためのコードって VSCode で自動生成できたんだ
よくおまじないとかファクトリー関数の戻り値とかで構造体が interface
を満たしているかどうかを確認することがあるかと思います。
package main
import "context"
var _ Interface = (*Struct)(nil) // おまじない
type Interface interface {
Method(ctx context.Context)
}
type Struct struct{}
// ファクトリー関数
func NewStruct() Interface {
return &Struct{}
}
// ここまで自動生成してくれる
// Method implements Interface.
func (*Struct) Method(ctx context.Context) {
panic("unimplemented")
}
これまでは、おまじないを書いたあとビルドが通るようにメソッドを自分で書いていたのですが、VSCode で自動生成できることを最近知りました。便利すぎですね。
ちなみに、interface
は以下のように引数名を省略した形式で定義することもできます。
package main
import "context"
var _ Interface = (*Struct)(nil) // おまじない
type Interface interface {
Method(context.Context) // 引数名を省略
}
// ここまで自動生成してくれる
// Method implements Interface.
func (*Struct) Method(context.Context) { // 引数名が省略されて自動生成
panic("unimplemented")
}
いままで、私は "なんとなく" こっちの方がイケてると思っていたのですが、自動生成を活用するようになってからは引数名を省略しておくと自動生成したときに引数名を書く必要が出てきてめんどくさいとなりました。
最近は interface
で引数名も定義するようにしています。
また、golangci-lint
v1.55.0 で導入された inamedparam
という静的解析ツールは interface
定義で引数名が定義されているかどうかチェックしてくれるのでおすすめです。
golang 組織配下のリポジトリだからと安心はできない
(使いこなせているかは一旦おいておいて)
私は golang/mock ライブラリが好きです。
割と仲良くなれてきたのかなあと思った矢先の 2023 年 6月 Public archive となりました。
golang が管理しているからそんなことは起こらないと思っていましたが起こるのですね。
新たに go.uber.org が管理しているので大きな問題にはならなそうですがそんなこともあるのだなと気づきました。極力標準ライブラリで頑張りたくなりますね。
(載せ替えるモチベーションがなく既存の実装はまだ golang/mock です。)
みなさん、新たに mock ライブラリを選定する際は uber-go/mock を候補にしましょう。
メソッドの引数は値、戻り値はポインタ
以前は引数も戻り値も “値” を設定していましたが、戻り値はポインタを返した方が以下の点で良さそうと思いました。
-
error
が発生した際、正常な値を返す場合は空構造体を返すよりnil
の方が実装が楽 - もしメソッド利用者が
error
チェックをサボった場合、正常な値を受け取った側は空構造体で処理を進めてしまいゼロ値が永続化されてしまう可能性がある。- それなら
nil
を返しておいてアクセスされたときにニルポでpanic
を発生させた方がよさそう。 - panic が発生して処理が落ちるよりもゼロ値が永続化されてデータ不整合が発生する方が修正がめんどくさそう。
- それなら
package main
import (
"context"
"errors"
"fmt"
)
type Struct struct{}
type Input struct{}
func (in Input) Validate() error {
return errors.New("error")
}
type Output struct{}
// こっちよりも
func Function(
ctx context.Context,
input Input,
) (Output, error) {
if err := input.Validate(); err != nil {
return Output{}, fmt.Errorf("validate input: %w", err)
}
return Output{}, nil
}
// こっち派
func Function(
ctx context.Context,
input Input,
) (*Output, error) {
if err := input.Validate(); err != nil {
return nil, fmt.Errorf("validate input: %w", err)
}
return &Output{}, nil
}
ogen
@p1ass さんの紹介記事で知りました。
まだまだ深掘りはできていないけど 2023 年終盤はこれで遊んでいました。
2023 年中盤までは oapi-codegen + chi の組み合わせで遊んでいましたが紹介記事をきっかけに乗り換えました。
OpenAPI
での定義に対して厳密に実装ができる印象があります。 oapi-codegen
+ chi
は割と定義通りにレスポンスを返せていなかったなと ogen
を使ってて感じました。
ただ、ogen
では 3xx
系のレスポンスがうまく扱えない問題がありそうでそこはちょっと困っています。
※ net/htttp
の client
が自動でリダイレクトするので結果として 200 系が返ってきます。一方で ogen
が自動生成したコードでは 3xx
系のステータスコードを期待するように実装されているのでエラー扱いされてしまいます。
ORM どうしよう
Go 言語の ORM なにがいいのか問題 ...
当たり前ですが銀の弾丸はないですよね。
開発規模や開発フェイズによって使いたい ORM も変わりそうですし。
2023年中盤までは ent で遊んでいましたが地味にクセがあり、テーブル定義の実装方法もすぐ忘れたりするのでしっくりきませんでした。
2023年終盤は prisma でテーブル定義を管理・マイグレーションして Go の実装は database/sql
で頑張る方式にしています。( prisma 開発者体験めっちゃいいなって思ってます。 )
ただ、prisma も痒い所に手が届かなそうだったりするのでこんなネタ記事も書いてみました。
Go での実装は SQL が間違っていたりマッピングがうまくいかなかったりでデバックは大変ですがテストを書いて担保したり ChatGPT や Copilot の力を借りることでそこそこ実装できるのかなという印象です。
AST(抽象構文木)も Generative AI の力を借りれば仲良くなれそう
Go って構造体を埋め込むと埋め込んだ構造体のメソッドを呼び出せるのですね。
ただ、構造体にポインタとして構造体を埋め込む形式で初期化を忘れると nil
にアクセスが走り panic
が発生します。
package main
type Base struct{}
func (bs Base) String() string {
return "base"
}
type Struct struct {
*Base // ポインタとして埋め込む
}
func main() {
s := &Struct{}
println(s.String()) // panic
}
go run main.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4575f0]
goroutine 1 [running]:
main.main()
main.go:16 +0x10
exit status 2
これを避けるために構造体にポインタとして構造体を埋め込むのはやめようみたいな静的解析を検討したことがありました。
ChatGPT
に叩き台を作らせて挙動をみて修正していくことで形にすることができました。
いまだにASTの中身を理解できていない部分はありますが実装のハードルは一気に下がりました。
VSCode の拡張機能 Material Icon Theme で気分を上げる
ディレクトリにアイコンがついていると気分があがります。
また、私はディレクトリ名を考えるときアイコンが表示されるということは割と利用されている名前・概念なのだなと判断するために使ったりします。
それでもアイコンが変わらなかったりする名前があったので以下のような組み合わせを最近は設定するようにしました。
settings.json
"material-icon-theme.folders.associations": {
"internal": "home",
"domain": "class",
"adapter": "pipe",
"repository": "interface",
"gateway": "database",
"application": "app",
"usecase": "interface",
"interactor": "flow",
"driver": "cluster",
"openapi": "api",
"postgres": "database",
}
Go: Fill struct
が使えなくなった ...
VSCode で それは突然訪れました ...
cmd + shift + p
から Go: Fill struct
が消えたのです ...
よくよく調べると上記 release ノートに辿り着きました。
VSCode のリファクタリング機能で同等のことができるので不要になったとのことです。
cmd + shift + p
-> fill
の導線が cmd + .
に変わりました!
これはちょっと便利になりましたね!!
プロジェクトレイアウト
2023年9月21日(たぶん…) 全 Gopher が待ち望んでいたドキュメント "Organizing a Go module - The Go Programming Language" が公開されました。
長年頭を悩まされてきた golang-standards スタンダードじゃない問題もついに終止符が打たれたといえるのではないでしょう。
とはいえ、Go チームから公開されたドキュメントのレイアウトはとてもシンプルなものです。
基本的にはこのレイアウトで始めるのがよいでしょう。
でもなんだかんだコード量が多くなってくるとレイヤーを分けたくなってきちゃいますよね。
ということで以下のようなディレクトリ構成を考えました。
サンプルリポジトリ
TODOをCRUDできるようなシンプルなアプリケーションを構築しています。
(途中で飽きて細かいところまで実装できていないです。あくまでこんな感じのディレクトリ構成という雰囲気を共有するためのリポジトリです ... )
ディレクトリ構成
root
├── api
├── schema
├── cmd
├── internal
│ ├── driver
│ │ ├── config
│ │ ├── env
│ │ ├── postgres
│ │ └── server
│ ├── adapter
│ │ ├── controller
│ │ └── gateway
│ ├── application
│ │ ├── interactor
│ │ └── usecase
│ └── domain
│ ├── model
│ └── repository
├── test
│ ├── e2e
│ └── integration
├── go.mod
├── go.sum
└── README.md
api
- OpenAPI の定義ファイルを配置
- ogen はこのファイルを指定
schema
- テーブル定義 (
prisma
) に関するファイルを配置 - (ローカル環境で自動マイグレーションする仕組み化ができていないのでそのうちやりたい)
cmd
- Go アプリのエントリーポイントとなる
main.go
をxxx/main.go
のように配置-
xxx
はアプリ名を想定
-
pkg
- テストを書く必要がない Go コードを配置
- 自動生成による Go コードを配置
- このディレクトリは未だしっくりきていないです。
internal
配下でもいいかなとか思ったり。
test
- E2Eテストと統合テスト (ミドルウェアとの動作確認) を配置
- もちろん Go コードで記載
internal
- テストを書きたい(書くつもりの) Go コード
- クリーンアーキテクチャなのかヘキサゴナルアーキテクチャを意識
- 4層は以下の通り
- driver
- adapter
- application
- domain
driver
環境変数やConfig、PostgreSQLやServer設定など外部から与える値、もしくは外部とのやりとりに必要な情報を配置
- env
- config
- postgres
- server
etc ...
adapter
クライアントからの入力や永続化層への保存の実装
- controller
- ogen によって生成された
interface
を実装 - クライアントからの入力および usecase の出力を変換するのが責務
- ogen によって生成された
- gateway
- repository に定義した
interface
を実装 - 永続化層への保存が責務
- repository に定義した
application
ビジネスドメインに関するコードを駆使して各ユースケースの処理を実装
- usecase
-
interface
を定義
-
- interactor
- usecase に定義した
interface
を実装
- usecase に定義した
domain
ドメインに関するコードの実装
- model
- 具体的なモデルを実装
- ビジネスロジック
- repository
- model の 永続化に関する
interface
を定義
- model の 永続化に関する
ポイントっぽいこと
- スタンダードではないけれど
golang-standards/project-layout
のエッセンスも参考
- VSCode の Material Icon Theme の拡張機能にあう命名を
- ディレクトリのアイコンが色づくとテンションが上がる
- 設定がないものを設定しちゃう!
settings.json
"material-icon-theme.folders.associations": { "internal": "home", "domain": "class", "adapter": "pipe", "repository": "interface", "gateway": "database", "application": "app", "usecase": "interface", "interactor": "flow", "driver": "cluster", "openapi": "api", "postgres": "database", },
- なんとなくレイヤーがあって依存性逆転でテストが楽に書ければいいやくらいのモチベーション
- 3層か4層かで迷うけど4層に
- domain/model 配下を user とか todo とか細かく切っていたけどフラットに
- 細かく切ると import のときに
domain/model/xxx
とすることができxxx.Create()
みたいなことができる。 - それはそれで好きだけど、めんどくさくなってきた。
model.CreateXxx()
でいいかなって気持ち。
- 細かく切ると import のときに
おわりに
2023年は6月に Go Conference 2023 に参加したり 会社でも家でも Go の実装ができたり Let’s Go Talk #10 にオフラインで参加したり Go 1.21 リリースパーティ & GopherCon 2023 報告会にオフラインで参加したり Go に関するオンラインイベントに参加したりと、やっと Gopher としての人生が開幕したなと感じた1年でした。
Go に関する本もいくつか出版されましたね。
中でも 「Go言語 100Tips ありがちなミスを把握し、実装を最適化する」 は読んでいろいろと発見がありました。
ベストバイブックでした。 :clap:
2024 年も真の Gopher となるために開発を楽しみます。
(会社の輪読会で英語本を読み始め、意外にも読むことできるなと所感を得たのでいけるだろうと手を出した Efficient Go (English Edition) は速攻で心が折れて読めなかったので心残りです。)
おまけ: Go と関係ないけど気づき
技術負債に立ち向かう時、それは幸せなことなんですね。みなさんが使ってくれているからこそ問題となってくるわけで。使われなかったら誰も気づくことないのですし技術負債にならないですよね。だから私はこれから技術負債に出会ったらまずは感謝します。ありがとうございます。
テストが書きやすい・書きづらいという視点で実装していいんじゃないでしょうか。実装者が使いづらい(書きづらい)コードはきっと回り回ってみなさんにとっても使いづらいものになるのではないでしょうか。
Discussion