6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go における HTTP リクエストの受け取り方

Last updated at Posted at 2022-08-23

前回

HTTPリクエストの構造

net/http におけるハンドラ(関数)は、以下のように *http.Request という引数を持っているのでした。

とあるハンドラ(関数)
func hoge(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "hoge")
}

このハンドラ(関数)の引数にある http.Request の構造は下記のようになっています。

net/http
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 型で返ってきます。

net/http/header.go
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-urlencodedmultipart/form-data の場合、
別のアプローチをとることができます。

その際 http.Request.Body の代わりに、
http.Request.Formhttp.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 の場合

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 の場合

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 型で結果が返ってきていました。

net/url/url.go
type Values map[string][]string

たかが map なので、

  • r.Form["double"][Cyclone Joker]
  • r.Form["double"][0]Cyclone
  • r.Form["double"][1]Joker

を取得できます。

もしスライスの最初の要素さえ取得できればいいなら、
url.ValuesGet() メソッドも使えます。
すなわち r.Form.Get("double")Cyclone を取得できます。

FormValue() と PostFormValue()

そして実は、http.Request.ParseForm() を我々がわざわざ書かずとも、
いきなりキーの値にアクセスする手段が用意されています。

FormValue()
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 にアクセスします。

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 にはファイルの部分が格納されています。
型を確認してみましょう。

mime/multipart/formdata.go
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() メソッドが定義されており、
それを使ってファイルを開くことができます。

multipart/form-data のファイルを取得
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() メソッドのを使用する方が断然ラクです。

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 です。

参考

次回

Go における HTTP レスポンスの返し方

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?