Modèles de développement Go
Modèles Go idiomatiques et bonnes pratiques pour construire des applications robustes, efficaces et maintenables.
Quand activer
- Écrire du nouveau code Go
- Réviser du code Go
- Refactoriser du code Go existant
- Concevoir des packages/modules Go
Principes fondamentaux
1. Simplicité et clarté
Go privilégie la simplicité plutôt que l'ingéniosité. Le code doit être évident et facile à lire.
// Bien : Clair et direct
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// Mal : Trop astucieux
func GetUser(id string) (*User, error) {
return func() (*User, error) {
if u, e := db.FindUser(id); e == nil {
return u, nil
} else {
return nil, e
}
}()
}
2. Rendre la valeur zéro utile
Concevez les types pour que leur valeur zéro soit immédiatement utilisable sans initialisation.
// Bien : La valeur zéro est utile
type Counter struct {
mu sync.Mutex
count int // la valeur zéro est 0, prête à l'emploi
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// Bien : bytes.Buffer fonctionne avec la valeur zéro
var buf bytes.Buffer
buf.WriteString("hello")
// Mal : Nécessite une initialisation
type BadCounter struct {
counts map[string]int // une map nil va paniquer
}
3. Accepter des interfaces, retourner des structures
Les fonctions doivent accepter des paramètres d'interface et retourner des types concrets.
// Bien : Accepte une interface, retourne un type concret
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// Mal : Retourne une interface (cache inutilement les détails d'implémentation)
func ProcessData(r io.Reader) (io.Reader, error) {
// ...
}
Modèles de gestion d'erreurs
Emballage des erreurs avec contexte
// Bien : Emballer les erreurs avec contexte
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Types d'erreur personnalisés
// Définir des erreurs spécifiques au domaine
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Erreurs sentinelles pour les cas courants
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
Vérification des erreurs avec errors.Is et errors.As
func HandleError(err error) {
// Vérifier une erreur spécifique
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
// Vérifier un type d'erreur
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
// Erreur inconnue
log.Printf("Unexpected error: %v", err)
}
Ne jamais ignorer les erreurs
// Mal : Ignorer l'erreur avec l'identifiant vide
result, _ := doSomething()
// Bien : Gérer ou documenter explicitement pourquoi il est sûr d'ignorer
result, err := doSomething()
if err != nil {
return err
}
// Acceptable : Quand l'erreur n'a vraiment pas d'importance (rare)
_ = writer.Close() // Nettoyage au mieux, erreur journalisée ailleurs
Modèles de concurrence
Pool de workers
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Contexte pour annulation et délais d'attente
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Arrêt gracieux
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
errgroup pour goroutines coordonnées
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Capturer les variables de boucle
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Éviter les fuites de goroutines
// Mal : Fuite de goroutine si le contexte est annulé
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data // Bloque pour toujours s'il n'y a pas de récepteur
}()
return ch
}
// Bien : Gère correctement l'annulation
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1) // Canal tamponné
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
Conception d'interfaces
Interfaces petites et ciblées
// Bien : Interfaces à méthode unique
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composer les interfaces selon les besoins
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Définir les interfaces là où elles sont utilisées
// Dans le package consommateur, pas le fournisseur
package service
// UserStore définit ce dont ce service a besoin
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
// L'implémentation concrète peut être dans un autre package
// Elle n'a pas besoin de connaître cette interface
Comportement optionnel avec des assertions de type
type Flusher interface {
Flush() error
}
func WriteAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Vider si supporté
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
Organisation des packages
Structure de projet standard
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # Point d'entrée
├── internal/
│ ├── handler/ # Gestionnaires HTTP
│ ├── service/ # Logique métier
│ ├── repository/ # Accès aux données
│ └── config/ # Configuration
├── pkg/
│ └── client/ # Client d'API public
├── api/
│ └── v1/ # Définitions d'API (proto, OpenAPI)
├── testdata/ # Données de test
├── go.mod
├── go.sum
└── Makefile
Nommage des packages
// Bien : Court, minuscules, sans underscores
package http
package json
package user
// Mal : Verbeux, casse mélangée ou redondant
package httpHandler
package json_parser
package userService // Suffixe 'Service' redondant
Éviter l'état au niveau du package
// Mal : État mutable global
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// Bien : Injection de dépendances
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Conception des structures
Modèle d'options fonctionnelles
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // valeur par défaut
logger: log.Default(), // valeur par défaut
}
for _, opt := range opts {
opt(s)
}
return s
}
// Utilisation
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
Embedding pour la composition
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // Embedding - Server obtient la méthode Log
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
// Utilisation
s := NewServer(":8080")
s.Log("Starting...") // Appelle Logger.Log embedded
Mémoire et performance
Préallouer les slices quand la taille est connue
// Mal : Agrandit la slice plusieurs fois
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
// Bien : Allocation unique
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
Utiliser sync.Pool pour les allocations fréquentes
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// Traitement...
return buf.Bytes()
}
Éviter la concaténation de chaînes dans les boucles
// Mal : Crée de nombreuses allocations de chaînes
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
// Bien : Allocation unique avec strings.Builder
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// Meilleur : Utiliser la bibliothèque standard
func join(parts []string) string {
return strings.Join(parts, ",")
}
Intégration des outils Go
Commandes essentielles
# Build et exécution
go build ./...
go run ./cmd/myapp
# Tests
go test ./...
go test -race ./...
go test -cover ./...
# Analyse statique
go vet ./...
staticcheck ./...
golangci-lint run
# Gestion des modules
go mod tidy
go mod verify
# Formatage
gofmt -w .
goimports -w .
Configuration de linter recommandée (.golangci.yml)
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
enable:
- shadow
issues:
exclude-use-default: false
Référence rapide : Idiomes Go
| Idiome | Description |
|---|---|
| Accepter des interfaces, retourner des structures | Les fonctions acceptent des paramètres d'interface, retournent des types concrets |
| Les erreurs sont des valeurs | Traiter les erreurs comme des valeurs de première classe, pas des exceptions |
| Ne communiquez pas en partageant la mémoire | Utiliser les canaux pour la coordination entre goroutines |
| Rendre la valeur zéro utile | Les types doivent fonctionner sans initialisation explicite |
| Une petite copie vaut mieux qu'une petite dépendance | Éviter les dépendances externes inutiles |
| Clair est mieux qu'astucieux | Prioriser la lisibilité plutôt que l'ingéniosité |
| gofmt n'est le favori de personne mais l'ami de tout le monde | Toujours formater avec gofmt/goimports |
| Retourner tôt | Gérer les erreurs d'abord, garder le chemin heureux non indenté |
Anti-patrons à éviter
// Mal : Retours nus dans des fonctions longues
func process() (result int, err error) {
// ... 50 lignes ...
return // Qu'est-ce qui est retourné ?
}
// Mal : Utiliser panic pour le flux de contrôle
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Ne faites pas ça
}
return user
}
// Mal : Passer le contexte dans une structure
type Request struct {
ctx context.Context // Le contexte doit être le premier paramètre
ID string
}
// Bien : Contexte comme premier paramètre
func ProcessRequest(ctx context.Context, id string) error {
// ...
}
// Mal : Mélanger les récepteurs valeur et pointeur
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // Récepteur valeur
func (c *Counter) Increment() { c.n++ } // Récepteur pointeur
// Choisissez un style et soyez cohérent
Rappelez-vous : Le code Go devrait être ennuyeux de la meilleure façon possible - prévisible, cohérent et facile à comprendre. Dans le doute, gardez-le simple.