Open3

Goならわかるシステムプログラミングを読んだ

kenshokensho

io.Writer

io.Writerは、Goの世界から何かを書き込む動作をする際、そのメソッドを抽象化するためインターフェース。
様々な書き込み(ファイル、コネクション、標準出力...)をする型を統一的に扱えるように定義されている。

io.Writerを満たす型

os.File型

os.File構造体のWrite()は引数で渡した[]byte型の内容をファイルに書き込む。戻り地では書き込んだバイト数nとerrorを返す。

func main() {
	f, err := os.Create("hoge.txt")
	if err != nil {
		panic(err)
	}
	defer f.Close()
	f.Write([]byte("書き込まれる文字列"))
}

標準出力をおこなうためのosバッケージの定数、os.Stdoutもos.File型なので、同じ使い方で出力できる。

func main() {
	os.Stdout.Write([]byte("書き込まれる文字列"))
}

bytes.Buffer型

bytes.Buffer はWriteメソッドで書き込みれた内容をためておき、後で吐き出すことができる。

func main() {
	var buffer bytes.Buffer
	// bytes.Bufferに書き込む
	buffer.Write([]byte("bytes.Buffer! \n"))
	// 書き込まれた内容を文字列として取得
	fmt.Println(buffer.String())
}

net.Conn(net.TPCConn)

net.Connはnet.Deal()関数の戻り値。内容はio.Writerio.Reader を満たすインターフェース。
(インターフェースを返す実装はアカンとどっかで聞いた気がするけど、一旦無視する。)
実態はnet.TCPConnが返ってきている。

func main() {
	conn, err := net.Dial("tcp", "example.com:80")
	if err != nil {
		panic(err)
	}

	conn.Write([]byte("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n"))
	io.Copy(os.Stdout, conn)
}

http.ResponseWriter型やhttp.Request型も同様に、Writeメソッドを使うこと書き出しができる。

io.Writerのデコレーター

io.MultiWriter()

io.MultiWriter()関数は、2つのio.Writerを受け取り、io.multiWriter型のポインタを返す。
以下のように使うと、同時に2つの場所に書き込むことができる。

func main() {
	f, err := os.Create("MultiWriter.txt")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	w := io.MultiWriter(f, os.Stdout)
	w.Write([]byte("標準出力とファイル、同時に書き込めます\n"))
}

gzip.NewWriter()

func main() {
	f, err := os.Create("test.txt.gz")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	w := gzip.NewWriter(f)
	defer w.Close()
	w.Header.Name = "test.txt"
	io.WriteString(w, "hogehoge, gzip.Writer\n")
}

練習問題

Q2.1:ファイルに対するフォーマット出力

func main() {
	fmt.Fprintf(os.Stdout, "%d %s %f", 3, "fefe", 2.4)
}

Q2.2:CSV 出力

func main() {
	f, err := os.Create("MultiWriter.csv")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	cw := csv.NewWriter(f)
	cw.Write([]string{"hoge", "fuga", "piyo"})
}

Q2.3:gzip された JSON 出力をしながら、標準出力にログを出力

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Encoding", "gzip")
	w.Header().Set("Content-Type", "application/json")
	source := map[string]string{
		"Hello": "World",
	}

	f, _ := os.Create("hoge.json.gz")
	defer f.Close()
	gf := gzip.NewWriter(f)
	defer gf.Close()

	multiWriter := io.MultiWriter(gf, os.Stdout)

	encoder := json.NewEncoder(multiWriter)
	encoder.SetIndent("", "   ")
	err := encoder.Encode(source)
	if err != nil {
		log.Fatal(err)
		return
	}

	gf.Flush()
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}
kenshokensho

io.Reader

アプリケーションが外部からデータを読み込むための機能を抽象化したインターフェース。

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

pに予め確保しておいたバッファ(メモリ)に読み込んだ内容を入れる。

便利な補助関数

読み込む内容が用意したバッファに入り切らない内容の場合、何度もReadメソッドを呼び出すのはめんどう。
なので、Readメソッドを直接使う場面は少なく(~ほんと?~)多くの場合下記する補助関数が使われる。

io.ReadAll

buffer, err := io.ReadAll(reader)

のように使う。メモリに収まらないような内容でなければ大抵のケースでこれでいける。

io.ReadFull

確実に決まったバイト数読み込みたいときに使える。

// 4バイト読み込めないとエラー
buffer := make([]byte, 4)
size, err := io.ReadFull(reader, buffer)

io.Copy

io.Readerからそのままio.Writerにデータを渡すときに使う。

// wはio.Writer, rはio.Reader
io.Copy(w, r)

複合インターフェースとキャスト

多くのio.Readerやio.Writerの具象型は、単体のインターフェースを満たすのではなくてio.Readerio.Closerの複合インターフェースのio.ReadCloserなど、複合されたインターフェースを満たすようにできている。
また、io.ReadCloserを引数で受け取る関数をテストしたいが、テスト中はio.Readerを満たすbytes.Bufferを使いたいときなどもある。
そんなときは、io.NopCloser()関数を使うと、bytes.BufferをあたかもCloseメソッドを持つio.ReadCloserであるかのように使うことができる。

os.Stdinメソッド

io.Readerio.Writerを満たす。

func main() {
	for {
		buf := make([]byte, 5)
		size, err := os.Stdin.Read(buf)
		if err == io.EOF {
			fmt.Println("EOF")
			break
		}
		fmt.Printf("size=%d input='%s'\n", size, string(buf))
	}
}

net.Conn

kenshokensho

チャネル

goroutine

Goに組み込まれている軽量なスレッド。スレッドはプロセス(実行中のプログラム)の中の処理の単位。
同じプロセスのスレッド同士はリソースを共有できる。

func sub() {
	fmt.Println("sub() is running")
	time.Sleep(time.Second)
	fmt.Println("sub() is finished")
}

func main() {
	fmt.Println("start sub()")
	go sub()
	time.Sleep(2 * time.Second)
}

以上のgo sub()ように、関数の呼び出しの前にgoとつけるだけでgoroutineは立ち上がる。

チャネル

goの並行処理の中で使われるqueueのこと。FIFO(先出し先入れ)のデータ構造。

  • チャネルは読み込み・書き込みで準備ができるまでブロッキングする。
    • データがないチャネルを読み込もうとするとそこでブロッキングが走るし、バッファのない(空きがない)チャネルに書き込もうとすると、読み込みの準備が完了するまでそこでブロッキングが走る。
      以下のようにチャネルは処理の待ち合わせに使用できる。
func main() {
	fmt.Println("start")
	// 終了を受け取るためのチャネル
	done := make(chan bool)
	go func() {
		fmt.Println("goroutine is finished")
		// 終了を通知
		done <- true
	}()
	// 終了を待つ
	<-done
	fmt.Println("all tasks are finished")
}

チャネルはクローズしなくてもGCに回収される。データを流すようなチャネルはクローズした場合、

  • 書き込み時はパニック
  • 読み込み時はデフォルト値(空文字や0)が流れてくる

ことが想定されるためクローズしないほうが良いかも。

待ち合わせ用のチャネルはそのチャネルを使用している箇所全てに同時に終了を知らせることができるのでクローズしたほうが良い。