Validar una dirección de correo en Go son tres tareas bajo un mismo nombre. Los
tutoriales que te enseñan una expresión regular y ahí se quedan han resuelto
exactamente una de ellas, y encima la menos útil. La validación de verdad va por
capas: una comprobación de sintaxis barata, una consulta DNS y un sondeo SMTP del
buzón, de la más barata a la más cara. La biblioteca estándar de Go cubre las dos
primeras con soltura mediante net/mail y net, sin ninguna dependencia; la
tercera es un problema de red que conviene delegar. Esta guía construye cada capa
con Go funcional e idiomático y muestra exactamente dónde toma el relevo una API
de verificación.
La respuesta corta#
Usa net/mail.ParseAddress para la sintaxis, net.LookupMX para la consulta MX y
una API de verificación para la comprobación SMTP del buzón, en ese orden, de la
más barata a la más cara, cortando en cuanto una capa sea concluyente. No recurras
a net/smtp para sondear buzones desde tu propio servidor: el puerto 25 saliente
está bloqueado en la mayoría de los hosts y, donde no lo está, la respuesta depende
de la reputación de la IP de envío y del comportamiento de greylisting que no
querrás reimplementar. Cada capa descarta direcciones de forma más barata que la
anterior; solo la última puede dar por buena una dirección.
Capa 1: sintaxis con net/mail#
Aquí Go no necesita ninguna expresión regular hecha a mano. El paquete net/mail
de la biblioteca estándar analiza direcciones según la RFC 5322, y ParseAddress
devuelve un error no nil ante cualquier cosa mal formada, que es justo la señal de
sí/no que necesita un filtro de sintaxis:
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
}
La comparación addr.Address == email es la parte que se saltan la mayoría de los
ejemplos. ParseAddress está diseñado para leer una línea de buzón completa, así
que acepta encantado un nombre visible: mail.ParseAddress("Jane Doe <jane@example.com>")
no devuelve ningún error, con addr.Address fijado a jane@example.com. Para un
campo de registro quieres la dirección desnuda y nada más, así que compara la
Address analizada con la entrada original y rechaza todo lo que arrastre un
nombre, corchetes angulares o espacios finales:
isValidSyntax("jane@example.com") // true
isValidSyntax("Jane Doe <jane@example.com>") // false — no es una dirección desnuda
isValidSyntax("not-an-email") // false — ParseAddress da error
isValidSyntax("a@@b.com") // false
Un aprobado aquí significa "vale la pena comprobarla", no "válida". Te dice que la
cadena tiene forma de dirección; no dice nada sobre si el correo llegará. Es el
mismo muro contra el que choca cualquier comprobación de correo basada en
regex, y net/mail está en el mismo lado
de ese muro: es un analizador, no un oráculo de entregabilidad. Todas las capas de
abajo dan por hecho que la sintaxis ya es correcta.
Capa 2: ¿puede el dominio recibir correo?#
Aquí es donde la biblioteca estándar se gana el sueldo sin ninguna dependencia. Un
dominio que no publica registros MX no tiene dónde recibir el correo, así que una
sola consulta elimina dominios muertos, nombres de empresa mal escritos y TLD
inventados. net.LookupMX los resuelve:
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 devuelve un slice de *net.MX ordenado por preferencia, o un
*net.DNSError cuando el dominio no resuelve. Tratar cualquier error, o un slice
vacío, como "sin ruta de correo" es la lectura segura para un validador. Si
quieres tener en cuenta los dominios que aceptan correo en un registro A sin MX
explícito (el llamado MX implícito), recurre a net.LookupHost(domain) cuando el
slice de MX vuelva vacío. Para la inmensa mayoría de las direcciones reales, sin
embargo, una comprobación MX es el filtro adecuado.
Una nota operativa: LookupMX se bloquea esperando a tu resolvedor DNS, así que un
servidor de nombres lento o inaccesible deja la goroutine parada. En una ruta de
petición, usa el resolvedor que reconoce el contexto —(&net.Resolver{}).LookupMX(ctx, domain)—
con un límite de tiempo, y trata un timeout como "comprobar más tarde", no como un
rechazo tajante, para que un contratiempo transitorio del DNS nunca eche a un
cliente real.
Capa 3: ¿existe realmente el buzón?#
Las capas 1 y 2 solo pueden descartar una dirección. Un dominio puede publicar
registros MX perfectos y aun así no tener ningún buzón en la dirección que tienes
entre manos: noreply-9f2x@gmail.com es sintaxis válida en una ruta de correo
activa y nunca fue un buzón real. Confirmar un buzón concreto implica la
conversación de entrega SMTP: conectarse al host de correo, emitir RCPT TO, leer
la respuesta y desconectar antes de enviar nada.
Go trae las herramientas para intentarlo. net/smtp te da smtp.Dial y un
Client con los métodos Mail, Rcpt y Close, y en principio podrías conducir
ese intercambio tú mismo. En la práctica no deberías ejecutarlo desde tu servidor
de aplicaciones. La mayoría de los proveedores en la nube bloquean del todo el
puerto 25 saliente, así que la conexión simplemente agota el tiempo en producción
aunque funcionara en tu portátil. Donde el puerto está abierto, la respuesta
depende de la reputación de la IP desde la que te conectas, y los servidores
receptores aplican greylisting y limitan la tasa a los remitentes desconocidos, de
modo que un sondeo ingenuo se aplaza, se estrangula o acaba en una lista de
bloqueo. Y hay más que un solo ida y vuelta: los dominios catch-all aceptan todas
las direcciones y derrotan a un único RCPT TO.
Cómo funciona la verificación de correo
recorre el proceso completo. Esta es la capa que merece la pena delegar.
Hacer la comprobación completa con una API#
La API en tiempo real de Qualisend ejecuta todo el proceso —sintaxis, DNS y el
sondeo SMTP del buzón— desde una infraestructura con reputación gestionada creada
para ello, y devuelve un veredicto. Es un simple POST en JSON, así que net/http y
encoding/json de la biblioteca estándar son todo lo que necesitas. Descodifica el
sobre result directamente en un 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
}
El endpoint y la clave de arriba son marcadores de posición: lee tu clave con
alcance limitado desde la variable de entorno QUALISEND_API_KEY en lugar de
codificarla directamente, y consulta la referencia de la API en la página de
desarrolladores para conocer la forma exacta de la petición y la
respuesta. El objeto result lleva un status de deliverable, risky,
undeliverable o unknown, un score numérico, un reason y un mapa sub_flags
(dirección de rol, desechable, proveedor gratuito, catch-all y demás): los campos
sobre los que de verdad tomas decisiones.
Como verify recibe un context.Context, quien lo llama es el dueño del timeout y
la cancelación. Pasa un límite de tiempo para que una verificación lenta nunca deje
colgada una petición de registro:
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)
}
Juntándolo todo para validar una dirección de correo en Go#
De la más barata a la más cara, y para en cuanto tengas una respuesta. Las dos capas locales no cuestan nada y rechazan la mayor parte de la basura al instante; la llamada a la API se ejecuta solo con las direcciones que las han superado y que merecen el ida y vuelta por la red:
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)
}
Después, ramifica según el estado como tu producto necesite: rechaza las no entregables, deja pasar las entregables y decide función por función qué hacer con los veredictos más grises:
result, err := validateEmail(ctx, "jane@example.com")
if err != nil {
// fallo de red/API — falla abierto o reintenta, no bloquees a un usuario real
}
switch result.Status {
case "deliverable":
// aceptar
case "undeliverable":
// rechazar en el formulario
default:
// risky | unknown — marcar para revisión, o permitir con un aviso suave
}
Ese orden es todo el truco, y es la misma forma que encontrarás en las versiones Node.js y Python de esta guía: lo que hace fiable la validación es el trabajo por capas, no el lenguaje. Consulta cómo funciona la verificación de correo para entender por qué cada etapa está donde está.
Preguntas frecuentes#
¿Basta con net/mail.ParseAddress para validar una dirección de correo en Go?#
Para la sintaxis, sí: net/mail.ParseAddress es la comprobación correcta de
primera capa y una apuesta mucho mejor que una expresión regular hecha a mano, ya
que analiza según la RFC 5322 y da error con entradas mal formadas. Pero valida la
forma, no la entregabilidad: nunca resuelve DNS ni contacta con un servidor de
correo, así que un error nil significa "parece una dirección", no "se entregará".
Compara addr.Address con tu entrada original para rechazar los nombres visibles
y, después, combina la comprobación con una consulta MX y un sondeo SMTP del buzón.
¿Cómo consulto los registros MX en Go?#
Usa net.LookupMX(domain) de la biblioteca estándar. Devuelve un slice de registros
*net.MX ordenados por preferencia, o un *net.DNSError cuando el dominio no
resuelve. Trata un error o un slice vacío como "sin ruta de correo" y, en una ruta
de petición, usa la variante que reconoce el contexto (*net.Resolver).LookupMX(ctx, domain),
para que un servidor de nombres lento no pueda dejar la goroutine bloqueada más allá
de tu límite de tiempo.
¿Puedo verificar un buzón en Go sin un servicio externo?#
En parte. net.LookupMX confirma que el dominio acepta correo, lo que descarta
gratis los dominios muertos y no necesita nada más que la biblioteca estándar.
Confirmar el buzón en sí implica una conversación SMTP, que puedes programar con
net/smtp pero que no deberías ejecutar desde el servidor de tu aplicación: el
puerto 25 está bloqueado en muchos sitios y el resultado depende de la reputación de
tu IP de envío. Esa es la capa que existe para que la gestione un servicio de
verificación; puedes ver la diferencia con cualquier dirección usando el
comprobador de correo gratuito.
¿Debo validar los correos en el registro o al limpiar una lista?#
Ambas cosas, con distinta profundidad. Ejecuta la sintaxis y la consulta MX de forma síncrona en el registro (son lo bastante rápidas como para bloquear la petición y dar respuesta instantánea) y actúa allí también sobre el veredicto inmediato de la API. Reserva el trabajo más profundo, confirmado por SMTP, para la limpieza de listas y las tareas de back-office; la guía de registro serverless muestra el patrón en tiempo real de principio a fin.
¿Listo para añadir la capa SMTP a tu servicio en Go? La
referencia de la API de Qualisend tiene el endpoint /verify con
claves de alcance limitado y ejemplos para copiar y pegar, y el
comprobador de correo gratuito te deja ver cómo una dirección
sintácticamente perfecta vuelve como undeliverable antes de que escribas una sola
línea de código.