ただでは済まなかったペルー旅行(1日目)

2015年9月中旬、夫婦(夫30歳、妻28歳)にてペルーを訪問。南米というと地理的にも移動手段的にも日本人にはとっつきにくく、事前情報も限られている地域である。地球の歩き方やインターネット上の情報を頼りに計画を立て旅行を実行したが、困難に直面した場面が多々あり、これからペルー旅行を計画する旅行者のための情報提供として旅行記を記したい。

旅程

休暇が限られたサラリーマン故、旅程は以下のとおり。いつ倒れてもおかしくないスケジュール。

日程 行動 宿泊
1日目 18:30成田発-16:10ダラス着(AA60)
22:15ダラス発-翌5:19リマ着(AA980)
機内泊
2日目 8:05リマ発-9:15クスコ着(2I1181)
クスコ観光
クスコ泊
3日目 マチュピチュへ移動(バス+徒歩) マチュピチュ村泊
4日目 マチュピチュ観光
クスコへ移動(列車+バス)
クスコ泊
5日目 10:25クスコ発-11:45リマ着(2I1114)
リマ観光
リマ泊
6日目 7:30リマ発-15:00ナスカ着(観光バスCruz del Sur) ナスカ泊
7日目 ナスカ地上絵鑑賞
イカに移動(バス)
イカ泊
8日目 14:00イカ発-18:20リマ着(観光バスCruz del Sur) リマ泊
9日目 リマ観光
23:45リマ発-翌7:10ダラス着(AA988)
機内泊
10日目 10:40ダラス発-翌14:20成田着(AA175) 機内泊

突然の欠航

成田に着くと、アメリカン航空のチェックインカウンターでダラス-リマ便が欠航になった旨が告げられる。代替便として、ダラスからさらにマイアミで乗り継いでリマに半日程度遅れて着く便を提示されるが、それではリマの乗り継ぎに間に合わない。

アメリカン航空のスタッフが色々なところに電話したりして最終的に提案してくれたアグレッシヴな代替便が、以下のとおり。

  • 17:25成田発-同日11:25ロス・アンゼルス着(AA170)
  • 13:20ロス・アンゼルス発-23:45リマ着(LA601)

続きを読む

HerokuにGo言語+MongoDBのWebアプリケーションをデプロイする

Good! Counter Application

Herokuでは公式ではGo言語はサポートされていないにも関わらず、意外なほど簡単にデプロイすることができる。

http://good-counter-go.herokuapp.com/
https://github.com/inatus/good-counter-go

前準備

  • Herokuへアカウント登録
  • Heroku toolbeltのインストール
  • $GOPATHの設定

ググれば情報がいくらでも出てくる。

Go言語のソースフォルダの作成

$GOPATH/src内にソースフォルダを作成する必要がある。

$ mkdir $GOPATH/src/good-counter-go
$ cd $GOPATH/src/good-counter-go
$ git init

herokuプロジェクトの作成

Heroku Toolbeltにログインし、新規にプロジェクトを作成する。

$ heroku login
Enter your Heroku credentials.
Email: you@example.com
Password:
Uploading ssh public key /Users/you/.ssh/id_rsa.pub
$ heroku create -b https://github.com/kr/heroku-buildpack-go.git
Creating vast-brook-7638... done, stack is cedar
BUILDPACK_URL=https://github.com/kr/heroku-buildpack-go.git
http://vast-brook-7638.herokuapp.com/ | git@heroku.com:vast-brook-7638.git

プロジェクト名の変更

ランダムにプロジェクト名が付けられるので、任意の名前に変更する。

$ heroku apps:rename good-counter-go
Renaming vast-brook-7638 to good-counter-go... done
http://good-counter-go.herokuapp.com/ | git@heroku.com:good-counter-go.git
Git remote heroku updated

HerokuプロジェクトにMongoDBアドオンの追加

プロジェクトにMongoDBアドオン(無料版)を追加する。無料版を利用する場合であってもHerokuの設定画面からクレジットカード情報を入力する必要があるようだ。

$ heroku addons:add mongohq
Adding mongohq on good-counter-go... done, v4 (free)
Use `heroku addons:docs mongohq` to view documentation.

Go Webアプリケーションを作成

MongoDBと連携したWebアプリケーションを作成する。ポイントは以下の通り。

httpサーバのポートに環境変数PORTを指定

http.ListenAndServe(":"+os.Getenv("PORT"), nil)

MongoDBのURLに環境変数MONGOHQ_URLを指定

環境変数MONGOHQ_URLにユーザ名、パスワードも含まれている。

