Leer y escribir CSV en Go

Go trae un lector y un escritor de CSV en la biblioteca estándar, y se encargan solos de lo que suele salir mal a mano: las comillas, los separadores dentro de un campo y los saltos de línea. No hace falta ninguna dependencia.

Escribir

func escribir(ruta string, filas []Fila) error {
	f, err := os.Create(ruta)
	if err != nil {
		return err
	}
	defer f.Close()

	w := csv.NewWriter(f)
	defer w.Flush()

	if err := w.Write([]string{"nombre", "ciudad", "unidades"}); err != nil {
		return err
	}
	for _, fi := range filas {
		reg := []string{fi.Nombre, fi.Ciudad, strconv.Itoa(fi.Unidades)}
		if err := w.Write(reg); err != nil {
			return err
		}
	}
	return w.Error()
}
clientes.csvnombre,ciudad,unidades
Ana,Madrid,12
Luis,Lima,7
Marta,"Bogotá; centro",23

Fíjate en la última fila: el valor llevaba un punto y coma y el escritor lo entrecomilló solo. Eso es justo lo que se estropea cuando uno pega las comas a mano.

El Flush no es opcional. El escritor acumula en memoria, así que sin Flush() el archivo puede quedarse vacío o a medias. Y como Flush no devuelve error, hay que preguntarlo después con w.Error(): ahí es donde aparece un disco lleno.

Leer el archivo entero

f, err := os.Open("clientes.csv")
if err != nil {
	return err
}
defer f.Close()

r := csv.NewReader(f)
registros, err := r.ReadAll()
if err != nil {
	return err
}

total := 0
for _, reg := range registros[1:] {      // [1:] se salta la cabecera
	u, err := strconv.Atoi(reg[2])
	if err != nil {
		fmt.Println("fila mal formada:", reg)
		continue
	}
	total += u
	fmt.Printf("%-6s %-16s %3d\n", reg[0], reg[1], u)
}
fmt.Println("total de unidades:", total)
salidaAna    Madrid            12
Luis   Lima               7
Marta  Bogotá; centro    23
total de unidades: 42

Cada registro es un []string: el CSV no tiene tipos, así que los números hay que convertirlos. strconv.Atoi devuelve también un error, y conviene usarlo en vez de ignorarlo: una celda vacía o con un texto es lo más común en los archivos reales.

Fila a fila, para los archivos grandes

ReadAll carga todo en memoria. Con un archivo de varios gigabytes hay que ir leyendo:

r := csv.NewReader(f)
r.Comma = ';'            // separador distinto
r.FieldsPerRecord = 2    // exige exactamente dos campos

if _, err := r.Read(); err != nil {   // descartar la cabecera
	return err
}
for {
	reg, err := r.Read()
	if errors.Is(err, io.EOF) {
		break
	}
	if err != nil {
		fmt.Println("error en una fila:", err)
		continue
	}
	fmt.Printf("%s vive en %s\n", reg[0], reg[1])
}
salidaAna vive en Madrid
Luis vive en Lima

El bucle termina cuando Read devuelve io.EOF. Nótese que se comprueba con errors.Is y no con ==: funciona igual aunque el error venga envuelto.

Cuando una fila está mal

Con FieldsPerRecord puesto, el lector avisa en vez de devolver datos descuadrados:

salidarecord on line 2: wrong number of fields

Dice la línea exacta. Y como el error es de esa fila y no del archivo, se puede saltar con continue y seguir procesando el resto, que es casi siempre lo que quieres.

Las opciones que vas a tocar

Campo Para qué
r.Comma = ';' Cambiar el separador. Muy habitual con archivos de Excel en español
r.Comment = '#' Ignorar las líneas que empiecen por ese carácter
r.FieldsPerRecord Exigir un número fijo de campos. Con -1 acepta cualquiera
r.TrimLeadingSpace Quitar los espacios tras el separador
w.Comma Lo mismo, al escribir

Todo el código se compiló y ejecutó con Go 1.25 antes de publicarla; las salidas y el contenido del archivo están copiados de esa ejecución. Documentación oficial: paquete encoding/csv.