🐁

2023年の Go での気づきに思いを馳せて構築するオレオレ Go サーバープロジェクトレイアウト

2023/12/09に公開

はじめに

https://qiita.com/advent-calendar/2023/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 定義で引数名が定義されているかどうかチェックしてくれるのでおすすめです。

https://github.com/macabu/inamedparam

golang 組織配下のリポジトリだからと安心はできない

(使いこなせているかは一旦おいておいて)
私は golang/mock ライブラリが好きです。

https://github.com/golang/mock

割と仲良くなれてきたのかなあと思った矢先の 2023 年 6月 Public archive となりました。
golang が管理しているからそんなことは起こらないと思っていましたが起こるのですね。

新たに go.uber.org が管理しているので大きな問題にはならなそうですがそんなこともあるのだなと気づきました。極力標準ライブラリで頑張りたくなりますね。

https://github.com/uber-go/mock

(載せ替えるモチベーションがなく既存の実装はまだ 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 さんの紹介記事で知りました。

https://blog.p1ass.com/posts/ogen/

まだまだ深掘りはできていないけど 2023 年終盤はこれで遊んでいました。

2023 年中盤までは oapi-codegen + chi の組み合わせで遊んでいましたが紹介記事をきっかけに乗り換えました。

OpenAPI での定義に対して厳密に実装ができる印象があります。 oapi-codegen + chi は割と定義通りにレスポンスを返せていなかったなと ogen を使ってて感じました。
ただ、ogen では 3xx 系のレスポンスがうまく扱えない問題がありそうでそこはちょっと困っています。
net/htttpclient が自動でリダイレクトするので結果として 200 系が返ってきます。一方で ogen が自動生成したコードでは 3xx 系のステータスコードを期待するように実装されているのでエラー扱いされてしまいます。

ORM どうしよう

Go 言語の ORM なにがいいのか問題 ...
当たり前ですが銀の弾丸はないですよね。
開発規模や開発フェイズによって使いたい ORM も変わりそうですし。

2023年中盤までは ent で遊んでいましたが地味にクセがあり、テーブル定義の実装方法もすぐ忘れたりするのでしっくりきませんでした。

https://entgo.io/ja/

2023年終盤は prisma でテーブル定義を管理・マイグレーションして Go の実装は database/sql で頑張る方式にしています。( prisma 開発者体験めっちゃいいなって思ってます。 )

https://www.prisma.io/

ただ、prisma も痒い所に手が届かなそうだったりするのでこんなネタ記事も書いてみました。

https://zenn.dev/otakakot/articles/85e3620bcd2daa

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 で気分を上げる

https://marketplace.visualstudio.com/items?itemName=PKief.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",
  }

VSCode で Go: Fill struct が使えなくなった ...

それは突然訪れました ...

cmd + shift + p から Go: Fill struct が消えたのです ...

https://github.com/golang/vscode-go/releases/tag/v0.40.0

よくよく調べると上記 release ノートに辿り着きました。
VSCode のリファクタリング機能で同等のことができるので不要になったとのことです。

cmd + shift + p -> fill の導線が cmd + .に変わりました!
これはちょっと便利になりましたね!!

プロジェクトレイアウト

2023年9月21日(たぶん…) 全 Gopher が待ち望んでいたドキュメント "Organizing a Go module - The Go Programming Language" が公開されました。

https://go.dev/doc/modules/layout

長年頭を悩まされてきた golang-standards スタンダードじゃない問題もついに終止符が打たれたといえるのではないでしょう。

とはいえ、Go チームから公開されたドキュメントのレイアウトはとてもシンプルなものです。
基本的にはこのレイアウトで始めるのがよいでしょう。
でもなんだかんだコード量が多くなってくるとレイヤーを分けたくなってきちゃいますよね。

ということで以下のようなディレクトリ構成を考えました。

サンプルリポジトリ

TODOをCRUDできるようなシンプルなアプリケーションを構築しています。

https://github.com/otakakot/2023-golang-project-layout

(途中で飽きて細かいところまで実装できていないです。あくまでこんな感じのディレクトリ構成という雰囲気を共有するためのリポジトリです ... )

ディレクトリ構成

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 はこのファイルを指定

https://www.openapis.org/

schema

  • テーブル定義 (prisma ) に関するファイルを配置
  • (ローカル環境で自動マイグレーションする仕組み化ができていないのでそのうちやりたい)

cmd

  • Go アプリのエントリーポイントとなる main.goxxx/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 の出力を変換するのが責務
  • gateway
    • repository に定義した interface を実装
    • 永続化層への保存が責務

application

ビジネスドメインに関するコードを駆使して各ユースケースの処理を実装

  • usecase
    • interface を定義
  • interactor
    • usecase に定義した interface を実装

domain

ドメインに関するコードの実装

  • model
    • 具体的なモデルを実装
    • ビジネスロジック
  • repository
    • model の 永続化に関する  interface を定義

ポイントっぽいこと

  • スタンダードではないけれど golang-standards/project-layout のエッセンスも参考

https://github.com/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() でいいかなって気持ち。

おわりに

2023年は6月に Go Conference 2023 に参加したり 会社でも家でも Go の実装ができたり Let’s Go Talk #10 にオフラインで参加したり Go 1.21 リリースパーティ & GopherCon 2023 報告会にオフラインで参加したり Go に関するオンラインイベントに参加したりと、やっと Gopher としての人生が開幕したなと感じた1年でした。

Go に関する本もいくつか出版されましたね。

中でも 「Go言語 100Tips ありがちなミスを把握し、実装を最適化する」 は読んでいろいろと発見がありました。

https://book.impress.co.jp/books/1122101133

ベストバイブックでした。 :clap:

2024 年も真の Gopher となるために開発を楽しみます。

(会社の輪読会で英語本を読み始め、意外にも読むことできるなと所感を得たのでいけるだろうと手を出した Efficient Go (English Edition) は速攻で心が折れて読めなかったので心残りです。)

おまけ: Go と関係ないけど気づき

技術負債に立ち向かう時、それは幸せなことなんですね。みなさんが使ってくれているからこそ問題となってくるわけで。使われなかったら誰も気づくことないのですし技術負債にならないですよね。だから私はこれから技術負債に出会ったらまずは感謝します。ありがとうございます。

テストが書きやすい・書きづらいという視点で実装していいんじゃないでしょうか。実装者が使いづらい(書きづらい)コードはきっと回り回ってみなさんにとっても使いづらいものになるのではないでしょうか。

Discussion