Modern Go Engineering
Chapitre 4
04 - Web avec Go
04 - Web avec Go
Cours 04 : Web avec Go
1. net/http avancé
1.1 Handler et HandlerFunc
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// HandlerFunc adapte une fonction en Handler
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
// Utilisation
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}
1.2 ServeMux (Go 1.22+)
mux := http.NewServeMux()
// Patterns améliorés (Go 1.22+)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Path values
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User ID: %s", id)
}
// Sous-patterns
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
// Catch-all pour /api/*
})
1.3 ResponseWriter
func handler(w http.ResponseWriter, r *http.Request) {
// Headers
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", r.Header.Get("X-Request-ID"))
// Status code
w.WriteHeader(http.StatusOK)
// Body
json.NewEncoder(w).Encode(map[string]any{
"message": "success",
})
}
// Écrire response JSON helper
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
2. Frameworks Web
2.1 Chi
import "github.com/go-chi/chi/v5"
func main() {
r := chi.NewRouter()
// Middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
// Routes
r.Get("/users", listUsers)
r.Post("/users", createUser)
r.Route("/users/{id}", func(r chi.Router) {
r.Use(userCtx)
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
// Groups
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/admin", adminHandler)
})
http.ListenAndServe(":8080", r)
}
2.2 Gin
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/users", listUsers)
r.POST("/users", createUser)
r.GET("/users/:id", getUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
// Groups
api := r.Group("/api")
{
api.GET("/users", listUsers)
api.POST("/users", createUser)
}
// Binding et validation
r.POST("/users", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(201, user)
})
r.Run(":8080")
}
2.3 Echo
import "github.com/labstack/echo/v4"
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/users/:id", getUser)
e.POST("/users", createUser)
// Groups
g := e.Group("/admin")
g.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == "secret", nil
}))
e.Logger.Fatal(e.Start(":8080"))
}
2.4 Fiber
import "github.com/gofiber/fiber/v2"
func main() {
app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
id := c.Params("id")
return c.JSON(fiber.Map{
"id": id,
"name": "Alice",
})
})
app.Listen(":8080")
}
2.5 Comparatif
| Framework | Performance | Popularité | Middleware | Routing | Learning |
|---|---|---|---|---|---|
| Chi | Haute | Haute | Excellente | Excellent | Facile |
| Gin | Très haute | Très haute | Bonne | Bon | Moyen |
| Echo | Haute | Haute | Bonne | Bon | Facile |
| Fiber | Excellente | Moyenne | Moyenne | Bon | Très facile |
3. Templates
3.1 html/template
import "html/template"
type PageData struct {
Title string
User *User
Items []string
IsAdmin bool
}
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles(
"templates/base.html",
"templates/nav.html",
"templates/content.html",
))
data := PageData{
Title: "Dashboard",
User: &User{Name: "Alice"},
Items: []string{"Item 1", "Item 2"},
}
tmpl.ExecuteTemplate(w, "base.html", data)
}
3.2 Templates avec partials
// templates/base.html
{{define "base"}}
<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}Default Title{{end}}</title>
</head>
<body>
{{template "nav" .}}
{{block "content" .}}{{end}}
{{template "footer" .}}
</body>
</html>
{{end}}
// templates/nav.html
{{define "nav"}}
<nav>
<a href="/">Home</a>
{{if .IsAdmin}}
<a href="/admin">Admin</a>
{{end}}
</nav>
{{end}}
3.3 Fonctions personnalisées
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"date": func(t time.Time) string {
return t.Format("January 2, 2006")
},
"inc": func(i int) int {
return i + 1
},
}
tmpl := template.New("").Funcs(funcMap)
tmpl = template.Must(tmpl.ParseGlob("templates/*.html"))
4. Middleware
4.1 Logging
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap ResponseWriter pour capturer le status
lw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(lw, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", lw.statusCode,
"duration", time.Since(start),
"remote", r.RemoteAddr,
)
})
}
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (lw *loggingResponseWriter) WriteHeader(code int) {
lw.statusCode = code
lw.ResponseWriter.WriteHeader(code)
}
4.2 Recovery
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recovered",
"error", err,
"path", r.URL.Path,
)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
4.3 Auth
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
4.4 CORS
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
4.5 Rate Limiting
type RateLimiter struct {
mu sync.Mutex
visitors map[string]*visitor
rate time.Duration
burst int
}
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
rl.mu.Lock()
v, exists := rl.visitors[ip]
if !exists {
v = &visitor{
limiter: rate.NewLimiter(rate.Every(rl.rate), rl.burst),
lastSeen: time.Now(),
}
rl.visitors[ip] = v
}
v.lastSeen = time.Now()
rl.mu.Unlock()
if !v.limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
5. Configuration (Viper)
import "github.com/spf13/viper"
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Logging LoggingConfig `mapstructure:"logging"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
func LoadConfig(path string) (*Config, error) {
viper.SetConfigFile(path)
viper.AutomaticEnv()
viper.SetEnvPrefix("APP")
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
6. Graceful Shutdown
func main() {
r := chi.NewRouter()
// ... routes setup ...
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Démarrer le serveur
go func() {
slog.Info("Server starting on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("server error", "error", err)
os.Exit(1)
}
}()
// Attendre le signal d'arrêt
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("server forced to shutdown", "error", err)
os.Exit(1)
}
slog.Info("Server stopped")
}
7. Tests HTTP
7.1 httptest
func TestHandler(t *testing.T) {
tests := []struct {
name string
method string
path string
body string
wantStatus int
wantBody string
}{
{"get user", "GET", "/users/1", "", 200, `{"id":1}`},
{"not found", "GET", "/users/999", "", 404, `{"error":"not found"}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(tt.method, tt.path,
strings.NewReader(tt.body))
rec := httptest.NewRecorder()
handler(rec, req)
assert.Equal(t, tt.wantStatus, rec.Code)
assert.JSONEq(t, tt.wantBody, rec.Body.String())
})
}
}
7.2 Golden Files
func TestHandlerWithGolden(t *testing.T) {
req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()
handler(rec, req)
// Mettre à jour avec -update
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, rec.Body.Bytes(), 0644)
}
expected, _ := os.ReadFile(golden)
assert.Equal(t, string(expected), rec.Body.String())
}
Résumé
- net/http : Handler, ServeMux (Go 1.22+ patterns), ResponseWriter
- Frameworks : Chi (router), Gin, Echo, Fiber
- Templates : html/template, partials, FuncMap
- Middleware : logging, auth, recovery, CORS, rate limiting
- Configuration : Viper, envconfig
- Graceful shutdown : signaux OS, context timeout
- Tests : httptest, golden files