20200909のGoに関する記事は6件です。

身に覚えのない"invalid character 'ã' looking for beginning of value"の原因

TL;DR

json.Unmarshal

invalid character 'ã' looking for beginning of value

というエラーが出た時は、JSON文字列に全角スペースが紛れている可能性大!!

スペースが全角になっている
{"name": "Taro"}

背景

APIサーバ実装中に上記のエラー発生。「aがなんで不正やねん」と思いがっつりハマったので備忘録として書きました。
(※実際にはチルダのついたãです)

環境

  • Go 1.14

サンプルコード

main.go
package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
}

func main() {
    // コロンの直後に全角が!
    jsonStr := `{"name": "Taro"}`
    person := Person{}
    err := json.Unmarshal([]byte(jsonStr), &person)
    if err != nil {
        fmt.Println(err.Error()) // invalid character 'ã' looking for beginning of value
    }
}

なぜエラーに'ã'が出てくるのか

全角文字の先頭バイト227に対応するutf-8の文字がãだからです。
Unmarshalには[]byteを渡しているので、全角文字の先頭バイトãを読み込んだ時点でパースエラーになったというのが原因でした。

fmt.Println([]byte(" ")) // [227 128 128]
fmt.Println(string(227)) // ã

encoding/jsonの実装を見ても、1バイトずつ読み込んで、スキャナが解釈できないバイトが見つかった時点でエラーを吐いていることが分かります。
Unmarshalの定義
checkValid(Unmarshal内部でJSON形式チェックに使用)の定義

最後に

全角スペースはちゃんとハイライトしよう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangの'nil slice'と'non-nil slice'で地味に詰まった話

Golangでテストコードを書いていた際に、地味に詰まったことがあったので共有。

サンプルコード

intのスライスを引数として受け取り、その中から正の整数だけを返す関数をサンプルとして用意しました。

example.go
package example

// Example は、テスト用に作成した正の整数だけを返す関数
func Example(nums []int) (ret []int) {
    for _, num := range nums {
        if num > 0 {
            ret = append(ret, num)
        }
    }
    return
}

この関数に対して、下記の様なテストコードを書きます。
(本当はもっとテストケースを用意するべきだと思います。)

example_test.go
package example

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestExample(t *testing.T) {

    tests := []struct {
        input    []int
        expected []int
    }{
        {
            input:    []int{1, -2, 3},
            expected: []int{1, 3},
        },
                {
            input:    []int{-1, -2, -3},
            expected: []int{},
        },
    }

    for _, test := range tests {
        result := Example(test.input)
        assert.Equal(t, result, test.expected)
    }
}

そして、このテストを実行してみると、下記のログが表示されテストは失敗します。

$ go test --run TestExample

--- FAIL: TestExample (0.00s)
    example_test.go:27: 
                Error Trace:    example_test.go:27
                Error:          Not equal: 
                                expected: []int(nil)
                                actual  : []int{}

                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1,2 +1,3 @@
                                -([]int) <nil>
                                +([]int) {
                                +}

                Test:           TestExample
FAIL
exit status 1

どうやら、
期待している値(expected)が、([]int) <nil>になっているのに対して、
実際に返される値(result)は、([]int) {}となっているため、テストが失敗しているようです。

nil sliceとnon-nil slice

このテスト失敗の理由を説明するためには、lengthとcapacityが共に0のスライスとして、
nil slicenon-nil sliceというものが存在することを説明する必要があります。

この2つのスライスは、見た目上ほとんど同じ挙動なのですが、
その名の通りnilかnilでないかという違いがあります。

package main

import "fmt"

func main() {
    // nil sliceになる
    var nSlice []int

    // non-nil sliceになる
    eSlice  := []int{}
    eSlice2 := make([]int, 0)

    fmt.Println(len(nSlice),  cap(nSlice),  nSlice  == nil)
    fmt.Println(len(eSlice),  cap(eSlice),  eSlice  == nil)
    fmt.Println(len(eSlice2), cap(eSlice2), eSlice2 == nil)
}
出力
0 0 true
0 0 false
0 0 false

テストの失敗の原因

Goの名前付き戻り値にsliceを指定して、何も値を代入しないままreturnした場合、nil sliceを返します。

サンプルコード
package main

import (
    "fmt"
)

func main() {
  slice := example()
  fmt.Println(len(slice),  cap(slice),  slice == nil)
}

func example () (slice []int) {
  return
}

