fmt.Scannerを使う

本記事は[Go3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/go3) 14日目の記事です。前回の記事は[@tennashi](https://qiita.com/tennashi)さんの[最近よく書く HTTP サーバ基礎部分](https://tennashi.hatenablog.com/entry/2019/12/13/010938)でした。

## はじめに
`fmt`パッケージではスキャニング用の関数やインターフェイスが提供されています。これらを使用することで、パースなどの入力から受け取ったデータを意味ある何らかの値へと変換するような操作をより簡単に書くことができます。本記事ではそれらの紹介を行います。

## 使ってみる

紹介がてらにHTTP/1.0リクエストの最初の行であるRequest Lineをパースする例を考えてみたいと思います。Request Lineは以下のようにフォーマットが定められています [^1]。

```
Method Request-URI HTTP/major.minor CRLF
```
もちろん`strings.Split`などを使ってパースすることもできます。あえて書いてみると以下のようになると思います。

```go
func Example() {
	src := "GET /index.html HTTP/1.0\n"

	src = strings.TrimRight(src, "\n")
	splited := strings.Split(src, " ")
	if len(splited) != 3 {
		return
	}

	method, uri, version := splited[0], splited[1], strings.TrimPrefix(splited[2], "HTTP/")
	splited = strings.Split(version, ".")
	if len(splited) != 2 {
		return
	}
	major, err := strconv.Atoi(splited[0])
	if err != nil {
		return
	}
	minor, err := strconv.Atoi(splited[1])
	if err != nil {
		return
	}

	fmt.Printf("%s %s %d %d", method, uri, major, minor)
	// Output:
	// GET /index.html 1 0
}
```
`fmt`パッケージのスキャン用の関数を使うと以下のように書くことができます。ここでは`string`を入力にできる`fmt.Sscanf`を使っています。

```go
func Example() {
	src := "GET /index.html HTTP/1.0\n"

	var (
		method, uri  string
		major, minor int
	)
	if _, err := fmt.Sscanf(src, "%s %s HTTP/%d.%d\n", &method, &uri, &major, &minor); err != nil {
		return
	}

	fmt.Printf("%s %s %d %d", method, uri, major, minor)
	// Output:
	// GET /index.html 1 0
}
```

前者の例では必要であったトリムや型の変換などの操作が、後者の例では登場しません。その分後者のコードの方がスッキリしているように思えます。実際それらの操作はコードの読み手に前提としているフォーマットを意識させるため、可読性を低下させる要因になっていると思います。

私の経験上の話ではありますが、パースなどのフォーマットにしたがって入力から意味ある値を引き抜きたい際に、このように`fmt.Scan`を使うと簡単にそれができる場合がありました。以下では`fmt.Scan`の基本的な紹介をします。


## Fscan, Scan, Sscan

`fmt`パーケージで提供されているスキャニング用関数には、`io.Reader`からスキャンするFscan系、`io.Stdin`からスキャンするScan系、`string`からスキャンするSscan系があります。それぞれにデフォルト(後述)のフォーマットでスキャンする関数(`Fscan``Scan`、`Sscan`)、指定したフォーマットからスキャンする関数(`Fscanf`、`Scanf`、`Sscanf`)と、最初の改行までをスキャンする関数(`Fscanln`、`Scanln`、`Sscanln`)が用意されています([1])。

デフォルトのフォーマットはスペース、あるいは改行区切りというものです。このフォーマットは、フォーマットを指定しない`Fscan`、`Scan`、`Sscan`と`Fscanln`、`Scanln`、`Sscanln`で使用されます。ただ`Fscanln`、`Scanln`、`Sscanln`は最初の改行でスキャンを止めるので、これらは実際はスペース区切りでスキャンします。他方`Fscanf`、`Scanf`、`Sscanf`に対して指定するフォーマットではスペースと改行は区別されます。そのため以下のコードは一見すると同じように見えますが期待するフォーマットが違います。

```go
var a, b, c string

fmt.Scan(&a, &b, &c)

fmt.Scanf("%s %s %s", &a, &b, &c)
```

後者は指定した通りの`%s %s %s`を期待するものですが、前者の場合は例えば`a\nb\nc`のような入力でもエラーを返さずスキャンします。

ちなみにFscan系に渡した`io.Reader`は1 byteずつ読み込むのに使用されます([2])。そのため`*os.File`などの`Read`を呼ぶコストが高い型は、`bufio.Reader`などへラップしておいた方が良いと思います。下のベンチマークはファイル({'a'*2047}'\b'{'a'*2047})を読み込む際に、`*os.File`をそのまま渡した場合(BenchmarkFile)と`bufio.Reader`へとラップした場合(BenchmarkBuffer)との比較です。

```
go test -bench .
goos: darwin
goarch: amd64
BenchmarkFile-8         1000000000               0.00775 ns/op
BenchmarkBuffer-8       1000000000               0.000097 ns/op
```


## ScanState

スペースや改行区切りのような単純な場合はすでに用意されている`Fscanf`等を使用すればスキャンできます。しかし現実には、扱う対象がもっと複雑な場合も多いでしょう。例えばHTTP/1.0では、ヘッダーに,区切りで値が続くフィールドがいくつかありますが、何度値が続くかはもちろんわからないため、可変引数を渡すということもできません。

```
Allow: HEAD, GET, ...
```

`fmt.Scanner`([3])を用意することで、独自な実装でスキャンを行うことができます。`fmt.Scanner`のシグネチャーは以下の通りです。

```go
type Scanner interface {
	Scan(state ScanState, verb rune) error
}
```

`fmt.Print`などでの`fmt.Stringer`や`fmt.Formatter`などと同じように、
独自の`Scanner`を用意して`Fprint`などに渡せば、独自実装が使われるようになります([4])。

```go
// If the parameter has its own Scan method, use that.
if v, ok := arg.(fmt.Scanner); ok {
	err = v.Scan(s, verb)
	if err != nil {
		if err == io.EOF {
			err = io.ErrUnexpectedEOF
		}
		s.error(err)
	}
	return
}
```

`Scanner.Scan`の第1引数には`ScanState`([5])という以下のインターフェイスが、第2引数にはフォーマットで指定した%sのsなどの動詞(verb)が渡されます。

```go
type ScanState interface {
    ReadRune() (r rune, size int, err error)
    UnreadRune() error
    SkipSpace()
    Token(skipSpace bool, f func(rune) bool) (token []byte, err error)
    Width() (wid int, ok bool)
    Read(buf []byte) (n int, err error)
}
```

独自実装では、この`ScanState`を使って、すでに渡してある`io.Reader`から読み込んでいくことになります。ここに渡される`ScanState`の実体は`fmt`パッケージのunexportedな型になっています([6])。上記の通り`ScanState`は`io.Reader`と`io.RuneScannar`でもあります。しかしドキュメントに書かれている通り([7])、`ScanState`はすでに`io.RuneScannar`を満たしているので、`ScanState.Read`は使われるべきではないでしょう。`fmt`パッケージから渡される`ScanState.Read`の実装はその旨のエラーを返すようになっています([8])。

ただ`io.Reader`であることには変わりないので、独自実装側でもFscan系の第一引数に渡すことができます。

```go
func (f *foo) Scan(state fmt.ScanState, _ rune) error {
    // ...
    if _, err := fmt.Fprint(state, &f.bar); err != nil {
        return err
    }
}
```
Fprint系は渡された`io.Reader`が`io.RuneScanner`でもあった場合は、そちらを使うようになっています([9])。

```go
if rs, ok := r.(io.RuneScanner); ok {
    s.rs = rs
} else {
    s.rs = &readRune{reader: r, peekRune: -1}
}
```
これらを踏まえると、上であげたHTTP/1.0ヘッダーのフィールドは以下のようにスキャンすることができます。

```go
func Example() {
	src := "Allow: HEAD, GET"

	var field headerField
	if _, err := fmt.Sscan(src, &field); err != nil {
		return
	}

	fmt.Print(field.key, " ", field.values)
	// Output:
	// Allow [HEAD GET]
}

type headerField struct {
	key    headerFieldKey
	values headerFieldValues
}

func (h *headerField) Scan(state fmt.ScanState, _ rune) error {
	if _, err := fmt.Fscanf(state, "%s:%s\n", &h.key, &h.values); err != nil {
		return err
	}

	return nil
}

type headerFieldKey string

func (k *headerFieldKey) Scan(state fmt.ScanState, _ rune) error {
	read, err := state.Token(true, func(char rune) bool {
		return char != ':'
	})
	if err != nil {
		return err
	}

	*k = headerFieldKey(read)

	return nil
}

type headerFieldValues []string

func (vs *headerFieldValues) Scan(state fmt.ScanState, _ rune) error {
	state.SkipSpace()

	read, err := state.Token(true, func(char rune) bool {
		return char != '\n'
	})
	if err != nil {
		return err
	}

	splited := strings.Split(string(read), ",")
	*vs = make(headerFieldValues, len(splited))
	for i, v := range splited {
		(*vs)[i] = strings.TrimLeft(v, " ")
	}

	return nil
}
```
この例で登場する`ScanState.Token`は第2引数に渡した関数が`true`を返す限り読み続けます。上述の通り`fmt`パッケージが渡す`ScanState`の実装は`Read`でエラーを返すため、`ioutil.ReadAll`などの純粋な`io.Reader`が求められる場合には使用できません。この`Token`を使えば、独自実装内でも例えばEOFまで読むということができます。

```go
read, err := state.Token(true, func(char rune) bool {
    return true
})
if err != nil && err != io.EOF {
    // ...
}
```
あるいはGoでの関数リテラルがクロージャーであること([10])を利用して、Content-Length分だけ読み込むということもできます。

```go
type body struct {
	len    int
	content string
}

func (b *body) Scan(state fmt.ScanState, _ rune) error {
	var n int
	read, err := state.Token(false, func(char rune) bool {
		defer func() { n++ }()
		return n < b.len
	})
	if err != nil && err != io.EOF {
		return err
	}

	(*b).content = string(read)

	return nil
}
```
`fmt`パッケージから渡される`Token`の実装は、直前に読み込んだものを[]byteでバッファリングし、それを戻り値として返します([11])。そのためドキュメントに示されている通り([12])、`Token`を連続で読んだりすると、それぞれの戻り値が最後に読み込んだものに上書きされてしまいます。`Token`を使う際には受け取った値はすぐにcopyしておくと良いと思います。

また`ScanState`には`UnreadRune`がありますが、Fprint系に独自の`io.RuneScanner`を渡さない限り、すでに`UnreadRune`していた場合はエラーになり([13])、`fmt`パッケージの`ScanState`の実装はそのエラーを返しません([14]) [^2]。そのため`UnreadRune`を呼ぶ場合は、常に`ReadRune`の直後の1回だけになると思います。

```go
func (r *readRune) UnreadRune() error {
	if r.peekRune >= 0 {
		return errors.New("fmt: scanning called UnreadRune with no rune available")
	}
	// Reverse bit flip of previously read rune to obtain valid >=0 state.
	r.peekRune = ^r.peekRune
	return nil
}
```

```go
func (s *ss) UnreadRune() error {
	s.rs.UnreadRune()
	s.atEOF = false
	s.count--
	return nil
}
```

`ScanState`には他にも、%5sでの5のような幅を返す`Width`や、`Scanner`にはverb(動詞）も渡されるので、これらも活用できると思います。

## おわりに
本記事では`fmt.Scanner`について紹介しました。スペースやコンマ区切りの単純なケースではデフォルトの`Scanner`にそのまま任せることができます。スキャンの間により複雑な処理が必要な場合は、独自実装な`Scanner`に移譲することもできます。どちらの場合も`Fprint`などのAPIを使用することができます。特に'スペース'という言葉にスペースも改行も含まれる等のフォーマットの部分が複雑ですが、この記事がその解決の糸口になれば幸いです。

ちなみに`net.Conn`は`io.Reader`を満たしているので上記の例を組み合わせると、HTTP/1.0系のリクエストのパースは

```go
fmt.Fscan(conn, &req)
```
という具合に書けたりします。

***

1. https://golang.org/pkg/fmt/#hdr-Scanning
2. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L321
3.  https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L55
4. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
5. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L21
6. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L157
7. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L45
8. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L179
9. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
10. https://golang.org/ref/spec#Function_literals
11. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L446
12. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L38
13. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L370
14. https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L233

[1]: https://golang.org/pkg/fmt/#hdr-Scanning
[2]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L321
[3]:  https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L55
[4]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
[5]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L21
[6]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L157
[7]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L45
[8]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L179
[9]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
[10]: https://golang.org/ref/spec#Function_literals
[11]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L446
[12]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L38
[13]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L370
[14]: https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L233


[^1]: [RFC1945](https://tools.ietf.org/html/rfc1945#section-5.1)をもとに簡略化しています。
[^2]: ただ`ScanState`の内部実装である`ss`のcountはどんどん引かれたりしているので、もしかしたら修正される必要があるのかもしれません。