Goの標準入力を理解する
[到達していたい状態]
- strconvパッケージとは何かを人に説明できる状態
- strconvパッケージをどのように使って入力を受け取れるのか人に説明できる
- strconvパッケージを使ってGoで標準入力を実装できる
[やること]
- strconvパッケージの公式ドキュメントを読む
- strconvパッケージをどのようにGoで使えば標準入力を受け取れるのかを理解する
そもそもプロセスとは、
プロセスとは、ざっくりいうとメモリ上で実行中のプログラムのことである。
(プロセスを起動するという言葉を目にするが、プロセスを起動するとは、プロセスの実行に必要なメモリやリソースなどを確保して、実行可能な状態にすること)
シェルもプロセスとして、メモリ上で実行されている
↓ 参考記事
スレッドとは
スレッドとは、実際にCPUで実行される処理の単位である。
一つのプロセスの中には一つ以上のスレッドが存在している。
[スレッドの特徴]
- 一つのプロセスの中に一つ以上のスレッドが存在している。
- 同一プロセスに所属するスレッドは、同じコードなどのメモリ空間を共有している。
- そのため、各スレッドはそれらのデータを参照しながら、強調して動作できる
- 複数のスレッドは同時に並行動作できる
- アプリケーションの動作をスムーズにできて、CPUのマルチコアの機能をうまく引き出せる
- そもそもコアとは、スレッドから送り出された命令を処理するもの。
- コアはCPU内に存在する
- コアはCPUの脳みその様なもの。一つのCPUが複数のコアを持つことで、複数の命令を並行処理できる。 CPUコア数が並行処理できるスレッド数を表す。
- そもそもコアとは、スレッドから送り出された命令を処理するもの。
- アプリケーションの動作をスムーズにできて、CPUのマルチコアの機能をうまく引き出せる
↓ 参考記事
並列処理と並行処理の違い
並行処理とは
並行処理とは、共通の期間内で複数個のスレッドを実行すること。
どのようにスレッドを処理しているかはどうでもいい(逐次実行しているかもしれないし、並列に実行しているかもしれないし)
Goでは並行処理を簡単に行うためにゴルーチンという仕組みがある。
並列処理とは
並列処理とは、複数個のスレッドを同時に実行すること。
そもそもプログラムはどうやって実行されているのかざっくり理解する
- プログラムのファイルはハードディスクに存在している。
- ビルド(またはコンパイル)を実行して、機械語で書かれた実行ファイルを作成する。この実行ファイルもハードディスクに保存される
- プログラムを実行するときは、OSが実行ファイルをメモリに展開する。プログラムはメモリに展開されるとプロセスとして実行される。プロセスは状態を保持しており、プロセスのライフサイクル(生成〜消滅)を管理している
- プロセス内にはスレッドが存在していて、準備ができたスレッドから順番にCPUコアに割り当てられて、処理が実行される。どのスレッドをどのCPUコアで実行するかは、スケジューラが決定する。スケジューラはOSの一部として実行されるプログラムであり、実行すべきタスクやプロセスを管理している。(要はどのスレッドをどのCPUコアに割り当てるののも、OSが決めていると解釈できる)
↓ 参考記事
ランタイムとは
ランタイムとは、ざっくり理解すると、あるプログラムの実行時のこと、または、あるプログラムを実行する時に必要なプログラムのこと。
ランタイム、メモリ、プロセスの関係性
- ランタイムはあるプログラムを実行する際に、必ず実行する必要がある。
- つまり、あるプログラムをプロセスとして起動した際に、必ずランタイムも別プロセスとして起動する必要がある。
- 実行ファイルにランタイムが含まれている場合、ランタイムをわざわざ別プロセスとして起動する必要はない。
- Goの場合、ビルドして実行ファイルを作ると、その実行ファイルにはGoのランタイムが含まれている。
標準入力と標準出力の詳細については以下のスクラップでまとめた。 とはいえ、スクラップ見に行くのもめんどくさいので、ざっとまとめる。
標準入力とは
プロセスが最初から持っている入力に関するストリームのこと。何も指定していないと、キーボードが標準入力を通してプロセスとつながる(標準入力にキーボードが指定されていると解釈できる)。
標準出力とは
プロセスが最初から持っている出力に関するストリームのこと。何も指定していないと、ディスプレイが標準出力を通してプロセスとつながる(標準出力にディスプレイが指定されていると解釈できる)。
このプログラムが何をやっているかをちゃんと理解する。
このプログラムはキーボードから文字を受け取り、それを出力している。
package main
import (
"bufio"
"fmt"
"os"
)
func StrStdin() string {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return scanner.Text()
}
func main() {
fmt.Print("Input x:");
x := StrStdin();
fmt.Println(x);
}
osパッケージとは何か?
osパッケージは、オペレーティングシステム機能へのプラットフォームに依存しないインターフェイスを提供します。エラー処理はGoライクですが、デザインはUnixライクです:失敗した呼び出しは、エラー番号ではなく、エラー型の値を返します。
なるほど、つまり、osパッケージで提供されるインターフェースを利用すると、OS(カーネル)に定義してあるハードウェアの機能を利用できるってことか。 確かにosパッケージのWriteをデバッガで深ぼってみると最終的にはsyscallパッケージに定義してあるWriteが呼び出されていることを確認できた。このWriteがシステムコールのインターフェースになっていて、最終的にシステムコールが呼び出されるからOSに、OS(カーネル)で定義されているハードウェアの機能を使えと命令できるのか。
OSのプラットフォームに依存しないインターフェースなのはなぜかというと、syscallパッケージがOSの違いによる差分を吸収してくれているため。本来OSが違うならOSに定義されているハードウェアの定義とかも異なるはずで、システムコールも当然違ってくる。osパッケージが提供するインターフェースを使えば、内部でsyscallパッケージを呼び出しているから、そこを気にしなくてもいいんだよってことを言いたんだなと思った。
あと、このエラー番号の話は、システムコールが失敗するとエラー番号を返すけど、Goの場合はエラー型の値だよってことを言いたいんだなと思った。
↓ 参考記事
os.Stdinとは何か?
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 はオープン 標準入力、標準出力、標準エラーのファイルディスクリプタを指すファイルです。Goランタイムは、パニックとクラッシュのために標準エラーに書き込むことに注意してください; Stderrを閉じると、これらのメッセージが他の場所、おそらく後で開かれるファイルに行くかもしれません。
翻訳が間違っている可能性があるので、コードから理解する。一旦Stdinだけ理解する。
// osパッケージ
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
)
- syscall.Stdin
- syscall.Stdinとは、syscallパッケージに定義してある変数。0が初期値として代入してある。この0は標準入力のファイルディスクリプタを表す。
- uitptr
- uintptrとは、アドレスを格納できる大きさを持つ整数型である。
- /dev/stdin
- 標準入力に対応するデバイスに対するシンボリックリンク。
- NewFile
-
func NewFile(fd uintptr, name string) *Fileという型である。 - NewFile は、与えられたファイルディスクリプタと名前を持つ新しい File を返す。fdが有効なファイルディスクリプタでない場合、戻り値はnilである。
-
つまり、os.Stdinを呼び出すと、標準入力のファイルディスクリプタで、"/dev/stdin"という名前の*File型の値を返していたのか。Linuxの入門書を見ると、標準入出力力ライブラリでどのストリームなのかを指すためにFileという型へのポインタを使うと書いているので、おそらくそれと同じようなことをしたいんだと思った。
↓ 参考記事
bufioパッケージ
bufioパッケージはバッファードI/Oを実装しています。io.Readerまたはio.Writerオブジェクトをラップして、同じくインターフェイスを実装し、バッファリングとテキストI/Oのためのいくつかの支援を提供する別のオブジェクト(ReaderまたはWriter)を作成します。
- buffered IOとは、バッファを経由してバイト列の読み書きをすること。小さいサイズのバイト列で何回もシステムコールしていたら、システムコール自体が重い処理なので、全体の処理がめちゃくちゃ遅くなる。バッファを利用してある一定のバイトが溜まったらシステムコールをするようにすれば、システムコールの回数が減らせるので、早く処理できる。
- 説明を見ると、Goの入出力処理にバッファ処理を付加したパッケージだとわかる。
bufio.NewScanner
NewScannerはrから読み込む新しいScannerを返します。分割関数のデフォルトはScanLinesです。
- NewScannerの型は、
func NewScanner(r io.Reader) *Scanner - rは標準入力のファイルディスクリプタを持つ
*File型の値なので、標準入力から読み込むための*Scanner型の値を返すと解釈できる。 - NewScannerの引数に*File型の値がなぜ指定できるかというと、*File型に対して、インターフェースの実装をしたためである。ioパッケージのReader型はReadメソッドを持つインターフェースである。ある型に対してReadメソッドを実装すれば、その型は
io.Reader型として機能することができる。
type Reader interface {
Read(p []byte) (n int, err error)
}
-
- NewScannerで作成されたスキャナーは、デフォルトで入力を「行」単位でトークンにするように設定される。splitフィールドは、分割する関数を定義している。つまり、ScanLinesが指定されているので、入力を行単位でトークンにしている。行は末尾に改行コードが入って行と認識される。
// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
今気づいたけど、このNew〇〇って型のコンストラクタパターンか。
[後で調べること]
普通のファイルで読み込んだ場合、標準入力がファイルってことになるのか?
ファイルディスクリプタ的には0なのか?
気になる。
Linuxにはstdioライブラリがあって、バッファが一杯になるまたは、改行コードが含まれた書き込みを実行する。そのため、Goのbufioパッケージでbuffered IOを実装しているってことは、バッファに改行コードが含まれた文字が含まれるまでは、スキャン処理がスキャンを待ってくれると思われる。
scannerの型
bufioパッケージに定義してある。
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
bufio.Scan
Scan は Scanner を次のトークンに進め、Bytes または Text メソッドで利用可能になります。スキャンが停止した場合、入力の終端に達するか、エラーが発生した場合に false を返します。Scanがfalseを返した後、Errメソッドはスキャン中に発生したあらゆるエラーを返しますが、io.EOFだった場合はErrはnilを返します。Scanは、split関数が入力を進めずに空のトークンを多く返した場合にパニックになります。これはスキャナの一般的なエラーモードである。
- Scanの型は、
func (s *Scanner) Scan() boolである。 - この関数を呼び出したときに、スキャン処理が実行される。入力をスキャンする。スキャン処理が成功する限りはtrueを返し続ける。
- 入力の終端に達するか、エラーが発生した場合にスキャンが停止してfalseを返す。
EOFとは、ファイルの終端を表す記号やデータのこと
とりあえずどんなことをしているのか理解できた。
package main
import (
"bufio"
"fmt"
"os"
)
func StrStdin() string {
// os.Stdinで、標準入力のファイルディスクリプタを持つ*FIle型の値を返している
// ewScannerで引数から読み込むための新しいスキャナー(*Scanner型の値)を生成している。
// 今回のパターンの場合、rは標準入力のファイルディスクリプタを持つ*File型の値なので、
// 準入力から読み込むためのスキャナーを返すと解釈できる。
scanner := bufio.NewScanner(os.Stdin)
// スキャン処理を実行する
// バッファに標準入力からバイト列が渡されるまではスキャンを待機している。
// scannerがbuffered IOを考慮しているので、おそらくそのような挙動だと思われる。
scanner.Scan()
// スキャンした内容をstring型で取得する
return scanner.Text()
}
func main() {
fmt.Print("Input x:");
x := StrStdin();
fmt.Println(x);
}
次はこれを理解しよう。
func IntStdin() (int, error) {
stringInput := StrStdin()
return strconv.Atoi(strings.TrimSpace(stringInput))
}
strconvパッケージ
strconvパッケージは、Goの基本的なデータ型とstring型との相互変換をサポートする機能がまとめられたパッケージである。
strconv.Atoiについて
Atoi は ParseInt(s, 10, 0) と同等で、int 型に変換される。
https://pkg.go.dev/strconv#Atoi
てか、strconvって、string conversions(文字列の変換)の略だな。
上の引用文を見る限り、strconv.Atoiは、strconv.ParseInt(s, 10, 0)のショートカット用の関数って感じか。
strconv.ParseIntは文字列を整数に変換する関数。第一引数には対象文字列、第二引数には基数、最後の引数には、整数の精度をビット数で表している。精度を0に指定した場合、Goプラットフォームのint型及びuint型の精度が指定される。
Atoiのコードリーディングをしてみる。確かにstrconv.Atoiの内部で、strconv.ParseInt(s, 10, 0)を呼び出していることが分かった。strconv.ParseInt(s, 10, 0)の戻り値は、int64, error型なので、int64をint型に変換していることが分かった。
// Atoi is equivalent to ParseInt(s, 10, 0), converted to type int.
func Atoi(s string) (int, error) {
// "Atoi"という謎の定数を定義
const fnAtoi = "Atoi"
//
sLen := len(s)
if intSize == 32 && (0 < sLen && sLen < 10) ||
intSize == 64 && (0 < sLen && sLen < 19) {
// Fast path for small integers that fit int type.
s0 := s
if s[0] == '-' || s[0] == '+' {
s = s[1:]
if len(s) < 1 {
return 0, syntaxError(fnAtoi, s0)
}
}
n := 0
for _, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return 0, syntaxError(fnAtoi, s0)
}
n = n*10 + int(ch)
}
if s0[0] == '-' {
n = -n
}
return n, nil
}
// Slow path for invalid, big, or underscored integers.
i64, err := ParseInt(s, 10, 0)
if nerr, ok := err.(*NumError); ok {
nerr.Func = fnAtoi
}
return int(i64), err
}
stringsパッケージとは
stringsパッケージとは、文字列操作の機能がまとめられたパッケージである。strings.Splitをよく使う。
strings.TrimSpaceパッケージとは
strings.TrimSpaceは、文字列の前後に連なる「スペース」を取り除いた新しい文字列を返す関数である。
strings.TrimSpaceを実行することで、スペースを取り除けるのか。
➜ stdin git:(main) ✗ go run main.go
Input Y:11
output 11
➜ stdin git:(main) ✗ go run main.go
Input Y: 11
output 11
大体理解できたからクローズする
参考記事