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.
Menulis CSV: Writer, Flush, dan Error #
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:
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:
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? trueMari kita telaah baris demi baris program di atas:
- Baris 12-13:
bytes.Buffermenjadi tujuan tulis;csv.NewWriter(&buf)membungkus buffer tersebut. - Baris 14-25: tiga pemanggilan
w.Writemengirim rekam; rekam ketiga sengaja memuat koma di dalam field"Siti, Ny."supaya mekanisme pembungkusan tanda kutip terlihat pada output. - Baris 26-30:
w.Flush()mengosongkan buffer ke tujuan, laluw.Error()memeriksa galat penulisan yang tertunda. - Baris 31: verb
%qmencetak isi buffer mentah; terlihat baris"Siti, Ny."dibungkus tanda kutip dan tanda\nmenjadi penutup baris. - Baris 33-44:
csv.NewReadermembaca buffer yang sama; loopr.Read()berjalan sampaiio.EOF, satu rekam per iterasi, dan tanda kutip pembungkus sudah dilepas. - Baris 46-51: teks
Budi\nhanya berisi satu kolom padahal baris pertama mendefinisikan dua kolom, sehinggaReadAllmengembalikan 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:
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:
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:
- Baris 13-15: tabel
nama;hargadibaca denganr.Comma = ';'sehingga desimal koma pada harga tetap utuh dalam satu field. - Baris 26-28: data TSV dibaca dengan
rTab.Comma = '\t'; mekanisme lain tidak berubah. - Baris 40-42:
FieldsPerRecord = -1menonaktifkan validasi jumlah kolom, sehingga baris1,2,3danxditerima apa adanya. - Baris 53-55:
LazyQuotes = truemembuat tanda kutip liar di tengah field (Budi "Si Cepat") diperlakukan sebagai isi field, bukan galat sintaks. - Baris 64-67: Writer diatur dengan
Comma = ';'danUseCRLF = true; output%qmemperlihatkan penutup baris\r\nsesuai 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 #
- Dokumentasi package encoding/csv — referensi resmi Reader, Writer, dan galat CSV.
- Dokumentasi csv.Reader — daftar field pengaturan beserta artinya.
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 berkaspackage 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 berkaspackage 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())
}