Aller au contenu

Bonnes pratiques

Go est un langage opinionated : il impose un style de code via gofmt, encourage des nommages courts, des packages plats et une gestion d'erreurs explicite. Cette section couvre les idiomes fondamentaux issus d'Effective Go, les anti-patterns fréquents et les techniques d'optimisation des performances.


Conventions de code (Effective Go)

Formatage — gofmt est obligatoire

gofmt est l'outil de formatage officiel. Il n'est pas configurable et son résultat fait référence. Tout code Go doit passer gofmt avant d'être commite.

# Formater tous les fichiers en place
gofmt -w .

# Verifier sans modifier (utile en CI)
test -z "$(gofmt -l .)"

# goimports ajoute/supprime les imports en plus du formatage
goimports -w .

Nommage — court et précis

// Mauvais — verbeux, redondant avec le type
func GetUserByIdentifier(userIdentifier int) (*UserModel, error) { ... }

// Bon — concis, le contexte apporte le sens
func GetUser(id int) (*User, error) { ... }

// Variables de courte portee : une ou deux lettres
for i, v := range items { ... }
if err := doSomething(); err != nil { ... }

// Variables de longue portee : nom descriptif
type Server struct {
    maxConnections int
    shutdownCh     chan struct{}
}

// Acronymes en majuscules : URL, HTTP, ID (pas Url, Http, Id)
type HTTPClient struct { ... }
func ParseURL(s string) (*URL, error) { ... }

Packages — plats et coherents

// Structure recommandee — packages plats par domaine
myapp/
├── main.go
├── server.go       // Package main ou package server
├── items/
│   ├── item.go     // Struct Item, logique metier
│   ├── store.go    // Interface et implementations de persistance
│   └── handler.go  // Handlers HTTP
└── config/
    └── config.go

// A eviter — packages util, common, helpers (fourre-tout)
myapp/
├── utils/          // Mauvais : que met-on ici ?
├── helpers/        // Mauvais : idem
└── common/         // Mauvais : trop vague

Idiomes Go

Gestion d'erreurs — toujours vérifier

// Mauvais — erreur ignoree, potentiel panic ou comportement incorrect
data, _ := os.ReadFile("config.json")

// Bon — chaque erreur est geree explicitement
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("lecture de la configuration : %w", err)
}

Interfaces implicites — programmation par contrat

// L'interface est definie par le consommateur, pas le producteur.
// ItemRepository est defini dans le package qui en a besoin.
type ItemRepository interface {
    FindByID(ctx context.Context, id int) (*Item, error)
    Save(ctx context.Context, item *Item) error
    Delete(ctx context.Context, id int) error
}

// GormItemRepository implemente ItemRepository implicitement —
// aucun mot-cle "implements" n'est necessaire.
type GormItemRepository struct {
    db *gorm.DB
}

// Verification statique que GormItemRepository satisfait ItemRepository.
// Cette ligne echoue a la compilation si l'interface n'est pas satisfaite.
var _ ItemRepository = (*GormItemRepository)(nil)

func (r *GormItemRepository) FindByID(ctx context.Context, id int) (*Item, error) {
    var item Item
    if err := r.db.WithContext(ctx).First(&item, id).Error; err != nil {
        return nil, fmt.Errorf("FindByID(%d) : %w", id, err)
    }
    return &item, nil
}

Composition par embedding

// Embedding permet la composition sans heritage.
type BaseHandler struct {
    logger *slog.Logger
    db     *gorm.DB
}

func (h *BaseHandler) respond(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

// ItemHandler compose BaseHandler et herite de ses methodes.
type ItemHandler struct {
    BaseHandler          // Embedding — ItemHandler.respond() est disponible
    repo ItemRepository
}

func (h *ItemHandler) GetItem(w http.ResponseWriter, r *http.Request) {
    id, _ := strconv.Atoi(chi.URLParam(r, "id"))
    item, err := h.repo.FindByID(r.Context(), id)
    if err != nil {
        h.respond(w, http.StatusNotFound, map[string]string{"error": err.Error()})
        return
    }
    h.respond(w, http.StatusOK, item) // Methode heritee de BaseHandler
}

defer — nettoyage garanti

func lireFichier(chemin string) ([]byte, error) {
    f, err := os.Open(chemin)
    if err != nil {
        return nil, err
    }
    defer f.Close() // Fermeture garantie, meme en cas d'erreur

    return io.ReadAll(f)
}

// defer avec capture de valeur de retour nommee
func creerTransaction(db *sql.DB) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    defer func() {
        if err != nil {
            tx.Rollback() // Rollback si une erreur a ete assignee
        }
    }()

    if err = executerOperations(tx); err != nil {
        return err // Le defer effectuera le rollback
    }

    return tx.Commit()
}

