CSV menyimpan data tabular dalam teks biasa: satu baris berarti satu rekam, satu kolom dipisah satu karakter pemisah. Format ini adalah bahasa umum ekspor-impor antar spreadsheet, basis data, dan aplikasi analitik. Package encoding/csv pada standard library Go membaca dan menulis CSV penuh aturan: tanda kutip, field berisi pemisah, sampai validasi jumlah kolom. Pada chapter ini kita akan melakukan round-trip lewat bytes.Buffer, mengganti pemisah kolom untuk data Indonesia, lalu menangkap galat jumlah kolom yang paling sering muncul.

csv.NewWriter membungkus io.Writer apa pun dan menampung rekam dalam buffer internal. Setiap pemanggilan w.Write([]string{...}) mengubah satu rekam menjadi satu baris CSV: field yang memuat pemisah atau tanda kutip otomatis dibungkus tanda kutip ganda, dan tanda kutip asli didobel. Buffer tidak dikirim ke tujuan sampai w.Flush() dipanggil, sehingga Flush wajib ada di akhir penulisan, dan w.Error() memeriksa galat yang terjadi selama proses.

Secara bawaan Writer menutup setiap baris dengan \n dan memakai koma sebagai pemisah. Field UseCRLF pada Writer mengganti penutup baris menjadi \r\n sesuai RFC 4180, sedangkan Reader menerima kedua gaya penutup tanpa perlu pengaturan.

Program csv-demo.go berikut menulis tiga rekam ke bytes.Buffer, membaca kembali isinya, lalu memancing galat jumlah kolom:

go
package main

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
)

func main() {
	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	if err := w.Write([]string{"nama", "kota", "email"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"Budi", "Bandung", "[email protected]"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"Siti, Ny.", "Surabaya", "[email protected]"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	w.Flush()
	if err := w.Error(); err != nil {
		fmt.Println("flush gagal:", err)
		return
	}
	fmt.Printf("isi buffer mentah: %q\n", buf.String())

	r := csv.NewReader(bytes.NewReader(buf.Bytes()))
	for {
		rekam, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("baca gagal:", err)
			return
		}
		fmt.Printf("rekam: %q\n", rekam)
	}

	dataBuruk := []byte("nama,kota\nBudi\n")
	r2 := csv.NewReader(bytes.NewReader(dataBuruk))
	if _, err := r2.ReadAll(); err != nil {
		fmt.Println("galat:", err)
		fmt.Println("galat adalah ErrFieldCount?", errors.Is(err, csv.ErrFieldCount))
	}
}

Output yang Dihasilkan:

text
isi buffer mentah: "nama,kota,email\nBudi,Bandung,[email protected]\n\"Siti, Ny.\",Surabaya,[email protected]\n"
rekam: ["nama" "kota" "email"]
rekam: ["Budi" "Bandung" "[email protected]"]
rekam: ["Siti, Ny." "Surabaya" "[email protected]"]
galat: record on line 2: wrong number of fields
galat adalah ErrFieldCount? true

Mari kita telaah baris demi baris program di atas:

  1. Baris 12-13: bytes.Buffer menjadi tujuan tulis; csv.NewWriter(&buf) membungkus buffer tersebut.
  2. Baris 14-25: tiga pemanggilan w.Write mengirim rekam; rekam ketiga sengaja memuat koma di dalam field "Siti, Ny." supaya mekanisme pembungkusan tanda kutip terlihat pada output.
  3. Baris 26-30: w.Flush() mengosongkan buffer ke tujuan, lalu w.Error() memeriksa galat penulisan yang tertunda.
  4. Baris 31: verb %q mencetak isi buffer mentah; terlihat baris "Siti, Ny." dibungkus tanda kutip dan tanda \n menjadi penutup baris.
  5. Baris 33-44: csv.NewReader membaca buffer yang sama; loop r.Read() berjalan sampai io.EOF, satu rekam per iterasi, dan tanda kutip pembungkus sudah dilepas.
  6. Baris 46-51: teks Budi\n hanya berisi satu kolom padahal baris pertama mendefinisikan dua kolom, sehingga ReadAll mengembalikan galat; errors.Is(err, csv.ErrFieldCount) membuktikan jenis galatnya.

Pemisah Kolom Kustom: Titik Koma dan Tab #

Data Indonesia sering memakai koma sebagai pemisah desimal (12.500,50), sehingga koma tidak bisa sekaligus menjadi pemisah kolom. Kebiasaan lokal yang umum adalah mengganti pemisah kolom menjadi titik koma — persis seperti opsi delimiter pada spreadsheet. Reader maupun Writer menyimpan pemisah pada field Comma. Format TSV (tab-separated values) diatur dengan cara yang sama, Comma = '\t'.

Program csv-kustom-demo.go berikut memperagakan pemisah titik koma, TSV, kolom beragam, LazyQuotes, dan UseCRLF:

go
package main

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"strings"
)

