💬

GoのDIに触れ、改めて学び直して気づいたこと

2024/12/17に公開

この記事は、tacoms Advent Calendar 2024の17日目です!
他メンバーのAdvent Calendarはこちらからご覧ください!👇
https://qiita.com/advent-calendar/2024/tacoms

概要

これまでJavaのSpring Boot、PHPのLaravel、TypeScriptのNest.jsといったフレームワークを使い、開発を行ってきました。それらのフレームワークでは、DI(依存性注入)はDIコンテナによって解決されるもので、「何がどこで注入されているかを深く考える機会」は少なかったように思います。

最近、新たにGoのDI(特に手動でのDI)を初めて触れたとき、「これ、自分でやるのか…」とぼやいてしましました。
公式ドキュメントやサンプルプロジェクトを読み、Goらしいシンプルさと哲学に触れ、改めて学び直す良い機会となりました。
この記事では、過去の経験をベースにGoのDIを理解した流れと感想をまとめます。


他言語でのDIの感覚

他のフレームワークでは、DIは非常に便利な機能として提供されていました。以下のようなことが当たり前でした。

1. アノテーションやデコレータで自動注入

各フレームワークの代表的にSpringの@AutowiredやLaravelのApp::make()、Nest.jsの@Injectable()によって、依存関係が宣言的に解決されることが多いです。
インジェクションの仕方は様々ですが、以下一部の例です。

SpringBoot

@Service
public class MyService {
    @Autowired
    private Dependency dependency;
}

Laravel

class MyService
{
    protected $dependency;

    public function __construct(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }
}

Nest.js

@Injectable()
export class MyService {
    constructor(private dependency: Dependency) {}
}

2. DIコンテナが全体を管理

依存関係を全てDIコンテナに任せることで、明示的にインスタンスを生成する必要はほとんどありませんでした。

3. シンプルに書けるがブラックボックス化しがち

DIコンテナが便利すぎるため、どのインスタンスがどのように生成され、どこで使われているのかが見えにくくなるケースがありました。
また、依存関係を意識せず無邪気に各モジュールが使えることから短期的な生産性を向上させる反面、学習機会を奪ってしまうリスクがあります。

GoのDIに触れて気づいたこと

GoではDIコンテナは標準で用意されていません。代わりに、以下のように手動で依存関係を注入します。

package main

import "fmt"

// Serviceインターフェース
type Service interface {
    DoSomething() string
}

// 実装クラス
type RealService struct{}

func (r *RealService) DoSomething() string {
    return "Hello from RealService!"
}

// コントローラ
type Controller struct {
    service Service
}

// コンストラクタで依存性を注入
func NewController(s Service) *Controller {
    return &Controller{service: s}
}

func main() {
    // 実際の実装を注入
    service := &RealService{}
    controller := NewController(service)

    fmt.Println(controller.service.DoSomething())
}

学んだこと

1. インターフェース設計が基本

Goでは依存性を注入するために、インターフェースが重要な役割を果たします。依存する部分をインターフェースで定義し、実際の実装を注入します。この設計により、テストの際にモック実装を簡単に差し替えることができます。

type MockService struct{}

func (m *MockService) DoSomething() string {
    return "Mock Service Response"
}

過去に触れてきたフレームワークでは、インターフェースを意識せずともDIができていたため、どういう使われ方をするかは知っているものの「インターフェースを使う意味」について深く考える機会が少なかったと気づきました。

2. 明示的な依存注入

GoではDIコンテナがないため、すべての依存関係を明示的に記述します。
手間がかかる一方で、コードを読むだけで依存関係が全て分かるというメリットがあります。

3. 柔軟性とシンプルさ

手動でのDIは煩雑ではありますが、コードの振る舞いがわかりやすいです。このシンプルさはGoの哲学といったところなのでしょうか。

とはいえ、中・大規模プロジェクトでは

手動のDIはシンプルでわかりやすいものの、中・大規模なプロジェクトでは依存関係が増えるため管理が煩雑になりがちです。
その場合は、Google WireのようなDIライブラリを検討する価値がありそうです。
ちなみに、Wireではコンパイル時に依存関係を解決してから、コードを生成するらしく、ランタイムのオーバーヘッドがない(らしい)。

筆者がGoのDIから得た学びと感想

  1. シンプルさの大切さ
    他言語ではDIコンテナが便利すぎて依存解決がブラックボックス化していましたが、Goでは明示的なDIによってコードの可読性と理解が向上しました。
  2. 自分に合った方法を選ぶ
    小規模なプロジェクトでは手動のDIで十分ですが、中・大規模なプロジェクトではWireやUberのfxを使うとのも選択肢としてあがりそうです。

まとめ

GoのDIは、他言語のフレームワークに比べて記述量が増えますが、そのシンプルさが合理性と言えるでしょう。他言語のフレームワークに慣れているほど「面倒」と感じる部分もあるかもしれませんが、設計やコードの本質的な考え方について知るいい機会になりました。
まずはシンプルな手動DIを試し、プロジェクトの規模や複雑性を鑑みてWireのようなツールを導入してみることをおすすめします。

tacomsテックブログ

Discussion