読者です 読者をやめる 読者になる 読者になる

write ahead log

ロールフォワード用

golangで画像を扱う(imageパッケージを使う)

golang

書店でInterface 5月号を見つけて, 画像処理が懐かしくなったので.
(つい買ってしまった.他にも車載用OSとか面白いしね.)

golangにもimageパッケージという2D画像用ライブラリが標準で備わってるようなのでちょっと使ってみる.

1. 概要

golangの他の標準パッケージと同様, このパッケージも最低限の機能しか用意されていないみたいです.

The Go Programming Language - Package image

サブディレクトリ含めてもそんなにない

ざっと見た感じだと, 以下くらいの機能なのかな?

  • アルファ値
  • 色空間や濃淡(グレースケール)
    • RGB
    • CMY
    • Gray
  • 矩形
  • カラーパレット(pallete)
  • 上記の組み合わせ(draw)
  • 画像フォーマットのデコード処理(gif/png/jpeg)

Uniformって何.まっさらなとこからやるときに使うのか?

まぁいいや.

あと, 現時点だと扱える画像フォーマットは

だそうです.

2. 何か書いてみる

とにかく使ってみましょう. 簡単なグラフィックから.

2.1. 箱を描く

簡単そうなのから.

NewRGBAでRGB形式の画像を作成できます.

package main

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

func main() {
    x := 0
    y := 0
    width := 100
    height := 50

    // RectからRGBAを作る(ゼロ値なので黒なはず)
    img := image.NewRGBA(image.Rect(x, y, width, height))

    // 出力用ファイル作成(エラー処理は略)
    file, _ := os.Create("sample.jpg")
    defer file.Close()

    // JPEGで出力(100%品質)
    if err := jpeg.Encode(file, img, &jpeg.Options{100}); err != nil {
        panic(err)
    }
}

[実行結果]

f:id:twinbird_htn:20170423001840j:plain

2.2. 箱を描く(色指定)

一歩前へ.

Setでピクセル単位に色をセットしていくだけです.

画像サイズはimg.Rectの中にあるX, Yで取得できます.

package main

import (
    "os"
    "image"
    "image/jpeg"
    "image/color"
)

// 画像を単色に染める
func fillRect(img *image.RGBA, col color.Color) {
    // 矩形を取得
    rect := img.Rect

    // 全部埋める
    for h := rect.Min.Y; h < rect.Max.Y; h++ {
        for v := rect.Min.X; v < rect.Max.X; v++ {
            img.Set(v, h, col)
        }
    }
}

func main() {
    x := 0
    y := 0
    width := 100
    height := 50

    // RectからRGBAを作る(ゼロ値なので黒なはず)
    img := image.NewRGBA(image.Rect(x, y, width, height))
    // 赤色に染める(透過なし)
    fillRect(img, color.RGBA{255, 0, 0, 0})

    // 出力用ファイル作成(エラー処理は略)
    file, _ := os.Create("sample.jpg")
    defer file.Close()

    // JPEGで出力(100%品質)
    if err := jpeg.Encode(file, img, &jpeg.Options{100}); err != nil {
        panic(err)
    }
}

[実行結果]

f:id:twinbird_htn:20170423001904j:plain

2.3. 箱を描く(枠だけ)

せっかくなので一捻り入れてみました.

package main

import (
    "os"
    "image"
    "image/jpeg"
    "image/color"
)

// 画像を単色に染める
func fillRect(img *image.RGBA, col color.Color) {
    // 矩形を取得
    rect := img.Rect

    // 全部埋める
    for h := rect.Min.Y; h < rect.Max.Y; h++ {
        for v := rect.Min.X; v < rect.Max.X; v++ {
            img.Set(v, h, col)
        }
    }
}

// 枠線を描く
func drawBounds(img *image.RGBA, col color.Color) {
    // 矩形を取得
    rect := img.Rect

    // 上下の枠
    for h := 0; h < rect.Max.X; h++ {
        // 上の枠
        img.Set(h, 0, col)
        // 下の枠
        img.Set(h, rect.Max.Y-1, col)
    }

    // 左右の枠
    for v := 0; v < rect.Max.Y; v++ {
        // 左の枠
        img.Set(0, v, col)
        // 右の枠
        img.Set(rect.Max.X-1, v, col)
    }
}

func main() {
    x := 0
    y := 0
    width := 100
    height := 50

    // RectからRGBAを作る(ゼロ値なので黒なはず)
    img := image.NewRGBA(image.Rect(x, y, width, height))
    // 白色に染める(透過なし)
    fillRect(img, color.RGBA{255, 255, 255, 0})
    // 赤枠を付ける
    drawBounds(img, color.RGBA{255, 0, 0, 0})

    // 出力用ファイル作成(エラー処理は略)
    file, _ := os.Create("sample.jpg")
    defer file.Close()

    // JPEGで出力(100%品質)
    if err := jpeg.Encode(file, img, &jpeg.Options{100}); err != nil {
        panic(err)
    }
}

[実行結果]

f:id:twinbird_htn:20170423001922j:plain

2.4. 円を描く
package main

import (
    "image"
    "image/color"
    "image/jpeg"
    "math"
    "os"
)

// 画像を単色に染める
func fillRect(img *image.RGBA, col color.Color) {
    // 矩形を取得
    rect := img.Rect

    // 全部埋める
    for h := rect.Min.Y; h < rect.Max.Y; h++ {
        for v := rect.Min.X; v < rect.Max.X; v++ {
            img.Set(v, h, col)
        }
    }
}

type Circle struct {
    p image.Point
    r int
}

// 円を描く
func (c *Circle) drawBounds(img *image.RGBA, col color.Color) {
    for rad := 0.0; rad < 2.0*float64(c.r); rad += 0.1 {
        x := int(float64(c.p.X) + float64(c.r)*math.Cos(rad))
        y := int(float64(c.p.Y) + float64(c.r)*math.Sin(rad))
        img.Set(x, y, col)
    }
}

