VTRyo Blog

一歩ずつ前に進むブログ

Ruby書きが困惑したGoの標準入力 fmt.Scan()とbufio.NewScanner()に関するメモ

標準入力から改行やスペース区切りの文字を受け取るにはどうすればよいのか。

現状Ruby書きなので、Goで書こうとすると全然ピンとこない。 例えばRubyなら、このように書くだけで標準入力を出力できたので、どうしたらいいんだろうと困惑した。*1

# sample.rb
input = gets.to_i
puts input

# % ruby sample.rb
1
1

3行来るとわかっているならこれでも可能。

# sample.rb

input = []

3.times do
  input << gets.to_i
end
p input

#  % ruby sample.rb
1
2
3
[1, 2, 3]

標準入力

調べると、だいたいこのあたりがヒットした。今回はこの2つについてメモする。

  • fmt.Scan()
  • bufio.NewScanner()

fmt.Scan()

xn--go-hh0g6u.com

fmt.Scan()に変数を渡すのはわかるのだが、&aを渡すことになり困惑した。

普段の癖で、直感的にfmt.Scanにはaを渡しそうになるが、そうは行かなかった。

package main
import "fmt"

func main() {
  var a int
  fmt.Scan(&a)
  fmt.Println(a)
}

fmt.Scanはポインタを利用して標準入力を受け取るということで、fmt.Scan(&a)とする必要があった。

しかし、上のドキュメントではポインタに関すること特に言及してないので、そのドキュメントを見ても理解できないような気がしている。

……と思ったら、ドキュメントの上の方にちゃんと書かれていた。

スキャンされるすべての引数は,基本型へのポインタまたは Scanner インターフェースの実装のいずれかでなければなりません。

xn--go-hh0g6u.com

なるほど完全に理解した。

このfmt.Scan()ではスペースで自動で区切って変数に格納できるので、Hello worldという入力を受け付ける場合、a, bの2つの変数を用意するとそれぞれに格納してくれるトノコト。

package main
import "fmt"

func main() {
    var a, b string
    fmt.Scan(&a, &b)
    fmt.Println(a, b)
}

最後はおまけで標準出力させた。

bufio.NewScanner()

bufioパッケージを使った標準入力。

package main

import (
    "bufio"
    "fmt"
    "os"
)
// Ctl+Cするまで無限入力状態
func main() {
    sc := bufio.NewScanner(os.Stdin)
    for sc.Scan() {
        fmt.Println(sc.Text())
    }
}

bufio.NewScanner(os.Stdin)と書いたが、引数はos.Stdinである必要はなく、io.Readerが渡されれば良いとドキュメントには書いてある。

func NewScanner(r io.Reader) *Scanner

NewScanner は, r から読み込む新しい Scanner を返します。

xn--go-hh0g6u.com

io.Readerの定義は以下のようになっていて、つまりNewScannerで受け取ったos.StdinがReadを持っていれば読み込んでくれるという事だ。

type Reader interface {
    Read(p []byte) (n int, err error)
}

ではos.Stdinのドキュメントを見てみる。

xn--go-hh0g6u.com

var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

Stdin, Stdout ,および Stderr は,標準入力,標準出力,および標準エラーのファイル記述子を指す開いたファイルです。

NewFileの定義は以下。渡されたuintptr(syscall.Stdin)がFileとして返ってくる。

func NewFile(fd uintptr, name string) *File

NewFile は与えられたファイルディスクリプタと名前で新しい File を返します。

で、もちろんFile型にはReadがあるので、os.StdinすることでNewScannerの引数にos.Stdinを渡しても問題ないということがわかる。

func (f *File) Read(b []byte) (n int, err error)

xn--go-hh0g6u.com

こうしてbufio.NewScanner(os.Stdin)によって、標準入力で受け取った値を読み込める。

io.Readerが汎用的に読み込めるおかげであまり意識せずに読み込みをさせることができるようだ。これがインターフェースか……。

で、最後に、宣言したscでScan()とText()を使うことで標準入力にアクセスしてみた。

func main() {
    sc := bufio.NewScanner(os.Stdin)
    for sc.Scan() {
        fmt.Println(sc.Text())
    }
}

xn--go-hh0g6u.com

Scan は Scanner で次のトークンまで処理し, そのトークンは Bytes あるいは Text メソッドで取得可能になります。

xn--go-hh0g6u.com

Text は,Scan 呼び出しで最後に生成されたトークンを返します。 このトークンのバイトを持つ,新たにメモリを割り当てられた文字列を返します。

最後はおまけで標準出力させた。

参考

文中以外に参考にした記事。

qiita.com

qiita.com

qiita.com

qiita.com

baubaubau.hatenablog.com

もっと詳しい話

より詳細な指摘があればコメントお待ちしてます。

*1:Rubyの標準入力についてそんなに深くまで探ってないだけかもしれない