| name | collection-idioms |
| description | Load when writing or reviewing Go that builds or transforms slices/maps. Prefer samber/lo, samber/lo/it, and stdlib slices/maps callback & iterator utilities over hand-rolled make + range + append; keep an explicit loop only where side effects or control flow are genuinely complex. |
Collection idioms
Default to declarative transforms. A out := make([]T, 0, len(xs)); for _, x := range xs { out = append(out, f(x)) }; return out should almost always be one expression instead.
Toolbox
- slice โ slice, pure 1:1 โ
lo.Map(xs, func(x T, _ int) U { return ... })
- slice โ slice, filtered โ
lo.Filter(xs, func(x T, _ int) bool { ... }); filter + transform in one pass โ lo.FilterMap(xs, func(x T, _ int) (U, bool) { ...; return u, keep })
- iterator (
iter.Seq) โ slice โ slices.Collect(it.Map(seq, func(x T) U { ... })), where it is github.com/samber/lo/it (range-over-func iterators)
- sorted map keys โ
maputil.SortedKeys(m) returns iter.Seq[K] (ascending); maputil.SortedNames(items, getName) returns the unique names as iter.Seq[string]. Under the hood these are slices.Sorted(maps.Keys(m)) โ use the stdlib form directly when you don't want the helper.
- stdlib building blocks โ
slices.Collect, slices.Sorted, slices.SortFunc, slices.Values, maps.Keys, maps.Values
- lookup โ
lo.Find, lo.FindIndexOf, lo.MaxBy
Use it.Map/it.Filter only when the source is genuinely an iter.Seq (e.g. maputil.SortedKeys(m)). If the source is a plain slice, use lo.Map โ don't wrap a slice in an iterator just to transform it.
Point-free transforms and the _ int boilerplate
lo.Map's iteratee is func(item T, index int) R, so handing it an existing func(T) R needs a throwaway wrapper โ lo.Map(xs, func(x T, _ int) R { return f(x) }). it.Map's transform is func(item T) R, so it accepts such a function directly.
So when the result is consumed by range, prefer starting from an iterator and using it.Map: you pass the function point-free and drop the _ int.
for _, tag := range lo.Map(keys, func(k string, _ int) domain.Tag { return toTag(k) }) {
...
}
for tag := range it.Map(slices.Values(keys), toTag) {
...
}
When the source is already an iter.Seq (e.g. maputil.SortedKeys(m)) it's cleaner still โ no slices.Values wrap.
When it's not worth it: if you need a materialized []U rather than a range, the point-free form becomes slices.Collect(it.Map(slices.Values(xs), f)) โ the extra slices.Values/slices.Collect wrapping generally isn't worth it over a plain lo.Map(xs, func(x T, _ int) U { ... }). Reserve the iterator + it.Map point-free style for range-terminal consumption; for slice-producing transforms, lo.Map (with its _ int) is the simpler choice.
Keep an explicit loop when the bodyโฆ
This is the "complex side effects" carve-out โ readability wins, don't force a helper:
- side-effects per element โ writes to an
io.Writer, logs, mutates external state
- control flow โ early
return/break/continue-as-search, or it's a find/any/all
- cross-iteration state โ a dedup
seen set, a running accumulator, pagination (NextToken) that appends across pages
- builds a map with computed keys, or mutates a map in place (
delete) โ the lo/it map helpers target slices, not map construction
- type mismatch โ e.g. a struct key that isn't
cmp.Ordered, which needs slices.SortFunc, not maputil.SortedKeys
- forcing the functional form would be less readable than the loop
A single per-element if that only sets fields on the value being produced is fine inside a lo.Map/lo.FilterMap closure โ that is not "complex".
Out of scope
lo.FromPtr / lo.ToPtr are pointer helpers, unrelated to this convention. Leave them as-is; pointer-shape cleanups are a separate concern.
Examples
Slice โ slice:
entries := make([]genericlist.Entry, len(result.Entries))
for i, e := range result.Entries {
entries[i] = genericlist.Entry{Name: e.Name, Value: e.Value, Error: e.Error}
}
entries := lo.Map(result.Entries, func(e ListEntry, _ int) genericlist.Entry {
return genericlist.Entry{Name: e.Name, Value: e.Value, Error: e.Error}
})
Iterator (sorted keys) โ slice:
out := make([]domain.Tag, 0, len(tags))
for k := range maputil.SortedKeys(tags) {
out = append(out, domain.Tag{Key: k, Value: lo.FromPtr(tags[k])})
}
return out
return slices.Collect(it.Map(maputil.SortedKeys(tags), func(k string) domain.Tag {
return domain.Tag{Key: k, Value: lo.FromPtr(tags[k])}
}))
Filter + transform:
entries := make([]Entry, 0, len(rows))
for _, row := range rows {
if !match(row) {
continue
}
entries = append(entries, toEntry(row))
}
entries := lo.FilterMap(rows, func(row Row, _ int) (Entry, bool) {
if !match(row) {
return Entry{}, false
}
return toEntry(row), true
})
Note on empty results
lo.Map/lo.Filter return a non-nil empty slice ([]) for empty input, whereas an un-appended make(..., 0, 0) or a nil-declared slice marshals to null. This is almost always invisible, but if a JSON contract distinguishes [] from null (as some serializers do), verify the round-trip โ see internal/maputil.Set.MarshalJSON for a case that guards it explicitly.