GoでのファイルI/O

GoでのファイルI/Oについて,改めてまとめた.
いろいろな方法があるので,それぞれどういったものかを確認しながらまとめる.

ファイルオープン

読み書きを行う前に,まずファイルオープンしないとどうにもならないのでそこから.
osパッケージを見ると,2つのファイルオープンメソッドがあることがわかる.

  • os.Open
  • os.OpenFile

os.Open

func Open(name string) (*File, error)

引数に与えられた名前のファイルを 読み取り専用 でオープンする.
そのため,もしファイルが存在しなければエラーとなる( *PathError が返却される)

// os.Open attempts to open given file as read only mode.
// Therefore, if it doesn't exist, then *os.PathError will occur.
_, err := os.Open("thisdoesntexist.txt")
if err != nil {
    if os.IsNotExist(err) {
        log.Println("file not found", err)
    } else {
        log.Println(err)
    }
}

上の例では便利メソッドとして os.IsNotExist を使っている.
このメソッドに os.Open から返却されたエラーを渡すと,ファイルが存在しないために発生したエラーかどうかを教えてくれる.
os.IsNotExist の返り値が true なら,ファイルが存在しないという意味になる.

os.OpenFile

func OpenFile(name string, flag int, perm FileMode) (*File, error)

引数に与えられた名前のファイルを,指定したモード,パーミッションでオープンする.
flag の指定方法次第で,追記モードや,存在しない場合に作成する,等が可能になる.

// os.OpenFile attempts to open given file as given mode and permission.
// In this example, open "newfile.txt" as write-only mode and it permission is 0600 (r/w only allowed to file owner)
file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

(おそらく)最も基本となる方法

ファイルをオープンし,バイト型のスライスを使ってデータの読み書きを行う方法.

Read

前提として,ファイルからの読み取りができるモードでオープンされている必要がある.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

// *File.Read reads slice of bytes up to len(slice) from file.
buffer := make([]byte, 1024)
n, err := file.Read(buffer)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes read by *File.Read()\n", n)
log.Printf("file content: %s\n", string(buffer))

Write

前提として,ファイルに書き込みができるモードでオープンされている必要がある.

// os.OpenFile attempts to open given file as given mode and permission.
// In this example, open "newfile.txt" as write-only mode and it permission is 0600 (r/w only allowed to file owner)
file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

