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

repeated_combination

Ruby の Array#repeated_combination は重複を許す組み合わせを返すメソッドだ(厳密にいうとEnumerator を返す)。
次のブログ記事で見つけた。

cf. 続:Haskell/Clojureでrepeated_combinationを実装してみる – Programmer’s Note

これを Go でやってみよう、ということで Haskell のコードを参考に作ってみた。

package main

import (
    "fmt"
)

func main() {
    xs := []int{ 1,2,3 }
    fmt.Println(repeatedCombination(xs, 3))
}

func repeatedCombination(xs []int, n int) [][]int {
    c := make([][]int, 0)
    if n == 1 {
        for _, x := range xs {
            c = append(c, []int{ x })
        }
    } else {
        for _, x := range xs {
            for _, y := range repeatedCombination(xs, n - 1) {
                c = append(c, append([]int{ x }, y...))
            }
        }
    }
    return c
}

ところが、実行結果が Array#repeated_combination と合わない。
上のコードの実行結果はこう。

^o^ > go run repeated_combination.go
[[1 1 1] [1 1 2] [1 1 3] [1 2 1] [1 2 2] [1 2 3] [1 3 1] [1 3 2] [1 3 3] [2 1 1] [2 1 2] [2 1 3] [2 2 1] [2 2 2] [2 2 3] [2 3 1] [2 3 2] [2 3 3] [3 1 1] [3 1 2] [3 1 3] [3 2 1] [3 2 2] [3 2 3] [3 3 1] [3 3 2] [3 3 3]]

対して Array#repeated_combination はこう。

irb(main):001:0> [1,2,3].repeated_combination(3).to_a
=> [[1, 1, 1], [1, 1, 2], [1, 1, 3], [1, 2, 2], [1, 2, 3], [1, 3, 3], [2, 2, 2], [2, 2, 3], [2, 3, 3], [3, 3, 3]]

順列ではなく組み合わせなので、Array#repeated_combination では例えば [1,1,2] と [1,2,1] と [2,1,1] を同じものとしているのに対して、Go のコードでは別のものとして数え上げてしまっている。
そこで改良を加えたのがこれだ。

package main

import (
    "fmt"
)

func main() {
    xs := []int{ 1,2,3 }
    fmt.Println(repeatedCombination(xs, 3))
}

func repeatedCombination(xs []int, n int) [][]int {
    c := make([][]int, 0)
    if n == 1 {
        for _, x := range xs {
            c = append(c, []int{ x })
        }
    } else {
        for _, x := range xs {
            for _, y := range repeatedCombination(xs, n - 1) {
                if x <= y[0] {
                    c = append(c, append([]int{ x }, y...))
                }
            }
        }
    }
    return c
}

実行結果。

^o^ > go run repeated_combination2.go
[[1 1 1] [1 1 2] [1 1 3] [1 2 2] [1 2 3] [1 3 3] [2 2 2] [2 2 3] [2 3 3] [3 3 3]]

今度はうまくいった。

コインの両替

最近見かけたブログの記事。

cf. Haskell/Clojureで数学パズル:コインの両替 – Programmer’s Note

同じような問題はずっと前に Haskell でやったことがあるんだけど、今回は Go でやってみる。はじめに思い付いたのはリンク記事の後半に出てくる再帰を使ったほうなんだけど、まずはリンク記事と同じ順で for を使ったやり方で。

package main

import (
    "fmt"
)

func main() {
    fmt.Println(change_coin(1000, 15))
}

func change_coin(money, max_coins int) int {
    patterns := 0
    for m10 := 0; m10 <= max_coins; m10++ {
        for m50 := 0; m50 <= max_coins; m50++ {
            for m100 := 0; m100 <= max_coins; m100++ {
                for m500 := 0; m500 <= max_coins; m500++ {
                    if m10 + m50 + m100 + m500 <= max_coins && 10 * m10 + 50 * m50 + 100 * m100 + 500 * m500 == money {
                        patterns += 1
                    }
                }
            }
        }
    }
    return patterns
}

ああ、ネストした for 文が美しくない!!

で、こっちが再帰を使ったほう。

package main

import (
    "fmt"
)

func main() {
    coins := []int{ 10, 50, 100, 500 }
    fmt.Println(change_coin2(1000, coins, 15))
}

func change_coin2(money int, coins []int, max_coins int) int {
    if money == 0 {
        return 1
    } else if len(coins) == 0 {
        return 0
    } else if max_coins == 0 {
        return 0
    } else {
        patterns := 0
        for use := 0; use <= max_coins; use++ {
            patterns += change_coin2(money - coins[0] * use, coins[1:], max_coins - use) } return patterns
    }
}