func main() {
    x := 0
    y := 0
    width := 500
    height := 500

    // RectからRGBAを作る(ゼロ値なので黒なはず)
    img := image.NewRGBA(image.Rect(x, y, width, height))
    // 白色に染める(透過なし)
    fillRect(img, color.RGBA{255, 255, 255, 0})

    // 円を描く
    // 中心点
    center := image.Point{250, 250}
    // 円
    circle := Circle{center, 50}
    // 描く
    circle.drawBounds(img, color.RGBA{255, 0, 0, 0})

    // 出力用ファイル作成(エラー処理は略)
    file, _ := os.Create("sample.jpg")
    defer file.Close()

    // JPEGで出力(100%品質)
    if err := jpeg.Encode(file, img, &jpeg.Options{100}); err != nil {
        panic(err)
    }
}

[実行結果]

f:id:twinbird_htn:20170423001935j:plain

3. ファイルから画像を読み込み加工する

グラフィックも面白いですが, 画像加工がしてみたいので色々やってみましょう.

元画像はやはりレナさんで.

f:id:twinbird_htn:20170423002030p:plain

3.1. 画像を読み込み, サイズを調べる
package main

import (
    "image"
    _ "image/png"
    "os"
    "fmt"
)

func main() {
    // 画像ファイルを開く
    file, _ := os.Open("./rena.png")
    defer file.Close()

    // 設定をデコード
    config, formatName, err := image.DecodeConfig(file)
    if err != nil {
        panic(err)
    }
    // フォーマット名表示
    fmt.Println(formatName)
    // サイズ表示
    fmt.Println(config.Width)
    fmt.Println(config.Height)
}

[実行結果]

$ ./image.exe
png
512
512
3.2. 画像を合成する

https://golang.org/doc/gopher/に自由に使えるgopher君の画像があるので合成してみます.

package main

import (
    "image"
    "image/png"
    "os"
    "image/draw"
)

func main() {
    // 画像ファイルを開く(書き込み元)
    src, _ := os.Open("./run.png")
    defer src.Close()
    // 画像ファイルを開く(書き込み先)
    dst, _ := os.Open("./rena.png")
    defer dst.Close()

    // デコードしてイメージオブジェクトを準備
    srcImg, _, err := image.Decode(src)
    if err != nil {
        panic(err)
    }
    dstImg, _, err := image.Decode(dst)
    if err != nil {
        panic(err)
    }

    // 書き出し用のイメージを準備
    outRect := image.Rectangle{image.Pt(0, 0), dstImg.Bounds().Size()}
    out := image.NewRGBA(outRect)

    // 描画する
    // 元画像をまず描く
    dstRect := image.Rectangle{image.Pt(0, 0), dstImg.Bounds().Size()}
    draw.Draw(out, dstRect, dstImg, image.Pt(0, 0), draw.Src)
    // 上書きする
    srcRect := image.Rectangle{image.Pt(0, 0), srcImg.Bounds().Size()}
    draw.Draw(out, srcRect, srcImg, image.Pt(0, 0), draw.Over)

    // 書き出し用ファイル準備
    outfile, _ := os.Create("out.png")
    defer outfile.Close()
    // 書き出し
    png.Encode(outfile, out)
}

[実行結果]

f:id:twinbird_htn:20170423002122p:plain

なんかえらくシュールな画像になってしまった…

4. もっと画像処理する

せっかくなのでもう少し色々やります.

4.1. グレースケール化する

標準パッケージに用意されています.

package main

import (
    "image"
    "image/png"
    "image/color"
    "os"
)

func main() {
    // 画像ファイルを開く(書き込み元)
    src, _ := os.Open("./rena.png")
    defer src.Close()

    // デコードしてイメージオブジェクトを準備
    srcImg, _, err := image.Decode(src)
    if err != nil {
        panic(err)
    }
    srcBounds := srcImg.Bounds()

    // 出力用イメージ
    dest := image.NewGray(srcBounds)

    // グレー化
    for v := srcBounds.Min.Y; v < srcBounds.Max.Y; v++ {
        for h := srcBounds.Min.X; h < srcBounds.Max.X; h++ {
            c := color.GrayModel.Convert(srcImg.At(h, v))
            gray, _ := c.(color.Gray)
            dest.Set(h, v, gray)
        }
    }

    // 書き出し用ファイル準備
    outfile, _ := os.Create("out.png")
    defer outfile.Close()
    // 書き出し
    png.Encode(outfile, dest)
}

[実行結果]

f:id:twinbird_htn:20170423002146p:plain

4.2. 2値化する

ちょっと強引かな.

ColorModelとConverterを自作するのが正統派なんですかね.

いずれにしても関数化ぐらいしてもよかったかも.

package main

import (
    "image"
    "image/png"
    "image/color"
    "os"
)

// 二値化のしきい値
const threshold = 128

func main() {
    // 画像ファイルを開く(書き込み元)
    src, _ := os.Open("./rena.png")
    defer src.Close()

    // デコードしてイメージオブジェクトを準備
    srcImg, _, err := image.Decode(src)
    if err != nil {
        panic(err)
    }
    srcBounds := srcImg.Bounds()

    // 出力用イメージ
    dest := image.NewGray(srcBounds)

    // 二値化
    for v := srcBounds.Min.Y; v < srcBounds.Max.Y; v++ {
        for h := srcBounds.Min.X; h < srcBounds.Max.X; h++ {
            c := color.GrayModel.Convert(srcImg.At(h, v))
            gray, _ := c.(color.Gray)
            // しきい値で二値化
            if gray.Y > threshold {
                gray.Y = 255
            } else {
                gray.Y = 0
            }
            dest.Set(h, v, gray)
        }
    }

    // 書き出し用ファイル準備
    outfile, _ := os.Create("out.png")
    defer outfile.Close()
    // 書き出し
    png.Encode(outfile, dest)
}

[実行結果]

f:id:twinbird_htn:20170423002210p:plain

まとめ

