こんにちは。Tomoyuki(@tomoyuki65)です。
Go言語のフレームワーク「Echo」において、CSVファイルをインポートして処理させたい場合もあると思います。
この記事では、Go言語(Golang)のEchoでCSVファイルをインポートする方法について解説します。
【関連記事】

Go言語(Golang)のEchoでCSVファイルをインポートする方法
以前に「Go言語(Golang)のEchoでシンプルかつ実務的なバックエンドAPI開発方法まとめ」という記事を書きましたが、これにCSVファイルのインポート用APIを追加する方法を通して解説していきます。
まずは以下のコマンドを実行し、各種ファイルを作成します。
$ mkdir -p src/internal/usecases/csv && touch src/internal/usecases/csv/import_csv.go
$ mkdir -p src/internal/handlers/csv && touch src/internal/handlers/csv/csv.go src/internal/handlers/csv/csv_test.go
$ mkdir -p src/internal/handlers/csv/data && touch src/internal/handlers/csv/data/test-data-ok.csv src/internal/handlers/csv/data/test-data-err.csv
次に作成した各種ファイルをそれぞれ以下のように記述します。
・「src/internal/usecases/csv/import_csv.go」
package csv
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"mime/multipart"
"net/http"
"unicode/utf8"
utilContext "go-echo-v2/util/context"
"go-echo-v2/util/logger"
"github.com/jszwec/csvutil"
"github.com/labstack/echo/v4"
)
// レスポンス結果の型定義
type ImportCsvData struct {
No string `csv:"no" json:"no" validate:"required"`
LastName string `csv:"last_name" json:"last_name" validate:"required"`
FirstName string `csv:"first_name" json:"first_name" validate:"required"`
Email string `csv:"email" json:"email" validate:"required"`
}
// インターフェースの定義
type CsvUsecase interface {
Exec(c echo.Context) error
}
// 構造体の定義
type csvUsecase struct{}
// インスタンス生成用関数の定義
func NewCsvUsecase() CsvUsecase {
return &csvUsecase{}
}
// CSVファイルがutf8か判定する関数
func checkUTF8(fh *multipart.FileHeader) (bool, error) {
f, err := fh.Open()
if err != nil {
return false, err
}
defer f.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, f)
b := buf.Bytes()
return utf8.Valid(b), nil
}
// メソッド定義
func (s *csvUsecase) Exec(c echo.Context) error {
ctx := utilContext.CreateContext(c)
// CSVファイルを取得(CSVファイルにはヘッダー行が必要)
fh, err := c.FormFile("csv")
if err != nil {
msg := fmt.Sprintf("CSVファイルが指定されていません。: %v", err)
logger.Warn(ctx, msg)
return echo.NewHTTPError(http.StatusBadRequest, msg)
}
// CSVファイルのutf8チェック
isUTF8, err := checkUTF8(fh)
if err != nil {
msg := fmt.Sprintf("CSVファイルの文字コードチェックに失敗しました。: %v", err)
logger.Error(ctx, msg)
return echo.NewHTTPError(http.StatusInternalServerError, msg)
}
if !isUTF8 {
msg := "CSVファイルがutf8形式ではありません。"
logger.Warn(ctx, msg)
return echo.NewHTTPError(http.StatusUnprocessableEntity, msg)
}
// CSVファイルをオープン
f, err := fh.Open()
if err != nil {
msg := fmt.Sprintf("CSVファイルのオープンに失敗しました: %v", err)
logger.Error(ctx, msg)
return echo.NewHTTPError(http.StatusInternalServerError, msg)
}
defer f.Close()
// CSVリーダーの作成
reader := csv.NewReader(f)
// CSVデコーダーの作成
decoder, err := csvutil.NewDecoder(reader)
if err != nil {
msg := fmt.Sprintf("CSVデコーダーの作成に失敗しました。: %v", err)
logger.Error(ctx, msg)
return echo.NewHTTPError(http.StatusInternalServerError, msg)
}
// データマッピング
var csvData []ImportCsvData
if err := decoder.Decode(&csvData); err != nil {
msg := fmt.Sprintf("データマッピングに失敗しました。: %v", err)
logger.Error(ctx, msg)
return echo.NewHTTPError(http.StatusInternalServerError, msg)
}
// バリデーションチェック
var errMsgs []string
for i, data := range csvData {
// CSVデータをログ出力
logger.Info(ctx, fmt.Sprintf("No: %s, LastName: %s, FirstName: %s, Email: %s", data.No, data.LastName, data.FirstName, data.Email))
if err := c.Validate(data); err != nil {
msg := fmt.Sprintf("%d件目[%v]", i+1, err)
errMsgs = append(errMsgs, msg)
}
}
var errMsgText string
if len(errMsgs) > 0 {
for _, msg := range errMsgs {
errMsgText += msg + " "
}
msg := fmt.Sprintf("バリデーションエラー: %v", errMsgText)
logger.Warn(ctx, msg)
return echo.NewHTTPError(http.StatusUnprocessableEntity, msg)
}
return c.JSON(http.StatusOK, csvData)
}
・「src/internal/handlers/csv/csv.go」
package csv
import (
usecaseCsv "go-echo-v2/internal/usecases/csv"
"github.com/labstack/echo/v4"
)
// OpenAPI仕様書用の型定義
type ImportCsvResponse struct {
No string `json:"no" example:"1"`
LastName string `json:"last_name" example:"田中"`
FirstName string `json:"first_name" example:"太郎"`
Email string `json:"email" example:"t.tanaka@example.com"`
}
type BadRequestResponse struct {
Message string `json:"message" example:"Bad Request"`
}
type UnauthorizedResponse struct {
Message string `json:"message" example:"Unauthorized"`
}
type UnprocessableEntityResponse struct {
Message string `json:"message" example:"Unprocessable Entity"`
}
type InternalServerErrorResponse struct {
Message string `json:"message" example:"Internal Server Error"`
}
// @Description CSVファイルのインポート用API
// @Tags csv
// @Accept multipart/form-data
// @Param file formData file true "CSV file to upload"
// @Security Bearer
// @Success 200 {object} ImportCsvResponse
// @Failure 400 {object} BadRequestResponse
// @Failure 401 {object} UnauthorizedResponse
// @Failure 422 {object} UnprocessableEntityResponse
// @Failure 500 {object} InternalServerErrorResponse
// @Router /api/v1/csv/import [post]
func ImportCsv(c echo.Context) error {
// インスタンス生成
csvUsecase := usecaseCsv.NewCsvUsecase()
// ユースケースの実行
return csvUsecase.Exec(c)
}
・「src/internal/handlers/csv/csv_test.go」
package csv
import (
"bytes"
"encoding/json"
"log/slog"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"go-echo-v2/middleware"
"go-echo-v2/util/validator"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
echoMiddleware "github.com/labstack/echo/v4/middleware"
)
type ImportCsvData struct {
No string `csv:"no" validate:"required"`
LastName string `csv:"last_name" validate:"required"`
FirstName string `csv:"first_name" validate:"required"`
Email string `csv:"email" validate:"required"`
}
func TestImportCsvOK(t *testing.T) {
// .env ファイルの読み込み
if err := godotenv.Load("../../../.env"); err != nil {
slog.Error(".envファイルの読み込みに失敗しました。")
}
// テスト用のENV設定
env := os.Getenv("ENV")
os.Setenv("ENV", "testing")
defer os.Setenv("ENV", env)
// ミドルウェアの適用
e := echo.New()
e.Use(middleware.RequestMiddleware)
e.Use(middleware.LoggerMiddleware())
e.Use(middleware.CorsMiddleware())
e.Use(echoMiddleware.Recover())
// バリデーター設定
e.Validator = validator.NewCustomValidator()
v1 := e.Group("/api/v1")
v1.POST("/csv/import", ImportCsv, middleware.AuthMiddleware)
// CSVファイルのパス
csvFilePath := "data/test-data-ok.csv"
// リクエストボディの作成
reqBody := &bytes.Buffer{}
writer := multipart.NewWriter(reqBody)
csvData, err := os.ReadF ile(csvFilePath)
if err != nil {
t.Fatalf("failed to read CSV file: %v", err)
}
part, err := writer.CreateFormFile("csv", filepath.Base(csvFilePath))
if err != nil {
t.Fatalf("failed to create form file: %v", err)
}
_, err = part.Write(csvData)
if err != nil {
t.Fatalf("failed to write CSV file to form file: %v", err)
}
writer.Close()
// テスト用リクエストの作成
req := httptest.NewRequest(http.MethodPost, "/api/v1/csv/import", reqBody)
req.Header.Set("Content-Type", writer.FormDataContentType())
token := "zMdtq_glzI7oqq8yXjMgEOW6XfrSUMFGqw"
bearerToken := "Bearer " + token
req.Header.Set("Authorization", bearerToken)
rec := httptest.NewRecorder()
// テスト実行
e.ServeHTTP(rec, req)
// レスポンス結果のJSONを取得
var resbody []map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resbody); err != nil {
t.Fatal(err)
}
// 検証
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, 2, len(resbody))
assert.Equal(t, "1", resbody[0]["no"])
assert.Equal(t, "田中", resbody[0]["last_name"])
assert.Equal(t, "太郎", resbody[0]["first_name"])
assert.Equal(t, "t.tanaka@example.com", resbody[0]["email"])
assert.Equal(t, "2", resbody[1]["no"])
assert.Equal(t, "佐々木", resbody[1]["last_name"])
assert.Equal(t, "一郎", resbody[1]["first_name"])
assert.Equal(t, "ichirou.sasaki@example.com", resbody[1]["email"])
}
func TestImportCsvValidErr(t *testing.T) {
// .env ファイルの読み込み
if err := godotenv.Load("../../../.env"); err != nil {
slog.Error(".envファイルの読み込みに失敗しました。")
}
// テスト用のENV設定
env := os.Getenv("ENV")
os.Setenv("ENV", "testing")
defer os.Setenv("ENV", env)
// ミドルウェアの適用
e := echo.New()
e.Use(middleware.RequestMiddleware)
e.Use(middleware.LoggerMiddleware())
e.Use(middleware.CorsMiddleware())
e.Use(echoMiddleware.Recover())
// バリデーター設定
e.Validator = validator.NewCustomValidator()
v1 := e.Group("/api/v1")
v1.POST("/csv/import", ImportCsv, middleware.AuthMiddleware)
// CSVファイルのパス
csvFilePath := "data/test-data-err.csv"
// リクエストボディの作成
reqBody := &bytes.Buffer{}
writer := multipart.NewWriter(reqBody)
csvData, err := os.ReadF ile(csvFilePath)
if err != nil {
t.Fatalf("failed to read CSV file: %v", err)
}
part, err := writer.CreateFormFile("csv", filepath.Base(csvFilePath))
if err != nil {
t.Fatalf("failed to create form file: %v", err)
}
_, err = part.Write(csvData)
if err != nil {
t.Fatalf("failed to write CSV file to form file: %v", err)
}
writer.Close()
// テスト用リクエストの作成
req := httptest.NewRequest(http.MethodPost, "/api/v1/csv/import", reqBody)
req.Header.Set("Content-Type", writer.FormDataContentType())
token := "zMdtq_glzI7oqq8yXjMgEOW6XfrSUMFGqw"
bearerToken := "Bearer " + token
req.Header.Set("Authorization", bearerToken)
rec := httptest.NewRecorder()
// テスト実行
e.ServeHTTP(rec, req)
// レスポンス結果のJSONを取得
var resbody map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resbody); err != nil {
t.Fatal(err)
}
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, rec.Code)
assert.Contains(t, resbody["message"], "バリデーションエラー")
assert.Contains(t, resbody["message"], "1件目[Key: 'ImportCsvData.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag]")
assert.Contains(t, resbody["message"], "2件目[Key: 'ImportCsvData.LastName' Error:Field validation for 'LastName' failed on the 'required' tagKey: 'ImportCsvData.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag]")
}
※「os.ReadF ile(csvFilePath)」のFとiの間には意図的にスペースを入れているため、実際に試す場合はスペースを削除して下さい。
・「src/internal/handlers/csv/data/test-data-ok.csv」
no,last_name,first_name,email
1,田中,太郎,t.tanaka@example.com
2,佐々木,一郎,ichirou.sasaki@example.com
・「src/internal/handlers/csv/data/test-data-err.csv」
no,last_name,first_name,email
1,田中,,t.tanaka@example.com
2,,,ichirou.sasaki@example.com
次にファイル「src/router/router.go」を以下のように修正します。
package router
import (
"go-echo-v2/internal/handlers/csv"
"go-echo-v2/internal/handlers/healthcheck"
"go-echo-v2/internal/handlers/index"
"go-echo-v2/internal/handlers/user"
"go-echo-v2/middleware"
"github.com/labstack/echo/v4"
)
func SetupRouter(e *echo.Echo) {
e.GET("/", index.Index)
v1 := e.Group("/api/v1")
v1.GET("/", index.Index, middleware.AuthMiddleware)
v1.GET("/healthcheck", healthcheck.Healthcheck, middleware.ApiKeyAuthMiddleware)
v1.POST("/user", user.CreateUser)
v1.GET("/users", user.GetAllUsers, middleware.AuthMiddleware)
v1.GET("/user/:uid", user.GetUserByUID, middleware.AuthMiddleware)
v1.PUT("/user/:uid", user.UpdateUserByUID, middleware.AuthMiddleware)
v1.DELETE("/user/:uid", user.DeleteUserByUID, middleware.AuthMiddleware)
v1.POST("/csv/import", csv.ImportCsv, middleware.AuthMiddleware)
}
次に以下のコマンドを実行し、各種修正内容を反映させます。
$ docker compose exec api go mod tidy
$ docker compose exec api go fmt ./...
$ docker compose exec api staticcheck ./...
$ docker compose exec api swag i
次にPostmanでAPIを実行して試してみます。
まずはBearerトークンとCSVファイル「test-data-ok.csv」を設定してPOSTメソッドで「http://localhost:8080/api/v1/csv/import」を実行し、CSVファイルの内容がレスポンス結果に出力されればOKです。
次にCSVファイル「test-data-err.csv」を変更してPOSTメソッドで「http://localhost:8080/api/v1/csv/import」を実行し、バリデーションチェックでエラーになればOKです。
次に以下のコマンドを実行し、テストコードを試します。
$ docker compose exec api go test -v ./internal/handlers/...
テスト実行後、下図のように全てのテストがPASSすればOKです。
最後に
今回はGo言語(Golang)のEchoでCSVファイルをインポートする方法について解説しました。
CSVファイルをインポートして何らかの処理をさせるAPIを作りたい場合、今回ご紹介した方法をぜひ参考にしてみて下さい。
コメント