ゴルーチンの同期

チャネルを使ってゴルーチンの同期をとることができる。
次のコードを見てほしい。

package main

import (
    "fmt"
    "time"
)

func makeRoutine(code string, in <-chan int) chan int {
    out := make(chan int)
    go func() {
        for {
            <- in fmt.Print(code)
            time.Sleep(200 * time.Millisecond) out <- 0
        }
    }()
    return out
}

func main() {
    ch1 := make(chan int)
    ch2 := makeRoutine("h", ch1)
    ch3 := makeRoutine("e", ch2)
    ch4 := makeRoutine("y", ch3)
    ch5 := makeRoutine("!", ch4)
    ch6 := makeRoutine(" ", ch5)
    for i := 0; i < 10; i++ {
        ch1 <- 0 <- ch6
    }
}

makeRoutine 関数は、文字列 code とチャネル in を引数にとる。まずチャネル out を作っておいて、無名関数をゴルーチンで起動、最後に out を返している。無名関数は無限ループになっていて、in から何か送信されてくるのを待って code を出力し、200 ミリ秒待ってから out にデータを送信する。
main 関数では、最初の(送信用)チャネル ch1 を作り、makeRoutine 関数に渡している。返ってきたチャネルは、次の makeRoutine 関数に渡され、返ってきたチャネルはさらに次の makeRoutine 関数に渡される。こうして5つの makeRoutine 関数の中のゴルーチンがチャネルを通じて数珠つなぎのようになる。これらのゴルーチンは、最初のチャネル ch1 にデータを送信することによって動作を開始し、最後のチャネル(main 関数から見ると ch6)にデータを送信して終わる。これを10回繰り返している。

実行してみよう。

^o^ > go run go_hey.go
hey! hey! hey! hey! hey! hey! hey! hey! hey! hey!

この実行例ではわからないけど、1文字ずつ時間を空けて hey! の文字列が10回出力されている。各ゴルーチンは1文字出力するだけなので、うまく同期して動作している様子がわかる。

sync.WaitGroup

ゴルーチンの終了待ちには、チャネルを使うほかに sync パッケージの WaitGroup を使う方法もある。
使い方はこうだ:

  1. sync.WaitGroup の変数を作る
  2. その変数に、終了待ちをするゴルーチンの数を設定する
  3. ゴルーチンを呼び出す。このとき、sync.WaitGroup の変数を渡す
  4. ゴルーチン側では、終了したら Done 関数を呼ぶ
  5. メインルーチン側で、Wait 関数を呼ぶ

実際に試してみよう。

package main

import (
    "fmt"
    "time"
    "sync"
)

func test(n int, name string, wg *sync.WaitGroup) {
    for i := 0; i < n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup wg.Add(3)
    go test(6, "foo", &wg)
    go test(4, "bar", &wg)
    go test(8, "baz", &wg) wg.Wait()
}
^o^ > go run go_waitgroup.go
0 baz
0 bar
0 foo
1 baz
1 bar
1 foo
2 baz
2 bar
2 foo
3 baz
3 bar
3 foo
4 baz
4 foo
5 baz
5 foo
6 baz
7 baz

チャネルとゴルーチンの終了待ち

チャネルはゴルーチンの間で通信するためのデータだ。次のように生成する。

ch := make(chan T, bufsize)

T はチャネルでやり取りするデータの型、bufsize はデータを格納するバッファのサイズで省略すると 0 になる。チャネルの型は chan T。
関数の引数や変数の型指定の時、chan の前に <- をつけると受信専用に、後に <- をつけると送信専用になる。 チャネルを使うと、ゴルーチンの終了待ちができるようになる。 次の例では test 関数をゴルーチンとして呼び出し、チャネルを渡している。test 関数は 0.5 秒間隔で name を出力し、終了するときにチャネルを通じて name を送ってくる。main 関数ではチャネルからデータが送られてくるのを待っている。

package main

import (
    "fmt"
    "time"
)

func test(n int, name string, c chan<- string) {
    for i := 1; i <= n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
    c <- name
}

func main() {
    c := make(chan string)
    go test(6, "foo", c)
    go test(4, "bar", c)
    go test(8, "baz", c)
    for i := 0; i < 3; i++ {
        name := <- c fmt.Println(name)
    }
}

実行してみよう。

^o^ > go run go_channel.go
1 foo
1 baz
1 bar
2 foo
2 baz
2 bar
3 foo
3 baz
3 bar
4 foo
4 baz
4 bar
5 foo
5 baz
bar
6 foo
6 baz
foo
7 baz
8 baz
baz