最初にお見せしたテストでは、example関数はnil sliceを返すにもかかわらず、
non-nil sliceと比較するようなテストになってしまっていたため、テストが失敗してしまったのです。

シンプルなミスであるにもかかわらず、
nil sliceとnon-nil sliceのことを自分があまり意識していなかったために、
原因に気づくまでに少し時間がかかってしまいました。反省です。

まとめ

  • lengthとcapacityが共に0であるsliceには、nilとnon-nilの2種類がある。
  • 名前付き戻り値で、sliceをそのままreturnすると、nil sliceになる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでsliceをpoolするときの罠

sync.Poolのすすめ

Goの高速化テクニックの一つにsync.Poolを使うというものがあります. bufferとして使う領域を毎回allocせずに使いまわすことで, allocとGCにかかる時間を省くのが狙いです.

poolが効果を発揮するのは可変長データをgo routineで並列処理する場合です.
予め適切なbuffer sizeがわからない場合でも, 足りなくなったら都度交換することで, 十分な時間が経てばpool内は大きなbufferだけが残る事になり, 償却的にzero allocにできます.

// pool初期化
pool := sync.Pool {
        New: func() interface{} {
            return &Buffer{}
        },
}

//...

// 使うとき
buf := pool.Get().(*Buffer)
buf.Reset()
if !buf.EnoughSize() {
    buf = NewLargerBuffer() //bufferが足りないので大きいbufferに取り替える
}
pool.Put(buf)

ここで古い小さなbufferをpoolに戻さないところがポイントです.

例えばhttp handlerでzero allocを達成できれば, webサーバーの高速化にもかなり貢献するはずです.

sliceをpoolする

bufferとしてsliceを使うユースケースは多いと思いますが, sliceをpoolに突っ込んだ場合, zero allocを達成するまでにいくつか罠を踏んだので書いておきます.

slice実体をpoolする(罠1)

sliceをpoolしようとして, まず私はこう書きました. poolは[]intを貯めています.

func BenchmarkSlicePool(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            return make([]int, 0, 0) // 適切なsizeがわからない場合を想定してあえてcap = 0とした. 
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            arr := pool.Get().([]int)[0:0] // キャストしてlenリセット
            arr = append(arr, 42)
            pool.Put(arr)
        }
    })
}

slice = ポインタという感覚からすると正しいように思えますが, しかしこれはzero allocにはなりません.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePool 12 100 28.2 ns/op 32 B/op 1 allocs/op

このallocは pool.Put(arr) から来ています.
Putの引数はinterface{}型なので, sliceをinterface{}にキャストしますがそのときこの中でslice headerをheapにコピーする必要があります.

実際にキャストの内部を見てみると...

src/runtime/iface.go
   380  func convTslice(val []byte) (x unsafe.Pointer) {
   381      // Note: this must work for any element type, not just byte.
   382      if (*slice)(unsafe.Pointer(&val)).array == nil {
   383          x = unsafe.Pointer(&zeroVal[0])
   384      } else {
   385          x = mallocgc(unsafe.Sizeof(val), sliceType, true)
   386          *(*[]byte)(x) = val
   387      }
   388  }   

なお, slice headerはsliceの内部表現で, 下記の通りptr, len, capを持つstructです.

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

sliceのポインタをpoolする(罠2)

OK. じゃあポインタにすっかという事で, return &make([]int)がsyntax errorになる事に若干イライラしつつ, こうします. 今度は*[]intを貯めています.

func BenchmarkSlicePtrPool_1(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            arr := (*pool.Get().(*[]int))[0:0] // キャストしてderefしてlenリセット
            arr = append(arr, 42)
            pool.Put(&arr)
        }
    })
}

しかしこれもzero allocならず.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_1 12 100 27.0 ns/op 32 B/op 1 allocs/op

escape解析をしてみると, Getの行でarrがheapに行ってしまっていることがわかります.

$ go test . -gcflags="-m"
...
moved to heap: arr

Putでheapに行くことがわかっているので, arrをstackに確保しなかったということですね.

sliceのポインタをpoolし, ポインタを残す(罠3)

arrをstackに確保するべく, 次の手は

func BenchmarkSlicePtrPool_2(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ptr := pool.Get().(*[]int) // キャスト
            arr := *ptr                // deref
            arr = arr[0:0]             // lenリセット
            arr = append(arr, 42)
            pool.Put(ptr)
        }
    })
}

これもzero allocならず.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_2 12 100 12.4 ns/op 8 B/op 1 allocs/op

今度は, appendでせっかくgrowしたarrがPutできていません. Putの時点でptr != &arrであり, ptrの指すsliceは依然としてcap == 0のため, 毎回growしてしまいます.

sliceのポインタをpoolし, ポインタを残し, Put前にheaderをコピーする (多分これが一番速いと思います)

Putする前に, ptrが指すheap上のslice headerにstack上のslice header (arr)をコピーします.

func BenchmarkSlicePtrPool_3(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ptr := pool.Get().(*[]int)
            arr := *ptr
            arr = arr[0:0]
            arr = append(arr, 42)
            *ptr = arr                   //slice headerの書き戻し
            pool.Put(ptr)
        }
    })
}

これでようやく zero alloc達成できました.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_3 12 100 3.39 ns/op 0 B/op 0 allocs/op
BenchmarkSlicePool (再掲) 12 100 28.2 ns/op 32 B/op 1 allocs/op

slice実体のpoolから比べると8.3x速くなっています. 配列の領域自体は使い回しできていても, 高々32Bのallocでも馬鹿にできないことがわかります.

検証環境

go version go1.15.1 darwin/amd64
Mac Pro (Late 2013)
3.5 GHz 6-Core Intel Xeon E5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでslice poolを使うときの罠

sync.Poolのすすめ

Goの高速化テクニックの一つにsync.Poolを使うというものがあります. bufferとして使う領域を毎回allocせずに使いまわすことで, allocとGCにかかる時間を省くのが狙いです.

poolが効果を発揮するのは可変長データをgo routineで並列処理する場合です.
予め適切なbuffer sizeがわからない場合でも, 足りなくなったら都度交換することで, 十分な時間が経てばpool内は大きなbufferだけが残る事になり, 償却的にzero allocにできます.

// pool初期化
pool := sync.Pool {
        New: func() interface{} {
            return &SmallBuffer{}
        },
}

//...

// 使うとき
buf := pool.Get().(*Buffer)
buf.Reset()
if !buf.EnoughSize() {
    buf = NewLargerBuffer() //bufferが足りないので大きいbufferに取り替える
}
pool.Put(buf)

ここで古い小さなbufferをpoolに戻さないところがポイントです.

例えばhttp handlerでzero allocを達成できれば, サーバーの高速化にかなり貢献するはずです.

sliceをpoolする

bufferとしてsliceをユースケースとして使うことは多いと思いますが, sliceをpoolに突っ込んだ場合, zero allocを達成するまでにいくつか罠を踏んだので書いておきます.

slice実体をpoolする(罠1)

sliceをpoolしようとして, まず私はこう書きました. poolは[]intを貯めています.

func BenchmarkSlicePool(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            return make([]int, 0, 0) // 適切なsizeがわからない場合を想定してあえてcap = 0とした. 
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            arr := pool.Get().([]int)[0:0] // キャストしてlenリセット
            arr = append(arr, 42)
            pool.Put(arr)
        }
    })
}

slice = ポインタという感覚からすると正しいように思えますが, しかしこれはzero allocにはなりません.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePool 12 100 28.2 ns/op 32 B/op 1 allocs/op

このallocは pool.Put(arr) から来ています.
Putの引数はinterface{}型なので, sliceをinterface{}にキャストしますがそのときこの中でslice headerをheapにコピーする必要があります.

実際にキャストの内部を見てみると...

src/runtime/iface.go
   380  func convTslice(val []byte) (x unsafe.Pointer) {
   381      // Note: this must work for any element type, not just byte.
   382      if (*slice)(unsafe.Pointer(&val)).array == nil {
   383          x = unsafe.Pointer(&zeroVal[0])
   384      } else {
   385          x = mallocgc(unsafe.Sizeof(val), sliceType, true)
   386          *(*[]byte)(x) = val
   387      }
   388  }   

なお, slice headerはsliceの内部表現で, 下記の通りptr, len, capを持つstructです.

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

sliceのポインタをpoolする(罠2)

OK. じゃあポインタにすっかという事で, return &make([]int)がsyntax errorになる事に若干イライラしつつ, こうします. 今度は*[]intを貯めています.

func BenchmarkSlicePtrPool_1(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            arr := (*pool.Get().(*[]int))[0:0] // キャストしてderefしてlenリセット
            arr = append(arr, 42)
            pool.Put(&arr)
        }
    })
}