もうちょっと色々遊ぼうかと思ってましたが, この辺にしときます.

ベースは標準ライブラリで提供されているので, 画像処理アルゴリズムを試したりするのにはちょうどいい気がします.

本格的にやる際には自作でライブラリを用意するほうがいいかもしれないです.
(そしてそれが面白かったりしますよね)

参考

package - Image

package - Image/draw

package - Image/png

package - Image/jpeg

package - Image/color

The Go Blog - The Go image/draw package

とある子育てパパの日記 - 2つの画像を重ねる/golang

msys2(msys2-launcher利用下)で環境変数を引き継ぎたい

開発ツール

msys2を使っているとwindows標準の環境変数が引き継がれなくてしょんぼりする.

探していると対処法があるそうでめもらんだむ - MSYS2 で PATH が引き継がれない

この記事はホントにありがたかったんですが, msys2-launchar使ってるせいか, うまくいきませんでした.

あれこれ見てみましたが, 最終的にはmsys2.iniの以下を変更するとうまくいきました.

#MSYS=winsymlinks:nativestrict
#MSYS=error_start:mingw64/bin/qtcreator.exe|-debug|<process-id>
#CHERE_INVOKING=1
#MSYS2_PATH_TYPE=inherit  # <= ここの#を外す
MSYSTEM=MSYS 

これで今度から少し楽です.

Docker Toolboxを使ってWindows10 HomeでDocker環境を構築する

インフラ

DockerをWindowsで使おうとするとHyper-VのあるProfessional以上の環境を要求されます.

まぁ, 買えばいいんですが, そうはいってもない状況で使いたい場合もあるんですね.

そういう方はVirtualbox上で構築できるDocker Toolboxというものがあるそうです.

そりゃいいねと. そういうわけで使ってみました.

0. 環境

64bit環境だとIntel VTのCPUがいるらしいです.

私が試した環境は

です

1. ダウンロード

まずはダウンロードします.

URLは以下です.

Docker Toolbox

2. インストールする

基本はひたすら「Next」です.

VirtualBoxやgit for windowsが入っている場合はチェックは不要です.

Install VirtualBox with NDIS5 driver[default NDIS6]にチェックを入れないとネットワークがうまくいかない例があるっぽいので入れておきます

こちらの記事で手順の中でNDIS5を指定しているので, そういうことかなと.

3. 起動する

デスクトップにショートカット(Docker Quickstart Terminal)が出来ているので実行します.

起動すると勝手にVMを作成したりDocker-imageをダウンロードしたりしてくれます.

応援しながら待ちます.

4.runしてみる

以下が動けば大丈夫でしょう.

docker run hello-world

Virtual boxの画面を開くとdefaultマシンが一つできています.

あー

Qiitaに同種の記事がありました.

たぶんこっち見た方がいいですね…詳しいし

golangでcontextパッケージを使う

golang

使わないから全然覚えられない.

とりあえずサンプルを書いて覚えておく.

詳細や思想はdeeeetさんの記事が非常にわかりやすいのでこれ読めばそれでいいと思う.

大雑把な理解

  1. context.Backgroundでcontextを作るか, よそからもらってきたcontextを使って
  2. With****メソッドを使ってcontextに意味を持たせる
  3. 結果や値はcontextオブジェクトの中に入ってる
メソッド

With****メソッドは以下がある.

メソッド名 用途
WithCancel キャンセル可能にする
WithTimeout 一定時間でキャンセルされるようにする
WithDeadline 指定時間でキャンセルされるようにする
WithValue 値を一緒に引き渡す

どれも新しいcontextオブジェクトを返してくれる.

メンバ

contextのインターフェースは以下のメンバでとてもシンプル. コメントで書いてる意味がホントに正しいかはちょっと怪しいけど.

type Context interface {
    // デッドライン時刻とデッドラインが設定されているかどうか(falseなら未設定)
    Deadline() (deadline time.Time, ok bool)
    // 完了/キャンセルを知らせるチャネル
    Done() <-chan struct{}
    // エラー
    Err() error
    // WithValueで持ち運ぶ値
    Value(key interface{}) interface{}
}

キャンセル可能にする

キャンセル可能にするにはWithCancelを使います.

以下は無限ループで助けを呼び続けるゴルーチンを2秒後にキャンセルする例です.

WithCancelで返される2つ目の関数を呼び出せばキャンセルされるんですね.

package main

import (
    "context"
    "fmt"
    "time"
)

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    go infiniteLoop(ctx)

    // 2秒待ってキャンセルする
    time.Sleep(2 * time.Second)
    cancel()

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
.
.
.
Help!
Help!
context canceled
Help!
Help!
Help!
Help!

プロセスが死ぬまでゴルーチンが生きてるので, ずっと助けてと叫んでますが….

指定時間経過したらキャンセルする

上記の例だとWithTimeoutを使ってもっとシンプルに書くことができます.

package main

import (
    "context"
    "fmt"
    "time"
)

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒待ってキャンセルする
    ctx, cancel := context.WithTimeout(ctx, 2 * time.Second)
    defer cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
Help!
.
.
.
Help!
Help!
Help!
context deadline exceeded
Help!
Help!
Help!
Help!
Help!
Help!
Help!

指定時間になったらキャンセルする

相対的な時刻指定だけではなく, 絶対的な時刻指定も行う事ができます.

package main

import (
    "context"
    "fmt"
    "time"
)

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒後をデッドラインにする
    ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
    defer cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

Help!
Help!
Help!
Help!
.
.
.
Help!
Help!
Help!
Help!
context deadline exceeded
Help!

時間設定したけどやっぱり手動でキャンセルする

WithTimeoutやWithDeadlineでキャンセル時刻を指定していても, 戻り値のcancel関数を呼び出せば好きなタイミングでキャンセルを行えます.

package main

import (
    "context"
    "fmt"
    "time"
)

func infiniteLoop(ctx context.Context) {
    // 終わらないやつ
    for {
        fmt.Println("Help!")
    }
}

