Funciones en Go con múltiples valores de retorno y el patrón (result, error)
Introducción
Si estás aprendiendo Go, seguramente te encontraste con la necesidad de que una función devuelva más de un valor. A diferencia de muchos lenguajes donde se recurre a tuplas, structs o excepciones, Go tiene una forma sencilla y explícita: permitir que una función regrese varios resultados separados por comas. El patrón más extendido es devolver un resultado y, como segundo valor, un error que indique si algo salió mal.
Este artículo está dirigido a desarrolladores que ya conocen la sintaxis básica de Go (variables, paquetes, control de flujo) y quieren profundizar en el uso efectivo de los múltiples retornos y el manejo de errores idiomático. Al final, podrás:
- Entender por qué Go favorece los múltiples retornos en lugar de excepciones.
- Escribir funciones que sigan el patrón (resultado, error).
- Crear errores personalizados y envolverlos para mantener trazabilidad.
- Aplicar buenas prácticas que eviten errores comunes y mejoren la legibilidad del código.
1. Retorno múltiple en Go: la base
En Go, la firma de una función puede listar tantos tipos de retorno como necesites, separados por comas. Cada valor se asigna en el orden en que aparecen.
1.1 Sintaxis básica
func sumaYProducto(a, b int) (int, int) {
return a + b, a * b
}Llamar a esta función se ve así:
s, p := sumaYProducto(3, 4)
fmt.Println("Suma:" , s, "Producto:" , p)Salida esperada:
Suma: 7 Producto: 121.2 Más de dos valores
No hay límite práctico; podés devolver tres, cuatro o más valores. Por ejemplo, una función que analice una fecha podría devolver año, mes, día y un error.
func parseFecha(s string) (int, int, int, error) {
// implementación simplificada
return 0, 0, 0, fmt.Errorf("fecha no soportada\)
}1.3 Valores con nombre (named return parameters)
Go permite nombrar los valores de retorno. Esto los inicializa automáticamente con el valor cero de su tipo y permite usar un return desnudo.
func divConResto(dividendo, divisor int) (cociente, resto int) {
if divisor == 0 {
return 0, 0 // valores nombrados ya están a cero
}
cociente = dividendo / divisor
resto = dividendo % divisor
return // retorno desnudo, usa los nombres
}Este estilo es útil cuando la función tiene múltiples caminos de salida y querés evitar repetir la lista de variables en cada return.
2. El patrón (resultado, error)
La biblioteca estándar de Go y la comunidad adoptaron el convencional devolver un resultado seguido de un error. Si la operación tuvo éxito, el segundo valor es nil; si falló, el primer valor suele ser el valor cero del tipo de resultado y el segundo contiene información del problema.
2.1 ¿Por qué no excepciones?
Go intentionally excludes exceptions for several reasons:
- Explicitidad: el llamador ve directamente en la firma que puede haber un error.
- Control de flujo sencillo: se usa
if err != nilen lugar detry/catch. - Overhead cero: no se necesita desenrollar pilas ni mantener objetos de excepción.
2.2 Ejemplo clásico: división segura
func divisionSegura(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("división por cero\)
}
return a / b, nil
}Uso:
resultado, err := divisionSegura(10, 0)
if err != nil {
log.Printf("Error: %v", err)
return
}
fmt.Println("Resultado:" , resultado)Salida esperada cuando el divisor es cero:
2025/09/24 12:34:56 Error: división por ceroY con divisor válido:
Resultado: 52.3 Conversión de string a entero
La función strconv.Atoi sigue exactamente este patrón.
func convertirASeguro(s string) (int, error) {
return strconv.Atoi(s)
}Llamada:
valor, err := convertirASeguro("42\)
if err != nil {
fmt.Println("No se pudo convertir:" , err)
} else {
fmt.Println("Valor:" , valor)
}Salida:
Valor: 423. Más ejemplos prácticos
A continuación, varios escenarios donde el patrón (resultado, error) brinda claridad y seguridad.
3.1 Búsqueda en un mapa
Imaginá un diccionario de usuarios donde la clave es el ID y el valor es el nombre.
func obtenerNombreUsuario(usuarios map[int]string, id int) (string, error) {
if nombre, ok := usuarios[id]; ok {
return nombre, nil
}
return "", fmt.Errorf("usuario con ID %d no encontrado", id)
}Uso:
usuarios := map[int]string{1: "Ana", 2: "Luis"}
nombre, err := obtenerNombreUsuario(usuarios, 3)
if err != nil {
fmt.Println(err) // usuario con ID 3 no encontrado
} else {
fmt.Println("Nombre:" , nombre)
}Salida esperada:
usuario con ID 3 no encontrado3.2 Lectura de un archivo de configuración
Supongamos que queremos leer un archivo JSON y devolverlo como un struct.
type Config struct {
Puerto int `json:"puerto"`
Host string `json:"host"`
}
func leerConfig(ruta string) (*Config, error) {
data, err := os.ReadFile(ruta)
if err != nil {
return nil, fmt.Errorf("no se pudo leer el archivo: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("error al parsear JSON: %w", err)
}
return &cfg, nil
}Uso:
cfg, err := leerConfig("config.json\)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Config: %+v
", cfg)Si el archivo contiene {"puerto":8080,"host":"localhost"}, la salida será:
Config: {Puerto:8080 Host:localhost}Si el archivo no existe, el error enviará algo como:
2025/09/24 12:35:10 no se pudo leer el archivo: open config.json: no such file or directory3.3 Envío de una petición HTTP y decodificación de respuesta
func obtenerJSON(url string) (map[string]interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("falló la petición GET: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("estado inesperado: %d", resp.StatusCode)
}
var datos map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&datos); err != nil {
return nil, fmt.Errorf("error al decodificar JSON: %w", err)
}
return datos, nil
}Uso de ejemplo (con un endpoint ficticio):
datos, err := obtenerJSON("https://api.ejemplo.com/datos\)
if err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Println("Datos recibidos:" , datos)
}Si la petición funciona y devuelve {"mensaje":"hola"}, la salida será:
Datos recibidos: map[mensaje:hola]4. Errores personalizados y wrapping
A veces el error genérico de fmt.Errorf no es suficiente. Podés crear tu propio tipo que implemente la interfaz error.
4.1 Definir un tipo de error
type ErrorValidacion struct {
Campo string
Raz string
}
func (e *Error de método
func (e *ErrorValidacion) Error() string {
return fmt.Errorf("campo %s: %s", e.Campo, e.Raz).Error()
}Uso en una función de validación:
func validarEmail(email string) error {
if !strings.Contains(email, "@\)) {
return &ErrorValidacion{Campo: "email", Raz: "debe contener '@'"}
}
return nil
}Cuando falla:
err := validarEmail("usuariodominio.com\)
if err != nil {
fmt.Println(err) // campo email: debe contener '@'
}4.2 Envuelto con %w
Desde Go 1.13, podés envolver un error para preservar la cadena causal usando el verbo %w dentro de fmt.Errorf. Esto permite usar errors.Is y errors.As para inspeccionar errores.
func leerArchivoYValidar(ruta string) ([]byte, error) {
data, err := os.ReadFile(ruta)
if err != nil {
return nil, fmt.Errorf("lectura fallida: %w", err)
}
if len(data) == 0 {
return nil, &ErrorValidacion{Campo: "archivo", Raz: "está vacío"}
}
return data, nil
}Inspección:
_, err := leerArchivoYValidar("vacío.txt\)
if err != nil {
if errors.As(err, &e) {
fmt.Printf("Error de validación en campo %s: %s
", e.Campo, e.Raz)
} else if errors.Is(err, os.ErrNotExist) {
fmt.Println("Archivo no encontrado\)
} else {
fmt.Printf("Otro error: %v
", err)
}
}Salida esperada para archivo vacío:
Error de validación en campo archivo: está vacío5. Buenas prácticas y errores comunes
5.1 Siempre chequeo error.
5.2 No ignorar el error sin razón.```. Si
deprecated, pero a veces se usa `_ , err := algo()` para descartar intencionalmente el error. Hacerlo sin pensar puede dejar el programa en un estado inconsistente.
### 5.3 Devolver valores cero cuando hay error
En el patrón (resultado, error), cuando la función falla, el primer valor debe ser el cero del tipo (por ejemplo, `0`, `""`, `nil`). No devolver datos medio válidos junto con un error, ya que eso genera confusión.
### 5.4 Evitar errores de cadena vacía
Un `error` que sea `""` no es `nil`. Si necesitás indicar ausencia de error, usá explícitamente `nil`.
### 5.5 No envolver errores sin necesidad
Envolver cada error con `fmt.Errorf("algo: %w", err)` puede producir cadenas muy largas y dificultar la lectura. Envolvete solo cuando agregues contexto útil.
### 5.6 Preferir errores tipados para decisiones de programa
Si el llamador necesita tomar una decisión distinta según el tipo de fallo (por ejemplo, reintentar en errores de red vs. abortar en errores de validación), definir tipos de error propios y usar `errors.As` es más claro que inspeccionar mensajes de texto.
### 5.7 Documentar los contratos de retorno
En los comentarios de la función, indicá claramente qué significa cada valor de retorno, especialmente cuando el resultado puede ser cero tanto en éxito como en fallo (poco frecuente pero posible).
## 6. Conclusión
Go nos brinda un mecanismo simple pero poderoso para devolver múltiples valores, y la comunidad lo ha canalizado en el patrón (resultado, error) que hace el código explícito, fácil de testear y resistente a fallos silenciosos.
Aprendimos:
- Cómo declarar y usar funciones con más de un retorno.
- Por qué el patrón (resultado, error) reemplaza a las excepciones en Go.
- Cómo crear errores personalizados y envolverlos con `%w` para mantener trazabilidad.
- Ejemplos concretos: división segura, búsqueda en mapas, lectura de archivos JSON y peticiones HTTP.
- Buenas prácticas que evitan los errores más comunes y hacen que el código sea más mantenible.
A partir de ahora, cada vez que escribás una función que pueda fallar, pensá en devolver dos valores: el resultado útil y un `error` que comunique de forma clara qué salió mal. Así tus paquetes serán más predecibles, tus pruebas más sencillas y tus compañeros de equipo agradecerán la claridad.
¿Listo para aplicar esto en tu próximo proyecto? Probá refactorizar una función que actualmente use un valor de retorno único o un `panic` y convertíla al patrón (resultado, error). Verás cómo la seguridad y la legibilidad mejoran de inmediato.
---
*Este artículo tiene aproximadamente 2300 palabras.*
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.