ストリーム

チャネルを使って、数列を生成することを考える。数列のような一連のデータの流れを、ストリームとよぶ。
まずは単純な例から。

package main

import (
    "fmt"
)

type Stream chan int

func makeInt(n, m int) Stream {
    s := make(Stream)
    go func() {
        for i := n; i <= m; i++ {
            s <- i
        }
        close(s)
    }()
    return s
}

func main() {
    s := makeInt(1, 20)
    for x := range s {
        fmt.Print(x, " ")
    }
    fmt.Println("")
}

type を使って、整数をやり取りするチャネルに Stream という別名をつけている。
makeInt 関数は、整数2つ(nm)を引数にとって Stream を返す。中では、Stream を作っておき、ゴルーチンで匿名関数を実行する。このゴルーチンは for ループの中で n から m までを Stream に書き込み、最後に閉じる。
こうすることによって、数列が生成できるわけだ。
実行例:

^o^ > go run stream.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

次に、Stream を高階関数に渡してみよう。マッピングを行う streamMap とフィルタリングを行う streamFilter を作ってみる。どちらも関数と Stream を受け取って、新しい Stream を返す。

package main

import (
    "fmt"
)

type Stream chan int

func makeInt(n, m int) Stream {
    s := make(Stream)
    go func() {
        for i := n; i <= m; i++ {
            s <- i
        }
        close(s)
    }()
    return s
}

func streamMap(f func(int) int, in Stream) Stream {
    s := make(Stream)
    go func() {
        for {
            x, ok := <- in
            if !ok {
                break
            }
            s <- f(x)
        }
        close(s)
    }()
    return s
}

func streamFilter(f func(int) bool, in Stream) Stream {
    s := make(Stream)
    go func() {
        for {
            x, ok := <- in
            if !ok {
                break
            }
            if f(x) {
                s <- x
            }
        }
        close(s)
    }()
    return s
}

func main() {
    s0 := makeInt(1, 20)
    for x := range s0 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
    square := func(x int) int {
        return x * x
    }
    s1 := streamMap(square, makeInt(1, 20))
    for x := range s1 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
    isOdd := func(x int) bool {
        return x % 2 != 0
    }
    s2 := streamFilter(isOdd, makeInt(1, 20))
    for x := range s2 {
        fmt.Print(x, " ")
    }
    fmt.Println("")
} 

実行例:

^o^ > go run stream2.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 289 324 361 400
1 3 5 7 9 11 13 15 17 19

タイムアウト

非常に時間のかかる処理をゴルーチンにするといつまでも終わらなくなる可能性がある。selecttime パッケージの After を組み合わせると、タイムアウトの処理ができる。
After は一定時間が経過したらデータを送信するチャネルを返す。なので、select と併用することでタイムアウトの処理ができるわけだ。

package main

import (
    "fmt"
    "time"
)

func fibo(n int) int {
    if n < 2 {
        return 1
    } else {
        return fibo(n - 2) + fibo(n - 1)
    }
}

func main() {
    ch := make(chan int, 5)
    for _, n := range []int{ 41, 40, 38, 36, 34 } {
        go func(x int) {
            ch <- fibo(x)
        }(n)
    }
    for i := 5; i > 0; {
        select {
            case n := <- ch: fmt.Println(n)
            i--
            case <- time.After(500 * time.Millisecond): fmt.Println("Timeout")
            i = 0
        }
    }
}

ここでは、時間のかかる処理(関数)として、フィボナッチ数を求める関数を用意した。これで5つのゴルーチンを起動しておき、その終了を待つと同時に time.After で500ミリ秒でタイムアウトするようにしている。つまり time.After の返り値であるチャネルからデータが送られてくる前に処理が終わったゴルーチンだけが結果を表示するわけだ。

^o^ > go run channel_after.go
9227465
24157817
63245986
Timeout

このとおり、500ミリ秒では3つしか終わらなかった。

Goで画像アップローダーを作ってみた

