概要
Go言語のWeb Application FrameworkであるRevelを使ってHello World
を表示するアプリケーションを開発した内容を簡単にまとめました。
目標
アプリケーションの開発手順と実装する機能は下記の通りです。
Revelの機能やその使い方を確認することに重点を置きましたのでアプリケーションの実用性はあまりありません。
- [Step1] アプリケーションの雛形を作成
- [Step2] Hello Worldを表示するページ
- [Step3] 検索結果を表示するページ
- [Step4] 検索結果をjsonで返すAPI
- [Step5] エラーページの表示
- [Step6] jobの実行機能
環境
- Windows7 (64bit)
- [The Go Programming Language 1.4.2] (http://golang.org/)
- [Revel 0.12.0] (https://revel.github.io/)
- [IntelliJ IDEA 14.1.2 Community Edition] (https://www.jetbrains.com/idea/)
Revelのインストール
> go get -u -v github.com/revel/revel
> go get -u -v github.com/revel/cmd/revel
参考サイト
この記事の作成に関して下記のサイトを参考にしました。
Revel
- [Manual] (https://revel.github.io/manual/index.html)
- [GoDoc] (https://godoc.org/github.com/robfig/revel)
Qiita
- [「最速」フルスタックWebフレームワーク「revel」の紹介] (http://qiita.com/2KB/items/7a743033d8d1a87f1b72)
- [Go言語製のWeb Application Framework - Revel - を触る] (http://qiita.com/futoase/items/8134bd96a5c950c497c7)
Blog
- [Richard Clayton - Revel, GORP, and MySQL] (https://rclayton.silvrback.com/revel-gorp-and-mysql)
Github
ソースコードは[rubytomato/rhw] (https://github.com/rubytomato/rhw)にあります。
アプリケーションの開発
アプリケーション名はrhw
(Revel de Hello World)としました。
開発は雛形の作成からはじめて徐々に機能を増やしていく方法をとります。
[Step1] アプリケーションの雛形を作成
revel new
コマンドで雛形を作成します。
> revel.exe new rhw
アプリケーションの実行
revel run
コマンドで雛形のアプリケーションを実行することができます。
アプリケーションが起動したら下記のURLにアクセスしページが表示されることを確認します。
アプリケーションの停止はコンソールからCtrl + C
で行います。
> revel.exe run rhw
> revel.exe help run
~
~ revel! http://revel.github.io
~
usage: revel run [import path] [run mode] [port]
Run the Revel web application named by the given import path.
For example, to run the chat room sample application:
revel run github.com/revel/samples/chat dev
The run mode is used to select which set of app.conf configuration should
apply and may be used to determine logic in the application itself.
Run mode defaults to "dev".
You can set a port as an optional third parameter. For example:
revel run github.com/revel/samples/chat prod 8080
アプリケーションのビルド
revel build
コマンドはアプリケーションの実行可能なバイナリファイルを生成します。(Windowsだとexeファイル)
ビルドをする前にビルドで使用するテンポラリーフォルダを作成します。
この例ではC:/tmp
に作成しました。
> revel.exe build rhw "c:/tmp/rhw"
usage: revel build [import path] [target path]
Build the Revel web application named by the given import path.
This allows it to be deployed and run on a machine that lacks a Go installation.
WARNING: The target path will be completely deleted, if it already exists!
For example:
revel build github.com/revel/samples/chat /tmp/chat
ビルドが成功するとc:/tmp/rhw
に下記のファイルが生成され、run.batファイルを実行するとwebアプリケーションが起動します。
> dir /b
run.bat
run.sh
src
rhw.exe
run.batの内容
@echo off
rhw.exe -importPath rhw -srcPath %CD%\src -runMode prod
IDEにアプリケーションを取り込む
IntelliJを起動し、Welcome画面からImport Project
→Select File or Directory to Import
画面で上記で作成したプロジェクトフォルダを選択します。
インポート後の状態
インポート後のディレクトリ構成は下記の通りです。
rhw (アプリケーションROOT)
|
+--- /app (アプリケーションのソースコードはここに格納します)
| |
| +--- /controllers (アプリケーションコントローラーはここに格納します)
| | |
| | +--- app.go (newで生成されたサンプルコード)
| |
| +--- /routes
| | |
| | +--- routes.go (revelが自動生成するコード、DO NOT EDIT)
| |
| +--- /tmp
| | |
| | +--- main.go (revelが自動生成するコード、DO NOT EDIT)
| |
| +--- /views (テンプレートファイルはここに格納します)
| | |
| | +--- /App (newで生成されたサンプルテンプレート)
| | | |
| | | +--- Index.html
| | |
| | +--- /errors (エラーページ)
| | | |
| | | +--- 404.html
| | | +--- 500.html
| | |
| | +--- debug.html
| | +--- flash.html
| | +--- footer.html
| | +--- header.html
| |
| +--- init.go (フィルター)
|
+--- /conf
| |
| +--- app.conf (アプリケーションの設定ファイル)
| +--- routes (ルーティングの設定ファイル)
|
+--- /messages (メッセージリソースはここに格納します)
|
+--- messages.en
[Step2] Hello Worldを表示するページ
まず"Hello World"というメッセージを表示するページを開発します。
完成図
Controller
- rhw > /app > /controllers > hello.goを作成
Hello
がコントローラ名、Index()
,Greet()
がアクション名になります。
package controllers
import "github.com/revel/revel"
type Hello struct {
*revel.Controller
}
func (c Hello) Index() revel.Result {
greeting := "Hello World!"
message := "これはStep2の実装です。(その1)"
return c.Render(greeting, message)
}
func (c Hello) Greet(greeting string) revel.Result {
message := "これはStep2の実装です。(その2)"
c.RenderArgs["greeting"] = greeting
c.RenderArgs["message"] = message
return c.RenderTemplate("Hello/Index.html")
}
View
- rhw > /app > /views > /Helloフォルダを作成
- rhw > /app > /views > /Hello > Index.htmlを作成
revelはテンプレートファイルをviewsフォルダ内から探します。
Hello.Index()
に対応するテンプレートファイルはviews/Hello/Index.htmlになります。
{{set . "title" "Hello World"}}
{{set . "description" "Revel Web Framework sample application"}}
{{template "header.html" .}}
{{/* ヘッダー */}}
<header class="page-header">
<div class="container">
<div class="row">
<h1>{{.greeting}}</h1>
<p>{{.message}} ({{len .message}})</p>
</div>
</div>
</header>
{{/* ボディ */}}
<div class="container">
<div class="row">
<div class="span12">
{{template "flash.html" .}}
<div class="well well-small">
<h2>{{msg . "hello.index.title"}}</h2>
<h3>{{msg . "say" "ワールド"}}</h3>
</div>
</div>
</div>
</div>
{{template "footer.html" .}}
データの表示
Hello.Index()
アクションのRender()
メソッドに変数を渡すと、テンプレート内ではその変数名を使ってデータを表示することができます。
<div class="row">
<h1>{{.greeting}}</h1>
<p>{{.message}} ({{len .message}})</p>
</div>
メッセージリソースの利用(i18n)
- rhw > /conf > app.confを修正
デフォルトで使用する言語をja
に変更します。
# The default language of this application.
- i18n.default_language = en
+ i18n.default_language = ja
- rhw > /messages > messages.jaを作成
- rhw > /messages > messages.enを作成
メッセージリソースはmessagesフォルダ内に格納します。ファイルの数やファイル名の付け方に制限はありませんが拡張子はロケールにする必要があります。
この例では日本語用のmessages.ja
と英語用のmessages.en
の2つを作成します。
リソースはkey = value
のように記述します。
valueには%s
や%d
などのプレースフォルダを使って後からデータを埋め込むことができます。%sが文字列、%dが数値に対応します。
また%(key)s
の書式でkeyの部分に他のリソースキーを指定するとそのメッセージリソースを参照します。
日本語用メッセージリソースファイルの拡張子はja
です。
say = ハロー、%s!
hello.index.title = Revel frameworkでハロー、ワールド
英語用メッセージリソースファイルの拡張子はen
です。
say = hello %s!
hello.index.title = Revel framework Hello World
msg
はRevelのTemplateFuncs
マップに事前定義されている関数です。
"say"がメッセージリソースのキーで、次の"ワールド"が%s
にセットされる文字列になります。
<h2>{{msg . "say" "ワールド"}}</h2>
source code
"msg": func(renderArgs map[string]interface{}, message string, args ...interface{}) template.HTML {
str, ok := renderArgs[CurrentLocaleRenderArg].(string)
if !ok {
return ""
}
return template.HTML(Message(str, message, args...))
},
[Revel manual - Interantionalization] (https://revel.github.io/manual/i18n-messages.html)
Route
- rhw > /app > /conf > routesを編集
routesファイルに下記の行を追加します。
1行目は、GETメソッドでlocalhost:9000/hello
へアクセスするとHello.Index()
へルーティングされます。
2行目は、GETメソッドで、例えばlocalhost:9000/greet/こんにちは
へアクセスするとHello.Greet()
へルーティングされ、URLの"こんにちは"の部分がGreet()アクションの引数に設定されます。
+ GET /hello Hello.Index
+ GET /greet/:greeting Hello.Greet
Greet()
アクションの(規約的な)デフォルトのテンプレートは/views/Hello/Greet.html
になりますがRenderTemplate()で任意のテンプレートファイルを使用することができます。
この例では、Hello.Index()
アクションと同じテンプレートファイルを使用しています。
return c.RenderTemplate("Hello/Index.html")
Catch all
routesファイルの最下行に下記の記述がありますが、これによりURLにコントローラー名とアクション名を使用することができます。
例えば/hello
の他に/Hello/Index
でもHello.Index()
へルーティングします。
# Catch all
* /:controller/:action :controller.:action
[Revel manual - URL routing] (https://revel.github.io/manual/routing.html)
これで「STEP2 Hello Worldを表示するページ」の実装は終わりです。
localhost:9000/hello
にアクセスすると"Hello World"が表示されると思います。
[Step3] 検索結果を表示するページ
Step3では、海外ドラマのエピソードを検索するページと検索条件を満たすエピソードの一覧ページを作成します。
完成図
Form
- rhw > /app > /formsフォルダを作成
- rhw > /app > /forms > searchForm.goを作成
パッケージはforms
とし検索フォームの値を受け取る型を定義します。フィールドのデータタイプは全てstringにします。
また、この型に入力値を検証するValidationメソッドを実装します。
package forms
import (
"regexp"
"strconv"
"strings"
"github.com/revel/revel"
)
type SearchForm struct {
Title string //タイトル
Runtime string //放送時間
OriginalAirDate string //放送日
GuestStaring string //ゲスト出演
}
func (s SearchForm) Validate(v *revel.Validation, locale string) {
if str := strings.Trim(s.Title, " "); str != "" {
v.MinSize(str, 6).Message(revel.Message(locale, "search.form.validate.title.min", 6)).Key("s.Title")
}
if str := strings.Trim(s.Runtime, " "); str != "" {
rt, err := strconv.Atoi(str)
if err != nil {
v.Error(revel.Message(locale, "search.form.validate.runtime.number")).Key("s.Runtime")
} else {
v.Range(rt, 50, 120).Message(revel.Message(locale, "search.form.validate.runtime.range")).Key("s.Runtime")
}
}
if str := strings.Trim(s.OriginalAirDate, " "); str != "" {
v.Match(str, regexp.MustCompile("^(January|February|March|April|May|June|July|August|September|October|November|December).*$")).Message(revel.Message(locale, "search.form.validate.originalairdate.match")).Key("s.OriginalAirDate")
}
}
Validation
Revelに用意されているValidationメソッドには下記に引用した種類があります。ここではCheck
とError
の使い方の補足をします。
Check
は1つのフィールドに対して複数のバリデーションをまとめて行ったり、独自のバリデータを使って検証することができます。
v.Check(s.Title,
revel.ValidRequired(),
revel.ValidMinSize(6)).Message("タイトルのエラーです").Key("s.Title")
独自バリデータを作成するにはValidatorインターフェースを実装します。
source
type Validator interface {
IsSatisfied(interface{}) bool
DefaultMessage() string
}
Error
は、検証を行わずにエラーを追加します。
v.Error("タイトルのエラーです").Key("s.Title")
GoDoc
|method |definition |
|:--------|:-------------------------------------------------------------------------|
|Required |func (v *Validation) Required(obj interface{}) *ValidationResult
|
|Min |func (v *Validation) Min(n int, min int) *ValidationResult
|
|Max |func (v *Validation) Max(n int, max int) *ValidationResult
|
|MinSize |func (v *Validation) MinSize(obj interface{}, min int) *ValidationResult
|
|MaxSize |func (v *Validation) MaxSize(obj interface{}, max int) *ValidationResult
|
|Range |func (v *Validation) Range(n, min, max int) *ValidationResult
|
|Length |func (v *Validation) Length(obj interface{}, n int) *ValidationResult
|
|Match |func (v *Validation) Match(str string, regex *regexp.Regexp) *ValidationResult
|
|Email |func (v *Validation) Email(str string) *ValidationResult
|
|Check |func (v *Validation) Check(obj interface{}, checks ...Validator) *ValidationResult
|
|Error |func (v *Validation) Error(message string, args ...interface{}) *ValidationResult
|
[Revel manual - validation] (https://revel.github.io/manual/validation.html)
Model
- rhw > /app > /modelsフォルダを作成
- rhw > /app > /models > episodes.goを作成
パッケージはmodels
とし海外ドラマのエピソードのデータを持つ型を定義します。jsonで出力することを考えてjsonタグを付けます。
package models
import (
"fmt"
"time"
)
type Episodes struct {
Title string `json:"title"`
OriginalAirDate string `json:"original_air_date"`
Runtime int `json:"runtime"`
GuestStaring string `json:"guest_staring"`
GuestStaringRole string `json:"guest_staring_role"`
DirectedBy string `json:"directed_by"`
WrittenBy []string `json:"written_by"`
Teleplay []string `json:"teleplay"`
Season int `json:"season"`
NoInSeason int `json:"no_in_season"`
NoInSeries int `json:"no_in_series"`
JapaneseTitle string `json:"japanese_title"`
JapaneseAirDate time.Time `json:"japanese_air_date"`
}
func (c *Episodes) toString() string {
return fmt.Sprintf("%+v", c)
}
サンプルデータの準備
このアプリケーションで使用するサンプルデータを準備します。
本来ならDBよりデータを検索するところですが記事が長くなるため省略します。その代わりにサンプルデータをinit関数で配列に格納しておきます。下記のサンプルデータのコードは一部分です。全文はページ末尾に記載しました。
var episodes []*models.Episodes
func init() {
lc, _ := time.LoadLocation("Asia/Tokyo")
episodes = []*models.Episodes{
&models.Episodes{Title: "Prescription: Murder", OriginalAirDate: "February 20, 1968", Runtime: 98, GuestStaring: "Gene Barry", GuestStaringRole: "Dr. Ray Fleming (Gene Barry), a psychiatrist", DirectedBy: "Richard Irving", WrittenBy: []string{"Richard Levinson & William Link"}, Teleplay: []string{""}, Season: 0, NoInSeason: 1, NoInSeries: 1, JapaneseTitle: "殺人処方箋", JapaneseAirDate: time.Date(1972, 8, 27, 0, 0, 0, 0, lc)},
...省略...
}
}
Controller
- rhw > /app > /controllers > columbo.goを作成
ColumboコントローラーにSearch()
,Confirm()
,Result()
アクションを追加します。
package controllers
import (
"strconv"
"strings"
"time"
"github.com/revel/revel"
"rhw/app/forms"
"rhw/app/models"
"rhw/app/routes"
)
type Columbo struct {
*revel.Controller
}
func (c Columbo) Search(s forms.SearchForm) revel.Result {
message := "これはStep3の実装です。"
return c.Render(s, message)
}
func (c Columbo) Confirm(s forms.SearchForm) revel.Result {
s.Validate(c.Validation, c.Request.Locale)
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(routes.Columbo.Search(s))
}
return c.Redirect(routes.Columbo.Result(s))
}
func (c Columbo) Result(s forms.SearchForm) revel.Result {
lists := []*models.Episodes{}
unmatch := false
for _, v := range episodes {
unmatch = false
if s.Title != "" {
if !isContains(v.Title , s.Title) {
unmatch = true
}
}
if s.Runtime != "" {
runtime, err := strconv.Atoi(s.Runtime)
if err != nil {
unmatch = true
} else {
if v.Runtime != runtime {
unmatch = true
}
}
}
if s.OriginalAirDate != "" {
if !isContains(v.OriginalAirDate , s.OriginalAirDate) {
unmatch = true
}
}
if s.GuestStaring != "" {
if !isContains(v.GuestStaring, s.GuestStaring) {
unmatch = true
}
}
if !unmatch {
lists = append(lists, v)
}
}
return c.Render(s, lists)
}
func isContains(v string, s string) bool {
if s == "" {
return false
}
if v == "" {
return false
}
return strings.Contains(strings.ToLower(v),strings.ToLower(s))
}
View
ナビゲーションリンク
- rhw > /app > /views > navi.htmlを作成
- rhw > /app > /views > /Hello > Index.htmlを修正
各ページでインクルードするナビゲーションリンクページを作成します。
aタグのhrefに記述する{{url ...}
はRevelのTemplateFuncs
マップに事前定義されているReverseUrl
という関数です。
引数にコントローラー名とアクション名を指定するとroutesファイルからurlを逆引きしてくれます。
func ReverseUrl(args ...interface{}) (string, error)
<div class="container">
<div class="row">
<div class="span12">
<ul class="nav nav-pills">
<li><a href="{{url "Hello.Index"}}">Hello World</a></li>
<li><a href="{{url "Hello.Greet" "コンニチハ"}}">Hello World(2)</a></li>
<li><a href="{{url "Columbo.Search"}}">Columbo Search</a></li>
</ul>
</div>
</div>
</div>
下記がレンダリングされたhtmlコードです。
<div class="container">
<div class="row">
<div class="span12">
<ul class="nav nav-pills">
<li><a href="/hello">Hello World</a></li>
<li><a href="/greet/%e3%82%b3%e3%83%b3%e3%83%8b%e3%83%81%e3%83%8f">Hello World(2)</a></li>
<li><a href="/columbo/search">Columbo Search</a></li>
</ul>
</div>
</div>
</div>
Index.htmlのheaderとdivの間にnavi.htmlをインクルードするように修正します。
templateアクションの引数にインクルードしたいテンプレートファイル名を指定します。
</div>
</header>
+
+ {{template "navi.html" .}}
+
<div class="container">
<div class="row">
検索フォームページ
- rhw > /app > /views > /Columboフォルダを作成
- rhw > /app > /views > /Columbo > Search.htmlを作成
- rhw > /app > /views > /Columbo > Result.htmlを作成
検索フォームを表示するページ
{{set . "title" "Search"}}
{{set . "description" "Revel Web Framework sample application"}}
{{template "header.html" .}}
<header class="page-header">
<div class="container">
<div class="row">
<h1>Episodes Search</h1>
<p>{{.message}} ({{len .message}})</p>
</div>
</div>
</header>
{{template "navi.html" .}}
<div class="container">
<div class="row">
<div class="span12">
{{template "flash.html" .}}
<div class="well well-small">
<h2>{{msg . "search.form.title"}}</h2>
<form action="/columbo/confirm" method="GET" name="search" class="form-horizontal" role="form">
<!-- s.Title -->
<div class="control-group">
{{with $field := field "s.Title" .}}
<label class="control-label" for="{{$field.Id}}">タイトル</label>
<div class="controls {{$field.ErrorClass}}">
<input type="text" class="form-control" id="{{$field.Id}}" name="{{$field.Name}}" value="{{$field.Value}}"/>
{{if $field.Error}}<span class="text-error">*{{$field.Error}}</span>{{end}}
</div>
{{end}}
</div>
<!-- s.Runtime -->
<div class="control-group">
{{with $field := field "s.Runtime" .}}
<label class="control-label" for="{{$field.Id}}">放送時間</label>
<div class="controls {{$field.ErrorClass}}">
<select name="{{$field.Name}}" id="{{$field.Id}}">
<option value="">---</option>
{{option $field "73" "73分"}}
{{option $field "85" "85分"}}
{{option $field "98" "98分"}}
</select>
{{if $field.Error}}<span class="text-error">*{{$field.Error}}</span>{{end}}
</div>
{{end}}
</div>
<!-- s.OriginalAirDate -->
<div class="control-group">
{{with $field := field "s.OriginalAirDate" .}}
<label class="control-label" for="{{$field.Id}}">放送月</label>
<div class="controls {{$field.ErrorClass}}">
<select name="{{$field.Name}}" id="{{$field.Id}}">
<option value="">---</option>
{{option $field "January" "1月"}}
{{option $field "February" "2月"}}
{{option $field "March" "3月"}}
{{option $field "April" "4月"}}
{{option $field "May" "5月"}}
{{option $field "June" "6月"}}
{{option $field "July" "7月"}}
{{option $field "August" "8月"}}
{{option $field "September" "9月"}}
{{option $field "October" "10月"}}
{{option $field "November" "11月"}}
{{option $field "December" "12月"}}
</select>
{{if $field.Error}}<span class="text-error">*{{$field.Error}}</span>{{end}}
</div>
{{end}}
</div>
<!-- s.GuestStaring -->
<div class="control-group">
{{with $field := field "s.GuestStaring" .}}
<label class="control-label" for="{{$field.Id}}">ゲスト出演</label>
<div class="controls {{$field.ErrorClass}}">
<input type="text" class="form-control" id="{{$field.Id}}" name="{{$field.Name}}" value="{{$field.Value}}"/>
{{if $field.Error}}<span class="text-error">*{{$field.Error}}</span>{{end}}
</div>
{{end}}
</div>
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-default" value='{{msg . "search.form.submit"}}'/>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{template "footer.html" .}}
入力フィールドのヘルパー
field
という入力フィールドのヘルパー関数が用意されています。
このヘルパー関数を使用すると、たとえばバリデーションエラーが起きたら入力ページへリダイレクトし入力フォームから再入力を促すといった場合に、「直前の入力値を入力フィールドへ初期値としてセット」したり、エラーを起こした「フィールド毎にエラーメッセージをそのフィールドの近くに表示する」といったことが簡単にできます。
(with
はGolangのhtml/template
パッケージのアクションです。$filedの値が空でなければブロックが実行されます。)
下記の's.Title'というフィールドの例で説明すると、"abc"という値の入力が文字列長が足りずにバリデーションエラーになった場合
{{with $field := field "s.Title" .}}
を実行すると$field
には下記の情報がセットされます。
-
{{$field.Name}}
には、"s.Title"という文字列がセットされています。(これはfield関数の1つ目の引数の値です) -
{{$field.Id}}
には、"s_Title"という文字列がセットされています。 -
{{$field.Flash}}
には、(Flashにセットした場合に)入力値の"abc"が格納されています。 -
{{$field.Value}}
には、(renderArgsにセットした場合に)入力値の"abc"が格納されています。 -
{{$field.Error}}
には、バリデーション時に設定したエラーメッセージが格納されています。 -
{{$field.ErrorClass}}
には、"hasError"という文字列がセットされています。エラーでなければ""です。
<div class="control-group">
{{with $field := field "s.Title" .}}
<label class="control-label" for="{{$field.Id}}">タイトル</label>
<div class="controls {{$field.ErrorClass}}">
<input type="text" class="form-control" id="{{$field.Id}}" name="{{$field.Name}}" value="{{$field.Value}}"/>
{{if $field.Error}}<span class="text-error">*{{$field.Error}}</span>{{end}}
</div>
{{end}}
</div>
レンダリングされたhtmlコードは下記の通りです。
<div class="control-group">
<label class="control-label" for="s_Title">タイトル</label>
<div class="controls hasError">
<input type="text" class="form-control" id="s_Title" name="s.Title" value="abc">
<span class="text-error">*タイトルは最低6文字以上を入力してください。</span>
</div>
</div>
エラー発生時のスクリーンショット
field
の実体はRevelのTemplateFuncs
マップに事前定義されているNewField
という関数で、nameとrenderArgsを引数に取りField型のポインタを返します。
func NewField(name string, renderArgs map[string]interface{}) *Field
source code
func NewField(name string, renderArgs map[string]interface{}) *Field {
err, _ := renderArgs["errors"].(map[string]*ValidationError)[name]
return &Field{
Name: name,
Error: err,
renderArgs: renderArgs,
}
}
Field型にはいくつかのメソッドが定義されています。
source code
type Field struct {
Name string
Error *ValidationError
// contains filtered or unexported fields
}
GoDoc
|name |definition |description|
|:------------|:------------------------------------------|:----------|
|ErrorClass() |func (f *Field) ErrorClass() string
| the raw string "hasError", if there was an error, else "".|
|Flash() |func (f *Field) Flash() string
|the flash value of the field.|
|FlashArray() |func (f *Field) FlashArray() []string
||
|Id() |func (f *Field) Id() string
|the field name, converted to be suitable as a HTML element ID.|
|Value() |func (f *Field) Value() interface{}
|the value of the field in the current RenderArgs|
検索結果を表示するページ
{{set . "title" "Result"}}
{{set . "description" "Revel Web Framework sample application"}}
{{template "header.html" .}}
<header class="page-header">
<div class="container">
<div class="row">
<h1>Episodes Search Result</h1>
</div>
</div>
</header>
{{template "navi.html" .}}
<div class="container">
<div class="row">
<div class="span12">
{{template "flash.html" .}}
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="span12">
{{with $list := .lists}}
<p>件数:{{len $list}}</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>idx</th>
<th>放送回</th>
<th>シーズン</th>
<th>放送日</th>
<th>タイトル(邦題)</th>
<th>放送時間</th>
<th>日本放送日</th>
</tr>
</thead>
<tbody>
{{range $index, $elem := $list}}
<tr>
<td>{{$index}}</td>
<td>{{$elem.NoInSeries}}</td>
<td>{{$elem.Season}} / {{$elem.NoInSeason}}</td>
<td>{{$elem.OriginalAirDate}}</td>
<td>{{$elem.Title}}<br/>({{$elem.JapaneseTitle}})</td>
<td>{{$elem.Runtime}}分</td>
<td>{{date $elem.JapaneseAirDate}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
</div>
</div>
</div>
{{template "footer.html" .}}
日付フィールドの表示フォーマット
date
はRevelのTemplateFuncs
マップに事前定義されている関数です。
date型のデータをapp.confで設定した書式にフォーマットします。
<td>{{date $elem.JapaneseAirDate}}</td>
app.confを修正して日付の表示フォーマットを変更します。
# The date format used by Revel. Possible formats defined by the Go `time`
# package (http://golang.org/pkg/time/#Parse)
- format.date = 01/02/2006
+ format.date = 2006/01/02
- format.datetime = 01/02/2006 15:04
+ format.datetime = 2006/01/02 15:04
メッセージリソースの追加
messages.ja
に下記のメッセージリソースを追加します。
search.form.title = エピソードサーチ
search.form.submit = 検索
search.form.validate.title.required = タイトルは必須です。
search.form.validate.title.min = タイトルは最低%d文字以上を入力してください。
search.form.validate.runtime.number = 放送時間は数値を入力してください。
search.form.validate.runtime.range = 放送時間は50から120の範囲で指定してください。
search.form.validate.originalairdate.match = 放送月の指定が不正です。
Route
- rhw > /app > /conf > routesを編集
routesファイルに下記の行を追加します。
GET /columbo/search Columbo.Search
GET /columbo/confirm Columbo.Confirm
GET /columbo/result Columbo.Result
[Revel manual - Templates] (https://revel.github.io/manual/templates.html)
これで「STEP3 検索結果を表示するページ」の実装は終わりです。
localhost:9000/columbo/search
にアクセスするとエピソード検索ページが表示されると思います。
[Step4] 検索結果をjsonで返すAPI
放送回を指定しそのエピソードをjsonで返すAPIを実装します。
完成
{
title: "Prescription: Murder",
original_air_date: "February 20, 1968",
runtime: 98,
guest_staring: "Gene Barry",
guest_staring_role: "Dr. Ray Fleming (Gene Barry), a psychiatrist",
directed_by: "Richard Irving",
written_by: [
"Richard Levinson & William Link"
],
teleplay: [
""
],
season: 0,
no_in_season: 1,
no_in_series: 1,
japanese_title: "殺人処方箋",
japanese_air_date: "1972-08-27T00:00:00+09:00"
}
Controller
- rhw > /app > /controllers > columbo.goを修正
レスポンスをjsonにするにはコントローラーのRenderJson
メソッドを使用します。
func (c Columbo) ResultJSON(no int) revel.Result {
if no <= 0 {
no = 1
}
obj := &models.Episodes{}
for _, v := range episodes {
if v.NoInSeries == no {
obj = v
break
}
}
return c.RenderJson(obj)
}
jsonの表示形式
results.pretty
をtrueに設定すると見やすく整形して表示します。
results.pretty = true
Route
- rhw > /app > /conf > routesを編集
routesファイルに下記の行を追加します。
GET /columbo/json/:no Columbo.ResultJSON
urlパターンの:no
の部分に放送回数を指定します。
たとえば、localhost:9000/columbo/json/20
にアクセスすると放送20回目のエピソードをjsonで取得できます。
[Revel manual - app.conf] (https://revel.github.io/manual/appconf.html)
View
- rhw > /app > /views > navi.htmlを修正
ついでにテストしやすいようにリンクを追加しておきます。
<ul class="nav nav-pills">
<li><a href="{{url "Hello.Index"}}">Hello World</a></li>
<li><a href="{{url "Hello.Greet" "コンニチハ"}}">Hello World(2)</a></li>
<li><a href="{{url "Columbo.Search"}}">Columbo Search</a></li>
+ <li><a href="{{url "Columbo.ResultJSON" 1}}">JSON</a></li>
</ul>
これで「STEP4 検索結果をjsonで返すAPI」の実装は終わりです。
[Step5] エラーページ
カスタマイズしたエラーページを実装します。
ここではカスタマイズ方法や機能を説明することを目的としましたのでデザインや文言は気にしないでください。
Controller
- rhw > /app > /controllers > hello.goを編集
エラーページを表示するのに用意されているcontrollerのメソッドには下記のものがあります。
-
RenderError()
は、Http status 500を返します。 -
NotFound()
は、Http status 404を返します。 -
Forbidden()
は、Http status 403を返します。
これら以外のHttp statusコードを返したい場合はコントローラーのResponse.Status
に任意のコードを設定し、RenderTemplate()
にテンプレートファイルを指定します。
func (c Hello) HelloServerError() revel.Result {
err := errors.New("Hello ServerError!")
return c.RenderError(err)
}
func (c Hello) HelloNotFound() revel.Result {
time := time.Now()
msg := "Error Message"
return c.NotFound("Hello NotFound!, %s, [%s]", msg, time.String())
}
func (c Hello) HelloForbidden() revel.Result {
return c.Forbidden("Hello Forbidden!")
}
func (c Hello) HelloCustomError() revel.Result {
c.Response.Status = http.StatusBadRequest
c.RenderArgs["custom"] = "カスタムエラー"
return c.RenderTemplate("errors/custom.html")
}
View
Http statusコードとテンプレートファイルのファイル名が対応しています。
アプリケーションの雛形を生成したときに404.html、500.htmlも生成されていますが、その他に下記のテンプレートを作成します。ファイル名に'-dev'とついているものはdevモードで実行されたときに使用される開発用テンプレートファイルです。(revelの機能というより、生成されたテンプレートにそのような工夫がされていたので今回利用しました。)
- rhw > /app > /views > /errors > 403.htmlを作成
- rhw > /app > /views > /errors > 403-dev.htmlを作成
- rhw > /app > /views > /errors > 404-dev.htmlを作成
- rhw > /app > /views > /errors > 500-dev.htmlを作成
- rhw > /app > /views > /errors > custom.htmlを作成
<!DOCTYPE html>
<html lang="en">
<head>
<title>Forbidden</title>
</head>
<body>
{{if eq .RunMode "dev"}}
{{template "errors/403-dev.html" .}}
{{else}}
{{with .Error}}
<h1>
{{.Title}}
</h1>
<p>
{{.Description}}
</p>
{{end}}
{{end}}
</body>
</html>
<h1>[DEV-MODE] Forbidden</h1>
<div>
<h2>{{.Error}}</h2>
<div>
<dl>
{{range $index, $elem := .}}
<dt>{{$index}}</dt>
<dd>{{$elem}}</dd>
{{end}}
</dl>
</div>
</div>
<h1>[DEV-MODE] NotFound</h1>
<div>
<h2>{{.Error}}</h2>
<div>
<dl>
{{range $index, $elem := .}}
<dt>{{$index}}</dt>
<dd>{{$elem}}</dd>
{{end}}
</dl>
</div>
</div>
<h1>[DEV-MODE] Internal Server Error</h1>
<div>
<h2>{{.Error}}</h2>
<div>
<dl>
{{range $index, $elem := .}}
<dt>{{$index}}</dt>
<dd>{{$elem}}</dd>
{{end}}
</dl>
</div>
</div>
<!DOCTYPE html>
<html>
<head>
<title>Application error</title>
</head>
<body>
<h1>Custom error page</h1>
<div>
<h2>{{.custom}}</h2>
<div>
<dl>
{{range $index, $elem := .}}
<dt>{{$index}}</dt>
<dd>{{$elem}}</dd>
{{end}}
</dl>
</div>
</div>
</body>
</html>
Routes
- rhw > /app > /conf > routesを編集
エラーページを確認するためにルーティングを追加します。
* /hello/error Hello.HelloServerError
* /hello/notfound Hello.HelloNotFound
* /hello/forbidden Hello.HelloForbidden
* /error Hello.HelloCustomError
これで「STEP5 エラーページの表示」の実装は終わりです。
通常はエラーページ用のアクションは作らないと思いますがエラーページの特徴を簡単に確認するためにこのような内容になりました。
[Step6] jobの実行機能
jobモジュールの有効化
- rhw > /app > /conf > app.confを修正
- rhw > /app > /conf > routesを修正
app.confとroutesファイルを修正してjobモジュールを有効にします。
+ # http://revel.github.io.manual/jobs.html
+ module.jobs = github.com/revel/modules/jobs
# ~~~~
module:testrunner
+ module:jobs
GET / App.Index
jobの設定
app.confでjobの設定を行うことができます。
jobs.pool = 10
jobs.selfconcurrent = false
jobs.acceptproxyaddress = false
また、実行スケジュールに任意の名前をつけて管理することができます。
スケジュールはcron形式です。
cron.every_1h = 0 0 * ? * ?
cron.every_10m = 0 */10 * ? * ?
CRON Expression Format
A cron expression represents a set of times, using 6 space-separated fields.
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun",
and "sun" are equally accepted.
jobの実装
- rhw > /app > /jobsフォルダを作成
- rhw > /app > /jobs > myjob.goファイルを作成
独自jobを開発するにはjobインターフェースを実装します。
source code
type Job interface {
Run()
}
jobインターフェースを実装しなくてもjobs.Func()
を使用することでスケジューリングできます。
source code
type Func func()
独自jobの実装
パッケージはjobs
にします。
jobで実行したい処理はRunメソッドに記述します。今回の例では実行タイミングの確認を目的としますのでfmt.Printf()のみにしました。
package jobs
import (
"fmt"
"time"
"github.com/revel/modules/jobs/app/jobs"
"github.com/revel/revel"
)
type MyJob struct {
Name string
}
func (j MyJob) Run() {
fmt.Printf("MyJob: %s %s\n", j.Name, time.Now().String())
}
func reminder() {
fmt.Printf("reminder: every 1m : %s \n", time.Now().String())
}
func init() {
revel.OnAppStart(func() {
jobs.Schedule("0 */5 * ? * ?", MyJob{Name: "job1"})
jobs.Schedule("cron.every_1h", MyJob{Name: "job2"})
jobs.Schedule("cron.every_10m", MyJob{Name: "job3"})
jobs.Schedule("@every 1m", jobs.Func(reminder))
jobs.Now(MyJob{Name: "job Now"})
})
}
jobをスケジューリングして実行する方法
指定間隔で実行
jobs.Every(time.Hour, MyJob{Name: "JOB Every1H"})
cronで実行間隔を指定
jobs.Schedule("0 0 0 * * ?", MyJob{Name: "JOB Every1D"})
jobs.Schedule("cron.everyhours", MyJob{Name: "JOB Every1H"})
jobを1回だけ実行する方法
即時実行
jobs.Now(MyJob{Name:"JOB JUST NOW"})
指定時間後に実行
jobs.In(10 * time.Minute, MyJob{Name:"JOB AFTER 10m"})
スケジュールされたjobのステータス
ブラウザベースのjobステータス確認ページがあります。
[Revel manual - Jobs] (https://revel.github.io/manual/jobs.html)
[GoDoc - package jobs] (https://godoc.org/github.com/robfig/revel/modules/jobs/app/jobs)
これで「STEP6 jobの実行機能」の実装は終わりです。
付録
サンプルデータの準備
下記にサンプルデータを登録する処理の全文を記述します。
var episodes []*models.Episodes
func init() {
lc, _ := time.LoadLocation("Asia/Tokyo")
episodes = []*models.Episodes{
&models.Episodes{Title: "Prescription: Murder", OriginalAirDate: "February 20, 1968", Runtime: 98, GuestStaring: "Gene Barry", GuestStaringRole: "Dr. Ray Fleming (Gene Barry), a psychiatrist", DirectedBy: "Richard Irving", WrittenBy: []string{"Richard Levinson & William Link"}, Teleplay: []string{""}, Season: 0, NoInSeason: 1, NoInSeries: 1, JapaneseTitle: "殺人処方箋", JapaneseAirDate: time.Date(1972, 8, 27, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Ransom for a Dead Man", OriginalAirDate: "March 1, 1971", Runtime: 98, GuestStaring: "Lee Grant", GuestStaringRole: "Leslie Williams, a brilliant lawyer and pilot", DirectedBy: "Richard Irving", WrittenBy: []string{"Richard Levinson & William Link"}, Teleplay: []string{"Dean Hargrove"}, Season: 0, NoInSeason: 2, NoInSeries: 2, JapaneseTitle: "死者の身代金", JapaneseAirDate: time.Date(1973, 4, 22, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Murder by the Book", OriginalAirDate: "September 15, 1971", Runtime: 73, GuestStaring: "Jack Cassidy", GuestStaringRole: "Ken Franklin is one half of a mystery writing team", DirectedBy: "Steven Spielberg", WrittenBy: []string{"Steven Bochco"}, Teleplay: []string{""}, Season: 1, NoInSeason: 1, NoInSeries: 3, JapaneseTitle: "構想の死角", JapaneseAirDate: time.Date(1972, 11, 26, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Death Lends a Hand", OriginalAirDate: "October 6, 1971", Runtime: 73, GuestStaring: "Robert Culp", GuestStaringRole: "Carl Brimmer, The head of a private detective agency", DirectedBy: "Bernard L. Kowalski", WrittenBy: []string{"RRichard Levinson & William Link"}, Teleplay: []string{""}, Season: 1, NoInSeason: 2, NoInSeries: 4, JapaneseTitle: "指輪の爪あと", JapaneseAirDate: time.Date(1973, 1, 21, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Dead Weight", OriginalAirDate: "October 27, 1971", Runtime: 73, GuestStaring: "Eddie Albert", GuestStaringRole: "Major General Martin Hollister, a retired Marine Corps war hero", DirectedBy: "Jack Smight", WrittenBy: []string{"John T. Dugan"}, Teleplay: []string{""}, Season: 1, NoInSeason: 3, NoInSeries: 5, JapaneseTitle: "ホリスター将軍のコレクション", JapaneseAirDate: time.Date(1972, 9, 24, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Suitable for Framing", OriginalAirDate: "November 17, 1971", Runtime: 73, GuestStaring: "Ross Martin", GuestStaringRole: "Dale Kingston, Art critic", DirectedBy: "Hy Averback", WrittenBy: []string{"Jackson Gillis"}, Teleplay: []string{""}, Season: 1, NoInSeason: 4, NoInSeries: 6, JapaneseTitle: "二枚のドガの絵", JapaneseAirDate: time.Date(1972, 10, 22, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Lady in Waiting", OriginalAirDate: "December 15, 1971", Runtime: 73, GuestStaring: "Susan Clark", GuestStaringRole: "Beth Chadwick, sister of domineering, Bryce", DirectedBy: "Norman Lloyd", WrittenBy: []string{"Barney Slater"}, Teleplay: []string{"Steven Bochco"}, Season: 1, NoInSeason: 5, NoInSeries: 7, JapaneseTitle: "もう一つの鍵", JapaneseAirDate: time.Date(1972, 12, 17, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Short Fuse", OriginalAirDate: "January 19, 1972", Runtime: 73, GuestStaring: "Roddy McDowall", GuestStaringRole: "Roger Stanford, A chemist", DirectedBy: "Edward M. Abrams", WrittenBy: []string{"Lester & Tina Pine", "Jackson Gillis"}, Teleplay: []string{"Jackson Gillis"}, Season: 1, NoInSeason: 6, NoInSeries: 8, JapaneseTitle: "死の方程式", JapaneseAirDate: time.Date(1973, 3, 18, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Blueprint for Murder", OriginalAirDate: "February 9, 1972", Runtime: 73, GuestStaring: "Patrick O'Neal", GuestStaringRole: "Elliot Markham, An architect", DirectedBy: "Peter Falk", WrittenBy: []string{"William Kelley"}, Teleplay: []string{"Steven Bochco"}, Season: 1, NoInSeason: 7, NoInSeries: 9, JapaneseTitle: "パイルD-3の壁", JapaneseAirDate: time.Date(1973, 2, 25, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Étude in Black", OriginalAirDate: "September 17, 1972", Runtime: 98, GuestStaring: "John Cassavetes", GuestStaringRole: "Alex Benedict, The conductor of the Los Angeles Philharmonic Orchestra", DirectedBy: "Nicholas Colasanto", WrittenBy: []string{"Richard Levinson & William Link"}, Teleplay: []string{"Steven Bochco"}, Season: 2, NoInSeason: 1, NoInSeries: 10, JapaneseTitle: "黒のエチュード", JapaneseAirDate: time.Date(1973, 9, 30, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "The Greenhouse Jungle", OriginalAirDate: "October 15, 1972", Runtime: 73, GuestStaring: "Ray Milland", GuestStaringRole: "Jarvis Goodland, An expert in orchids", DirectedBy: "Boris Sagal", WrittenBy: []string{"Jonathan Latimer"}, Teleplay: []string{""}, Season: 2, NoInSeason: 2, NoInSeries: 11, JapaneseTitle: "悪の温室", JapaneseAirDate: time.Date(1973, 5, 27, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "The Most Crucial Game", OriginalAirDate: "November 5, 1972", Runtime: 73, GuestStaring: "Robert Culp", GuestStaringRole: "Paul Hanlon, The general manager of the Los Angeles Rockets football team", DirectedBy: "Jeremy Kagan", WrittenBy: []string{"John T. Dugan"}, Teleplay: []string{""}, Season: 2, NoInSeason: 3, NoInSeries: 12, JapaneseTitle: "アリバイのダイヤル", JapaneseAirDate: time.Date(1973, 6, 24, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Dagger of the Mind", OriginalAirDate: "November 26, 1972", Runtime: 98, GuestStaring: "Richard Basehart", GuestStaringRole: "Actors Nicholas Framer and his wife, Lillian Stanhope", DirectedBy: "Richard Quine", WrittenBy: []string{"Richard Levinson & William Link"}, Teleplay: []string{"Jackson Gillis"}, Season: 2, NoInSeason: 4, NoInSeries: 13, JapaneseTitle: "ロンドンの傘", JapaneseAirDate: time.Date(1973, 7, 29, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Requiem for a Falling Star", OriginalAirDate: "January 21, 1973", Runtime: 73, GuestStaring: "Anne Baxter", GuestStaringRole: "movie star Nora Chandler", DirectedBy: "Richard Quine", WrittenBy: []string{"Jackson Gillis"}, Teleplay: []string{""}, Season: 2, NoInSeason: 5, NoInSeries: 14, JapaneseTitle: "偶像のレクイエム", JapaneseAirDate: time.Date(1973, 8, 26, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "A Stitch in Crime", OriginalAirDate: "February 11, 1973", Runtime: 73, GuestStaring: "Leonard Nimoy", GuestStaringRole: "Cardiac surgeon Dr. Barry Mayfield", DirectedBy: "Hy Averback", WrittenBy: []string{"Shirl Hendryx"}, Teleplay: []string{""}, Season: 2, NoInSeason: 6, NoInSeries: 15, JapaneseTitle: "溶ける糸", JapaneseAirDate: time.Date(1973, 10, 28, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "The Most Dangerous Match", OriginalAirDate: "March 4, 1973", Runtime: 73, GuestStaring: "Laurence Harvey", GuestStaringRole: "Chess Grandmaster Emmett Clayton", DirectedBy: "Edward M. Abroms", WrittenBy: []string{"Jackson Gillis", "Richard Levinson & William Link"}, Teleplay: []string{"Jackson Gillis"}, Season: 2, NoInSeason: 7, NoInSeries: 16, JapaneseTitle: "断たれた音", JapaneseAirDate: time.Date(1973, 11, 25, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Double Shock", OriginalAirDate: "March 25, 1973", Runtime: 73, GuestStaring: "Martin Landau", GuestStaringRole: "Flamboyant television chef Dexter Paris and his twin brother, conservative banker Norman", DirectedBy: "Robert Butler", WrittenBy: []string{"Jackson Gillis", "Richard Levinson & William Link"}, Teleplay: []string{"Steven Bochco"}, Season: 2, NoInSeason: 8, NoInSeries: 17, JapaneseTitle: "二つの顔", JapaneseAirDate: time.Date(1973, 12, 23, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Lovely But Lethal", OriginalAirDate: "September 23, 1973", Runtime: 73, GuestStaring: "Vera Miles", GuestStaringRole: "Cosmetics queen Viveca Scott", DirectedBy: "Jeannot Szwarc", WrittenBy: []string{"Myrna Bercovici"}, Teleplay: []string{"Jackson Gillis"}, Season: 3, NoInSeason: 1, NoInSeries: 18, JapaneseTitle: "毒のある花", JapaneseAirDate: time.Date(1974, 9, 14, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Any Old Port in a Storm", OriginalAirDate: "October 7, 1973", Runtime: 98, GuestStaring: "Donald Pleasence", GuestStaringRole: "Wine connoisseur Adrian Carsini", DirectedBy: "Leo Penn", WrittenBy: []string{"Larry Cohen"}, Teleplay: []string{"Stanley Ralph Ross"}, Season: 3, NoInSeason: 2, NoInSeries: 19, JapaneseTitle: "別れのワイン", JapaneseAirDate: time.Date(1974, 6, 29, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Candidate for Crime", OriginalAirDate: "November 4, 1973", Runtime: 98, GuestStaring: "Jackie Cooper", GuestStaringRole: "Nelson Hayward, is coercing the womanizing senatorial candidate", DirectedBy: "Boris Sagal", WrittenBy: []string{"Larry Cohen"}, Teleplay: []string{"Irving Pearlberg & Alvin R. Friedman", "Roland Kibbee & Dean Hargrove"}, Season: 3, NoInSeason: 3, NoInSeries: 20, JapaneseTitle: "野望の果て", JapaneseAirDate: time.Date(1974, 8, 17, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Double Exposure", OriginalAirDate: "December 16, 1973", Runtime: 73, GuestStaring: "Robert Culp", GuestStaringRole: "Dr. Bart Keppel, A motivation research specialist", DirectedBy: "Richard Quine", WrittenBy: []string{"Stephen J. Cannell"}, Teleplay: []string{""}, Season: 3, NoInSeason: 4, NoInSeries: 21, JapaneseTitle: "意識の下の映像", JapaneseAirDate: time.Date(1974, 8, 10, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Publish or Perish", OriginalAirDate: "January 18, 1974", Runtime: 73, GuestStaring: "Jack Cassidy", GuestStaringRole: "Riley Greenleaf, Publisher", DirectedBy: "Robert Butler", WrittenBy: []string{"Peter S. Fischer"}, Teleplay: []string{""}, Season: 3, NoInSeason: 5, NoInSeries: 22, JapaneseTitle: "第三の終章", JapaneseAirDate: time.Date(1974, 12, 14, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Mind Over Mayhem", OriginalAirDate: "February 10, 1974", Runtime: 73, GuestStaring: "José Ferrer", GuestStaringRole: "Dr. Marshall Cahill, director of a high-tech Pentagon think tank", DirectedBy: "Alf Kjellin", WrittenBy: []string{"Robert Specht"}, Teleplay: []string{"Steven Bochco", "Dean Hargrove & Roland Kibbee"}, Season: 3, NoInSeason: 6, NoInSeries: 23, JapaneseTitle: "愛情の計算", JapaneseAirDate: time.Date(1974, 8, 31, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Swan Song", OriginalAirDate: "March 3, 1974", Runtime: 98, GuestStaring: "Johnny Cash", GuestStaringRole: "Gospel-singing superstar Tommy Brown", DirectedBy: "Nicholas Colasanto", WrittenBy: []string{"Stanley Ralph Ross"}, Teleplay: []string{"David Rayfiel"}, Season: 3, NoInSeason: 7, NoInSeries: 24, JapaneseTitle: "白鳥の歌", JapaneseAirDate: time.Date(1974, 9, 21, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "A Friend in Deed", OriginalAirDate: "May 5, 1974", Runtime: 98, GuestStaring: "Richard Kiley", GuestStaringRole: "Deputy police commissioner Mark Halperin", DirectedBy: "Ben Gazzara", WrittenBy: []string{"Peter S. Fischer"}, Teleplay: []string{""}, Season: 3, NoInSeason: 8, NoInSeries: 25, JapaneseTitle: "権力の墓穴", JapaneseAirDate: time.Date(1974, 10, 5, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "An Exercise in Fatality", OriginalAirDate: "September 15, 1974", Runtime: 98, GuestStaring: "Robert Conrad", GuestStaringRole: "Renowned exercise guru Milo Janus", DirectedBy: "Bernard L. Kowalski", WrittenBy: []string{"Larry Cohen"}, Teleplay: []string{"Peter S. Fischer"}, Season: 4, NoInSeason: 1, NoInSeries: 26, JapaneseTitle: "自縛の紐", JapaneseAirDate: time.Date(1975, 12, 27, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Negative Reaction", OriginalAirDate: "October 6, 1974", Runtime: 98, GuestStaring: "Dick Van Dyke", GuestStaringRole: "professional photographer Paul Galesko", DirectedBy: "Alf Kjellin", WrittenBy: []string{"Peter S. Fischer"}, Teleplay: []string{""}, Season: 4, NoInSeason: 2, NoInSeries: 27, JapaneseTitle: "逆転の構図", JapaneseAirDate: time.Date(1975, 12, 20, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "By Dawn's Early Light", OriginalAirDate: "October 27, 1974", Runtime: 98, GuestStaring: "Patrick McGoohan", GuestStaringRole: "Colonel Lyle C. Rumford, head of the Haynes Military Academy", DirectedBy: "Harvey Hart", WrittenBy: []string{"Howard Berk"}, Teleplay: []string{""}, Season: 4, NoInSeason: 3, NoInSeries: 28, JapaneseTitle: "祝砲の挽歌", JapaneseAirDate: time.Date(1976, 1, 10, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Troubled Waters", OriginalAirDate: "February 9, 1975", Runtime: 98, GuestStaring: "Robert Vaughn", GuestStaringRole: "Auto executive Hayden Danziger", DirectedBy: "Ben Gazzara", WrittenBy: []string{"Jackson Gillis", "William Driskill"}, Teleplay: []string{"William Driskill"}, Season: 4, NoInSeason: 4, NoInSeries: 29, JapaneseTitle: "歌声の消えた海", JapaneseAirDate: time.Date(1976, 1, 3, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Playback", OriginalAirDate: "March 2, 1975", Runtime: 73, GuestStaring: "Oskar Werner", GuestStaringRole: "Harold Van Wick, the gadget-obsessed president of Midas Electronics", DirectedBy: "Bernard L. Kowalski", WrittenBy: []string{"David P. Lewis & Booker T. Bradshaw"}, Teleplay: []string{""}, Season: 4, NoInSeason: 5, NoInSeries: 30, JapaneseTitle: "ビデオテープの証言", JapaneseAirDate: time.Date(1976, 12, 11, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "A Deadly State of Mind", OriginalAirDate: "April 27, 1975", Runtime: 73, GuestStaring: "George Hamilton", GuestStaringRole: "Psychiatrist Dr. Mark Collier", DirectedBy: "Harvey Hart", WrittenBy: []string{"Peter S. Fischer"}, Teleplay: []string{""}, Season: 4, NoInSeason: 6, NoInSeries: 31, JapaneseTitle: "5時30分の目撃者", JapaneseAirDate: time.Date(1976, 12, 18, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Forgotten Lady", OriginalAirDate: "September 14, 1975", Runtime: 85, GuestStaring: "Janet Leigh", GuestStaringRole: "Aging former movie star Grace Wheeler", DirectedBy: "Harvey Hart", WrittenBy: []string{"Bill Driskill"}, Teleplay: []string{""}, Season: 5, NoInSeason: 1, NoInSeries: 32, JapaneseTitle: "忘れられたスター", JapaneseAirDate: time.Date(1977, 1, 3, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "A Case of Immunity", OriginalAirDate: "October 12, 1975", Runtime: 73, GuestStaring: "Héctor Elizondo", GuestStaringRole: "Hassan Salah, chief diplomat of the Legation of Swahari", DirectedBy: "Ted Post", WrittenBy: []string{"James Menzies"}, Teleplay: []string{"Lou Shaw"}, Season: 5, NoInSeason: 2, NoInSeries: 33, JapaneseTitle: "ハッサン・サラーの反逆", JapaneseAirDate: time.Date(1976, 12, 25, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Identity Crisis", OriginalAirDate: "November 2, 1975", Runtime: 98, GuestStaring: "Patrick McGoohan", GuestStaringRole: "speech-writing consultant Nelson Brenner", DirectedBy: "Patrick McGoohan", WrittenBy: []string{"Bill Driskill"}, Teleplay: []string{""}, Season: 5, NoInSeason: 3, NoInSeries: 34, JapaneseTitle: "仮面の男", JapaneseAirDate: time.Date(1977, 9, 24, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "A Matter of Honor", OriginalAirDate: "February 1, 1976", Runtime: 73, GuestStaring: "Ricardo Montalban", GuestStaringRole: "Luis Montoya, A Mexican national hero", DirectedBy: "Ted Post", WrittenBy: []string{"Brad Radnitz"}, Teleplay: []string{""}, Season: 5, NoInSeason: 4, NoInSeries: 35, JapaneseTitle: "闘牛士の栄光", JapaneseAirDate: time.Date(1977, 10, 1, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Now You See Him...", OriginalAirDate: "February 29, 1976", Runtime: 85, GuestStaring: "Jack Cassidy", GuestStaringRole: "Great Santini, a magician extraordinaire", DirectedBy: "Harvey Hart", WrittenBy: []string{"Michael Sloan"}, Teleplay: []string{""}, Season: 5, NoInSeason: 5, NoInSeries: 36, JapaneseTitle: "魔術師の幻想", JapaneseAirDate: time.Date(1977, 12, 31, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Last Salute to the Commodore", OriginalAirDate: "May 2, 1976", Runtime: 98, GuestStaring: "Robert Vaughn", GuestStaringRole: "Son-in-law Charles Clay", DirectedBy: "Patrick McGoohan", WrittenBy: []string{"Jackson Gillis"}, Teleplay: []string{""}, Season: 5, NoInSeason: 6, NoInSeries: 37, JapaneseTitle: "さらば提督", JapaneseAirDate: time.Date(1977, 10, 8, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Fade in to Murder", OriginalAirDate: "October 10, 1976", Runtime: 73, GuestStaring: "William Shatner", GuestStaringRole: "Egocentric actor Ward Fowler", DirectedBy: "Bernard L. Kowalski", WrittenBy: []string{"Henry Garson"}, Teleplay: []string{"Lou Shaw", "Peter S. Feibleman"}, Season: 6, NoInSeason: 1, NoInSeries: 38, JapaneseTitle: "ルーサン警部の犯罪", JapaneseAirDate: time.Date(1977, 12, 17, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Old Fashioned Murder", OriginalAirDate: "November 28, 1976", Runtime: 73, GuestStaring: "Joyce Van Patten", GuestStaringRole: "Ruth Lytton, Owner of the Lytton Museum", DirectedBy: "Robert Douglas", WrittenBy: []string{"Lawrence Vail"}, Teleplay: []string{"Peter S. Feibleman"}, Season: 6, NoInSeason: 2, NoInSeries: 39, JapaneseTitle: "黄金のバックル", JapaneseAirDate: time.Date(1977, 12, 24, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "The Bye-Bye Sky High IQ Murder Case", OriginalAirDate: "May 22, 1977", Runtime: 73, GuestStaring: "Theodore Bikel", GuestStaringRole: "Oliver Brandt, a senior partner in an accounting firm", DirectedBy: "Sam Wanamaker", WrittenBy: []string{"Robert Malcolm Young"}, Teleplay: []string{""}, Season: 6, NoInSeason: 3, NoInSeries: 40, JapaneseTitle: "殺しの序曲", JapaneseAirDate: time.Date(1978, 5, 20, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Try and Catch Me", OriginalAirDate: "November 21, 1977", Runtime: 73, GuestStaring: "Ruth Gordon", GuestStaringRole: "Mystery author Abigail Mitchell", DirectedBy: "James Frawley", WrittenBy: []string{"Gene Thompson"}, Teleplay: []string{"Gene Thompson & Paul Tuckahoe"}, Season: 7, NoInSeason: 1, NoInSeries: 41, JapaneseTitle: "死者のメッセージ", JapaneseAirDate: time.Date(1978, 4, 8, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Murder Under Glass", OriginalAirDate: "January 30, 1978", Runtime: 73, GuestStaring: "Louis Jourdan", GuestStaringRole: "Renowned restaurant critic Paul Gerard", DirectedBy: "Jonathan Demme", WrittenBy: []string{"Robert van Scoyk"}, Teleplay: []string{""}, Season: 7, NoInSeason: 2, NoInSeries: 42, JapaneseTitle: "美食の報酬", JapaneseAirDate: time.Date(1978, 5, 27, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "Make Me a Perfect Murder", OriginalAirDate: "February 28, 1978", Runtime: 98, GuestStaring: "Trish Van Devere", GuestStaringRole: "TV programmer Kay Freestone", DirectedBy: "James Frawley", WrittenBy: []string{"Robert Blees"}, Teleplay: []string{""}, Season: 7, NoInSeason: 3, NoInSeries: 43, JapaneseTitle: "秒読みの殺人", JapaneseAirDate: time.Date(1979, 1, 2, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "How to Dial a Murder", OriginalAirDate: "April 15, 1978", Runtime: 73, GuestStaring: "Nicol Williamson", GuestStaringRole: "Mind control seminar guru Dr. Eric Mason", DirectedBy: "James Frawley", WrittenBy: []string{"Anthony Lawrence"}, Teleplay: []string{"Tom Lazarus"}, Season: 7, NoInSeason: 4, NoInSeries: 44, JapaneseTitle: "攻撃命令", JapaneseAirDate: time.Date(1979, 1, 4, 0, 0, 0, 0, lc)},
&models.Episodes{Title: "The Conspirators", OriginalAirDate: "May 13, 1978", Runtime: 98, GuestStaring: "Clive Revill", GuestStaringRole: "Famous Irish poet and author Joe Devlin", DirectedBy: "Leo Penn", WrittenBy: []string{"Howard Berk"}, Teleplay: []string{""}, Season: 7, NoInSeason: 5, NoInSeries: 45, JapaneseTitle: "策謀の結末", JapaneseAirDate: time.Date(1979, 1, 3, 0, 0, 0, 0, lc)},
}
}
IntelliJ IDEAのインストール
IDEはIntelliJ IDEA Community Edition(以降IntelliJと表記します)を使用します。
jetbrainsのダウンロードページよりインストーラーをダウンロードしてインストールを行います。
go-lang-idea-plugin
go-lang-idea-pluginをインストールします。インストール方法はjetbrainsのManaging Enterprise Plugin Repositoriesで詳しく解説されています。
ここでは手順を簡単に説明します。
- IntelliJを起動しWelcome画面で"Configure"→"Plugins"→"Browse repositories"をクリック
- "Manage repositories"をクリック
- Custom Plugin Repositories画面で右上の緑十字アイコンをクリック
- "Add Repository"にプラグインのリポジトリURLを入力
- Browser Repositories画面でプルダウンから上記で追加したリポジトリを選択
- Browser Repositories画面にGo Pluginが表示されるので"Install Plugin"ボタンをクリック
プラグインのインストールが終わったらGO SDKの設定を行います。
- IntelliJのWelcome画面で"Configure"→"Project Defaults"→"Project Structure"をクリック
- Default Project Structure画面で"Platform Setting"→"SDKs"をクリック
- 緑十字のアイコンをクリックしてリストから"Go SDK"を選択
- Select Home Directory for Go SDK画面でGoのインストールディレクトリを指定