はじめに
こんにちは、昨日に引き続き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