画像アップローダーっていうと、あれだ、ユーザーがアップロードした画像を保存しておいて掲示板かなんかから参照できるようになってるやつだ。あれ、なんでアップローダーっていうんだろうね。
ま、とにかく Go で WEB アプリケーションを書く練習に作ってみた。Go ってワンバイナリでサーバーが作れるからいいよね。
名前は Sulaimān (スライマーン)にした。

cf. github.com/takatoh/sulaiman

シングルページアプリケーション

複雑なシステムではないので、html を読み込むのはルートにアクセスしたときだけで、あとはすべて ajax で非同期に更新するシングルページアプリケーションにした。
おかげでサーバーサイドの Go だけじゃなくて、クライアントサイドの JavaScript も結構書いた。

サーバーサイド

サーバーサイドの WEB フレームワークには Echo というのを使った。ググってみた範囲では、結構簡単そうだったし、速度も速いらしい。それからデータベースの OR マッパーには gorm というのを採用。Ruby や Python の OR マッパーとちょっと勝手が異なるけど、データベースといってもテーブルひとつの簡単なものだし、Go の中では割とメジャーなものみたい。

cf. Echo
cf. gorm

ほかにも JSON を扱うライブラリとか、画像のサムネイルを作るのにはこないだちょっと書いた resize を使った。
コードのすべては上でリンクした GitHub を見てもらいたいけど、メインのファイルだけ載せておこう。

package main
import (
    "encoding/json"
    "io/ioutil"
    "strconv"

    "github.com/labstack/echo"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/sqlite"
    "github.com/takatoh/sulaiman/handler"
    "github.com/takatoh/sulaiman/data"
)

func main() {
    var config = new(data.Config)
    jsonString, err := ioutil.ReadFile("config.json")
    if err != nil {
        panic(err)
    }
    json.Unmarshal(jsonString, config)

    db, err := gorm.Open("sqlite3", "sulaiman.sqlite")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    db.AutoMigrate(&data.Photo{})

    e := echo.New()
    h := handler.New(db, config)

    e.GET("/", h.Index)
    e.GET("/title", h.Title)

    e.Static("/css", "static/css")
    e.Static("/js", "static/js")
    e.Static("/img", config.PhotoDir + "/img")
    e.Static("/thumb", config.PhotoDir + "/thumb")

    e.GET("/list/:page", h.List)
    e.POST("/upload", h.Upload)

    port := ":" + strconv.Itoa(config.Port)
    e.Logger.Fatal(e.Start(port))
}

設定ファイルやデータベースの読み込みをしたあと、Echo とハンドラーのインスタンスを作って、ルーティングの設定をしている。GET とか POST とかのメソッドでルーティングの設定をするのは Ruby の Sinatra なんかと雰囲気が似ている。

クライアントサイド

上に書いた通り、シングルページアプリケーションなので、いったんページを読み込んだ後はすべて ajax で非同期に処理をする。ページ遷移はない。
で、JavaScript のライブラリには定番の jQuery と、ページ描画のフレームワークには Vue.js というを使ってみた。jQuery は以前使ったことがあるけど Vue.js は初めてだ。

cf. Vue.js

でも、Vue.js は学習コストが低いのも売りらしく、実際特に苦労することもなかった。もっとも大したことはしてないんだけども。

ちょっとひっかかった点

それでもうまくいかなかったところはあって、ひとつにはページのタイトルがあげられる。ページタイトルは設定ファイルから読み込んで表示するようにした。まあ、当たり前の発想だよね。で、どうやって表示しようかというときに、サーバー側でテンプレートを使うことを最初に考えた。Go には html/template というライブラリが標準であって、それを使おうとしたんだけど、テンプレートの構文が Vue.js のものと同じ(データを埋め込みたいところに {{ foo }} みたいに書く)で、共存ができなかった。仕方がないので、タイトルも ajax でとってきてクライアントサイドで更新するようにした。
もうひとつもクライアントサイド。次のページを読み込む Next リンクをクリックしたときの動作を、ajax で次のようにやろうとしたところ、うまくいかない。

$("#next_link").on("click", function(event) {
    ...
});

これ、結構悩んでいろいろ調べて、結局こうなった。

