Skip to content
Inizia con 100 crediti di verifica gratuiti
Qualisend
Tutti gli articoli
Ingegneria / 15 giugno 2026

Come validare un indirizzo email in Go

7 min di lettura

Qualisend team
Come validare un indirizzo email in Go

Validare un indirizzo email in Go significa fare tre lavori sotto un solo nome. I tutorial che ti mostrano una regex e si fermano lì ne hanno risolto esattamente uno, e per giunta il meno utile. La vera validazione è a livelli: un controllo economico della sintassi, una ricerca DNS e un test SMTP della casella, partendo dal più economico. La libreria standard di Go copre i primi due in modo pulito con net/mail e net, senza alcuna dipendenza; il terzo è un problema di rete che conviene delegare. Questa guida costruisce ogni livello con codice Go funzionante e idiomatico e mostra esattamente dove subentra un'API di verifica.

In breve#

Usa net/mail.ParseAddress per la sintassi, net.LookupMX per la ricerca MX e un'API di verifica per il controllo SMTP della casella, in quest'ordine: si parte dal più economico e ci si ferma non appena un livello è risolutivo. Non ricorrere a net/smtp per testare le caselle dal tuo server: la porta 25 in uscita è bloccata dalla maggior parte degli host e, anche dove non lo è, il risultato dipende dalla reputazione dell'IP di invio e dal comportamento del greylisting, cose che non vuoi reimplementare. Ogni livello esclude indirizzi a un costo minore del successivo; solo l'ultimo può confermarne uno.

Livello 1: sintassi con net/mail#

In Go non serve una regex scritta a mano. Il pacchetto net/mail della libreria standard analizza gli indirizzi RFC 5322 e ParseAddress restituisce un errore non nil su qualsiasi input malformato, ed è proprio il segnale sì/no che serve a un filtro di sintassi:

package main

import "net/mail"

func isValidSyntax(email string) bool {
	if len(email) > 320 {
		return false
	}
	addr, err := mail.ParseAddress(email)
	return err == nil && addr.Address == email
}

Il confronto addr.Address == email è la parte che manca alla maggior parte degli esempi. ParseAddress è fatto per leggere una riga di indirizzo completa, quindi accetta senza problemi un nome visualizzato: mail.ParseAddress("Jane Doe <jane@example.com>") non restituisce errori e imposta addr.Address su jane@example.com. Per un campo di iscrizione vuoi solo l'indirizzo e nient'altro, quindi confronta l'Address analizzato con l'input e scarta tutto ciò che contiene un nome, parentesi angolari o spazi finali:

isValidSyntax("jane@example.com")            // true
isValidSyntax("Jane Doe <jane@example.com>") // false — not a bare address
isValidSyntax("not-an-email")                // false — ParseAddress errors
isValidSyntax("a@@b.com")                    // false

Un esito positivo qui significa "merita un controllo", non "valido". Ti dice che la stringa ha la forma di un indirizzo, ma nulla sul fatto che la posta arrivi. È lo stesso muro contro cui sbatte ogni controllo email basato su regex, e net/mail sta dalla stessa parte: è un parser, non un oracolo della deliverability. Ogni livello successivo dà per scontato che la sintassi sia già a posto.

Livello 2: il dominio può ricevere posta?#

Qui la libreria standard dà il meglio di sé, senza alcuna dipendenza. Un dominio che non pubblica record MX non ha dove far arrivare la posta, quindi una sola ricerca elimina domini morti, nomi di aziende scritti male e TLD inventati. net.LookupMX risolve i record MX:

package main

import "net"

func hasMailRoute(domain string) bool {
	mxs, err := net.LookupMX(domain)
	if err != nil {
		return false
	}
	return len(mxs) > 0
}
hasMailRoute("gmail.com")               // true
hasMailRoute("company-that-folded.com") // false

LookupMX restituisce una slice di *net.MX ordinata per preferenza, oppure un *net.DNSError quando il dominio non si risolve. Per un validatore, la lettura sicura è trattare qualsiasi errore (o una slice vuota) come "nessun instradamento della posta". Se vuoi gestire i domini che accettano posta su un record A senza un MX esplicito (il cosiddetto MX implicito), ripiega su net.LookupHost(domain) quando la slice degli MX torna vuota. Per la stragrande maggioranza degli indirizzi reali, però, un controllo MX è il filtro giusto.