func main() {
	// Data Indonesia: angka desimal memakai koma, jadi pemisah kolom
	// diganti titik koma agar tidak bentrok.
	tabelTitikKoma := "nama;harga\nBuku Go;95.000,50\nKopi;12.500,00\n"
	r := csv.NewReader(strings.NewReader(tabelTitikKoma))
	r.Comma = ';'
	rekam, err := r.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekam {
		fmt.Printf("titik koma: %q\n", baris)
	}

	// TSV: nilai dipisah tab.
	tsv := "nama\tkota\nBudi\tBandung\nSiti\tSurabaya\n"
	rTab := csv.NewReader(strings.NewReader(tsv))
	rTab.Comma = '\t'
	rekamTab, err := rTab.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekamTab {
		fmt.Printf("tab: %q\n", baris)
	}

	// Baris dengan jumlah kolom berbeda: FieldsPerRecord = -1 melonggarkan
	// aturan jumlah kolom.
	beragam := "a,b\n1,2,3\nx\n"
	rAneh := csv.NewReader(strings.NewReader(beragam))
	rAneh.FieldsPerRecord = -1
	rekamAneh, err := rAneh.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekamAneh {
		fmt.Printf("beragam: %q\n", baris)
	}

	// LazyQuotes: tanda kutip di tengah field tidak dianggap galat sintaks.
	kutip := `Budi "Si Cepat",Bandung`
	rLazy := csv.NewReader(strings.NewReader(kutip))
	rLazy.LazyQuotes = true
	rekamLazy, err := rLazy.Read()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	fmt.Printf("lazy: %q\n", rekamLazy)

	// Writer dengan Comma titik koma dan CRLF ala RFC 4180.
	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	w.Comma = ';'
	w.UseCRLF = true
	if err := w.Write([]string{"harga", "total"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"12.500,50", "25.001,00"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	w.Flush()
	if err := w.Error(); err != nil {
		fmt.Println("flush gagal:", err)
		return
	}
	fmt.Printf("tulis kustom: %q\n", buf.String())
}

Output yang Dihasilkan:

text
titik koma: ["nama" "harga"]
titik koma: ["Buku Go" "95.000,50"]
titik koma: ["Kopi" "12.500,00"]
tab: ["nama" "kota"]
tab: ["Budi" "Bandung"]
tab: ["Siti" "Surabaya"]
beragam: ["a" "b"]
beragam: ["1" "2" "3"]
beragam: ["x"]
lazy: ["Budi \"Si Cepat\"" "Bandung"]
tulis kustom: "harga;total\r\n12.500,50;25.001,00\r\n"

Mari kita telaah baris demi baris program di atas:

  1. Baris 13-15: tabel nama;harga dibaca dengan r.Comma = ';' sehingga desimal koma pada harga tetap utuh dalam satu field.
  2. Baris 26-28: data TSV dibaca dengan rTab.Comma = '\t'; mekanisme lain tidak berubah.
  3. Baris 40-42: FieldsPerRecord = -1 menonaktifkan validasi jumlah kolom, sehingga baris 1,2,3 dan x diterima apa adanya.
  4. Baris 53-55: LazyQuotes = true membuat tanda kutip liar di tengah field (Budi "Si Cepat") diperlakukan sebagai isi field, bukan galat sintaks.
  5. Baris 64-67: Writer diatur dengan Comma = ';' dan UseCRLF = true; output %q memperlihatkan penutup baris \r\n sesuai RFC 4180.

Galat Bacaan dan FieldsPerRecord #

Secara bawaan, Reader mengunci jumlah kolom dari baris pertama dan menolak baris lain yang berbeda jumlahnya dengan galat bertipe *csv.ParseError yang membungkus csv.ErrFieldCount. Perilaku bawaan lain yang berguna: baris kosong diabaikan otomatis, dan field Comment bisa menandai karakter komentar sehingga baris komentar dilompati. Ketika berkas datang dari pihak ketiga yang tidak disiplin, kombinasi LazyQuotes dan FieldsPerRecord = -1 menjaga pipeline tetap jalan — dengan konsekuensi validasi bentuk dipindah ke kode kita, pola yang dibahas pada chapter Validasi Data .

Referensi #

JSON, XML, dan CSV semuanya berupa teks yang bisa dibaca mata. Untuk pertukaran data biner khusus antar program Go, standard library menyediakan gob yang kita bahas pada chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

csv-demo.go

Unduh berkas
go
package main

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"io"
)

func main() {
	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	if err := w.Write([]string{"nama", "kota", "email"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"Budi", "Bandung", "[email protected]"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"Siti, Ny.", "Surabaya", "[email protected]"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	w.Flush()
	if err := w.Error(); err != nil {
		fmt.Println("flush gagal:", err)
		return
	}
	fmt.Printf("isi buffer mentah: %q\n", buf.String())

	r := csv.NewReader(bytes.NewReader(buf.Bytes()))
	for {
		rekam, err := r.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("baca gagal:", err)
			return
		}
		fmt.Printf("rekam: %q\n", rekam)
	}

	dataBuruk := []byte("nama,kota\nBudi\n")
	r2 := csv.NewReader(bytes.NewReader(dataBuruk))
	if _, err := r2.ReadAll(); err != nil {
		fmt.Println("galat:", err)
		fmt.Println("galat adalah ErrFieldCount?", errors.Is(err, csv.ErrFieldCount))
	}
}

csv-kustom-demo.go

Unduh berkas
go
package main

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"strings"
)

func main() {
	// Data Indonesia: angka desimal memakai koma, jadi pemisah kolom
	// diganti titik koma agar tidak bentrok.
	tabelTitikKoma := "nama;harga\nBuku Go;95.000,50\nKopi;12.500,00\n"
	r := csv.NewReader(strings.NewReader(tabelTitikKoma))
	r.Comma = ';'
	rekam, err := r.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekam {
		fmt.Printf("titik koma: %q\n", baris)
	}

	// TSV: nilai dipisah tab.
	tsv := "nama\tkota\nBudi\tBandung\nSiti\tSurabaya\n"
	rTab := csv.NewReader(strings.NewReader(tsv))
	rTab.Comma = '\t'
	rekamTab, err := rTab.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekamTab {
		fmt.Printf("tab: %q\n", baris)
	}

	// Baris dengan jumlah kolom berbeda: FieldsPerRecord = -1 melonggarkan
	// aturan jumlah kolom.
	beragam := "a,b\n1,2,3\nx\n"
	rAneh := csv.NewReader(strings.NewReader(beragam))
	rAneh.FieldsPerRecord = -1
	rekamAneh, err := rAneh.ReadAll()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	for _, baris := range rekamAneh {
		fmt.Printf("beragam: %q\n", baris)
	}

	// LazyQuotes: tanda kutip di tengah field tidak dianggap galat sintaks.
	kutip := `Budi "Si Cepat",Bandung`
	rLazy := csv.NewReader(strings.NewReader(kutip))
	rLazy.LazyQuotes = true
	rekamLazy, err := rLazy.Read()
	if err != nil {
		fmt.Println("baca gagal:", err)
		return
	}
	fmt.Printf("lazy: %q\n", rekamLazy)

	// Writer dengan Comma titik koma dan CRLF ala RFC 4180.
	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	w.Comma = ';'
	w.UseCRLF = true
	if err := w.Write([]string{"harga", "total"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	if err := w.Write([]string{"12.500,50", "25.001,00"}); err != nil {
		fmt.Println("tulis gagal:", err)
		return
	}
	w.Flush()
	if err := w.Error(); err != nil {
		fmt.Println("flush gagal:", err)
		return
	}
	fmt.Printf("tulis kustom: %q\n", buf.String())
}