Skip to main content
Version: v2.2.82 (latest)
Getting started · Section 04 / 04

Why fp-go?

When and why to use functional programming in Go — and how fp-go helps you write better, more maintainable code.

// Core wins
5benefits
Type safety, composability, testability, maintainability, auto error propagation.
// Fewer bugs
Up to 60% reduction in case studies.
// Use it for
Complex error handling and data pipelines.
01

The problem with traditional error handling.

Go's error handling is explicit and clear — but it becomes verbose and repetitive in complex scenarios.

processUser.go
func processUser(id string) (*User, error) {
  // Fetch user
  user, err := fetchUser(id)
  if err != nil {
      return nil, fmt.Errorf("fetch user: %w", err)
  }

  // Validate user
  if err := validateUser(user); err != nil {
      return nil, fmt.Errorf("validate user: %w", err)
  }

  // Enrich user data
  enriched, err := enrichUser(user)
  if err != nil {
      return nil, fmt.Errorf("enrich user: %w", err)
  }

  // Transform user
  transformed, err := transformUser(enriched)
  if err != nil {
      return nil, fmt.Errorf("transform user: %w", err)
  }

  return transformed, nil
}
Beforeissues

Repetitive if err != nil checks.

Error handling tangled with business logic.

Hard to see the actual data flow.

Difficult to compose operations.

Easy to forget error checks.

fp-gosee right tab

No repetitive checks.

Clear data flow pipeline.

Business logic stays prominent.

Composable, impossible to forget error handling.

02

Core benefits.

Five concrete wins from adopting fp-go.

1. Type safety

fp-go leverages Go's type system to make errors impossible to ignore.

unsafe.go
// Idiomatic Go - easy to forget error check
result, _ := riskyOperation()  // Ignoring error!
doSomething(result)            // Potential panic

2. Composability

Build complex operations from simple, reusable pieces.

imperative.go
func processData(data []int) ([]string, error) {
  // Filter
  var filtered []int
  for _, v := range data {
      if v > 0 {
          filtered = append(filtered, v)
      }
  }

  // Transform
  var doubled []int
  for _, v := range filtered {
      doubled = append(doubled, v*2)
  }

  // Validate
  for _, v := range doubled {
      if v > 100 {
          return nil, errors.New("value too large")
      }
  }

  // Convert to strings
  var result []string
  for _, v := range doubled {
      result = append(result, fmt.Sprintf("%d", v))
  }

  return result, nil
}

3. Testability

Pure functions are trivial to test — no mocks, no setup, no teardown.

impure.go
// Impure function - depends on external state
var db *sql.DB

func getUser(id string) (*User, error) {
  // Uses global db connection
  row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
  // ... parsing logic
}

// Test requires:
// - Database setup
// - Test data insertion
// - Connection management
// - Cleanup

4. Maintainability

Before fp-go50+ lines

Nested if statements.

Mixed concerns.

Hard to follow the logic.

Difficult to add new steps.

With fp-goclear pipeline
snippet.go
return function.Pipe5(
  step1,
  step2,
  step3,
  step4,
  step5,
)

5. Automatic error propagation

manual.go
func process() error {
  result1, err := step1()
  if err != nil {
      return err
  }

  result2, err := step2(result1)
  if err != nil {
      return err
  }

  result3, err := step3(result2)
  if err != nil {
      return err
  }

  return step4(result3)
}
03

When to use fp-go.

The honest fit guide — where it shines, and where idiomatic Go is the better answer.

Excellent fit

Complex business logic

order.go
// Multiple validation steps
// Data transformations
// Error handling at each step
func validateAndProcessOrder(order Order) result.Result[ProcessedOrder] {
  return function.Pipe5(
      validateCustomer(order.CustomerID),
      result.Chain(func(customer Customer) result.Result[Order] {
          return validateInventory(order)
      }),
      result.Chain(calculatePricing),
      result.Chain(applyDiscounts),
      result.Chain(finalizeOrder),
  )
}