$("body").on("click", "#next_link", function(event) {
    ...
});

なんでこうじゃなきゃ動かないのかわからない。あとで調べよう。
ともかくもこれで一通りはできた。

やり残したこと

たいていのアップローダーにはついている、ファイルの削除機能がない。一応、削除のためのキーをアップロードの際に入力するようにはしてあるので、今後実装するかもしれない。でもどんな UI にしようか悩んでいる。
それと、ページの下までスクロールしたら自動で次のページを読み込む、いわゆる無限スクロールも実装してみたい。
いつになるかわからないけど。

JPEG画像のリサイズ

Go で画像をリサイズしたいときには、github.com/nfnt/resize を使うといいらしい。

cf. github.com/nfnt/resize

今回はこれを使って、JPEG 画像のサムネイルを作ってみた。サムネイルは 120×120 に納まり、アスペクト比を保つものとした。

package main

import (
    "os"
    "image"
    "image/jpeg"

    "github.com/nfnt/resize"
)

func main() {
    orig_filename := os.Args[1]
    resized_filename := os.Args[2]

    orig_file, _ := os.Open(orig_filename)
    config, _, _ := image.DecodeConfig(orig_file)

    orig_file.Seek(0, 0)
    img, _, _ := image.Decode(orig_file)
    orig_file.Close()

    var resized_img image.Image
    if config.Width >= config.Height {
        resized_img = resize.Resize(120, 0, img, resize.Lanczos3)
    } else {
        resized_img = resize.Resize(0, 120, img, resize.Lanczos3)
    }

    out, _ := os.Create(resized_filename)
    jpeg.Encode(out, resized_img, nil)
    out.Close()
}

少し引っかかった点が2つ。
1つは、rezise.Resize 関数のサイズを指定する第1、第2引数の指定について。単に 120×120 にリサイズしたいだけならそのように指定すればいいけど、これだとアスペクト比が保存されずに正方形のサムネイルができてしまう。アスペクト比を保存するには、幅か高さのうち小さいほうに 0 を指定してやればいいんだけど、画像が横長なのか縦長なのかで場合分けをする必要があった。

もう1つは、場合分けをするために元画像の幅と高さが必要なので、image.DecodeConfig 関数で取得している。けど、その後、そのまま画像のデコードをしようと image.Decode 関数を呼び出してもエラーになってしまうこと。原因は、image.DecodeConfig 関数でファイルの途中まで読み込んでいるので、そのままだと image.Decode 関数はファイルの途中から読み込むことになって正常にデコードできない、ということだと思う。
そこで (*File) Seek 関数で読み込み位置をファイルの先頭に戻している。この関数は引数を2つとり、1つ目は一のオフセット、2つ目はそのオフセットをどこからにするかの指定。0 だとファイルの先頭から、1 だと現在位置から、2 だとファイルの終わりからになる。
というわけで、Seek(0, 0) として読み込み位置をファイルの先頭に戻してから image.Decode を呼び出して成功した。

一応実行例。

^o^ > go run img_resize.go sample.jpg resized.jpg

横長の画像も、縦長の画像もうまくサムネイルができた。

select

select 文を使うと、複数のゴルーチンとの通信を選択的に処理することができる。
次の例では、main 関数で起動された3つのゴルーチンからの通信を for ループの中で select 文を使って待ち受けている。もし、どれかのゴルーチンから通信を受信すれば、受信したデータを出力して次のループに移る。どれからも受信できなければ default 節が実行されて "None" を出力し、250 ミリ秒待った後に次のループに移る。
各ゴルーチンは終了するときに専用のチャネル quit を使ってデータ(この例では 0 を使っているけど何でもいい。単に終了の通知をするだけなので。)を送ってくるので、全部のゴルーチンが終了したら for ループも終了する。

package main

import (
    "fmt"
    "strconv"
    "time"
)

func test1(n int, ch, quit chan<- int) {
    for ; n > 0; n-- {
        ch <- n time.Sleep(500 * time.Millisecond)
    }
    quit <- 0
}

