Go Admin

O que Todo Dev Deve Saber sobre Funções em Go: Múltiplos Retornos, Variádicas e Funções como Valores Já leu

Múltiplos Retornos em Go Go é uma das poucas linguagens modernas que suporta nativamente múltiplos retornos de funções. Diferente de linguagens como Python que retornam tuplas ou Java que exigem wrapper objects, em Go você simplesmente declara quantos valores deseja retornar e o compilador cuida do resto. Essa é uma característica fundamental que influencia toda a forma como tratamos erros e valores na linguagem. O padrão mais comum é retornar um valor útil seguido de um erro. Isso elimina a necessidade de exceções e torna o fluxo de erro explícito no código. Quando você chama uma função que retorna múltiplos valores, é obrigado a lidar com todos eles — não é possível ignorar silenciosamente um erro. Veja como funciona na prática: Closures e Captura de Variáveis Funções anônimas em Go podem capturar variáveis do escopo externo, criando closures. A captura é por referência, não por valor — se a variável externa muda, a função enxerga a mudança. Essa característica é

Múltiplos Retornos em Go

Go é uma das poucas linguagens modernas que suporta nativamente múltiplos retornos de funções. Diferente de linguagens como Python que retornam tuplas ou Java que exigem wrapper objects, em Go você simplesmente declara quantos valores deseja retornar e o compilador cuida do resto. Essa é uma característica fundamental que influencia toda a forma como tratamos erros e valores na linguagem.

O padrão mais comum é retornar um valor útil seguido de um erro. Isso elimina a necessidade de exceções e torna o fluxo de erro explícito no código. Quando você chama uma função que retorna múltiplos valores, é obrigado a lidar com todos eles — não é possível ignorar silenciosamente um erro. Veja como funciona na prática:

package main

import (
    "fmt"
    "strconv"
)

func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divisão por zero não permitida")
    }
    return a / b, nil
}

func buscarUsuario(id int) (string, int, error) {
    if id <= 0 {
        return "", 0, fmt.Errorf("ID inválido")
    }
    // Simulando busca em banco de dados
    usuarios := map[int]string{1: "Alice", 2: "Bob"}
    nome, existe := usuarios[id]
    if !existe {
        return "", 0, fmt.Errorf("usuário não encontrado")
    }
    return nome, id, nil
}

func main() {
    // Capturando múltiplos retornos
    resultado, err := dividir(10, 2)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Printf("Resultado: %.2f\n", resultado)
    }

    // Descartando retornos com blank identifier
    nome, _, err := buscarUsuario(1)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Println("Usuário encontrado:", nome)
    }

    // Capturando todos os retornos
    nome, id, err := buscarUsuario(2)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Printf("Nome: %s, ID: %d\n", nome, id)
    }
}

Nomeando Retornos

Go permite nomear os valores de retorno na assinatura da função. Quando você faz isso, essas variáveis são inicializadas com seus valores zero automaticamente e podem ser retornadas implicitamente com a instrução return vazia. Isso torna o código mais legível, especialmente em funções com muitos retornos, mas use com moderação — retornos vazios podem obscurecer a lógica se abusados.

package main

import "fmt"

// Retornos nomeados - bom para documentação
func calcularMedia(notas []float64) (media float64, total float64, err error) {
    if len(notas) == 0 {
        err = fmt.Errorf("nenhuma nota fornecida")
        return
    }

    for _, nota := range notas {
        total += nota
    }
    media = total / float64(len(notas))
    return
}

// Sem retornos nomeados - mais explícito
func calcularMediaExplicito(notas []float64) (float64, float64, error) {
    if len(notas) == 0 {
        return 0, 0, fmt.Errorf("nenhuma nota fornecida")
    }

    var media, total float64
    for _, nota := range notas {
        total += nota
    }
    media = total / float64(len(notas))
    return media, total, nil
}

func main() {
    notas := []float64{7.5, 8.0, 9.5}

    media, total, err := calcularMedia(notas)
    if err != nil {
        fmt.Println("Erro:", err)
    } else {
        fmt.Printf("Média: %.2f, Total: %.2f\n", media, total)
    }
}

Funções Variádicas

Funções variádicas são aquelas que aceitam um número indefinido de argumentos do mesmo tipo. Você as declara usando reticências (...) antes do tipo do parâmetro. Internamente, Go converte esses argumentos em um slice, então você manipula como tal. Essa abordagem é muito mais elegante do que passar um slice e evita a necessidade de wrapping manual.

A vantagem principal é a liberdade do chamador: pode passar zero argumentos, um ou vários, tudo com a mesma sintaxe intuitiva. Go usa isso extensivamente, como em fmt.Println() que aceita quantos valores você quiser imprimir.

