1
2

【Go】リクエスト

Last updated at Posted at 2024-07-09

今読んでいる技術書の内容で大事だと思った部分を少しずつまとめていきたいと思います。

今回読んでいる本はこちら
Goプログラミング実践入門

この書籍では、フレームワークに頼らず標準ライブラリでweb開発をすることができます。
個人的にフレームワークはブラックボックスが多すぎて面倒であまり好きではない派なので、これを理解できることで理解が深まるのではないかと考え購入しました。

この本ではプログラミング言語Goとその標準のライブラリだけを使って、
ゼロからWebアプリケーションを開発するのに必要な事柄を解説します。

ほかのライブラリやその他のトピック(Webアプリケーションのテストやデプロイなど)について解説するページもありますが、Go言語の標準ライブラリのみを用いたWeb開発を解説することがこの本の主目的です。

本記事では、リクエストとレスポンスについてまとめています。

Request

まず、net/httpパッケージのhttp.Request構造体は以下の通り定義されています。

フィールド 説明
Method HTTPメソッド(GET, POST, PUTなど)を指定します。クライアントリクエストの場合、空文字列はGETを意味します。
URL 要求されているURI(サーバーリクエストの場合)またはアクセスするURL(クライアントリクエストの場合)を指定します。
Proto サーバーリクエストのプロトコルバージョンを指定します。
ProtoMajor プロトコルのメジャーバージョン(例:1)。
ProtoMinor プロトコルのマイナーバージョン(例:0)。
Header リクエストヘッダーを含みます。
Body リクエストのボディ。クライアントリクエストの場合、nilはボディがないことを意味します。
GetBody クライアントリクエストでリダイレクトが必要な場合に、ボディの新しいコピーを返すための関数。
ContentLength 関連するコンテンツの長さを記録します。-1は長さが不明であることを示します。
TransferEncoding 外側から内側への転送エンコーディングのリストを示します。
Close 応答後に接続を閉じるかどうかを示します。
Host サーバーリクエストの場合、URLが探されるホストを指定します。クライアントリクエストの場合、送信するHostヘッダーをオーバーライドします。
Form 解析されたフォームデータ(URLフィールドのクエリパラメーターとPATCH, POST, PUTのフォームデータを含む)。
PostForm PATCH, POST, PUTのボディパラメーターから解析されたフォームデータ。
MultipartForm 解析されたマルチパートフォーム(ファイルアップロードを含む)。
Trailer リクエストボディの後に送信される追加ヘッダーを指定します。
RemoteAddr リクエストを送信したネットワークアドレス。
RequestURI クライアントがサーバーに送信したリクエストターゲットの未修正のRequest-Line。
TLS リクエストが受信されたTLS接続に関する情報を記録します。
Cancel クライアントリクエストがキャンセルされるべきことを示すチャネル。
Response このリクエストが作成される原因となったリダイレクト応答。
ctx クライアントまたはサーバーのコンテキスト。
pat ServeMuxによってマッチされたリクエストのパターン。
matches パターンのワイルドカードに対応する値。
otherValues ワイルドカードと一致しないPathValueの設定に使用される値。

リクエストのURL

フィールド名 説明
Scheme string スキーム (例: "http", "https")
Opaque string エンコードされた不透明データ
User *Userinfo ユーザー名とパスワード情報
Host string ホストまたはホスト:ポート (例: "example.com", "example.com:8080")
Path string パス (相対パスは先頭のスラッシュを省略可能)
RawPath string エンコードされたパスのヒント (EscapedPath メソッド参照)
OmitHost bool 空のホスト (authority) を出力しない
ForceQuery bool RawQuery が空でもクエリ ('?') を追加する
RawQuery string エンコードされたクエリ値 ( '?' は含まない)
Fragment string 参照用のフラグメント ( '#' は含まない)
RawFragment string エンコードされたフラグメントのヒント (EscapedFragment メソッド参照)

