Frequently asked questions.
Common questions about fp-go, functional programming in Go, and when to use these patterns.
Getting started.
fp-go is a comprehensive functional programming library for Go providing monadic types (Result, Either, Option, IO, Reader, …) and utilities for composing operations, handling errors, and managing side effects in a type-safe, functional way.
- Type-safe error handling with Result/Either
- Automatic error propagation through pipelines
- IO monad for managing side effects
- Reader monad for dependency injection
- Comprehensive collection operations
- Full composition utilities (Pipe, Flow, Compose)
Short answer: not initially, but learning FP concepts will help you use it effectively.
Start with Result/Either for error handling. Learn Pipe for composition. Gradually explore Map, Chain, and the rest.
Resources: 5-Minute Quickstart, Core Concepts, Pure Functions, Monads Explained.
You're on Go 1.24+.
Starting a new project.
Want latest features (Result, Effect, idiomatic packages).
Want better type inference.
Stuck on Go 1.18–1.23.
Have existing v1 codebase.
Need Writer monad (v1 only).
Recommendation: use v2 for all new projects. See the migration guide for upgrading.
Yes. fp-go is used in production at IBM (creator and maintainer) and other companies. v2 is stable, actively maintained, and recommended. v1 is stable and in maintenance mode.
Performance
Standard packages: small overhead from function calls. Typically 5–15% slower than hand-written code. Negligible for most applications. Worth it for type safety and maintainability.
Idiomatic packages (v2 only): 2–32× faster than standard packages. Near-native performance. Use native Go tuples instead of generic types. Recommended for performance-critical code.
| Variant | Speed | ns/op | B/op | Δ |
|---|---|---|---|---|
| Filter — stdlib | 1.00x | — | baseline | |
| Filter — fp-go (idiomatic) | 1.03x | — | +3% | |
| Filter — fp-go (standard) | 1.14x | — | +14% | |
| Map — fp-go (idiomatic) | 1.02x | — | +2% | |
| Map — fp-go (standard) | 1.12x | — | +12% | |
| Reduce — fp-go (idiomatic) | 1.04x | — | +4% | |
| Reduce — fp-go (standard) | 1.15x | — | +15% |
Fast enough for 99% of use cases. Use idiomatic packages for hot paths.
Performance is critical.
Processing large datasets.
In tight loops.
Hot code paths.
Type safety matters most.
Performance is adequate.
Code clarity matters most.
Not performance-critical.
// Standard - type-safe, slightly slower
result := array.Map(func(x int) int { return x * 2 })(data)
// Idiomatic - near-native speed
result := idiomatic.Map(data, func(x int) int { return x * 2 })See the performance guide for details.
No more than idiomatic Go. Result/Either/Option are single allocations per value. Pipelines have intermediate allocations (same as manual code). Idiomatic packages have minimal allocations. For memory-efficient patterns: use iterators for lazy evaluation, use idiomatic packages for large datasets, avoid unnecessary intermediate collections.
Error handling.
For error handling.
Error type is always error.
Simpler API, more idiomatic for Go.
You need non-error Left values.
Sum types beyond error handling.
Porting from other FP languages.
// Result - recommended for errors
func divide(a, b int) result.Result[int] {
if b == 0 {
return result.Err[int](errors.New("division by zero"))
}
return result.Ok(a / b)
}
// Either - for generic sum types
func parseValue(s string) either.Either[ParseError, Value] {
// Left can be any type, not just error
}Option 1: Stop at first error (default).
result := function.Pipe3( step1(), result.Chain(step2), result.Chain(step3), ) // Stops at first error
Option 2: Accumulate errors.
results := array.TraverseResult(validate)(items) // Returns Result[[]Item] - all or nothing // For accumulating all errors, use Validation applicative // (Advanced pattern - see recipes)
Option 3: Collect errors manually.
var errors []error
for _, item := range items {
if err := validate(item); err != nil {
errors = append(errors, err)
}
}Yes — use the conversion helpers.
// From (value, error) to Result
func fetchUser(id string) result.Result[User] {
user, err := db.Query(id)
return result.FromGoError(user, err)
}
// From Result to (value, error)
func legacyAPI() (User, error) {
result := fetchUser("123")
return result.ToGoError()
}Usage patterns.
Complex error handling logic.
Data transformation pipelines.
Composable business logic.
Side effect management.
Dependency injection patterns.
Simple CRUD operations.
Straightforward logic.
Performance-critical hot paths.
When team is unfamiliar with FP.
// Complex pipeline with error handling
func processOrder(order Order) result.Result[Receipt] {
return function.Pipe5(
validateOrder(order),
result.Chain(checkInventory),
result.Chain(calculatePrice),
result.Chain(applyDiscounts),
result.Chain(generateReceipt),
)
}
// Simple database query
func getUser(id string) (*User, error) {
return db.QueryUser(id)
}Yes — they work together seamlessly.
// Idiomatic Go function
func fetchFromDB(id string) (*User, error) {
return db.Query(id)
}
// Wrap in Result for fp-go pipeline
func getUser(id string) result.Result[*User] {
user, err := fetchFromDB(id)
return result.FromGoError(user, err)
}
// Use in pipeline
result := function.Pipe2(
getUser("123"),
result.Map(enrichUser),
result.Chain(validateUser),
)Use IO types.
// Pure function returning IO
func readFile(path string) io.IO[[]byte] {
return func() []byte {
data, _ := os.ReadFile(path)
return data
}
}
// Compose IO operations
program := function.Pipe2(
readFile("config.json"),
io.Map(parseConfig),
io.Map(validateConfig),
)
// Execute when ready
config := program() // Side effect happens hereUse IOResult when the effect can fail:
func readFile(path string) ioresult.IOResult[[]byte] {
return func() result.Result[[]byte] {
data, err := os.ReadFile(path)
return result.FromGoError(data, err)
}
}See Effects and IO.
Learning & adoption.
| Phase | You learn | Difficulty |
|---|---|---|
Week 1 | Result/Either, Pipe, MapLow–Medium. | |
Week 2–4 | Chain (FlatMap), IO, ReaderMedium. | |
Month 2+ | All monadic types, monad laws, opticsMedium–High. |
- Use for new features only.
- Show concrete benefits (fewer bugs, easier testing).
- Provide training and examples.
- Let the team see the value.
Address concerns: show benchmarks for performance; provide training for the learning curve; start with simple patterns for complexity; do gradual migration for adoption.
Success metrics: reduced bug count, faster development, easier code reviews, better test coverage.
Official: Quickstart, Core Concepts, Recipes, API docs.
Learning path: Read Why fp-go? → Complete Quickstart → Study Pure Functions → Learn Monads → Practice with Recipes.
Comparison
Simple collection operations.
Low learning curve.
Excellent performance.
No monadic types, no error handling.
Full FP toolkit.
Built-in error handling.
Monadic composition.
Steeper learning curve; idiomatic packages for speed.
// Use lo for simple operations filtered := lo.Filter(items, predicate) // Use fp-go for error handling result := result.TraverseArray(processWithErrors)(filtered)
See the comparison guide.
Yes — fp-go is heavily inspired by fp-ts. Same monadic types, similar API design, same composition patterns, same concepts. Main differences: Go's type system limits (no HKT), different syntax, performance characteristics, ecosystem.
Troubleshooting
Common causes:
1. Type parameters in wrong order.
// Wrong - B cannot be inferred
result.Map(func(x int) string { return fmt.Sprintf("%d", x) })
// Right - specify B explicitly
result.Map[string](func(x int) string { return fmt.Sprintf("%d", x) })2. Missing type parameters.
// Wrong
return result.Err(errors.New("error"))
// Right
return result.Err[int](errors.New("error"))3. Ambiguous types.
// Wrong - compiler can't infer result := result.Ok(nil) // Right - specify type result := result.Ok[*User](nil)
Use intermediate logging:
result := function.Pipe3(
step1(),
result.Map(func(x T) T {
fmt.Printf("After step1: %+v\n", x)
return x
}),
result.Chain(step2),
result.Map(func(x T) T {
fmt.Printf("After step2: %+v\n", x)
return x
}),
)Use Fold to inspect:
result.Fold(
func(err error) {
fmt.Printf("Error: %v\n", err)
},
func(val T) {
fmt.Printf("Success: %+v\n", val)
},
)Use the logging package:
import "github.com/IBM/fp-go/v2/logging"
result := logging.WithLogging(
"operation",
func() result.Result[T] {
return operation()
},
)Migration
5 breaking changes:
- Generic type aliases —
type X = Yinstead oftype X Y - Type parameter reordering — non-inferrable params first
- Pair operates on second — v1 was first, v2 is second
- Compose is right-to-left — mathematical composition
- No
generic/subpackages — removed
Steps: update imports → fix type-parameter order → update Pair usage → update Compose usage → remove generic/ imports.
See the complete migration guide.
Yes — they can coexist.
import ( v1 "github.com/IBM/fp-go/either" v2 "github.com/IBM/fp-go/v2/result" ) // Use both in same codebase v1Result := v1.Right[error](42) v2Result := v2.Ok(42)
Contributing
- Report bugs
- Suggest features
- Improve documentation
- Submit pull requests
- Help in discussions
- Star the repository
Read the contributing guide; check the good first issues; join GitHub Discussions.
- GitHub Discussions — ask questions
- GitHub Issues — report bugs
- Documentation — comprehensive guides
- API reference
- Recipes — practical examples