本記事はGo3 Advent Calendar 2019 14日目の記事です。前回の記事は@tennashiさんの最近よく書く HTTP サーバ基礎部分でした。
はじめに
fmt
パッケージではスキャニング用の関数やインターフェイスが提供されています。これらを使用することで、パースなどの入力から受け取ったデータを意味ある何らかの値へと変換するような操作をより簡単に書くことができます。本記事ではそれらの紹介を行います。
使ってみる
紹介がてらにHTTP/1.0リクエストの最初の行であるRequest Lineをパースする例を考えてみたいと思います。Request Lineは以下のようにフォーマットが定められています 1。
Method Request-URI HTTP/major.minor CRLF
もちろんstrings.Split
などを使ってパースすることもできます。あえて書いてみると以下のようになると思います。
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
を使っています。
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
に対して指定するフォーマットではスペースと改行は区別されます。そのため以下のコードは一見すると同じように見えますが期待するフォーマットが違います。
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
のシグネチャーは以下の通りです。
type Scanner interface {
Scan(state ScanState, verb rune) error
}
fmt.Print
などでのfmt.Stringer
やfmt.Formatter
などと同じように、
独自のScanner
を用意してFprint
などに渡せば、独自実装が使われるようになります(4)。
// 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)が渡されます。
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系の第一引数に渡すことができます。
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)。
if rs, ok := r.(io.RuneScanner); ok {
s.rs = rs
} else {
s.rs = &readRune{reader: r, peekRune: -1}
}
これらを踏まえると、上であげたHTTP/1.0ヘッダーのフィールドは以下のようにスキャンすることができます。
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まで読むということができます。
read, err := state.Token(true, func(char rune) bool {
return true
})
if err != nil && err != io.EOF {
// ...
}
あるいはGoでの関数リテラルがクロージャーであること(10)を利用して、Content-Length分だけ読み込むということもできます。
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回だけになると思います。
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
}
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系のリクエストのパースは
fmt.Fscan(conn, &req)
という具合に書けたりします。
- https://golang.org/pkg/fmt/#hdr-Scanning
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L321
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L55
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L21
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L157
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L45
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L179
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L385
- https://golang.org/ref/spec#Function_literals
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L446
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L38
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L370
- https://github.com/golang/go/blob/7d30af8e17d62932f8a458ad96f483b9afec6171/src/fmt/scan.go#L233