スペースで区切られた値を、連続した引数として入力するには、fmt パッケージの Scan 関数を利用すれば良いのですが、空白を含む文字列全体を入力としたい場合や、特定の文字に達するまでしか入力を受け付けない場合は、どうしたらよいでしょうか。
このトピックでは、bufio パッケージについて学びます。bufio パッケージには、上記のような高度な入力操作を実行するための関数が含まれています。
まず、bufio.NewReader() 関数を用いて、Reader を作成することから始めます。簡単に言うと、Reader 型はデフォルトサイズ4 kB のバッファと、そこからデータを保存したり読み出したりするためのリーダーを含んでいます。
標準入力(stdin)からデータを取得することをプログラムに知らせるために、 bufio.NewReader() 関数に引数として os.Stdin を渡す必要があります。
Reader を作成した後、データを読み込むためによく使われる関数は次の通りです。
ReadBytes() - データもしくはエラーのあるバイトのスライスを返します。
ReadString() - データもしくはエラーが含まれる文字列を返します。
どちらの関数も引数として指定されたデリミター(通常は改行文字 \n)を受け取り、指定されたデリミターに到達するまでデータを読みます。ただし、以下の例のように、ルーン文字d1 つなど他のデリミタも使用できます。
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
b, err := reader.ReadBytes('\n')
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
s, err := reader.ReadString('d')
if err != nil {
log.Fatal(err)
}
fmt.Println(s)
}
ReadBytes() と ReadString() 関数の重要な点は、返されるバイトまたは文字列のスライスには指定したデリミタが含まれることです。
例えば、入力がHelloWorld!\nの場合、デリミタである\nは含まれ、入力が ZenApp.dev の場合、dは含まれます。→ ZenApp.d
上記の Reader 型とは別に、Scanner 型も作成することができます。Reader 同様、Scanner タイプにはデータを保存するためのデフォルトサイズ4 kB のバッファと、保存されたデータを読み出すためのリーダーが含まれています。
Scanner を作成するには、NewScanner() 関数を使用します。また、NewScanner() に引数として os.Stdin を渡す必要があります。これは、標準入力からデータを取得することをプログラムに知らせるためのものです。
Scanner の最も一般的な使い方は、例えばある入力を一行ずつ読み取ることです。
...
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text() // 入力:This is the input\n
fmt.Println(line) // 出力:This is the input
}
}
上記の例では、scanner.Scan() 関数で for ループを使っています。デフォルトの ScanLines() 関数で一行ずつデータをスキャンしています。
Reader 関数とは異なり、ScanLines() 関数はスキャンしたデータに改行文字
\nを含んではくれません。
次に、line 変数を宣言して、scanner.Text() を代入しています。 最後に、スキャンした文字列出力しています。
Scanner とリーダーの主な違いは、Scanner はデフォルトの ScanLines() 関数で分割された行のトークンとしてデータを読み取ることです。しかし、Scanner は、次のような異なるタイプのトークンとしてデータを読み取ることもできます。
ScanWords() ScanRunes()ScanBytes()あるいは、要件に応じて、特定のタイプのトークンしか読み取らないカスタム分割関数を作成することもできます。
...
func main() {
wordScanner := bufio.NewScanner(os.Stdin)
// Set the 'Split' function to scan for words (space-delimited tokens):
wordScanner.Split(bufio.ScanWords)
for wordScanner.Scan() { // Input: Among Us ඞ\n
fmt.Println(wordScanner.Text())
}
}
// Output:
// Among
// Us
// ඞ
上記の例では、ScanWords()関数を使った wordScanner を紹介しています。スペースで区切られた単語をスキャンするように wordScanner を正しく設定するには、wordScanner.Split(bufio.ScanWords)ステートメントで Split()関数を設定する必要があることに注意してください。
次に、wordScanner.Scan()関数を用いて for ループを行います。先に設定した ScanWords()関数でデータを単語単位でスキャンしています。
最後に、fmt.Println(wordScanner.Text())ステートメントで、スキャンした単語を 1 つずつ出力しています。
前述したように、Scanner 用のカスタム分割関数を作成することもできます。ここでは、bool 型の入力のみを検証する splitBool() 関数を作成します。
// The custom 'splitBool' function validates `bool` type input only:
func splitBool(data []byte, atEOF bool) (advance int, token []byte, err error) {
advance, token, err = bufio.ScanWords(data, atEOF)
if err == nil && token != nil {
_, err = strconv.ParseBool(string(token))
}
return
}
splitBool() は、data - スキャンするデータを含むバイトのスライス、および atEOF - データがファイルの終端にあるかどうかを示す bool 型、の 2 つの引数をとる。
さらに、スキャンされたバイト数を含む advance、スキャンされた単語を含むバイトのスライスである token、および発生したエラーを含む err の 3 つの値を返します。
splitBool()の内部で、advance, token, err を bufio.ScanWords()関数の戻り値として設定し、data と atEOF をその引数として渡しています。次に、if 文の中で、エラーがないこと、スキャンされたトークンが nil でないことを検証しています。この検証を経て、スキャンされたトークンを bool 値として解析することを試みます。
...
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Set 'splitBool' as the split function for the scanning operation
scanner.Split(splitBool)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err) // Exit if the scanned value is not a 'bool'
}
}
// Input: true false Hello World!
// Output:
// true
// false
// 2022/02/24 23:02:04 strconv.ParseBool: parsing "Hello": invalid syntax
新しいスキャナを作成し、標準入力からデータを取得するように設定した後、最も重要な部分は、スキャナの分割関数として splitBool を設定することです。
最後に、プログラムはスキャンされた bool 値を出力するが、スキャンされた値が bool 型でなくなるまで、プログラムはデータのスキャンを続ける。
この場合、プログラムはエラーを返して終了する。
Scanner と Reader の主な違いは、Scanner はデフォルトでデータを分割行のトークンとして読み込みますが、異なるデータ型のトークンとして読み込むことも可能です。
Reader がデータを読み込むために使用する最も一般的な関数はReadBytes()とReadString()であり、どちらの関数も指定したデリミタを引数として受け取り、指定したデリミタに到達するまでデータを読み込みます。
ScanWords() では、スペースで区切られた単語を、ScanRunes()では UTF-8 のルーンを、ScanBytes()ではバイトを、さらにカスタム分割関数を作成して特定の種類のトークンだけを読み込んで検証することも可能です。
