はじめに
2026年8月3日〜7日にかけて開催されたDMM Sprint Goコースに参加してきました。
参加レポートはこちらになります。
この記事では、自分が期間中に学んだことをアウトプットとして、もくもくと書いていきます。
著者はGo、アーキテクチャに関する理解が間違っている可能性があります。
間違っていたら教えてもらえると助かります。
自己紹介
はじめまして、もりりんです。
情報系の大学に通っている28卒で、普段は TypeScriptをメインに利用しており、GoはDMM Sprint Goで初めて書きました。
Goの記法
まず最初に、Go言語の特徴として戻り値とエラーを返すというものです。
func ReadAll(r io.Reader) (string, error) {
b, err := io.ReadAll(r)
if err != nil {
return "", err
}
return string(b), nil
}
次に「文字の先頭が大文字なら、そのパッケージの外から参照できる」というものです。
今回sqlxを用いてDB操作を行いましたが、DTOに値を入れる際にフィールド名を小文字にしていたことで外部パッケージであるsqlxが値を入れられずにエラーを吐いてしまいました。
type UserDTO struct {
id uint64 `db:"id"`
name string `db:"name"`
}
レシーバー
自分はクラスがある言語を使っていたので、結構レシーバーはつまずきました。
type BankAccount struct {
Owner string
Balance int
}
func (b *BankAccount) Deposit(amount int) {
b.Balance += amount
}
func main() {
account := BankAccount{
Owner: "Taro",
Balance: 1000,
}
account.Deposit(500)
fmt.Println(account.Balance) // 1500
}
実体がある構造体に対して、Depositに値を渡すとその構造体に値を入れることができます。
レシーバーで定義された関数は、構造体に結び付けられるためDeposit(500)などの呼び方はできません。
構造体にメソッドを追加するイメージです。
オニオンアーキテクチャ
長期のインターンでクリーンアーキテクチャについては概念的なものは知っていましたが、オニオンアーキテクチャは初めて聞きました。
クリーンアーキテクチャとオニオンアーキテクチャの違いは、調べた感じ各層の役割がちょっとずつ違いそう?いう印象を感じました。
詳しく解説されている記事がありました。
どちらも共通して、依存関係が内側に向くことと内側は外側の具体的な実装を知ってはいけないというものです。
| オニオンアーキテクチャ | クリーンアーキテクチャ |
|---|---|
| Domain | Entities |
| Usecase | Use Cases |
| Infrastructure | Interface Adapters / Frameworks & Drivers |
| UI | Frameworks & Drivers 周辺 |
完全な1対1対応ではない気がしますが、自分の認識的にはこんな感じでした。
この先では、例を用いてオニオンアーキテクチャについてのみ記載します。

