# gocsv: CSV serialization driven by Go struct tags and reflection

> gocsv turns a struct with csv tags into a CSV file and back again, using MarshalCSV and UnmarshalCSV methods for the fields the standard library cannot convert on its own.

**gocarina/gocsv** — The GoCSV package aims to provide easy CSV serialization and deserialization to the golang programming language

- Repository: https://github.com/gocarina/gocsv
- Stars: 2,199 · Forks: 263
- Language: Go
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/gocarina-gocsv

## Installing gocsv and what the module file says about age

Installation is a single line, taken straight from the README:

```bash
go get -u github.com/gocarina/gocsv
```

The API and the techniques are described in the README as inspired by gopkg.in/mgo.v2, which is worth knowing because that library's struct tag style is where the `csv:"field_name"` convention comes from.

The module file is short and informative:

```go
module github.com/gocarina/gocsv

go 1.13
```

A declared Go version of 1.13 tells you the library has been in place long enough to be used widely and has not needed a language feature bump. It is not a limit you will hit. What it does mean is that there is no dependency list, because gocsv builds on the standard library's `encoding/csv` and nothing else. There are also no published GitHub releases for the repository, so the commit history is the only record of what changed.

The repository tree separates the concern the package name suggests. `decode.go` and `encode.go` are the two directions of the conversion, `reflect.go` holds the struct walking, `types.go` defines the interface a custom type has to satisfy, and `safe_csv.go` is a separate writer that defers error reporting rather than panicking on the first bad record. Test files sit next to each of them, plus `callback_error_test.go` and `custom_extra_columns_test.go` for behaviours that are easy to get wrong.

## Binding columns to fields with the csv struct tag

The README's full example starts from a plain CSV file with a `client_id`, `client_name`, `client_age` header and three data rows, then maps it onto a struct. The struct in the example is where the whole design sits:

```go
type Client struct { // Our example struct, you can use "-" to ignore a field
	Id            string `csv:"client_id"`
	Name          string `csv:"client_name"`
	Age           string `csv:"-"`
	NotUsedString string `csv:"-"`
	NotUsedStruct NotUsed `csv:"-"`
}
```

Three things are happening there. A `csv` tag names the column, so `client_id` in the file reaches the `Id` field even though the Go name is different. A `-` tag excludes a field in both directions, and the README notes you can use that on a whole struct as well as a string. And the fields are declared as `string` rather than `int`, which is the deliberate choice that lets a row with a missing value load without a parse error.

That last point is the library's real behavioural contract. The example appends four more clients after loading, and two of them omit `Age` entirely, which only works because an absent column is an empty string rather than a zero value that must be parsed. If you declare `Age int`, gocsv will attempt conversion and the omitted values become a different kind of problem.

The read and write calls are ordinary functions. `gocsv.UnmarshalFile` takes an `io.ReadSeeker` and a pointer to a slice; `gocsv.MarshalString` returns the CSV as a string, and `gocsv.MarshalFile` writes it back to the same handle. The example seeks back to offset zero before writing, which is the one piece of file handling you have to remember yourself: gocsv does not rewind for you.

## Custom converters for types the standard library cannot handle

A `time.Time` field is the case where the built-in conversion gives up, and the README's answer is a pair of methods on your own type. The example defines a `DateTime` wrapper and implements three methods on it:

```go
func (date *DateTime) MarshalCSV() (string, error) {
	return date.Time.Format("20060201"), nil
}

func (date *DateTime) UnmarshalCSV(csv string) (err error) {
	date.Time, err = time.Parse("20060201", csv)
	return err
}
```

The layout string `20060201` is the unusual part and it is worth pausing on. That is a date without separators, so a CSV cell reads `20260825` rather than `2026-08-25`. It is a legitimate choice for fixed-width export, but it is a choice that changes the shape of your data files, and you should treat the README example as a demonstration of the interface rather than a recommendation.

The `String()` method in the example is labelled redundant in the README's own comment, which is a useful signal about how the resolution order works: gocsv looks for the CSV interface methods first, and a plain `String()` is a fallback for display, not for serialization.

Because the contract is just two methods, the pattern extends past dates. A money type, an enum stored as a code, or a type that must serialize as a nested object all fit the same shape. `types.go` in the repository is the file to read when you want the exact interface signatures rather than the example.

