Bibliothèque lo pour Go : utilitaires génériques (Lodash pour Go)
Dans le monde du développement en Go, on ressent souvent le besoin de fonctions pratiques pour manipuler les collections, comme celles disponibles dans d'autres langages (par exemple, map, filter, reduce en JavaScript ou Python). La bibliothèque standard de Go offre un ensemble d'outils minimal, et écrire des boucles for répétitives pour chaque opération devient fastidieux. C'est là que la bibliothèque lo intervient — un ensemble puissant d'utilitaires génériques, inspiré de la célèbre bibliothèque JavaScript Lodash.
La bibliothèque lo utilise les génériques (types paramétrés), introduits dans Go 1.18+, pour fournir des fonctions type-safe pour travailler avec des slices, des maps, des channels et d'autres structures de données. Au lieu d'écrire for i := range slice { ... } pour chaque transformation, vous pouvez utiliser une seule ligne de code : lo.Map(slice, transformFunc). Cela rend le code plus lisible, concis et moins sujet aux erreurs.
Pourquoi utiliser lo ? Premièrement, pour gagner du temps : vous arrêtez d'écrire du code boilerplate. Deuxièmement, pour améliorer la fiabilité : les fonctions de la bibliothèque sont minutieusement testées et couvrent de nombreux cas particuliers. Troisièmement, pour l'uniformité : toute l'équipe utilise les mêmes fonctions avec des noms clairs, ce qui simplifie les revues de code. lo est un incontournable pour tout projet Go qui manipule activement des collections de données.
Installation
L'installation de la bibliothèque lo se fait de manière standard pour Go via go get. Assurez-vous d'avoir installé Go version 1.18 ou supérieure (car la bibliothèque utilise les génériques).
go get github.com/samber/lo@latest
Après avoir exécuté cette commande, la bibliothèque sera ajoutée à votre fichier go.mod. Vous pouvez maintenant l'importer dans votre code :
import "github.com/samber/lo"
Démarrage rapide
Écrivons un programme simple qui utilise lo pour filtrer et transformer un slice de nombres.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
// Slice de nombres initial
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Filtrer uniquement les nombres pairs
evenNumbers := lo.Filter(numbers, func(x int, _ int) bool {
return x%2 == 0
})
fmt.Println("Nombres pairs :", evenNumbers) // [2 4 6 8 10]
// Doubler chaque nombre
doubled := lo.Map(evenNumbers, func(x int, _ int) int {
return x * 2
})
fmt.Println("Nombres pairs doublés :", doubled) // [4 8 12 16 20]
// Additionner tous les nombres
sum := lo.Sum(doubled)
fmt.Println("Somme :", sum) // 60
}
Comme vous pouvez le voir, le code est devenu beaucoup plus déclaratif et lisible. Nous avons évité d'écrire trois boucles for distinctes.
Méthodes et fonctions principales
Voici une liste des fonctions clés de la bibliothèque lo avec des exemples d'utilisation. La plupart des fonctions prennent un slice et une fonction de rappel (callback).
1. lo.Filter
Signature : func Filter[T any](collection []T, predicate func(item T, index int) bool) []T
Fonction : Crée un nouveau slice contenant uniquement les éléments du slice original pour lesquels la fonction de prédicat a retourné true.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
names := []string{"Alice", "Bob", "Charlie", "David", "Eve"}
// Garder uniquement les noms de plus de 3 caractères
longNames := lo.Filter(names, func(name string, _ int) bool {
return len(name) > 3
})
fmt.Println(longNames) // [Alice Charlie David]
}
2. lo.Map
Signature : func Map[T any, R any](collection []T, iteratee func(item T, index int) R) []R
Fonction : Transforme chaque élément d'un slice en un nouvel élément d'un autre type, en appliquant une fonction de transformation.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Convertir chaque nombre en chaîne de caractères
strings := lo.Map(numbers, func(x int, _ int) string {
return fmt.Sprintf("Nombre : %d", x)
})
fmt.Println(strings) // [Nombre : 1 Nombre : 2 Nombre : 3 Nombre : 4 Nombre : 5]
}
3. lo.Reduce
Signature : func Reduce[T any, R any](collection []T, accumulator func(agg R, item T, index int) R, initial R) R
Fonction : Réduit un slice à une seule valeur en appliquant une fonction d'accumulation de gauche à droite.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Calculer le produit de tous les nombres
product := lo.Reduce(numbers, func(agg int, item int, _ int) int {
return agg * item
}, 1)
fmt.Println("Produit :", product) // 120
}
4. lo.Find
Signature : func Find[T any](collection []T, predicate func(item T) bool) (T, bool)
Fonction : Recherche le premier élément du slice qui satisfait le prédicat. Retourne l'élément et un booléen indiquant si un élément a été trouvé.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{10, 20, 30, 40, 50}
// Trouver le premier nombre supérieur à 25
found, ok := lo.Find(numbers, func(x int) bool {
return x > 25
})
if ok {
fmt.Println("Trouvé :", found) // 30
} else {
fmt.Println("Aucun élément trouvé")
}
}
5. lo.Uniq
Signature : func Uniq[T comparable](collection []T) []T
Fonction : Supprime les doublons d'un slice et retourne un nouveau slice avec des éléments uniques, en conservant l'ordre d'apparition.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
uniqueNumbers := lo.Uniq(numbers)
fmt.Println("Nombres uniques :", uniqueNumbers) // [1 2 3 4 5]
}
6. lo.Chunk
Signature : func Chunk[T any](collection []T, size int) [][]T
Fonction : Divise un slice en plusieurs sous-slices de taille spécifiée (sauf le dernier qui peut être plus petit).
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7}
chunks := lo.Chunk(numbers, 3)
fmt.Println("Morceaux :", chunks) // [[1 2 3] [4 5 6] [7]]
}
7. lo.Flatten
Signature : func Flatten[T any](collection [][]T) []T
Fonction : Aplatit un slice de slices en un seul slice.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
nested := [][]int{{1, 2}, {3, 4, 5}, {6}}
flat := lo.Flatten(nested)
fmt.Println("Aplati :", flat) // [1 2 3 4 5 6]
}
8. lo.Sum
Signature : func Sum[T constraints.Integer | constraints.Float](collection []T) T
Fonction : Calcule la somme de tous les éléments d'un slice de nombres.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []float64{1.5, 2.5, 3.0}
total := lo.Sum(numbers)
fmt.Println("Somme :", total) // 7.0
}
9. lo.Contains
Signature : func Contains[T comparable](collection []T, element T) bool
Fonction : Vérifie si un élément est présent dans le slice.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
fruits := []string{"pomme", "banane", "cerise"}
hasBanana := lo.Contains(fruits, "banane")
fmt.Println("Contient banane ?", hasBanana) // true
hasMango := lo.Contains(fruits, "mangue")
fmt.Println("Contient mangue ?", hasMango) // false
}
10. lo.Keys et lo.Values
Signatures :
func Keys[K comparable, V any](m map[K]V) []Kfunc Values[K comparable, V any](m map[K]V) []V
Fonctions : Extraient respectivement les clés et les valeurs d'une map sous forme de slices.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
ages := map[string]int{"Alice": 30, "Bob": 25, "Charlie": 35}
keys := lo.Keys(ages)
fmt.Println("Clés :", keys) // [Alice Bob Charlie] (ordre non garanti)
values := lo.Values(ages)
fmt.Println("Valeurs :", values) // [30 25 35] (ordre non garanti)
}
Fonctions avancées
En plus des fonctions de base, lo propose des utilitaires plus avancés pour des cas d'utilisation spécifiques.
11. lo.GroupBy
Signature : func GroupBy[T any, K comparable](collection []T, iteratee func(item T) K) map[K][]T
Fonction : Groupe les éléments d'un slice selon une clé calculée par une fonction.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
words := []string{"pomme", "banane", "poire", "abricot", "cerise"}
// Grouper par première lettre
grouped := lo.GroupBy(words, func(word string) string {
return string(word[0])
})
fmt.Println("Groupé :", grouped)
// Sortie possible : map[a:[abricot] b:[banane] c:[cerise] p:[pomme poire]]
}
12. lo.SortBy
Signature : func SortBy[T any, K constraints.Ordered](collection []T, iteratee func(item T) K) []T
Fonction : Trie un slice selon une clé calculée par une fonction.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
people := []struct {
Name string
Age int
}{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// Trier par âge
sorted := lo.SortBy(people, func(p struct{ Name string; Age int }) int {
return p.Age
})
fmt.Println("Trié par âge :", sorted)
// [{Bob 25} {Alice 30} {Charlie 35}]
}
13. lo.PartitionBy
Signature : func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [][]T
Fonction : Divise un slice en sous-slices basés sur une clé calculée. Les éléments consécutifs avec la même clé sont regroupés.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
// Partitionner par valeur (les éléments égaux consécutifs sont regroupés)
partitions := lo.PartitionBy(numbers, func(x int) int {
return x
})
fmt.Println("Partitions :", partitions) // [[1] [2 2] [3 3 3] [4] [5 5]]
}
Fonctions pour les channels
lo propose également des fonctions pour travailler avec les channels Go, facilitant les opérations concurrentes.
14. lo.ChannelDispatcher
Signature : func ChannelDispatcher[T any](stream <-chan T, nbWorkers int, bufferSize int) []<-chan T
Fonction : Distribue les messages d'un channel d'entrée vers plusieurs channels de sortie (workers), en mode round-robin.
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
input := make(chan int, 10)
for i := 1; i <= 6; i++ {
input <- i
}
close(input)
// Distribuer sur 3 workers
workers := lo.ChannelDispatcher(input, 3, 10)
for i, worker := range workers {
go func(id int, ch <-chan int) {
for val := range ch {
fmt.Printf("Worker %d a reçu : %d\n", id, val)
}
}(i, worker)
}
// Attendre un peu pour voir les résultats
// (dans un vrai programme, utilisez sync.WaitGroup)
}
Conclusion
La bibliothèque lo est un outil extrêmement puissant pour tout développeur Go. Elle apporte une touche de programmation fonctionnelle et rend le code plus propre, plus lisible et plus maintenable. Que vous travailliez avec des slices, des maps, des channels ou d'autres structures de données, lo vous offre des fonctions prêtes à l'emploi qui vous feront gagner un temps précieux.
N'hésitez pas à explorer la documentation officielle de lo sur GitHub pour découvrir toutes les fonctions disponibles. Avec plus de 70 utilitaires, vous trouverez certainement une solution élégante pour chaque problème de manipulation de collections en Go.
Alors, prêt à dire adieu aux boucles for répétitives ? Essayez lo dès aujourd'hui et transformez votre façon d'écrire du Go !