# [Go] uber-go/fx で curl ライクな CLI を作ってみる


published: 2024-12-18

uber-go/fx は Go の DI ライブラリ。
フレームワークに近く、アプリケーション全体で導入するのが望ましい。

fx はプログラム実行時に依存関係を決定し注入してくれる。
学習コストがほんのちょっとあり、自分の理解を整理するためにもサンプルアプリを作ってみた。


## サンプルアプリ

curl ライクな CLI を作ってみる。

・-X フラグで HTTP メソッドを指定
・URL を引数として渡す
・レスポンスボディを標準出力する


```bash
$ go run . -X GET https://example.com/
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
    
    // 省略
    // example.com へ HTTP GET リクエストをして、レスポンスボディを標準出力する

```

## コード

### `clientfx/client.go`

```go
package clientfx

import (
	"io"
	"net/http"
)

func New() IClient {
	return &Client{}
}

type IClient interface {
	Do(method string, url string) (string, error)
}

type Client struct{}

func (c *Client) Do(method string, url string) (string, error) {
	client := http.Client{}

	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return "", err
	}

	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	bodybytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(bodybytes), nil
}

```

### `clientfx/module.go`

```go
package clientfx

import "go.uber.org/fx"

var Module = fx.Module(
	"clientfx",
	fx.Provide(New),
)

```

### `cmdfx/cmd.go`

```go
package cmdfx

import (
	"flag"
	"fmt"

	"github.com/enuesaa/lab/data/go-fx-di-httpcall-cli/clientfx"
)

func New(client clientfx.IClient) ICmd {
	cmd := Cmd{
		client: client,
	}
	return &cmd
}

type ICmd interface {
	Parse() error
	Run() error
}

type Cmd struct {
	client clientfx.IClient

	url    string
	method string
}

func (c *Cmd) Parse() error {
	flag.StringVar(&c.method, "X", "GET", "HTTP Method. Example: GET, POST, PUT, DELETE")
	flag.Parse()

	args := flag.Args()
	if len(args) < 1 {
		return fmt.Errorf("missing required argument: <url>")
	}
	c.url = args[0]

	return nil
}

func (c *Cmd) Run() error {
	// http リクエスト
	body, err := c.client.Do(c.method, c.url)
	if err != nil {
		return err
	}
	fmt.Printf("%s", body)

	return nil
}

```

### `cmdfx/module.go`

```go
package cmdfx

import "go.uber.org/fx"

var Module = fx.Module(
	"cmdfx",
	fx.Provide(New),
)

```

### `logger.go`

```go
package main

import (
	"fmt"

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

func NewLogger() fxevent.Logger {
	return &Logger{}
}

type Logger struct{}

func (l *Logger) LogEvent(event fxevent.Event) {
	switch e := event.(type) {
	case *fxevent.Invoked:
		if e.Err != nil {
			fmt.Printf("Error: %s\n", e.Err.Error())
		}
	}
}

```

### `go.mod`

```mod
module github.com/enuesaa/lab/data/go-fx-di-httpcall-cli

go 1.24.0

require go.uber.org/fx v1.23.0

require (
	go.uber.org/dig v1.18.0 // indirect
	go.uber.org/multierr v1.10.0 // indirect
	go.uber.org/zap v1.26.0 // indirect
	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
)

```

### `main.go`

```go
package main

import (
	"github.com/enuesaa/lab/data/go-fx-di-httpcall-cli/clientfx"
	"github.com/enuesaa/lab/data/go-fx-di-httpcall-cli/cmdfx"
	"go.uber.org/fx"
)

func main() {
	fxapp := fx.New(
		cmdfx.Module,
		clientfx.Module,

		// エントリポイント
		fx.Invoke(func(cmd cmdfx.ICmd, client clientfx.IClient, shutdowner fx.Shutdowner) error {
			// fx.App をシャットダウン
			defer shutdowner.Shutdown()

			// メインロジック
			// コマンドライン引数をパース
			if err := cmd.Parse(); err != nil {
				return err
			}
			// http request
			if err := cmd.Run(); err != nil {
				return err
			}
			return nil
		}),

		// fx.App のロガー。Err のみ出力する
		fx.WithLogger(NewLogger),
	)
	fxapp.Run()
}

```

### memos

**fx.New()**

fx app を立ち上げる


mark: `main.go:10`

cmdfx.Module と clientfx.Module があり、
両者を fx.New() に渡している。


mark: `main.go:11`

エントリポイント


mark: `main.go:15`

実処理


mark: `main.go:19`

**Module**

ファクトリ関数を fx に登録


mark: `cmdfx/module.go:7`

**cmdfx.New()**

ファクトリ関数。clientfx.IClient へ依存している。
fx は関数のシグネチャを見て依存関係を構築する。


mark: `cmdfx/cmd.go:10`

cmdfx と clientfx は interface を介してやり取りしている。これにより mock へ差し替えできる


mark: `cmdfx/cmd.go:23`

## Links

- [https://github.com/uber-go/fx](https://github.com/uber-go/fx)
