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.