write ahead log

ロールフォワード用

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

【追記】もうちょっと例が欲しくなったので, その2も書きました


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.org

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

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

golangでデータベース(RDBMS)を扱う[MySQLの例]

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

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

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

準備

まずは有難くパッケージを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もいいのがあるのかもしれませんが, 標準でとりあえずやりたいことは事足りそうです.

golangのimport文で別名つける

golangのimport文は別名をつけることができる.

普通のimport文は

package main

import (
    "fmt" //ここ
)

func main() {
    fmt.Println("hello")
}

こんな感じで文字列でパッケージ名を指定するだけ.

別名をつける場合には以下のようになる.

package main

import (
    f "fmt" //ここ
)

func main() {
    f.Println("hello")
}

ここまでは知ってたんだけど, パッケージ名指定なしにもできるらしい.

package main

import (
    . "fmt" //ここ
)

func main() {
    Println("hello")
}

.(ドット)ね.あーなるほどね.

_(アンダースコア)の方はdatabase/sqlで割と使う事が多いんじゃないかと思う.

宣言したファイルでは使わないけど, 依存パッケージでは使うよっていう宣言.

import (
    "database/sql" // SQL標準のアクセス用パッケージ
    _ "github.com/go-sql-driver/mysql"  // MySQLのドライバ(↑で使う)
)

地味なとこで知らないことがおおい...

golangでバイナリを操作する

golangは低レベルな処理(バイナリの取り扱いとかね)もちゃんと行うことができる.

基本(byte型を使う)

golangは組み込みでbyte型が用意されている.

これはその名の通りバイトを扱うための型で, 配列やスライスにすれば単純なバイトストリームを扱える.

package main

import (
    "fmt"
)

func main() {
    b := []byte{0xDe, 0xaD, 0xBe, 0xeF}
    fmt.Printf("%b\n", b) // 2進表示
    fmt.Printf("%d\n", b) // 10進表示
    fmt.Printf("%x\n", b) // 16進小文字表示
    fmt.Printf("%X\n", b) // 16進大文字表示
}

[実行結果]

[11011110 10101101 10111110 11101111]
[222 173 190 239]
deadbeef
DEADBEEF
bytesパッケージ

byteのスライスを扱う場面は多いので, 標準で操作するパッケージが用意されている.
(Package bytes)https://golang.org/pkg/bytes/

以下のように比較も簡単だし, 文字列操作を意識しているのかその手の関数が多い.
(byteのスライスはstringとしてみなせるし)

package main

import (
    "fmt"
    "bytes"
)

func main() {
    b := []byte{0xDe, 0xaD, 0xBe, 0xeF}
    b2 := []byte{0xDe, 0xaD, 0x00, 0x00}
    b3 := []byte{0xDe, 0xaD, 0xBe, 0xeF}

    c := bytes.Compare(b, b2) // => 1
    fmt.Println(c)
    c2 := bytes.Compare(b, b3) // => 0
    fmt.Println(c2)
}

[実行結果]

1
0
ファイルに書いてみる

ファイルはただのバイト列なので, ファイルにも書けますね.

package main

import (
    "io/ioutil"
)

func main() {
    b := []byte{0xDe, 0xaD, 0xBe, 0xeF}

    ioutil.WriteFile("test.bin", b, 0644)
}

[実行結果]

Endianの都合で逆になっちゃってますが...(Windowsはリトルエンディアン)

$ od -x test.bin
0000000 adde efbe
0000004
バッファを扱う(bytes.bufferを使う)

バイトストリームを操作する時にイチイチ操作関数を使っていると面倒でしょうがないです. io.Readerやio.Writerの操作メソッドを使いたいですね.
あと, いきなりファイルに書いたりせずにバッファリングしたい場面がほとんどです.

bytesパッケージにはbufferという構造体が用意されています.

標準入出力のテストにも使えます.

以下の様なパッケージがあるとして

// 挨拶するパッケージ
package hello

import (
    "fmt"
    "io"
)

func hello(out io.Writer) {
    fmt.Fprintf(out, "hello, world")
}

上記のテストコードは以下になります.
bufferへ書き出して比較するという感じです.
helloの引数にinterfaceを使ったおかげでいい感じになります.

package hello

import (
    "bytes"
    "testing"
)

func TestHello(t *testing.T) {
    expected := []byte("hello, world")

    stdout := new(bytes.Buffer)
    hello(stdout)

    if bytes.Compare(stdout.Bytes(), expected) != 0 {
        t.Fatal("greeting is not matched")
    }
}

このテストは無事通ります.

[実行結果]