出典 : https://little-hands.hatenablog.com/entry/2017/10/11/075634
基本
依存関係は外側から内側に向き、内側の層は外側にあるDBやHTTP、フレームワークなどの具体的な実装を知りません。
UI層
- リクエストを受け取る
- jsonをGoが解釈できるようにデコードし、構造体に入れ替え
-
server.goでは、httpやルーティングの設定などを行い、Usecaseのオブジェクトを生成するために、DomainやInfraを呼び出して注入している
Usecase層
- UI層から渡されたものとドメインモデルを組み合わせてロジックを実装
- トランザクションの管理
- エンドポイントのメインとなる処理を記述
- Domain層で定義したRepositoryを読んで使う
トランザクションというのは、SQL操作でのデータ操作を1つのまとまりとして扱い、エラーが起こった際にロールバックできるような仕組みです。
ん?トランザクションなのになんでUsecase層に...?
自分はここで戸惑いました。
1つのユースケースでは、必ずしも1つのテーブルに対して1回だけ操作を行うわけではなく、複数のRepositoryを通して複数のDB操作を行う場合があります。
そのため、途中の1つのSQL操作でエラーが発生した場合、それまでに行ったDB操作をすべてロールバックしたいことがあります。
そこで、Usecase層でトランザクションをどの範囲で張るかをきめ、複数のDB操作を1つのトランザクションとしてまとめて実行します。
つまり、どの処理を1つのトランザクションとして扱うかはユースケースによって決まるため、Usecase層でトランザクションの範囲を管理してます。
// UI層から呼び出すためのインターフェース
type SignUpUseCase interface {
SignUp(ctx context.Context, username) (*user.User, error)
}
// おまじない
var _ SignUpUseCase = (*SignUpUseCaseImpl)(nil)
// 構造体の定義
type SignUpUseCaseImpl struct {
userRepo repository.User // domain層のrepository
transactor transactor.Transactor
}
// 構造体を実体にする関数
func NewUserCreateUseCase(
userRepo repository.User,
transactor transactor.Transactor,
) *SignUpUseCaseImpl {
return &SignUpUseCaseImpl{
userRepo: userRepo,
transactor: transactor,
}
}
func (uc *SignUpUseCaseImpl) SignUp(ctx context.Context, username string) (*user.User, error) {
result, err := uc.transactor.TransactionWithValue(ctx, func(ctx context.Context) (any, error) {
// 複数Repositoryの処理
pendingUser, err := user.NewPendingUser(username)
if err != nil {
return nil, err
}
user, err := uc.userRepo.Insert(ctx, pendingUser)
if err != nil {
return nil, err
}
return user, nil
})
...
return user, nil
}
Usecase層では、このように内側の層で定義した構造体を実体にして、組み合わせていきます。
Domain層
- アプリケーションのビジネス知識を表現している
- 機能を実装するときのコアになるものが書かれる
- 業務ロジックなど
- オブジェクトに対する責務がある → 構造体を定義する
- Domain層はUsecase→DomainとInfra→Domainの2方向から依存されることがある
type User struct {
id uint64
username string
createdAt time.Time
}
type PendingUser struct {
username string
}
// usecaseから呼び出され、構造体を実体する。
func NewPendingUser(username) (*PendingUser, error) {
user := &PendingUser{}
...
}
// バリデーション系はここで書く
func (u *PendingUser)SetUsername(username string) error {
u.username = username
return nil
}
domain/repository
ここでは、Infra層で実装するメソッドを抽象化し、定義しています。
type User interface{
Insert(ctx context.Context, pendingUser *user.PendingUser) (*user.User, error)
}
Infra層
ここでは、domain/repositoryで定義したRepositoryのinterfaceを実装します。
Usecase層がMySQLなどの具体的なDB実装へ直接依存するのではなく、Domain側で定義したinterfaceに依存し、Infra層がそのinterfaceを実装します。
このように、上位の処理が下位の具体的な実装ではなく抽象に依存するようにするのが、依存性逆転の原則(DIP)です。
依存性逆転の原理(DIP)の何がいいか
メリットとして以下があります。
- 変更に強くなる
- テストを作成しやすくなる
今回の場合、もしDBの先が変更したとしてもInfra層のみを変更すれば良いという状況になります。
// おまじない
var _ repository.User = (*UserRepoImpl)(nil)
// 構造体の定義
type UserRepoImpl struct{}
// 構造体を実体に
func NewUserRepository() *UserRepoImpl {
return &UserRepoImpl{}
}
func (ur *UserRepoImpl)Insert(ctx context.Context, pendingUser *user.PendingUser) (*user.User, error){
// トランザクションを取り出して実行
// SQL
return usr, nil
}
実装の中におまじないが登場します。
これはInfra層で実装するメソッドと、Domain/repositoryのinterfaceで定義したものがあっているかのコンパイルチェックをしてくれます。
もし、ここでDomain/repositoryが下のように2つ定義したのにも関わらず、Infra層で実装がされていないと先ほどのおまじないの部分でエラーが出るようになります。
type User interface{
Insert(ctx context.Context, pendingUser *user.PendingUser) (*user.User, error)
Update(ctx context.Context, updateUser *user.updateUser)
}
Insertの部分で構造体のポインタを受け取っているのは、今回は小さい構造体ですが、大きい構造体の場合、コピーすることになり重くなります。
ポインタを渡すと軽量になるのが1つの理由らしいです。
まとめ
今回のインターンでは、Goを用いたバックエンド開発を通して、Goの基本的な書き方だけでなく、オニオンアーキテクチャやデータベース操作、トランザクションなど、実際の開発を意識した実装について学ぶことができました。
特に、オニオンアーキテクチャに沿ったコードを実際に読み書きすることで、各層がどのような役割を持っているのか、なぜ依存関係を意識して設計する必要があるのかについて理解を深めることができました。
一方で、Goのポインタやinterface、Contextなど、まだ理解が十分ではない部分も多く見つかりました。
今回学んだ内容をインターンだけで終わらせず、今後の個人開発などでも実際に使いながら、より理解を深めていきたいと思います。