## Nested structs get prefixed columns unless you say otherwise

Struct fields that are themselves structs are flattened into columns, and the README documents the naming rule precisely. Given a `Foo` with a `Bar` field and an `X int`, and a `Bar` with `A` and `B`, the default output columns are `B.a, B.b, x`. The parent's field name becomes a prefix.

You override that with a dot in the tag, which inlines the nested struct's own field names:

```go
type Foo struct {
	B Bar `csv:"."`
	X int `csv:"x"`
}
```

which produces columns `a, b, x`. The distinction between the two forms matters more than it looks, because it is the difference between a file other tools can read and a file that has invented column names in it.

The README is also candid about pointer allocation here. When decoding nested pointer structs, gocsv allocates the pointers needed to reach each field, and an `omitempty` field still omits an empty leaf pointer value on output while not preventing allocation of the enclosing structs. In other words, `omitempty` controls what gets written, not what gets allocated on the way in. If your model has deep nesting and you care about allocation, that sentence is the one to design around.

## Swapping the reader and writer for a different dialect

gocsv does not parse CSV itself, so changing the dialect means handing it a differently configured `encoding/csv` reader. The README shows this through `gocsv.SetCSVReader`, and three variants appear in the example, all returning a `gocsv.CSVReader`:

```go
func main() {
	...

	gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
		r := csv.NewReader(in)
		r.Comma = '|'
		return r // Allows use pipe as delimiter
	})

	...
}
```

Setting `Comma` to a pipe makes that the delimiter. Setting `LazyQuotes` to true with a dot delimiter handles files where quoting is inconsistent, which is common in exports written by older tools. A third option returns `gocsv.LazyCSVReader` directly, which allows quotes inside unquoted fields without configuring anything.

The writer has the mirror hook, `gocsv.SetCSVWriter`, which takes an `io.Writer` and returns a `*gocsv.SafeCSVWriter`. The Safe variant is what the returned type tells you: errors are collected instead of being lost, because `encoding/csv`'s writer panics on a malformed record and a batch import is exactly the place you do not want a panic.

One design consequence worth naming: these are package level functions, not options on a call. They set global state for the process, so configuring the dialect once in an initialiser is the sane pattern, and configuring it differently for two files in the same program is not something the API supports.

## Conclusion

gocsv earns its place in code that exchanges CSV files as a normal part of a pipeline rather than as a special case, because the mapping stays in your struct definition instead of in a column list that drifts. Its limits are equally clear: it delegates parsing to encoding/csv, it has never published a release, and its go.mod still declares Go 1.13, which tells you the module predates the current toolchain even though the last push to master was on 2026-09-26. If your files need a custom delimiter or lenient quoting, the SetCSVReader and SetCSVWriter hooks cover it; if you need streaming over a very large file, the UnmarshalFile shape will buffer. For anything else, start with the Client struct from the README and check that your column headers line up before writing the rest of the mapping.

## FAQ

### How can I read a CSV file in Golang?

The standard library reads CSV through encoding/csv, and gocsv builds on that by adding struct binding on top. You declare a struct with `csv:"column_name"` tags on the fields you want, then call gocsv.UnmarshalFile with a pointer to a slice of that struct.

### How do I convert a custom type such as a date to a CSV string?

Implement the converter methods on your type, as the README's DateTime example does: a MarshalCSV returning a string and an UnmarshalCSV that parses one. gocsv checks for these methods on any field type, so the pattern extends to money types, enums and nested values.

### Can gocsv handle CSV files that use a different delimiter?

Yes, through gocsv.SetCSVReader. The README shows configuring an encoding/csv reader with Comma set to a pipe, and a second example combining LazyQuotes with a dot delimiter. The matching writer hook is gocsv.SetCSVWriter, which returns a SafeCSVWriter that collects errors rather than panicking.

### What license is gocsv released under?

The repository is MIT licensed. The package has no published GitHub releases, so its module file declares Go 1.13 and its history is tracked through commits only.

## Sources

- [gocarina/gocsv on GitHub](https://github.com/gocarina/gocsv)
- [Issues](https://github.com/gocarina/gocsv/issues)
- [License: MIT](https://github.com/gocarina/gocsv/blob/master/LICENSE)
- [README](https://github.com/gocarina/gocsv/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gocarina-gocsv