func main() {
    ctx := context.Background()

    // 2秒後をデッドラインにする
    ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
    defer cancel()

    // やっぱすぐやめた
    cancel()

    go infiniteLoop(ctx)

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

$ ./context.exe
Help!
context canceled
Help!
Help!
Help!
Help!

キャンセルを伝搬させる

流石にプロセス死ぬまで叫び続けるのはかわいそうなので, キャンセル時には脱出させてあげます.

contextを作成する際にcontextを渡してあげると, Done()を経由してcancelが伝搬していきます.

package main

import (
    "context"
    "fmt"
)

func infiniteLoop(ctx context.Context) {
    innerCtx, cancel := context.WithCancel(ctx)
    defer cancel()
    // 終わらないやつ だったのを終わるようにした
    for {
        fmt.Println("Help!")

        select {
        case <- innerCtx.Done():
            fmt.Println("Exit from hell.")
            return
        }
    }
}

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)

    // 無限ループに入る
    go infiniteLoop(ctx)

    // やっぱすぐやめた
    cancel()

    select {
    case <- ctx.Done():
        fmt.Println(ctx.Err())
    }
}

[実行結果]

$ ./context.exe
Help!
Exit from hell.
context canceled

地獄から出てこれました.よかったですね.

値を引き渡す

contextを使って値を渡していく時にはcontext.WithValueを使います.

値を取得するにはcontext.Valueにキーを渡して取得します.
(interface型が返るので, 型アサーションがいりますね)

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "hoge", 1)

    fmt.Println(ctx.Value("hoge").(int))
}

[実行結果]

$ ./context.exe
1
追記

deeeetさんのeが一つ多かったので訂正しました.(ごめんなさい)

@niconegotoさんありがとうございました

golangでテンプレートエンジンを使う

golang

golangではテンプレートエンジンが標準で用意されています.

なんだけど, ちょっと癖があって僕はすぐにはなじめなかった.

まぁ, テンプレートエンジンなんて慣れの問題な気がするけど.

調べながら書いたらめちゃ長くなった.

ざっくり概要

golangのテンプレートエンジンはtext/templateパッケージに入っている.

Web開発では同じインターフェースを持ったhtml/templateパッケージを利用することになる.(デフォルトでエスケープ処理が行われる)

このパッケージのメソッドに, UTF-8のテキストとデータを渡せばいい感じにテキストを生成してくれる.

以下は基本的にtext/templateを利用した説明になるけど, html/templateでもそのまま動くはず.(エスケープされた結果で)

一番簡単な例

一番簡単なのはパラメータもない単純なテキストからテンプレートを作る例でしょう.

package main

import (
    "text/template"
    "os"
)

func main() {
    // テンプレート名:"template name"で"Hello, text/template"という内容のテンプレートオブジェクトを生成
    tmpl, err := template.New("template name").Parse("Hello, text/template")
    if err != nil {
        panic(err)
    }
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err = tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果]

$ ./template.exe
Hello, text/template

ファイルからテンプレートのテキストを読みだす

いちいちParseに文字列渡すなんて面倒でしょうがないですから, 当然ファイルから読みだしてParseするメソッドがあります.

package main

import (
    "text/template"
    "os"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    tmpl, err := template.ParseFiles("test.tmpl")
    if err != nil {
        panic(err)
    }
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err = tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(test.tmpl)]

Hello, text/template.

[実行結果]

$ ./template.exe
Hello, text/template

テンプレートオブジェクトの名前を調べる

ところで上の例ではテンプレートオブジェクトの名前を指定していません.

名前は何になっているんですかね.

Nameというメソッドがあるので, これを使って調べてみます.

package main

import (
    "text/template"
    "fmt"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    tmpl, err := template.ParseFiles("test.tmpl")
    if err != nil {
        panic(err)
    }
    // 名前を出力
    fmt.Println(tmpl.Name())
}

[実行結果]

$ ./template.exe
test.tmpl

なるほど, ファイル名になっているようです.

Parse時にエラー処理を行う

上記までの例ではParseした後, いちいちエラー処理を書いていますが, 面倒です.

そもそも大半のアプリでは初期化時で1度Parseすれば十分でしょう.
(ファイルが無くてpanicとか起動時に知りたいですし)

そのためにMustというメソッドが用意されています.

これはエラーが起きたらpanic起こす, という単純なメソッドです.

package main

import (
    "text/template"
    "os"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    // ファイルが無かったりして失敗したらpanic起こす
    tmpl := template.Must(template.ParseFiles("test.tmpl"))
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果(ファイルあり)]

$ ./template.exe
Hello, text/template.

[実行結果(ファイルなし)]

$ ./template.exe
panic: open test.tmpl: The system cannot find the file specified.

goroutine 1 [running]:
panic(0x510b60, 0xc042056210)
        C:/Go/src/runtime/panic.go:500 +0x1af
text/template.Must(0x0, 0x5b8400, 0xc042056210, 0x0)
        C:/Go/src/text/template/helper.go:23 +0x6d
main.main()
        C:/msys64/home/twinbird/dropbox/lab/template/sample.go:10 +0x76

これで少し楽できます.

複数のテンプレートを扱う

templateパッケージでは複数のテンプレートを1つのテンプレートオブジェクトで取り扱うことができます.

下記の例ではParseFilesで3つのテンプレートファイルを展開しておき, ExecuteTemplateでテンプレートを選択して出力しています.

package main

import (
    "text/template"
    "os"
)

