MFormations
Modern Backend Engineering

Chapitre 4

Chapitre 04 — Go

Chapitre 04 — Go

Cours complet — Go

1. Goroutines

Qu'est-ce qu'une goroutine ?

  • Green thread : thread léger géré par le runtime Go
  • Stack variable : commence à 2KB (vs 1MB+ pour un thread OS)
  • M:N scheduling : M goroutines multiplexées sur N threads OS
  • Coût : ~4KB mémoire, création en ~1μs

Go scheduler

G = Goroutine
M = Machine (thread OS)
P = Processor (contexte d'exécution, GOMAXPROCS)

GOMAXPROCS = nombre de cœurs logiques (défaut)

┌───── P ─────┐     ┌───── P ─────┐
│ Local runq   │     │ Local runq   │
│ [G1] [G2]    │     │ [G3] [G4]    │
└──────┬───────┘     └──────┬───────┘
       │ M (thread)         │ M (thread)
       ▼                    ▼
    CPU Core              CPU Core

Global runq: [G5] [G6] ← Goroutines en attente

Work stealing : un P sans goroutine vole depuis un autre P ou la queue globale.

Goroutine lifecycle

go myFunc()      // Lance une goroutine (fire-and-forget)

func myFunc() {
    defer wg.Done()
    // ...
}

// Synchronisation avec WaitGroup
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Println("Worker", id)
    }(i)
}
wg.Wait()

Goroutine leaks

  • Cause : goroutine bloquée sur un channel qui ne sera jamais lu
  • Détection : runtime.NumGoroutine(), pprof
  • Prévention : context cancellation, timeouts, structuration
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 1 // Bloqué si personne ne lit
    }()
    // Oubli de lire → goroutine leak
}

2. Channels

Types de channels

// Non-buffered (synchrone)
ch := make(chan int)
ch <- 1          // Bloque jusqu'à ce qu'un goroutine lise

// Buffered (asynchrone, taille fixe)
ch := make(chan int, 10)
ch <- 1          // Bloque seulement si plein

// Unidirectional
func readOnly(ch <-chan int) {}    // Lecture seule
func writeOnly(ch chan<- int) {}   // Écriture seule

Channel patterns

Fan-out (1 producteur, N consommateurs) :

func fanOut(source <-chan int, workers int) {
    for i := 0; i < workers; i++ {
        go func(id int) {
            for val := range source {
                process(val)
            }
        }(i)
    }
}

Fan-in (N producteurs, 1 consommateur) :

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for val := range c {
                out <- val
            }
        }(ch)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

Pipeline :

// generator → filter → squarer → printer
func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func filter(in <-chan int, predicate func(int) bool) <-chan int {
    out := make(chan int)
    go func() {
        for val := range in {
            if predicate(val) {
                out <- val
            }
        }
        close(out)
    }()
    return out
}

func squarer(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for val := range in {
            out <- val * val
        }
        close(out)
    }()
    return out
}

// Usage
for result := range squarer(filter(generator(1, 2, 3, 4, 5), func(n int) bool { return n%2 == 0 })) {
    fmt.Println(result) // 4, 16
}

Channel closing

  • Seul l'expéditeur ferme le channel (panic si fermé deux fois)
  • Vérifier fermeture : val, ok := <-ch (ok=false si fermé)
  • Range automatique : for val := range ch { }

3. Select

Multiplexage de channels

select {
case msg1 := <-ch1:
    fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("Received from ch2:", msg2)
case <-time.After(1 * time.Second):
    fmt.Println("Timeout")
default:
    fmt.Println("No channel ready")  // Non-bloquant
}

Patterns select

Timeout :

select {
case result := <-doWork():
    return result
case <-time.After(5 * time.Second):
    return nil, errors.New("timeout")
}

Ticker :

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
    select {
    case <-ticker.C:
        doPeriodicWork()
    case <-ctx.Done():
        return ctx.Err()
    }
}

Quit channel :