package main

import (
    "fmt"
    "strings"
)

// Função variádica básica
func somar(numeros ...int) int {
    total := 0
    for _, num := range numeros {
        total += num
    }
    return total
}

// Variádica com múltiplos retornos
func processarStrings(separador string, textos ...string) (resultado string, contagem int) {
    resultado = strings.Join(textos, separador)
    contagem = len(textos)
    return
}

// Variádica com argumentos fixos antes
func criarMensagem(prefixo string, palavras ...string) string {
    return prefixo + ": " + strings.Join(palavras, ", ")
}

func main() {
    // Chamadas com diferentes quantidades de argumentos
    fmt.Println("Soma de 1,2,3:", somar(1, 2, 3))
    fmt.Println("Soma de 5,10:", somar(5, 10))
    fmt.Println("Soma vazia:", somar())

    // Expandindo um slice com ...
    numeros := []int{4, 5, 6}
    fmt.Println("Soma do slice:", somar(numeros...))

    // Variádica com outros parâmetros
    resultado, cont := processarStrings(" | ", "Go", "é", "legal")
    fmt.Printf("Resultado: %s (contagem: %d)\n", resultado, cont)

    msg := criarMensagem("Linguagens", "Go", "Rust", "Python")
    fmt.Println(msg)
}

Cuidados com Variádicas

Uma armadilha comum é tentar passar um slice diretamente sem usar o operador de expansão (...). Sem ele, você estará passando o próprio slice como um único argumento, não seus elementos. Além disso, a variádica deve ser sempre o último parâmetro da função — você não pode ter parâmetros após ela.

package main

import "fmt"

func imprimirNomes(nomes ...string) {
    for _, nome := range nomes {
        fmt.Println(nome)
    }
}

// ERRADO: variádica não é o último parâmetro
// func errado(nomes ...string, idade int) {} // Isso não compila

func main() {
    // Correto: usando operador de expansão
    lista := []string{"Alice", "Bob", "Charlie"}
    imprimirNomes(lista...)

    // Direto com literais
    imprimirNomes("Diana", "Eve")

    // Sem argumentos (válido)
    imprimirNomes()
}

Funções como Valores

Em Go, funções são cidadãs de primeira classe — você pode atribuir uma função a uma variável, passá-la como argumento, retorná-la de outra função ou armazená-la em uma estrutura. Isso abre possibilidades poderosas para programação funcional e patterns como callbacks, middlewares e estratégias.

O tipo de uma função é definido pela sua assinatura: quantos e quais parâmetros ela aceita, e quantos e quais valores retorna. Duas funções só são do mesmo tipo se tiverem exatamente a mesma assinatura.

package main

import (
    "fmt"
    "sort"
    "strings"
)

// Definindo um tipo de função
type Operacao func(int, int) int

// Função que retorna uma função
func criarMultiplicador(fator int) func(int) int {
    return func(x int) int {
        return x * fator
    }
}

// Função que recebe uma função como argumento
func aplicarOperacao(a, b int, op Operacao) int {
    return op(a, b)
}

// Função que filtra usando uma função predicado
func filtrar(numeros []int, predicado func(int) bool) []int {
    resultado := []int{}
    for _, num := range numeros {
        if predicado(num) {
            resultado = append(resultado, num)
        }
    }
    return resultado
}

func main() {
    // Atribuindo funções a variáveis
    somar := func(a, b int) int {
        return a + b
    }
    subtrair := func(a, b int) int {
        return a - b
    }

    fmt.Println("Soma:", aplicarOperacao(10, 5, somar))
    fmt.Println("Subtração:", aplicarOperacao(10, 5, subtrair))

    // Usando tipo de função definido
    var op Operacao
    op = func(a, b int) int { return a * b }
    fmt.Println("Multiplicação:", aplicarOperacao(10, 5, op))

    // Retornando uma função
    vezes3 := criarMultiplicador(3)
    fmt.Println("10 * 3 =", vezes3(10))
    vezes5 := criarMultiplicador(5)
    fmt.Println("10 * 5 =", vezes5(10))

    // Passando função como argumento
    numeros := []int{1, 2, 3, 4, 5, 6, 7, 8}
    pares := filtrar(numeros, func(n int) bool { return n%2 == 0 })
    fmt.Println("Números pares:", pares)

    maiores := filtrar(numeros, func(n int) bool { return n > 4 })
    fmt.Println("Maiores que 4:", maiores)
}

Closures e Captura de Variáveis

Funções anônimas em Go podem capturar variáveis do escopo externo, criando closures. A captura é por referência, não por valor — se a variável externa muda, a função enxerga a mudança. Essa característica é essencial para patterns como callbacks e decoradores.

