Funções: múltiplos retornos e named returns

1. Sintaxe Básica de Múltiplos Retornos

Em Go, uma função pode retornar múltiplos valores. A declaração é feita na assinatura da função, listando os tipos entre parênteses:

func dividir(a, b int) (int, int) {
    quociente := a / b
    resto := a % b
    return quociente, resto
}

Para capturar os retornos, usamos múltiplas variáveis separadas por vírgula:

q, r := dividir(10, 3)
fmt.Printf("Quociente: %d, Resto: %d\n", q, r) // Quociente: 3, Resto: 1

Quando não precisamos de todos os retornos, utilizamos o blank identifier (_):

q, _ := dividir(10, 3)
fmt.Println("Apenas quociente:", q)

2. Casos de Uso Comuns

O padrão mais difundido em Go é retornar valor + erro:

func lerArquivo(nome string) (string, error) {
    dados, err := os.ReadFile(nome)
    if err != nil {
        return "", err
    }
    return string(dados), nil
}

Outro caso comum é retornar resultados parciais com status:

func buscarUsuario(id int) (Usuario, bool) {
    for _, u := range usuarios {
        if u.ID == id {
            return u, true
        }
    }
    return Usuario{}, false
}

Operações de busca frequentemente retornam valor e flag de existência:

func primeiroPar(numeros []int) (int, bool) {
    for _, n := range numeros {
        if n%2 == 0 {
            return n, true
        }
    }
    return 0, false
}

3. Named Returns: Declaração e Escopo

Named returns permitem nomear os valores de retorno na própria assinatura:

func dividirNamed(a, b int) (quociente, resto int) {
    quociente = a / b
    resto = a % b
    return
}

As variáveis nomeadas são inicializadas automaticamente com seus valores zero:

func valoresZero() (x int, s string, b bool) {
    // x = 0, s = "", b = false
    return // retorna 0, "", false
}

O escopo dessas variáveis é o corpo inteiro da função, funcionando como variáveis locais declaradas no início.

4. Comportamento do return com Named Returns

O return sem argumentos (bare return) retorna os valores atuais das variáveis nomeadas:

func calcular(a, b int) (soma, produto int) {
    soma = a + b
    produto = a * b
    return // equivalente a return soma, produto
}

Podemos misturar atribuições explícitas com bare returns:

func processar(valor int) (dobro, metade int) {
    dobro = valor * 2
    if valor == 0 {
        return // retorna 0, 0
    }
    metade = valor / 2
    return
}

É importante notar que o bare return sempre reflete o estado atual das variáveis nomeadas no momento da execução.

5. Comparação: Retornos Nomeados vs. Anônimos

Named returns melhoram a legibilidade ao documentar implicitamente o significado dos retornos:

// Sem nome - precisa ler o corpo
func parse(data string) (string, int, error)

// Com nome - auto-documentado
func parse(data string) (nome string, idade int, err error)

No stack trace e depuração, named returns aparecem com seus nomes, facilitando a identificação:

func exemplo() (resultado string, err error) {
    // Em caso de panic, o stack trace mostra "resultado" e "err"
}

Quanto à performance, não há overhead — named returns são puramente uma convenção em tempo de compilação, sem custo em tempo de execução.

6. Armadilhas e Boas Práticas

Bare returns são concisos, mas devem ser usados com moderação:

// RUIM: função longa com bare return confuso
func processarDados(dados []byte) (resultado string, err error) {
    if len(dados) == 0 {
        err = errors.New("dados vazios")
        return // o que retorna? resultado vazio e erro
    }
    // ... 50 linhas de código ...
    return
}

// BOM: função curta com bare return claro
func soma(a, b int) (total int) {
    total = a + b
    return
}

Shadowing de variáveis nomeadas pode causar bugs sutis:

func exemplo() (valor int) {
    valor = 10
    valor := 20 // shadowing! Cria nova variável local
    return      // retorna 10, não 20
}

Evite uso excessivo em funções longas — prefira retornos explícitos quando a função ultrapassa 10-15 linhas.

7. Exemplos Práticos e Anti-Padrões

Divisão com quociente e resto:

// Versão sem named returns
func dividirAnonimo(a, b int) (int, int) {
    return a / b, a % b
}

// Versão com named returns
func dividirNomeado(a, b int) (quociente, resto int) {
    quociente = a / b
    resto = a % b
    return
}

Parse de dados com múltiplos retornos e erros:

func parseConfig(linha string) (chave, valor string, err error) {
    partes := strings.SplitN(linha, "=", 2)
    if len(partes) != 2 {
        err = fmt.Errorf("formato inválido: %s", linha)
        return // retorna "", "", erro
    }
    chave = strings.TrimSpace(partes[0])
    valor = strings.TrimSpace(partes[1])
    if chave == "" {
        err = errors.New("chave vazia")
        return
    }
    return // retorna chave, valor, nil
}

Anti-padrão: named returns em funções complexas:

func processarPedido(pedido Pedido) (total float64, desconto float64, frete float64, err error) {
    // 30 linhas de validação
    // 20 linhas de cálculo
    // 15 linhas de aplicação de regras
    // return implícito no final - difícil de rastrear
}

Neste caso, prefira retornos explícitos ou até mesmo uma struct de resultado.

Referências