func worker(quit <-chan struct{}) {
    for {
        select {
        case <-quit:
            return
        default:
            // Continue working
        }
    }
}

quit := make(chan struct{})
go worker(quit)
// Later: close(quit) → arrête tous les workers

4. Standard library (net/http)

HTTP server

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

// Handler avec pattern
func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        listUsers(w, r)
    case http.MethodPost:
        createUser(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Go 1.22+ : /users/{id}
    switch r.Method {
    case http.MethodGet:
        getUser(w, r, id)
    case http.MethodPut:
        updateUser(w, r, id)
    case http.MethodDelete:
        deleteUser(w, r, id)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    user.ID = len(users) + 1
    user.CreatedAt = time.Now()
    users = append(users, user)
    
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })
    mux.HandleFunc("GET /users", usersHandler)
    mux.HandleFunc("POST /users", usersHandler)
    mux.HandleFunc("GET /users/{id}", userHandler)
    mux.HandleFunc("PUT /users/{id}", userHandler)
    mux.HandleFunc("DELETE /users/{id}", userHandler)
    
    server := &http.Server{
        Addr:         ":8080",
        Handler:      middleware(mux),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  30 * time.Second,
    }
    
    log.Fatal(server.ListenAndServe())
}

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}

Middleware pattern

// Middleware chain
type Middleware func(http.Handler) http.Handler

func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// Usage
mux := http.NewServeMux()
handler := Chain(
    mux,
    LoggerMiddleware,
    AuthMiddleware,
    RateLimitMiddleware,
)

5. Performance

Profiling

# CPU profile
go test -bench=. -cpuprofile=cpu.prof
go tool pprof -http=:8080 cpu.prof

# Memory profile
go test -bench=. -memprofile=mem.prof
go tool pprof -http=:8081 mem.prof

# Trace
go test -trace=trace.out
go tool trace trace.out

# Runtime profiling
import _ "net/http/pprof"
// /debug/pprof/goroutine
// /debug/pprof/heap
// /debug/pprof/profile?seconds=30

Optimisations

  1. Éviter les allocations : make([]T, 0, n) plutôt que append
  2. String builder : strings.Builder > concaténation
  3. Pool d'objets : sync.Pool pour objets réutilisables
  4. Éviter les interfaces hot path : inline friendly
  5. Éviter reflection : génériques (Go 1.18+) ou codegen
// Pool d'objets
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process(data []byte) string {
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufferPool.Put(buf)
    
    buf.Write(data)
    return buf.String()
}

GC optimizations

  • GOGC : ratio de croissance heap (défaut 100)
  • GOMEMLIMIT : soft memory limit (Go 1.19+)
  • GC tunings : réduire les allocations, utiliser des pools

6. Compilation

Go build

# Cross-compilation
GOOS=linux GOARCH=amd64 go build -o app-linux
GOOS=darwin GOARCH=arm64 go build -o app-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o app.exe

# Optimisations
go build -ldflags="-s -w"    # Strip debug + DWARF
go build -ldflags="-s -w -X main.version=1.0.0"  # Injecter version

# CGO disabled (static binary)
CGO_ENABLED=0 go build -o app-static

// TinyGo (WebAssembly, microcontrollers)
tinygo build -o app.wasm -target=wasm

Binary size

Hello world:  ~1.5MB (static)
Standard API: ~10MB   (avec net/http)
Avec dépendances: ~20-40MB

-ldflags="-s -w" → -30% size UPX compression → -70% size

7. Interfaces

Interface design

// Go favorise les petites interfaces (1-3 méthodes)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Composition d'interfaces
type ReadWriter interface {
    Reader
    Writer
}

// Interface discovery (sans import)
type Storer[T any] interface {
    Get(id string) (T, error)
    Set(id string, value T) error
    Delete(id string) error
}

Accept interfaces, return structs

// Bon : accepte une interface
func ProcessReader(r io.Reader) error {
    data, err := io.ReadAll(r)
    // ...
}

// Bon : retourne un struct concret
func NewSQLStore(dsn string) *SQLStore {
    return &SQLStore{db: connect(dsn)}
}

