###### tags: `golang` `Goプログラミング実践入門` # Goプログラミング実践入門4章<br>リクエストのデータ構造とその処理 ## この章でやること ハンドラの実際の処理を学習する。 * Requestの構造 * FormDataの取得 * ResponseWriterへの書き込み * Cookie :gem: [4章ソースコード](https://github.com/mushahiroyuki/gowebprog/tree/master/ch04) ## 4.1 リクエストとレスポンス リクエストもレスポンスも基本的に同じ構造となっている。 1. リクエスト行、またはレスポンス行 2. 0行以上のヘッダ 3. 空行 4. 任意指定のメッセージボディ :gem: 参照[HTTP メッセージ](https://developer.mozilla.org/ja/docs/Web/HTTP/Overview) ### 4.1.1 Request --- 構造体[Request](https://golang.org/pkg/net/http/#Request)は、クライアントから送信されるリクエストメッセージを表現する。 * URL * Header * Body * Form, PostForm, MultipatForm リクエスト内のクッキー、参照元URL、ユーザエージェント(ブラウザなど)にもRequestのメソッドでアクセス可能。 ### 4.1.2 リクエストのURL --- `Request.URL`フィールドは、`url.URL`型へのポインタをフィールドとして持つ。 [URL](https://golang.org/pkg/net/url/#URL)は次のような構造体である。 ```go type URL struct{ Scheme string Opaque string // encoded opaque data User *Userinfo // username and password information Host string // host or host:port Path string // path (relative paths may omit leading slash) RawPath string // encoded path hint (see EscapedPath method); added in Go 1.5 ForceQuery bool // append a query ('?') even if RawQuery is empty; added in Go 1.7 RawQuery string // encoded query values, without '?' Fragment string // fragment for references, without '#' } ``` URLの一般形式は `[scheme:][//[userinfo@]host][/]path[?query][#fragment]` となっている。スキームのあとに「/」がない場合は `scheme:opaque[?query][#fragment]` となる。 例:`http://www.example.com/post?id=123&thread_id=456`を`URL`に入れた場合 ```go= package main import ( "fmt" "log" "net/url" ) func main() { u, err := url.Parse("http://www.example.com/post?id=123&thread_id=456") if err != nil { log.Fatal(err) } fmt.Printf("%#v",u) } ``` ```go // result &url.URL{ Scheme:"http", Opaque:"", User:(*url.Userinfo)(nil), Host:"www.example.com", Path:"/post", RawPath:"", ForceQuery:false, RawQuery:"id=123&thread_id=456", Fragment:"", } ``` ### 4.1.3 リクエストヘッダ --- リクエストとレスポンスのヘッダは、[Header](https://golang.org/pkg/net/http/#Header)型で記述される。 ```go type Header map[string][]string ``` ![図4.1](https://i.imgur.com/xVe2zHl.png) `Header`のメソッドにより、keyに対応するvalue読み込み、設定することができる。 <summary>リスト4.2 リクエストヘッダ内の読み取り</summary> ```go= package main import ( "fmt" "net/http" ) func headers(w http.ResponseWriter, r *http.Request) { h := r.Header fmt.Fprintln(w, h) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/headers", headers) server.ListenAndServe() } ``` 特定のヘッダ1つだけを取得したい場合、2通りある。 ```go // h: []string h := r.Header["Accept-Encoding"] // ["gzip", "delete"] // h: string h := r.Header.Get("Accept-Encoding") // "gzip, delete" ``` ### 4.1.4 リクエストのボディ --- リクエストとレスポンスのボディは`Body`フィールドで表されている。 このフィールドは`io.ReadCloser`で実現される。 `io.ReadCloser`はインターフェース[Reader](https://golang.org/pkg/io/#Reader)を持つため、`Read()`メソッドを用いてボディを取得する。 ```mermaid classDiagram Reader --<| ReadCloser Closer --<| ReadCloser class Reader{ << interface >> + Read(p []byte) (n int, err error) } class Closer{ << interface >> + Close() error } class ReadCloser{ << interface >> + Reader interface + Closer interface } ``` ボディを取得する際は、予め読み出すデータ量を決め、バイト配列を生成する必要がある。 <summary>リスト4.3 リクエストボディからのデータ読み取り</summary> ```go= package main import ( "fmt" "net/http" ) func body(w http.ResponseWriter, r *http.Request) { len := r.ContentLength body := make([]byte, len) r.Body.Read(body) fmt.Fprintln(w, string(body)) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/body", body) server.ListenAndServe() } ``` ## 4.2 HTMLフォームとGo言語 フォームから値を取得する方法について触れる。 フォーム画面例: ![](https://i.imgur.com/WNKHksq.png) ```htmlmixed <form action="/authenticate" method="post"> <input type="email" name="email"> <input type="password" name="password"> <button type="submit">ログイン</button> </form> ``` POSTリクエストで送信する名前と値のペアの形式は、HTMLフォームのコンテンツタイプ(Content-Type)で指定される。 デフォルトは`enctype="application/x-www-form-urlencoded"`。 `method=GET`の場合は、リクエストにボディはなく、URL内に名前と値のペアが組み込まれる。 ### 4.2.1-4.2.3 フォーム関連 --- `Request`には、URLやボディ、または両方からデータを取得できる関数がある。 ![](https://i.imgur.com/T21QBNF.png) 一般的なフォームデータの取り出し方は、次の手順通り。 1. `ParseForm`または`ParseMultipartForm`を呼び出して、リクエストを解析 2. 必要に応じて`Form`、`PostForm`、`MultipartForm`というフィールドから取得 次の例は、フォームを解析する例となっている。9行目でフォームを解析し、10行目でResponseWriterにフォーム内容を書き込んでいる。 <summary>リスト4.4 フォームデータの解析</summary> ```go= package main import ( "fmt" "net/http" ) func process(w http.ResponseWriter, r *http.Request) { r.ParseForm() fmt.Fprintln(w, r.Form) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() } ``` 以下の条件でフォームが送られるとする。 * URL: http://127.0.0.1:8080/process?hello=world&thread=123 * フォーム内容: * hello: sau sheong * post: 456 `ParseForm`でフォームを解析し、`Form`で取り出した場合、次のようなレスポンスとなる。 ``` map[thread:[123] hello:[sau sheong world] post:[456]] ``` 10行目を`PostForm`に変更した場合 ``` map[post:[456] hello:[sau sheong]] ``` 9行目を`ParseMultipartForm` 10行目を`MultipartForm`に変更した場合 ``` &{map[hello:[sau sheong] post:[456]] map[]} ``` 10行目を`FormValue("hello")`に変更した場合 ```go sau sheong ``` このように、様々な方法でフォームから値を取り出せる。 再掲: ![](https://i.imgur.com/T21QBNF.png) ### 4.2.4 ファイル --- `Content-type:multipart/formdata`が最もよく使われるのは、ファイルアップロードする場合であると思われる。次の例は、アップロードされたファイルをレスポンスする例となっている。 <summary>リスト4.6 MultipartFormフィールドを使用したアップロードファイルの受信</summary> ```go= package main import ( "fmt" "io/ioutil" "net/http" ) func process(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(1024) fileHeader := r.MultipartForm.File["uploaded"][0] file, err := fileHeader.Open() if err == nil { data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(w, string(data)) } } } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() } ``` アップロードされるファイルが1つの場合、以下の[FormFile](https://golang.org/pkg/net/http/#Request.FormFile)で簡略化することができる。 <summary>リスト4.7 FormFileを使用したアップロードファイルの取得</summary> ```go= package main import ( "fmt" "io/ioutil" "net/http" ) func process(w http.ResponseWriter, r *http.Request) { file, _, err := r.FormFile("uploaded") if err == nil { data, err := ioutil.ReadAll(file) if err == nil { fmt.Fprintln(w, string(data)) } } } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() } ``` `Request.FormFile`は以下の戻り値となっている。 ```go func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) ``` [multipart.File](https://golang.org/pkg/mime/multipart/#File)はインターフェースであり、読み込むには`io.Reader`のお作法に従う必要がある。[ioutil.ReadAll](https://golang.org/pkg/io/ioutil/#ReadAll)を用いれば、簡単に読み込みを行う事ができる。 ```go func ReadAll(r io.Reader) ([]byte, error) ``` :gem: 参考: [低レベルアクセスへの入り口(2):io.Reader前編](https://ascii.jp/elem/000/001/252/1252961/) ## 4.3 ResponseWriter クライアントにレスポンスを送信する場合、[ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter)を用いる。`ResponseWriter`には3つのメソッドがある。 1. Header -> レスポンスヘッダーの項目を追加する 2. WriteHeader -> HTTPステータスコードを設定する 3. Write -> レスポンスボディに書き込む 書籍では、以下のコードを分割し、レスポンスを送信する方法について解説している。 ```go= package main import ( "fmt" "encoding/json" "net/http" ) type Post struct { User string Threads []string } func writeExample(w http.ResponseWriter, r *http.Request) { str := `<html> <head><title>Go Web Programming</title></head> <body><h1>Hello World</h1></body> </html>` w.Write([]byte(str)) } func writeHeaderExample(w http.ResponseWriter, r *http.Request) { w.WriteHeader(501) fmt.Fprintln(w, "そのようなサービスはありません。ほかを当たってください") } func headerExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "http://google.com") w.WriteHeader(302) } func jsonExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") post := &Post{ User: "Sau Sheong", Threads: []string{"1番目", "2番目", "3番目"}, } json, _ := json.Marshal(post) w.Write(json) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/write", writeExample) http.HandleFunc("/writeheader", writeHeaderExample) http.HandleFunc("/redirect", headerExample) http.HandleFunc("/json", jsonExample) server.ListenAndServe() } ``` ### 4.3.1 ResponseWriterへの書き込み --- メソッド`Write`は、バイト配列を受け取って、HTTPレスポンスのボディに書き込む。ヘッダにコンテンツタイプが設定されていない場合は、データの先頭512byteでコンテンツタイプを判定する。 <summary>ResponseWriter.Writeより抜粋</summary> ``` If the Header does not contain a Content-Type line, Write adds a Content-Type set to the result of passing the initial 512 bytes of written data to DetectContentType. ``` [DetectContentType](https://golang.org/pkg/net/http/#DetectContentType) 次のコードは、`Write`の使い方をしめしたもの。 <summary>リスト4.8 クライアントにレスポンスを送信するための書き込み</summary> ```go func writeExample(w http.ResponseWriter, r *http.Request) { str := `<html> <head><title>Go Web Programming</title></head> <body><h1>Hello World</h1></body> </html>` w.Write([]byte(str)) } ``` <!-- 確認用 --> <!-- curl -i localhost:8080/write --> 次に`WriteHeader`というメソッドを確認する。これは、HTTPレスポンスの返すステータスコードをセットするためのメソッドである。==注意点として、`WiteHeader`メソッドを実行したあと、ヘッダに書き込むことはできない。== `WriteHeader`は、主にステータスコード200以外を返す際に利用する。 <summary>リスト4.9 WriteHeaderによるレスポンスヘッダの書き込み</summary> ```go func writeHeaderExample(w http.ResponseWriter, r *http.Request) { w.WriteHeader(501) fmt.Fprintln(w, "そのようなサービスはありません。ほかを当たってください") } ``` <!-- 確認用 --> <!-- curl -i localhost:8080/writeheader --> 次に`Header`メソッドは、変更可能なヘッダのマップを返す。ヘッダに項目を設定する場合は、`Header().Set()`を用いる。 下記のコードは、リダイレクトを行っている例である。 ```go func headerExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "http://google.com") w.WriteHeader(302) } ``` <!-- 確認用 --> <!-- curl -i localhost:8080/redirect --> 最後に、`POST`をJSONに変換し、レスポンスする例を示す。`Content-Type`には`application/json`を設定する必要がある。 <summary>リスト4.11 JSON出力の書き込み</summary> ```go func jsonExample(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") post := &Post{ User: "Sau Sheong", Threads: []string{"1番目", "2番目", "3番目"}, } json, _ := json.Marshal(post) w.Write(json) } ``` [json.Marshell](https://golang.org/pkg/encoding/json/#Marshal)によって`struct`を`[]byte`に変換している。 <!-- 確認用 --> <!-- curl -i localhost:8080/json --> ## 4.4 クッキー chitchatで、クッキーを使って認証用のセッションを生成していた。この節では、クライアントにデータを保持させるためにクッキーを利用する方法を説明する。 各節では、以下のコードを抜粋して解説している。 ```go= package main import ( "fmt" "net/http" ) func setCookie(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name: "first_cookie", Value: "Go Web Programming", HttpOnly: true, } c2 := http.Cookie{ Name: "second_cookie", Value: "Manning Publications Co", HttpOnly: true, } http.SetCookie(w, &c1) http.SetCookie(w, &c2) } func getCookie(w http.ResponseWriter, r *http.Request) { c1, err := r.Cookie("first_cookie") if err != nil { fmt.Fprintln(w, "Cannot get the first cookie") } cs := r.Cookies() fmt.Fprintln(w, c1) fmt.Fprintln(w, cs) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_cookie", setCookie) http.HandleFunc("/get_cookie", getCookie) server.ListenAndServe() } ``` ### 4.4.1 Go言語によるクッキーの処理 構造体[Cookie](https://golang.org/pkg/net/http/#Cookie)は以下の通りとなっている。 ```go type Cookie struct { Name string Value string Path string // optional Domain string // optional Expires time.Time // optional RawExpires string // for reading cookies only // 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 SameSite SameSite // Go 1.11 Raw string Unparsed []string // Raw text of unparsed attribute-value pairs } ``` `Cookie.Expires`が指定されていない場合、ブラウザが閉じたれたときに削除される。指定されている場合、期限切れや削除されるまで保持される。 有効期限の指定方法 * MaxAge ブラウザ内で生成されてから、指定された秒数まで有効。IEの古いバージョンではサポートされていない。 * Expires HTTP1.1では、MaxAgeが優先されるため、非推奨。ただしマルチブラウザ対応のため設定する。 ### 4.4.2 ブラウザへのクッキーの送信 構造体`Cookie`を作成し、[http.SetCookie](https://golang.org/pkg/net/http/#SetCookie)によって`ResponseWriter`に`Cookie`をセットする。 <summary>リスト4.14 SetCookieによるブラウザへのクッキー送信</summary> ```go func setCookie(w http.ResponseWriter, r *http.Request) { c1 := http.Cookie{ Name: "first_cookie", Value: "Go Web Programming", HttpOnly: true, } c2 := http.Cookie{ Name: "second_cookie", Value: "Manning Publications Co", HttpOnly: true, } http.SetCookie(w, &c1) http.SetCookie(w, &c2) } ``` <!-- 確認用 --> <!-- curl -i localhost:8080/set_cookie --> ### 4.4.3 ブラウザからのクッキーの取得 下記のコードで取得される。 ただし、単一の文字列である`[]string`での取得となってしまう。 <summary>リスト4.15 ヘッダーからのクッキーの取得</summary> ```go h := r.Header["Cookie"] ``` 個別のキーと値を取りたい場合、Go言語では2種類の方法を用意している。 ```go func (r *Request) Cookie(name string) (*Cookie, error) func (r *Request) Cookies() []*Cookie ``` これらを使った例は、以下のコードとなっている。 <summary>リスト4.16 メソッドCookieとメソッドCookiesの利用</summary> ```go func getCookie(w http.ResponseWriter, r *http.Request) { c1, err := r.Cookie("first_cookie") if err != nil { fmt.Fprintln(w, "Cannot get the first cookie") } cs := r.Cookies() fmt.Fprintln(w, c1) fmt.Fprintln(w, cs) } ``` <!-- 確認用 --> <!-- curl -i localhost:8080/set_cookie --> <!-- curl -i localhost:8080/get_cookie --> ### クッキーによるフラッシュメッセージ 一時的にメッセージを表示させる「フラッシュメッセージ」を実装する。 今回は、ページを再表示すると削除されるセッションクッキーに対して、メッセージを入れるという方法で行う。 <summary>リスト4.17 クッキーを利用したフラッシュメッセージの実装</summary> ```go= package main import ( "encoding/base64" "fmt" "net/http" "time" ) func setMessage(w http.ResponseWriter, r *http.Request) { msg := []byte("Hello World!") c := http.Cookie{ Name: "flash", Value: base64.URLEncoding.EncodeToString(msg), } http.SetCookie(w, &c) } func showMessage(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie("flash") if err != nil { if err == http.ErrNoCookie { fmt.Fprintln(w, "メッセージがありません。") } } else { rc := http.Cookie{ Name: "flash", MaxAge: -1, Expires: time.Unix(1, 0), } http.SetCookie(w, &rc) val, _ := base64.URLEncoding.DecodeString(c.Value) fmt.Fprintln(w, string(val)) } } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/set_message", setMessage) http.HandleFunc("/show_message", showMessage) server.ListenAndServe() } ``` * setMessage * クッキーのセット方法は、前節と同じ。 * 値を`base64`でエンコードしている。空白やパーセント記号などの特殊文字をURLエンコードする必要があるため。 :gem: 参考: [base64ってなんぞ??理解のために実装してみた](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4#base64%E3%81%A8%E3%81%AF) <summary>リスト4.18 メッセージの設定</summary> ```go func setMessage(w http.ResponseWriter, r *http.Request) { msg := []byte("Hello World!") c := http.Cookie{ Name: "flash", Value: base64.URLEncoding.EncodeToString(msg), } http.SetCookie(w, &c) } ``` * showMessage * クッキーを取得。取得できない場合はエラーメッセージを返す。 * 同じ名前のクッキーを作成。`MaxAge`を負の数を設定。`Expires`は過去の有効期限を設定。 * `SetCookie`でブラウザに送信。 <summary>リスト4.19 メッセージの表示</summary> ```go func showMessage(w http.ResponseWriter, r *http.Request) { c, err := r.Cookie("flash") if err != nil { if err == http.ErrNoCookie { fmt.Fprintln(w, "メッセージがありません。") } } else { rc := http.Cookie{ Name: "flash", MaxAge: -1, Expires: time.Unix(1, 0), } http.SetCookie(w, &rc) val, _ := base64.URLEncoding.DecodeString(c.Value) fmt.Fprintln(w, string(val)) } } ``` 既存のクッキーを置き替えているが、MaxAgeフィールドが負の数でExpireフィールドが過去を示しているため、新しいクッキーも削除される(=フラッシュメッセージ)。 ## 4.5 まとめ * Go言語はリクエストを表す各種構造体を提供しており、リクエストからデータを取り出すのに利用できる。 * Go言語の構造体Requestには、Form、PostForm、MultipartFormという3つのフィールドがあり、リクエストから各種データを容易に取り出せる。フィールドからデータを取得するには、メソッドParseFormやParseMultipartFormを呼び出してリクエストを解析してから、必要に応じてForm、PostForm、MultipartFormのフィールドから取得する。 * FormはURLとHTMLフォームのURLエンコードされたデータに対して利用する。PostFormはHTMLフォームのURLエンコードされたデータに対してのみ、MultipartFormはURLとHTMLフォームのマルチパートデータに対して利用する。 * クライアントにデータを送り返すには、ヘッダとボディのデータをResponseWriterに書き込む必要がある。 * クライアント側でデータを保持するには、ResponseWriterでクッキーを送信する。 * クッキーはフラッシュメッセージの実装に利用できる。