4.4 KiB
Advanced Patterns
Composing Transformations
Chain lo functions to build multi-step pipelines. Each function returns a new collection that feeds into the next.
// Pipeline: extract active user emails grouped by role
emailsByRole := lo.GroupBy(
lo.Map(
lo.Filter(users, func(u User, _ int) bool {
return u.Active && u.EmailVerified
}),
func(u User, _ int) UserEmail {
return UserEmail{Role: u.Role, Email: u.Email}
},
),
func(ue UserEmail, _ int) string {
return ue.Role
},
)
For long chains, break into named intermediate variables for readability:
active := lo.Filter(users, func(u User, _ int) bool {
return u.Active
})
names := lo.Map(active, func(u User, _ int) string {
return u.Name
})
unique := lo.Uniq(names)
lo + stdlib Interop
Prefer stdlib when it covers the operation — lo adds value for functional transforms the stdlib doesn't provide.
| Operation | stdlib (prefer) | lo (use when stdlib lacks) |
|---|---|---|
| Contains | slices.Contains(s, v) |
lo.ContainsBy(s, fn) — predicate-based |
| Sort | slices.SortFunc(s, cmp) |
— (lo doesn't provide sort) |
| Keys | maps.Keys(m) |
lo.UniqKeys(m) — deduplicated keys |
| Clone | slices.Clone(s) |
lo.Map(s, fn) — when you need transform during clone |
| Min/Max | slices.Min(s) |
lo.MinBy(s, fn) — by extractor function |
Rule of thumb: If slices.* or maps.* does what you need, use it. Reach for lo when you need predicates, transforms, grouping, or error variants.
lo + samber/mo Integration
samber/mo provides monadic types (Option, Result, Either). They compose naturally with lo:
// Filter users with valid optional emails
validEmails := lo.FilterMap(users, func(u User, _ int) (string, bool) {
email, ok := u.Email.Get() // mo.Option[string]
return email, ok
})
// Map with Result — collect successes
results := lo.FilterMap(urls, func(url string, _ int) (Response, bool) {
res := fetchURL(url) // returns mo.Result[Response]
val, err := res.Get()
return val, err == nil
})
Iterator Patterns (loi)
Requires Go 1.23+. Lazy iterators avoid intermediate allocations.
Eager vs lazy comparison
// Eager — allocates 2 intermediate slices
result := lo.Map(lo.Filter(bigSlice, filterFn), mapFn)
// Lazy — zero intermediate allocations
for v := range loi.Map(loi.Filter(bigSlice, filterFn), mapFn) {
process(v)
}
Building lazy pipelines
// Lazy pipeline: filter → map → take first 10
pipeline := loi.Take(
loi.Map(
loi.Filter(records, func(r Record) bool {
return r.Score > 0.8
}),
func(r Record) string {
return r.Name
},
),
10,
)
// Consume with range
for name := range pipeline {
fmt.Println(name)
}
Performance-Sensitive Patterns
When to switch from lo to lom
Trigger: go tool pprof -alloc_objects shows lo.Filter or lo.Map as top allocators in a hot path.
// Before — allocates new slice every call
filtered := lo.Filter(events, isValid)
// After — zero allocations, modifies in-place
events = lom.Filter(events, isValid)
// Warning: 'events' is now modified. Original data is lost.
Parallel transforms
Trigger: go tool pprof -cpu shows transform function dominating CPU on large datasets.
// Switch from sequential to parallel
results := lop.Map(largeSlice, expensiveTransform)
Testing with lo
lo helpers simplify test data generation and assertions:
// Generate test fixtures
users := lo.Times(100, func(i int) User {
return User{ID: i, Name: fmt.Sprintf("user-%d", i)}
})
// Assert subset relationships
assert.True(t, lo.Every(expected, lo.Map(actual, extractID)))
// Generate random test data
ids := lo.Times(50, func(_ int) string {
return lo.RandomString(16, lo.AlphanumericCharset)
})
// Quick frequency check
counts := lo.CountValues(results)
assert.Equal(t, 3, counts["success"])
Slice-to-Map Conversion
Common pattern: convert a slice into a lookup map.
// By ID
userByID := lo.SliceToMap(users, func(u User) (int, User) {
return u.ID, u
})
// By key function
userByEmail := lo.KeyBy(users, func(u User) string {
return u.Email
})
// Filter + convert in one pass
activeByID := lo.FilterSliceToMap(users, func(u User) (int, User, bool) {
return u.ID, u, u.Active
})