net/httpメモ
目次
net/httpの概要
サーバとしての機能を備えている必須パッケージ。
ハンドラは勿論、
リクエストからデータを取り出すためのリクエストを表す各種構造体を提供してくれていたり、レスポンスを返すためのインターフェースも実装している。
Request編
httpリクエスト、レスポンスの構造について
- リクエストもレスポンスも構造は基本的に同一で、順に
- リクエスト行、またはレスポンス行
- 0行以上のヘッダ
- 空行
- 任意指定のメッセージボディ
で構成されている
-
curl --verbose https://www.kyoto-u.ac.jp/ja
でリクエストヘッダを見ると
GET /ja HTTP/1.1
Host: www.kyoto-u.ac.jp
User-Agent: curl/7.55.1
Accept: /
(空行)
となっており確かに基本構造に則っている
Request構造体について
-
httpパッケージにはHTTPリクエストについての情報を保持するための、そしてそれらを解析するための構造体
Request
が存在している -
Requestの重要な構成要素は次のようなものがある
- URL
- リクエスト行のURLに関する情報を保持する構造体
- Header(ヘッダ)
- リクエストヘッダ内のキーとバリューを保持
-
type Header map[string][]string
で定義されたマップ
- Body(ボディ)
- リクエストボディ内のキーとバリューを保持
- io.ReadCloserであり、ReadメソッドとCloseメソッドを実装している
- Form、PostForm、MultipartForm
- URLかボディ、またはその両方からのパラメータを保持する(ただし読み取るには専用のメソッドでこれらのフィールドを更新する必要がある)
- mapである(ただしMultipartFormはmapを二つ要素に持つ構造体)
- URL
パラメータを受け取る様々なメソッドとそれらの特徴
既にRequest構造体の重要な構成要素としてForm,PostForm,MultipartFormという三つのフィールドを伝えた。
Form,PostFormはキーが文字列、バリューが文字列のスライスであるマップで、
後で分かることだが前者はURLパラメータとボディパラメータの両方を含み、後者は(名前から分かることだが)ボディパラメータのみを含むことになる。
MultipartFormはForm,PostFormと同様のマップと、それに加えてファイルを格納するためのマップも備えた構造体だ。
マルチパートという特殊なコンテンツタイプを持つボディパラメータにのみ対応している。
-
以上三つのフィールドにパラメータを格納するには、それぞれ特定のメソッドが必要になってくる
-
またそれらとは別に、一つずつパラメータを取り出すのに特化したメソッドが二つ存在するのでそれにも触れていく
補足
- ボディパラメータのコンテンツタイプ、具体的には、実際のHTMLフォームのenctypeで指定されているのは通常URLエンコードかマルチパートの二種類
ParseForm
-
func (r *Request) ParseForm() error
-
ParseForm は URL から生のクエリを解析し、r.Form を更新する
-
ボディにパラメータが存在すればボディも読み込み対象となり結果を r.PostForm と r.Form の両方に入れる。
-
r.Formにおいて、ボディのパラメータは URLパラメータよりもよりも優先される。
つまり、(r.Formは文字列:文字列のスライス のマップなので)同じキー名のバリューがURL、ボディそれぞれに存在していたらそのキーに対応するバリューのスライスの先頭にくるのはボディパラメータの方ということ -
このことはFormValueで効いてくる
-
でもキー名が同名のパラメータがGET、POSTで同時に渡される機会なんてあるの?無さそう
ParseMultipartForm
-
func (r *Request) ParseMultipartForm(maxMemory int64) error
-
引数で指定したバイト数分だけマルチパートのボディパラメータを取得し、MultipartForm構造体の一つ目のマップに格納する
-
MultipartForm構造体の二番目のマップにはファイルを格納することになる
FormValue
-
func (r *Request) FormValue(key string) string
-
これまでのパラメータを取り出す例では、
- ParseForm等のメソッドを呼び出し
- Form等にパラメータを格納する
-
といった二段階の処理をこちらで行っていたが、
FormValueメソッド、そして次に紹介するPostFormValueメソッドではその必要がない -
FormValueは必要に応じてParseMultipartFormとParseFormを呼び出し、 Formからキーに対応したバリューを一つだけ返す
PostFormValue
-
func (r *Request) PostFormValue(key string) string
-
PostFormValueは必要に応じてParseMultipartFormとParseFormを呼び出し、 PostFormからキーに対応したバリューを一つだけ返す
ファイルをアップロードする
-
ボディパラメータのコンテンツタイプがURLエンコードかマルチパートの二種類であることは説明した
-
一般的にはURLエンコードが多いが、特定の場合にマルチパートが使われる
-
その特定の場合とはファイルのアップロードを目的としたリクエストの場合である
-
ファイルのアップロードができるHTMLは次のようになっている
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=utf-8" />
<title>Go Web Programming</title>
</head>
<body>
<form action="http://localhost:8080/process"
method="post" enctype="multipart/form-data">
<input type="file" name="uploaded">
<input type="submit">
</form>
</body>
</html>
-
http://localhost:8080/process
にPOSTメソッドで(method="post"
)コンテンツタイプをマルチパートとして(enctype="multipart/form-data"
)ファイルをsubmitしている -
ハンドラの方は次の通り
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()
}
-
何度も出ていることだがMultipartFormは構造体で、マルチコンテンツのパラメータを保持するマップとは別にファイルを保持するマップを要素に持っている
-
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["uploaded"][0]
file, err := fileHeader.Open()
メソッドParseMultipartFormを呼び出すと、MultipartFormフィールドのFileフィールドからFileHeaderを取得し、そのOpenメソッドを呼び出してファイルを取得している -
data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
サーバにファイルを送るとハンドラがそのファイルを受け取ってバイト配列に読み込み、それをブラウザ上に表示する
- さて、MultipartFormをより具体的に見るとこんな構造をしている
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64 // Go 1.9
// contains filtered or unexported fields
}
- 試しにFileHeaderの値が見たいのでハンドラ関数を変更する
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["uploaded"][0]
fmt.Fprintln(w, fileHeader)
}
- アクセスするとこんな感じになる
- 送られるファイルが一つの時は、FormValueメソッドのみたくFormFileという簡単なメソッドがあるのでそちらがおすすめ
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))
}
}
}
Response編
http.ResponseWriterと各メソッド
-
ハンドラ関数は必ず
func(w http.ResponseWriter, r *http.Request)
という形をとる。これは今まで例で見てきたとおり -
Responseを返すにはハンドラ関数に第一引数として渡るhttp.ResponseWriterに内容を書き込んでいることが分かる
-
ResponseWriterは名前の通り、Writerメソッドを実装し、io.Writer型である。これはfmt.Fprint()が使えることからも分かる
-
また、ResponseWriterはio.Writer型であると同時に、独自のメソッドを要素に持つインターフェースである。
type ResponseWriter interface {
Header() Header
// type Header map[string][]string
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
-
ここで説明する ResponseWriter interfaceのメソッドは
Write([]byte) (int, os.Error)
Header() Header
-
WriteHeader(statusCode int)
の三種
-
Write([]byte) (int, os.Error)
-
既に述べたとおりio.Writerのメソッド。
ResponseWriter.Write(バイトスライス)
の形でレスポンス内容を流し込むのにつかえる。
そのうえio.Writer型を実装できるのでその他便利関数も使えるようになる -
内容はレスポンスのボディに書き込まれる
-
-
Header() Header
- こちらで変更可能なヘッダのマップを返す(ヘッダはキー:バリューのマップに相応しい形式)
-
返値のマップ.set("キー名":"バリュー")
とすることでヘッダに追加できる
-
WriteHeader(statusCode int)
- ステータスコードを引数にとり、それをレスポンスのステータスコードへ書き込む
参考
Goプログラミング実践入門 ―標準ライブラリでゼロからWebアプリを作る―
gihyo.jp - 第4章 標準パッケージ―JSON,ファイル,HTTP,HTMLを扱う
公式doc