Crear una eina en Go per convertir CSV a JSON
Tutorial senzill per llegir CSV, validar dades, convertir a JSON i generar una CLI bàsica en Go. Projecte ideal per començar.

Aquest és probablement el projecte útil més simple que pots construir en Go. Sense frameworks, sense bases de dades, sense HTTP. Només fitxers, structs i la llibreria estàndard. Llegeixes un CSV, el converteixes a JSON i el treus per stdout o l’escrius a un fitxer. Res més. I en el procés toques lectura de fitxers, parsing, validació, serialització, flags de línia de comandos i gestió d’errors. Tot el que necessites per sentir-te còmode amb el llenguatge abans de ficar-te en coses més complexes.
Si estàs buscant projectes per aprendre Go, aquest és un punt de partida excel·lent. És prou petit com per acabar-lo en una tarda i prou real com perquè el resultat sigui alguna cosa que pots fer servir de debò.
Què anem a construir
Una eina de línia de comandos que:
- Rep un fitxer CSV com a argument.
- Llegeix i parseja el seu contingut.
- Valida les dades: detecta camps buits, tipus incorrectes, files mal formades.
- Converteix les files a una estructura Go tipada.
- Serialitza aquesta estructura a JSON.
- Suporta sortida compacta o amb format llegible (pretty print).
- Permet especificar un fitxer de sortida o imprimir per stdout.
El resultat final s’usa així:
csvtojson -input dades.csv -output resultat.json -prettyO en mode compacte directe a stdout:
csvtojson -input dades.csvSense dependències externes. Tot amb la llibreria estàndard de Go.
Setup del projecte
Crea el directori del projecte i inicialitza el mòdul:
mkdir csvtojson && cd csvtojson
go mod init csvtojsonCrea un fitxer main.go. Aquesta serà tota l’estructura per ara. Un sol fitxer, un sol paquet. Quan el projecte sigui més gran pots separar-lo, però per a una eina com aquesta no té sentit complicar-ho des del principi.
Si no tens experiència amb el comandament go, el bàsic que necessites saber és que go mod init crea el fitxer go.mod que defineix el mòdul, i go run main.go compila i executa directament sense generar un binari.
Per al CSV d’exemple, crea un fitxer dades.csv amb aquest contingut:
nom,edat,email,ciutat
Ana García,34,ana@example.com,Madrid
Pedro López,28,pedro@example.com,Barcelona
María Torres,,maria@example.com,Valencia
,45,sense-email,Sevilla
Carlos Ruiz,abc,carlos@example.com,BilbaoHe inclòs dades brutes a propòsit: una edat buida, un nom buit i una edat que no és un número. Això ens servirà per a la part de validació.
Llegir CSV amb encoding/csv
Go té el paquet encoding/csv a la seva llibreria estàndard. No necessites instal·lar res. El paquet exposa un csv.Reader que pren un io.Reader i retorna registres com a slices de strings.
package main
import (
\"encoding/csv\"
\"fmt\"
\"os\"
)
func main() {
file, err := os.Open(\"dades.csv\")
if err != nil {
fmt.Fprintf(os.Stderr, \"error en obrir el fitxer: %v\n\", err)
os.Exit(1)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
fmt.Fprintf(os.Stderr, \"error en llegir CSV: %v\n\", err)
os.Exit(1)
}
for i, record := range records {
fmt.Printf(\"Fila %d: %v\n\", i, record)
}
}reader.ReadAll() carrega tot el fitxer en memòria. Per a fitxers petits o mitjans (fins a centenars de milers de files) és el més simple i no hi ha problema de rendiment. Si necessites processar fitxers enormes, pots fer servir reader.Read() en un bucle per llegir fila a fila, però per a aquesta eina no cal.
El primer registre (records[0]) conté les capçaleres. La resta són dades. Aquesta distinció és important perquè farem servir les capçaleres per mapar cada camp a la seva posició.
Un detall del csv.Reader: per defecte assumeix que el delimitador és la coma. Si necessites punt i coma o un altre caràcter, pots canviar-lo amb reader.Comma = ';' abans de cridar ReadAll().
Mapar files a structs
Treballar amb slices de strings està bé per llegir, però per serialitzar a JSON necessitem una estructura tipada. Anem a definir un struct que representi cada fila del CSV i una funció que mapi els registres.
type Persona struct {
Nom string `json:\"nom\"`
Edat int `json:\"edat\"`
Email string `json:\"email\"`
Ciutat string `json:\"ciutat\"`
}Els tags json:\"...\" controlen com es serialitza cada camp a JSON. Sense ells, Go faria servir el nom del camp amb la primera lletra en majúscula, que no és el que volem.
Ara la funció de mapament:
import \"strconv\"
func mapRecordToPersona(header []string, record []string) (Persona, error) {
if len(record) != len(header) {
return Persona{}, fmt.Errorf(\"la fila té %d camps, s'esperaven %d\", len(record), len(header))
}
fieldMap := make(map[string]string)
for i, h := range header {
fieldMap[h] = record[i]
}
edat, err := strconv.Atoi(fieldMap[\"edat\"])
if err != nil {
return Persona{}, fmt.Errorf(\"camp 'edat' no és un número vàlid: %q\", fieldMap[\"edat\"])
}
return Persona{
Nom: fieldMap[\"nom\"],
Edat: edat,
Email: fieldMap[\"email\"],
Ciutat: fieldMap[\"ciutat\"],
}, nil
}Fem servir un mapa intermedi (fieldMap) per no dependre de l’ordre de les columnes. Això fa que l’eina funcioni encara que algú canviï l’ordre de les columnes en el CSV, sempre que els noms de les capçaleres siguin els mateixos.
strconv.Atoi converteix un string a int. Si el valor no és un número, retorna un error. Go t’obliga a gestionar-ho explícitament. Sense excepcions, sense conversions implícites. Cada error es comprova on ocorre.
Validar dades: camps buits, tipus incorrectes
La funció de mapament ja detecta tipus incorrectes en l’edat. Però necessitem una validació més completa. Camps buits, emails que no tenen sentit, files amb dades incompletes. Anem a afegir una funció de validació que retorni errors descriptius.
import \"strings\"
type ValidationError struct {
Row int
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf(\"fila %d, camp '%s': %s\", e.Row, e.Field, e.Message)
}
func validatePersona(row int, header []string, record []string) []ValidationError {
var errs []ValidationError
if len(record) != len(header) {
errs = append(errs, ValidationError{
Row: row,
Field: \"-\",
Message: fmt.Sprintf(\"nombre de camps incorrecte: té %d, s'esperaven %d\", len(record), len(header)),
})
return errs
}
fieldMap := make(map[string]string)
for i, h := range header {
fieldMap[h] = strings.TrimSpace(record[i])
}
if fieldMap[\"nom\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"nom\", Message: \"camp buit\"})
}
if fieldMap[\"edat\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"edat\", Message: \"camp buit\"})
} else if _, err := strconv.Atoi(fieldMap[\"edat\"]); err != nil {
errs = append(errs, ValidationError{Row: row, Field: \"edat\", Message: fmt.Sprintf(\"no és un número vàlid: %q\", fieldMap[\"edat\"])})
}
if fieldMap[\"email\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"email\", Message: \"camp buit\"})
} else if !strings.Contains(fieldMap[\"email\"], \"@\") {
errs = append(errs, ValidationError{Row: row, Field: \"email\", Message: \"format d'email invàlid\"})
}
if fieldMap[\"ciutat\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"ciutat\", Message: \"camp buit\"})
}
return errs
}Definim un tipus ValidationError que implementa la interfície error. Això és idiomàtic en Go: en lloc de llançar excepcions, retornes valors que descriuen el problema. El codi que crida decideix què fer amb ells. Pot avortar l’execució, saltar la fila o acumular els errors i mostrar un resum.
La validació de l’email és bàsica: només comprovem que contingui @. Per a una eina real podries usar net/mail.ParseAddress, però per al nostre cas és suficient.
Convertir a JSON amb encoding/json
Amb les dades validades i mapades a structs, la conversió a JSON és trivial. El paquet encoding/json de la llibreria estàndard fa tota la feina.
import \"encoding/json\"
func toJSON(persones []Persona, pretty bool) ([]byte, error) {
if pretty {
return json.MarshalIndent(persones, \"\", \" \")
}
return json.Marshal(persones)
}json.Marshal serialitza qualsevol struct amb tags JSON a un []byte. json.MarshalIndent fa el mateix però amb indentació llegible. Els dos primers arguments extra són el prefix (normalment buit) i la cadena d’indentació.
El resultat amb pretty = true és així:
[
{
\"nom\": \"Ana García\",
\"edat\": 34,
\"email\": \"ana@example.com\",
\"ciutat\": \"Madrid\"
},
{
\"nom\": \"Pedro López\",
\"edat\": 28,
\"email\": \"pedro@example.com\",
\"ciutat\": \"Barcelona\"
}
]I amb pretty = false:
[{\"nom\":\"Ana García\",\"edat\":34,\"email\":\"ana@example.com\",\"ciutat\":\"Madrid\"},{\"nom\":\"Pedro López\",\"edat\":28,\"email\":\"pedro@example.com\",\"ciutat\":\"Barcelona\"}]La versió compacta és millor per a pipelines i processament automatitzat. La versió amb format és millor per depurar o per a humans.
Pretty printing vs sortida compacta
La diferència entre ambdós modes no és només estètica. En producció, quan connectes la sortida d’una eina amb una altra mitjançant pipes, la sortida compacta és el que necessites. Cada byte compta si estàs processant milions de registres.
Però durant el desenvolupament, o quan vols inspeccionar el resultat manualment, el pretty print estalvia temps. No necessites passar el JSON per jq o enganxar-lo en un formatter online.
Per això la nostra eina suporta ambdós modes. El flag -pretty activa la indentació. Sense ell, la sortida és compacta per defecte. És una convenció habitual en eines CLI: el mode silenciós i eficient és el default, el mode humà s’activa explícitament.
Un truc útil: si la teva eina només escriu a stdout, pots combinar-la amb jq per formatar sobre la marxa:
csvtojson -input dades.csv | jq .Però tenir el flag integrat és més còmode i elimina la dependència de jq.
Afegir flags CLI: fitxer d’entrada, de sortida i opcions de format
Go té el paquet flag a la seva llibreria estàndard. No necessites cobra, urfave/cli ni cap altra dependència per a una eina simple. Si més endavant necessites subcomandos o autocompletat, pots mirar com crear una CLI en Go amb eines més avançades. Però per a això, flag és perfecte.
import \"flag\"
func main() {
inputFile := flag.String(\"input\", \"\", \"fitxer CSV d'entrada (obligatori)\")
outputFile := flag.String(\"output\", \"\", \"fitxer JSON de sortida (stdout si no s'especifica)\")
pretty := flag.Bool(\"pretty\", false, \"format JSON amb indentació\")
strict := flag.Bool(\"strict\", false, \"avortar si hi ha errors de validació\")
flag.Parse()
if *inputFile == \"\" {
fmt.Fprintln(os.Stderr, \"error: has d'especificar un fitxer d'entrada amb -input\")
flag.Usage()
os.Exit(1)
}
}flag.String i flag.Bool retornen punters. Cal desreferenciar-los amb * per obtenir el valor. És una de les coses que al principi es fa estrany en Go, però té la seva lògica: flag.Parse() omple els valors després de definir-los, de manera que necessita una referència mutable.
El flag -strict és útil per a diferents escenaris. En mode normal, l’eina salta les files amb errors i mostra warnings. En mode estricte, qualsevol error de validació atura l’execució. Això és important quan uses l’eina dins d’un pipeline de dades on no vols dades parcials.
Gestió d’errors: missatges clars per a l’usuari
Go no té excepcions. Cada funció que pot fallar retorna un error com a últim valor de retorn. Això significa que el codi de gestió d’errors és sempre visible, sempre explícit. A canvi, obtens control total sobre què fer en cada cas.
Per a una eina CLI, els missatges d’error han de ser útils. Res d‘“error: something went wrong”. L’usuari necessita saber quin fitxer ha fallat, en quina fila és el problema i quin camp té dades incorrectes.
func processCSV(inputPath string, strict bool) ([]Persona, error) {
file, err := os.Open(inputPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(\"el fitxer '%s' no existeix\", inputPath)
}
if os.IsPermission(err) {
return nil, fmt.Errorf(\"sense permisos per llegir '%s'\", inputPath)
}
return nil, fmt.Errorf(\"no es pot obrir '%s': %w\", inputPath, err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf(\"error en parsejar CSV: %w\", err)
}
if len(records) < 2 {
return nil, fmt.Errorf(\"el fitxer CSV és buit o només té capçaleres\")
}
header := records[0]
var persones []Persona
var allErrors []ValidationError
for i, record := range records[1:] {
rowNum := i + 2 // +2 perquè comencem en 1 i saltem la capçalera
validationErrs := validatePersona(rowNum, header, record)
if len(validationErrs) > 0 {
allErrors = append(allErrors, validationErrs...)
if strict {
return nil, fmt.Errorf(\"mode estricte: %v\", validationErrs[0])
}
for _, ve := range validationErrs {
fmt.Fprintf(os.Stderr, \"warning: %v\n\", ve)
}
continue
}
persona, err := mapRecordToPersona(header, record)
if err != nil {
fmt.Fprintf(os.Stderr, \"warning: fila %d: %v\n\", rowNum, err)
continue
}
persones = append(persones, persona)
}
if len(persones) == 0 {
return nil, fmt.Errorf(\"cap fila vàlida trobada (%d errors)\", len(allErrors))
}
if len(allErrors) > 0 {
fmt.Fprintf(os.Stderr, \"\n%d files amb errors, %d files processades correctament\n\", len(allErrors), len(persones))
}
return persones, nil
}Hi ha diversos punts importants aquí:
- Wrapping d’errors amb
%w: permet que el codi que crida inspeccioni la causa arrel amberrors.Isoerrors.As. És la forma estàndard d’encadenar errors en Go des de la versió 1.13. - Warnings a stderr: els missatges d’avís van a
os.Stderrper no contaminar la sortida JSON que va a stdout. Això és fonamental si l’eina s’usa en un pipeline. - Resum final: en acabar, l’usuari veu quantes files s’han processat i quantes han fallat. Informació, no només un exit code.
Compilar i distribuir el binari
Un dels avantatges pràctics de Go és que compila a un binari estàtic. Sense dependències de runtime, sense necessitat que la màquina destí tingui Go instal·lat. Copies el binari i funciona.
go build -o csvtojson main.goAixò genera un executable csvtojson per al teu sistema operatiu i arquitectura actual. Per distribuir-lo a altres plataformes, pots fer cross-compilation:
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -o csvtojson-linux-amd64 main.go
# macOS ARM (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o csvtojson-darwin-arm64 main.go
# Windows
GOOS=windows GOARCH=amd64 go build -o csvtojson.exe main.goNo necessites Docker, ni una VM, ni un entorn de CI complet. Go compila per a qualsevol plataforma des de qualsevol plataforma. És una de les raons per les quals Go és tan popular per a eines de línia de comandos.
Per reduir la mida del binari, pots usar el flag -ldflags:
go build -ldflags=\"-s -w\" -o csvtojson main.go-s elimina la taula de símbols i -w elimina la informació de debug DWARF. En una eina petita com aquesta passes d’uns 5-6 MB a 3-4 MB. No és crític, però és bona pràctica.
Si vols instal·lar l’eina directament al teu $GOPATH/bin:
go installI ja pots executar csvtojson des de qualsevol directori.
Estendre l’eina: filtrat, ordre i més formats
Un cop tens la base funcionant, hi ha extensions naturals que pots afegir sense canviar l’arquitectura. Cadascuna t’obliga a aprendre alguna cosa nova de Go.
Filtrar files
Afegeix un flag -filter que accepti una expressió simple com ciutat=Madrid:
filterExpr := flag.String(\"filter\", \"\", \"filtrar files (camp=valor)\")I després filtra després de la validació:
func filterPersones(persones []Persona, field, value string) []Persona {
var result []Persona
for _, p := range persones {
match := false
switch field {
case \"nom\":
match = strings.EqualFold(p.Nom, value)
case \"ciutat\":
match = strings.EqualFold(p.Ciutat, value)
case \"email\":
match = strings.EqualFold(p.Email, value)
}
if match {
result = append(result, p)
}
}
return result
}strings.EqualFold fa comparació case-insensitive. És més robust que convertir tot a minúscules manualment.
Ordenar resultats
Usa el paquet sort de la llibreria estàndard:
import \"sort\"
func sortPersones(persones []Persona, field string, ascending bool) {
sort.Slice(persones, func(i, j int) bool {
var less bool
switch field {
case \"nom\":
less = persones[i].Nom < persones[j].Nom
case \"edat\":
less = persones[i].Edat < persones[j].Edat
case \"ciutat\":
less = persones[i].Ciutat < persones[j].Ciutat
default:
less = persones[i].Nom < persones[j].Nom
}
if ascending {
return less
}
return !less
})
}sort.Slice és la forma estàndard d’ordenar slices en Go. Rep una funció de comparació que retorna true si l’element i ha d’anar abans que el j.
Suport per a altres formats de sortida
Pots afegir sortida en format YAML, NDJSON (una línia JSON per registre, útil per a streaming) o fins i tot taules de text:
func toNDJSON(persones []Persona) ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
for _, p := range persones {
if err := encoder.Encode(p); err != nil {
return nil, fmt.Errorf(\"error codificant persona: %w\", err)
}
}
return buf.Bytes(), nil
}NDJSON és especialment pràctic quan treballes amb eines com jq, perquè cada línia és un JSON vàlid independent. Pots processar-les amb grep, head, tail o qualsevol eina Unix estàndard.
Codi complet
Aquí hi ha el programa complet, llest per compilar i executar:
package main
import (
\"bytes\"
\"encoding/csv\"
\"encoding/json\"
\"flag\"
\"fmt\"
\"os\"
\"strconv\"
\"strings\"
)
type Persona struct {
Nom string `json:\"nom\"`
Edat int `json:\"edat\"`
Email string `json:\"email\"`
Ciutat string `json:\"ciutat\"`
}
type ValidationError struct {
Row int
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf(\"fila %d, camp '%s': %s\", e.Row, e.Field, e.Message)
}
func validatePersona(row int, header []string, record []string) []ValidationError {
var errs []ValidationError
if len(record) != len(header) {
errs = append(errs, ValidationError{
Row: row,
Field: \"-\",
Message: fmt.Sprintf(\"nombre de camps incorrecte: té %d, s'esperaven %d\", len(record), len(header)),
})
return errs
}
fieldMap := make(map[string]string)
for i, h := range header {
fieldMap[h] = strings.TrimSpace(record[i])
}
if fieldMap[\"nom\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"nom\", Message: \"camp buit\"})
}
if fieldMap[\"edat\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"edat\", Message: \"camp buit\"})
} else if _, err := strconv.Atoi(fieldMap[\"edat\"]); err != nil {
errs = append(errs, ValidationError{Row: row, Field: \"edat\", Message: fmt.Sprintf(\"no és un número vàlid: %q\", fieldMap[\"edat\"])})
}
if fieldMap[\"email\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"email\", Message: \"camp buit\"})
} else if !strings.Contains(fieldMap[\"email\"], \"@\") {
errs = append(errs, ValidationError{Row: row, Field: \"email\", Message: \"format d'email invàlid\"})
}
if fieldMap[\"ciutat\"] == \"\" {
errs = append(errs, ValidationError{Row: row, Field: \"ciutat\", Message: \"camp buit\"})
}
return errs
}
func mapRecordToPersona(header []string, record []string) (Persona, error) {
if len(record) != len(header) {
return Persona{}, fmt.Errorf(\"la fila té %d camps, s'esperaven %d\", len(record), len(header))
}
fieldMap := make(map[string]string)
for i, h := range header {
fieldMap[h] = strings.TrimSpace(record[i])
}
edat, err := strconv.Atoi(fieldMap[\"edat\"])
if err != nil {
return Persona{}, fmt.Errorf(\"camp 'edat' no és un número vàlid: %q\", fieldMap[\"edat\"])
}
return Persona{
Nom: fieldMap[\"nom\"],
Edat: edat,
Email: fieldMap[\"email\"],
Ciutat: fieldMap[\"ciutat\"],
}, nil
}
func processCSV(inputPath string, strict bool) ([]Persona, error) {
file, err := os.Open(inputPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf(\"el fitxer '%s' no existeix\", inputPath)
}
if os.IsPermission(err) {
return nil, fmt.Errorf(\"sense permisos per llegir '%s'\", inputPath)
}
return nil, fmt.Errorf(\"no es pot obrir '%s': %w\", inputPath, err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf(\"error en parsejar CSV: %w\", err)
}
if len(records) < 2 {
return nil, fmt.Errorf(\"el fitxer CSV és buit o només té capçaleres\")
}
header := records[0]
var persones []Persona
var allErrors []ValidationError
for i, record := range records[1:] {
rowNum := i + 2
validationErrs := validatePersona(rowNum, header, record)
if len(validationErrs) > 0 {
allErrors = append(allErrors, validationErrs...)
if strict {
return nil, fmt.Errorf(\"mode estricte: %v\", validationErrs[0])
}
for _, ve := range validationErrs {
fmt.Fprintf(os.Stderr, \"warning: %v\n\", ve)
}
continue
}
persona, err := mapRecordToPersona(header, record)
if err != nil {
fmt.Fprintf(os.Stderr, \"warning: fila %d: %v\n\", rowNum, err)
continue
}
persones = append(persones, persona)
}
if len(persones) == 0 {
return nil, fmt.Errorf(\"cap fila vàlida trobada (%d errors)\", len(allErrors))
}
if len(allErrors) > 0 {
fmt.Fprintf(os.Stderr, \"\n%d files amb errors, %d files processades correctament\n\", len(allErrors), len(persones))
}
return persones, nil
}
func toJSON(persones []Persona, pretty bool) ([]byte, error) {
if pretty {
return json.MarshalIndent(persones, \"\", \" \")
}
return json.Marshal(persones)
}
func toNDJSON(persones []Persona) ([]byte, error) {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
for _, p := range persones {
if err := encoder.Encode(p); err != nil {
return nil, fmt.Errorf(\"error codificant registre: %w\", err)
}
}
return buf.Bytes(), nil
}
func main() {
inputFile := flag.String(\"input\", \"\", \"fitxer CSV d'entrada (obligatori)\")
outputFile := flag.String(\"output\", \"\", \"fitxer JSON de sortida (stdout si no s'especifica)\")
pretty := flag.Bool(\"pretty\", false, \"format JSON amb indentació\")
strict := flag.Bool(\"strict\", false, \"avortar si hi ha errors de validació\")
format := flag.String(\"format\", \"json\", \"format de sortida: json o ndjson\")
flag.Parse()
if *inputFile == \"\" {
fmt.Fprintln(os.Stderr, \"error: has d'especificar un fitxer d'entrada amb -input\")
flag.Usage()
os.Exit(1)
}
persones, err := processCSV(*inputFile, *strict)
if err != nil {
fmt.Fprintf(os.Stderr, \"error: %v\n\", err)
os.Exit(1)
}
var output []byte
switch *format {
case \"json\":
output, err = toJSON(persones, *pretty)
case \"ndjson\":
output, err = toNDJSON(persones)
default:
fmt.Fprintf(os.Stderr, \"error: format desconegut '%s' (usa 'json' o 'ndjson')\n\", *format)
os.Exit(1)
}
if err != nil {
fmt.Fprintf(os.Stderr, \"error en generar la sortida: %v\n\", err)
os.Exit(1)
}
if *outputFile != \"\" {
err = os.WriteFile(*outputFile, output, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, \"error en escriure '%s': %v\n\", *outputFile, err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, \"JSON escrit a '%s' (%d registres)\n\", *outputFile, len(persones))
} else {
fmt.Println(string(output))
}
}Executar i provar
# Compilar
go build -o csvtojson main.go
# Sortida compacta per stdout
./csvtojson -input dades.csv
# Sortida formatada a fitxer
./csvtojson -input dades.csv -output resultat.json -pretty
# Mode estricte: falla al primer error
./csvtojson -input dades.csv -strict
# Format NDJSON
./csvtojson -input dades.csv -format ndjsonAmb el nostre CSV d’exemple, la sortida inclourà warnings per a les files amb dades problemàtiques i només les files vàlides apareixeran en el JSON resultant:
$ ./csvtojson -input dades.csv -pretty
warning: fila 4, camp 'edat': camp buit
warning: fila 5, camp 'nom': camp buit
warning: fila 5, camp 'email': format d'email invàlid
warning: fila 6, camp 'edat': no és un número vàlid: \"abc\"
3 files amb errors, 2 files processades correctament
[
{
\"nom\": \"Ana García\",
\"edat\": 34,
\"email\": \"ana@example.com\",
\"ciutat\": \"Madrid\"
},
{
\"nom\": \"Pedro López\",
\"edat\": 28,
\"email\": \"pedro@example.com\",
\"ciutat\": \"Barcelona\"
}
]Menys de 200 línies i zero dependències
Amb menys de 200 línies de Go has construït una eina de línia de comandos funcional que llegeix CSV, valida dades, converteix a JSON i suporta múltiples formats de sortida. Tot amb la llibreria estàndard. Sense dependències externes, sense frameworks, sense màgia. En el procés has tocat els paquets fonamentals del llenguatge: encoding/csv i encoding/json per a la transformació de dades, flag per a arguments, strconv per a conversió de tipus, i os per a la interacció amb el sistema de fitxers. Però el més important és que has practicat el patró if err != nil i l’ús de structs amb tags, que són la base de qualsevol programa Go.
Aquest tipus de projecte és exactament el que necessites per assentar els fonaments del llenguatge. No és un exercici teòric. És una eina que pots usar en el teu dia a dia, estendre segons les teves necessitats i compilar per a qualsevol plataforma amb un sol comandament.
Si vols continuar practicant amb projectes reals, fes un cop d’ull a la llista de projectes per aprendre Go. I si t’interessa portar les eines CLI més lluny, amb subcomandos i autocompletat, el següent pas natural és construir una CLI en Go amb biblioteques especialitzades.