URLの一般形式を以下のフィールド名を使って表すと、次のようになります。

Scheme://[UserInfo@]Host[Path][?RawQuery][#Fragment]

例えば、以下のURLの場合URL.RawQueryを使うと、次のような結果が得られます。
http://example.com/search?q=golang&lang=en

u, _ := url.Parse("http://example.com/search?q=golang&lang=en")
fmt.Println(u.RawQuery) // "q=golang&lang=en"

同じようにSchemePathなども取得して使用できます。便利ですね。

リクエストヘッダ 値の追加・削除・取得・設定

リクエストとレスポンスのヘッダはHeader型で記述され、この型はHTTPヘッダ内のキーと値のペアを表すマップになっています。キーは文字列型、値は文字列型のスライスです。type Header map[string][]string

具体的にはこんな形です。

Header{
    "Content-Type": {"application/json"},
    "Accept": {"text/html", "application/xhtml+xml", "application/xml;q=0.9", "*/*;q=0.8"},
    "X-Custom-Header": {"custom value"},
}

それぞれのメソッドはheader.goに定義されていますが、使い方は以下の通りです。

func handler(w http.ResponseWriter, r *http.Request) {
	// 値の追加
	r.Header.Add("X-Custom-Header", "custom value")
	w.Header().Add("Content-Type", "application/json")

	// 値の取得
    customHeader := r.Header
	customHeader := r.Header.Get("X-Custom-Header")
	contentType  := w.Header().Get("Content-Type")

	// 値の設定
	r.Header.Set("X-Custom-Header", "new custom value")
	w.Header().Set("Content-Type", "application/xml")

	// 値の削除
	r.Header.Del("X-Custom-Header")
	w.Header().Del("Content-Type")
}

リクエストボディ

リクエストとレスポンスのボディはともにBodyフィールドで表され、このフィールドはインタフェースio.ReadCloserで実現されます。

引用
Body の型として io.ReadCloserというものが指定されています。これは標準パッケージ io の中で定義されているインターフェース型で、2 つのメソッドを持ちそれぞれ以下のような処理を行うことができます。

• Read メソッド: ボディの中身を、引数として渡したバイトスライスに格納する
• Close メソッド: ボディの中身を読み終わった時にcloseする

Read(p []byte) (n int, err error)
Close() error

そのため、http.Request.Body の中からリクエストボディの内容を読み出す処理としては、

  1. 何らかの方法でバイトスライスを用意
  2. Body の Read メソッドを呼び出して、1 で用意したバイトスライスに内容を書き込む
  3. 使い終わった Body を Close メソッドで閉じる
    という3ステップとなります。

io.ReadAllにbodyを渡すことで初期に512バイトのスライスを作成し、容量が足りなくなったら追加で増やしそこに書き込んでいきます。最終的に書き込まれたスライスとエラーを返却します。

io.go
func ReadAll(r Reader) ([]byte, error) {
	b := make([]byte, 0, 512)
	for {
		n, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+n]
		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}

		if len(b) == cap(b) {
			// Add more capacity (let append pick how much).
			b = append(b, 0)[:len(b)]
		}
	}
}

これを使ってハンドラーを作成します。
大きな流れとして、リクエストのBodyに格納されたデータをio.ReadAllに渡し、バイトスライスを取得します。そして、それを文字列へ型変換しFPrintwに書き込みます。

package main

import (
	"fmt"
	"io"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request){
	body, err := io.ReadAll(r.Body)
	if err != nil{
		http.Error(w, "Failed to read request body", http.StatusInternalServerError)
		fmt.Printf("something went wrong %s", err)
		return
	}
	defer r.Body.Close()
	fmt.Fprint(w, string(body))
}

func main(){
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}

	http.HandleFunc("/body", handler)
	server.ListenAndServe()
}

そして、以下のようにPOSTでボディに'Hello, World!'とデータを指定してPOSTすると、Hello, World!とレスポンスが返ってきました。