func main() {
    // tmplディレクトリの下の3つのファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("tmpl/test.tmpl", "tmpl/test2.tmpl", "tmpl/test3.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力

    // Template1
    err := tmpl.ExecuteTemplate(os.Stdout, "test.tmpl", nil)
    if err != nil {
        panic(err)
    }
    // Template2
    err = tmpl.ExecuteTemplate(os.Stdout, "test2.tmpl", nil)
    if err != nil {
        panic(err)
    }
    // Template3
    err = tmpl.ExecuteTemplate(os.Stdout, "test3.tmpl", nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(test.tmpl)]

Template1

[テンプレート(test2.tmpl)]

Template2

[テンプレート(test3.tmpl)]

Template3

[実行結果]

$ ./template.exe
Template1
Template2
Template3

複数のテンプレートを扱う(もう少し)

ここまで来ると使う機会なさそうな機能ですけど.

package main

import (
    "text/template"
    "os"
    "fmt"
)

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseGlob("tmpl/*.tmpl"))

    // Debug用にテンプレート一覧を出力
    fmt.Println(tmpl.DefinedTemplates())

    // templateのスライスを取得してテンプレート数を出力してみる
    fmt.Println(len(tmpl.Templates()))

    // test.tmplのオブジェクトを取得
    tmpl1 := tmpl.Lookup("test.tmpl")

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl1.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果]

$ ./template.exe
; defined templates are: "test.tmpl", "test2.tmpl", "test3.tmpl"
3
Template1

引数(パラメータ)を渡す

さて, 実際テンプレートエンジンを使う際, パラメータを埋め込めなければ意味なんてあまりありません.

templateでは {{と}}で囲んだ中身が「アクション」と呼ばれます.

アクションの中では「式」や「パイプライン」を使ってデータや式を評価することができます.
(そしてこれが個人的には曲者です)

一番簡単な例

以下の例ではGreeting構造体をテンプレートに渡して, テンプレート内では.(ドット)で参照して表示に使っています.

package main

import (
    "os"
    "text/template"
)

// テンプレートへ引き渡すオブジェクト
// メンバはエクスポート(大文字)しておかないと参照できない
type Greeting struct {
    Messege string
    Target  string
}

func main() {
    // テンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // テンプレートへ渡すデータを作る
    g := &Greeting{
        "hello",
        "world",
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, g) // <- 第二引数にデータを渡す
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Messege}}, {{.Target}}.

[実行結果]

$ ./template.exe
Greeting

hello, world.
複数データ(スライス)を扱う

複数のデータを扱えないとさすがに困る.

rangeというアクションがあるのでこれを使うと繰り返しが書けます.

ちょい戸惑ったのがrangeの中では.(ドット)の意味が変わるということ.

range~endのループの間では.(ドット)に各要素の値がセットされます.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{end}}

[実行結果]

$ ./template.exe
Greeting


hello, world.

こんにちは, 世界.

Hallo, Welt.

你好, 世界.
テンプレートを内で分岐する

あまりやりたいことではないですが, テンプレート内でif文を使って分岐したい事がありますね.

templateパッケージで分岐する際にはifアクションを利用します.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
        false,
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
        true,
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
        false,
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
        true,
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{if .Print}}
{{.Messege}}, {{.Target}}.
{{end}}
{{end}}

[実行結果]

$ ./template.exe
Greeting





こんにちは, 世界.





你好, 世界.

else文, else if文も以下の感じで普通に使えます.

Greeting

{{range .}}
{{if .Print}}
{{.Messege}}, {{.Target}}.
{{else}}
NO PRINT.
{{end}}
{{end}}
rangeを使ってデータの有無で分岐する

rangeを使うとデータセットの有無で分岐を行うことができます. ちなみにrangeに渡せるのは以下の通りです.

  • 配列
  • スライス
  • マップ
  • チャネル
package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // 空のスライス準備
    messeges := make([]*Greeting, 0)

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{else}}
NO MESSEGE.
{{end}}

[実行結果]

$ ./template.exe
Greeting


NO MESSEGE.
テンプレート内から別のテンプレートを呼び出す

テンプレートにテンプレートを埋め込む事ができます.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl", "inner_hello.tmpl"))

    // 空のスライス準備
    messeges := make([]*Greeting, 0)

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[親のテンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{else}}
{{template "inner_hello.tmpl"}}
{{end}}

[子のテンプレート(inner_hello.tmpl)]

Hello, world from inner template.

[実行結果]

$ ./template.exe
Greeting


Hello, world from inner template.
埋め込んだテンプレートに引数を渡す

埋め込んだテンプレートに対して引数を渡せないと, 利点は薄いですね.

少し冗長というか, 無駄っぽい例ですが.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl", "inner_hello.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
        false,
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
        true,
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
        false,
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
        true,
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[親テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{template "inner_hello.tmpl" .}}
{{end}}

[子テンプレート(inner_hello.tmpl)]

{{.Messege}} {{.Target}} from inner template.
$ ./template.exe
Greeting


hello world from inner template.


こんにちは 世界 from inner template.


Hallo Welt from inner template.


你好 世界 from inner template.
変数を扱う

滅多にない事な気がしますが, テンプレート内で変数を扱う事ができます.

変数は$(ドルマーク)を先頭につけて宣言, 利用します.

package main

import (
    "os"
    "text/template"
)

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{$variable := "変数だよ"}}

{{$variable}}

[実行結果]

$ ./template.exe
Greeting



変数だよ

ちなみにrangeアクションは普通のgoプログラムと同じで, indexと要素それぞれを返すことができます.

[例]

{{ /* .にはスライスとかが入ってる感じ */ }}
{{range $index, $element := . }}
コメント

コメントは普通に

/* something */

の形式です.

メソッド(引数なし)

メソッドはテンプレート呼び出し元のgoプログラムで書かれたものをそのまま利用できます.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    Value string
}

func (obj *Object)Method() string {
    return obj.Value
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{"Hello, world from method"}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Method}}

[実行結果]

$ ./template.exe
Greeting

Hello, world from method
メソッド(引数あり)

テンプレートから引数付きでメソッドを呼び出すときにはスペース区切りでパラメータを渡します.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    Value string
}

func (obj *Object)Method(str1 string, str2 string) string {
    return obj.Value + " " + str1 + " " + str2
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{"Hello, world from method"}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Method "param1" "param2"}}

[実行結果]

$ ./template.exe
Greeting

