GO言語のmime/multipartパッケージでファイルをアップロードしましょう

Goによるファイルアップロード(ランタイムの中の様子)

はじめに

こんにちは。エウレカ APIチームのジェームスです。
最近Go言語でファイルをアップロードしたりするAPIクライントを作っていて、色々と学ぶことがありました。今回は基本に戻りマルチパートメッセージの仕様を考えながら、標準ライブラリのmime/multipartパッケージでファイルをアップロードする方法を紹介していきます。
全てのコードはサンプルレポジトリにあります。

マルチパートメッセージのおさらい

マルチパートメッセージはHTTPのデータ転送に利用されることが多いですが、その定義はもともとSMTPメールのために設計されたMIME規格にあります。そのため、「メッセージ」などの通常HTTPリクエストに対して使わない単語が出たりします。

HTTPの場合は、ファイルのアップロードによく使いますが、「マルチパート」という名の通り複数のパート(ファイル、テキストなど)を1つのメッセージの中で転送するための形式です。パートはそれぞれ以下の要素からなります。

  • バウンダリ(パートの区切り文字)
  • ヘッダエリア(通常Content-DispositionContent-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-stream
hello
--c7245ee369df31f524686275eb89381b30581b1ca5557de2453f9f8cf66c--

Go言語でマルチパートメッセージを生成する

Go言語のmime/multipartパッケージはマルチパートメッセージを読み書きするための便利な型や関数を提供しています。メッセージを作るにはmultipart.Writerという型を使います。まずはその定義を見てみましょう。

NewWriterは引数にメッセージ全体のデータを保持するio.Writerw)を取ります。呼び出されたらバウンダリ(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チームが決して喜ばないでしょう。次回はio.Pipeを使った、より効率のいいやり方を見ていきます。

参照

Cover photo by Markus Spiske temporausch.com from Pexels