Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 15 de junho de 2026

Como validar um endereço de e-mail em Go

8 minutes read

Qualisend team
Um diagrama em camadas de um endereço de e-mail em Go passando pela validação de sintaxe do net/mail.ParseAddress, por uma consulta de MX com net.LookupMX e por uma sondagem de caixa postal via SMTP, afunilando até um único veredito de entregabilidade

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started