Hello, world from method param1 param2
関数を使う

テンプレート内での関数はgoプログラムの関数の名前空間とは異なるものになっています.

デフォルトではいくつかの関数がグローバル空間に定義されていて, それ以上の独自のものは自分で登録する必要があります.
(call関数やメソッドで何とかする手もあります.が, それが面倒な状況もあるでしょう)

日本語ドキュメントが整備されていて, 標準の関数群の日本語訳がありましたありがたい.

ややこしいcallだけ例を書いておきます.

特に以下は注意が必要でしょう.

最初の引数は関数型の値(printのような組み込み関数ではないもの)を生成する評価結果でなくてはなりません。 関数は1つまたは2つの値を返す必要があり、2番目はerror型になります。 引数が関数型でない、あるいは戻り値のerror値がnilでなかった場合、実行は停止します。

要するに関数は

func() string

func() (string, error)

である必要があるということでしょう.
(もちろんstringは任意)

2つ目の例ではエラーがnilでなければ停止ということですね.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    SomethingToDo func(str1 string, str2 string) (string, error)
}

func SampleFunction(str1 string, str2 string) (string, error) {
    return "This is sample." + " " + str1 + " " + str2, nil
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{SampleFunction}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{call .SomethingToDo "param1" "param2"}}

[実行結果]

$ ./template.exe
Greeting

This is sample. param1 param2
独自関数を作る

テンプレート用に独自関数を用意するにはFuncsメソッドを使います.

Parse前に設定してやらないとダメみたいです.(そりゃそうか)

ハマったのは新規でテンプレートを作成して, そこに関数を定義しましたが, ParseFilesを使うと ファイル名がテンプレート名になるので, 関数が未定義というpanicが発生しました.

うまい方法があるのかもしれませんが, この辺り良いやり方がわかってないです.

package main

import (
    "os"
    "text/template"
    "fmt"
)

func numberToDollar(v float64) string {
    return fmt.Sprintf("$%f", v)
}

func numberToYen(v float64) string {
    return fmt.Sprintf("¥%f", v)
}

func main() {
    // テンプレート用の関数マップを作成
    // 関数名: 関数オブジェクトの形式
    funcMap := template.FuncMap{
        "ToDollar": numberToDollar,
        "ToYen": numberToYen,
    }
    // テンプレート作成(ParseFiles使うときは名前をParseFilesに使うものと同じにしないとpanicになる)
    tmpl := template.New("hello_world.tmpl")
    // 関数定義
    tmpl = tmpl.Funcs(funcMap)
    // ファイルの内容からテンプレートオブジェクトを生成
    tmpl = template.Must(tmpl.ParseFiles("hello_world.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

Dollar: {{ToDollar 123.456}}
Yen: {{ToYen 123.456}}

[実行結果]

$ ./template.exe
Greeting

Dollar: $123.456000
Yen: ¥123.456000
テンプレート内でのテンプレート定義(defineアクション)

テンプレート内でテンプレートを定義することができます. (defineアクションを利用)

用途があまり思いつかない….

Web開発とかでは部分ごとにテンプレートを分けて, templateアクションでまとめるのが王道っぽい気がします.

package main

import (
    "os"
    "text/template"
)

func main() {
    // ファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("page.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(page.tmpl)]

{{define "header"}}
Header

This is a header text.
{{end}}

{{define "body"}}
Body

This is a body text.
{{end}}

{{define "footer"}}
Footer

This is a footer text.
{{end}}

{{define "page"}}
{{template "header"}}
{{template "body"}}
{{template "footer"}}
{{end}}

{{template "page"}}

[実行結果]










Header

This is a header text.


Body

This is a body text.


Footer

This is a footer text.
パイプライン

上述までのアクションで使う変数は実はパイプラインと呼ばれるそうです.
(また新しい単語が…)

パイプラインはUnixのシェルと同じ様な考え方をします.

シェルのパイプと同様に|で変数や関数を繋いでいくわけです.

パイプで繋がれた先の関数では第一引数に繋ぎ元の結果が渡されます.

第二引数でerrがnilでなければpanicです.

[例1]

単一項目のパイプライン

{{10.00}}

[実行結果1]

10

[例2]

チェインしたパイプライン

{{10.00 | printf %.2f}}

[実行結果2]

10.00

Web開発のためにHTML(とJS)の内容をエスケープする

エスケープ用のメソッドが用意されています.

HTMLEscape - golang-jp.org

テンプレート用の関数にもデフォルトでhtmlなどのメソッドがあります.

とはいえ, 特別な事情がない限り, デフォルトでエスケープされるhtml/templateを利用するべきでしょう.

golangでRDBMSを扱う(MySQLの例)

golang

まぁ, ドキュメント読めという話なんですが.

一々例を載せたら長くなってしまいました.

とはいえ, 僕の様なコピペプログラマにはこれくらいしておいた方が…

準備

まずは有難くパッケージをgo getします.

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

操作方法

では見ていきます.
テーブルは以下のようにしましょう.

DEPT(部署)

キー 列名 意味
主キー DNO 部署コード 文字列(2ケタ)
DNAME 部署名 文字列(20ケタ)
BUDGET 予算 数値(10ケタ, 内小数2ケタ)
LAST_UPDATE 最終更新日時 日付

EMP(従業員)

キー 列名 意味
主キー ENO 従業員コード 文字列(2ケタ)
ENAME 従業員名 文字列(20ケタ)
外部キー DNO 部署コード 文字列(2ケタ)
SALARY 給与 数値(10ケタ, 内小数2ケタ)
LAST_UPDATE 最終更新日時 日付

C.J Dateのデータベース実践講義を元に日付を足しています.

事前にDBは作っておきましょう.

C:\Users\twinbird>mysql -u root -ppassword
mysql> create database demo default character set utf8;
Query OK, 1 row affected (0.01 sec)

接続してみる

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()
}

エラーが出ず終了すれば問題ないでしょう.

テーブルを作る(DDLを発行する)

結果を利用しないクエリの発行にはExecメソッドを使います.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // DDL発行
    // Resultが戻されるけど, DDLの場合は使い道がなさそう
    _, err = db.Exec(`
      CREATE TABLE dept (
          dno VARCHAR(20) PRIMARY KEY,
          dname VARCHAR(20),
          budget NUMERIC(10,2),
          lastupdate DATETIME
      )
  `)
    if err != nil {
        panic(err.Error())
    }
}

行を入れる(Insert)

LastInsertIdやRowsAffectedでは実行後の結果を取得できます.

LastInsertIdはAutoIncrementでIDなどを設定した際に値が入ってきます.

RowsAffectedは発行したクエリが影響を与えた行数を返してくれます.

今回はAutoIncrementのテーブルにしなかったので, 例を誤った感があります.

とはいえ行は入ります.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Insert文発行
    // Resultが戻される
    result, err := db.Exec(`
      INSERT INTO dept(dno, dname, budget) VALUES('D1', 'Marketing', 10000)
  `)
    if err != nil {
        panic(err.Error())
    }

    // AutoIncrementの型で使える
    // 最後に挿入したキーを返す(が, 今回は主キーをAutoIncrementにしていないので使えない.例を誤った感)
    id, err := result.LastInsertId()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(id)

    // 影響を与えた行数を返す
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
0
1

探す(Select)

1行探すのにはQueryRowメソッドを使います.

DateTimeを取り扱おうとするとgolangではstringでは扱う必要があるようです.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

type Dept struct {
    DNo string
    DName string
    Budget float64
    LastUpdate string
}

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    var dept Dept

    // Select文発行
    err = db.QueryRow(`
      SELECT
           dno
          ,dname
          ,budget
          ,lastupdate
      FROM
          dept
  `).Scan(&(dept.DNo), &(dept.DName), &(dept.Budget), &(dept.LastUpdate))

    if err != nil {
        panic(err.Error())
    }

    fmt.Println(dept)
}

time.Timeを使いたいので, 調べると下記が見つかりました.

おおたの物置 - goでtime.Timeをmysqlから読む

どうやらDSNにparseTime=trueオプションをつける必要があるようです.

公式にも記載がありますね.

やってみましょう.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
    "time"
)