数字とともに出力されているのが test 関数内で出力したもの、数字のないのがゴルーチンが終了した後に main 関数で出力したものだ。3つのゴルーチンが並行して動き、main 関数ではその終了を待っていることがわかる。

reverse

Go でスライスを逆順にしたかったんだけど、そういう関数は用意されてないようだ。じゃあどうするかというと for 文を使ってひとつずつ入れ替えてくしかないみたい。こんなふうに。

package main

import (
    "fmt"
)

func main() {
    s := []int{ 1,2,3,4,5 }
    for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 {
        s[i], s[j] = s[j], s[i]
    }
    fmt.Println(s)
}
^o^ > go run reverse.go
[5 4 3 2 1]

文字列を逆順にするにはいったん rune 型のスライスにしてから。

package main

import (
    "fmt"
)

func main() {
    s := "あいうえお"

    runes := []rune(s)
    for i, j := 0, len(runes) - 1; i < j; i, j = i + 1, j - 1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    fmt.Println(string(runes))
}
^o^ > go run reverse_string.go
おえういあ

ゴルーチン

ゴルーチンは、Go で並行プログラミングを実現する機能だ。Elixir のプロセスと同じようなものだと理解した。
ゴルーチンを使うには次のように関数呼び出しの前に go をつけるだけだ。これでその関数は新しいゴルーチンの中で実行され、プログラムはゴルーチンの終了を待つことなく次の処理に移る。つまりゴルーチンの処理とメインの処理が平行に動作するってわけだ。
例を見てみよう。まずは普通の(ゴルーチンを使わない)プログラム。

package main

import (
    "fmt"
    "time"
)

func test(n int, name string) {
    for i := 1; i <= n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    test(5, "foo") test(5, "bar")
}

2度呼び出されている test 関数は、それぞれ “foo” と “bar” を5回ずつ出力する。これは当然書いてある順に処理される。

^o^ > go run go_name.go
1 foo
2 foo
3 foo
4 foo
5 foo
1 bar
2 bar
3 bar
4 bar
5 bar

じゃあ、次はゴルーチンを使ってみよう。ひとつめの関数呼び出しをゴルーチンに渡してみる。こんなプログラムになる。

package main

import (
    "fmt"
    "time"
)

func test(n int, name string) {
    for i := 1; i <= n; i++ {
        fmt.Println(i, name)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    go test(5, "foo")
    test(5, "bar")
}

実行してみる。

^o^ > go run go_name2.go
1 bar
1 foo
2 bar
2 foo
3 bar
3 foo
4 bar
4 foo
5 bar
5 foo

“foo” と “bar” が交互に出力され、二つの関数呼び出しが平行に動作している様子がわかる。

ちなみに、メインのプログラムが終了するとゴルーチンも終了するので、2つの関数呼び出しを両方ともゴルーチンにしてしまうと、何も出力されなくなる。

Ubuntu 16.04にGolang 1.9をインストール

ググると、Ubuntu の公式パッケージにある Go は 1.6 と古いので非公式のリポジトリを登録しろ、という情報があるんだけど、apt search golang してみたら golang-1.9 というパッケージがあった。なので、これをインストールする。

takatoh@envelopes $ sudo apt install golang-1.9

ところが go version コマンドを実行しても、インストールされてない、apt install golang-go をしろ、と言われる。素直にそうしてみると、今度は Go 1.6 がインストールされてしまった。
調べてみると、/usr/lib の下に golang-1.9 と golang-1.6 があって、/usr/bin/go から 1.6 の方へリンクがはられている。ということはこのリンクを 1.9 の方へはりなおしてやればいいはず。いったん 1.6 をアンインストールしてから、リンクをはりなおした。

takatoh@envelopes $ sudo ln -s /usr/lib/go-1.9/bin/go /usr/bin/go
takatoh@envelopes $ sudo ln -s /usr/lib/go-1.9/bin/gofmt /usr/bin/gofmt

これで無事完了。

takatoh@envelopes $ go version
go version go1.9.2 linux/amd64

JSONを整形するツール

本当はゴルーチンについて書きたいんだけど、まだ頭の中で整理ができてないので今日は別のことを書く。

Web サービスやなんかの API で JSON を返してくれるのはよくあること。ただ、基本的にプログラムが処理するように想定されていて、人間が見やすい形にはなっていないことが多い。まあ、当たり前ではあるんだけど、それでも目で見て確かめたいときもある。そういう時は見やすく整形してくれるツールがほしくなる。
ググると jq っていうツールが見つかる。このツール自体は、JSON の整形だけでなく検索とかいろいろできるようで、おまけに Windows 用のバイナリもあるのでちょっと使ってみた。
ところが!もとの JSON が UTF-8 のせいかもしれないけど、Windows のコマンドプロンプトでは日本語が文字化けしてしまって読めない。おまけにどういうわけか、コマンドプロンプトのフォントが変更されてしまうという、謎の現象に見舞われた。
これでは使えないので、じゃあ、Go で整形するだけのツールを書いてみようか、と思って書いたら意外にも簡単だった、というのが今日の話。

サンプルの JSON はこんなの。Web で拾ったサンプルだけど、どこのページだか忘れてしまった。

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "pref": "富山県", "city1": "下新川郡", "city2": "朝日町" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 136.111111, 36.111111 ], [ 136.222222, 36.222222 ] ] ] } }, { "type": "Feature", "properties": { "pref": "富山県", "city1": "氷見市", "city2": "" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 136.333333, 36.333333 ], [ 136.444444, 36.444444 ] ] ] } }, { "type": "Feature", "properties": { "pref": "富山県", "city1": "高岡市", "city2": "" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 136.555555, 36.555555 ], [ 138.666666, 36.666666 ] ] ] } } ]}