sess, err := mgo.Dial(os.Getenv("MONGOHQ_URL"))

データベース名をMongoHQの管理画面を確認して指定

HerokuのApps管理画面からMongoHQの管理画面に入り、DB名を確認する。Goが接続するDB名にこれを設定する。

Heroku Apps管理画面

MongoHQ管理画面

const MONGO_DB_NAME = "app21817638"
c := mgoSession.DB(MONGO_DB_NAME).C("count")

DBにコレクション・ドキュメントを挿入

コマンドラインから追加する場合

Admin画面のUsersタブより新規ユーザを追加し、コマンドラインから以下のようにコンソールにログインする。

MongoHQ User追加画面

$ mongo troup.mongohq.com:10084/app21817638 -u <user> -p <password>

管理画面から追加する場合

Colletions画面のCreate a collectionからコレクションを作成する。

MongoHQ Collection追加画面

Herokuレポジトリへのアプリケーションの登録

ソースをレポジトリにコミットする。

$ git add -A .
$ git commit -m 'Add source'

Heroku用の設定ファイルを作成、ライブラリをダウンロードし、レポジトリにコミットする。

$ echo 'web: good-counter-go' > Procfile
$ go get github.com/kr/godep
$ godep save 
$ git add -A .
$ git commit -m 'Add dependencies'

最後に、Herokuにpushする。

$ git push heroku master
-----> Fetching custom git buildpack... done
-----> Go app detected
-----> Using go1.1.2
-----> Running: godep go install -tags heroku ./...
-----> Discovering process types
       Procfile declares types -> web
-----> Compressing... done, 3.0MB
-----> Launching... done, v5
       http://good-counter-go.herokuapp.com deployed to Heroku

動作確認

$ heroku open

リソースをGithubにもpushする場合

Githubで新規レポジトリを作成し(自動initにしない)、以下のようにする。

$ git remote add origin https://github.com/inatus/good-counter-go.git
$ git push -u origin master

Go言語:go-gtkでネットワーク対戦型○×ゲームのネイティブアプリケーションを実装する

screen_shot

Go言語のGUIライブラリgo-gtkを利用してネットワークを介して対戦できる○×ゲームのネイティブアプリケーションを実装した。

コード:
github.com/inatus/noughts-and-crosses-go

使い方

事前準備

  • PC2台と対戦してくれる友達を用意する
  • GTK+2のインストール
    Macならbrew install gtk+でインストールするのが楽。
  • 6392ポートを開けておく。
    通信に勝手にこのポートを使う。

ビルド

$ go get github.com/inatus/noughts-and-crosses-go
$ cd $GOPATH/src/github.com/inatus/noughts-and-crosses-go
$ go build
$ ./noughts-and-crosses-go

解説

実装上の特徴は以下のとおり。

  • GUIにGTKを利用。つまりマルチプラットフォームに動作する(はず)。
    ライブラリはgo-gtkを利用。
  • 対戦のための通信にはUDPを利用。ポート番号は決め打ち。
    標準ライブラリnetパッケージを利用。

go-gtkライブラリには説明がなくて分かりにくかったが、他のGTK APIの解説サイトで解決。
これが役に立った。だいたい似ているメソッド名になっている。
GTK+ リファレンスマニュアル

ネットワーク対戦に必要な通信は下記の3つで成り立っている。

  • 他端末に自分の存在を知らせるために一定間隔でブロードキャストを繰り返すためのgoroutineを起動
  • 他端末から送られてくる通信を受信するためのgoroutineを起動
  • Startボタンや手を決めるボタンのクリック時に対戦相手にデータを送信

Go言語でもこういったネイティブで動くGUIアプリが作れるとなるとかなり用途が広がるだろう。

Go言語:goroutineとchannelを使ってWebSocketでブラウザと通信するXMPPチャットWebクライアントを実装する

screen_shot

Go言語でGoogle Hangout(旧Google Talk)などで利用できるXMPPチャットクライアントのWebアプリを実装した。
github.com/inatus/xmpp-web-client-go

使用した技術要素は以下のとおり。

  • Webブラウザとの通信はWebSocketを利用
  • チャットサーバとの通信はXMPPを利用
  • 受信データはGo言語のgoroutine、channelを用いて並行処理を行う

WebSocketには準標準パッケージのcode.google.com/p/go.net/websocketを利用する。

XMPPはcode.google.com/p/rsc/xmppを使おうと思ったが、Roster(コンタクトリスト)がうまく取得できなかったため、github.com/agl/xmppを利用することにした。