type Dept struct {
    DNo string
    DName string
    Budget float64
    LastUpdate time.Time
}

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    var dept Dept

    // Select文発行
    err = db.QueryRow(`
      SELECT
           dno
          ,dname
          ,budget
          ,lastupdate
      FROM
          dept
  `).Scan(&(dept.DNo), &(dept.DName), &(dept.Budget), &(dept.LastUpdate))

    if err != nil {
        panic(err.Error())
    }

    fmt.Println(dept)
}

あっさり読めました.

やはりDB毎の差分を完全に吸収できるわけではなさそうです.(そりゃそうだ)

複数件探す(Select)

複数件を検索するにはQueryメソッドを使います.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
    "time"
)

type Dept struct {
    DNo string
    DName string
    Budget float64
    LastUpdate time.Time
}

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Select文発行
    rows, err := db.Query(`
      SELECT
           dno
          ,dname
          ,budget
          ,lastupdate
      FROM
          dept
  `)
    if err != nil {
        panic(err.Error())
    }
    defer rows.Close()

    // 1行ずつ取得
    for rows.Next() {
        var dept Dept
        err := rows.Scan(&(dept.DNo), &(dept.DName), &(dept.Budget), &(dept.LastUpdate))
        if err != nil {
            panic(err.Error())
        }
        fmt.Println(dept)
    }

    // 上のイテレーション内でエラーがあれば表示
    if err := rows.Err(); err != nil {
        panic(err.Error())
    }
}

[実行結果]

$ ./mysql.exe
{D1 Marketing 10000 2017-03-29 00:00:00 +0000 UTC}
{D2 Development 12000 2017-03-29 00:00:00 +0000 UTC}
{D3 Research 5000 2017-03-29 00:00:00 +0000 UTC}

ちゃんと読めますね.

列名を取得する(Select)

列名を取得することもできます.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Select文発行
    rows, err := db.Query(`
      SELECT
           dno
          ,dname
          ,budget
          ,lastupdate
      FROM
          dept
  `)
    if err != nil {
        panic(err.Error())
    }
    defer rows.Close()

    // 列名取得
    cols, err := rows.Columns()
    if err != nil {
        panic(err.Error())
    }
    // 列名表示
    for _, name := range cols {
        fmt.Println(name)
    }
}

[実行結果]

$ ./mysql.exe
dno
dname
budget
lastupdate

更新する(Update)

Execを使います.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Update文発行
    result, err := db.Exec(`
      UPDATE
          dept
      SET
           budget = 20000
          ,lastupdate = '2017/03/31'
      WHERE
          dno = 'D1'
  `)
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得して表示
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
1

削除する(Delete)

Updateと同じでExecを使います.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // DELETE文発行
    result, err := db.Exec(`
      DELETE
      FROM
          dept
      WHERE
          dno = 'D1'
  `)
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
1

引数を使う

引数の取り扱いで一番簡単なのは, ExecやQuery, QueryRowのメソッドの第2引数以降に
順番にパラメータを並べてやる方法です.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // 引数付きでInsert文発行
    result, err := db.Exec(`
      INSERT INTO dept(dno, dname, budget) VALUES(?, ?, ?)
  `, "D1", "Marketing", 10000)
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
1

名前付き引数を使う(Named Parameter)

しかしまぁ現実問題, 名前付き引数なしでは業務では辛いパターンが多いでしょう.
goも1.8から名前付き引数をサポートするようになりました.

名前付き引数は
sql.Named(引数名, 引数)
の形でパラメータを作り, 上の例と同様, クエリを発行するメソッドの第2引数以降に渡してやります.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // 引数付きでInsert文発行
    result, err := db.Exec(`
      INSERT INTO dept(dno, dname, budget) VALUES(@DNO, @DNAME, @BUDGET)
  `, sql.Named("DNO", "D1"), sql.Named("DNAME", "Marketing"), sql.Named("BUDGET", 10000))
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
panic: sql: driver does not support the use of Named Parameters

