11
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

uber-go/fxを触りました。

Last updated at Posted at 2019-12-02

はじめに

こんにちは、昨日に引き続きMedia Do Advent Calendar 3日目を担当するおかざわです。

uber-go/digの記事と同じく、ubar-go/fxを勉強を勉強する機会がありましたのでその時のまとめになります。
今回は短いですが、昨日と同じく暖かい目で見てやってください。

概要

Package fxは、再利用可能で構成可能なモジュールからアプリケーションを簡単に構築できるフレームワークです。
Fxアプリケーションは依存関係注入を使用して、関数呼び出しを手動でつなぐ面倒な作業を行うことなくグローバルを排除します。依存性注入に対する他のアプローチとは異なり、Fxは単純なGo関数で動作します。構造体タグを使用したり、特殊な型を埋め込む必要がないため、FxはほとんどのGoパッケージで自動的に動作します。

uber-go/fxのdocのgoogle翻訳より

やはり、ぱっと見では何をしてくれるものなのかよくわからないですが、以下の部分からuber-go/digと同じ様なことをすることが伺えます。(実際uber-go/fxはuber-go/digを内部で使用している)

Fxアプリケーションは依存関係注入を使用して、関数呼び出しを手動でつなぐ面倒な作業を行うことなくグローバルを排除します。

実際に使ってみて何をするのかを調べます。

インストール

go get go.uber.org/fx@v1

go modulesをしている前提のインストールです。
それ以外はubar-go/fxを参照ください。

使う

比較

uber-go/digを触りました。で使用したコードを使ってfxと見比べてみます。

package main

// 通常とdigとfxの比較

import (
	"fmt"

	"go.uber.org/dig"
	"go.uber.org/fx"
)

type Usecase interface {
	Use()
}

type Repository interface {
	RepoPrint()
}

type usecase struct {
	repo Repository
}

func NewUsecase(r Repository) Usecase {
	return &usecase{
		repo: r,
	}
}
func (u usecase) Use() {
	u.repo.RepoPrint()
}

type repository struct{}

func NewRepository() Repository {
	return &repository{}
}

func (r repository) RepoPrint() {
	fmt.Println("1")
}

func main() {

	// 通常の依存関係注入
	repo := NewRepository()
	usecase := NewUsecase(repo)
	usecase.Use()

	// uber-go/digを使った依存関係注入
	c := dig.New()
	c.Provide(NewUsecase)
	c.Provide(NewRepository)
	c.Invoke(func(u Usecase) {
		u.Use()
	})

	// uber-go/fxを使った依存関係注入
	// 中身ではuber-go/digが使われている(なのでdigでできることはだいたいできる)
	app := fx.New(
		fx.Provide(
			NewUsecase,
			NewRepository,
		),
		fx.Invoke(func(u Usecase) {
			u.Use()
		}),
	)
	// 完了する
	app.Done()
}

main()の部分で通常のDI、digを使ったDI、fxを使ったDIを見比べることができます。
digではコンテナを生成し、コンテナに対して、Provide()Invoke()をしますが、
fxではappを生成するタイミングでProvide()Invoke()を行っています。
また、生成したappはapp.Done()を行って処理を終了しています。

fxの機能

昨日のuber-go/digの説明をしていた時はInvoke()で処理を実行して終了するものを扱うのが前提でした。
uber-go/fxも同じくInvoke()で処理を実行しますが、生成したappを「実行」することができます。

これを説明するためにはhttpサーバ の様な「起動」と「終了」を明確に実行できるアプリケーションを作るのがわかりやすいです。

package main

// Start&StopとRunの活用

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"go.uber.org/fx"
)

func NewMux(lc fx.Lifecycle) *http.ServeMux {
	log.Print("Executing NewMux.")
	mux := http.NewServeMux()
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	lc.Append(fx.Hook{
		OnStart: func(context.Context) error {
			log.Print("Starting HTTP server.")
			go server.ListenAndServe()
			return nil
		},
		OnStop: func(ctx context.Context) error {
			log.Print("Stopping HTTP server.")
			return server.Shutdown(ctx)
		},
	})

	return mux
}

func Register(mux *http.ServeMux) {
	// 「/」の設定
	log.Println("Create Handler Default World")
	mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "Default World")
		log.Println("Passing Default World")
	}))
	// 「/hello」の設定
	log.Println("Create Handler Hello World")
	mux.HandleFunc("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "Hello World")
		log.Println("Passing Hello World")
	}))
}

func main() {
	app := fx.New(
		fx.Provide(
			NewMux,
		),
		fx.Invoke(Register),
	)
	app.Run()
}