しかしこれもzero allocならず.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_1 12 100 27.0 ns/op 32 B/op 1 allocs/op

escape解析をしてみると, Getの行でarrがheapに行ってしまっていることがわかります.

$ go test . -gcflags="-m"
...
moved to heap: arr

Putでheapに行くことがわかっているので, arrをstackに確保しなかったということですね.

sliceのポインタをpoolし, ポインタを残す(罠3)

arrをstackに確保するべく, 次の手は

func BenchmarkSlicePtrPool_2(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ptr := pool.Get().(*[]int) // キャスト
            arr := *ptr                // deref
            arr = arr[0:0]             // lenリセット
            arr = append(arr, 42)
            pool.Put(ptr)
        }
    })
}

これもzero allocならず.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_2 12 100 12.4 ns/op 8 B/op 1 allocs/op

今度は, appendでせっかくgrowしたarrがPutできていません. Putの時点でptr != &arrであり, ptrの指すsliceは依然としてcap == 0のため, 毎回growしてしまいます.

sliceのポインタをpoolし, ポインタを残し, Put前にheaderをコピーする (多分これが一番速いと思います)

Putする前に, ptrが指すheap上のslice headerにstack上のslice header (arr)をコピーします.

func BenchmarkSlicePtrPool_3(b *testing.B) {
    pool := sync.Pool{
        New: func() interface{} {
            s := make([]int, 0, 0)
            return &s
        },
    }
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            ptr := pool.Get().(*[]int)
            arr := *ptr
            arr = arr[0:0]
            arr = append(arr, 42)
            *ptr = arr                   //slice headerの書き戻し
            pool.Put(ptr)
        }
    })
}

これでようやく zero alloc達成できました. なお, ptr == &arrの場合は代入を省けますが, 条件分岐する方が遅かったです.

bench name cores parallelism ns / op alloc bytes / op alloc times / op
BenchmarkSlicePtrPool_3 12 100 3.39 ns/op 0 B/op 0 allocs/op

slice実体のpoolから比べると8.3x速くなっています. 配列の領域をコピーしていなくても, 高々32Bのallocでも馬鹿にできないことがわかります.

検証環境

go version go1.15.1 darwin/amd64
Mac Pro (Late 2013)
3.5 GHz 6-Core Intel Xeon E5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超簡単】GoのインストールとVSCode設定方法

インストール

インストール

Mac

以下のコマンドを実行します。

$ brew install go

Windows

以下のページより、Windows用のインストーラー(.msiファイル)をダウンロードして実行します。
Download and install

go-download.PNG

インストーラーに表示される手順に従って進めます。
インストール先フォルダは、特段の理由がなければデフォルトのまま変更しない方がいいです。

インストール後確認

インストール後、コマンドラインで以下を実行して動作すればOK。

$ go version
go version go1.15.1 windows/amd64

環境変数の設定

PATHに%GOPATH%\binを登録します。
PATHに登録しておかないとgo getでインストールしたコマンドを実行することができません。

Mac

以下のコマンドを実行して設定します。

$ echo "export GOPATH=$(go env GOPATH)" >> ~/.bash_profile
$ echo "export PATH=$PATH:$(go env GOPATH)/bin" >> ~/.bash_profile
$ source ~/.bash_profile

Windows

環境変数の設定がわからない場合は、以下が参考になります。
WindowsにGo言語をインストールする方法まとめ

VSCode Extension

VSCodeをインストールしていない場合は、こちらからインストールします。

インストール

VSCodeを起動し、拡張機能(Ctrl+Shift+X)を開いてGoで検索します。
検索結果に表示される以下をインストールします。

go-extension.PNG

主な機能

  • Lint&Format
  • Debug
  • コード補完

Tips

VSCodeで開発をしていて、補完が効かない、エラーが消えないなどの時は、VSCode上で以下のコマンドを実行するとたいてい解決します。

  • Ctrl + Shift + Pでコマンドパレットを表示
  • Go: Restart Language Serverと入力して実行

プロジェクトのはじめかた・おすすめライブラリ

手前みそですが、以下の記事を参考にして頂けると幸いです。
Goプロジェクトのはじめかたとおすすめライブラリ8.5選。ひな形にも使えるサンプルもあるよ。
⇒Qiitaのデイリートレンド5位に入りました!

さいごに

Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヒヨコとはじめる、やさしいGo言語の並行処理

こんばんは、ねじねじおです。

Goの並行処理をヒヨコと一緒に学んでいきます。