// *File.Write writes slice of bytes to file.
byteData := []byte("Hello world\n")
n, err := file.Write(byteData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written by os.Write()\n", n)

また,バイト型のスライスの代わりにstringを書き込むこともできる.
stringの書き込みには WriteString メソッドを使用する.

// *File.WriteString writes strings to file instead of slice of bytes.
stringData := "We can write not only []byte but also string :)"
n, err = file.WriteString(stringData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written by os.WriteString()\n", n)

ファイルの内容すべてを読み込む

io/ioutilパッケージの ReadAll メソッドを使用すると,ファイルの内容すべてを読み込むことができる.

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

func ReadAll(r io.Reader) ([]byte, error)

ReadAllの引数に与える io.Reader は, Read メソッドを持つインタフェースと定義されている.
そのため,通常通りオープンしたファイルをそのまま渡すことができる.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

bytes, err := ioutil.ReadAll(file)
if err != nil {
    log.Fatalln(err)
}
log.Printf("Read all contents by ioutil.ReadAll(): %s\n", string(bytes))

バッファありのファイルI/O

bufioパッケージのメソッドを使用すると,読み書きの際に内部でバッファを使ってくれる.
そのため,そのままデータを読み書きするよりも効率的に処理を行うことができる.

ファイルI/Oに使えそうなものは次の3種類.

  • bufio.Reader
  • bufio.Scanner
  • bufio.Writer

bufio.Reader

基本的な使い方は通常のファイルと似ているが,いくつか便利なメソッドが定義されている.

reader := bufio.NewReader(file)
buffer := make([]byte, 5)
// basic Read method
if _, err := reader.Read(buffer); err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", string(buffer))

// ReadBytes reads until delimiter found.
// Read contents is slice of bytes.
// In this example, read until first '\n' character found.
bytes, err := reader.ReadBytes('\n')
if err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", string(bytes))

// ReadString reads until delimiter found.
// Read contents is string.
// In this example, read until first '\n' character found.
str, err := reader.ReadString('\n')
if err != nil {
    log.Fatalln(err)
}
log.Printf("content: %s\n", str)

bufio.Scanner

bufio.Readerと似ているが,こちらは改行区切りのテキストを扱う時に便利なものになっている.

file, err := os.Open("newfile.txt")
if err != nil {
    log.Fatalln(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    log.Println(scanner.Text())
}

Text メソッドを呼ぶと,改行文字まで(=1行分の文字)を返してくれる.

Scantrue の間は,まだ読んでいない行があるということを示している.
なので, Scanfalse になるまでループを回してあげれば結果的にファイルの内容すべてを読むことができる.

bufio.Writer

Readerと同様,io.Writerと似ている.
注意しなければならない点として,最後に Writer.Flush を呼び出す必要がある点がある.
これを呼び出さないと正常に書き込みされないので注意する.

file, err := os.OpenFile("newfile.txt", os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
    log.Fatalln(err)
}
defer file.Close()

writer := bufio.NewWriter(file)

byteData := []byte("Hello world\n")
n, err := writer.Write(byteData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written\n", n)

stringData := "Write string :)"
n, err = writer.WriteString(stringData)
if err != nil {
    log.Fatalln(err)
}
log.Printf("%d bytes written\n", n)

writer.Flush()

References

pipeによるプロセス間通信

fork() で作成した子プロセスと親プロセスの間で情報のやり取りをするために,IPC(Inter Process Communication)の一つであるパイプを利用した.
一度理解してしまえば特に難しいものではなかったので,文章としてまとめておく.

パイプの概要

シェルを使用していると,「何かのコマンドの出力をgrepしたい」というとき等,あるコマンドの出力を別のコマンドの入力として扱いたいということが多々ある.このような場合,「パイプ」という機能を使って次のようにコマンドを実行することで実現できる.

$ cat something.txt | grep Hello

上記のコマンドを実行すると,catコマンドの出力から, Hello を含んでいる行のみを画面に出力させることができる.
(上記の例ではパイプを使わずともgrepコマンド単体で同じことが可能であるが)

このように,パイプの入口・出口となるファイルディスクリプタを接続することができるという機能を持つ.

パイプの利用

次のような簡単なサンプルプログラムを作成した.

特に難しいことはやっておらず,ただ単に fork() した後,親プロセスから子プロセスに文字列を送るだけのプログラム.

パイプを使うため, fork() を呼び出す前に pipe() を呼び出しておく.
pipe() システムコールを呼び出すと,引数に与えた配列の0番目に「読み取り用」,1番目に「書き込み用」のファイルディスクリプタを格納してくれる.
これらに対して書き込み・読み取りをすると,それぞれ対応するファイルディスクリプタから読み取り・書き込みを行うことができる.

pipe() を呼び出した跡は通常通り fork() を呼び出す.
これにより,子プロセスが作成され, pipe() によって作成されたファイルディスクリプタのペアも複製される.
その後,親プロセスと子プロセスで,次の必要ないファイルディスクリプタをそれぞれクローズしておく.

  • 親プロセス→読み取り用のファイルディスクリプタ( fds[0]
  • 親プロセスからは書き込みのみを行うため
  • 子プロセス→書き込み用のファイルディスクリプタ( fds[1]
  • 子プロセスからは読み取りのみを行うため

あとは,通常通り read()write() を呼び出すだけ.

今回は, fork() した後でも親・子ともに同じプログラムを実行していたが, execve() 等を使って子プロセスでは別のプログラムを動作させることももちろん可能.
この場合,子プロセスで標準入力からデータを読み込みたい場合は, dup2() を使って fds[0]0 (標準入力)に複製してあげると良い.

参考文献

  • Michael Kerrisk,Linuxプログラミングインタフェース,2012年12月 発行,ISBN978-4-87311-585-6

Docker Engine API試用

前に少し気になっていたDocker Engine APiを使ってみたので,それについて.

特に複雑なことはせず,引数に与えたイメージを削除するというプログラムを作ってみた.
ただ単純に削除するだけではつまらないので,何世代分保存しておくか,というのをオプションで指定できるようにした.

コードは mas9612/docker-tools/image-remove に置いてある.
あまりきれいなコードではないのでご注意ください.

Client.ImageList() メソッドでローカルにあるイメージの一覧が取得できるが, filter でイメージ名を指定できなさそうだったので,愚直にfor文で1つ1つ確認している.
アルゴリズムは得意ではないので,良い方法があれば教えてください…

images, err := client.ImageList(ctx, types.ImageListOptions{})
if err != nil {
    log.Fatalf("[ERROR] client.ImageList(): %s\n", err)
}

for _, image := range images {
    for _, repotag := range image.RepoTags {
        repository := strings.Split(repotag, ":")
        if repository[0] == *imageName {
            imageInfos = append(imageInfos, imageInfo{
                ID:      image.ID,
                Created: image.Created,
                Name:    repotag,
            })
        }
    }
}

削除対象のイメージをリスト出来たら,それを作成日時でソートし,指定した世代分は残してそれ以外を Client.ImageRemove() メソッドで削除している.
デフォルトでは,イメージ名にマッチしたもの全てを削除するようになっているのでお気をつけください.

if *generation > len(imageInfos) {
    *generation = len(imageInfos)
}
removeOptions := types.ImageRemoveOptions{
    Force: *force,
}
for _, image := range imageInfos[*generation:] {
    _, err := client.ImageRemove(ctx, image.ID, removeOptions)
    if err != nil {
        log.Fatalf("[ERROR] client.ImageRemove(): %s\n", err)
    }
    fmt.Printf("Image %s was deleted.\n", image.Name)
}

やっている事自体は簡単なので,ドキュメントと見比べて頂ければわかると思います.

GoでMySQLを使う – database/sql package

GoからMySQLを使う方法について調べた.
O/Rマッパーを使う方法も気になったが,まずGo標準パッケージで用意されている機能を使い,SQLを地道に実行していく方法を試した.

ソースコードは以下.

実行

Dockerを使って簡単にローカルにMySQLを準備する.

$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mypass -e MYSQL_DATABASE=testdb -e MYSQL_USER=mysql -e MYSQL_PASSWORD=mypass -d --name mysql mysql

DBの準備後,作成したソースコードを実行する.

$ go run mysql_example.go
ID: 1, Name: Tom
ID: 2, Name: Bob
ID: 3, Name: Alice

INSERTしたデータが正しく取得できていそうである.
念のため,MySQLに入って確認してみる.

$ docker exec -it mysql mysql -u root -p
Enter password:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| testdb             |
+--------------------+
5 rows in set (0.01 sec)

mysql> use testdb;
Database changed

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| test_tbl         |
+------------------+
1 row in set (0.00 sec)

mysql> select * from test_tbl;
+------+-------+
| id   | name  |
+------+-------+
|    1 | Tom   |
|    2 | Bob   |
|    3 | Alice |
+------+-------+
3 rows in set (0.00 sec)

上記の通り,正常にテーブルの作成とデータの追加が行えていることが確認できた.

解説

準備

Goの database/sql パッケージを使うと,色々なDBを扱うことができる.
しかし, database/sql パッケージとは別に,ここから使いたいDBのdriverを探してインストールしておく必要がある.
今回はMySQLを使いたいので,go-sql-driver/mysqlを利用した.
下記コマンドで go-sql-driver/mysql をインストールする.

$ go get -u github.com/go-sql-driver/mysql

データベースへの接続

データベースへ接続するには, sql.Open() メソッドを使用する.
第1引数に使用したいdriver名,第2引数に接続先を指定する.

db, err := sql.Open("mysql", "mysql:mypass@/testdb")

接続確認を行いたい場合は, sql.Open() の後に DB.Ping() メソッドを呼び出すことでできる.

if err = db.Ping(); err != nil {
    log.Fatalf("db.Ping(): %s\n", err)
}

SQLの実行

SQLの実行は, DB.Exec() 及び DB.Query() メソッドで行うことができる.
CREATE文やINSERT文など,DBからデータが返ってこないものに関しては DB.Exec() メソッドを用い,SELECT文などDBからデータを取得するのが目的であるものに関しては DB.Query() メソッドを用いる.

Exec()

_, err = db.Exec("create table test_tbl (id int, name varchar(32))")
if err != nil {
    log.Fatalf("db.Exec(): %s\n", err)
}

Query()

DBからの結果は sql.Rows に入っている.
Rows.Scan() メソッドで,1レコードの中から値(今回であればSELECT文での取得対象に * をしているため,全てのカラム = idname )を取得することができる.
Rows.Scan() メソッドの引数にはポインタを渡すことに注意する.

1レコード分の処理が終了し,次のレコードに移るためには Rows.Next() メソッドを呼び出す.

var rows *sql.Rows
rows, err = db.Query("select * from test_tbl")
if err != nil {
    log.Fatalf("db.Query(): %s\n", err)
}
defer rows.Close()

for rows.Next() {
    var (
        id   int
        name string
    )
    err = rows.Scan(&id, &name)
    if err != nil {
        log.Fatalf("rows.Scan(): %s\n", err)
    }

    fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err = rows.Err(); err != nil {
    log.Fatalf("rows.Err(): %s\n", err)
}

Go net package – Goでソケット通信

Goのnetパッケージについて軽く勉強した.
簡単なソケット通信についてまとめる.

ソケット通信

ソースコードは以下.

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    host := "localhost"
    port := "8000"
    address := net.JoinHostPort(host, port)
    conn, err := net.Dial("tcp", address)
    if err != nil {
        log.Fatalf("net.Dial(): %s\n", err)
    }
    defer conn.Close()

    request := "GET / HTTP/1.1\n\n"
    fmt.Println([]byte(request))
    _, err = conn.Write([]byte(request))
    if err != nil {
        log.Fatalf("Conn.Write(): %s\n", err)
    }

    buffer := make([]byte, 1024)
    var n int
    for {
        n, err = conn.Read(buffer)
        if n == 0 {
            break
    }
    if err != nil {
        log.Fatalf("Conn.Read(): %s\n", err)
    }
    fmt.Print(string(buffer))
    }
}

以下に簡単な解説を.

Goでのソケット通信は, Conn オブジェクトを作成するところから始まる.
まず, net.Dial()Conn オブジェクトを作成する.

conn, err := net.Dial("tcp", address)

第1引数にはネットワークの種類を,第2引数には接続したい先のアドレスを指定する.
今回はサンプルとしてローカルに立てたHTTPサーバへ接続してみるので,ネットワークの種類は TCP を指定しておく.

Conn オブジェクトを作成できたら,後は Conn.Read()Conn.Write() でデータの読み書きができる.

_, err = conn.Write([]byte(request))
...
...
n, err = conn.Read(buffer)

簡単なソケット通信は以上の3つのメソッドを使うことで簡単にできる.

Pythonで簡単にローカルHTTPサーバをたて,作成したプログラムを実行してみる.

$ python -m http.server
$ go run main.go
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.6.2
Date: Sun, 04 Feb 2018 12:47:14 GMT
Content-type: text/html; charset=utf-8
Content-Length: 336

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="main.go">main.go</a></li>
</ul>
<hr>
</body>
</html>

JoinHostPort()/SplitHostPort()

上の例でも使っているが,これらのメソッドを使うことでアドレスとポート番号の結合・分離が簡単にできる.

address := net.JoinHostPort("localhost", "8000")
fmt.Println(address) // localhost:8000

host, port, err := net.SplitHostPort("localhost:3000")
if err != nil {
log.Fatalf("net.SplitHostPort(): %s\n", err)
}
fmt.Printf("Host: %s, Port: %s\n", host, port) // Host: localhost, Port: 3000