gocsvとmime/multipartを利用してmultipart/formdata形式のCSVをPOSTする
こんにちは!CastingONE の岩井です。
はじめに
わたしは普段、フロントエンドとバックエンドの両方を担当しています。どっちもめちゃ楽しいのですが、今回はバックエンドで外部 API に CSV データを送る実装をした際に、結構つまづいてしまったので備忘録としてその方法を残しておこうと思います。
net/http
を利用して multipart/formdata
形式の POST リクエストを送る必要があったのですが、Go でリクエスト組み立てるのが、難しかったです。
同じように悩まれている方がいらしたら、ぜひ参考になれば嬉しいです 😊
multipart/formdata データ形式とは
実際に実装する前に、どういったリクエストを組み立てるべきか想像できた方が良いので、multipart/formdata
の形式について確認しておきます。わたしはここがかなり危うい理解だったということもあり、根本的な理解も含めて調べながらだったので、実装が難航しました。
既に知っているよという方は、読み飛ばしていただいてかまいません。
マルチでないリクエストとの違い
フロントエンドの実装をしていると、フォームを利用したファイルアップロード等でmultipart/formdata
という形式をよく聞くと思います。multipart/formdata
は「一度の HTTP リクエストで、複数のデータ形式(MIMEType
)を扱いたい」ときに利用します。
普通のリクエストの場合、形式が json データであればContent-type: application/json
を指定し、body に json をぎゅっと詰めてあげるだけです。しかし、複数のデータ形式の場合は Body の中に仕切りをつくり、「ここは json、ここからはテキスト!」リクエストを受け取った人がわかりやすい形にする必要があります。
そういった処理を行う必要があるので、multipart/formdata
には他のデータ形式にはないいくつかの概念があります。
- part
- Body の中に「ここの中は json!」と指定した入れ物を指します。
- boundary
- 「ここまでが json!」の仕切り部分のことを指します。
また、「ここの中は json!」をはじめとした中の情報を伝えるために、Content-Type
やContent-Disposition
といったヘッダーぽい指定も、通常のヘッダーとは別で必要になります。
実装
以上のmultipart/formdata
形式についての理解を踏まえて、go でリクエストを組み立てていきます。
利用パッケージ(gocsv mime/multipart)
今回利用したパッケージは、以下の通りです。
-
gocsv
- csv へ変換するためのツール
-
mime/multipart
- 複数のパート(part)を作るためのツール
-
net/http
- HTTP で POST するためのツール
CSV の作成
CSV の作成にはgocsvを利用しました。
構造体を定義してデータを流し込み、メソッドを実行することで、とっても簡単に CSV 形式に沿ったデータを生成してくれます。
package main
import (
"fmt"
"github.com/gocarina/gocsv"
)
func main() {
type Person struct {
Id string `csv:"id"`
Name string `csv:"name"`
Age string `csv:"client_age"`
}
var payload = []*Person{
{"a1", "たろう", "エンジニア"},
{"a2", "はなこ", "デザイナー"},
{"a3", "じろう", "ディレクター"},
}
// CSVを簡単に生成
cs, err := gocsv.MarshalString(&payload)
if err != nil {
panic(err)
}
// ヘッダーを消したければこのように指定するなど
// たくさんの便利なメソッドが用意されています
cswh, err = gocsv.MarshalStringWithoutHeaders(&payload)
if err != nil {
panic(err)
}
}
multipart 部分を作成
次に肝心の multipart の中身を作成していきます。
先に説明した通り、この形式で送るためにはいくつかのヘッダー情報や boundary を書き込まなくてはいけないのですが、それを作成してくれるのがmime/multipartです。
以下のようにして、1 つの part が出来上がります。
func main() {
// CSV作成部分は省略
cs, err := gocsv.MarshalString(&payload)
if err != nil {
panic(err)
}
payloadBuffer := bytes.NewBufferString(cs)
var b bytes.Buffer
// boundaryを持つWriter
mw := multipart.NewWriter(&b)
part := make(textproto.MIMEHeader)
part.Set("Content-Type", "text/csv")
part.Set("Content-Disposition", `form-data; name="file"; filename="file"`)
// 引数に渡されたheader情報(上でセットしたContent-Type等)を持つpartを作成
pw, err := mw.CreatePart(part)
if err != nil {
panic(err)
}
// 作成したpartに情報を書き込む
if _, err := io.Copy(pw, payloadBuffer); err != nil {
panic(err)
}
mw.Close()
// 出力してみるとヘッダー情報やboundaryが追加されたpartが出来上がっていることがわかります
// fmt.Println(b.String());
}
ヘッダー情報や boundary がきちんと入っていることがわかります。
その他 HTTP リクエスエト部分を作成
あとは net/http
のシンプルな通信でできました。
Content-Type
に指定したmw.FormDataContentType()
を出力してみると、multipart/form-data; boundary=xxxxxx
というようにきちんと boundary が指定されているのがわかると思います。リクエストを受け取った人はこのヘッダー情報に指示されている boundary を見て、どこまでがどういうデータ形式のリクエストかをきちんと判定できるようになりました。
func main() {
// multipart作成部分は省略
req, err := http.NewRequest("POST", "URL", &b)
if err != nil {
panic(err)
}
// boundary付きのmultiple/formdata形式のヘッダー情報をセット
req.Header.Set("Content-Type", mw.FormDataContentType())
// 出力してみるとリクエスト本体の方のContent-Typeがどう指定されているのかわかります
// fmt.Println(mw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}
以上で無事に、gocsv
と mime/multipart
を利用して multipart/formdata
形式の HTTP を POST することができました!
おわりに
multipart/formdata
をはじめとした根本の理解も進んで、とっても良い経験になりました。
何か、もっとこうした方が良いというのがあれば、ぜひ教えてください!
CastingONE について
Discussion