Go言語のmime/multipartパッケージで
ファイルをアップロードしましょう
はじめに
こんにちは。エウレカ APIチームのジェームスです。
最近Go言語でファイルをアップロードしたりするAPIクライントを作っていて、色々と学ぶことがありました。今回は基本に戻りマルチパートメッセージの仕様を考えながら、標準ライブラリのmime/multipart
パッケージでファイルをアップロードする方法を紹介していきます。
全てのコードはサンプルレポジトリにあります。
マルチパートメッセージのおさらい
マルチパートメッセージはHTTPのデータ転送に利用されることが多いですが、その定義はもともとSMTPメールのために設計されたMIME規格にあります。そのため、「メッセージ」などの通常HTTPリクエストに対して使わない単語が出たりします。
HTTPの場合は、ファイルのアップロードによく使いますが、「マルチパート」という名の通り複数のパート(ファイル、テキストなど)を1つのメッセージの中で転送するための形式です。パートはそれぞれ以下の要素からなります。
- バウンダリ(パートの区切り文字)
- ヘッダエリア(通常
Content-Disposition
とContent-Type
) - 空白行
- ボディエリア(ファイルの中身など)
バウンダリはボディエリアの中で出現してはいけないので、基本はランダムに生成されます。Content-Disposition
ヘッダはファイル名やフィールド名など、パートのフィールドについての情報を含みます。Content-Type
ヘッダの種類についてはウェブ開発者向けの重要な MIME タイプを参照してください。
マルチパートのHTTPリクエストを送信する時に、リクエストレベルのContent-Type
ヘッダにmultipart/form-data
というサブタイプを指定しバウンダリも含めます。ボディにはパートを1つ以上含め、末尾にバウンダリがもう一度挿入されます。
以下はHTTPリクエスト例です。バウンダリがそれぞれContent-Type
ヘッダ、パートの冒頭、メッセージの末尾にあることが確認できます。
POST /upload HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip
Content-Length: 254
Content-Type: multipart/form-data; boundary=c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c
User-Agent: Go-http-client/1.1--c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c
Content-Disposition: form-data; name="file"; filename="hello.txt"
Content-Type: application/octet-streamhello--c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c--
Go言語でマルチパートメッセージを生成する
Go言語のmime/multipart
パッケージはマルチパートメッセージを読み書きするための便利な型や関数を提供しています。メッセージを作るにはmultipart.Writer
という型を使います。まずはその定義を見てみましょう。
NewWriter
は引数にメッセージ全体のデータを保持するio.Writer
(w
)を取ります。呼び出されたらバウンダリ(boundary
)に使うbase-16文字列がランダムに生成されます。パートを追加するたびにw
にそのデータが書き込まれ、最後に追加したパートのポインター(lastpart
)が更新されます。
パートを追加するには、ヘッダを引数にCreatePart
を呼び出し、返却されるio.Writer
にパートのボディを書き込みます。最後にClose
の呼び出しでメッセージの最終バウンダリが挿入されます。その流れをコードで見てみましょう。
パート毎にヘッダを設定するのが冗長になるため、それを省いてくれるCreateFromFile
(ファイル用)とCreateFromField
(フォームフィールド用)メソッドが準備されています。他にもリクエストのContent-Type
ヘッダに使える値を出すFormDataContentType
もあります。以下のようにバウンダリを含みます。
multipart/form-data; boundary=0d9f057fe9d23d97213ee9b391c3acff605dbde7478fdb97e079f4649a0e
ファイルをアップロードする
これでファイルをアップロードするための知識が全て揃っています。コードコメントと一緒にサンプルを一通り見てみましょう。
ファイルとリクエストの操作以外は前とほぼ同じ流れです。*bytes.Buffer
を使ったのでContent-Length
ヘッダはhttp.Request
が生成される時に自動で設定されます。アップロードが確認できるように簡単なサーバーを作っておきましょう。
サーバーを起動してからアップロードのリクエストをもう一度送信したら、サーバー側でhttputil.DumpRequest
の出力が上記のHTTPリクエスト例とほぼ同じになります。サンプルレポジトリで試すことができます。
まとめ
Go言語の標準ライブラリのパッケージは使い勝手が良く、読みやすいコードに繋がることが多いです。mime/multipart
でも完結で分かりやすいコードでファイルアップロードが実装できました。
ところが、注意事項が1つあります。今回のサンプルではリクエストが送信される時にボディがファイルの中身全体を保持していました。ファイルが小さい場合は問題ないかもしれませんが、大きい場合はSREチームが決して喜ばないでしょう。「Go言語のio.Pipeでファイルを効率よくアップロードする方法」ではio.Pipe
を使った、より効率のいいやり方を見ていきます。
参照
- Multipurpose Internet Mail Extensions(ウィキペディア)
- RFC 2046の仕様書(RFCの部屋)
- Package multipart(golang.org)
Cover photo by Markus Spiske temporausch.com from Pexels