Una nota operativa: LookupMX resta in attesa del tuo resolver DNS, quindi un nameserver lento o irraggiungibile blocca la goroutine. In un percorso di richiesta usa il resolver che accetta un context, (&net.Resolver{}).LookupMX(ctx, domain), con una scadenza, e tratta un timeout come "ricontrolla più tardi", non come un rifiuto definitivo, così un problema DNS passeggero non manda mai via un cliente reale.

Livello 3: la casella esiste davvero?#

I livelli 1 e 2 possono solo escludere un indirizzo. Un dominio può pubblicare record MX perfetti e non avere comunque alcuna casella all'indirizzo che hai in mano: noreply-9f2x@gmail.com ha una sintassi valida su un instradamento della posta attivo e non ha mai corrisposto a una casella reale. Confermare una casella specifica richiede la conversazione di recapito SMTP: connettersi all'host di posta, inviare RCPT TO, leggere la risposta e disconnettersi prima di spedire qualsiasi cosa.

Go include gli strumenti per provarci. net/smtp ti dà smtp.Dial e un Client con i metodi Mail, Rcpt e Close, e in linea di principio potresti gestire tu quello scambio. In pratica non dovresti eseguirlo dal tuo server applicativo. La maggior parte dei provider cloud blocca del tutto la porta 25 in uscita, quindi in produzione la connessione va semplicemente in timeout anche se sul tuo portatile funzionava. Dove la porta è aperta, il risultato dipende dalla reputazione dell'IP da cui ti connetti e i server riceventi applicano greylisting e limiti di frequenza ai mittenti sconosciuti, così un test ingenuo viene rinviato, rallentato o finisce in blacklist. E non basta un solo round-trip: i domini catch-all accettano ogni indirizzo e vanificano un singolo RCPT TO. Come funziona la verifica email ripercorre l'intero processo. Questo è il livello che conviene delegare.

Il controllo completo con un'API#

L'API in tempo reale di Qualisend esegue l'intero processo (sintassi, DNS e test SMTP della casella) da un'infrastruttura con reputazione gestita costruita apposta e restituisce un esito. È una semplice POST JSON, quindi ti bastano net/http e encoding/json della libreria standard. Decodifica l'involucro result direttamente in una struct:

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

type VerifyResult struct {
	Status   string          `json:"status"` // deliverable | risky | undeliverable | unknown
	Score    int             `json:"score"`
	Reason   string          `json:"reason"`
	SubFlags map[string]bool `json:"sub_flags"`
	SMTP     string          `json:"smtp"` // pending | cached | skipped
	JobID    string          `json:"-"`    // from the top-level job_id
}

type verifyResponse struct {
	JobID  string       `json:"job_id"`
	Result VerifyResult `json:"result"`
}

func verify(ctx context.Context, email string) (VerifyResult, error) {
	body, err := json.Marshal(map[string]string{"email": email})
	if err != nil {
		return VerifyResult{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://app.qualisend.com/api/v1/verify", bytes.NewReader(body))
	if err != nil {
		return VerifyResult{}, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("QUALISEND_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return VerifyResult{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return VerifyResult{}, fmt.Errorf("qualisend: unexpected status %s", res.Status)
	}

	var payload verifyResponse
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		return VerifyResult{}, fmt.Errorf("qualisend: decode response: %w", err)
	}
	payload.Result.JobID = payload.JobID
	return payload.Result, nil
}

Leggi la tua chiave con scope limitati dalla variabile d'ambiente QUALISEND_API_KEY invece di scriverla nel codice e controlla il riferimento API nella pagina per sviluppatori per la forma esatta di richiesta e risposta. L'oggetto result contiene uno status tra deliverable, risky, undeliverable e unknown, uno score numerico, un reason e una mappa sub_flags (indirizzo generico, usa e getta, provider di posta gratuita, catch-all e così via): sono i campi in base a cui deciderai davvero.

La risposta non aspetta il test della casella e SMTP ti dice che tipo di risposta hai. cached o skipped significano che il risultato è già definitivo. pending significa che status riflette i controlli locali, reason è ancora vuoto e l'esito SMTP arriverà sul job: GET https://app.qualisend.com/api/v1/jobs/{job_id}?include=results, con l'id in JobID. Interrogalo a intervalli di pochi secondi da un worker in background, mai dentro la richiesta di iscrizione, finché il job non è completed, canceled o failed, e non richiamare /verify per aggiornare lo stesso indirizzo.

Poiché verify accetta un context.Context, timeout e annullamento sono in mano al chiamante. Passa una scadenza, così una verifica lenta non blocca mai una richiesta di iscrizione:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

result, err := verify(ctx, "jane@example.com")
if err != nil {
	log.Printf("verify failed: %v", err)
}

Mettere tutto insieme per validare un indirizzo email in Go#

Parti dal più economico e fermati appena hai una risposta. I due livelli locali non costano nulla e scartano all'istante la maggior parte degli indirizzi spazzatura; la chiamata API viene eseguita solo sugli indirizzi che li hanno superati e che meritano il round-trip di rete:

package main

import (
	"context"
	"strings"
)

func validateEmail(ctx context.Context, email string) (VerifyResult, error) {
	if !isValidSyntax(email) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_email"}, nil
	}

	domain := email[strings.LastIndex(email, "@")+1:]
	if !hasMailRoute(domain) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_domain"}, nil
	}

	return verify(ctx, email)
}

