post Image
revelでHello World

概要

Go言語のWeb Application FrameworkであるRevelを使ってHello Worldを表示するアプリケーションを開発した内容を簡単にまとめました。

目標

アプリケーションの開発手順と実装する機能は下記の通りです。
Revelの機能やその使い方を確認することに重点を置きましたのでアプリケーションの実用性はあまりありません。

  • [Step1] アプリケーションの雛形を作成
  • [Step2] Hello Worldを表示するページ
  • [Step3] 検索結果を表示するページ
  • [Step4] 検索結果をjsonで返すAPI
  • [Step5] エラーページの表示
  • [Step6] jobの実行機能

環境

Revelのインストール

install
> go get -u -v github.com/revel/revel
> go get -u -v github.com/revel/cmd/revel

参考サイト

この記事の作成に関して下記のサイトを参考にしました。

Revel

Qiita

Blog

Github

ソースコードはrubytomato/rhwにあります。

アプリケーションの開発

アプリケーション名はrhw(Revel de Hello World)としました。
開発は雛形の作成からはじめて徐々に機能を増やしていく方法をとります。

[Step1] アプリケーションの雛形を作成

revel newコマンドで雛形を作成します。

example
> revel.exe new rhw

アプリケーションの実行

revel runコマンドで雛形のアプリケーションを実行することができます。
アプリケーションが起動したら下記のURLにアクセスしページが表示されることを確認します。
アプリケーションの停止はコンソールからCtrl + Cで行います。

http://localhost:9000

example
> revel.exe run rhw
usage
> 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に作成しました。

example
> revel.exe build rhw "c:/tmp/rhw"
usage
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の内容

run.bat
@echo off
rhw.exe -importPath rhw -srcPath %CD%\src -runMode prod

IDEにアプリケーションを取り込む

IntelliJを起動し、Welcome画面からImport ProjectSelect File or Directory to Import画面で上記で作成したプロジェクトフォルダを選択します。

インポート後の状態

インポート後のディレクトリ構成は下記の通りです。

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”というメッセージを表示するページを開発します。

完成図

hello_world_1.png

Controller

  • rhw > /app > /controllers > hello.goを作成

Helloがコントローラ名、Index(),Greet()がアクション名になります。

hello.go
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になります。

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()メソッドに変数を渡すと、テンプレート内ではその変数名を使ってデータを表示することができます。

exmaple
<div class="row">
  <h1>{{.greeting}}</h1>
  <p>{{.message}} ({{len .message}})</p>
</div>

メッセージリソースの利用(i18n)

  • rhw > /conf > app.confを修正

デフォルトで使用する言語をjaに変更します。

app.conf
# 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です。

messages.ja
say = ハロー、%s!
hello.index.title = Revel frameworkでハロー、ワールド

英語用メッセージリソースファイルの拡張子はenです。

messages.en
say = hello %s!
hello.index.title = Revel framework Hello World

msgはRevelのTemplateFuncsマップに事前定義されている関数です。
“say”がメッセージリソースのキーで、次の”ワールド”が%sにセットされる文字列になります。

example
<h2>{{msg . "say" "ワールド"}}</h2>

source code

msg
"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

Route

  • rhw > /app > /conf > routesを編集

routesファイルに下記の行を追加します。
1行目は、GETメソッドでlocalhost:9000/helloへアクセスするとHello.Index()へルーティングされます。
2行目は、GETメソッドで、例えばlocalhost:9000/greet/こんにちはへアクセスするとHello.Greet()へルーティングされ、URLの”こんにちは”の部分がGreet()アクションの引数に設定されます。

routes
+ GET     /hello                                  Hello.Index
+ GET     /greet/:greeting                        Hello.Greet

Greet()アクションの(規約的な)デフォルトのテンプレートは/views/Hello/Greet.htmlになりますがRenderTemplate()で任意のテンプレートファイルを使用することができます。
この例では、Hello.Index()アクションと同じテンプレートファイルを使用しています。

hello.go
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

これで「STEP2 Hello Worldを表示するページ」の実装は終わりです。
localhost:9000/helloにアクセスすると”Hello World”が表示されると思います。

[Step3] 検索結果を表示するページ

Step3では、海外ドラマのエピソードを検索するページと検索条件を満たすエピソードの一覧ページを作成します。

完成図

episodes_search.png

episodes_search_result.png

Form

  • rhw > /app > /formsフォルダを作成
  • rhw > /app > /forms > searchForm.goを作成

パッケージはformsとし検索フォームの値を受け取る型を定義します。フィールドのデータタイプは全てstringにします。
また、この型に入力値を検証するValidationメソッドを実装します。

searchForm.go
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メソッドには下記に引用した種類があります。ここではCheckErrorの使い方の補足をします。

Checkは1つのフィールドに対して複数のバリデーションをまとめて行ったり、独自のバリデータを使って検証することができます。

example
v.Check(s.Title,
  revel.ValidRequired(),
  revel.ValidMinSize(6)).Message("タイトルのエラーです").Key("s.Title")

独自バリデータを作成するにはValidatorインターフェースを実装します。