// Éviter : retourner une interface sauf nécessaire
// Retourner une interface = dépendance sur le package appelant

Type assertion vs switch

// Type assertion
if s, ok := val.(string); ok {
    fmt.Println(s)
}

// Type switch
switch v := val.(type) {
case string:
    fmt.Println("string:", v)
case int:
    fmt.Println("int:", v)
case error:
    fmt.Println("error:", v)
default:
    fmt.Printf("unknown type %T\n", v)
}

8. Gestion d'erreurs

Go 1.13+ error handling

// Error wrapping
type NotFoundError struct {
    Resource string
    ID       string
    Err      error
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %s not found: %v", e.Resource, e.ID, e.Err)
}

func (e *NotFoundError) Unwrap() error {
    return e.Err
}

// Création
return &NotFoundError{Resource: "user", ID: id, Err: err}

// Vérification
if errors.As(err, &nfErr) {
    fmt.Println("Not found:", nfErr.Resource, nfErr.ID)
}

// Sentinelles
var ErrNotFound = errors.New("not found")

if errors.Is(err, ErrNotFound) {
    // Handle
}

// Joindre (Go 1.20+)
err = errors.Join(err1, err2, err3)

Error handling patterns

// 1. Check and return
result, err := doSomething()
if err != nil {
    return fmt.Errorf("doing something: %w", err)
}

// 2. Defer close
f, err := os.Open(file)
if err != nil {
    return err
}
defer f.Close()

// 3. Just enough error handling
if err := doSomething(); err != nil {
    log.Printf("warning: %v", err)
    // Continue anyway
}

// 4. Error type for HTTP
type HTTPError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func WriteHTTPError(w http.ResponseWriter, err error) {
    var httpErr *HTTPError
    if errors.As(err, &httpErr) {
        w.WriteHeader(httpErr.Code)
    } else {
        w.WriteHeader(http.StatusInternalServerError)
        httpErr = &HTTPError{Code: 500, Message: "Internal error"}
    }
    json.NewEncoder(w).Encode(httpErr)
}

Panic / Recover

// Utiliser pour des cas exceptionnels (pas pour l'erreur normale)
func handler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("panic: %v", err)
            http.Error(w, "Internal error", 500)
        }
    }()
    doWork()
}

9. Patterns Go

Options (functional options)

type ServerOption func(*Server)

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithLogger(logger *log.Logger) ServerOption {
    return func(s *Server) {
        s.logger = logger
    }
}

func WithTLS(cert, key string) ServerOption {
    return func(s *Server) {
        s.tlsConfig = &TLSConfig{Cert: cert, Key: key}
    }
}

func NewServer(opts ...ServerOption) *Server {
    s := &Server{
        port:   8080,
        logger: log.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
server := NewServer(
    WithPort(3000),
    WithLogger(customLogger),
    WithTLS("cert.pem", "key.pem"),
)

Context pattern

func HandleRequest(ctx context.Context, req Request) (*Response, error) {
    // Ajouter des valeurs au contexte
    ctx = context.WithValue(ctx, "request_id", req.ID)
    
    // Timeout
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    result := make(chan *Response, 1)
    go func() {
        result <- process(req)
    }()
    
    select {
    case res := <-result:
        return res, nil
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

Repository pattern

type UserRepository interface {
    Get(ctx context.Context, id string) (*User, error)
    List(ctx context.Context, offset, limit int) ([]User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
}

type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) Get(ctx context.Context, id string) (*User, error) {
    user := &User{}
    err := r.db.QueryRowContext(ctx,
        "SELECT id, name, email, created_at FROM users WHERE id = $1", id,
    ).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, &NotFoundError{Resource: "user", ID: id}
    }
    return user, err
}

Références

  • Go Doc (go.dev/doc)
  • Effective Go (go.dev/doc/effective_go)
  • Go Blog (go.dev/blog)
  • The Go Programming Language (Donovan & Kernighan)