だいぶマシだけど else if がなあ。改めて Haskell のパターンマッチの美しいことを思い知る。

ゴルーチンとチャネルでジェネレータ

ゴルーチンとチャネルを使ってジェネレータを作ることもできる。

package main

import (
    "fmt"
)

type Item interface {
    Eq(Item) bool
    Less(Item) bool
}

type Node struct {
    item Item
    left, right *Node
}

func newNode(x Item) *Node {
    p := new(Node)
    p.item = x
    return p
}

func insertNode(node *Node, x Item) *Node {
    switch {
        case node == nil: return newNode(x)
        case x.Eq(node.item): return node
        case x.Less(node.item): node.left = insertNode(node.left, x)
        default: node.right = insertNode(node.right, x)
    }
    return node
}

func foreachNode(f func(Item), node *Node) {
    if node != nil {
        foreachNode(f, node.left)
        f(node.item)
        foreachNode(f, node.right)
    }
}

type Tree struct {
    root *Node
}

func newTree() *Tree {
    return new(Tree)
}

func (t *Tree) insertTree(x Item) {
    t.root = insertNode(t.root, x)
}

func (t *Tree) foreachTree(f func(Item)) {
    foreachNode(f, t.root)
}

func (t *Tree) makeGen() func() Item {
    ch := make(chan Item)
    go func() {
        t.foreachTree(func(x Item) {
            ch <- x }
        )
        close(ch)
    }()
    return func() Item {
        return <- ch
    }
}

type Int int

func (n Int) Eq(m Item) bool {
    return n == m.(Int)
}

func (n Int) Less(m Item) bool {
    return n < m.(Int)
}

func main() {
    a := newTree() b := []int { 5,6,4,7,3,8,2,9,1,0 }
    for _, x := range b {
        a.insertTree(Int(x))
    }
    resume := a.makeGen()
    for i := 0; i < 11; i++ {
        fmt.Println(resume())
    }
}

makeGen 関数は無名関数を返す。この無名関数はクロージャになっていて、makeGen 内のゴルーチンからチャネルを使って受け取った値を返す。ゴルーチンは二分木の値を昇順に返すので、結果として無名関数も呼び出しごとに同じ順で値を返すことになる。
実行してみよう。

^o^ > go run go_gen.go
0
1
2
3
4
5
6
7
8
9
<nil>

最後の <nil> は何だろう?

チャネルとrange

チャネルから送られてくるデータは、for ループの range で受けることもできる。

for v := チャネル { 処理 }

チャネルの場合、スライスやマップと違って返り値は多値ではない。あと、チャネルにデータを送る側では、送信が終わったらチャネルを close する必要がある。
次の例は、二分木にチャネルを使ってデータを送信する each 関数を追加したものだ。

package main

import (
    "fmt"
)

type Item interface {
    Eq(Item) bool
    Less(Item) bool
}

type Node struct {
    item Item
    left, right *Node
}

func newNode(x Item) *Node {
    p := new(Node)
    p.item = x
    return p
}

func insertNode(node *Node, x Item) *Node {
    switch {
        case node == nil: return newNode(x)
        case x.Eq(node.item): return node
        case x.Less(node.item):
            node.left = insertNode(node.left, x)
        default:
            node.right = insertNode(node.right, x)
    }
    return node
}

func foreachNode(f func(Item), node *Node) {
    if node != nil {
        foreachNode(f, node.left)
        f(node.item)
        foreachNode(f, node.right)
    }
}

type Tree struct {
    root *Node
}

func newTree() *Tree {
    return new(Tree)
}

func (t *Tree) insertTree(x Item) {
    t.root = insertNode(t.root, x)
}

func (t *Tree) foreachTree(f func(Item)) {
    foreachNode(f, t.root)
}

func (t *Tree) each() chan Item {
    ch := make(chan Item)
    go func() {
        t.foreachTree(func(x Item) { ch <- x })
        close(ch)
    }()
    return ch
}

type Int int

func (n Int) Eq(m Item) bool {
    return n == m.(Int)
}

func (n Int) Less(m Item) bool {
    return n < m.(Int)
}

func main() {
    a := newTree() b := []int { 5,6,4,3,7,8,2,1,9,0 }
    for _, x := range b {
        a.insertTree(Int(x))
    }
    for x := range a.each() {
        fmt.Println(x)
    }
}

for ループの range には二分木の each 関数を呼び出していて、その each 関数では、データを一つずつチャネルに送信し、最後にチャネルを close している。
実行結果はこう:

^o^ > go run go_range.go
0
1
2
3
4
5
6
7
8
9

ゴルーチンの同期

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

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文字出力するだけなので、うまく同期して動作している様子がわかる。