方針

本プログラムでは以下のように、異なるプロトコルの通信を扱う必要がある。

xmpp_chat_client_overview

Webブラウザから入力されたメッセージは、WebSocket経由で受信し(①)、XMPPサーバへ送信する(②)。
チャット先の相手がメッセージを送信した際には、XMPPサーバからメッセージを受信し(③)、WebSocket経由でWebブラウザに送信する(④)。
このようなリモートからの通信(①、③)を受ける際には並行処理が必要になる。

今回は、以下のようにWebSocket、XMPPからの通信を受けるgoroutineを別々に立て、メインの処理を走らせるgoroutineに集めて処理を行う方法をとった。

goroutine_structure

Go言語では、goroutine、channelを用いることで、こういった並行処理を驚くほど簡単に実現できる。

実装

まずコネクションを1つの型にまとめる。
goroutineとして実行する関数は、この型のメソッドとして定義する。(後述)

type session struct {
    talk *xmpp.Conn
    ws   *websocket.Conn
}

その上で、それぞれのコネクションからの受信データを格納するchannelを定義し、これを引数にしてgoroutineを起動する。
メインのgoroutineはそのままselect-case構文によりchannelからのデータを待ち受ける。
case文にそれぞれのchannelを指定してやれば複数のchannelでデータを待ち受けることが可能。
なお、okにはchannelが開いていればtrue、閉じていればfalseが返る。

ses := session{
    talk: talk, // XMPPのコネクション
    ws:   ws, // WebSocketのコネクション
}

stanzaChan := make(chan xmpp.Stanza) // xmpp.Stanza型のchannelを定義
go ses.receiveMessage(stanzaChan) // goroutineの起動

webSocketChan := make(chan string) // string型のchannelを定義
go ses.receiveWebSocket(webSocketChan) // goroutineの起動

for {
    select {
    case receivedStanza, ok := &lt;-stanzaChan: // XMPP用channelからのデータを待ち受ける
        if !ok {
            // エラー処理
        }
        // 処理
    case receivedMessage, ok := &lt;-webSocketChan: // WebSocket用channelからのデータを待ち受ける
        if !ok {
            // エラー処理
        }
        // 処理
    }
}

XMPP、WebSocketからの通信を待ち受けるgoroutineで行うことは、受け取ったデータを引数のchannelに渡すことのみである。

func (ses *session) receiveMessage(stanzaChan chan&lt;- xmpp.Stanza) {
    defer close(stanzaChan)
    for {
        receivedStanza, err := ses.talk.Next() // XMPPからのデータを待ち受ける
        if err != nil {
            // エラー処理
        }
        stanzaChan &lt;- receivedStanza // channelにデータを渡す
    }
}

func (ses *session) receiveWebSocket(webSocketChan chan&lt;- string) {
    defer close(webSocketChan)
    for {
        var receivedMessage string
        if err := websocket.Message.Receive(ses.ws, &amp;receivedMessage); err != nil {  // WebSocketからのデータを待ち受ける
            // エラー処理
        }
        webSocketChan &lt;- receivedMessage // channelにデータを渡す
    }
}

たったこれだけで上記の図に示すような並行処理を組むことができる。

Go言語:sshパッケージを使って対話型SSHクライアントを実装する

code.google.com/p/go.crypto/sshで提供されているsshパッケージを使って、標準のsshコマンドと同等の対話型SSHクライアントを実装してみた。

コードは以下。
https://github.com/inatus/ssh-client-go

API・コード例はGoDocに記載されている。
http://godoc.org/code.google.com/p/go.crypto/ssh

しかし、Sessionを作るところまでは例があるからいいとして、その先がよくわからない。
セッションを開始するメソッドと以下のようなものがあるようだ。

  • Run: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果はSessionのStdio、Stderrに出力される。
  • Start: コマンドを引数に指定。コマンドの実行完了を待たずに先に進む。結果はSessionのStdio、Stderrに出力される。つまり、Startメソッドを実行した直後には結果が出力されない。
  • Shell: コマンドを実行せずにシェルを開始する。
  • Output: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果が戻り値に返る。
  • CombinedOutput: コマンドを引数に指定。コマンドの実行が完了するまでWaitが掛かり、結果が戻り値に返る。標準出力に加え標準エラー出力が返る。

また、Sessionに対してこれらのメソッドは一度しか実行できない。
これらのメソッドの中で、Shell以外は非対話型的な実行を想定していると思われる。したがって、Shellを用いる。

