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

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.Writer
と io.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)
}

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.Reader
とio.Closer
の複合インターフェースのio.ReadCloser
など、複合されたインターフェースを満たすようにできている。
また、io.ReadCloser
を引数で受け取る関数をテストしたいが、テスト中はio.Reader
を満たすbytes.Buffer
を使いたいときなどもある。
そんなときは、io.NopCloser()
関数を使うと、bytes.Buffer
をあたかもCloseメソッドを持つio.ReadCloser
であるかのように使うことができる。
os.Stdinメソッド
io.Reader
とio.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

チャネル
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)が流れてくる
ことが想定されるためクローズしないほうが良いかも。
待ち合わせ用のチャネルはそのチャネルを使用している箇所全てに同時に終了を知らせることができるのでクローズしたほうが良い。