GoでCSVの内容をデータベースに保存する

# 概要
CSV出力って結構いろんなシステムに機能として搭載されてますよね。
そんなCSVですが、あるサービスやシステムからCSVファイルを出力して、全く別のシステムやサービスのDBにその情報を保存しておきたいなんてケースがあるんじゃないでしょうか。

GoでCSVを扱うには標準ライブラリの[encoding/csv](https://pkg.go.dev/encoding/csv)があります。
CSVの内容をDBに保存するにあたって構造体にマッピングできた方が処理が楽なのですが、[encoding/csv](https://pkg.go.dev/encoding/csv)ではマッピングに手間がかかるため、今回はサードパーティのライブラリとして[csvutil](https://pkg.go.dev/github.com/jszwec/csvutil)を使ってDBに保存してみます。


# CSVを扱うライブラリ
ちなみにGoとCSVで検索してヒットするライブラリはgocsvとcsvutilが多数だと思います。
今回csvutilを選んだのには、決定的な理由があります。
それは **CSVで値なしのとき、DBにnullとして登録できる** という点です。

gocsvではCSVに値がないものを、Goの構造体にマッピングすると、ポインタ型でも`0`がセットされてしまいます。
例えば`*int型`にCSVの値なしをマッピングすると、`nil`ではなく`0`がセットされます。

これだと、CSV側で元々値が`0`だったのか、値がなくてGoのゼロ値として`0`になったのかがわかりません。
そんなわけで元々のCSVに値がない場合、DBには`0`ではなく`NULL`で登録されて欲しいので、Goでマッピングした時にnilだったらNULLとしてDBに登録するためにcsvutilを選んでいます。


## gocsv

https://pkg.go.dev/github.com/gocarina/gocsv

## csvutil

https://pkg.go.dev/github.com/jszwec/csvutil

ちなみにのちなみに[csvutilのGithubのREADME](https://github.com/jszwec/csvutil#performance)にはcsvutilとgocsvのパフォーマンスの差がベンチマークの結果によって示されています。
パフォーマンス的にもcsvutilがよさそうですね。

https://github.com/jszwec/csvutil#performance


## 少しだけ補足
JavaScriptでは`undefined`や`null`で値が未設定であることを判別すると思いますが、
Goでは、ポインタ型が`nil`かどうかで未設定であることを判別します。
この記事が参考になりました。

https://tech.griphone.co.jp/2022/12/10/go-nil-pointer-or-zero-value/


# やってみた
とりあえず最低限の環境構築をします。
<details><summary>環境構築用ファイル</summary>

```yml:docker-compose.yml
version: '3.9'
services:
  app:
    image: golang:1.20.7
    ports:
      - 8080:8080
    volumes:
      - .:/app
    tty: true
    working_dir: /app

  db:
    image: mysql:8.0
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test
      MYSQL_USER: test
      MYSQL_PASSWORD: test
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    volumes:
      - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
```
```sql:docker/mysql/initdb.d/01_create_table.sql
CREATE TABLE user (
  id INT NOT NULL AUTO_INCREMENT,
  name VARCHAR(100),
  age INT,
  memo TEXT,
  PRIMARY KEY (id)
);
```

</details>

一応、gocsvとcsvutilでそれぞれ結果がどうなるか確認してみます。
今回CSVのサンプル値はコードに埋め込みますが、実際にはファイルから読み込むことになるため、その時はShiftJISからutf-8へ文字コードの変換が必要だったりします。

```go:main.go
package main

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
	"github.com/gocarina/gocsv"
	"github.com/jszwec/csvutil"
)

type user struct {
	Name string `csv:"name"`
	Age  *int   `csv:"age"`
}

// CSVサンプル値
var csvInput = []byte(`
name,age
jack,26
john,,
`)

func main() {
	// DB設定
	db, err := sql.Open("mysql", "test:test@tcp(db:3306)/test")
	if err != nil {
		fmt.Println("DB Connection Error: ", err)
		return
	}
	defer db.Close()
	// DB接続確認
	if err = db.Ping(); err != nil {
		fmt.Println("Ping Error: ", err)
		return
	}

	// csvutilで登録
	csvutilUsers := []user{}
	if err := csvutil.Unmarshal(csvInput, &csvutilUsers); err != nil {
		fmt.Println("csvutil Unmarshal Error:", err)
	}
	insertUser(db, csvutilUsers, "csvutil")

	// gocsvで登録
	gocsvUsers := []user{}
	if err := gocsv.UnmarshalBytes(csvInput, &gocsvUsers); err != nil {
		fmt.Println("gocsv Unmarshal Error:", err)
	}
	insertUser(db, gocsvUsers, "gocsv")
}

// DBにユーザー登録
func insertUser(db *sql.DB, users []user, lib string) {
	for _, u := range users {
		age := sql.NullInt16{}
		if u.Age != nil {
			age = sql.NullInt16{Int16: int16(*u.Age), Valid: true}
		}
		query := "INSERT INTO user (name, age, memo) VALUES (?, ?, ?)"
		_, err := db.Exec(query, u.Name, age, lib)
		if err != nil {
			fmt.Println("INSERT Error: ", err)
		}
	}
}
```

同じ構造体、同じCSVサンプル値に対して、gocsvとcsvutilでそれぞれUnmarshalしてDBに登録した結果、こうなりました。

```
mysql> select * from user;
+----+------+------+---------+
| id | name | age  | memo    |
+----+------+------+---------+
|  1 | jack |   26 | csvutil |
|  2 | john | NULL | csvutil |
|  3 | jack |   26 | gocsv   |
|  4 | john |    0 | gocsv   |
+----+------+------+---------+
```

csvutilではCSVに値がないときにNULLが登録され、
gocsvではCSVに値がないときに0が登録されています。

# 所感
ライブラリを読み込んだわけではないのでひょっとするとgocsvでもNULLで登録ができるのかもしれません。
ただパッと使ってみた感じではcsvutilが簡単にいろんなことができそうな印象です。