curl -X POST http://127.0.0.1:8080/body -d 'Hello, World!'
Hello, World!

io.ReadAllで返却されるのはバイトスライスなので今回の場合は[72 101 108 108 111 44 32 87 111 114 108 100 33]が返却されます。このままでは使えないので、stringへ型変換することで期待しているHello, World!という文字列になります。

また、今回はfmt.Fprintを使用していますがw.Write(body)でも書き込むことが可能です。

EOFについて

ReadAllメソッドの中でEOF(End Of File)かどうか判定していますが、ここを少し深ぼっておきます。
例えば、RequestにはReadメソッドが用意されていますが、このメソッドはバイトスライスを引数で受け取り、読み取ったデータをそこに書き込んでくれます。

そして、正常にボディの中身を最後まで読み取ることができた場合はエラーとしてio.EOFを返却します。そして、エラーがio.EOF以外だった場合はボディの読み取り中に異常が発生したことを意味します。

これを先ほどのReadAllメソッドで確認すると以下のように判定しています。
つまり、エラーの場合でもEOFは厳密には正常終了なので、エラーは空として扱います。それ以外は通常通りエラーなので、エラーを返却しています。

		if err != nil {
			if err == EOF {
				err = nil
			}
			return b, err
		}

正常に終了していますがEOFはエラーとして返ってくるので、err != nilと安易に判定しないよう注意が必要です。

HTMLフォームとGO言語

POSTリクエストからのフォームデータの取得に深入りする前にHTMLフォームについて詳しくみておく。

enctype(encoding type)は、HTMLフォームがデータを送信する際のエンコーディング方法を指定するための属性です。特にPOSTリクエストでフォームデータを送信する際に使われます。enctype属性にはいくつかの異なる値があり、それぞれの形式によってデータが異なる方法でエンコードされます。

application/x-www-form-urlencoded(デフォルト値)

テキストデータやURLで使用される標準的な形式

  • フォームデータをURLエンコードされた形式で送信
  • すべての文字はキーバリューペア(name=value)としてエンコードされ、&で区切られる
    例: name1=value1&name2=value2
<form action="/submit" method="post" enctype="application/x-www-form-urlencoded">
  <input type="text" name="username" value="user1">
  <input type="submit" value="Submit">
</form>

multipart/form-data

ファイルアップロード時やバイナリデータの送信時に使用

  • バイナリデータやファイルを含むフォームデータを送信する際に使用
  • フォームデータは境界線(boundary)で区切られた部分(パート)に分けられ、各パートにはヘッダと内容が含まれる
--boundary
Content-Disposition: form-data; name="field1"

value1
--boundary
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain

(file content here)
--boundary--
<form action="/submit" method="post" enctype="multipart/form-data">
  <input type="text" name="username" value="user1">
  <input type="file" name="profile_picture">
  <input type="submit" value="Submit">
</form>

text/plainもありますが、ほとんど使用されないため割愛。

では、これらのテキストやファイルデータをどのようにGo側で使用することができるのか確認します。

基本的にはParseForm()もしくはParseMultipartFormを使用してリクエストを解析し、フィールドにアクセスし取得することができる。

例えばapplication/x-www-form-urlencodedの場合、ざっくりと以下のように取得できる。

r.ParseForm()
username := r.Form("username")

この2つは、以下のような使い分けで使用することができる。

形式 application/x-www-form-urlencoded multipart/form-data クエリパラメータを含む 解析の必要性
フォームデータの解析方法 r.ParseForm() r.ParseMultipartForm(maxMemory) - -
取得方法 r.Form -
取得方法 r.PostForm r.MultipartForm ×
取得方法 r.FormValue - ×
取得方法 r.PostFormValue - × ×
取得方法 - r.FormFile × ×

* ただし、ParseMultipartFormで解析する際はマルチパートのフォームから何バイト取得するかを指定する必要がある。

