この記事は every Tech Blog Advent Calendar 2024(夏) 25 日目の記事です。
はじめに
こんにちは、24 新卒として 4 月から入社し、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている新谷です。
現在取り組んでいる業務で、画像を受け取って外部に送信する処理を行う API を作成する機会がありました。そこで学んだ、Go 言語における multipart/form-data
を使った画像の取り扱い方について紹介します。
背景
開発した機能としては、クライアントがアップロードした画像を API サーバーで受け取り、外部の API に POST するというものです。開発した部分は以下の図で表すと、API サーバーの部分になります。
この処理を実現するためには、画像データの受信かつ送信を行う必要があります。今回は、画像を取り扱う際によく使われるmultipart/form-data
形式を使用して画像を受け取り、外部 API に送信するように実装したので、その方法について紹介します。
multipart/form-data とは
multipart/form-data
は、Web ページのフォームからファイルをアップロードする際に使用されるデータ形式です。multipart/form-data
形式のリクエストは、以下のような特徴があります。
- リクエストボディが複数の部品(part)から構成される
- 各部品はヘッダーとボディから構成される
- 各部品は
boundary
で区切られる - ヘッダーには Content-Disposition や Content-Type などが含まれる
- ボディにはファイルのデータが含まれる
以下は、multipart/form-data
形式のリクエストの例です。
POST /post HTTP/1.1 Host: httpbin.org User-Agent: Go-http-client/1.1 Content-Length: 376 Content-Type: multipart/form-data; boundary=600fcf99bf89273352a59587b26bc07642bd bfc97ce30f8445ecc0c3873a Accept-Encoding: gzip --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="file"; filename="sample.txt" Content-Type: application/octet-stream this is dummy. --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a Content-Disposition: form-data; name="key1" value1 --600fcf99bf89273352a59587b26bc07642bdbfc97ce30f8445ecc0c3873a--
ここでは、boundaryで区切られた 2 つの部品が含まれています。1 つ目の部品はファイルのデータを含み、2 つ目の部品はフォームデータを含んでいます。
実装
API Server での画像の受け取り方
本 API Server は、Go の echo フレームワークを使用しています。echo のc.FormFile
を使用して画像を受け取る実装を以下に示します。
file, err := c.FormFile("image_file") if err != nil { return types.ErrInvalidParameters } src, err := file.Open() if err != nil { return types.ErrInvalidParameters } defer src.Close()
それぞれの処理について解説します。
c.FormFile("image_file")
は、リクエストから"image_file"という名前のフィールドを取得します。先ほどのmultipart/form-data
形式のリクエストの例では、name="file"
としていた部分に対応します。
image_fileフィールドから取得したfileにはmultipart.FileHeader型のファイル情報が格納され、ファイル名やファイルサイズなどの情報を取得できます。
次に、file.Open()
でファイルを開き、src
にファイルの中身を格納します。file.Open()
は、ファイルを開いてmultipart.File型のファイルオブジェクトを返します。multipart.Fileは以下のインターフェースを実装しています。
type File interface { io.Reader io.ReaderAt io.Seeker io.Closer }
io.Reader
、io.ReaderAt
、io.Seeker
、io.Closer
のインターフェースを実装しているため、io.ReadAll
などを使用してファイルの中身を取得することもできます。
ファイルの中身を取得した後は、defer src.Close()
でファイルをクローズします。
外部 API への画像の POST
外部 API に画像を POST する際は、multipart/form-data
形式でリクエストを送信する必要があります。Go 言語では、標準ライブラリのmime/multipart
パッケージを使用してmultipart.Writerを作成し、リクエストボディを構築します。
body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("image_file", file.Filename) if err != nil { return types.ErrInternalServer } _, err = io.Copy(part, src) if err != nil { return types.ErrInternalServer } err = writer.Close() if err != nil { return types.ErrInternalServer } req, err := http.NewRequest("POST", "https://example.com", body) if err != nil { return types.ErrInternalServer } req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{} resp, err := client.Do(req) if err != nil { return types.ErrInternalServer } defer resp.Body.Close()
body
にはリクエストボディを格納するバッファを作成します。
次に、multipart.NewWriter(body)
でマルチパート形式のリクエストボディを構築するmultipart.Writerオブジェクトを作成します。
writer.CreateFormFile("image_file", file.Filename)
は、フォームフィールド名を"image_file"とし、ファイル名を file.Filename とするファイルパートを作成します。
そのファイルパートに、io.Copy(part, src)
でファイルの中身をコピーします。
writer.Close()
をすることで、最後のboundaryを書き込みます。
その後は、http.NewRequest
でリクエストを作成し、req.Header.Set("Content-Type", writer.FormDataContentType())
でリクエストヘッダーにContent-Typeを設定します。writer.FormDataContentType()
は、multipart.Writerのboundaryを含むContent-Typeを返します。
気をつけたこと
外部 API に画像を送信する際は、エラーが発生する可能性があるためリトライ処理を実装しました。
for tries < MaxLimit { // リクエスト構築 req, err = r.buildRequest(file, fileName, hurl) if err != nil { return nil, err } // リクエスト処理 body, err = r.doRequest(ctx, req) if err != nil { file.Seek(0, 0) tries++ continue } }
リトライ処理で気を付けるべき点は、一度リクエストを送信すると画像ファイルが読み込まれた状態になるため、リトライ時にファイルのポインタを先頭に戻す必要がある点です。
ファイルポインタを戻さないと、リトライ時にファイルの中身が空になってしまいます。
multipart.File
はio.Seeker
インターフェースを実装しているため、ファイルポインタを先頭に戻すには、file.Seek(0, 0)
を使用することができます。
まとめ
今回は、Go 言語でmultipart/form-data
形式を使って画像を受け取り、外部 API に送信する方法を紹介しました。echo フレームワークとmime/multipart
パッケージを利用することで、画像の受け取りや送信が簡単に実装できます。しかし、まずmultipart/form-data
形式のリクエストの仕組みを理解することが重要だと感じました。また、リトライ処理を実装する際には、リクエストボディを元の状態に戻すことを忘れないようにしましょう。