Sommaire (6 parties)
La garde tenait en une ligne : if perSecond <= 0 { return erreur }.
Une garde est un test à l'entrée d'une fonction, qui accepte ou refuse une valeur.
Celle-ci refuse zéro, elle refuse les négatifs, elle a l'air complète.
Puis une question idiote : et si le débit n'était pas un nombre ?
J'ai écrit NaN (« pas un nombre ») dans la configuration.
Puis j'ai envoyé 100 requêtes d'un coup à un limiteur réglé sur 5 par seconde.
Les 100 sont passées. Pas une erreur, pas une ligne de log.
Ce limiteur est un rate limiter : un portier. Il compte les requêtes d'un client (les appels reçus) et bloque celles qui dépassent un débit fixé. Le coupable n'est pas la bibliothèque, c'est un réflexe. Avec des entiers, « refuser zéro et les négatifs » suffit. Je l'avais appliqué à un flottant, un nombre à virgule, qui peut valoir NaN.
En Go (le langage), on l'écrit souvent avec la bibliothèque golang.org/x/time/rate. Elle repose sur un seau de jetons : chaque requête retire un jeton, et sans jeton elle est bloquée.
Le seau se remplit au rythme du débit, un nombre à virgule. La rafale est la taille du seau, plein au départ : avec 3, un client peut envoyer 3 requêtes d'un coup.
Cette garde vérifie le débit, le paramètre perSecond.
Deux écritures qui paraissent équivalentes ne se comportent pas pareil devant NaN. La suite le montre.
La garde qui a l'air complète
Le premier réflexe est de refuser zéro et les négatifs. Zéro est dangereux, mais à l'inverse de NaN : d'après le code de la bibliothèque, un débit nul ne recharge jamais le seau. Le client épuise sa rafale, puis tout est bloqué. La garde naïve :
func New(perSecond float64, burst int) (*rate.Limiter, error) {
if perSecond <= 0 || burst <= 0 {
return nil, fmt.Errorf("rate must be positive, got %v req/s, burst %d", perSecond, burst)
}
return rate.NewLimiter(rate.Limit(perSecond), burst), nil
}
Elle refuse 0 et −1 et se relit sans effort.
Le débit vient d'un texte, lu dans une variable d'environnement (un réglage lu au démarrage).
La fonction strconv.ParseFloat le convertit en flottant (float64 en Go), le nom d'un nombre à virgule.
Un flottant peut valoir NaN, une valeur spéciale qui veut dire « pas un nombre ».
ParseFloat rend le nombre et une erreur, mais tester l'erreur ne suffit pas. J'ai regardé ce qu'elle accepte (Go 1.26) :
"NaN"et"nan"donnent NaN, sans erreur ;"Inf"et"+Inf"donnent+Inf(l'infini positif), sans erreur ;"1e400"(1 suivi de 400 zéros, trop grand pour unfloat64) donne+Inf, avec une erreur.
Donc un texte « nan » en configuration donne un NaN, sans la moindre erreur.
J'ai essayé caarlos0/env (v11.4.1), une bibliothèque de configuration qui lit ces variables à ta place.
Le débit (RATE_PER_SECOND) est un flottant lu par ParseFloat. La rafale (RATE_BURST) est un entier, un nombre sans virgule, lu par ParseInt.
Voici ce qu'elle rend quand je lui donne NaN ou Inf :
RATE_PER_SECOND=NaN RATE_BURST=3 -> {PerSecond:NaN Burst:3} err=<nil>
RATE_PER_SECOND=5 RATE_BURST=NaN -> {PerSecond:5 Burst:0} err=env: parse error on field "Burst" of type "int": strconv.ParseInt: parsing "NaN": invalid syntax
RATE_PER_SECOND=Inf RATE_BURST=3 -> {PerSecond:+Inf Burst:3} err=<nil>
Pour la rafale, la lecture renvoie une erreur : un entier ne peut jamais valoir NaN, on le voit plus bas. Pour le débit, tout passe sans bruit.
Que fait la garde naïve d'un NaN ?
NaN : une combinaison de bits qui n'est pas un nombre
Tout vient de la façon dont chaque type est rangé en mémoire, en bits (des 0 et des 1).
Un entier de 64 bits (int64) utilise ses 64 bits pour écrire un nombre.
Il y a 264 combinaisons de 0 et de 1, et chacune est un nombre précis.
Aucune combinaison n'est libre pour dire « ceci n'est pas un nombre » : un entier ne vaut donc jamais NaN.
Un float64 suit la norme IEEE 754, qui réserve des combinaisons de bits à l'infini et à NaN (Not a Number).
Pourquoi ? Un calcul sur des flottants peut n'avoir aucun résultat, comme 0 divisé par 0 (j'ai mesuré en Go, avec des variables : NaN). NaN le dit, plutôt que de faire planter le programme.
Le piège : si une variable n vaut NaN, toutes les comparaisons d'ordre et d'égalité répondent faux, même n == n. Seul != répond vrai.
n := math.NaN()
fmt.Println(n == n, n > 0, n <= 0) // false false false
fmt.Println(n != n, math.IsNaN(n)) // true true
n > 0 et n <= 0 sont faux tous les deux, car NaN n'est ni positif ni négatif.
La garde naïve demande « perSecond <= 0 : le débit est-il négatif ou nul ? ». Pour NaN la réponse est non, donc elle accepte la valeur.
Posée dans l'autre sens, la condition devient !(perSecond > 0) : « le débit n'est pas plus grand que zéro » (le ! veut dire « non »). « Plus grand que zéro » est faux pour NaN, donc la condition est vraie et la valeur est refusée.
Ce que ça fait au limiteur : plus rien n'est limité
Voici un programme que tu peux lancer. Il lit « NaN » avec ParseFloat.
Puis il envoie 100 requêtes au même instant, sans temps pour recharger le seau. Deux limiteurs les reçoivent : un de débit 5, un de débit NaN. La rafale est de 3.
package main
import (
"fmt"
"math"
"strconv"
"time"
"golang.org/x/time/rate"
)
func main() {
v, err := strconv.ParseFloat("NaN", 64)
fmt.Println(v, err, v <= 0, !(v > 0))
now := time.Now()
for _, perSecond := range []float64{5, math.NaN()} {
l := rate.NewLimiter(rate.Limit(perSecond), 3)
allowed := 0
for range 100 {
if l.AllowN(now, 1) {
allowed++
}
}
fmt.Printf("rate %v: %d of 100 allowed\n", perSecond, allowed)
}
}
NaN <nil> false true
rate 5: 3 of 100 allowed
rate NaN: 100 of 100 allowed
Dans la première ligne, la valeur lue (v) est NaN, sans erreur (nil veut dire « aucune »). Puis <= 0 est faux, donc la garde naïve accepte NaN, et !(v > 0) est vrai, donc l'autre écriture le refuse.
Dans les deux suivantes, un débit de 5 laisse passer 3 requêtes sur 100 (la rafale), un débit NaN les laisse passer toutes.
Le tableau ajoute les débits 0 et +Inf, mesurés en local avec golang.org/x/time v0.16.0.
| Débit | Garde naïve <= 0 |
Requêtes autorisées sur 100 (débit passé tel quel) | Garde finale (section suivante) |
|---|---|---|---|
| 5 | valeur acceptée | 3 | valeur acceptée |
| 0 | valeur refusée | 3 | valeur refusée |
| NaN | valeur acceptée | 100 | valeur refusée |
| +Inf | valeur acceptée | 100 | valeur refusée |
Avec NaN, tout passe. Voici pourquoi, dans l'ordre où ça se produit. À chaque appel, le limiteur ajoute « temps écoulé × débit » jetons au seau, sans dépasser sa taille, puis retire celui de la requête. Si le nombre de jetons passe sous zéro, il n'y en avait plus : la requête est bloquée. Avec un débit NaN, l'ajout vaut NaN, puisque toute opération arithmétique (+, −, ×, ÷) qui touche un NaN donne NaN. Le nombre de jetons devient NaN, la comparaison « sous zéro ? » répond faux, et la requête passe.
+Inf donne aussi 100 sur 100, par un autre chemin que je ne détaille pas ici.
Le pire, c'est le silence. Le service démarre normalement, et la bibliothèque n'affiche aucun message. Une limite qui disparaît ne casse rien tant que personne n'abuse. Un test (un petit programme qui vérifie le code) qui n'essaie que des valeurs normales ne la voit pas non plus.
Je ne suis pas le seul à passer le débit tel quel au limiteur. Le middleware de rate limiting d'echo (v4.13.4), un code exécuté avant chaque requête dans ce framework web Go, le fait ligne 244 de rate_limiter.go.
Rien n'y vérifie le débit avant, donc c'est à toi de refuser un NaN en amont. L'effet exact d'un NaN dans echo, je ne l'ai pas testé.
Écrire ce qui est valide, refuser le reste
La correction tient en un principe : au lieu de lister ce qu'on refuse, on écrit ce qu'on accepte.
Un débit valide est un nombre strictement positif, et assez petit pour être une vraie limite.
Donc ni NaN, ni infini, ni la plus grande valeur d'un float64.
La garde finale :
func New(perSecond float64, burst int) (*rate.Limiter, error) {
valid := perSecond > 0 && perSecond < math.MaxFloat64
if !valid || burst <= 0 {
return nil, fmt.Errorf("rate must be a finite positive number, got %v req/s, burst %d", perSecond, burst)
}
return rate.NewLimiter(rate.Limit(perSecond), burst), nil
}
perSecond > 0 est faux pour NaN, donc la garde finale le refuse sans cas particulier.
perSecond < math.MaxFloat64 refuse +Inf, et aussi math.MaxFloat64 lui-même : la bibliothèque le nomme rate.Inf et en fait « aucune limite ».
Dans un vrai service, je préférerais une borne métier, par exemple perSecond <= 100_000.
Personne n'a besoin de 10308 requêtes par seconde, et ça se lit tout seul.
Le test de la garde finale tient en quelques lignes :
func TestNewRefusesRatesThatAreNotUsable(t *testing.T) {
for _, perSecond := range []float64{0, -1, math.NaN(), math.Inf(1), math.Inf(-1), math.MaxFloat64} {
if _, err := New(perSecond, 3); err == nil {
t.Errorf("rate %v accepted, want an error", perSecond)
}
}
}
J'ai vérifié qu'il sert à quelque chose. Avec la garde naïve à la place, New ne renvoie plus d'erreur pour NaN, et le test échoue, comme voulu.
Il affiche rate NaN accepted, want an error.
Un test qui ne casse jamais ne prouve rien.
Conclusion
Avec des entiers, « refuser zéro et les négatifs » suffit, parce que tout entier est négatif, nul ou positif. Un flottant a une valeur qui n'est ni négative, ni nulle, ni positive : NaN. Mon code raisonnait comme sur des entiers, et ce réflexe se casse en silence sur un flottant.
Mon avis : une garde doit décrire ce qu'on accepte, jamais ce qu'on refuse. Refuser une liste de cas, c'est parier qu'on les a tous pensés. Son test doit contenir les valeurs que seul le flottant possède : NaN, les infinis et le plus grand flottant. Pose-la au démarrage : un service qui refuse de démarrer se remarque, un limiteur muet non. Pour le mécanisme du seau de jetons lui-même, je l'ai détaillé dans Rate limiter en Go : token bucket par IP en pratique. Pour l'ordre et les pièges des middlewares en production, voir Middleware Go en production : best practices, ordre et pièges réels.
Sources
Documentation consultée le 2 novembre 2026 :
golang.org/x/time/rate, limiteur à seau de jetons : pkg.go.dev/golang.org/x/time/ratestrconv.ParseFloat, valeurs acceptées : pkg.go.dev/strconv#ParseFloatmath.NaN,math.MaxFloat64: pkg.go.dev/math#NaN- Lecture de variables d'environnement,
caarlos0/envv11.4.1 : pkg.go.dev/github.com/caarlos0/env/v11 - Middleware de rate limiting d'echo : echo.labstack.com/docs/middleware/rate-limiter
- Code du middleware d'echo (v4.13.4) : github.com/labstack/echo, middleware/rate_limiter.go
- IEEE 754, format des flottants : en.wikipedia.org/wiki/IEEE_754