Go CSV download API

こんにちは、マットです。
都内ITベンチャーのエンジニアです。
Go/Next.js/GraphQLを使っています。

今回はGo製のAPICSVをダウンロードする方法を見ていきたいと思います。

普段はGraphQLについて発信していますが、今回はRestで実装し、"encoding/csv"パッケージなどメイン機能部分のみ解説していきます。

ルーター

Restなのでまずはルーティングを作ります。 echoのFWを使っています。

func csvRouter(g *echo.Group, ctrl *controller.Controller) {
    constroller := controller.NewAnalyticsController(ctrl)
    g.GET("/analytics", func(e echo.Context) error {
        return constroller.Analytics(e)
    })
}

controller

次はコントローラーです。 dbを持たせた構造体とコンストラクタを定義します。

package controller

import (
    "github.com/jinzhu/gorm"
)

type Controller struct {
    db          *gorm.DB
}

func NewController(db *gorm.DB) *Controller {
    return &Controller{
        db:          db,
    }
}

AnalyticsController

いよいよCSV出力の実装です。 全体のコードを見たあとに一つ一つ解説していきます。

package controller

import (
    "bytes"
    "encoding/csv"
    "fmt"
    "net/http"
    "strconv"

    "github.com/hoge/backend/src/domain"
    "github.com/labstack/echo"
)

type AnalyticsController struct {
    controller *Controller
}

func NewAnalyticsController(controller *Controller) *AnalyticsController {
    return &AnalyticsController{
        controller: controller,
    }
}

func (a *AnalyticsController) Analytics(c echo.Context) error {
    var tasks []domain.Task

    if err := a.controller.db.Find(&tasks).Error; err != nil {
        fmt.Println("error")
        return err
    }

    fmt.Println(tasks)

    csvBytes, err := convertCSV(tasks)
    if err != nil {
        fmt.Println("convertError:", err)
        return err
    }
    return newCSVResponse(c, http.StatusOK, csvBytes)
}

func newCSVResponse(c echo.Context, status int, data []byte) error {
    c.Response().Writer.Header().Set("Content-Disposition", "attachment; filename=tasks.csv")
    c.Response().Writer.Header().Set("Content-Type", "text/csv")
    return c.Blob(status, "text/csv", data)
}

func convertCSV(tasks []domain.Task) ([]byte, error) {
    b := new(bytes.Buffer)
    w := csv.NewWriter(b)

    var header = []string{
        "id",
        "userID",
        "title",
        "note",
        "completed",
        "createdAt",
        "updatedAt",
    }
    w.Write(header)

    for _, task := range tasks {

        var col = []string{
            strconv.Itoa(task.ID),
            strconv.Itoa(task.UserID),
            task.Title,
            task.Note,
            strconv.Itoa(task.Completed),
            task.CreatedAt.String(),
            task.UpdatedAt.String(),
        }
        w.Write(col)
    }
    w.Flush()

    if err := w.Error(); err != nil {
        return nil, err
    }
    return b.Bytes(), nil
}

以上がAPIの実装部分となります。 エラー時の処理が適当ですがご了承ください。

Analytics()

まずはAnalyticsメソッドです。 事前に定義していたドメインエンティティのTaskを全て取得します。 ORMはgormです。

ちなみにTaskは以下のような構造体になっています。

type Task struct {
    ID        int
    UserID    int
    Title     string
    Note      string
    Completed int
    CreatedAt time.Time
    UpdatedAt time.Time
}

convertCSV()

convertCSV()メソッドはCSVで出力したい形にtasksを変換するメソッドです。 今回はシンプルに以下のような形で出力します。

f:id:shikatech:20211205081001p:plain

コードの中身を確認していきます。 "encoding/csv"パッケージにはcsv用の Writer構造体が用意されているので、bytes.Bufferから生成します。

b := new(bytes.Buffer)
w := csv.NewWriter(b)

次にcsvのヘッダーに当たる配列を作成します。 csvはstring配列の組み合わです。

   var header = []string{
        "id",
        "userID",
        "title",
        "note",
        "completed",
        "createdAt",
        "updatedAt",
    }
    w.Write(header)

次は表の中身となる部分を作ります。 ヘッダーと同じくstring配列なので各フィールドをstringに変換し一件ずつバッファリングします。

   for _, task := range tasks {

        var col = []string{
            strconv.Itoa(task.ID),
            strconv.Itoa(task.UserID),
            task.Title,
            task.Note,
            strconv.Itoa(task.Completed),
            task.CreatedAt.String(),
            task.UpdatedAt.String(),
        }
        w.Write(col)
    }

最後にバッファリングされたデータを書き込み、エラーハンドリングをして バイト配列でreturnします。

    w.Flush()

    if err := w.Error(); err != nil {
        return nil, err
    }
    return b.Bytes(), nil

バイト列を生成できたのであとはHttpレスポンスを返すだけです。

newCSVResponse()

今回の実装はechoに依存しています。

ヘッダーにはダウロードファイルである事を示す "Content-Disposition", "attachment; filename=tasks.csv"

Content-Disposition - HTTP | MDN

csvファイルである事を示す "Content-Type", "text/csv" を付与しています。

最後にechoのBlob()メソッドでhttpレスポンスを送信します。

Response | Echo - High performance, minimalist Go web framework

func newCSVResponse(c echo.Context, status int, data []byte) error {
    c.Response().Writer.Header().Set("Content-Disposition", "attachment; filename=tasks.csv")
    c.Response().Writer.Header().Set("Content-Type", "text/csv")
    return c.Blob(status, "text/csv", data)
}

これで実装が完了しました。

最後にリクエストを送ってみます。

curl head http://localhost:8080/api/v1/analytics --output tasks.csv

tasks.csvがダウンロードできれば完了です。

CSV以外にもGoの io パッケージ、bytes.Bufferではさまざまな形式の入出力が可能です。

こちらの記事に大変詳しく書かれていました。

Goメモ-153 (Goでのファイル I/O のやり方まとめ) - いろいろ備忘録日記

今回は以上です。

最後に、 このブログではweb開発について発信していくのでまたご覧頂けると嬉しいです。 最後までお読み頂きありがとうございました。