Golang の標準ライブラリのみで、時間や配列、カスタム構造体(独自の型)を引数として受け取れるようにする

## 想定読者
- **Go で CLI 作成時の引数処理に困っている人**
    - 特に、コンマ区切りの配列や特定のjsonを受け取りたいケース
- Go の標準ライブラリに慣れ親しみたい人
    - 特に flag を使ったフラグ付きの引数処理

## 要約

Go の標準ライブラリ(標準パッケージ) [flag](https://golang.org/pkg/flag/) を用いて `Value` インターフェースを実装することで、**時間や配列、カスタム構造体を引数として受け取る**実装を**シンプルに**実現できます。

日本語読むより、[こちらのレポジトリ](https://github.com/nrnrk/go-flag-example)を見ていただくのが早いかもしれません。 

## flag パッケージ

[flag パッケージ](https://golang.org/pkg/flag/)とは、Go の標準ライブラリでコマンドラインのフラグをパースするツールです。 [viper](https://github.com/spf13/viper) という有名パッケージもありますが、そちらに比べてシンプルなものになっています。

## Value インターフェース

さて、こちらの flag では、文字列や整数のパースはパッケージに含まれるメソッド `IntVar` や `StringVar` で行うことができますが、日付や時間、カスタム構造体のパースはどうやったら良いのでしょうか？

[公式ドキュメント](https://golang.org/pkg/flag/)を読むと、以下のようになります。

> Or you can create custom flags that satisfy the Value interface (with pointer receivers) and couple them to flag parsing by
> `flag.Var(&flagVal, "name", "help message for flagname")`

`Value` インターフェースなるものを実装することで `Var` メソッドを使って実現することができそうです。 

`Value` インターフェースはどういうインターフェースかについても[こちら](https://golang.org/pkg/flag/#Value)に記載されていました。以下のようなインターフェースで、`String` は値を表示するためのメソッドで、`Set` がフラグを受け取ったときに値を解釈して入れるためのメソッドのようです。

```golang
type Value interface {
    String() string
    Set(string) error
}
```

以下で具体的な実装例を通して、利用方法を確認していきます。

## 各種実装例

### URL

flag パッケージの例にも登場しますが、ほんの少しだけ修正を加えています。

#### `Value` インターフェースの実装

以下のような形で `Value` インターフェースを実装することで、URLの型としてパースすることができます。

```url.go
package main

import (
	"errors"
	"net/url"
)

// URL example from https://golang.org/pkg/flag/#Value
type URLValue struct {
	URL *url.URL
}

func (v URLValue) String() string {
	if v.URL != nil {
		return v.URL.String()
	}
	return ""
}

func (v URLValue) Set(s string) error {
	if u, err := url.Parse(s); err != nil {
		return errors.New(`Format is not acceptable`)
	} else {
		*v.URL = *u
	}
	return nil
}
```

#### `Var` メソッド呼び出し

`Value` インターフェースが実装できたら、以下のように `Var` メソッドを呼び出して URL をパースします。

```main.go
// URL のみバージョン
package main

import (
	"flag"
	"fmt"
	"net/url"
)

var u = &url.URL{}

func main() {
	fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
	// URL argument. Check `url.go` for details
	fs.Var(&URLValue{u}, "url", "URL to parse")

	fs.Parse([]string{
		"-url",
		"https://golang.org/pkg/flag/",
	})

	// URL
	fmt.Printf("url: {scheme: %q, host: %q, path: %q}\n", u.Scheme, u.Host, u.Path)
}
```

### 時刻

#### `Value` インターフェースの実装

```time.go
package main

import (
	"errors"
	"time"
)

const defaultLayout = time.RFC3339

// define acceptable layouts
var expectedLayouts = []string{
	defaultLayout,
	time.RFC1123,
	time.RFC1123Z,
}

// Time example
type TimeValue struct {
	Time *time.Time
}

func (v TimeValue) String() string {
	if v.Time != nil {
		return v.Time.Format(defaultLayout)
	}
	return ``
}

func (v TimeValue) Set(s string) error {
	success := false
	for _, layout := range expectedLayouts {
		t, err := time.Parse(layout, s)
		if err != nil {
			continue
		}
		*v.Time = t
		success = true
	}
	if !success {
		return errors.New(`Format is not acceptable`)
	}
	return nil
}
```

#### `Var` メソッド呼び出し

```main.go
// 時刻 のみバージョン
package main

import (
	"flag"
	"fmt"
	"time"
)

var t = &time.Time{}

func main() {
	fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
	// Time argument. Check `time.go` for details
	fs.Var(&TimeValue{t}, "time", "Time to parse (RFC3339, RFC1123, RFC1123Z)")

	fs.Parse([]string{
		"-time",
		"Mon, 02 Jan 2006 15:04:05 MST",
	})

	// Time
	fmt.Printf("time: %q\n", t.Format(defaultLayout))
}
```

### 時間

#### `Value` インターフェースの実装

```duration.go
package main

import (
	"errors"
	"time"
)

// Duration example
type DurationValue struct {
	Duration *time.Duration
}

func (v DurationValue) String() string {
	if v.Duration != nil {
		return v.Duration.String()
	}
	return ``
}

func (v DurationValue) Set(s string) error {
	d, err := time.ParseDuration(s)
	if err != nil {
		return errors.New(`Format is not acceptable`)
	}
	*v.Duration = d
	return nil
}
```

#### `Var` メソッド呼び出し

```main.go
// 時間 のみのバージョン
package main

import (
	"flag"
	"fmt"
	"time"
)

var d time.Duration

func main() {
	fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
	// Duration argument. Check `duration.go` for details
	fs.Var(&DurationValue{&d}, "duration", "Duration to parse (supported units: 'ns', 'ms', 's', 'm', 'h') ")

	fs.Parse([]string{
		"-duration",
		"75s",
	})

	// Duration
	fmt.Printf("duration: %d\n", d)
}

```

### 配列

#### `Value` インターフェースの実装

```array.go
package main

import (
	"fmt"
	"strings"
)

const arraySeparator = `,`

// Array example
type ArrayValue struct {
	Array *[]string
}

func (v ArrayValue) String() string {
	if v.Array != nil {
		return fmt.Sprintf("%#v", *v.Array)
	}
	return ``
}

func (v ArrayValue) Set(s string) error {
	*v.Array = strings.Split(s, arraySeparator)
	return nil
}
```

#### `Var` メソッド呼び出し

```main.go
// 配列 のみのバージョン
package main

import (
	"flag"
	"fmt"
)

var a []string

func main() {
	fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
	// Array argument. Check `array.go` for details
	fs.Var(&ArrayValue{&a}, "array", "Array to parse (spearator: ',') ")

	fs.Parse([]string{
		"-array",
		"a,b,c",
	})

	// Array
	fmt.Printf("array: %q\n", a)
}
```

### カスタム構造体

#### `Value` インターフェースの実装

```hoge.go
package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type Hoge struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func (h *Hoge) String() string {
	return fmt.Sprintf("Hoge{id: %d, name: %s}", h.ID, h.Name)
}

// Hoge (Custom struct) example
type HogeValue struct {
	Hoge *Hoge
}

func (v HogeValue) String() string {
	if v.Hoge != nil {
		return v.Hoge.String()
	}
	return ``
}

func (v HogeValue) Set(s string) error {
	var hoge Hoge
	if err := json.Unmarshal([]byte(s), &hoge); err != nil {
		return errors.New(`Format is not acceptable`)
	}
	*v.Hoge = hoge
	return nil
}
```

#### `Var` メソッド呼び出し

```main.go
// カスタム構造体 のみのバージョン
package main

import (
	"flag"
	"fmt"
)

var h = &Hoge{}

func main() {
	fs := flag.NewFlagSet("ExampleValue", flag.ExitOnError)
	// Hoge argument. Check `hoge.go` for details
	fs.Var(&HogeValue{h}, "hoge", "Hoge to parse (json format)")

	fs.Parse([]string{
		"-hoge",
		`{"id": 352, "name": "Diego"}`,
	})

	// Hoge (Custom struct)
	fmt.Printf("hoge: %#v\n", h)
}
```

## レポジトリ作成しました

上記の例を github のレポジトリに上げています。
https://github.com/nrnrk/go-flag-example

以下のコマンドで実行できます。

```bash
git clone git@github.com:nrnrk/go-flag-example.git
cd go-flag-example
go run .
```

## まとめ

- 時間や配列、カスタム構造体も flag ライブラリの `Value` インターフェースを利用して、比較的簡単にパースする実装を紹介しました
    - `XXXValue` 構造体を定義しましたが、実践でもこの手のラッパー構造体を挟んだ方が何かと扱いやすそうかなと思いました
- 利用したライブラリは**すべて標準ライブラリ**なので、golang に慣れ親しみたい人もレポジトリは参考になるかもしれません
    - 利用ライブラリ一覧
        - [flag](https://golang.org/pkg/flag/)
        - [net/url](https://golang.org/pkg/net/url/)
        - [time](https://golang.org/pkg/time/)
        - [fmt](https://golang.org/pkg/fmt/)
        - [encoding/json](https://golang.org/pkg/encoding/json/)
        - [errors](https://golang.org/pkg/errors/)