3
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.

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

Posted at

想定読者

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

要約

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

日本語読むより、こちらのレポジトリを見ていただくのが早いかもしれません。

flag パッケージ

flag パッケージとは、Go の標準ライブラリでコマンドラインのフラグをパースするツールです。 viper という有名パッケージもありますが、そちらに比べてシンプルなものになっています。

Value インターフェース

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

公式ドキュメントを読むと、以下のようになります。

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 インターフェースはどういうインターフェースかについてもこちらに記載されていました。以下のようなインターフェースで、String は値を表示するためのメソッドで、Set がフラグを受け取ったときに値を解釈して入れるためのメソッドのようです。

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

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

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

まとめ

  • 時間や配列、カスタム構造体も flag ライブラリの Value インターフェースを利用して、比較的簡単にパースする実装を紹介しました
    • XXXValue 構造体を定義しましたが、実践でもこの手のラッパー構造体を挟んだ方が何かと扱いやすそうかなと思いました
  • 利用したライブラリはすべて標準ライブラリなので、golang に慣れ親しみたい人もレポジトリは参考になるかもしれません
3
1
2

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
3
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?