Closed24

Goの標準入力を理解する

ハガユウキハガユウキ

[到達していたい状態]

  • strconvパッケージとは何かを人に説明できる状態
  • strconvパッケージをどのように使って入力を受け取れるのか人に説明できる
  • strconvパッケージを使ってGoで標準入力を実装できる

[やること]

  • strconvパッケージの公式ドキュメントを読む
  • strconvパッケージをどのようにGoで使えば標準入力を受け取れるのかを理解する
ハガユウキハガユウキ

そもそもプロセスとは、

プロセスとは、ざっくりいうとメモリ上で実行中のプログラムのことである。
(プロセスを起動するという言葉を目にするが、プロセスを起動するとは、プロセスの実行に必要なメモリやリソースなどを確保して、実行可能な状態にすること)
シェルもプロセスとして、メモリ上で実行されている

↓ 参考記事
https://lightning-brains.blogspot.com/2019/10/linux.html
https://e-words.jp/w/プロセス.html

ハガユウキハガユウキ

スレッドとは

スレッドとは、実際にCPUで実行される処理の単位である。
一つのプロセスの中には一つ以上のスレッドが存在している。

[スレッドの特徴]

  • 一つのプロセスの中に一つ以上のスレッドが存在している。
  • 同一プロセスに所属するスレッドは、同じコードなどのメモリ空間を共有している。
    • そのため、各スレッドはそれらのデータを参照しながら、強調して動作できる
    • 複数のスレッドは同時に並行動作できる
      • アプリケーションの動作をスムーズにできて、CPUのマルチコアの機能をうまく引き出せる
        • そもそもコアとは、スレッドから送り出された命令を処理するもの。
          • コアはCPU内に存在する
          • コアはCPUの脳みその様なもの。一つのCPUが複数のコアを持つことで、複数の命令を並行処理できる。 CPUコア数が並行処理できるスレッド数を表す。

↓ 参考記事
https://atmarkit.itmedia.co.jp/ait/articles/1410/30/news150.html
https://wa3.i-3-i.info/word12750.html
https://zenn.dev/hsaki/books/golang-concurrency/viewer/term#「並行」と「並列」の定義の違い

並列処理と並行処理の違い

並行処理とは

並行処理とは、共通の期間内で複数個のスレッドを実行すること。
どのようにスレッドを処理しているかはどうでもいい(逐次実行しているかもしれないし、並列に実行しているかもしれないし)
Goでは並行処理を簡単に行うためにゴルーチンという仕組みがある。

並列処理とは

並列処理とは、複数個のスレッドを同時に実行すること。

https://zenn.dev/hsaki/books/golang-concurrency/viewer/term#「並行」と「並列」の定義の違い

そもそもプログラムはどうやって実行されているのかざっくり理解する

  1. プログラムのファイルはハードディスクに存在している。
  2. ビルド(またはコンパイル)を実行して、機械語で書かれた実行ファイルを作成する。この実行ファイルもハードディスクに保存される
  3. プログラムを実行するときは、OSが実行ファイルをメモリに展開する。プログラムはメモリに展開されるとプロセスとして実行される。プロセスは状態を保持しており、プロセスのライフサイクル(生成〜消滅)を管理している
  4. プロセス内にはスレッドが存在していて、準備ができたスレッドから順番にCPUコアに割り当てられて、処理が実行される。どのスレッドをどのCPUコアで実行するかは、スケジューラが決定する。スケジューラはOSの一部として実行されるプログラムであり、実行すべきタスクやプロセスを管理している。(要はどのスレッドをどのCPUコアに割り当てるののも、OSが決めていると解釈できる)

↓ 参考記事
https://rainbow-engine.com/process-linux-howto/
https://zenn.dev/taikiuejo/scraps/0d51b9f90f9eb9
http://www.coins.tsukuba.ac.jp/~yas/coins/literacy-2018/2018-05-11/index.html
https://atmarkit.itmedia.co.jp/ait/articles/1410/30/news150.html

ハガユウキハガユウキ

ランタイムとは