全部が1行に詰め込まれてる上に、日本語が混じっている。

で、書いたツールがこれ。pj という名前にした(ファイル名は main.go だけど)。整形には encoding/json パッケージの Indent 関数を使っている。

package main

import (
    "fmt"
    "encoding/json"
    "bytes"
    "os"
    "io/ioutil"
    "flag"
)

const (
    progVersion = "v0.1.0"
)

func prettyJson(src []byte) string {
    buf := make([]byte, 0)
    dst := bytes.NewBuffer(buf)
    err := json.Indent(dst, src, "", " ")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    return dst.String()
}

func main() {
    flag.Usage = func() {
    fmt.Fprintf(os.Stderr,
    `Usage:
%s [options] 
Options:
`, os.Args[0])
    flag.PrintDefaults()
}
    opt_version := flag.Bool("version", false, "Show version.")
    flag.Parse()

    if *opt_version {
        fmt.Println(progVersion)
        os.Exit(0)
    }

    infile := flag.Args()[0]
    src, err := ioutil.ReadFile(infile)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    pretty := prettyJson(src)
    fmt.Println(pretty)
}

試してみよう。

^o^ > go build

^o^ > pj sample.json
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "pref": "富山県",
        "city1": "下新川郡",
        "city2": "朝日町"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              136.111111,
              36.111111
            ],
            [
              136.222222,
              36.222222
            ]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "pref": "富山県",
        "city1": "氷見市",
        "city2": ""
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              136.333333,
              36.333333
            ],
            [
              136.444444,
              36.444444
            ]
          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "pref": "富山県",
        "city1": "高岡市",
        "city2": ""
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              136.555555,
              36.555555
            ],
            [
              138.666666,
              36.666666
            ]
          ]
        ]
      }
    }
  ]
}

何のデータなんだかよくわからないけど、うまくいった。もとのエンコーディングは UTF-8 だけど、Windows のコマンドプロンプトに出力しても文字化けしない。もちろんファイルに書き出せばちゃんと UTF-8 で保存される。
うまくいったので GitHub にも公開しておいた。

cf. https://github.com/takatoh/pj

インストールするにはこうすればいい:

^o^ > go get github.com/takatoh/pj

定型データの入力

定型のデータを読み込んで、型変換をしてくれる関数があると便利だ。
fmt パッケージの ScanFscan は空白文字で区切られたテキストデータを読み込み、可変長引数で渡されたデータ形式の変数(のポインタ)に変換して格納してくれる。Scan は標準入力から、Fscanio.Reader から読み込む。
まずは Scan の例。

package main

