1
1

More than 1 year has passed since last update.

Get started with Fx

Posted at

※ソース記事はこちら
ここではFxの基礎を紹介する。このチュートリアルで、以下を行う。

  • 空のアプリケーションを開始
  • HTTPサーバーを追加
  • サーバーにハンドラーを登録
  • アプリケーションにロギングを追加
  • ハンドラの結びつきを離すようリファクタリング
  • サーバーに別のハンドラーの追加
  • 実装の一般化

初めに、チュートリアルの残りのためにセットアップをする。

  1. 新しい空のプロジェクトを始める。
    mkdir fxdemo
    cd fxdemo
    go mod init example.com/fxdemo
    
  2. 最新バージョンのFxをインストールする。
    go get go.uber.org/fx@latest
    

最小限のアプリケーションの作成

Fxアプリケーションのhello-world相当のものをビルドしよう。このアプリケーションは、一連のログを出力する以外はまだ何もしない。

  1. 最小限のmain.goを書く。
    package main
    import "go.uber.org/fx" 
    func main() {
      fx.New().Run()
    }
    
  2. アプリケーションを実行する。
    go run .
    
    以下のような出力が見えるだろう。
    [Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
    [Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    [Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    [Fx] RUNNING
    
    これはFxアプリケーションにデフォルトで提供されているオブジェクトを表示しているが、まだ何も意味のあるものではない。CTRL+Cでアプリケーションを終了する。
    [Fx] RUNNING
    ^C
    [Fx] INTERRUPT
    

ここでしたこと

引数無しのFx.Newを呼ぶことで空のFxアプリケーションをビルドした。アプリケーションは通常、コンポーネントをセットアップするために、引数を渡す。
その後、App.Runメソッドでアプリケーションを実行した。このメソッドは終了するためのシグナルを受け取るまでブロックし、その後、終了する前に必要な何らかのクリーンナップ操作を実行する。
Fxは主に、長時間実行するサーバーアプリケーションを意図している。つまりこれらのアプリケーションは通常、シャットダウンするときに配布システムからシグナルを受け取る。

HTTPサーバーの追加

前のセクションで、何もしない最小限のFxアプリケーションを書いた。それに対してHTTPサーバーを追加しよう。

  1. HTTPサーバーを立ち上げる関数を書く。
    // NewHTTPServerはHttpサーバーを立ち上げ、Fxアプリケーションが開始すると、要求を受け付け始める。
    func NewHTTPServer(lc fx.Lifecycle) *http.Server {
      srv := &http.Server{Addr: ":8080"}
      return srv
    }
    
    これは十分ではなく、HTTPサーバーの開始方法をFxに伝える必要がある。追加のfx.Lifecycle引数がそのためにある。
  2. fx.Lifecycleオブジェクトとともにアプリケーションに対してlifecycleフックを追加する。これはFxにHTTPサーバーの開始と終了方法を伝える。
    func NewHTTPServer(lc fx.Lifecycle) *http.Server {
      srv := &http.Server{Addr: ":8080"}
      lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
          ln, err := net.Listen("tcp", srv.Addr)
          if err != nil {
            return err
          }
          fmt.Println("Starting HTTP server at", srv.Addr)
          go srv.Serve(ln)
          return nil
        },
        OnStop: func(ctx context.Context) error {
          return srv.Shutdown(ctx)
        },
      })
      return srv
    }
    
  3. 上のFxアプリケーションに対し、fx.Provideでこちらを提供する。
    func main() {
      fx.New(
        fx.Provide(NewHTTPServer),
      ).Run()
    }
    
  4. アプリケーションを実行する。
    [Fx] PROVIDE    *http.Server <= main.NewHTTPServer()
    [Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
    [Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    [Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    [Fx] RUNNING
    
    え?何がおかしいかっただろうか?出力の最初の行は、サーバーが提供されたことを表しているが、"Starting HTTP server"メッセージは含まれていない。サーバーは実行されなかった。
  5. それを修正するには、起動したサーバーを要求するfx.Invokeを追加する。
    fx.New(
      fx.Provide(NewHTTPServer),
      fx.Invoke(func(*http.Server) {}),
    ).Run()
    
  6. 再度アプリケーションを実行する。今回は"Starting HTTP server"メッセージが出力に見えるに違いない。
    [Fx] PROVIDE    *http.Server <= main.NewHTTPServer()
    [Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
    [Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    [Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    [Fx] INVOKE             main.main.func1()
    [Fx] HOOK OnStart               main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer)
    Starting HTTP server at :8080
    [Fx] HOOK OnStart               main.NewHTTPServer.func1() called by main.NewHTTPServer ran 
    successfully in 7.958µs
    [Fx] RUNNING
    
  7. 実行中のサーバーに要求を送る。
    $ curl http://localhost:8080
    404 page not found
    
    リクエストは4o4なぜなら、サーバーはそれを扱う方法をまだ知らないからである。次のセクションでそれを修正する。
  8. アプリケーションを停止する。
    ^C
    [Fx] INTERRUPT
    [Fx] HOOK OnStop                main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer)
    [Fx] HOOK OnStop                main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 129.875µs
    

ここでしたこと

アプリケーションにHTTPサーバーを追加するためにfx.Provideを使った。サーバーはFxアプリケーションライフサイクルに接続する。App.Runが呼び出されると、要求を処理し始め、アプリケーションが終了シグナルを受け取ると、実行を停止する。HTTPサーバーが、アプリケーション内の他のコンポーネントから直接参照されていなくても、常にインスタンス化されるように、fx.Invokeを使う。

ハンドラの登録

サーバーがリクエストを受け取ることができるように作ったが、まだそれを扱う方法を知らない。修正しよう。

  1. 基本的なHTTPハンドラを定義し、到着するリクエストボディをレスポンスにコピーする。ファイルの末尾に次のように追加する。
    // Echoハンドラは、リクエストボディをコピーし、レスポンスに戻すhttp.Handler.
    type EchoHandler struct{}
    
    // NewEchoHandlerは新しいEchoハンドラを作る.
    func NewEchoHandler() *EchoHandler {
      return &EchoHandler{}
    }
    
    // ServeHTTPはHTTPリクエストを/echoエンドポイントで処理する。
    func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
      if _, err := io.Copy(w, r.Body); err != nil {
        fmt.Fprintln(os.Stderr, "Failed to handle request:", err)
      }
    }
    
    これをアプリケーションにProvideする。
    fx.Provide(
       NewHTTPServer,
       NewEchoHandler,
     ),
     fx.Invoke(func(*http.Server) {}),
    
  2. 次に*http.ServeMuxを作る関数を書く。*http.ServeMuxは、サーバーによって受け取られたリクエストを異なるハンドラにルーティングする。まず始めに/echoに送られたリクエストを*EchoHandlerにルーティングするため、そのコンストラクタは引数として*EchoHandlerを受け取るべきである。
    // NewServeMuxはServeMuxを作り、リクエストを与えられたEchoHandlerにルーティングする.
    func NewServeMux(echo *EchoHandler) *http.ServeMux {
      mux := http.NewServeMux()
      mux.Handle("/echo", echo)
      return mux
    }
    
    同様に、これをアプリケーションにProvideする。
    fx.Provide(
       NewHTTPServer,
       NewServeMux,
       NewEchoHandler,
     ),
    
    注意すべきは、NewServeMuxは上のNewEchoHandlerに追加された。fx.Provide内に与えられたコンストラクタの順序は重要ではない。
  3. 最後にNewHTTPServer関数を修正し、この*ServeMuxへサーバーを接続する。
    func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server {
      srv := &http.Server{Addr: ":8080", Handler: mux}
      lc.Append(fx.Hook{
    
  4. サーバーを実行する。
    [Fx] PROVIDE    *http.Server <= main.NewHTTPServer()
    [Fx] PROVIDE    *http.ServeMux <= main.NewServeMux()
    [Fx] PROVIDE    *main.EchoHandler <= main.NewEchoHandler()
    [Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
    [Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
    [Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
    [Fx] INVOKE             main.main.func1()
    [Fx] HOOK OnStart               main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer)
    Starting HTTP server at :8080
    [Fx] HOOK OnStart               main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.459µs
    [Fx] RUNNING
    
  5. サーバーにリクエストを送る。
    $ curl -X POST -d 'hello' http://localhost:8080/echo
    hello
    

ここでしたこと

fx.Provideで、より多くのコンポーネントを追加した。これらのコンポーネントはコンストラクタにパラメータを追加することで、お互いに依存性を宣言している。Fxは与えられた関数のパラメータと戻り値によってコンポーネントの依存性を解決する。

ロガーの追加

アプリケーションは現在、標準出力に"Starting HTTP server"メッセージを出力し、標準エラーにエラーを出力している。標準出力と標準エラー出力はどちらもグローバル状態でもある。ロガーオブジェクトに出力すべきである。
チュートリアルのこのセクションでは、Zapを使うが、どのようなロギングシステムも使うことができるはずである。

  1. ZapロガーをアプリケーションにProvideする。このチュートリアルでは、zap.NewExampleを使うが、本番のアプリケーション用にはzap.NewProductionを使うか、よりカスタマイズされたロガーを作るべきである。
    fx.Provide(
       NewHTTPServer,
       NewServeMux,
       NewEchoHandler,
       zap.NewExample,
    ),
    
  2. EchoHandler上でロガーを保持するフィールドを追加し、NewEchoHandler内でこのフィールドにセットするため、新しいロガー引数を追加する。
    type EchoHandler struct {
      log *zap.Logger
    }
    
    func NewEchoHandler(log *zap.Logger) *EchoHandler {
      return &EchoHandler{log: log}
    }
    
  3. EchoHandler.ServeHTTPメソッド内で、標準エラー出力に出力する代わりにロガーを使う。
    func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
      if _, err := io.Copy(w, r.Body); err != nil {
        h.log.Warn("Failed to handle request", zap.Error(err))
      }
    }
    
  4. 同様に、NewHTTPServerを更新し、ロガーが来ることを期待し、それに対して"Starting HTTP server"メッセージをロギングする。
    func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server {
      srv := &http.Server{Addr: ":8080", Handler: mux}
      lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
          ln, err := net.Listen("tcp", srv.Addr)
          if err != nil {
            return err
          }
          log.Info("Starting HTTP server", zap.String("addr", srv.Addr))
          go srv.Serve(ln)
    
  5. (任意)同様にFx自身のログのために同じロガーを使うことができる。
    func main() {
      fx.New(
        fx.WithLogger(func(log *zap.Logger) fxevent.Logger {
          return &fxevent.ZapLogger{Logger: log}
      }),
    
    これは[FX]メッセージをロガーに出力するメッセージに置き換える。
  6. アプリケーションを起動する。
    {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"}
    {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"}
    {"level":"info","msg":"provided","constructor":"main.NewEchoHandler()","type":"*main.EchoHandler"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"}
    {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"}
    {"level":"info","msg":"invoking","function":"main.main.func2()"}
    {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"}
    {"level":"info","msg":"Starting HTTP server","addr":":8080"}
    {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"6.292µs"}
    {"level":"info","msg":"started"}
    
  7. それに対してリクエストをポストする。
    $ curl -X POST -d 'hello' http://localhost:8080/echo
    hello
    

ここでしたこと

fx.Provideでアプリケーションにもう一つのコンポーネントを追加し、メッセージを出力するために必要な別のコンポーネントの中にそれをインジェクトした。そのために、コンストラクタに新しいパラメータを追加する必要があっただけだった。
任意のステップでは、Fxに対してFx自身の処理のためのカスタムロガーを提供したいと命じた。既存のfxevnt.ZapLoggerを使い、インジェクトされたロガーからカスタムロガーを作った。そうすることで、すべてのログは同じフォーマットに従う。

登録の分離

上のNewServeMuxは、EchoHandler上で明示的に依存して宣言している。これは不必要な緊密な結びつきである。NewServeMuxは正確なハンドラ実装を本当に知る必要はあるだろうか?NewServeMuxのためにテストを書きたいとき、EchoHandlerを構築する必要はあるべきではない。
これを修正しよう。

  1. main.goにRoute型を定義する。これはhttp.Handlerの拡張であり、そこでハンドラはその登録パスを知る。
    // Routeは一つのhttp.Handlerであり、そこに登録されたmuxパターンを知っている。
    type Route interface {
      http.Handler
    
      // Patternは登録されているパスを報告する。
      Pattern() string
    }
    
  2. EchoHandlerを修正し、このインターフェイスを実装する。
    func (*EchoHandler) Pattern() string {
      return "/echo"
    }
    
  3. main()内でNewEchoHandlerエントリをアノテーションし、ハンドラがRouteとしてProvideすべきことを明示する。
    fx.Provide(
      NewHTTPServer,
      NewServeMux,
      fx.Annotate(
        NewEchoHandler,
        fx.As(new(Route)),
      ),
      zap.NewExample,
    ),
    
  4. NewServeMuxを修正し、Routeを受け取り提供されているパターンを使用する。
    // NewServeMuxはServeMuxを作り、リクエストを与えられたRouteに対してルーティングする 
    func NewServeMux(route Route) *http.ServeMux {
      mux := http.NewServeMux()
      mux.Handle(route.Pattern(), route)
      return mux
    }
    
  5. アプリケーションを起動する。
    {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"}
    {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])","type":"main.Route"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"}
    {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"}
    {"level":"info","msg":"invoking","function":"main.main.func2()"}
    {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"}
    {"level":"info","msg":"Starting HTTP server","addr":":8080"}
    {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"10.125µs"}
    {"level":"info","msg":"started"}
    
  6. それに対してリクエストを送る。
    $ curl -X POST -d 'hello' http://localhost:8080/echo
    hello
    

ここでしたこと

利用者から実装を分離するインターフェイスを導入した。その後、Provideされたコンストラクタをfx.Annoateアノテーションし、fx.Asその結果をインターフェイスにキャストした。そうすることで、NewEchoHandler*EchoHandlerを戻し続けることができた。

もう一つのハンドラの登録

上で定義したハンドラは単一のハンドラを持っている。別のものを追加しよう。

  1. 同じファイル内で新規のハンドラを作る。
    // HelloHandlerはHTTP handlerであり、ユーザーに挨拶を出力する。 
    type HelloHandler struct {
      log *zap.Logger
    }
    
    // NewHelloHandlerは新規のHelloHandlerを作る。
    func NewHelloHandler(log *zap.Logger) *HelloHandler {
      return &HelloHandler{log: log}
    }
    
  2. このハンドラにRouteインターフェイスを実装する。
    func (*HelloHandler) Pattern() string {
      return "/hello"
    }
    
    func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
      body, err := io.ReadAll(r.Body)
      if err != nil {
        h.log.Error("Failed to read request", zap.Error(err))
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
      }
    
      if _, err := fmt.Fprintf(w, "Hello, %s\n", body); err != nil {
        h.log.Error("Failed to write response", zap.Error(err))
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
      }
    }
    
    このハンドラはリクエストボディを読み、呼び出し元にようこそメッセージを書き出す。
  3. これをアプリケーションに、NewEchoHandlerの隣に、RouteとしてProvideする。
    fx.Annotate(
      NewEchoHandler,
      fx.As(new(Route)),
      ),
    fx.Annotate(
      NewHelloHandler,
      fx.As(new(Route)),
    ),
    
  4. アプリケーションを実行する。サービスが開始に失敗する。
    [Fx] PROVIDE    *http.Server <= main.NewHTTPServer()
    [Fx] PROVIDE    *http.ServeMux <= main.NewServeMux()
    [Fx] PROVIDE    main.Route <= fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])
    [Fx] Error after options were applied: fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from:
    [...]
    [Fx] ERROR              Failed to start: the following errors occurred:
     -  fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from:
    [...]
    Failed: cannot provide function "main".NewHelloHandler ([..]/main.go:53): cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80)
    
    多くの出力があるが、エラーメッセージ内に、以下の出力があることがわかる。
    cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80)
    
    これはFxがアノテーション無しで、コンテナ内に同じ型の二つのインスタンスが許可されていないため、失敗している。NewServeMuxはどのRouteを使うかわからない。修正しよう。
  5. NewEchoHandlerNewHelloHandlermain()内で両方のハンドラのために名前でアノテーションする。
    fx.Annotate(
      NewEchoHandler,
      fx.As(new(Route)),
      fx.ResultTags(`name:"echo"`),
    ),
    fx.Annotate(
      NewHelloHandler,
      fx.As(new(Route)),
      fx.ResultTags(`name:"hello"`),
    ),
    
  6. もう一つのRouteパラメータをNewServeMuxに追加する。
    // NewServeMuxはServeMuxを作り、リクエストを与えられたRouteに対してルーティングする
    func NewServeMux(route1, route2 Route) *http.ServeMux {
      mux := http.NewServeMux()
      mux.Handle(route1.Pattern(), route1)
      mux.Handle(route2.Pattern(), route2)
      return mux
    }
    
  7. main()内でNewServeMuxが二つの名前の値をピックするようにアノテーションする。
    fx.Provide(
      NewHTTPServer,
      fx.Annotate(
        NewServeMux,
        fx.ParamTags(`name:"echo"`, `name:"hello"`),
      ),
    
  8. プログラムを起動する。
    {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"name:\\\"echo\\\"\" \"name:\\\"hello\\\"\"])","type":"*http.ServeMux"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"name:\\\"echo\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"echo\"]"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"name:\\\"hello\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"hello\"]"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"}
    {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"}
    {"level":"info","msg":"invoking","function":"main.main.func2()"}
    {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"}
    {"level":"info","msg":"Starting HTTP server","addr":":8080"}
    {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"56.334µs"}
    {"level":"info","msg":"started"}
    
  9. リクエストを送る。
    $ curl -X POST -d 'hello' http://localhost:8080/echo
    hello
    
    $ curl -X POST -d 'gopher' http://localhost:8080/hello
    Hello, gopher
    

ここでしたこと

既存の型と同じ型を持つ値を生成するコンストラクタを追加した。コンストラクタをfx.ResultTagでアノテーションし、名前つきの値を生成し、利用者はfx.ParamTagで名前付きの値を利用した。

多くのハンドラを登録

前回のセクションでは、二つのハンドラを追加したが、NewServeMuxを作る際に、それらを明示的に名前で参照している。さらにハンドラを追加する場合に、これはすぐに不便になるだろう。
NewServeMuxがハンドラの数や名前がわからない場合、代わりに登録すべきハンドラのリストを単に受け取るのが好ましい。
やってみよう。

  1. NewServeMuxを修正し、Routeオブジェクトのリストを操作するように修正する。
    func NewServeMux(routes []Route) *http.ServeMux {
      mux := http.NewServeMux()
      for _, route := range routes {
        mux.Handle(route.Pattern(), route)
      }
      return mux
    }
    
  2. main内のNewServeMuxエントリをアノテーションし、"routes"グループの内容を含むスライスを受け取るよう明示する。
    fx.Provide(
      NewHTTPServer,
      fx.Annotate(
        NewServeMux,
        fx.ParamTags(`group:"routes"`),
      ),
    
  3. 新規関数AsRouteを定義し、このグループをフェードする関数を作る。
    // AsRouteは与えられたコンストラクタに対して、"routes"グループにルートを提供することを宣言するアノテーションである。
    func AsRoute(f any) any {
      return fx.Annotate(
        f,
        fx.As(new(Route)),
        fx.ResultTags(`group:"routes"`),
      )
    }
    
  4. main内のNewEchoHandlerNewHelloHandlerのコンストラクタをAsRouteでラップし、このグループ内でそれぞれが自分のルートをフィードするようにする。
    fx.Provide(
      AsRoute(NewEchoHandler),
      AsRoute(NewHelloHandler),
      zap.NewExample,
    ),
    
  5. 最後にアプリケーションを起動する。
    {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"group:\\\"routes\\\"\"])","type":"*http.ServeMux"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"}
    {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"}
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"}   
    {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"}
    {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"}
    {"level":"info","msg":"invoking","function":"main.main.func2()"}
    {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"}
    {"level":"info","msg":"Starting HTTP server","addr":":8080"}   
    {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"5µs"}
    {"level":"info","msg":"started"}
    
  6. リクエストを送る。
    $ curl -X POST -d 'hello' http://localhost:8080/echo
    hello
    
    $ curl -X POST -d 'gopher' http://localhost:8080/hello
    Hello, gopher
    

ここでしたこと

NewServeMuxをアノテーションし、スライスとして値グループを利用し、既存のハンドラのコンストラクタをこの値グループ内でフィードするようにアノテーションした。アプリケーション内の他のどのようなコンストラクタも、結果がRouteインターフェイスに適合する限り、この値グループ内で値をフィードすることができる。それらはすべてお互いに収集され、ServeMuxコンストラクタに渡される。

参照リソース

1
1
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
1
1