$ go test
PASS
ok      _/C_/msys64/home/twinbird/test      0.128s
バイトストリームを構造体へ変換する(encoding/binaryを使う)

プログラムでデータを扱うとなるとやっぱり構造体でまとめたいですね.

一々バイトストリームから構造体へ変換する処理を書くのは面倒ですが,
golangにはいい感じにしてくれるパッケージがあります.

package main

import (
    "bytes"
    "fmt"
    "encoding/binary"
)

type Beef struct {
    One byte
    Two byte
    Three byte
    Four byte
}

func main() {
    b := []byte{0xDe, 0xaD, 0xBe, 0xeF}
    buf := bytes.NewBuffer(b)
    var beef Beef

    binary.Read(buf, binary.LittleEndian, &beef)
    fmt.Printf("%x\n", beef.One)
    fmt.Printf("%x\n", beef.Two)
    fmt.Printf("%x\n", beef.Three)
    fmt.Printf("%x\n", beef.Four)
}

[実行結果]

$ ./test.exe
de
ad
be
ef

読み出しだけ書いてみましたが, 書き出しもあります.

注意点としては

  • 構造体のメンバはちゃんとエクスポートしておかないとダメ
  • 渡すのは構造体へのポインタ

というところくらいだと思います.

エンディアンも指定出来て良いです.

golangでfnvハッシュ関数を使う

FNVハッシュ関数は64bit or 32bitでの出力を行うことができるハッシュ関数です.

SHAシリーズ(SHA-1など)やMDシリーズ(MD5など)などとは異なり, 衝突耐性よりも実装が効率的な事を重視しているっぽい.

逆にセキュリティなどを重視するところでは使っちゃダメですね.

サンプルコードをメモっとく.

package main

import (
    "fmt"
    "hash/fnv"
)

func hash32(message []byte) uint32 {
    h := fnv.New32()
    h.Write(message)
    sum := h.Sum32()
    return sum
}

func hash64(message []byte) uint64 {
    h := fnv.New64()
    h.Write(message)
    sum := h.Sum64()
    return sum
}

func main() {
    fmt.Println(hash32([]byte("Test Hashing")))
    fmt.Println(hash64([]byte("Test Hashing")))
}

golangでシグナルをハンドリングする

golangでコマンド作ってもctrl + cとかで止めたくなるじゃないですか.

ちょっと調べた.

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sig_ch := make(chan os.Signal, 1)
    signal.Notify(sig_ch,
        os.Interrupt,
        syscall.SIGHUP,
        syscall.SIGINT,
        syscall.SIGTERM,
        syscall.SIGQUIT)

    exit_ch := make(chan int)
    go func() {
        for {
            s := <-sig_ch
            switch s {
            case os.Interrupt:
                // windowsはこれで頑張る
                fmt.Println("Interrupt")
                exit_ch <- 0
            case syscall.SIGHUP:
                fmt.Println("SIGHUP")
                exit_ch <- 0
            case syscall.SIGINT:
                fmt.Println("SIGINT")
                exit_ch <- 0
            case syscall.SIGTERM:
                fmt.Println("SIGTERM")
                exit_ch <- 0
            case syscall.SIGQUIT:
                fmt.Println("SIGQUIT")
                exit_ch <- 0
            default:
                fmt.Println("Other")
                exit_ch <- 1
            }
        }
    }()
    code := <-exit_ch
    os.Exit(code)
}

Windowsはちょっと別枠ですが, ちゃんと取り扱えるのはいいですね.

余談ですが, Windows + msys2だとうまく動かないのでいい方法が知りたいです.

golangでtsv(csv)を読む

探せばいくらでも出てきそうだけど,メモしておく.

コード見たほうが早いと思うので.

package main

import (
    "encoding/csv"
    "fmt"
    "log"
    "strings"
)

func main() {
    // テスト用文字列
    str := "test\tテスト\nHello\tこんにちは"

    // CSVのReaderを用意
    r := csv.NewReader(strings.NewReader(str))

    // デリミタ(TSVなら\t, CSVなら,)設定
    r.Comma = '\t'

    // コメント設定(なんとコメント文字を指定できる!)
    r.Comment = '#'

    // 全部読みだす
    records, err := r.ReadAll()
    if err != nil {
        log.Fatal(err)
    }

    // 各行でループ
    for _, v := range records {
        // 1列目
        fmt.Print(v[0])

        fmt.Print(" | ")

        // 2列目
        fmt.Println(v[1])
    }
}

実行結果

$ ./tsv.exe
test | テスト
Hello | こんにちは

割と使いやすいライブラリと思う(クセがない)