今回のポイントは3つだけです。
1. goroutine で並行処理を実行する
2. WaitGroup で処理の終了を待つ
3. channel でキューをつくる

ここに10匹の泣いているヒヨコを慰めるプログラムを用意しました。このプログラムでは一人のヒヨコシッターが一匹ずつ順番にヒヨコを慰めます。一匹のヒヨコが泣き止むには1秒かかります。

// 泣いているヒヨコを1秒で慰める関数
func comfort(piyo string) string {
    time.Sleep(1 * time.Second)
    return strings.Replace(piyo, "(> e <)", "(・e・)ワーイ", 1)
}

func main() {
    start := time.Now()

    for i := 1; i <= 10; i++ {
        // 泣いてるヒヨコ
        piyo := fmt.Sprintf("[%v](> e <)", i)
        fmt.Println(piyo)
        // 慰める
        piyo = comfort(piyo)
        fmt.Println(piyo)
    }

    fmt.Printf("%f秒\n", (time.Now().Sub(start)).Seconds())
}

出力結果

[1](> e <)
[1](・e・)ワーイ
[2](> e <)
[2](・e・)ワーイ
[3](> e <)
[3](・e・)ワーイ
... 省略 ...
[10](> e <)
[10](・e・)ワーイ
10.030122秒

すべてのヒヨコを慰め終わるまでに、約10秒かかります。
このプログラムを使って、並行処理の書き方を学んでいきます。

1. goroutine で並行処理を実行する

goroutine は、Goのランタイムに管理される軽量なスレッドです。並行処理の肝です。
その実行は簡単で、go の後に続けてスレッド化したい関数を呼び出すだけです。次のプログラムでは、泣いているヒヨコの数だけスレッドを作ってヒヨコシッターを増やすことができます。

func main() {
    start := time.Now()

    for i := 1; i <= 10; i++ {
        // 泣いてるヒヨコ
        piyo := fmt.Sprintf("[%v](> e <)", i)
        // goroutine を作成
        go func(piyo string) {
            fmt.Println(piyo)
            // 慰める
            piyo = comfort(piyo)
            fmt.Println(piyo)
        }(piyo)
    }

    // すべての処理が終わるまで待つ
    time.Sleep(2 * time.Second)

    fmt.Printf("%f秒\n", (time.Now().Sub(start)).Seconds())
}

出力結果

[2](> e <)
[7](> e <)
[10](> e <)
[1](> e <)
[5](> e <)
[3](> e <)
[4](> e <)
[8](> e <)
[9](> e <)
[6](> e <)
[5](・e・)ワーイ
[4](・e・)ワーイ
[6](・e・)ワーイ
[8](・e・)ワーイ
[2](・e・)ワーイ
[1](・e・)ワーイ
[9](・e・)ワーイ
[7](・e・)ワーイ
[10](・e・)ワーイ
[3](・e・)ワーイ
2.005158秒

すべてのヒヨコが2秒で泣き止みました。同時に処理が実行されるのでヒヨコの順番はバラバラになります。
ちなみに、ねじおのMacのコア数は4です。

func main() {
    fmt.Println(runtime.NumCPU())
}
// 出力結果
// 4

2. WaitGroup で処理の終了を待つ

前のプログラムでは、goroutineの処理が終わるのを待つために最後に2秒のスリープを入れていました。しかし、実際には処理はもっと早く終わっているかもしれないし、もっと遅いかもしれません。もし、ヒヨコが泣き止む前に呼び出し元の処理が終了してしまうと、ヒヨコとヒヨコシッターは闇の世界に消えてしまいます。

そんな悲しい結末にならないように、sync.WaitGroupを使ってgoroutineの終了を確実に待機するようにします。
sync.WaitGroupの使い方は、4ステップです。

  1. WaitGroupをつくる。 wg := sync.WaitGroup{}
  2. goroutineを呼び出す前に、呼び出し元でインクリメントする。wg.Add(1)
  3. goroutineの処理が終わったら、goroutine内でデクリメントする。wg.Done()
  4. すべての処理が終わるのを待つ。wg.Wait()

それでは、main()を書き換えます。