Shellメソッドを実行すると、ログイン直後の出力がStdioに出力される。これをOSの標準出力に出力するようにした。

session.Stdout = os.Stdout
session.Stderr = os.Stderr

次に、ログイン後のコマンド受付処理を実装する。コマンドはStdinに受け付けられる。端末の標準入力から入力されたコマンドをStdinPipeを使ってSessionのStdinに伝える。

in, _ := session.StdinPipe()
for {
  reader := bufio.NewReader(os.Stdin)
  str, _ := reader.ReadString('n')
  fmt.Fprint(in, str)
}

この際、以下に注意する。

  • StdinPipeはセッションを開始する前に取得すること。
  • コマンドの末尾には改行コード(n)を付けること。

これで通常のsshコマンドと同等な対話型SSHクライアントが構築できる。
ただし、パスワードのマスクやテキストエディタの起動などがうまくできない。

Ubuntu 12.04 サーバでファイルシステム全体を定期的にバックアップする

Linuxを使っていて困るのがバックアップである。
MacだったらTimeMachineに任せればファイルシステム全体を自動でバックアップしてくれる。
LinuxでもCUI、GUIそれぞれにいろいろなツールがあるようだが、
duplicityとcronを使ってファイルシステム全体を定期的にバックアップできるようにしてみた。

概要

duplicityによるフルバックアップ・増分バックアップをcronで定期的実行するように設定する。

手順

Ubuntuには標準でduplicityとcronがインストールされているはず。
duplicityでフルバックアップ・増分バックアップを実行するバッチファイルを作成し
そのバッチファイルをcronに登録する。

バックアップ実行バッチファイルの作成

以下のようにduplicityでバックアップを実行するバッチファイルを作成する。
まずはフルバックアップ用。

#! /bin/sh
duplicity full --no-encryption 
--archive-dir /mnt/backup/archive 
--exclude /dev 
--exclude /mnt 
--exclude /tmp 
--exclude /proc 
--exclude /sys 
--exclude /run 
--exclude /var/run 
--exclude /var/lock 
/ file:///mnt/backup/data

引数・オプションの意味は以下のとおり。

  • --no-encryptionで暗号化なしを指定
  • --archive-dirにバックアップ状態を保存しておくディレクトリを指定
  • --excludeで以下のディレクトリをバックアップ対象から除外
    • /dev デバイスのファイルを格納
    • /mnt 外部ストレージ等のマウントポイント
    • /tmp 一時ファイルを格納
    • /proc プロセス情報を格納
    • /sys デバイスの情報を格納
    • /run システムデータを格納
    • /var/run 起動してからのシステムデータを格納
    • /var/lock 二重起動などをチェックするロックファイルを格納
  • バックアップ対象はファイルシステム全体
  • バックアップ先はfile:///mnt/backup/data

一度フルバックアップを手動で取っておく。

次に、差分バックアップのバッチファイルを作成する。

#! /bin/sh
duplicity incremental --no-encryption 
--archive-dir /mnt/backup/archive 
--exclude /dev 
--exclude /mnt 
--exclude /tmp 
--exclude /proc 
--exclude /sys 
--exclude /run 
--exclude /var/run 
--exclude /var/lock 
/ file:///mnt/backup/data

引数をfullをincrementalに変えただけ。

バッチファイルをcronに登録

最後に、バッチファイルの実行をcronに登録する。
まずはcrontabを実行。

$ sudo crontab -e
Select an editor.  To change later, run 'select-editor'.
  1. /bin/ed
  2. /bin/nano        <---- easiest
  3. /usr/bin/vim.basic
  4. /usr/bin/vim.tiny

Choose 1-4 [2]: 

好きなエディタを選択する。
男は黙ってvim。
テキストファイルが表示されるので、どこでも良いので以下のように記述する。

0 0 * * 0 /bin/sh /mnt/backup/script/incrementalbackup.sh > /dev/null 2>&1

指定の意味は以下のとおり。

  • 曜日
  • ユーザ
  • コマンド

コマンドには先ほど作成した増分バックアップ用バッチスクリプトを指定し、結果を/dev/nullに出力する。
これを指定しないと実行結果が毎回メールで届くらしい。

Go言語:templateパッケージでHTML等テキストファイルを読み込む(応用編2)

templateパッケージでHTML等の任意のテキストファイルを読み込み、出力する方法を説明する。
応用編その2ではテンプレート内に埋め込む「アクション」で利用できる様々な機能について解説する。
なお、APIバージョンGo1で説明する。
バージョン3ではパッケージ名、関数名が異なるので注意。

概要

