Les Identifiants Uniques Universels (UUID) sont essentiels dans les applications Go pour créer des identifiants uniques dans des systèmes distribués sans coordination. Go, avec son focus sur la simplicité et la performance, fournit plusieurs excellentes bibliothèques pour la génération d'UUID, chacune offrant différentes fonctionnalités et caractéristiques de performance adaptées à divers cas d'usage.
Ce guide complet explore plusieurs approches pour générer des UUID dans les applications Go, de l'utilisation de bibliothèques populaires à l'implémentation de solutions personnalisées. Que vous construisiez des microservices, des APIs ou des systèmes distribués, comprendre comment implémenter et exploiter correctement les UUID en Go est crucial pour le développement d'applications modernes.
Le package google/uuid est l'une des bibliothèques UUID les plus populaires et bien maintenues pour Go. Il fournit une API simple et supporte toutes les versions d'UUID définies dans RFC 4122.
package main
import (
"fmt"
"log"
"github.com/google/uuid"
)
func main() {
// Générer un nouvel UUID aléatoire (Version 4)
id := uuid.New()
fmt.Printf("UUID généré : %s\n", id)
// Générer UUID comme chaîne directement
idString := uuid.NewString()
fmt.Printf("UUID comme chaîne : %s\n", idString)
// Générer UUID nil
nilUUID := uuid.Nil
fmt.Printf("UUID nil : %s\n", nilUUID)
// Parser un UUID depuis une chaîne
parsed, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440000")
if err != nil {
log.Fatal(err)
}
fmt.Printf("UUID parsé : %s\n", parsed)
}
Le package google/uuid fournit une API propre et simple. La fonction uuid.New() génère un UUID Version 4 (aléatoire), tandis que uuid.NewString() le retourne directement sous forme de chaîne. Le package inclut également des capacités de parsing et des constantes prédéfinies comme uuid.Nil pour l'UUID nil. Cette bibliothèque est recommandée pour la plupart des applications Go en raison de sa simplicité et du support de Google.
Le package satori/go.uuid offre plus de flexibilité avec le support de différentes versions d'UUID et des fonctionnalités supplémentaires comme le tri d'UUID et les générateurs personnalisés.
package main
import (
"fmt"
"log"
"github.com/satori/go.uuid"
)
func main() {
// Générer UUID Version 1 (timestamp + adresse MAC)
v1, err := uuid.NewV1()
if err != nil {
log.Fatal(err)
}
fmt.Printf("UUID Version 1 : %s\n", v1)
// Générer UUID Version 4 (aléatoire)
v4, err := uuid.NewV4()
if err != nil {
log.Fatal(err)
}
fmt.Printf("UUID Version 4 : %s\n", v4)
// Générer UUID Version 5 (namespace + nom)
v5 := uuid.NewV5(uuid.NamespaceURL, "https://example.com")
fmt.Printf("UUID Version 5 : %s\n", v5)
// Convertir en différents formats
fmt.Printf("Octets : %x\n", v4.Bytes())
fmt.Printf("Chaîne : %s\n", v4.String())
}
Le package satori/go.uuid fournit des fonctions explicites pour générer différentes versions d'UUID. Les UUID Version 1 incluent des informations de timestamp et d'adresse MAC, les UUID Version 4 sont aléatoires, et les UUID Version 5 sont déterministes basés sur un namespace et un nom. Ce package est idéal lorsque vous avez besoin de versions spécifiques d'UUID ou plus de contrôle sur le processus de génération.
Différentes versions d'UUID servent à différents objectifs. Voici comment générer diverses versions d'UUID en utilisant le package google/uuid, qui supporte les Versions 1, 3, 4, 5 et 7.
package main
import (
"fmt"
"time"
"github.com/google/uuid"
)
func main() {
// Version 1 : UUID basé sur le temps
// Note : Nécessite l'accès à l'adresse MAC
uuid.SetNodeID([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab})
v1, _ := uuid.NewUUID()
fmt.Printf("Version 1 (Basé sur le temps) : %s\n", v1)
// Version 3 : Hash MD5 du namespace et du nom
v3 := uuid.NewMD5(uuid.NamespaceDNS, []byte("example.com"))
fmt.Printf("Version 3 (MD5) : %s\n", v3)
// Version 4 : UUID aléatoire (le plus commun)
v4 := uuid.New()
fmt.Printf("Version 4 (Aléatoire) : %s\n", v4)
// Version 5 : Hash SHA-1 du namespace et du nom
v5 := uuid.NewSHA1(uuid.NamespaceURL, []byte("https://example.com"))
fmt.Printf("Version 5 (SHA-1) : %s\n", v5)
// Version 7 : Ordonné par temps avec timestamp Unix (si supporté)
// Note : La Version 7 est un ajout plus récent et peut nécessiter la dernière version du package
v7, _ := uuid.NewV7()
fmt.Printf("Version 7 (Ordonné par temps) : %s\n", v7)
// Extraire le timestamp de l'UUID Version 1
timestamp, _ := uuid.GetTime()
fmt.Printf("Timestamp de V1 : %s\n", time.Unix(timestamp.UnixTime()))
}
Chaque version d'UUID a des cas d'usage spécifiques : Version 1 inclut le timestamp et l'adresse MAC (bon pour le débogage mais peut fuiter des informations), Version 3 et 5 créent des UUID déterministes à partir de namespaces (utile pour l'identification cohérente), Version 4 est purement aléatoire (le plus commun pour un usage général), et Version 7 fournit un ordonnancement temporel avec de meilleures performances d'index de base de données. Choisissez la version en fonction de vos exigences spécifiques.
Pour des fins d'apprentissage ou des exigences spécifiques, vous pouvez implémenter un générateur UUID de base en utilisant le package crypto/rand de Go pour la génération sécurisée de nombres aléatoires.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)
// UUID représente un UUID
type UUID [16]byte
// NewUUID génère un nouvel UUID Version 4
func NewUUID() (UUID, error) {
var uuid UUID
// Lire 16 octets aléatoires
_, err := io.ReadFull(rand.Reader, uuid[:])
if err != nil {
return uuid, err
}
// Définir les bits de version (4) et de variante selon RFC 4122
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variante 10
return uuid, nil
}
// String retourne l'UUID au format standard
func (u UUID) String() string {
var buf [36]byte
hex.Encode(buf[0:8], u[0:4])
buf[8] = '-'
hex.Encode(buf[9:13], u[4:6])
buf[13] = '-'
hex.Encode(buf[14:18], u[6:8])
buf[18] = '-'
hex.Encode(buf[19:23], u[8:10])
buf[23] = '-'
hex.Encode(buf[24:], u[10:16])
return string(buf[:])
}
func main() {
uuid, err := NewUUID()
if err != nil {
panic(err)
}
fmt.Printf("UUID personnalisé : %s\n", uuid)
}
Cette implémentation personnalisée démontre la structure de base d'un générateur d'UUID Version 4. Elle utilise crypto/rand pour des octets aléatoires cryptographiquement sûrs, définit les bits de version et de variante appropriés selon RFC 4122, et formate la sortie au format UUID standard. Bien qu'éducatif, il est recommandé d'utiliser des bibliothèques bien testées pour les applications en production.
Pour les applications qui utilisent fréquemment des UUID, il est bénéfique de créer un service qui centralise la logique de génération d'UUID et fournit des utilitaires supplémentaires :
package uuidservice
import (
"database/sql/driver"
"fmt"
"sync"
"github.com/google/uuid"
)
// Service fournit des fonctions de génération et d'utilité UUID
type Service struct {
mu sync.Mutex
generated int64
namespace uuid.UUID
}
// NewService crée un nouveau service UUID
func NewService(namespace string) (*Service, error) {
ns, err := uuid.Parse(namespace)
if err != nil {
// Utiliser un namespace par défaut si le parsing échoue
ns = uuid.NamespaceDNS
}
return &Service{
namespace: ns,
}, nil
}
// GenerateV4 génère un UUID v4 aléatoire
func (s *Service) GenerateV4() uuid.UUID {
s.mu.Lock()
defer s.mu.Unlock()
s.generated++
return uuid.New()
}
// GenerateV5 génère un UUID v5 déterministe à partir d'un nom
func (s *Service) GenerateV5(name string) uuid.UUID {
return uuid.NewSHA1(s.namespace, []byte(name))
}
// GenerateBatch génère plusieurs UUID efficacement
func (s *Service) GenerateBatch(count int) []uuid.UUID {
uuids := make([]uuid.UUID, count)
s.mu.Lock()
defer s.mu.Unlock()
for i := 0; i < count; i++ {
uuids[i] = uuid.New()
s.generated++
}
return uuids
}
// GetStats retourne les statistiques de génération
func (s *Service) GetStats() int64 {
s.mu.Lock()
defer s.mu.Unlock()
return s.generated
}
// NullableUUID gère les UUID nullables pour les opérations de base de données
type NullableUUID struct {
UUID uuid.UUID
Valid bool
}
// Scan implémente l'interface sql.Scanner
func (n *NullableUUID) Scan(value interface{}) error {
if value == nil {
n.UUID, n.Valid = uuid.Nil, false
return nil
}
n.Valid = true
return n.UUID.Scan(value)
}
// Value implémente l'interface driver.Valuer
func (n NullableUUID) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.UUID.Value()
}
// Exemple d'utilisation
func Example() {
service, _ := NewService("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
// Générer un UUID unique
id := service.GenerateV4()
fmt.Printf("Généré : %s\n", id)
// Générer UUID déterministe
userId := service.GenerateV5("user@example.com")
fmt.Printf("ID utilisateur : %s\n", userId)
// Générer un lot
batch := service.GenerateBatch(10)
fmt.Printf("Générés %d UUID\n", len(batch))
// Vérifier les statistiques
fmt.Printf("Total généré : %d\n", service.GetStats())
}
Ce service UUID fournit un moyen centralisé de gérer la génération d'UUID dans votre application. Il inclut la génération thread-safe, le suivi des statistiques, la génération par lots pour la performance, et le support d'UUID nullable compatible avec les bases de données. Le pattern de service facilite le mock de la génération d'UUID dans les tests et maintient une gestion cohérente des UUID dans toute votre application.
Lors de l'acceptation d'UUID provenant de sources externes, une validation appropriée est cruciale. Voici comment parser et valider efficacement les UUID en Go :
package main
import (
"fmt"
"regexp"
"strings"
"github.com/google/uuid"
)
// Validation UUID personnalisée utilisant regex
var uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`)
// ValidateUUID valide une chaîne UUID
func ValidateUUID(input string) (uuid.UUID, error) {
// Essayer de parser avec le package uuid
parsed, err := uuid.Parse(input)
if err != nil {
return uuid.Nil, fmt.Errorf("format UUID invalide : %w", err)
}
// Vérifier si c'est l'UUID nil
if parsed == uuid.Nil {
return uuid.Nil, fmt.Errorf("UUID nil non autorisé")
}
return parsed, nil
}
// ValidateUUIDVersion vérifie si un UUID est d'une version spécifique
func ValidateUUIDVersion(id uuid.UUID, expectedVersion uuid.Version) error {
if id.Version() != expectedVersion {
return fmt.Errorf("version UUID attendue %d, obtenue %d", expectedVersion, id.Version())
}
return nil
}
// NormalizeUUID gère divers formats d'entrée UUID
func NormalizeUUID(input string) (uuid.UUID, error) {
// Supprimer les caractères englobants communs
cleaned := strings.Trim(input, "{}()")
// Supprimer les espaces et convertir en minuscules
cleaned = strings.ToLower(strings.ReplaceAll(cleaned, " ", ""))
// Gérer UUID sans tirets
if len(cleaned) == 32 && !strings.Contains(cleaned, "-") {
// Insérer des tirets aux bonnes positions
cleaned = fmt.Sprintf("%s-%s-%s-%s-%s",
cleaned[0:8],
cleaned[8:12],
cleaned[12:16],
cleaned[16:20],
cleaned[20:32],
)
}
return uuid.Parse(cleaned)
}
func main() {
testCases := []string{
"550e8400-e29b-41d4-a716-446655440000", // Valide
"550e8400e29b41d4a716446655440000", // Valide sans tirets
"{550e8400-e29b-41d4-a716-446655440000}", // Valide avec accolades
"not-a-uuid", // Invalide
"00000000-0000-0000-0000-000000000000", // UUID nil
}
for _, test := range testCases {
fmt.Printf("Test : %s\n", test)
// Validation de base
if id, err := ValidateUUID(test); err != nil {
fmt.Printf(" Validation échouée : %v\n", err)
} else {
fmt.Printf(" UUID valide : %s (Version %d)\n", id, id.Version())
}
// Essayer la normalisation
if normalized, err := NormalizeUUID(test); err == nil {
fmt.Printf(" Normalisé : %s\n", normalized)
}
fmt.Println()
}
}
Cet exemple de validation montre plusieurs approches pour la validation d'UUID en Go. La fonction ValidateUUID utilise le parsing intégré du package uuid, tandis que NormalizeUUID gère divers formats d'entrée que les utilisateurs pourraient fournir. Le code démontre également la vérification de version et la détection d'UUID nil. Pour une utilisation en production, considérez quelles règles de validation sont appropriées pour votre cas d'usage spécifique.
Lors de la génération d'UUID à grande échelle, comprendre les caractéristiques de performance devient important. Voici comment benchmarker et optimiser la génération d'UUID en Go :
package main
import (
"fmt"
"runtime"
"sync"
"testing"
"time"
"github.com/google/uuid"
)
// BenchmarkUUIDGeneration benchmark différentes méthodes de génération d'UUID
func BenchmarkUUIDGeneration(b *testing.B) {
b.Run("Séquentiel", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uuid.New()
}
})
b.Run("Parallèle", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_ = uuid.New()
}
})
})
b.Run("Chaîne", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uuid.NewString()
}
})
}
// ConcurrentUUIDGenerator génère des UUID de manière concurrente
type ConcurrentUUIDGenerator struct {
workers int
buffer chan uuid.UUID
wg sync.WaitGroup
stop chan struct{}
}
// NewConcurrentGenerator crée un nouveau générateur UUID concurrent
func NewConcurrentGenerator(workers, bufferSize int) *ConcurrentUUIDGenerator {
g := &ConcurrentUUIDGenerator{
workers: workers,
buffer: make(chan uuid.UUID, bufferSize),
stop: make(chan struct{}),
}
g.start()
return g
}
// start démarre les goroutines de travail
func (g *ConcurrentUUIDGenerator) start() {
g.wg.Add(g.workers)
for i := 0; i < g.workers; i++ {
go func() {
defer g.wg.Done()
for {
select {
case <-g.stop:
return
case g.buffer <- uuid.New():
// UUID généré et envoyé au buffer
}
}
}()
}
}
// Get récupère un UUID du générateur
func (g *ConcurrentUUIDGenerator) Get() uuid.UUID {
return <-g.buffer
}
// Stop arrête gracieusement le générateur
func (g *ConcurrentUUIDGenerator) Stop() {
close(g.stop)
g.wg.Wait()
close(g.buffer)
}
func main() {
// Comparaison de performance
fmt.Println("Test de Performance de Génération UUID")
fmt.Printf("Cœurs CPU : %d\n\n", runtime.NumCPU())
count := 1_000_000
// Génération séquentielle
start := time.Now()
for i := 0; i < count; i++ {
_ = uuid.New()
}
sequential := time.Since(start)
fmt.Printf("Séquentiel : %v pour %d UUID\n", sequential, count)
fmt.Printf("Taux : %.0f UUID/seconde\n\n", float64(count)/sequential.Seconds())
// Génération concurrente
generator := NewConcurrentGenerator(runtime.NumCPU(), 1000)
start = time.Now()
for i := 0; i < count; i++ {
_ = generator.Get()
}
concurrent := time.Since(start)
generator.Stop()
fmt.Printf("Concurrent : %v pour %d UUID\n", concurrent, count)
fmt.Printf("Taux : %.0f UUID/seconde\n", float64(count)/concurrent.Seconds())
fmt.Printf("Accélération : %.2fx\n", sequential.Seconds()/concurrent.Seconds())
// Estimation de l'utilisation mémoire
fmt.Printf("\nMémoire par UUID : %d octets\n", len(uuid.New()))
fmt.Printf("Mémoire pour %d UUID : %.2f MB\n", count, float64(count*16)/(1024*1024))
}
Cet exemple de performance démontre à la fois des techniques de benchmarking et un générateur UUID concurrent pour des scénarios à haut débit. Le générateur concurrent utilise des goroutines de travail pour générer des UUID en parallèle, ce qui peut améliorer significativement les performances sur des systèmes multi-cœurs. Les benchmarks vous aident à comprendre les caractéristiques de performance de différentes méthodes de génération. Pour la plupart des applications, le uuid.New() standard est suffisant, mais pour des scénarios à haut volume, la génération concurrente peut fournir un meilleur débit.
Lors du travail avec des UUID dans les applications Go, choisissez la bibliothèque et la version d'UUID appropriées en fonction de vos exigences spécifiques. Le package google/uuid est recommandé pour la plupart des cas d'usage en raison de sa simplicité, de ses performances et de sa maintenance par Google. Pour les applications nécessitant des versions spécifiques d'UUID ou des fonctionnalités supplémentaires, satori/go.uuid offre plus de flexibilité. Validez toujours les UUID provenant de sources externes et considérez les implications de performance lors de la génération de grandes quantités d'UUID.
Rappelez-vous que bien que les UUID fournissent d'excellentes garanties d'unicité, ils viennent avec des compromis tels que des exigences de stockage plus importantes (16 octets vs 4-8 octets pour les entiers) et des impacts potentiels sur les performances des index de base de données. Pour les données de séries temporelles ou lorsque l'ordre est important, envisagez d'utiliser des UUID Version 1 ou Version 7. Pour des identifiants uniques à usage général, les UUID Version 4 aléatoires sont le choix le plus courant. Gérez toujours les erreurs de manière appropriée et envisagez d'implémenter une logique de retry pour la génération d'UUID dans les chemins critiques.
Pour une génération rapide d'UUID sans écrire de code, vous pouvez toujours utiliser notre outil de génération d'UUID en ligne, qui fournit diverses versions et formats d'UUID d'un simple clic.