Data transformation pipelines

etl.go
// ETL operations
// Data cleaning
// Format conversions
func transformData(raw []RawData) result.Result[[]CleanData] {
  return function.Pipe4(
      array.Filter(isValid),
      array.Map(normalize),
      array.TraverseResult(enrich),
      result.Map(array.Map(format)),
  )(raw)
}

API clients with error handling

client.go
// HTTP requests
// Response parsing
// Error handling
func fetchUserProfile(id string) ioresult.IOResult[Profile] {
  return function.Pipe3(
      buildRequest(id),
      ioresult.Chain(executeRequest),
      ioresult.Chain(parseResponse),
  )
}

Configuration management

config.go
// Load config
// Validate
// Apply defaults
func loadConfig(path string) result.Result[Config] {
  return function.Pipe3(
      readConfigFile(path),
      result.Chain(parseConfig),
      result.Chain(validateConfig),
      result.Map(applyDefaults),
  )
}

Use with caution

Simple CRUD operations.

For straightforward DB calls, idiomatic Go is fine. fp-go adds unnecessary complexity:

crud.go
func getUser(id string) (*User, error) {
  return db.QueryUser(id)
}
Performance-critical hot paths.

For tight loops, use idiomatic Go or fp-go's idiomatic packages. Direct operations are faster; the idiomatic packages offer near-native performance.

Team unfamiliar with FP.

Start with simple examples, provide training, introduce gradually, and use fp-go for new code first.

Trivial scripts and low-level system code.

For one-off scripts and code dealing with syscalls or memory management, stick with idiomatic Go.

04

Real-world success stories.

Case studyBeforeAfter
API Gateway500+ lines of nested error handling
150 lines of clear pipeline code. Each step independently testable. 60% reduction in bugs.
Data PipelineComplex state management, scattered error handling
Clear linear data flow. Centralized error handling. 40% faster development time.
MicroserviceInconsistent error handling, hard to compose
Consistent patterns. Easy composition. 50% reduction in production errors.
05

Compare with other approaches.

vs. idiomatic Go

AspectIdiomatic Gofp-go
Error handlingManual if err != nil
Automatic propagation.
ComposabilityLimited
Excellent.
Type safetyGood
Excellent.
Learning curveLow
Medium.
VerbosityHigh for complex logic
Low.
PerformanceExcellent
Good (excellent with idiomatic packages).
Best forSimple operations
Complex logic.

vs. other FP libraries

Featurefp-gosamber/lo · go-functional
Monadic typesFull support
samber/lo: limited · go-functional: yes.
Type safetyExcellent
samber/lo: uses any · go-functional: good.
Error handlingBuilt-in
samber/lo: manual · go-functional: built-in.
DocumentationComprehensive
samber/lo: good · go-functional: limited.
Active developmentYes
samber/lo: yes · go-functional: sporadic.
Production-readyYes (IBM)
samber/lo: yes · go-functional: unknown.
06

Getting started.

Adoption path0 / 4 complete
  • Start small — use Result for one functionstep 1
  • Learn core concepts (pure functions, monads, composition)step 2
  • Explore examples in the Recipes sectionstep 3
  • Adopt gradually — new features first, refactor complex logic over timestep 4

1. Start small

first.gotested
// Begin with simple Result usage
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)
}

2. Learn core concepts

3. Explore examples

4. Gradual adoption

Use for new features first; refactor complex logic gradually; keep simple code idiomatic.

07

Key takeaways.

Remember7 / 7 complete
  • fp-go excels at complex logic — error handling, transformations, business rules
  • Type safety prevents bugs — impossible to ignore errors
  • Composability improves maintainability
  • Testability is built-in — pure functions are trivial to test
  • Not a silver bullet — use idiomatic Go for simple operations
  • Production-ready — used at IBM and other companies
  • Gradual adoption works — start small, expand as you learn