それぞれ実際に取得できる値を試してみた。
リクエストで使用するコマンドは以下の通りでフォームデータにはname=formData、クエリパラメータにはqueryParamを指定した。(ただし、マルチパートの場合は-dではなく-Fとする。)

%curl -X POST -d "name=formData" "http://127.0.0.1:8080/test?name=queryParam"
map[name:[formData queryParam]]

r.Form

期待:解析後、フォームデータとクエリパラメータを取得できること

形式 application/x-www-form-urlencoded multipart/form-data クエリパラメータを含む 解析の必要性
フォームデータの解析方法 r.ParseForm() r.ParseMultipartForm(maxMemory) - -
取得方法 r.Form -
func hello(w http.ResponseWriter, r *http.Request){
	r.ParseForm()
	fmt.Fprintln(w, r.Form)
}

解析後、Formにアクセスするとフォームデータとクエリパラメータの両方を取得できている。

%curl -X POST -d "name=formData" "http://127.0.0.1:8080/test?name=queryParam"
map[name:[formData queryParam]]

ちなみにFormは解析が必要だが、しない場合はエラーではなく空のマップが返却された。

%curl "http://127.0.0.1:8080/test?name=example"
map[]

r.PostForm & MultipartForm

期待:解析後、フォームデータを取得できること

形式 application/x-www-form-urlencoded multipart/form-data クエリパラメータを含む 解析の必要性
フォームデータの解析方法 r.ParseForm() r.ParseMultipartForm(maxMemory) - -
取得方法 r.PostForm r.MultipartForm ×

まずPostFormを使って、

func hello(w http.ResponseWriter, r *http.Request){
	r.ParseForm()
	fmt.Fprintln(w, r.PostForm)
}

リクエストを投げるとフォームデータのみ取得できている。

curl -X POST -d "name=formData" "http://127.0.0.1:8080/test?name=queryParam"
map[name:[formData]]

次にMultipartFormを使うと、

func hello(w http.ResponseWriter, r *http.Request){
	r.ParseMultipartForm(32 << 20) // 32MBのメモリを使用
	fmt.Fprintln(w, r.MultipartForm)
}

以下の通りとなった。

%curl -X POST -F "name=formData" "http://127.0.0.1:8080/test?name=queryParam"
&{map[name:[formData]] map[]}

r.FormValue

期待:解析なしで、フォームデータとクエリパラメータを取得できること

形式 application/x-www-form-urlencoded multipart/form-data クエリパラメータを含む 解析の必要性
フォームデータの解析方法 r.ParseForm() r.ParseMultipartForm(maxMemory) - -
取得方法 r.FormValue - ×

続いて、r.FormValueを使用します。ただし、これはキーを指定する必要があります。

func hello(w http.ResponseWriter, r *http.Request){
	fmt.Fprintln(w, r.FormValue("name"))
}

フォームデータのみを取得することができました。
これは期待していたことと違います。ただし、同じキーに複数の値がある場合には最初の値を返します。
フォームデータとクエリパラメータの場合はフォームデータが最初の値であるため、こちらが返却される形となったようです。

%curl -X POST -d "name=formData" "http://127.0.0.1:8080/test?name=queryParam"
formData

試しに、データを消してリクエストするとクエリパラメータを取得できました。

%curl -X POST "http://127.0.0.1:8080/test?name=queryParam" 
queryParam

以上よりFormValueではフォームデータとクエリパラメータのどちらも取得できるが、同時に取得することはできないようです。

もし、複数取りたい場合はFormを使用するか、r.URL.Query().Get("name")のように別で取得するようにする必要があります。

r.PostFormValue

期待:解析なしで、フォームデータを取得できること

形式 application/x-www-form-urlencoded multipart/form-data クエリパラメータを含む 解析の必要性
フォームデータの解析方法 r.ParseForm() r.ParseMultipartForm(maxMemory) - -
取得方法 r.PostFormValue - × ×

最後にPostFormValueを使うと、

func hello(w http.ResponseWriter, r *http.Request){
	fmt.Fprintln(w, r.PostFormValue("name"))
}