ランタイムとは、ざっくり理解すると、あるプログラムの実行時のこと、または、あるプログラムを実行する時に必要なプログラムのこと。
https://wa3.i-3-i.info/word13464.html

ランタイム、メモリ、プロセスの関係性

  • ランタイムはあるプログラムを実行する際に、必ず実行する必要がある。
    • つまり、あるプログラムをプロセスとして起動した際に、必ずランタイムも別プロセスとして起動する必要がある。
    • 実行ファイルにランタイムが含まれている場合、ランタイムをわざわざ別プロセスとして起動する必要はない。
    • Goの場合、ビルドして実行ファイルを作ると、その実行ファイルにはGoのランタイムが含まれている。
ハガユウキハガユウキ

標準入力と標準出力の詳細については以下のスクラップでまとめた。
https://zenn.dev/yukihaga/scraps/49a22736e00c56
とはいえ、スクラップ見に行くのもめんどくさいので、ざっとまとめる。

標準入力とは

プロセスが最初から持っている入力に関するストリームのこと。何も指定していないと、キーボードが標準入力を通してプロセスとつながる(標準入力にキーボードが指定されていると解釈できる)。

標準出力とは

プロセスが最初から持っている出力に関するストリームのこと。何も指定していないと、ディスプレイが標準出力を通してプロセスとつながる(標準出力にディスプレイが指定されていると解釈できる)。

ハガユウキハガユウキ

このプログラムが何をやっているかをちゃんと理解する。
このプログラムはキーボードから文字を受け取り、それを出力している。

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の場合はエラー型の値だよってことを言いたいんだなと思った。

↓ 参考記事
https://ascii.jp/elem/000/001/234/1234843/
https://pkg.go.dev/os

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という型へのポインタを使うと書いているので、おそらくそれと同じようなことをしたいんだと思った。

↓ 参考記事
https://www.wdic.org/w/TECH//dev/stdin

ハガユウキハガユウキ

bufioパッケージ

bufioパッケージはバッファードI/Oを実装しています。io.Readerまたはio.Writerオブジェクトをラップして、同じくインターフェイスを実装し、バッファリングとテキストI/Oのためのいくつかの支援を提供する別のオブジェクト(ReaderまたはWriter)を作成します。

  • buffered IOとは、バッファを経由してバイト列の読み書きをすること。小さいサイズのバイト列で何回もシステムコールしていたら、システムコール自体が重い処理なので、全体の処理がめちゃくちゃ遅くなる。バッファを利用してある一定のバイトが溜まったらシステムコールをするようにすれば、システムコールの回数が減らせるので、早く処理できる。
  • 説明を見ると、Goの入出力処理にバッファ処理を付加したパッケージだとわかる。

https://pkg.go.dev/bufio
https://zetcode.com/golang/bufio

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,
	}
}

https://zenn.dev/hsaki/books/golang-io-package/viewer/bufio

ハガユウキハガユウキ

[後で調べること]
普通のファイルで読み込んだ場合、標準入力がファイルってことになるのか?
ファイルディスクリプタ的には0なのか?
気になる。

ハガユウキハガユウキ

Linuxにはstdioライブラリがあって、バッファが一杯になるまたは、改行コードが含まれた書き込みを実行する。そのため、Goのbufioパッケージでbuffered IOを実装しているってことは、バッファに改行コードが含まれた文字が含まれるまでは、スキャン処理がスキャンを待ってくれると思われる。
https://qiita.com/zurazurataicho/items/e66645bac16d9a482369#文字列を取得する

ハガユウキハガユウキ

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を返す。
ハガユウキハガユウキ

とりあえずどんなことをしているのか理解できた。

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);
}
ハガユウキハガユウキ

StrStdin内で使われている関数を見ると、戻り値でerrorを含めた2値を返しているパターンがないので、if err != nilのようなエラーの考慮を書く必要はないか。

ハガユウキハガユウキ

普通の文字列が書かれたファイルからスキャナーを作るのも確かできた気がする。

ハガユウキハガユウキ

次はこれを理解しよう。

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
このスクラップは2023/06/07にクローズされました