source

Validator
type Validator interface {
    IsSatisfied(interface{}) bool
    DefaultMessage() string
}

Errorは、検証を行わずにエラーを追加します。

example
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

Model

  • rhw > /app > /modelsフォルダを作成
  • rhw > /app > /models > episodes.goを作成

パッケージはmodelsとし海外ドラマのエピソードのデータを持つ型を定義します。jsonで出力することを考えてjsonタグを付けます。

episodes.go
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関数で配列に格納しておきます。下記のサンプルデータのコードは一部分です。全文はページ末尾に記載しました。

columbo.go
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()アクションを追加します。

columbo.go
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を逆引きしてくれます。

syntax
func ReverseUrl(args ...interface{}) (string, error)
navi.html
<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コードです。

navi.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アクションの引数にインクルードしたいテンプレートファイル名を指定します。

Index.html
  </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を作成

検索フォームを表示するページ

Search.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”という文字列がセットされています。エラーでなければ””です。
example
<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コードは下記の通りです。

html_code
<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>

エラー発生時のスクリーンショット

search_error.png

fieldの実体はRevelのTemplateFuncsマップに事前定義されているNewFieldという関数で、nameとrenderArgsを引数に取りField型のポインタを返します。

syntax
func NewField(name string, renderArgs map[string]interface{}) *Field

source code

NewField
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

Field
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

検索結果を表示するページ

Result.html
{{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で設定した書式にフォーマットします。

example
<td>{{date $elem.JapaneseAirDate}}</td>

app.confを修正して日付の表示フォーマットを変更します。

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に下記のメッセージリソースを追加します。

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ファイルに下記の行を追加します。

routes
GET     /columbo/search                         Columbo.Search
GET     /columbo/confirm                        Columbo.Confirm
GET     /columbo/result                         Columbo.Result

Revel manual – Templates

これで「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メソッドを使用します。

columbo.go
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に設定すると見やすく整形して表示します。

app.conf
results.pretty = true

Route

  • rhw > /app > /conf > routesを編集

routesファイルに下記の行を追加します。

routes
GET     /columbo/json/:no                       Columbo.ResultJSON

urlパターンの:noの部分に放送回数を指定します。
たとえば、localhost:9000/columbo/json/20にアクセスすると放送20回目のエピソードをjsonで取得できます。

Revel manual – app.conf

View

  • rhw > /app > /views > navi.htmlを修正

ついでにテストしやすいようにリンクを追加しておきます。

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()にテンプレートファイルを指定します。

hello.go
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を作成
403.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>
403-dev.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>
404-dev.html
<h1>[DEV-MODE] NotFound</h1>
<div>
   <h2>{{.Error}}</h2>
   <div>
     <dl>
     {{range $index, $elem := .}}
     <dt>{{$index}}</dt>
     <dd>{{$elem}}</dd>
     {{end}}
     </dl>
   </div>
</div>
500-dev.html
<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>
custom.html
<!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を編集

エラーページを確認するためにルーティングを追加します。

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モジュールを有効にします。

app.conf
+ #    http://revel.github.io.manual/jobs.html
+ module.jobs = github.com/revel/modules/jobs
routes
# ~~~~

module:testrunner
+ module:jobs

GET     /                                       App.Index

jobの設定

app.confでjobの設定を行うことができます。

app.conf
jobs.pool = 10
jobs.selfconcurrent = false
jobs.acceptproxyaddress = false

また、実行スケジュールに任意の名前をつけて管理することができます。
スケジュールはcron形式です。

app.conf
cron.every_1h = 0 0 * ? * ?
cron.every_10m = 0 */10 * ? * ?

doc.go

doc.go
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

job
type Job interface {
  Run()
}

jobインターフェースを実装しなくてもjobs.Func()を使用することでスケジューリングできます。

source code

Func
type Func func()

独自jobの実装

パッケージはjobsにします。
jobで実行したい処理はRunメソッドに記述します。今回の例では実行タイミングの確認を目的としますのでfmt.Printf()のみにしました。

myjob.go
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をスケジューリングして実行する方法

指定間隔で実行

example
jobs.Every(time.Hour, MyJob{Name: "JOB Every1H"})

cronで実行間隔を指定

example
jobs.Schedule("0 0 0 * * ?",  MyJob{Name: "JOB Every1D"})
example
jobs.Schedule("cron.everyhours", MyJob{Name: "JOB Every1H"})

jobを1回だけ実行する方法

即時実行

example
jobs.Now(MyJob{Name:"JOB JUST NOW"})

指定時間後に実行

example
jobs.In(10 * time.Minute, MyJob{Name:"JOB AFTER 10m"})

スケジュールされたjobのステータス

ブラウザベースのjobステータス確認ページがあります。

http://localhost:9000/@jobs

Revel manual – Jobs
GoDoc – package jobs

これで「STEP6 jobの実行機能」の実装は終わりです。

付録

サンプルデータの準備

下記にサンプルデータを登録する処理の全文を記述します。

columbo.go
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のインストールディレクトリを指定

『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。