###### 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
```

`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言語
フォームから値を取得する方法について触れる。
フォーム画面例:

```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やボディ、または両方からデータを取得できる関数がある。

一般的なフォームデータの取り出し方は、次の手順通り。
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
```
このように、様々な方法でフォームから値を取り出せる。
再掲:

### 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でクッキーを送信する。
* クッキーはフラッシュメッセージの実装に利用できる。