templateパッケージでは、任意のテキストを読み込み、テキストに埋め込まれたタグに対してデータの評価や制御(アクションという)をおこなって出力する。
templateパッケージ応用編1では、アクションの基本的な機能について説明したが、今回は以下のようなさらに高度な機能について説明する。

  • {{if}}アクションにnot、and、or評価をおこなう。
  • 配列、Slice、Mapのサイズや任意のインデックスの値を取得する。
  • 出力結果のフォーマットをおこなう。
  • 任意の演算をおこなった結果を出力する。

not演算によりbool値を評価する

package main

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

const templateString string = `
{{if not .}}TRUE {{/* not演算をおこなう */}}
{{else}}FALSE
{{end}}
`

func main() {
	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, true); err != nil { // trueを渡す
		fmt.Println(err.Error())
	}
}

実行結果

FALSE

and, or演算によりbool値を評価する

package main

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

const templateString string = `
{{with .}}
    {{if and .Field1 .Field2}}TRUE {{/* and演算をおこなう */}}
    {{else}}FALSE
    {{end}}
    {{if or .Field1 .Field2}}TRUE {{/* or演算をおこなう */}}
    {{else}}FALSE
    {{end}}
{{end}}
`

type Fields struct {
    Field1 bool
    Field2 bool
}

func main() {
    f := Fields{true, false}

	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, f); err != nil { // 構造体Fieldsを渡す
		fmt.Println(err.Error())
	}
}

実行結果

FALSE
    
TRUE

配列のサイズや任意のインデックスの値を出力する

package main

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

const templateString string = `
{{len .}} {{/* 配列のサイズを出力する */}}
{{index . 1}} {{/* 配列の[1]の値を出力する */}}
`

func main() {
    array := []int{1, 2, 4, 8}

	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, array); err != nil { // 配列を渡す
		fmt.Println(err.Error())
	}
}

実行結果

4
2

変数に一度値を格納し、出力する

package main

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

const templateString string = `
{{range $i, $v := .}}{{$i}}:{{$v}}
{{end}}
`

func main() {
    array := []int{1, 2, 4, 8}

	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, array); err != nil { // 配列を渡す
		fmt.Println(err.Error())
	}
}

実行結果

0:1
1:2
2:4
3:8

値のフォーマットを変更して出力する

package main

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

const templateString string = `
{{.Tags}}

{{html .Tags}} {{/* <や"などhtmlタグに用いる文字列をエスケープする */}}
{{.Tags | html}}

{{printf "%c" .Char}} {{/* 文字型で出力する */}}
{{.Char | printf "%c"}}
`

type Fields struct {
    Tags string
    Char int
}

func main() {
    f := Fields{"<a href="aaa">link</a>", 65}

	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, f); err != nil { // 構造体Fieldsを渡す
		fmt.Println(err.Error())
	}
}

実行結果


<a href="aaa">link</a>

&lt;a href=&#34;aaa&#34;&gt;link&lt;/a&gt;
&lt;a href=&#34;aaa&#34;&gt;link&lt;/a&gt;

A
A

構造体に定義された関数を呼び出した結果を利用する

package main

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

const templateString string = `
{{if call .F .Field}}TRUE {{/* 構造体に定義されたF関数をFieldフィールドを引数として呼び出す */}}
{{else}}FALSE
{{end}}
`

type Fields struct {
	Field int
    F func(n int) bool 
}

func validate(n int) bool {
    if n > 0 {
		return true
	}
	return false
}

func main() {
    f := Fields{-10, validate}

	var t = template.Must(template.New("text").Parse(templateString))

	if err := t.Execute(os.Stdout, f); err != nil { // 構造体Fieldsを渡す
		fmt.Println(err.Error())
	}
}

実行結果

FALSE

解説

変数

{{.}}{{range .}} {{.}} {{end}}のように直接値を出力するだけでなく、一度変数に格納した値を出力することができる。
任意の値を変数に格納するには以下のように記述する。

$変数名 := 値

配列の値だけでなく、インデックスを一緒に出力したい場合などに使用する。

{{range $i, $x := .}} {{$i}}:{{$x}} {{end}}

