Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 15 de junio de 2026

Cómo validar una dirección de correo electrónico en Go

8 minutes read

Qualisend team
Un diagrama por capas de una dirección de correo en Go que pasa por la validación de sintaxis net/mail.ParseAddress, una consulta MX con net.LookupMX y un sondeo SMTP del buzón, hasta reducirse a un único veredicto de entregabilidad

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.

Your reputation, protected.

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

Get started