Transparência: sou fundador da CPFHub, uma das APIs citadas no final. O pacote de validação é independente.
Go é ótimo para serviços de backend que recebem cadastro em alto volume. Vamos escrever um pacote cpf pequeno, sem dependências, e um cliente HTTP que respeita context, timeout e limites de taxa.
1. O pacote
package cpf
import "strings"
// Normalize remove tudo que não for dígito ASCII.
func Normalize(s string) string {
var b strings.Builder
for _, r := range s {
if r >= '0' && r <= '9' {
b.WriteRune(r)
}
}
return b.String()
}
// Valid confere tamanho, sequência repetida e dígitos verificadores.
func Valid(s string) bool {
c := Normalize(s)
if len(c) != 11 || strings.Count(c, c[:1]) == 11 {
return false
}
for t := 9; t < 11; t++ {
sum := 0
for i := 0; i < t; i++ {
sum += int(c[i]-'0') * (t + 1 - i)
}
if int(c[t]-'0') != sum*10%11%10 {
return false
}
}
return true
}
// Format aplica a máscara 000.000.000-00.
func Format(s string) string {
c := Normalize(s)
if len(c) != 11 {
return s
}
return c[:3] + "." + c[3:6] + "." + c[6:9] + "-" + c[9:]
}
2. Testes table-driven
func TestValid(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"529.982.247-25", true},
{"52998224725", true},
{"11111111111", false},
{"52998224724", false},
{"", false},
}
for _, c := range cases {
if got := Valid(c.in); got != c.want {
t.Errorf("Valid(%q) = %v, want %v", c.in, got, c.want)
}
}
}
Vale adicionar um fuzz test (go test -fuzz) garantindo que Valid nunca entra em pânico com entrada arbitrária.
3. Cliente HTTP
Depois que o DV passa, uma API externa confirma se o CPF existe. Um cliente idiomático:
type Data struct {
CPF string `json:"cpf"`
Name string `json:"name"`
Gender string `json:"gender"`
BirthDate string `json:"birthDate"`
}
var ErrNotFound = errors.New("cpf não encontrado")
type RateLimitError struct{ RetryAfter string }
func (e RateLimitError) Error() string { return "limite por minuto; retry-after " + e.RetryAfter }
type Client struct {
HTTP *http.Client
APIKey string
}
func (c *Client) Lookup(ctx context.Context, raw string) (*Data, error) {
n := cpf.Normalize(raw)
if !cpf.Valid(n) {
return nil, errors.New("cpf inválido")
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.cpfhub.io/cpf/"+n, nil)
req.Header.Set("x-api-key", c.APIKey)
res, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
var body struct {
Success bool `json:"success"`
Data Data `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
return nil, err
}
return &body.Data, nil
case http.StatusNotFound:
return nil, ErrNotFound
case http.StatusTooManyRequests:
return nil, RateLimitError{res.Header.Get("Retry-After")}
default:
return nil, fmt.Errorf("status %d", res.StatusCode)
}
}
Uso:
c := &Client{HTTP: &http.Client{Timeout: 10 * time.Second}, APIKey: os.Getenv("CPFHUB_API_KEY")}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
d, err := c.Lookup(ctx, "529.982.247-25")
switch {
case errors.Is(err, ErrNotFound):
// trate como resultado normal
case err != nil:
var rl RateLimitError
if errors.As(err, &rl) { /* reagende */ }
}
4. Controlando a taxa do seu lado
Toda API tem limite por minuto. Em vez de descobrir isso tomando 429, limite antes com golang.org/x/time/rate:
lim := rate.NewLimiter(rate.Every(2*time.Second), 1) // 30/min
for _, doc := range docs {
if err := lim.Wait(ctx); err != nil { break }
go process(doc)
}
Para listas grandes, prefira um endpoint de lote se o provedor tiver: uma requisição em vez de milhares.
5. Dicas
- Nunca logue o CPF completo; use
Formatcom mascaramento (***.982.247-**) nos logs. - Use
errors.Is/Asem vez de comparar strings de erro. - Não reutilize
http.DefaultClient: ele não tem timeout.
Sobre a CPFHub
O cliente acima já está apontado para a CPFHub: GET /cpf/{cpf} com x-api-key, ~150 ms típicos (não SLA), 404 sem custo, e um POST /cpf/bulk para listas de até 10.000 CPFs. O plano gratuito tem 50 créditos por mês. Mais na documentação.
Top comments (0)