goroutine 1 [running]:
main.main()
        C:/msys64/home/twinbird/dropbox/lab/mysql/sample.go:23 +0x452

ぐわぁ.

この記事を書いている時点ではこのMySQLドライバは名前付き引数をサポートしていないようです.残念.

Issueは上がっていますけど, どうでしょう.

SQLite3はサポートされているとの事ですので, 試してみました.

公式の説明ではSQL内では
@引数名
の形で引数を埋め込む例があります.

が, :で埋め込むと動きました.(ここら辺はDBMS依存なのかもしれません)

package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "fmt"
)

func main() {
    // 第2引数はDBファイル名
    db, err := sql.Open("sqlite3", "sample.db")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // 引数付きでInsert文発行
    result, err := db.Exec(`
      INSERT INTO dept(dno, dname, budget) VALUES(:DNO, :DNAME, :BUDGET)
  `, sql.Named("DNO", "D1"), sql.Named("DNAME", "Marketing"), sql.Named("BUDGET", 10000))
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

C:\Users\twinbird\Dropbox\lab\sqlite3>sqlite3.exe
1

動きます.

プリペアードステートメントを使う

普通に使えます.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // プリペアードステートメント作成
    stmt, err := db.Prepare("INSERT INTO dept(dno, dname, budget) VALUES(?, ?, ?)")
    if err != nil {
        panic(err.Error())
    }
    defer stmt.Close()

    // プリペアードステートメントを使ってInsert文発行
    result, err := stmt.Exec("D1", "Marketing", 10000)
    if err != nil {
        panic(err.Error())
    }
    // 影響を与えた件数を取得
    n, err := result.RowsAffected()
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(n)
}

[実行結果]

$ ./mysql.exe
1

トランザクションを扱う

業務でやるなら, トランザクションは必須でしょう.
(Web系とかは知らないけど)

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // 疑似乱数の種作っておく
    rand.Seed(time.Now().UnixNano())

    // トランザクション開始
    tx, err := db.Begin()
    if err != nil {
        panic(err.Error())
    }

    // recoverでロールバックするように設定しておく
    defer func() {
        if err := recover(); err != nil {
            if err := tx.Rollback(); err != nil {
                panic(err.Error())
            }
            fmt.Println("Rollbacked")
        }
    }()

    // Insert文発行
    _, err = db.Exec(`
      INSERT INTO dept(dno, dname, budget) VALUES('D1', 'Marketing', 10000)
  `)
    if err != nil {
        panic(err.Error())
    }

    // ランダムでパニクらせる
    if n := rand.Intn(10); n < 5 {
        panic("opps!!")
    }

    // コミット
    if err = tx.Commit(); err != nil {
        panic(err.Error())
    }
    fmt.Println("Commited")
}

[実行結果]

$ ./mysql.exe
Commited

$ ./mysql.exe
Commited

$ ./mysql.exe
Rollbacked

$ ./mysql.exe
Rollbacked

$ ./mysql.exe
Commited

isolation levelも扱えるっぽいんですが, よくわからなかったです.無能.

クエリをキャンセルする(context)

go1.8からはcontextをサポートしたのでクエリのキャンセルが出来るようになりました.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
    "context"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Cancel可能Context作る
    ctx, cancel := context.WithCancel(context.Background())
    // そっこーキャンセル
    go func() {
        cancel()
    }()

    // Select文発行
    row := db.QueryRowContext(ctx, `
      SELECT
          dno
      FROM
          dept
  `)
    // 読み取り
    var dno string
    if err := row.Scan(&dno); err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println(dno)
    }
}

[実行結果]

$ ./mysql.exe
context canceled

ホントはもうちょっとクエリ実行中感のある例が良いのですが.

Execとかにも同様にContextを扱うメソッドがあります.

接続状態を確認する(ping)

DBの接続状態を判断することができるpingメソッドが用意されています.

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
    "fmt"
)

func main() {
    // 第2引数の形式は "user:password@tcp(host:port)/dbname"
    db, err := sql.Open("mysql", "root:password@/demo?parseTime=true")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    if err := db.Ping(); err != nil {
        fmt.Println("疎通エラー")
    } else {
        fmt.Println("疎通してるよ")
    }
}

[実行結果]

$ ./mysql.exe
疎通してるよ

他のDBではどうするか

Driverが多数用意されているので他のDBでも同様の事が出来ると思います.

(もちろん程度はあるでしょうが)

ORMapperもいいのがあるのかもしれませんが, 標準でとりあえずやりたいことは事足りそうです.

Windows10にPostgreSQLを入れる

PostgreSQL

Windowsに入れたことないので, メモ書き.

ダウンロードする

PostgreSQL公式へ行くと, インストーラは別の場所で配布されているらしい.

ここからダウンロードしてインストールした.

インストールする

基本「Next」押しただけ.

パスワードは普通に設定します.

Localeを設定するところでJapaneseを選んだんだけど, どうもCがおすすめらしい.マジかよ.

とはいえバージョンもかなり上がっているので大丈夫かも.

最後に, Stack Builderを起動するか?みたいなメッセージが出てきたけど, 無視した.
(何なんだろうこれ)

環境変数の設定

まぁ, 普通に.

C:\Program Files\PostgreSQL\9.6\bin

を追加. (32bit版ならパスが違うだろうけど)

Windows10からは環境変数の管理画面もわかりやすくなってうれしいです.

動作確認

psqlでつないでみればよいかと.

C:\Users\twinbird>psql -U postgres
ユーザ postgres のパスワード:
psql (9.6.2)
"help" でヘルプを表示します.
結論
  1. 特にメモるほどの事はなかった
  2. PostgreSQLの公式が何故かググってもランクが低い気がする
  3. 一番役に立ったのはURLのメモ