import (
    "fmt"
    "os"
    "io"
)

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)

    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Scan(&n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 && err == io.EOF {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    fmt.Println(sumi, sumf, suma)
}
^o^ > go run scan.go
1 1.5 1
2 2.3 2
3 3.8 3
6 7.6 [1 2 3]

go run scan.go コマンドに続く3行が入力で、1行ごとに整数、実数、文字列に変換される。ctrl + c で入力を終了すると、結果が表示される。最後の行が結果だ。

次は Fscan の例。io.Reader から(つまりファイルから)読み込む。

package main

import (
    "fmt"
    "os"
    "io"
)

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)

    filename := os.Args[1]
    file, err := os.Open(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Fscan(file, &n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 && err == io.EOF {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    file.Close()
    fmt.Println(sumi, sumf, suma)
}
^o^ > cat fscan_sample.txt
10 1.234 foo
20 5.678 bar
30 9.876 baz

^o^ > go run fscan.go fscan_sample.txt
60 16.788 [foo bar baz]

ScanfFscanf は書式付き入力だ。引数で指定された書式で入力を解釈して読み込んでくれる。Scanf は標準入力から、Fscanfio.Reader (つまりファイル)から読み込む。
Scanf の例。

package main

import (
    "fmt"
    "os"
)

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)
    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Scanf("%d,%f,%q\n", &n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    fmt.Println(sumi, sumf, suma)
}

この例では整数、実数、クォートされた文字列をカンマ区切りしたものを入力として期待している。

^o^ > go run scanf.go
10,1.234,"foo"
20,5.678,"bar"
30,9.876,"baz"
60 16.788 [foo bar baz]

最後に Fscanf。これはファイルから読み込むことのほかは Scanf と同じだ。

package main

import (
    "fmt"
    "os"
)

func main() {
    sumi := 0
    sumf := 0.0
    suma := make([]string, 0)

    filename := os.Args[1]
    file, err := os.Open(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    for {
        var n int
        var m float64
        var s string
        i, err := fmt.Fscanf(file, "%d,%f,%q\n", &n, &m, &s)
        if i == 3 {
            sumi += n
            sumf += m
            suma = append(suma, s)
        } else if i == 0 {
            break
        } else {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
    file.Close()
    fmt.Println(sumi, sumf, suma)
}
^o^ > cat fscanf_sample.txt
10,1.234,"foo"
20,5.678,"bar"
30,9.876,"baz"

^o^ > go run fscanf.go fscanf_sample.txt
60 16.788 [foo bar baz]

cat

ファイル入出力の練習に cat コマンドを写経してみた。
まずひとつめ、bufioReadString 関数で1行ずつ処理。

package main

import (
    "os"
    "fmt"
    "io"
    "bufio"
)

func cat(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    rd := bufio.NewReader(file)
    for {
        s, err := rd.ReadString('\n')
        if err == io.EOF { break }
        fmt.Print(s)
    }
    file.Close()
}

func main() {
    for _, name := range os.Args[1:] {
        cat(name)
    }
}
^o^ > go build cat.go

^o^ > .\cat cat.go cat2.go
package main

import (
        "os"
        "fmt"
        "io"
        "bufio"
)

func cat(filename string) {
        file, err := os.Open(filename)
        if err != nil {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1)
        }
        rd := bufio.NewReader(file)
        for {
                s, err := rd.ReadString('\n')
                if err == io.EOF { break }
                fmt.Print(s)
        }
        file.Close()
}

func main() {
        for _, name := range os.Args[1:] {
                cat(name)
        }
}
package main

import (
        "os"
        "fmt"
        "io/ioutil"
)

func cat(filename string) {
        buff, err := ioutil.ReadFile(filename)
        if err != nil {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1)
        }
        os.Stdout.Write(buff)
}

func main() {
        for _, name := range os.Args[1:] {
                cat(name)
        }
}

もうひとつ、io/ioutil パッケージの ReadFile 関数でファイルまるごと読み込む。

package main

import (
    "os"
    "fmt"
    "io/ioutil"
)

func cat(filename string) {
    buff, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    os.Stdout.Write(buff)
}

func main() {
    for _, name := range os.Args[1:] {
        cat(name)
    }
}

実行結果はひとつめと同じなので省略。

ファイル入出力

今日はファイル入出力。
前に書いたように、ファイル入出力にはファイルディスクリプタを使う。ファイルディスクリプタは、os パッケージの Open 関数で取得する。取得したファイルディスクリプタで、ファイルからの入力には Read 関数、出力には Write 関数を使い、終わったら Close する。まあ、普通の手順だよな。
以下、サンプル。testin.txt ファイルから読み込んだ内容を testout.txt ファイルに書き込んでいる。

package main

import (
    "os"
    "fmt"
)

func main() {
    input, err := os.Open("testin.txt")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    output, _ := os.Create("testout.txt")
    buff := make([]byte, 256)
    for {
        c, _ := input.Read(buff)
        if c == 0 { break }
        output.Write(buff[:c])
    }
    input.Close()
    output.Close()
}
^o^ > cat testin.txt
Hello, Golang!

^o^ > go run fileio.go

^o^ > cat testout.txt
Hello, Golang!