前回
HTTPリクエストの構造
net/http
におけるハンドラ(関数)は、以下のように *http.Request
という引数を持っているのでした。
func hoge(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hoge")
}
このハンドラ(関数)の引数にある http.Request
の構造は下記のようになっています。
type Request struct {
// 省略
Header Header
// 省略
Body io.ReadCloser
// 省略
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
// 省略
}
net/http
を用いた Web アプリでは、
構造体 Request
を用いて HTTP リクエストのヘッダやボディを取得できます。
リクエストヘッダの取得
リクエストヘッダを取得するには http.Request.Header
にアクセスします。
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
h := r.Header
fmt.Fprintln(w, h)
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
結果:
map[Accept:[*/*] Accept-Encoding:[gzip, deflate, br] Connection:[keep-alive] Content-Length:[50] Content-Type:[application/json] ...(省略)... User-Agent:[PostmanRuntime/7.29.2]]
Header
型で返ってきます。
type Header map[string][]string
Header
型はただの map に過ぎないので、
-
r.Header["Accept-Encoding"]
で[gzip, deflate, br]
(スライス) -
r.Header["Accept-Encoding"][0]
でgzip, deflate, br
(文字列)
と、特定のキーの値を取得できますが、
-
r.Header.Get("Accept-Encoding")
でgzip, deflate, br
(文字列)
とすることもできます。
リクエストボディの取得
リクエストボディを取得するには http.Request.Body
にアクセスします。
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
len := r.ContentLength
body := make([]byte, len) // Content-Length と同じサイズの byte 配列を用意
r.Body.Read(body) // byte 配列にリクエストボディを読み込む
fmt.Fprintln(w, string(body))
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
このコードに対して Content-Type が application/json
のリクエストを投げると、
{
"Cyclone": "Joker",
"Heat": "Metal",
"Luna": "Trigger"
}
JSON がそのまま返ってきます。(それはそう)
...
もしリクエストの Content-Type が、
application/x-www-form-urlencoded
や multipart/form-data
の場合、
別のアプローチをとることができます。
その際 http.Request.Body
の代わりに、
http.Request.Form
や http.Request.MultipartForm
にアクセスします。
application/x-www-form-urlencoded を処理する
HTML の <form method="post">
において、
デフォルトで指定される Content-Type が application/x-www-form-urlencoded
です。
単純なテキストの送信に適しています。
Content-Type が application/x-www-form-urlencoded
のリクエストを処理するときは、
http.Request.Form
あるいは http.Request.PostForm
にアクセスします。
http.Request.Form の場合
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // リクエスト解析 & http.Request.Form へデータ投入
fmt.Printf("%T", r.Form) // url.Values
fmt.Fprintln(w, r.Form)
}
func main() {
server := http.Server{
Addr: "0.0.0.0:8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
これに対して、ボディが application/x-www-form-urlencoded
の POST を送ってみます。
http://localhost:8080?left=Shotaro&double=Joker
- right: Philip
- double: Cyclone
結果:
map[double:[Cyclone Joker] left:[Shotaro] right:[Philip]]
ボディだけでなくクエリパラメータも取得できました。
ボディとクエリパラメータでキーが重複する場合、
両方の値がスライスに取り込まれます。(ボディの値が先に来る)
http.Request.PostForm の場合
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // リクエスト解析 & http.Request.PostForm へデータ投入
fmt.Printf("%T", r.PostForm) // url.Values
fmt.Fprintln(w, r.PostForm)
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
同様に、ボディが application/x-www-form-urlencoded
の POST を送ってみます。
http://localhost:8080?left=Shotaro&double=Joker
- right: Philip
- double: Cyclone
結果:
map[double:[Cyclone] right:[Philip]]
ボディのみが取得され、クエリパラメータが無視されています。
キーの値にアクセス!
http.Request.Form
でも http.Request.PostForm
でも
url.Values
型で結果が返ってきていました。
type Values map[string][]string
たかが map なので、
-
r.Form["double"]
で[Cyclone Joker]
-
r.Form["double"][0]
でCyclone
-
r.Form["double"][1]
でJoker
を取得できます。
もしスライスの最初の要素さえ取得できればいいなら、
url.Values
の Get()
メソッドも使えます。
すなわち r.Form.Get("double")
で Cyclone
を取得できます。
FormValue() と PostFormValue()
そして実は、http.Request.ParseForm()
を我々がわざわざ書かずとも、
いきなりキーの値にアクセスする手段が用意されています。
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.FormValue("double"))
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
結果:Cyclone
これもスライスの最初の要素しか取得できないことに注意しましょう。
http.Request.PostFormValue()
は、
名前から察せるように、クエリパラメータを無視します。
multipart/form-data を処理する
ブラウザによって必ずサポートされている Content-Type が、
前述のapplication/x-www-form-urlencoded
と、
もう一つが multipart/fomr-data
です。
multipart/form-data
は、ファイルアップロードのような大量データを送信するのに適しています。
multipart/form-data
のリクエストを処理するときは、
http.Request.MultipartForm
にアクセスします。
package main
import (
"fmt"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024) // リクエスト解析 & http.Request.MultipartForm へデータ投入
// 引数(maxMemory): The whole request body is parsed and up to a total of maxMemory bytes of its file parts are stored in memory, with the remainder stored on disk in temporary files.
fmt.Printf("%T", r.MultipartForm) // *multipart.Form
fmt.Fprintln(w, r.MultipartForm)
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
これに対して、ボディが multipart/form-data
の POST を送ってみます。
http://localhost:8080?left=Shotaro&double=Joker
- right: Philip
- double: Cyclone
- cutecat: (かわいい猫の画像)
結果:
&{map[double:[Cyclone] right:[Philip]] map[cutecat:[0xc0001b8000]]}
2つの map を含んだ構造体が得られました。
クエリパラメータは入っていません。
1つ目の map には multipart/form-data
のファイル以外の部分が、
2つ目の map にはファイルの部分が格納されています。
型を確認してみましょう。
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64
content []byte
tmpfile string
}
*multipart.FileHeader
型には Open()
メソッドが定義されており、
それを使ってファイルを開くことができます。
package main
import (
"fmt"
"io"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024) // リクエスト解析 & http.Request.MultipartForm へデータ投入
// 引数(maxMemory): The whole request body is parsed and up to a total of maxMemory bytes of its file parts are stored in memory, with the remainder stored on disk in temporary files.
fileHeader := r.MultipartForm.File["cutecat"][0]
file, err := fileHeader.Open()
if err == nil {
data, err := io.ReadAll(file)
if err == nil {
fmt.Fprint(w, string(data))
}
}
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
このコードを実行すると、アップロードした cutecat
の画像がそのまま返ってきます。
今回のようにアップロードする画像が1つだけなら、
http.Request.FormFile()
メソッドのを使用する方が断然ラクです。
package main
import (
"fmt"
"io"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
file, _, err := r.FormFile("cutecat")
if err == nil {
data, err := io.ReadAll(file)
if err == nil {
fmt.Fprint(w, string(data))
}
}
}
func main() {
server := http.Server{
Addr: ":8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
http.Request.ParseMultipartForm()
とか *multipart.FileHeader
を意識しなくてよくなりました!
ちなみに http.Request.FormFile()
の2つ目の戻り値は *multipart.FileHeader
です。
参考
- https://pkg.go.dev/net/http
- Goプログラミング実践入門
次回
Go における HTTP レスポンスの返し方