関数

  • bool値の評価にnot, and, orを利用できる。
  • not 引数

    引数のbool値をnot演算した結果を返す。

    and 引数1 引数2

    引数1引数2のbool値をand演算した結果を返す。

    or 引数1 引数2

    引数1引数2のbool値をor演算した結果を返す。

  • 配列、Slice、Mapのサイズ取得にlen任意のインデックスの値の取得にindexを利用できる。
  • len 引数

    引数のサイズを返す。

    index 引数1 引数2

    引数1のインデックス[引数2]の値を返す。

  • <や"などhtmlタグに用いる文字列をエスケープするためにhtmlを利用できる。
  • html 引数
    値 | html

    どちらも出力結果は同じ。
    |により、|の左側で評価された値が右側の関数の第2引数として用いられる。

  • fmtパッケージによるフォーマット変更にprint, printf, printlnを利用できる。
  • print 引数1 引数2 ...
    printf 引数1 引数2 ...
    println 引数1 引数2 ...
    値 | print 引数1

    fmtパッケージの各Print系関数を利用する。
    引数1にはフォーマット、引数2以降には出力対象の値を指定する。
    使用できるフォーマットはfmtパッケージの各Print系関数と同じ。
    例として以下のように使用する。

    {{printf "%c, %d" .Var1 .Var2}}

  • 構造体や変数に定義された関数を呼び出すためにcallを利用できる。
  • templateではアクションタグ内で四則演算等、何らかのロジックを実行した結果を出力することができない。
    代わりに、templateに渡す変数や構造体に定義した関数を呼び出しその結果を用いることができる。
    以下のようにアクションタグを指定する。

    call 関数名 (引数1) ...

    関数名で指定した関数を引数1に続く引数で呼び出す。
    引数がない場合は引数1以降の部分は省略する。
    変数や構造体には以下のように関数を定義する。

    type FuncStruct struct {
        F func(n int) string
    }
    
    func doSomething(n int) string {
        // do something
    }
    
    func main() {
        s := FuncStruct{doSomething}
        ...
    }
    

    この例で関数doSomethingを呼び出した結果を出力するには以下のようにアクションタグを指定する。

    {{call .F 100}}

    Go言語:templateパッケージでHTML等テキストファイルを読み込む(応用編1)

    templateパッケージでHTML等の任意のテキストファイルを読み込み、出力する方法を説明する。
    応用編ではテンプレート内に埋め込む「アクション」について解説する。
    なお、APIバージョンGo1で説明する。
    バージョン3ではパッケージ名、関数名が異なるので注意。

    概要

    templateパッケージでは、任意のテキストを読み込み、テキストに埋め込まれたタグに対してデータの評価や制御(アクションという)をおこなって出力する。いくつかのアクションの指定により、以下のようなことができる。

    • テンプレートに渡された引数や構造体のフィールドを出力する。
    • テンプレートに渡されたbool値を評価し、出力を制御する。
    • テンプレートに渡された配列、Slice、Mapのデータを繰り返し出力する。

    配列の要素をすべて表示するサンプル

    package main
    
    import (
    	"fmt"
    	"os"
    	"text/template"
    )
    
    const templateString string = `テンプレートの始め
    {{range .}}
    繰り返し処理 {{.}}
    {{end}}
    テンプレートの終わり
    `
    
    func main() {
    	var stringArray []string = []string{"Hello", "World", "!!!!"}
    	var t = template.Must(template.New("html").Parse(templateString))
    
    	if err := t.Execute(os.Stdout, stringArray); err != nil { // 配列を渡す 
    		fmt.Println(err.Error())
    	}
    }
    

    出力結果

    テンプレートの始め
    
    繰り返し処理 Hello
    
    繰り返し処理 World
    
    繰り返し処理 !!!!
    
    テンプレートの終わり
    

    構造体、Mapの要素の表示、if文による条件分岐を用いたサンプル

    package main
    
    import (
    	"fmt"
    	"os"
    	"text/template"
    )
    
    type Struct struct {
        SampleString string
        SampleMap map[string]MapValue
        SampleBool bool
    }
    
    type MapValue struct {
        IntValue int
        StringValue string
    }
    
    const templateString string = `テンプレートの始め
    {{with .}}
        Structが空でなければこの先を出力する
        {{.SampleString}}:フィールドの値を出力
        {{range .SampleMap}}
            Mapの繰り返し処理 {{.IntValue}}: {{.StringValue}}
        {{end}}
        {{if .SampleBool}}
            bool変数がtrueならば出力
        {{else}}
            bool変数がfalseならば出力
        {{end}}
    {{end}}
    テンプレートの終わり
    `
    
    func main() {
        values := Struct{"サンプルテキスト", make(map[string]MapValue), false}
        values.SampleMap["first"] = MapValue{1, "Hello"}
        values.SampleMap["second"] = MapValue{2, "World"}
        values.SampleMap["third"] = MapValue{3, "!!!!"}
    	var t = template.Must(template.New("text").Parse(templateString))
    
    	if err := t.Execute(os.Stdout, values); err != nil { // 構造体を渡す 
    		fmt.Println(err.Error())
    	}
    }
    

    出力結果

    テンプレートの始め
    
        Structが空でなければこの先を出力する
        サンプルテキスト:フィールドの値を出力
        
            Mapの繰り返し処理 1: Hello
        
            Mapの繰り返し処理 2: World
        
            Mapの繰り返し処理 3: !!!!
        
        
            bool変数がfalseならば出力
        
    
    テンプレートの終わり
    

    解説

    アクションタグの種類

    {{.}}

    テンプレートに渡された引数を出力する。

    {{.Member}}

    テンプレートに渡された引数が構造体の場合、Memberフィールドの値を出力する。

    {{if .}} 分岐1 {{else}} 分岐2 {{end}}

    テンプレートに渡された引数のbool値がtrueの場合「分岐1」が出力され、falseの場合「分岐2」が出力される。{{else}}は省略できる。

    {{range .}} {{.}} {{else}} なし {{end}}

    テンプレートに渡された引数が配列、Slice、Mapの場合、lengthが1以上の場合はすべての要素を表示し({{.}}を要素数分繰り返し)、lengthが0の場合には「なし」が表示される。{{else}}は省略できる。

    {{with .}} 分岐1 {{else}} 分岐2 {{end}}

    テンプレートに渡された引数の値が空でない場合「分岐1」が出力され、空の場合「分岐2」が出力される。{{else}}は省略できる。

    応用

    テンプレートを実際のWebアプリケーションに用いる例を紹介する。
    サンプルとしてLegacy-BBSを参照してもらいたい。
    このサンプルは、下記の2つの部分に分かれている。

    • メッセージを投稿するためのフォーム部分
    • 投稿時に名前やメッセージが空白でないかのバリデーションを行い、エラーメッセージを表示する。
      バリデーションの結果を{{if}}で判定し、エラーの表示をおこなう。

    • 投稿されたメッセージを表示する部分
    • データストアに格納されている複数のメッセージを表示する。
      メッセージは構造体Entry_viewの配列に格納し、{{range}}により表示をおこなう。

    構造体とテンプレートのサンプルは下記の通り。
    なお、この例にはこれらの構造体のデータを取得・出力するロジックは省略している。
    ロジックについてはまた別の機会に説明する。

    type View struct {
    	Entries      []Entry_view
    	Errors       Validation
    }
    
    type Entry_view struct {
    	Name    string
    	Email   string
    	Title   string
    	Message string
    	Date    string
    }
    
    type Validation struct {
    	NameError    bool
    	MessageError bool
    }
    
    <html>
    <body>
        <p>Legacy BBS Sample</p>
        {{/* メッセージを投稿するためのフォーム部分 */}}
        <form method="POST" action="">
            {{with .Errors}}
                Submitter
                <input type="text" name="name" /><br />
                {{if .NameError}}<font color="red">Enter a submitter</font><br />{{end}}
                Mail
                <input type="text" name="email" /><br />
                Title
                <input type="text" name="title" /><br />
                Message
                <textarea name="message"></textarea><br />
                {{if .MessageError}}<font color="red">Enter a message</font><br />{{end}}
                <input type="submit" name="submit" value="Submit" />
            {{end}}
        </form>
    
        {{/* 投稿されたメッセージを表示する部分 */}}
        {{range .Entries}}
            <hr />
            <p>
                {{.Title}}  Submitter:
                {{if .Email}}<a href="mailto:{{.Email}}">{{.Name}}</a>
                {{else}}{{.Name}}
                {{end}}
                Date: {{.Date}}
            </p>
            <p>{{.Message}}</p>
        {{end}}
    </body>
    </html>
    

    応用編2ではアクションタグに使用できる関数や変数を説明する。

    Go言語:templateパッケージでHTML等テキストファイルを読み込む(基礎編)

    templateパッケージでHTML等の任意のテキストファイルを読み込み、出力する方法を説明する。
    なお、APIバージョンGo1で説明する。
    バージョン3ではパッケージ名、関数名が異なるので注意。

    概要

    templateパッケージでは、任意のテキストファイルを読み込み、テキストファイルに埋め込まれたプレースホルダに文字列を変換して出力する。例えば、以下のような機能を利用できる。

    • ブラウザからのgetリクエストに対して外部HTMLファイルを読み込み、任意のデータをHTMLに埋め込んでレスポンスとして返す。
    • メールの本文テンプレートとして作成したテキストファイルを読み込み、任意のデータを本文に埋め込んでメールを送信する。

    単にhtmlファイルを読み込んで出力するだけのサンプル

    package main
    
    import (
        "os"
        "fmt"
        "text/template"
    )
    
    func main() {
    	var t = template.Must(template.ParseFiles("plainSample.html")) // 外部テンプレートファイルの読み込み
        
    	if err := t.Execute(os.Stdout, nil); err != nil { // テンプレート出力
            fmt.Println(err.Error())
        }
    }

    plainSample.html

    <html>
        <head>
            <title>Sample Html</title>
        </head>
        <body>
            <h1>Htmlサンプル</h1>
            サンプルテキスト
        </body>
    </html>

    出力結果

    <html>
        <head>
            <title>Sample Html</title>
        </head>
        <body>
            <h1>Htmlサンプル</h1>
            サンプルテキスト
        </body>
    </html>

    プレースホルダを使用した場合のサンプル

    package templatesample
    
    import (
        "fmt"
        "net/http"
        "text/template"
    )
    
    type Member struct {
        Name string
        Message string
    }
    
    func init() {
        http.HandleFunc("/", handler)
    }
    
    func handler(w http.ResponseWriter, r *http.Request) {
        member := Member{"inatus", "はじめまして。<br>よろしくお願いいたします。"}
        var t = template.Must(template.ParseFiles("placeholderSample.html")) // 外部テンプレートファイルの読み込み
        
    	if err := t.Execute(w, member); err != nil { // テンプレート出力
            fmt.Println(err.Error())
        }
    }
    

    placeholderSample.html

    <html>
        <head>
            <title>Sample Html</title>
        </head>
        <body>
            <h1>Htmlサンプル</h1>
            <h3>{{.Name}}</h3>
            {{.Message}}
        </body>
    </html>

    ブラウザ表示結果

    出力をstring型のオブジェクトに格納するサンプル

    
    package main
    
    import (
        "fmt"
        "bytes"
        "text/template"
    )
    
    type Member struct {
        Name string
        Message string
    }
    
    func main() {
        member := Member{"inatus", "はじめまして。<br>よろしくお願いいたします。"}
        var body bytes.Buffer
        var t = template.Must(template.ParseFiles("placeholderSample.html")) // 外部テンプレートファイルの読み込み
        
    	if err := t.Execute(&body, member); err != nil { // テンプレート出力
            fmt.Println(err.Error())
        }
    
        var str string = body.String()
    }
    

    解説

    パッケージ

    下記の2種がある。

    • text/template: 汎用テンプレート用のパッケージ。
    • html/template: プレースホルダに変換対象の文字列のhtmlタグをエスケープする。サニタイジングに有効。

    インポートするパッケージをhtml/templateに変更して上記の「プレースホルダを使用した場合のサンプル」を実行した結果は以下の通り。

    内部的には<&lt;>&gt;といったように文字列変換される。

    テンプレートの読み込み

    var t = template.Must(template.ParseFiles("placeholderSample.html"))

    template.ParseFiles関数の引数で渡すパスにあるファイルを読み込む。
    パスではなくstringをそのまま引数に渡すこともできる。

    var t = template.Must(template.New("html").Parse("<html><body>サンプル</body></html>"))

    プレースホルダ変換文字列の指定と出力

    if err := t.Execute(w, member); err != nil { // テンプレート出力
        fmt.Println(err.Error())
    }

    上記のテンプレート読み込みの出力変数に対しExecute関数を実行する。
    1つ目の引数には出力先のio.Writerインタフェイスを指定する。io.Writerインタフェイスには例えば下記のようなものがある。

    • http.ResponseWriter: httpレスポンスへの出力
    • os.Stdout: 標準出力
    • bytes.Buffer: 変数への出力

    2つ目の引数にはプレースホルダへの変換対象となる文字列を含んだ変数(通常は構造体)を指定する。読み込んだテンプレートのプレースホルダ名({{.Variable}})は必ず構造体のメンバ名として存在しなければならない。

    応用編では使用できるプレースホルダのバリエーションを解説する。

    Go言語でWebアプリケーションを作成する

    githubレポジトリに公開しているSpring MVCで実装したLeagacyなBBSサンプルと同じ物をGo言語で実装してみた。
    ポイントはどこまでSpring MVCに肉薄できるか。


    Go言語実装のLegacyBBSサンプルのgithubレポジトリ
    Google App Engineにデプロイ済みサンプル

    次回以降、数回に分けて要素技術を紹介していく。