func main() {
    start := time.Now()

    wg := sync.WaitGroup{} //// 1. WaitGroupを作成
    for i := 1; i <= 10; i++ {
        // 泣いてるヒヨコ
        piyo := fmt.Sprintf("[%v](> e <)", i)
        // goroutine を作成
        wg.Add(1) //// 2. WaitGroupをインクリメント
        go func(piyo string) {
            defer wg.Done() //// 3. WaitGroupをデクリメント

            fmt.Println(piyo)
            // 慰める
            piyo = comfort(piyo)
            fmt.Println(piyo)
        }(piyo)
    }

    wg.Wait() //// 4. すべての処理が終わるまで待つ

    fmt.Printf("%f秒\n", (time.Now().Sub(start)).Seconds())
}

出力結果

[10](> e <)
[8](> e <)
[9](> e <)
[2](> e <)
[4](> e <)
[5](> e <)
[6](> e <)
[1](> e <)
[7](> e <)
[3](> e <)
[7](・e・)ワーイ
[3](・e・)ワーイ
[10](・e・)ワーイ
[8](・e・)ワーイ
[9](・e・)ワーイ
[2](・e・)ワーイ
[6](・e・)ワーイ
[4](・e・)ワーイ
[5](・e・)ワーイ
[1](・e・)ワーイ
1.000854秒

これで無駄なく確実にgoroutineの終了を待てるようになりました。

3. channel でキューをつくる

さて、これまでのプログラムでは10匹のヒヨコに対して10人のヒヨコシッターがいました。実際にはそんな贅沢なことは稀です。ヒヨコシッターが2人しかいないときはどうすればよいでしょうか?

ここでは、泣いているヒヨコに一列に並んでもらい、何人かのヒヨコシッターが先頭のヒヨコから順番に慰めていくことにします。所謂、キューってやつですね。

Goにはchannelというスレッド間で通信する仕組みがあり、これを使うと簡単にキューを作ることができます。

キューを作成する

queue := make(chan データ型, キューのサイズ)

キューに値を追加する

queue <- val

キューに空き枠がない場合、この処理は空き枠ができるまでブロック(待機)します。

キューから値を取り出す

val, ok <-queue
if !ok {
    // queue is closed.
}

キューが空で、かつ、キューの入り口が閉じていない場合、この処理はキューに値が入るまでブロック(待機)します。
キューが空で、かつ、キューの入り口が閉じている場合、okfalse が入ります。

キューの入り口を閉じる

close(queue)

キューの入り口が閉じれるとキューに値を追加できなくなります。
また、キューの受信者は、キューが空のとき、キューが閉じられていること(もう値が入ってくることはないこと)を知ることができます。

では、main()を書き換えます。ヒヨコシッターは2人です。

const (
    thread = 2 // スレッド数
)

func main() {
    start := time.Now()

    // キューを作成
    queue := make(chan string, thread*2)

    // キューを処理するスレッドを作成
    wg := sync.WaitGroup{}
    for i := 0; i < thread; i++ {
        // goroutine を作成
        wg.Add(1)
        go func() {
            defer wg.Done()
            for {
                // キューからヒヨコを取り出す
                piyo, ok := <-queue
                if !ok {
                    break
                }
                fmt.Println(piyo)
                // 慰める
                piyo = comfort(piyo)
                fmt.Println(piyo)
            }
        }()
    }

    // キューに泣いているヒヨコを追加
    for i := 1; i <= 10; i++ {
        piyo := fmt.Sprintf("[%v](> e <)", i)
        queue <- piyo
    }
    close(queue) // キューを閉じる

    wg.Wait() // すべての処理が終わるまで待つ

    fmt.Printf("%f秒\n", (time.Now().Sub(start)).Seconds())
}

出力結果

[2](> e <)
[1](> e <)
[1](・e・)ワーイ
[3](> e <)
[2](・e・)ワーイ
[4](> e <)
[3](・e・)ワーイ
[5](> e <)
[4](・e・)ワーイ
[6](> e <)
[5](・e・)ワーイ
[7](> e <)
[6](・e・)ワーイ
[8](> e <)
[7](・e・)ワーイ
[9](> e <)
[8](・e・)ワーイ
[10](> e <)
[10](・e・)ワーイ
[9](・e・)ワーイ
5.008001秒

うまくいったようです。2人で分担することで、約5秒で10匹のヒヨコを泣き止ますことができました。

さて、
退勤時間になったのでヒヨコシッターに仕事を中断させたいときにはどうすればよいのだろう?
ヒヨコが暴れだしてヒヨコシッターがパニックを起こしたらどうすれよいのだろう?
など疑問は残しつつも、今回はこれで終わります。

OK。
ねじねじおでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む