Go Programming

Go Generics en action : Construire des transformateurs JSON sûrs et des utilitaires de tranches personnalisés

Lorsque Go 1.18 a introduit les génériques, cela a marqué un moment charnière dans l'évolution du langage. Pendant des années, les développeurs Go comptaient sur la réflexion, les interfaces et la génération de code pour gérer les opérations génériques, ce qui donnait souvent du code verbeux, sujet aux erreurs ou aux performances compromises. Avec l'introduction des paramètres de type, nous disposons désormais d'un mécanisme de premier ordre pour écrire des algorithmes qui sont à la fois sûrs pour les types et réutilisables sur différents types de données, sans sacrifier les performances.

Dans cet article, nous explorerons comment exploiter les génériques pour résoudre deux problèmes courants du développement Go : la transformation de structures de données JSON hétérogènes et la création d'utilitaires de tranches réutilisables et sûrs pour les types. Ces exemples démontrent non seulement la syntaxe des génériques, mais aussi les avantages architecturaux de leur utilisation dans du code prêt pour la production.

Le problème des transformateurs basés sur la réflexion

Avant les génériques, la gestion de la transformation JSON impliquait généralement l'une des deux approches suivantes : écrire manuellement la logique de sérialisation pour chaque structure, ou utiliser la réflexion pour inspecter les structures au moment de l'exécution. Cette dernière, bien que flexible, est lente et opaque. Si un nom de champ est mal orthographié ou si une assertion de type échoue, vous ne découvrez le bug qu'au moment de l'exécution, souvent profondément dans la pile de votre application.

Les génériques nous permettent de définir des fonctions qui opèrent sur n'importe quel type satisfaisant des contraintes spécifiques. En combinant les génériques avec le package encoding/json de Go, nous pouvons créer des transformateurs qui sont vérifiés à la compilation. Cela garantit que si la structure de vos données change, le compilateur vous alertera immédiatement, bien avant que votre code n'atteigne la production.

Construction d'un transformateur JSON sûr pour les types

Créons une fonction générique qui transforme une tranche de n'importe quel type en une tranche d'un autre type, à condition que les deux types soient marshallables en JSON. Cela est particulièrement utile dans les architectures de microservices où vous pourriez avoir besoin de convertir des modèles de domaine internes en réponses d'API externes.

package transformer

import "encoding/json"

// TransformSlice convertit une tranche de type S en une tranche de type T.
// Elle suppose que S et T peuvent être marshallés et unmarshallés en JSON.
func TransformSlice[S any, T any](source []S) ([]T, error) {
	// Marshall la tranche source en octets JSON
	jsonBytes, err := json.Marshal(source)
	if err != nil {
		return nil, err
	}

	// Unmarshall les octets JSON dans le type cible T
	var target []T
	err = json.Unmarshal(jsonBytes, &target)
	if err != nil {
		return nil, err
	}

	return target, nil
}

// Exemple d'utilisation :
// type UserInternal struct { ID int64; Email string; CreatedAt time.Time }
// type UserExternal struct { UserID string; EmailAddress string; Timestamp string }
//
// Remarque : Ce transformateur simple repose sur la correspondance des noms de champs ou sur
// des tags json personnalisés. Pour des mappages complexes, un mappage manuel utilisant des génériques
// pourrait être plus approprié pour éviter la surcharge de sérialisation.

Bien que l'exemple ci-dessus soit concis, il entraîne le coût du marshalling et du unmarshalling JSON. Pour les scénarios à haute performance, nous pouvons écrire un mappage générique qui copie les champs directement. Cependant, l'approche basée sur JSON est inestimable pour l'évolution du schéma et l'interopérabilité entre des services aux schémas faiblement couplés.

Création d'utilitaires de tranches réutilisables

La bibliothèque standard de Go fournit le package slices à partir de Go 1.21+, mais comprendre comment construire ces utilitaires aide à créer des abstractions spécifiques au domaine. Implémentons une fonction générique Filter et une fonction Map qui peuvent être appliquées à n'importe quelle tranche.

package utils

// Filter retourne une nouvelle tranche contenant les éléments de 's' pour lesquels 'predicate' retourne true.
func Filter[S any, T any](s []S, predicate func(S) bool) []T {
	var result []T
	for _, item := range s {
		if predicate(item) {
			// Convertir ou transformer item si T diffère de S
			// Ici, nous supposons que T peut être dérivé de S ou est du même type
			if val, ok := any(item).(T); ok {
				result = append(result, val)
			} else {
				// Solution de repli pour les cas simples où S == T
				result = append(result, any(item).(T))
			}
		}
	}
	return result
}

// Map applique une fonction à chaque élément de 's' et retourne une nouvelle tranche de type 'T'.
func Map[S any, T any](s []S, f func(S) T) []T {
	result := make([]T, len(s))
	for i, item := range s {
		result[i] = f(item)
	}
	return result
}

Ces fonctions offrent un style de programmation fonctionnelle de plus en plus populaire dans Go. Elles permettent aux développeurs d'exprimer clairement leur intention. Par exemple, filtrer une liste d'utilisateurs par âge ou mapper une liste d'enregistrements de base de données vers des DTO devient une ligne de code, réduisant le code répétitif et améliorant la lisibilité.

Meilleures pratiques et considérations de performance

Bien que les génériques offrent de la flexibilité, ils introduisent une certaine surcharge due à la résolution des paramètres de type. Dans les boucles serrées, cela peut parfois être plus coûteux que l'utilisation d'interfaces, bien que les environnements d'exécution Go modernes aient considérablement optimisé cela. Profillez toujours votre code. Pour les transformations JSON, si la performance est critique, envisagez d'utiliser jsoniter ou la copie manuelle des champs avec des génériques au lieu du marshalling JSON complet.

De plus, évitez de surutiliser les génériques pour les types simples. Si vous avez simplement besoin de trier des entiers, utilisez la bibliothèque standard. Utilisez les génériques lorsque vous définissez une logique complexe qui doit s'appliquer à plusieurs types non liés (comme un motif de référentiel personnalisé ou un processeur de données complexe).

Conclusion

Les génériques en Go ne sont pas seulement du sucre syntaxique ; ils sont un outil puissant pour améliorer la sécurité et la réutilisabilité du code. En construisant des transformateurs JSON sûrs pour les types et des utilitaires de tranches personnalisés, vous pouvez réduire le code répétitif, détecter les erreurs à la compilation et écrire un code plus propre et plus maintenable. À mesure que l'écosystème Go continue de mûrir, l'adoption de ces modèles sera essentielle pour les développeurs seniors visant à écrire des applications robustes et performantes.

Commencez petit. Remplacez quelques boucles répétitives par des fonctions génériques Map et Filter, et observez l'amélioration de la clarté et de la fiabilité de votre base de code.

Share: