Validar um endereço de e-mail em Go são três tarefas usando um único nome. Os
tutoriais que mostram uma regex e param por aí resolveram exatamente uma delas — e
a menos útil, ainda por cima. A validação de verdade é feita em camadas: uma
verificação de sintaxe barata, uma consulta de DNS e uma sondagem de caixa postal
via SMTP, da mais barata para a mais cara. A biblioteca padrão do Go cobre as duas
primeiras de forma limpa com net/mail e net, sem dependência alguma; a terceira
é um problema de rede que vale a pena delegar. Este guia constrói cada camada com
Go funcional e idiomático e mostra exatamente onde uma API de verificação assume o controle.
A resposta curta#
Use net/mail.ParseAddress para a sintaxe, net.LookupMX para a consulta de MX e
uma API de verificação para a checagem da caixa postal via SMTP — nessa ordem, da
mais barata para a mais cara, interrompendo assim que uma camada for decisiva. Não
recorra ao net/smtp para sondar caixas postais a partir do seu próprio servidor: a
porta 25 de saída é bloqueada na maioria dos hosts e, mesmo onde não é, a resposta
depende da reputação do IP de envio e de comportamentos de greylisting que você não
vai querer reimplementar. Cada camada descarta endereços de forma mais barata do
que a anterior; só a última consegue aprovar um endereço.
Camada 1: sintaxe com net/mail#
O Go não precisa de uma regex feita à mão aqui. O pacote net/mail da biblioteca
padrão faz o parsing de endereços conforme a RFC 5322, e o ParseAddress retorna um
erro não nil em qualquer coisa malformada — que é exatamente o sinal de sim/não que
um portão de sintaxe precisa:
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
}
A comparação addr.Address == email é a parte que a maioria dos exemplos ignora.
O ParseAddress foi feito para ler uma linha completa de caixa postal, então ele
aceita sem problemas um nome de exibição: mail.ParseAddress("Jane Doe <jane@example.com>")
não retorna erro, com addr.Address definido como jane@example.com. Para um campo
de cadastro, você quer o endereço puro e nada mais, então compare o Address obtido
de volta com a entrada e rejeite qualquer coisa que traga um nome, colchetes angulares
ou espaços em branco no final:
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
Passar aqui significa "vale a pena verificar", não "válido". Diz que a string tem o
formato de um endereço; não diz nada sobre se o e-mail vai chegar. É a mesma parede
em que toda verificação de e-mail baseada em regex
esbarra, e o net/mail fica do mesmo lado
dela — é um parser, não um oráculo de entregabilidade. Toda camada abaixo pressupõe
que a sintaxe já está sã.
Camada 2: o domínio consegue receber e-mail?#
É aqui que a biblioteca padrão prova o seu valor com zero dependências. Um domínio
que não publica registros MX não tem onde receber o e-mail, então uma única consulta
elimina domínios mortos, nomes de empresas escritos errado e TLDs inventados.
O net.LookupMX os resolve:
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
O LookupMX retorna um slice de *net.MX ordenado por preferência, ou um *net.DNSError
quando o domínio não resolve. Tratar qualquer erro — ou um slice vazio — como "sem
rota de e-mail" é a leitura segura para um validador. Se você quiser contemplar
domínios que aceitam e-mail em um registro A sem MX explícito (o chamado MX
implícito), recorra ao net.LookupHost(domain) quando o slice de MX voltar vazio.
Para a esmagadora maioria dos endereços reais, no entanto, uma verificação de MX é o filtro certo.
Uma observação operacional: o LookupMX bloqueia no seu resolver de DNS, então um
servidor de nomes lento ou inacessível trava a goroutine. Em um caminho de
requisição, use o resolver sensível a contexto — (&net.Resolver{}).LookupMX(ctx, domain) — com
um prazo, e trate um timeout como "verificar depois", não como uma rejeição
definitiva, para que um soluço temporário de DNS nunca afaste um cliente de verdade.
Camada 3: a caixa postal realmente existe?#
As camadas 1 e 2 só conseguem descartar um endereço. Um domínio pode publicar
registros MX perfeitos e ainda assim não ter caixa postal alguma no endereço que
você tem em mãos — noreply-9f2x@gmail.com tem sintaxe válida em uma rota de e-mail
ativa e nunca foi uma caixa postal de verdade. Confirmar uma caixa postal específica
exige a conversa de entrega do SMTP: conectar ao host de e-mail, emitir RCPT TO,
ler a resposta e desconectar antes de enviar qualquer coisa.
O Go já vem com as ferramentas para tentar. O net/smtp oferece o smtp.Dial e um
Client com os métodos Mail, Rcpt e Close, e você poderia, em princípio,
conduzir esse handshake por conta própria. Na prática, você não deveria executá-lo a
partir do seu servidor de aplicação. A maioria dos provedores de nuvem bloqueia a
porta 25 de saída por completo, então a conexão simplesmente expira em produção mesmo
quando funcionou no seu notebook. Onde a porta está aberta, a resposta depende da
reputação do IP de onde você conecta, e os servidores de recebimento aplicam
greylisting e limitam a taxa de remetentes desconhecidos — então uma sondagem ingênua
acaba adiada, estrangulada ou colocada em blocklist. E tem mais do que uma única ida
e volta: domínios catch-all aceitam todos os endereços e derrotam um único RCPT TO.
Como funciona a verificação de e-mail percorre
o pipeline completo. Essa é a camada que vale a pena delegar.
Fazendo a verificação completa com uma API#
A API em tempo real da Qualisend executa o pipeline inteiro — sintaxe, DNS e a
sondagem de caixa postal via SMTP — a partir de uma infraestrutura com reputação
gerenciada, feita para isso, e devolve um veredito. É um simples POST em JSON, então
net/http e encoding/json da biblioteca padrão são tudo o que você precisa.
Decodifique o envelope result direto para uma 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"`
}
type verifyResponse struct {
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://api.qualisend.com/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)
}
return payload.Result, nil
}
O endpoint e a chave acima são apenas exemplos — leia a sua chave com escopo a partir
da variável de ambiente QUALISEND_API_KEY em vez de deixá-la fixa no código, e
consulte a referência da API na página de desenvolvedores para o
formato exato da requisição e da resposta. O objeto result traz um status de
deliverable, risky, undeliverable ou unknown, um score numérico, um
reason e um mapa sub_flags (endereço de função, descartável, provedor gratuito,
catch-all e afins) — os campos sobre os quais você de fato ramifica a lógica.
Como verify recebe um context.Context, quem chama controla o timeout e o
cancelamento. Passe um prazo para que uma verificação lenta nunca trave uma
requisição de cadastro:
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)
}
Juntando tudo para validar um endereço de e-mail em Go#
Da mais barata para a mais cara, parando assim que você tiver uma resposta. As duas camadas locais não custam nada e rejeitam a maior parte do lixo na hora; a chamada de API roda apenas nos endereços que sobreviveram a elas e que valem a ida e volta pela rede:
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)
}
Depois, ramifique a lógica conforme o status da forma que o seu produto precisa — rejeite os undeliverable, deixe passar os deliverable e decida, por funcionalidade, o que fazer com os vereditos mais nebulosos:
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
}
Essa ordenação é o truque inteiro, e é o mesmo formato que você vai encontrar nas versões em Node.js e Python deste guia — o que torna a validação confiável são as camadas, não a linguagem. Veja como funciona a verificação de e-mail para entender por que cada etapa fica onde fica.
Perguntas frequentes#
O net/mail.ParseAddress é suficiente para validar um endereço de e-mail em Go?#
Para a sintaxe, sim — o net/mail.ParseAddress é a verificação de primeira camada
correta e uma aposta muito melhor do que uma regex feita à mão, já que ele faz o
parsing conforme a RFC 5322 e retorna erro em entradas malformadas. Mas ele valida o
formato, não a entregabilidade: nunca resolve DNS nem contata um servidor de
e-mail, então um erro nil significa "parece um endereço", não "vai ser entregue".
Compare o addr.Address de volta com a sua entrada para rejeitar nomes de exibição
e, em seguida, combine a verificação com uma consulta de MX e uma sondagem de caixa
postal via SMTP.
Como faço para consultar registros MX em Go?#
Use o net.LookupMX(domain) da biblioteca padrão. Ele retorna um slice de
*net.MX ordenados por preferência, ou um *net.DNSError quando o domínio não
resolve. Trate um erro ou um slice vazio como "sem rota de e-mail" e, em um caminho de
requisição, use a versão sensível a contexto (*net.Resolver).LookupMX(ctx, domain)
para que um servidor de nomes lento não trave a goroutine além do seu prazo.
É possível verificar uma caixa postal em Go sem um serviço externo?#
Em parte. O net.LookupMX confirma que o domínio aceita e-mails, o que descarta
domínios mortos de graça e não precisa de nada além da biblioteca padrão. Confirmar a
caixa postal em si exige uma conversa SMTP, que você pode programar com net/smtp,
mas não deveria executar a partir do seu servidor de aplicação — a porta 25 é
amplamente bloqueada e o resultado depende da reputação do seu IP de envio. Essa é a
camada que um serviço de verificação existe para resolver; você pode ver a diferença
em qualquer endereço com o verificador de e-mail gratuito.
Devo validar e-mails no cadastro ou ao limpar uma lista?#
Ambos, em profundidades diferentes. Execute a sintaxe e a consulta de MX de forma síncrona no cadastro — elas são rápidas o suficiente para bloquear a requisição e dar feedback instantâneo — e aja também sobre o veredito imediato da API ali mesmo. Reserve o trabalho mais profundo confirmado por SMTP para a limpeza de listas e tarefas de back-office; o guia de cadastro serverless mostra o padrão em tempo real de ponta a ponta.
Pronto para adicionar a camada SMTP ao seu serviço em Go? A
referência da API da Qualisend tem o endpoint /verify com chaves de
escopo e exemplos para copiar e colar, e o
verificador de e-mail gratuito permite que você veja um
endereço sintaticamente perfeito voltar como undeliverable antes de você escrever
uma linha de código.