Poi distingui i casi in base allo status, come serve al tuo prodotto: rifiuta gli indirizzi non recapitabili, lascia passare quelli recapitabili e decidi funzionalità per funzionalità cosa fare con gli esiti più incerti:

result, err := validateEmail(ctx, "jane@example.com")
if err != nil {
	// network/API failure — fail open or retry, don't block a real user
}

switch result.Status {
case "deliverable":
	// accept
case "undeliverable":
	// reject at the form
default:
	// risky | unknown — flag for review, or allow with a soft warning
}

Tutto il trucco sta in quest'ordine, ed è la stessa forma che trovi nelle versioni Node.js e Python di questa guida: è la struttura a livelli, non il linguaggio, a rendere affidabile la validazione. Leggi come funziona la verifica email per capire perché ogni fase sta dove sta.

Domande frequenti

01net/mail.ParseAddress basta per validare un indirizzo email in Go?

Per la sintassi, sì: net/mail.ParseAddress è il controllo giusto per il primo livello e una scelta molto migliore di una regex scritta a mano, perché analizza secondo la RFC 5322 e restituisce un errore sull'input malformato. Ma valida la forma, non la deliverability: non interroga mai il DNS né contatta un server di posta, quindi un errore nil significa "sembra un indirizzo", non "verrà recapitato". Confronta addr.Address con il tuo input per scartare i nomi visualizzati, poi abbina al controllo una ricerca MX e un test SMTP della casella.

02Come cerco i record MX in Go?

Usa net.LookupMX(domain) della libreria standard. Restituisce una slice di record *net.MX ordinata per preferenza, oppure un *net.DNSError quando il dominio non si risolve. Tratta un errore o una slice vuota come "nessun instradamento della posta" e, in un percorso di richiesta, usa (*net.Resolver).LookupMX(ctx, domain), che accetta un context, così un nameserver lento non può bloccare la goroutine oltre la tua scadenza.

03Posso verificare una casella in Go senza un servizio esterno?

In parte. net.LookupMX conferma che il dominio accetta posta, il che esclude gratis i domini morti e non richiede altro che la libreria standard. Confermare la casella vera e propria richiede una conversazione SMTP, che puoi scrivere con net/smtp ma che non dovresti eseguire dal tuo server applicativo: la porta 25 è bloccata quasi ovunque e il risultato dipende dalla reputazione del tuo IP di invio. È il livello per cui esiste un servizio di verifica; puoi vedere la differenza su qualsiasi indirizzo con la verifica email gratuita.

04Devo validare le email all'iscrizione o quando pulisco una lista?

Entrambe le cose, con una profondità diversa. Esegui sintassi e ricerca MX in modo sincrono all'iscrizione (sono abbastanza veloci da bloccare la richiesta e dare un riscontro immediato) e usa lì anche l'esito immediato dell'API. Riserva il lavoro più approfondito, confermato via SMTP, alla pulizia delle liste e ai job di back-office; la guida all'iscrizione serverless mostra lo schema in tempo reale dall'inizio alla fine.


Vuoi aggiungere il livello SMTP al tuo servizio Go? Il riferimento API di Qualisend contiene l'endpoint /verify, le chiavi con scope limitati ed esempi pronti da copiare, e la verifica email gratuita ti permette di veder tornare come undeliverable un indirizzo sintatticamente perfetto prima ancora di scrivere una riga di codice.

La tua reputazione, protetta.

Pulisci la tua prima lista in pochi minuti. 100 crediti gratuiti, senza carta di credito.

Inizia