Invoke()にはマルチプレクサの登録をする関数が指定されており、その関数を実行するために*http.ServeMuxを取得する必要がある。
(マルチプレクサ?ってなる人は → https://qiita.com/huji0327/items/c85affaf5b9dbf84c11e
Provide()内で*http.ServeMuxを返す関数であるNewMuxが指定されているため実行される。
サーバ の設定をNewMuxから取得し、マルチプレクサの登録をする。

上記がInvoke()内でする処理である。

app.Run()NewMuxの引数に設定されているfx.Lifecycleの処理を実行する。
(以降fx.Lifecycleをライフサイクルと呼びます)

lc.Append(fx.Hook{
        OnStart: func(context.Context) error {
            log.Print("Starting HTTP server.")
            go server.ListenAndServe()
            return nil
        },
        OnStop: func(ctx context.Context) error {
            log.Print("Stopping HTTP server.")
            return server.Shutdown(ctx)
        },
    })

OnStartはアプリケーション実行時に処理され、
OnStopはアプリケーション終了時に処理される。

上記の処理によりサーバ の起動と終了処理を実行することができる。

もし、複数ライフサイクルに処理が紐付けされていた場合、Invoke()するstructの順番がそのままライフサイクルのstartの順番、stopは逆の順番で処理されます。

実行方法

appを実行する方法は大きく分けて3種類あります。

app.Done()はappで作成したアプリケーションを実行しない時に使用します。
終了のシグナルを出し、アプリケーションを終了します。

app.Done()

app.Start()app.Stop()は明確にStartの処理とStopの処理を実行させたい時に使用します。
goのhttpサーバ だと、http.Get()などを用いて自動で通信させたりする時に重宝します。

app.Start(/*コンテキスト*/)
app.Stop(/*コンテキスト*/)

app.Run()は内部でrun(Done)しており、run()は上記のStart()+Stop()を持っています。
app.Run()実行時点でStart()の処理が走り、終了のシグナル(control + c)を受信後Stop()の処理を実行します。

app.Run()

fxはdigとは違いアプリケーションの実行周りも管理することができるライブラリです。

その他機能

dig.Inを活用したfxのサンプル
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"time"

	"go.uber.org/fx"
)

func NewLogger() *log.Logger {
	logger := log.New(os.Stdout, "" /* prefix */, 0 /* flags */)
	logger.Print("Executing NewLogger.")
	return logger
}

func NewDefaultHandler(logger *log.Logger) (http.Handler, error) {
	logger.Print("Executing NewDefaultHandler.")
	return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		logger.Print("Default World")
	}), nil
}

func NewHelloHandler(logger *log.Logger) (http.Handler, error) {
	logger.Print("Executing NewHelloHandler.")
	return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		logger.Print("Hello World")
	}), nil
}

func NewMux(lc fx.Lifecycle, logger *log.Logger) *http.ServeMux {
	logger.Print("Executing NewMux.")
	mux := http.NewServeMux()
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}
	lc.Append(fx.Hook{
		OnStart: func(context.Context) error {
			logger.Print("Starting HTTP server.")
			go server.ListenAndServe()
			return nil
		},
		OnStop: func(ctx context.Context) error {
			logger.Print("Stopping HTTP server.")
			return server.Shutdown(ctx)
		},
	})

	return mux
}

type inHandle struct {
	fx.In

	DefaultH http.Handler `name:"default"`
	HelloH   http.Handler `name:"hello"`
}

type setHandle struct {
	defaultH http.Handler
	helloH   http.Handler
}

func Register(mux *http.ServeMux, h *setHandle) {
	mux.Handle("/", h.defaultH)
	mux.Handle("/hello", h.helloH)
}

func main() {
	app := fx.New(
		fx.Provide(
			NewLogger,
			NewMux,
			// fx.Annotated{}は登録するfuncに対して名前をつけることができる
			// この挙動はfx.Outのstructを作成することでもできる
			fx.Annotated{
				Name:   "default",
				Target: NewDefaultHandler,
			},
			fx.Annotated{
				Name:   "hello",
				Target: NewHelloHandler,
			},
			func(h inHandle) *setHandle {
				return &setHandle{
					defaultH: h.DefaultH,
					helloH:   h.HelloH,
				}
			},
		),
		fx.Invoke(Register),
	)

	// app.Run()

	startCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := app.Start(startCtx); err != nil {
		log.Fatal(err)
	}

	http.Get("http://localhost:8080/")
	http.Get("http://localhost:8080/hello")

	stopCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if err := app.Stop(stopCtx); err != nil {
		log.Fatal(err)
	}
}

上記のサンプルのmain()のProvide()の様に

fx.Annotated{
	Name:   "default",
	Target: NewDefaultHandler,
},

と記述することでdig.Outを作らずともNameオプションやgroupオプションを設定することができます。

感想

uber-go/digを事前に勉強していたためそこまでつまずいたところはなかったですが、golangのhttpサーバの関連のstructについてやインターフェースの構造など、別のところの知識が不足していて躓くことが多かったです。
それと参考文献が本当にないのでサンプルを作るのが大変だった。
以上。

参考文献

doc:https://godoc.org/go.uber.org/fx
github:https://github.com/uber-go/fx
https://journal.lampetty.net/entry/understanding-http-handler-in-go
https://qiita.com/huji0327/items/c85affaf5b9dbf84c11e

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?