package main

import "fmt"

func criarContador() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func aplicarFiltros(palavra string, filtros ...func(string) string) string {
    resultado := palavra
    for _, filtro := range filtros {
        resultado = filtro(resultado)
    }
    return resultado
}

func main() {
    // Closure capturando variável
    contador := criarContador()
    fmt.Println(contador()) // 1
    fmt.Println(contador()) // 2
    fmt.Println(contador()) // 3

    // Múltiplos contadores independentes
    contador2 := criarContador()
    fmt.Println(contador2()) // 1 (começa do zero novamente)

    // Usando closures como filtros
    converterMaiuscula := func(s string) string {
        return strings.ToUpper(s)
    }

    adicionarPrefixo := func(s string) string {
        return ">>> " + s
    }

    resultado := aplicarFiltros("hello", converterMaiuscula, adicionarPrefixo)
    fmt.Println(resultado) // >>> HELLO
}

Armazenando Funções em Estruturas

Um padrão poderoso é armazenar funções dentro de structs. Isso permite criar objetos com comportamento configurável ou implementar padrões de design como Strategy ou Command.

package main

import "fmt"

type Logger struct {
    logFunc func(string)
}

type Calculadora struct {
    operacao func(int, int) int
    nome     string
}

func main() {
    // Configurando logger com diferentes implementações
    loggerConsole := Logger{
        logFunc: func(msg string) {
            fmt.Println("[CONSOLE]", msg)
        },
    }

    loggerArquivo := Logger{
        logFunc: func(msg string) {
            fmt.Println("[ARQUIVO]", msg)
        },
    }

    loggerConsole.logFunc("Aplicação iniciada")
    loggerArquivo.logFunc("Log escrito em arquivo")

    // Calculadora com estratégia plugável
    calc1 := Calculadora{
        operacao: func(a, b int) int { return a + b },
        nome:     "Adição",
    }

    calc2 := Calculadora{
        operacao: func(a, b int) int { return a * b },
        nome:     "Multiplicação",
    }

    fmt.Printf("%s: %d\n", calc1.nome, calc1.operacao(5, 3))
    fmt.Printf("%s: %d\n", calc2.nome, calc2.operacao(5, 3))
}

Combinando os Três Conceitos

Agora que você compreende cada mecanismo isoladamente, vamos combiná-los em padrões mais realistas que você encontrará em código profissional.

package main

import (
    "fmt"
    "sort"
)

// Tipo que representa uma transformação
type Transformacao func(string) string

// Função que retorna uma transformação e possível erro
func obterTransformacao(tipo string) (Transformacao, error) {
    switch tipo {
    case "maiuscula":
        return func(s string) string {
            return strings.ToUpper(s)
        }, nil
    case "minuscula":
        return func(s string) string {
            return strings.ToLower(s)
        }, nil
    default:
        return nil, fmt.Errorf("tipo de transformação desconhecido: %s", tipo)
    }
}

// Função que aceita múltiplos transformadores e retorna resultado + contagem
func aplicarTransformacoes(texto string, transformadores ...Transformacao) (string, int) {
    resultado := texto
    for _, transformador := range transformadores {
        resultado = transformador(resultado)
    }
    return resultado, len(transformadores)
}

// Processador que armazena funções variádicas
type Processador struct {
    filtros []func(int) bool
}

func (p *Processador) adicionarFiltro(f func(int) bool) {
    p.filtros = append(p.filtros, f)
}

func (p *Processador) processar(numeros ...int) []int {
    resultado := []int{}
    for _, num := range numeros {
        passou := true
        for _, filtro := range p.filtros {
            if !filtro(num) {
                passou = false
                break
            }
        }
        if passou {
            resultado = append(resultado, num)
        }
    }
    return resultado
}

func main() {
    // Combinando retorno múltiplo + funções como valor
    transform, err := obterTransformacao("maiuscula")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    // Usando variádica com funções
    resultado, quantidade := aplicarTransformacoes(
        "hello world",
        transform,
        func(s string) string { return ">>> " + s },
    )
    fmt.Printf("Resultado: %s (%d transformações aplicadas)\n", resultado, quantidade)

    // Processador com múltiplos filtros
    proc := &Processador{}
    proc.adicionarFiltro(func(n int) bool { return n > 5 })
    proc.adicionarFiltro(func(n int) bool { return n < 20 })

    numeros := []int{1, 6, 10, 15, 25, 30}
    filtrados := proc.processar(numeros...)
    fmt.Println("Números filtrados:", filtrados)
}

Referências


Artigos relacionados