以下のようにリクエストするとフォームデータを取得できました。ちなみにクエリパラメータを取得できないことも確認できました。

%curl -X POST -d "name=formData" "http://127.0.0.1:8080/test?name=queryParam"

formData

これらをざっくりまとめると、こんな感じ

multipart/form-data: MultipartFormもしくは後述のFormFile
application/x-www-form-urlencoded: 取得したいデータに応じて使い分ける

ではファイルアップロードしてみる

ここまでリクエストからデータを取得する方法についてみてきました。
その中でmultipart/form-dataが出てきました。

これはファイルのアップロードをする場合によく使われます。それをGoで実装してみたいと思います。

以下のようにenctype="multipart/form-data"type="file"を用意しました。
これをサーバーに送ってGoで受信してみます。

<form action="/submit" method="post" enctype="multipart/form-data">
    <input type="file" name="uploaded">
    <input type="submit" value="Upload">
</form>

コードはこんな感じになります。

package main

import (
	"fmt"
	"io"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request){
	r.ParseMultipartForm(1024)
	fileHeader := r.MultipartForm.File["uploaded"][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: "127.0.0.1:8080",
	}

	http.HandleFunc("/process", handler)
	server.ListenAndServe()
}

MultiPartフィールドのFileフィールドからを取得します。
Fileフィールドは*FileHeaderのマップを返すようです。

type Form struct {
	Value map[string][]string
	File  map[string][]*FileHeader
}

で、そのFileHeader構造体が以下の通りです。

type FileHeader struct {
	Filename string
	Header   textproto.MIMEHeader
	Size     int64
	content   []byte
	tmpfile   string
	tmpoff    int64
	tmpshared bool
}

イメージしづらいので具体的にForm構造体にはこんな値が入るんじゃないかなと思います。

form := &Form{
	Value: map[string][]string{
		"username": {"john_doe"},
		"email":    {"john@example.com"},
	},
	File: map[string][]*FileHeader{
		"uploaded": {
			{
				Filename: "example.txt",
				Header: textproto.MIMEHeader{
					"Content-Disposition": {"form-data; name=\"uploaded\"; filename=\"example.txt\""},
					"Content-Type":        {"text/plain"},
				},
				Size:    1234,
				content: []byte("This is the content of the file."),
			},
		},
	},
}

したがって、File["uploaded"][0]でファイルの情報を取得できるようです。(複数ある場合はfor rangeで取得)

そして、この情報をfileHeader.Open()メソッドを使ってその内容を読み取ることができます。

この関数は、ファイルの内容がメモリ内にあるかディスク上にあるかに応じて適切な方法でファイルを開き、ファイルを返却します。

最後に、そのファイルをio.ReadAllで読み込み問題なければwに文字列で書き込みます。

func (fh *FileHeader) Open() (File, error) {
	if b := fh.content; b != nil {
		r := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b)))
		return sectionReadCloser{r, nil}, nil
	}
	if fh.tmpshared {
		f, err := os.Open(fh.tmpfile)
		if err != nil {
			return nil, err
		}
		r := io.NewSectionReader(f, fh.tmpoff, fh.Size)
		return sectionReadCloser{r, f}, nil
	}
	return os.Open(fh.tmpfile)
}

しかし、これだと長い

実は上のような書き方とは別にFormFileを使用するとより簡潔にアップロードファイルを取得することができます。

こちらだと解析はしなくて良い上にfileHeader.Open()の手間も減るのでこちらを使う方が良さそうです。
ただ、複数ファイルには対応できないので、その場合は先述のr.MultipartFormを使った書き方になりそうです。

func handler(w http.ResponseWriter, r *http.Request){
	file, _, err := r.FormFile("uploaded")
	if err == nil {
		data, err := io.ReadAll(file)
		if err == nil {
			fmt.Fprintln(w, string(data))
		}
	}
}
1
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
1
2