func test2(n int, ch chan<- float64, quit chan<- int) {
    for ; n > 0; n-- {
        ch <- float64(n) / 10.0 time.Sleep(250 * time.Millisecond)
    }
    quit <- 0
}

func test3(n int, ch chan<- string, quit chan<- int) {
    for ; n > 0; n-- {
        ch <- strconv.Itoa(n * 10) time.Sleep(750 * time.Millisecond)
    }
    quit <- 0
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan float64)
    ch3 := make(chan string)
    quit := make(chan int)

    go test1(6, ch1, quit)
    go test2(8, ch2, quit)
    go test3(4, ch3, quit)
    for n := 3; n > 0; {
        select {
            case c := <- ch1: fmt.Println(c)
            case c := <- ch2: fmt.Println(c)
            case c := <- ch3: fmt.Println(c)
            case <- quit: n--
            default: fmt.Println("None") time.Sleep(250 * time.Millisecond)
        }
    }
}
^o^ > go run channel_select.go
6
40
0.8
None
0.7
None
5
0.6
None
0.5
30
None
4
0.4
None
0.3
None
20
3
0.2
None
0.1
None
2
None
10
None
1
None
None

うまくいったようだ。

チャネルを使ってデータ交換

これまで、チャネルを使った例をいくつか見てきたけど、今回はもうちょっと意味のあるデータを通信してみる。

package main

import (
    "fmt"
)

type Req struct {
    Color string
    Reply chan<- int
}

func newReq(color string, ch chan int) *Req {
    req := new(Req)
    req.Color = color
    req.Reply = ch
    return req
}

func sendColor(n int, color string, ch chan<- *Req) {
    in := make(chan int)
    v := newReq(color, in)
    for ; n > 0; n-- {
        ch <- v <- in
    }
    ch <- nil
}

func receiveColor(n int, ch <-chan *Req) {
    for n > 0 {
        req := <- ch
        if req == nil {
            n--
        } else {
            fmt.Println(req.Color)
            req.Reply <- 0
        }
    }
}

func main() {
    ch := make(chan *Req)
    go sendColor(8, "red", ch)
    go sendColor(7, "green", ch)
    go sendColor(6, "blue", ch)
    receiveColor(3, ch)
}

ゴルーチンで起動される関数 sendColor は送信用のチャネル ch に、色名(文字列)と返信用のチャネルをまとめた Req 構造体を送信する。で、それを n 回繰り返したら chnil を送って終了する。
receiveColor はチャネル ch から Req が送られてきたら、その色名を出力してから返信用のチャネル Req.Reply0 を送信する。これは処理が完了したことを知らせるだけなので、0 でなくても int なら何でもいい。で、それを n がゼロになるまで繰り返す。この n は起動されたゴルーチンの数だ。

では、実行例。

^o^ > go run channel_color.go
blue
red
green
blue
red
green
blue
red
green
blue
red
green
blue
red
green
blue
red
green
red
green
red

Go言語で画像フォーマットのチェック

たまたま見つけたので、メモしておく。

cf. Go言語で画像ファイルか確認してみる – Qiita

画像を扱うには image パッケージを利用する。が、具体的なフォーマットを扱うにはそれぞれに対応したパッケージの import が必要。直接には使わないけど image パッケージの関数で内部的に使われる。注意しなきゃいけないのは、普通に import すると、インポートしてるのに使ってないぞエラーが出ること。そこで下のコードのように、パッケージ名の前に _ をつけている。

package main

import (
    "fmt"
    "os"
    "image"
    _ "image/jpeg"
    _ "image/png"
    _ "image/gif"

    _ "golang.org/x/image/bmp"
    _ "golang.org/x/image/tiff"
)

func main() {
    f, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

    _, format, err := image.DecodeConfig(f)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(format)
}

それから、標準で対応しているのは、JPEG, PNG, GIF のみ。ほかに BMP と TIFF が外部のパッケージを導入すれば対応できる。こんなふうに。

^o^ > go get golang.org/x/image/bmp

^o^ > go get golang.org/x/image/tiff

で、実行例。

