
この記事を理解するには、unixやwebという言葉に怖がらないように、少しプログラミングができる必要があります。 言語の基本については、記事で概説します。
インストールに行く
したがって、Goで最初に作業する必要があるのはLinux、FreeBSD(OS X)ですが、 MinGWは Windowsでも動作します。
Goをインストールする必要があります。そのためには、次のようなことを行う必要があります(dpkgを備えたシステムについての指示が与えられます)
$ sudo apt-get install bison ed gawk gcc libc6-dev make#ビルドツール $ sudo apt-get install python-setuptools python-dev build-essential#ツールのビルドとインストール $ sudo easy_install mercurial#まだお持ちでない場合は、リポジトリから(apt経由で)インストールしない方が良い $ hg clone -r release https://go.googlecode.com/hg/ go $ cd go / src $ ./all.bash
すべてがうまくいけば、以下を〜/ .bashrcまたは〜/ .profileに追加できます:
エクスポートGOROOT = $ HOME / go export GOARCH = 386#またはamd64、OSアーキテクチャに応じて エクスポートGOOS = linux エクスポートGOBIN = $ GOROOT / bin PATH = $ PATH:$ GOBIN
シェルを再入力してコンパイラーを呼び出すと(i386の場合は8g 、amd64の場合は6g 、 8gがあります)、ヘルプメッセージが表示されます。
gc:使用法8g [フラグ] file.go ...
これは、Goがインストールされて動作していることを意味し、アプリケーションに直接アクセスできます。
スタート。 データ構造
アプリケーションのディレクトリを作成します。
$ mkdir〜/ gowiki $ cd〜/ gowiki
テキストエディター( vimおよびemacsのバインダー)を 使用して、次の内容のwiki.goファイルを作成しましょう。
パッケージメイン インポート( 「fmt」 「io / ioutil」 「os」 )
名前から、アプリケーションがページの編集と保存を許可することは明らかです。
このコードは、Go標準ライブラリからfmt、ioutil、およびosライブラリをインポートします。 後で他のライブラリを追加します。
複数のデータ構造を定義します。 Wikiは、本文とタイトルを持つリンクされたページのコレクションです。 対応するデータ構造には2つのフィールドがあります。
type page struct { タイトル文字列 本体[]バイト }
[]バイトデータ型は、動的配列の類似体であるバイト型のスライスです(詳細: Effective Go )。記事の本文は、標準ライブラリでの作業の便宜上、 文字列ではなく[]バイトに格納されます。
データ構造は、データがメモリに格納される方法を記述します。 しかし、データを長時間保存する必要がある場合はどうでしょうか? ディスクに保存するsaveメソッドを実装します 。
func(p *ページ)save()os.Error { ファイル名:= p.title + ".txt" return ioutil.WriteFile(ファイル名、p.body、0600) }
この関数のシグネチャは次のとおりです。「これは、パラメーターなしでページポインターに適用可能なsaveメソッドで、タイプos.Errorの値を返します。」
このメソッドはテキストをファイルに保存します。 簡単にするために、ヘッダーはファイル名であると想定しています。 これが十分に安全でないと思われる場合は、 crypto / md5をインポートし、 md5.New(ファイル名)呼び出しを使用できます。
戻り値は、 WriteFile呼び出し(スライスをファイルに書き込むための標準ライブラリ関数)の戻り値に対応するos.Error型になります。 これは、将来ファイルに保存するエラーを処理できるようにするために行われます。 問題がない場合、 page.save()はnil (ポインター、インターフェース、およびその他のタイプの場合はヌル値)を返します。
WriteFile呼び出しの3番目のパラメーターである8進定数0600は、現在のユーザーに対してのみ読み取りおよび書き込み権限でファイルが保存されることを示します。
ページをロードすることも興味深いでしょう:
func loadPage(タイトル文字列)*ページ{ ファイル名:=タイトル+ ".txt" body、_:= ioutil.ReadFile(ファイル名) リターン&ページ{タイトル:タイトル、ボディ:ボディ} }
この関数は、ヘッダーからファイル名を取得し、内容をページ型の変数に読み取り、そのポインターを返します。
Goの関数は複数の値を返すことができます。 io.ReadFile標準ライブラリ関数は[]バイトとos.Errorを返します。 loadPage関数のエラーはまだ処理されていません。下線は「この値を保存しない」ことを意味します。
ReadFileがエラーを返すとどうなりますか? たとえば、そのタイトルのページはありません。 これは重大な間違いであり、無視することはできません。 関数が* pageとos.Errorの 2つの値も返すようにします。
func loadPage(タイトル文字列)(*ページ、os.Error){ ファイル名:=タイトル+ ".txt" body、err:= ioutil.ReadFile(ファイル名) if err!= nil { 戻り値nil、err } リターン&ページ{タイトル:タイトル、ボディ:ボディ}、nil }
これで、2番目のパラメーターの値を確認できます。nilの場合、ページは正常にロードされています。 それ以外の場合は、タイプos.Errorの値になります。
したがって、データ構造と、ロードおよびアンロードのメソッドがあります。 これがどのように機能するかを確認する時が来ました:
func main(){ p1:=&page {title: "TestPage"、body:[] byte( "Test page。")} p1.save() p2、_:= loadPage( "TestPage") fmt.Println(文字列(p2.body)) }
このコードをコンパイルして実行すると、 TestPage.txtファイルに値p1-> bodyが含まれます。 その後、この値はp2変数にロードされて表示されます。
プログラムをビルドして実行するには、次を実行する必要があります。
$ 8g wiki.go $ 8l wiki.8 $ ./8.out これはサンプルページです。
HTTPライブラリ
Goの最も単純なWebサーバーは次のようになります。
パッケージメイン インポート( 「fmt」 「http」 ) funcハンドラー(w http.ResponseWriter、r * http.Request){ fmt.Fprintf(w、 "Hello%s!"、r.URL.Path [1:]) } func main(){ http.HandleFunc( "/"、ハンドラー) http.ListenAndServe( ":8080"、なし) }
メイン関数はhttp.HandleFuncを呼び出します 。これは、すべての種類の要求( "/" )がハンドラー関数によって処理されることをhttpライブラリに伝えます。
次のhttp.ListenAndServeの呼び出しにより、ポート8080( ":8080" )上のすべてのインターフェースでリクエストを処理することを決定します。 2番目のパラメーターはまだ必要ありません。 プログラムは、強制終了するまでこのモードで動作します。
ハンドラー関数のタイプはhttp.HandlerFuncです。 パラメータとしてhttp.ResponseWriterとhttp.Requestへのポインタを取ります。
タイプhttp.ResponseWriterの値は、 http応答を生成します 。 データをそこに書き込む( Fprintfを呼び出す)ことで、ページのコンテンツをユーザーに返します。
http.Requestデータ構造はユーザーリクエストです。 文字列r.URL.Pathはパスです。 接尾辞[1:]は、「最初の文字から最後までのパススライス(サブストリング)を取得する」、つまり先頭のスラッシュを削除することを意味します。
ブラウザを起動し、 http:// localhost:8080 / habrahabrというURLを開くと、目的のページが表示されます。
こんにちはhabrahabr!
httpを使用してページを返す
httpライブラリをインポートします 。
インポート( 「fmt」 「http」 「io / ioutil」 「os」 )
記事を表示するハンドラーを作成します。
const lenPath = len( "/ view /") func viewHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、_:= loadPage(タイトル) fmt.Fprintf(w、 "<h1>%s </ h1> <div>%s </ div>"、p.title、p.body) }
まず、この関数は、指定されたURLのパスコンポーネントであるr.URL.Pathからヘッダーを取得します。 グローバル定数lenPathは、システム内の記事のテキストを表示することを示すパス内のプレフィックス「/ view /」の長さです。 部分文字列[lenPath:]が強調表示されます。つまり、記事のタイトルです。プレフィックスは除外されます。
この関数はデータをロードし、単純なhtmlタグで補完して、タイプhttp.ResponseWriterのパラメーターwに書き込みます。
os.Error型の戻り値を無視するために_を再利用しました。 これは単純化のために行われ、通常はあまりうまくいきません。 以下に、このようなエラーを正しく処理する方法を示します。
このハンドラーを呼び出すには、適切なviewHandlerを使用してhttpを初期化し、パス/ view /に沿って要求を処理するメイン関数を作成します。
func main(){ http.HandleFunc( "/ view /"、viewHandler) http.ListenAndServe( ":8080"、なし) }
( test.txtファイルに)テストページを作成し、コードをコンパイルして、ページを表示してみます。
$ echo "Hello world"> test.txt $ 8g wiki.go $ 8l wiki.8 $ ./8.out
サーバーの実行中、「Hello world」という単語を含む「test」という見出しのあるページは、 http:// localhost:8080 / view / testで利用できます。
ページを変更
ページを編集する機能がないこのウィキはどのようなものですか? 編集フォームを表示するeditHandlerと受信データを保存するsaveHandlerの 2つの新しいハンドラーを作成しましょう。
まず、それらをmain()に追加します。
func main(){ http.HandleFunc( "/ view /"、viewHandler) http.HandleFunc( "/ edit /"、editHandler) http.HandleFunc( "/ save /"、saveHandler) http.ListenAndServe( ":8080"、なし) }
editHandler関数はページをロードし(そのようなページがない場合は空の構造を作成します)、フォームを表示します:
func editHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、err:= loadPage(タイトル) if err!= nil { p =&ページ{タイトル:タイトル} } fmt.Fprintf(w、 "<h1> Editing%s </ h1>" + "<form action = \" / save /%s \ "method = \" POST \ ">" + "<テキストエリア名= \" body \ ">%s </テキストエリア>" + "<input type = \" submit \ "value = \" Save \ ">" + 「</ form>」、 p.title、p.title、p.body) }
この関数は正常かつ正常に機能しますが、見苦しくなります。 理由はhtmlハードコードですが、修正可能です。
ライブラリテンプレート
テンプレートライブラリは、標準のGoライブラリの一部です。 テンプレートを使用してHTMLマークアップをコードの外部に保存し、再コンパイルせずにマークアップを変更できるようにします。
まず、 テンプレートをインポートします:
インポート( 「http」 「io / ioutil」 「os」 「テンプレート」 )
edit.htmlファイルに次の内容のフォームテンプレートを作成します。
<h1> {title}の編集</ h1> <form action = "/ save / {title}" method = "POST"> <div> <text area name = "body" rows = "20" cols = "80"> {body | html} </ text area> </ div> <div> <input type = "submit" value = "Save"> </ div> </ form>
テンプレートを使用するようにeditHandlerを変更します。
func editHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、err:= loadPage(タイトル) if err!= nil { p =&ページ{タイトル:タイトル} } t、_:= template.ParseFile( "edit.html"、nil) t.Execute(p、w) }
template.ParseFileメソッドはedit.htmlファイルを読み取り、タイプ* template.Templateの値を返します。
t.Executeメソッドは、出現するすべての{title}および{body}をp.titleおよびp.bodyの値に置き換え 、結果のhtmlをhttp.ResponseWriter型の変数に出力します。
テンプレートで{body | html}テンプレートが検出されたことに注意してください。 これは、パラメーターがhtmlでの出力用にフォーマットされることを意味します。 エスケープされ、たとえば、 >は&gt;に置き換えられます。 。 これにより、フォーム内のデータが正しく表示されます。
プログラムにfmt.Sprintfの呼び出しがなくなったため、インポートからfmtを削除できます。
また、ページを表示するためのテンプレートview.htmlを作成します。
<h1> {title} </ h1> <p> [<a href="/edit/{titlele"">編集</a>] </ p> <div> {body} </ div>
それに応じてviewHandlerを変更します 。
func viewHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、_:= loadPage(タイトル) t、_:= template.ParseFile( "view.html"、nil) t.Execute(p、w) }
どちらの場合も、テンプレートを呼び出すためのコードはほぼ同じであることに注意してください。 このコードを別の関数に取り込むことで、重複を取り除きます。
func viewHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、_:= loadPage(タイトル) renderTemplate(w、「ビュー」、p) } func editHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] p、err:= loadPage(タイトル) if err!= nil { p =&ページ{タイトル:タイトル} } renderTemplate(w、「編集」、p) } func renderTemplate(w http.ResponseWriter、tmpl文字列、p *ページ){ t、_:= template.ParseFile(tmpl + "。html"、nil) t.Execute(p、w) }
ハンドラーが短くなり、シンプルになりました。
欠落ページの処理
/ view / APageThatDoesntExistに移動するとどうなりますか? プログラムは落ちます。 そして、すべてはloadPageによって返される2番目の値を処理しなかったためです 。 ページが存在しない場合、新しい記事を作成するためにユーザーをページにリダイレクトします。
func viewHandler(w http.ResponseWriter、r * http.Request、タイトル文字列){ p、err:= loadPage(タイトル) if err!= nil { http.Redirect(w、r、 "/ edit /" + title、http.StatusFound) 帰る } renderTemplate(w、「ビュー」、p) }
http.Redirect関数は、HTTPステータスhttp.StatusFound(302)とLocationヘッダーをHTTP応答に追加します。
ページを保存する
saveHandler関数は、フォームからのデータを処理します。
func saveHandler(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] body:= r.FormValue( "body") p:=&ページ{タイトル:タイトル、本文:[]バイト(本文)} p.save() http.Redirect(w、r、 "/ view /" + title、http.StatusFound) }
選択したセキュリティと本文で新しいページが作成されます。 save()メソッドはデータをファイルに保存し、クライアントは/ view /ページにリダイレクトされます。
FormValueによって返される値はstring型です。 ページ構造に保存するには、 [] byte(body)を書き込むことで[]バイトに変換します。
エラー処理
いくつかの場所でプログラムのエラーを無視します。 これは、エラーが発生するとプログラムがクラッシュするという事実につながるため、サーバーが動作し続けている間にユーザーにエラーメッセージを返すことをお勧めします。
まず、 renderTemplateにエラー処理を追加します:
func renderTemplate(w http.ResponseWriter、tmpl文字列、p *ページ){ t、err:= template.ParseFile(tmpl + "。html"、nil) if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) 帰る } err = t.Execute(p、w) if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) } }
http.Error関数は、選択したHTTPステータス(この場合は「Internal Server Error」)を送信し、エラーメッセージを返します。
saveHandlerで同様の編集を行いましょう。
func saveHandler(w http.ResponseWriter、r * http.Request、タイトル文字列){ body:= r.FormValue( "body") p:=&ページ{タイトル:タイトル、本文:[]バイト(本文)} err:= p.save() if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) 帰る } http.Redirect(w、r、 "/ view /" + title、http.StatusFound) }
p.save()で発生したエラーはすべてユーザーに渡されます。
テンプレートのキャッシュ
コードは十分に効率的ではありません。renderTemplateは、ページがレンダリングされるたびにParseFileを呼び出します。 プログラムの起動時にテンプレートごとにParseFileを 1回呼び出して、 *テンプレートタイプの受け取った値を将来の使用のために構造体に保存することをお勧めします。
まず、 * Templateの値を保存するテンプレートマップを作成します。マップ内のキーはテンプレートの名前になります。
var templates = make(map [string] * template.Template)
次に、 main()の前に呼び出す初期化関数を作成します。 template.MustParseFile関数は、エラーコードを返さないParseFileのラッパーであり、代わりにパニックします。 実際、不正なテンプレートを処理する方法がわからないため、この動作はプログラムに受け入れられます。
func init(){for _、tmpl:= range [] string {"edit"、 "view"} {templates [tmpl] = template.MustParseFile(tmpl + "。html"、nil)}}
forループは、 範囲構成で使用され、指定されたパターンを処理します。
次に、対応するテンプレートのExecuteメソッドを呼び出すようにrenderTemplate関数を変更します。
func renderTemplate(w http.ResponseWriter、tmpl文字列、p *ページ){ err:= templates [tmpl] .Execute(p、w) if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) } }
検証
すでに述べたように、プログラムには重大なセキュリティエラーがあります。 名前の代わりに、任意のパスを渡すことができます。 正規表現チェックを追加します。
正規表現ライブラリをインポートします。 RVを保存するグローバル変数を作成します。
var titleValidator = regexp.MustCompile( "^ [a-zA-Z0-9] + $")
regexp.MustCompile関数は正規表現をコンパイルし、 regexp.Regexpを返します。 template.MustParseFileのようなMustCompileは、エラーが発生した場合にパニックを起こすという点でCompileと異なり、 Compileはエラーコードを返します。
次に、URLからタイトルを抽出し、PB titleValidatorをチェックする関数を作成しましょう。
func getTitle(w http.ResponseWriter、r * http.Request)(タイトル文字列、err os.Error){ title = r.URL.Path [lenPath:] if!titleValidator.MatchString(title){ http.NotFound(w、r) err = os.NewError( "無効なページタイトル") } 帰る }
タイトルが正しい場合、 nilが返されます。 そうでない場合、「404 Not Found」がユーザーに表示され、エラーがハンドラーに返されます。
各ハンドラーにgetTitle呼び出しを追加します。
func viewHandler(w http.ResponseWriter、r * http.Request){ title、err:= getTitle(w、r) if err!= nil { 帰る } p、err:= loadPage(タイトル) if err!= nil { http.Redirect(w、r、 "/ edit /" + title、http.StatusFound) 帰る } renderTemplate(w、「ビュー」、p) } func editHandler(w http.ResponseWriter、r * http.Request){ title、err:= getTitle(w、r) if err!= nil { 帰る } p、err:= loadPage(タイトル) if err!= nil { p =&ページ{タイトル:タイトル} } renderTemplate(w、「編集」、p) } func saveHandler(w http.ResponseWriter、r * http.Request){ title、err:= getTitle(w、r) if err!= nil { 帰る } body:= r.FormValue( "body") p:=&ページ{タイトル:タイトル、本文:[]バイト(本文)} err = p.save() if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) 帰る } http.Redirect(w、r、 "/ view /" + title、http.StatusFound) }
機能タイプとクロージャー
エラーと戻り値をチェックすると、かなり均一なコードが生成されます。一度だけ記述するとよいでしょう。 これは、たとえば、対応する呼び出しでエラーを返す関数をラップする場合に可能です。機能タイプはこれに役立ちます。
titleパラメーターを追加して、ハンドラーを書き換えます。
func viewHandler(w http.ResponseWriter、r * http.Request、タイトル文字列) func editHandler(w http.ResponseWriter、r * http.Request、タイトル文字列) func saveHandler(w http.ResponseWriter、r * http.Request、タイトル文字列)
ここで、上記で定義した関数のタイプを取り、 http.HandlerFuncを返すラッパー関数を定義します( http.HandleFuncに渡すため)。
func makeHandler(fn func(http.ResponseWriter、* http.Request、string))http.HandlerFunc { return func(w http.ResponseWriter、r * http.Request){ //ここでは、リクエストからページタイトルを抽出し、 //そして提供されたハンドラー 'fn'を呼び出します } }
返される関数はクロージャーです。なぜなら 外部で定義された値(この場合、変数fn 、ハンドラー)を使用します。
getTitleのコードをここに移動してみましょう 。
func makeHandler(fn func(http.ResponseWriter、* http.Request、string))http.HandlerFunc { return func(w http.ResponseWriter、r * http.Request){ title:= r.URL.Path [lenPath:] if!titleValidator.MatchString(title){ http.NotFound(w、r) 帰る } fn(w、r、タイトル) } }
makeHandlerによって返されるクロージャーは、 http.ResponseWriterやhttp.Requestなどのパラメーターを取る関数です(つまり、 http.HandlerFuncなどの関数)。 このクロージャは、URLからタイトルを抽出し、そのPB titleValidatorをチェックします。 ヘッダーが正しくない場合、エラーがResponseWriterに送信されます( http.NotFoundを呼び出します )。 それ以外の場合、対応するfnハンドラーが呼び出されます。
main()関数にラッパー呼び出しを追加します。
func main(){ http.HandleFunc( "/ view /"、makeHandler(viewHandler)) http.HandleFunc( "/ edit /"、makeHandler(editHandler)) http.HandleFunc( "/ save /"、makeHandler(saveHandler)) http.ListenAndServe( ":8080"、なし) }
最後に、ハンドラー関数からgetTitleの呼び出しを削除して、それらをより簡単にします。
func viewHandler(w http.ResponseWriter、r * http.Request、タイトル文字列){ p、err:= loadPage(タイトル) if err!= nil { http.Redirect(w、r、 "/ edit /" + title、http.StatusFound) 帰る } renderTemplate(w、「ビュー」、p) } func editHandler(w http.ResponseWriter、r * http.Request、タイトル文字列){ p、err:= loadPage(タイトル) if err!= nil { p =&ページ{タイトル:タイトル} } renderTemplate(w、「編集」、p) } func saveHandler(w http.ResponseWriter、r * http.Request、タイトル文字列){ body:= r.FormValue( "body") p:=&ページ{タイトル:タイトル、本文:[]バイト(本文)} err:= p.save() if err!= nil { http.Error(w、err.String()、http.StatusInternalServerError) 帰る } http.Redirect(w、r、 "/ view /" + title、http.StatusFound) }
これが結果になるはずです
コードを再構築し、アプリケーションを実行します。
$ 8g wiki.go $ 8l wiki.8 $ ./8.out
http:// localhost:8080 / view / ANewPageには、フォームのあるページがあります。 ページを保存して、そこに行くことができます。
ご注意 habraparserを怒らせないように、コード内のtextareaを壊す必要がありました。