Gestion des erreurs

Wrapping avec fmt.Errorf et %w

// %w permet d'emballer une erreur tout en conservant la chaine causale.
func chargerConfig(chemin string) (*Config, error) {
    data, err := os.ReadFile(chemin)
    if err != nil {
        // L'appelant peut inspecter l'erreur originale avec errors.Is/As
        return nil, fmt.Errorf("chargerConfig(%q) : %w", chemin, err)
    }
    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("chargerConfig : parsing JSON : %w", err)
    }
    return &cfg, nil
}

Erreurs sentinelles et erreurs typees

import "errors"

// Erreurs sentinelles — comparaison par valeur avec errors.Is
var (
    ErrNotFound   = errors.New("element non trouve")
    ErrUnauthorized = errors.New("acces non autorise")
)

// Erreur typee — inspection de la valeur avec errors.As
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation de %q : %s", e.Field, e.Message)
}

// Utilisation
func traiter(id int) error {
    item, err := repo.FindByID(ctx, id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return fmt.Errorf("item %d introuvable : %w", id, ErrNotFound)
        }
        return err
    }

    if err := valider(item); err != nil {
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            log.Printf("Champ invalide : %s", valErr.Field)
        }
        return err
    }
    return nil
}

Anti-patterns

Pollution d'interfaces

// Mauvais — interface trop large, difficile a mocker et a implementer
type UserService interface {
    Create(user User) error
    Update(user User) error
    Delete(id int) error
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
    SendWelcomeEmail(user User) error
    GenerateToken(user User) (string, error)
    ValidateToken(token string) (*User, error)
}

// Bon — interfaces petites et focalisees (principe de segregation)
type UserReader interface {
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
}

type UserWriter interface {
    Create(user User) error
    Update(user User) error
    Delete(id int) error
}

Abus des goroutines et channels

// Mauvais — goroutine lancee sans supervision, fuite potentielle
func mauvais() {
    go func() {
        // Que se passe-t-il si cette goroutine panique ?
        // Comment savoir quand elle se termine ?
        processInBackground()
    }()
}

// Bon — goroutine supervisee avec context et WaitGroup
func bon(ctx context.Context, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        defer wg.Done()
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Panic recupere : %v", r)
            }
        }()
        processInBackground(ctx)
    }()
}

Abus de init()

// Mauvais — init() fait des I/O, peut paniquer, ordre non garanti
func init() {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        panic(err) // Impossible a tester proprement
    }
    globalDB = db
}

// Bon — initialisation explicite dans main()
func main() {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatalf("Impossible d'ouvrir la base : %v", err)
    }
    defer db.Close()

    server := NewServer(db)
    server.Run(":8080")
}

Performance

sync.Pool — réutilisation d'objets

import "sync"

// Pool de buffers pour eviter les allocations repetees
var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer)
    },
}

func marshalJSON(v any) ([]byte, error) {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufPool.Put(buf) // Retour au pool apres utilisation

    if err := json.NewEncoder(buf).Encode(v); err != nil {
        return nil, err
    }
    result := make([]byte, buf.Len())
    copy(result, buf.Bytes())
    return result, nil
}

Profiling avec pprof

import (
    "net/http"
    _ "net/http/pprof" // Import pour les effets de bord (enregistrement des handlers)
)

func main() {
    // Activer le serveur pprof sur un port distinct
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ... reste du programme
}
# Profil CPU pendant 30 secondes
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Profil memoire
go tool pprof http://localhost:6060/debug/pprof/heap

# Dans l'interface pprof interactive
(pprof) top10          # Top 10 fonctions par consommation
(pprof) web            # Graphe visuel dans le navigateur
(pprof) list NomFonction  # Detail ligne par ligne

# Analyse d'echappement (quelles variables vont sur le tas)
go build -gcflags="-m" . 2>&1 | head -50

Race detector

Lancez go test -race ./... systematiquement en CI. Le race detector détecté les accès concurrents non synchronisés avec un overhead mémoire de 5-10x. Gratuit en termes de code — il suffit d'ajouter le flag.