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
- Éviter les allocations :
make([]T, 0, n)plutôt que append - String builder :
strings.Builder> concaténation - Pool d'objets :
sync.Poolpour objets réutilisables - Éviter les interfaces hot path : inline friendly
- É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)