I wrote go-fp because Go's error handling was killing my code's readability. A few months later, it runs in production handling healthcare catalogue imports for Romania's national health system—crawling and updating data from the official sources.
This is not a library tutorial. This is what happens when you combine code generation with functional composition.
The Problem
Healthcare catalogues. Thirteen different XML types. For all 42 counties in Romania (41 counties + Bucharest). Each county publishes their own version—and each can contain their own errors and inconsistencies. It's tragic, really.
Each catalogue needs to be:
- Decoded from XML
- Validated against business rules
- Saved to PostgreSQL
The traditional Go approach: 13 import functions × 42 counties worth of edge cases, each with its own error handling, each slightly different, each a maintenance nightmare.
The XSD Pipeline
The Romanian national health system defines catalogues in XSD schemas. We generate Go structs from XSD. The compiler then checks everything.
XSD Schema → Code Generator → Go Structs → Compile-time Validation
A single catalogue type looks like this after generation:
type CataloguesAmbulance struct {
XMLName xml.Name `xml:"Catalogues"`
IssueDate ROTime `xml:"issueDate,attr"`
Countries []Country `xml:"Countries>Country"`
Districts []District `xml:"Districts>District"`
Hospitals []Hospital `xml:"Hospitals>Hospital"`
Physicians []Physician `xml:"Physicians>Physician"`
Drugs []Drug `xml:"Drugs>Drug"`
// ... 50+ more fields
}
Fifty-eight fields per catalogue. Thirteen catalogue types. All generated. All statically typed. The compiler catches schema mismatches before runtime.
Generated Validation
Each generated type gets a Validate() method that chains field validations:
func (ca CataloguesAmbulance) Validate() error {
return chainValidations(
func() error { return ca.IssueDate.Validate("catalogue ambulance", true) },
func() error { return Countries{Country: ca.Countries}.Validate() },
func() error { return Districts{District: ca.Districts}.Validate() },
func() error { return Hospitals{Hospital: ca.Hospitals}.Validate() },
// ... every field validated
)
}
The validators are generated from XSD constraints and PDF documentation that gets updated whenever new legislation passes—sometimes overnight. String length limits, required fields, numeric precision—all compile-time checked. When the rules change, regenerate, recompile, redeploy. Minutes, not days.
func ValidateString(value, field string, required bool, maxLen int) error {
if required && value == "" {
return fmt.Errorf("%s is required", field)
}
if maxLen > 0 && len(value) > maxLen {
return fmt.Errorf("%s exceeds max length %d", field, maxLen, len(value))
}
return nil
}
No runtime reflection. No string parsing. Just generated code that the compiler verifies.
Where go-fp Comes In
Generated code gives you types. But you still need to compose operations over those types. This is where go-fp turns 113 files into one generic function.
The Interface
Every generated catalogue type implements one interface:
type HasValidateAndSaveDB interface {
Validate() error
SaveToDB(ctx context.Context) error
}
That's it. Thirteen types. One contract. The interface tells you exactly what the system needs: validate your data, save yourself to the database.
The Chain
go-fp provides Chain[T], a monadic type that carries either a value or an error. No more if err != nil after every operation.
func DecodeXMLChain[T any](filePath string) chain.Chain[T] {
return chain.LiftResult(func() (T, error) {
var val T
f, err := os.Open(filePath)
if err != nil {
return val, err
}
defer f.Close()
err = xml.NewDecoder(f).Decode(&val)
return val, err
})
}
LiftResult takes a function that returns (T, error) and wraps it into a Chain[T]. The error is captured, not checked. It propagates automatically.
The Composition
Here's the entire import pipeline:
func ImportCatalogueChain[T HasValidateAndSaveDB](in CatalogueInput) chain.Chain[T] {
fileChain := chain.Wrap(in.FilePath)
xmlChain := chain.Bind(fileChain, DecodeXMLChain[T])
validateChain := chain.Bind(xmlChain, ValidateChain[T])
saveChain := chain.Bind(validateChain, func(cat T) chain.Chain[T] {
return SaveToDBChain(in.Ctx, cat)
})
return saveChain
}
Read it top to bottom:
- Wrap the file path
- Bind to XML decoding
- Bind to validation
- Bind to database save
If any step fails, the chain short-circuits. No nested ifs. No error checks between steps. The composition handles it.
The Helpers
func ValidateChain[T HasValidateAndSaveDB](cat T) chain.Chain[T] {
return chain.LiftResult(func() (T, error) {
return cat, cat.Validate()
})
}
func SaveToDBChain[T HasValidateAndSaveDB](ctx context.Context, cat T) chain.Chain[T] {
return chain.LiftResult(func() (T, error) {
return cat, cat.SaveToDB(ctx)
})
}
Each helper is three lines. Each does one thing. Compose them freely.
The Dynamic Dispatch
Catalogue types are determined by filename. A map handles the dispatch:
var CatalogueConstructors = map[string]func(CatalogueInput) chain.Chain[any]{
"medicaments": wrapChain(ImportCatalogueChain[Medicaments]),
"diagnostics": wrapChain(ImportCatalogueChain[Diagnostics]),
"procedures": wrapChain(ImportCatalogueChain[Procedures]),
// ... 10 more types
}
This is critical. If you pass interfaces around randomly in functions, you get runtime type checks. The compiler can't optimize. It's slow and fragile.
With the constructor map, each entry instantiates a concrete generic type at compile time. The compiler sees ImportCatalogueChain[Medicaments], not ImportCatalogueChain[interface{}]. It generates optimized, strongly-typed code for each catalogue type. The only runtime dispatch is the map lookup—one string comparison.
Add a new catalogue type: implement the interface, add one line to the map. Done. The compiler handles the rest.
The Execution
result := CatalogueConstructors[catalogueType](input)
chain.Match(result,
func(cat any) {
log.Printf("Imported %s successfully", catalogueType)
},
func(err error) {
log.Printf("Failed to import %s: %v", catalogueType, err)
},
)
Match forces you to handle both cases. Success or failure. No forgotten error checks.
Why This Matters
Code generation: XSD → Go structs → compile-time type safety
go-fp: Generic composition over all generated types
Result: 113 files, 13 catalogue types, 1 import function
The generated code gives you types. go-fp gives you composition. Together, they give you a system that scales without complexity.
Add a new catalogue type from the national health system:
- Run the generator on the new XSD
- Add one line to the constructor map
- Done. The generic function handles it.
The compiler catches type mismatches. go-fp catches error handling. Datasets are bounded in size and checked before processing. You write minimal code that is statically checked.
The Library
go-fp is minimal. The core primitives:
Chain[T]— the monadic containerWrap(value)— lift a value into ChainLiftResult(func() (T, error))— lift a Go-style functionBind(chain, func(T) Chain[U])— compose chainsMap(chain, func(T) U)— transform valuesMatch(chain, onSuccess, onError)— handle the result
That's the entire API that matters. Everything else builds on these.
Get It
go get github.com/KeibiSoft/go-fp
MIT licensed. Production tested. 113 files worth of healthcare data imports prove it works.