^o^ > go run imgcheck.go sample.jpg
jpeg

^o^ > go run imgcheck.go sample.png
png

^o^ > go run imgcheck.go sample.bmp
bmp

さくらVPSのRailsアプリをバージョンアップ

さくらの VPS で動かしている Rails アプリをバージョンアップした、その記録。
今回のバージョンアップでは、Rails のバージョンアップしないけど、データベースの変更をするので若干緊張しながらやった。

まずは、アプリを停止する。
で、万が一のためにデータベースのバックアップ。

[takatoh@tk2-254-36564 ~]$ mysqldump -u lathercraft -p lathercraft_production > lcp-20180502.sql

次に、アプリのディレクトリに移動して git fetchgit merge origin/master

[takatoh@tk2-254-36564 ~]$ cd /var/www/lathercraft
[takatoh@tk2-254-36564 lathercraft]$ sudo git fetch
Password: 
remote: Counting objects: 126, done.
remote: Compressing objects: 100% (126/126), done.
remote: Total 126 (delta 95), reused 0 (delta 0)
Receiving objects: 100% (126/126), 11.91 KiB | 6 KiB/s, done.
Resolving deltas: 100% (95/95), completed with 21 local objects.
From https://bitbucket.org/takatoh/lathercraft
   14f1281..7aecdc3  master     -> origin/master
 * [new branch]      support-release-datetime -> origin/support-release-datetime
 * [new tag]         v2.0.1     -> v2.0.1
From https://bitbucket.org/takatoh/lathercraft
 * [new tag]         v2.0.0     -> v2.0.0
[takatoh@tk2-254-36564 lathercraft]$ sudo git merge origin/master
Auto-merging .gitignore
Auto-merging db/schema.rb
CONFLICT (content): Merge conflict in db/schema.rb
Automatic merge failed; fix conflicts and then commit the result.

あれ、db/schema.rb がコンフリクトした?なんでだ?
中身を見てみると、文字列のカラムに limit:255 がついている。このファイルはデータベースをマイグレートすれば更新されるはずなので、元に戻しておく。

[takatoh@tk2-254-36564 lathercraft]$ sudo vim db/schema.rb

修正が済んだら commit

[takatoh@tk2-254-36564 lathercraft]$ sudo git add db/schema.rb
[takatoh@tk2-254-36564 lathercraft]$ sudo git commit -m "merge branch origin/master."

さて、いよいよデータベースのマイグレーション。

[takatoh@tk2-254-36564 lathercraft]$ sudo -s
[root@tk2-254-36564 lathercraft]# export SECRET_KEY_BASE=bundle exec rake secret
[root@tk2-254-36564 lathercraft]# bundle exec rake db:migrate RAILS_ENV=production
DEPRECATION WARNING: The configuration option `config.serve_static_assets` has been renamed to `config.serve_static_files` to clarify its role (it merely enables serving everything in the `public` folder and is unrelated to the asset pipeline). The `serve_static_assets` alias will be removed in Rails 5.0. Please migrate your configuration files accordingly. (called from block in  at /var/www/lathercraft/config/environments/production.rb:23)
== 20180430070208 AddReleaseDatetimeToItems: migrating ========================
-- add_column(:items, :release_datetime, :datetime)
   -> 1.5631s
== 20180430070208 AddReleaseDatetimeToItems: migrated (1.5635s) ===============

なんか警告が出てるけどまあいいや。次に進もう。

アプリを production 環境で立ち上げてみる。

[root@tk2-254-36564 lathercraft]# export SECRET_KEY_BASE=bundle exec rake secret
[root@tk2-254-36564 lathercraft]# bundle exec rake db:migrate RAILS_ENV=production

無事、立ち上がった。けど、あるページではエラーになってしまった。開発環境で修正して本番環境に反映すると、直った。多分これで大丈夫。

最後に本番用に立ち上げて完了。

[追記]

マイグレーションのところで出た警告だけど、ググってみたら config.serve_static_assets という設定項目が config.serve_static_files に名前変更されている、ということが分かった。なので、その通りに修正して完了。