Funções: declaração, múltiplos retornos e variádicas

Funções: declaração, múltiplos retornos e variádicas

Funções em Go: parâmetros agrupados, múltiplos retornos e o padrão (resultado, error), retornos nomeados e o defer que converte pânico em erro, variádicas que compartilham a fatia do chamador, closures, a armadilha do laço que o Go 1.22 desarmou e a ordem real das funções init.
Golang

• • 15 min de leitura

Em Go, funções são cidadãs de primeira classe. Elas podem ser atribuídas a variáveis, passadas como argumentos, retornadas por outras funções e declaradas anonimamente. Ao mesmo tempo, a linguagem impõe uma sintaxe clara e consistente que torna qualquer função Go imediatamente legível, independentemente de quem a escreveu.

Entender funções em profundidade é essencial porque praticamente tudo em Go passa por elas — desde o tratamento de erros até os padrões de concorrência que serão estudados nos módulos seguintes.

Declaração básica

A sintaxe de uma função em Go segue sempre a mesma estrutura: palavra-chave func, nome, parâmetros entre parênteses, tipo de retorno e corpo entre chaves:

package main

import "fmt"

func saudar(nome string) string {
    return "Olá, " + nome + "!"
}

func main() {
    mensagem := saudar("Ricardo")
    fmt.Println(mensagem) // Olá, Ricardo!
}

Quando a função não retorna nada, o tipo de retorno é simplesmente omitido:

func imprimirLinha(texto string) {
    fmt.Println("→", texto)
}

Parâmetros do mesmo tipo

Quando múltiplos parâmetros consecutivos têm o mesmo tipo, Go permite declarar o tipo apenas uma vez ao final:

// Forma verbosa
func somar(a int, b int) int {
    return a + b
}

// Forma idiomática
func somar(a, b int) int {
    return a + b
}

Essa sintaxe se estende a quantos parâmetros forem necessários:

func calcularMedia(a, b, c float64) float64 {
    return (a + b + c) / 3
}

Múltiplos valores de retorno

Esta é uma das características mais distintivas do Go. Funções podem retornar múltiplos valores, e isso não é um recurso de nicho — é o mecanismo central pelo qual Go trata erros.

package main

import (
    "errors"
    "fmt"
)

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

func main() {
    resultado, err := dividir(10, 3)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    fmt.Printf("Resultado: %.4f\n", resultado) // 3.3333
}

O padrão (resultado, error) está presente em praticamente toda função Go que pode falhar. Quem quer o resultado precisa receber também o erro — e, se não for tratá-lo, descartá-lo de forma explícita com _. Só uma chamada usada como instrução, sem nada à esquerda, descarta os dois em silêncio; é para esse caso que existe o linter errcheck. Esse design torna o tratamento de erros visível e auditável.

Retornos nomeados

Go permite dar nomes aos valores de retorno. Esses nomes funcionam como variáveis locais pré-declaradas e podem ser usados com o return nu — um return sem argumentos que devolve os valores nomeados no estado atual:

func minMax(nums []int) (min, max int) {
    min = nums[0]
    max = nums[0]

    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }

    return // retorna min e max implicitamente
}

func main() {
    menor, maior := minMax([]int{3, 1, 7, 2, 9, 4})
    fmt.Println(menor, maior) // 1 9
}

Retornos nomeados são úteis quando a função é curta e os nomes adicionam clareza à assinatura. Em funções longas, o return nu pode obscurecer o que está sendo retornado — nesses casos, prefira o retorno explícito.

Funções variádicas

Uma função variádica aceita um número indefinido de argumentos do mesmo tipo. O último parâmetro recebe o prefixo ... e é tratado internamente como um slice:

func somarTodos(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(somarTodos(1, 2, 3))          // 6
    fmt.Println(somarTodos(10, 20, 30, 40))   // 100
    fmt.Println(somarTodos())                  // 0
}

Quando você já tem um slice e quer passá-lo para uma função variádica, use o operador ... na chamada para expandir o slice:

numeros := []int{5, 10, 15, 20}
fmt.Println(somarTodos(numeros...)) // 50

A função fmt.Println é ela própria variádica — aceita qualquer número de argumentos de qualquer tipo, graças ao uso de ...interface{} (ou ...any na notação moderna).

Funções como valores

Em Go, funções têm tipo assim como int ou string têm. Uma variável pode armazenar uma função, e funções podem ser passadas como argumentos ou retornadas por outras funções:

package main

import "fmt"

func aplicar(nums []int, fn func(int) int) []int {
    resultado := make([]int, len(nums))
    for i, n := range nums {
        resultado[i] = fn(n)
    }
    return resultado
}

func dobrar(n int) int { return n * 2 }
func quadrado(n int) int { return n * n }

func main() {
    nums := []int{1, 2, 3, 4, 5}

    fmt.Println(aplicar(nums, dobrar))    // [2 4 6 8 10]
    fmt.Println(aplicar(nums, quadrado))  // [1 4 9 16 25]
}

O tipo func(int) int descreve uma função que recebe um int e retorna um int. Esse tipo pode ser nomeado com type para tornar o código mais legível:

type Transformador func(int) int

func aplicar(nums []int, fn Transformador) []int {
    // ...
}

Funções anônimas e closures

Funções anônimas são funções sem nome, declaradas diretamente onde são usadas. Quando uma função anônima referencia variáveis do escopo externo, ela forma uma closure — capturando essas variáveis e mantendo acesso a elas mesmo após o escopo original ter encerrado:

package main

import "fmt"

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

func main() {
    contador := contadorFabrica()

    fmt.Println(contador()) // 1
    fmt.Println(contador()) // 2
    fmt.Println(contador()) // 3

    outroContador := contadorFabrica()
    fmt.Println(outroContador()) // 1 — estado independente
}

A variável count é capturada pelo closure. Cada chamada a contadorFabrica cria um novo count independente. Esse padrão é muito utilizado para criar geradores, middlewares e funções com estado encapsulado.

Funções anônimas executadas imediatamente

Uma função anônima pode ser declarada e chamada na mesma expressão. Esse padrão é chamado de IIFE — Immediately Invoked Function Expression:

resultado := func(a, b int) int {
    return a + b
}(10, 20)

fmt.Println(resultado) // 30

Em Go, IIFEs são usadas com frequência junto ao defer e a goroutines para isolar escopo ou, em código anterior ao Go 1.22, capturar o valor atual da variável de um laço.

Closures em laço: a armadilha que o Go 1.22 desarmou

Durante mais de uma década, este foi o erro mais citado de Go: um closure criado dentro do laço capturava a variável do laço — uma só, compartilhada por todas as voltas — e não o valor que ela tinha em cada volta.

funcs := make([]func(), 3)
for i := 0; i < 3; i++ {
    funcs[i] = func() {
        fmt.Println(i)
    }
}

for _, f := range funcs {
    f()
}

Até o Go 1.21, esse código imprime 3 3 3: quando os closures finalmente rodam, o laço já terminou e a única variável i vale 3. Desde o Go 1.22, cada volta do for ganha a sua própria variável, e o mesmo código imprime 0 1 2 — mas só se o go.mod declarar go 1.22 ou mais. Com go 1.21 na diretiva, o programa compilado pelo Go 1.27 volta a imprimir 3 3 3: quem decide a semântica é o módulo, não a versão instalada.

Por isso ainda se encontra, em código escrito antes de 2024, uma cópia i := i na primeira linha do laço: era a correção da época. Em módulos novos ela é redundante, e o linter copyloopvar, do golangci-lint, aponta essas cópias para remoção. Numa manutenção de projeto antigo, confira a diretiva go antes de apagá-las: se ela ainda estiver abaixo de 1.22, a cópia é o que mantém o programa correto.

Funções init

Go permite declarar funções especiais chamadas init em qualquer arquivo de um pacote. Elas são executadas automaticamente antes da função main, sem precisar ser chamadas explicitamente. São usadas para inicializar estado de pacote, registrar drivers ou validar configurações:

package main

import "fmt"

var configuracao string

func init() {
    configuracao = "produção"
    fmt.Println("init executado")
}

func main() {
    fmt.Println("main executado")
    fmt.Println("Ambiente:", configuracao)
}
// Saída:
// init executado
// main executado
// Ambiente: produção

Um arquivo pode ter múltiplas funções init, e pacotes importados têm seus init executados antes do pacote que os importa. O uso deve ser moderado — lógica complexa em init dificulta testes e rastreamento de bugs.

Funções em Go são valores como quaisquer outros — podem ser guardadas, passadas e devolvidas —, e o retorno múltiplo é o que sustenta o tratamento de erros da linguagem inteira. Os detalhes que mais pesam no dia a dia estão nas beiradas: a fatia repassada com ... é a mesma do chamador, o retorno nomeado é o que permite a um defer transformar um pânico em erro, e a cópia i := i dentro de laços só continua necessária em módulos cujo go.mod ainda está abaixo do Go 1.22.

Fontes e leituras recomendadas

 

Exercícios

Exercício 1

A função minMax do artigo declara os retornos nomeados como (min, max int). Um colega pergunta se isso não conflita com as funções min e max da linguagem. Conflita? E o que acontece quando alguém chama minMax(nil) ou minMax([]int{})?

Ver resposta

✓ Resposta: Desde o Go 1.21, min e max são funções embutidas: min(3, 1, 7) dá 1 e max(2.5, 1) dá 2.5, para qualquer tipo ordenável. Como são identificadores pré-declarados, e não palavras-chave, os retornos nomeados da minMax as escondem dentro da função. O código compila, o go vet não reclama, e minMax([]int{3, 1, 7}) devolve 1 7 corretamente, mas qualquer tentativa de usar a função embutida lá dentro vira erro, já que min passou a ser um int. Nomes como menor e maior evitam a confusão. O outro problema é mais grave: a função lê nums[0] sem conferir o tamanho, e uma fatia vazia ou nil derruba o programa com index out of range [0] with length 0. Uma função que recebe coleção precisa decidir o que fazer com a coleção vazia, seja devolvendo um erro, um bool de "achou" ou documentando que o chamador deve garantir pelo menos um elemento. Hoje, aliás, slices.Min e slices.Max já fazem esse trabalho, e entram em pânico com fatia vazia de propósito, com uma mensagem que diz exatamente isso.

Exercício 2

Uma função variádica normalizar(valores ...float64) ajusta os valores recebidos no próprio parâmetro, dividindo cada um pelo maior. Um chamador passa uma fatia com normalizar(precos...) e depois descobre que a fatia precos original mudou. Outro chama normalizar(a, b, c) e as variáveis continuam intactas. Explique a diferença.

Ver resposta

✓ Resposta: Com valores soltos, o Go cria uma fatia nova só para aquela chamada, copiando a, b e c para dentro dela; a função mexe nessa cópia e as variáveis originais não são tocadas. Com precos..., nenhuma fatia nova é criada: o parâmetro recebe a própria fatia do chamador, apontando para o mesmo array por baixo. Num teste com uma função zerar(nums ...int), a chamada zerar(s...) deixa s valendo [0 0 0], enquanto zerar(a, b) mantém a e b em 1 e 2. O comportamento é intencional, porque evita copiar fatias grandes a cada chamada, mas transforma uma função aparentemente inocente numa que altera dados alheios. A regra de bom convívio é que funções variádicas não modifiquem o parâmetro; se precisarem de uma versão ajustada, criem outra fatia com make ou slices.Clone e devolvam o resultado.

Exercício 3

Uma função de biblioteca chama código de terceiros que, de vez em quando, entra em pânico. O requisito é que ela nunca derrube o processo e, nesse caso, devolva um error descritivo. Como retornos nomeados e defer resolvem isso juntos?

Ver resposta

✓ Resposta: Declarando o retorno com nome, como em func seguro() (err error), e registrando logo no início um defer com uma função anônima que chama recover(). Se houver pânico, o recover o interrompe e devolve o valor que foi passado ao panic, e a função anônima atribui err = fmt.Errorf("recuperado: %v", r). Isso só funciona porque o retorno tem nome: o defer roda depois que o valor de retorno já foi definido e ainda pode alterá-lo, algo impossível com um retorno sem nome, que não tem como ser referido. Num teste em que a função escreve num map nil, o chamador recebe o erro recuperado: assignment to entry in nil map e o programa segue. Dois cuidados. Primeiro, recover só tem efeito quando chamado diretamente dentro da função adiada, e não numa função chamada por ela. Segundo, esse padrão é para fronteiras, como a borda de uma biblioteca ou o tratamento de uma requisição; espalhado pelo código, ele esconde defeitos que deveriam aparecer.

Exercício 4

Um pacote declara var conexao = abrirConexao() no topo e duas funções init no mesmo arquivo, a segunda usando conexao. Um colega teme que o init rode antes de a variável estar pronta. Em que ordem as coisas acontecem? E por que o artigo aconselha moderação com init?

Ver resposta

✓ Resposta: O medo não procede. Dentro de um pacote, todas as variáveis de pacote são inicializadas primeiro, e só depois as funções init rodam, na ordem em que aparecem no arquivo (e, entre arquivos, na ordem em que os arquivos são apresentados ao compilador, em geral a alfabética). Num teste com uma variável cujo inicializador imprime uma mensagem e dois init, a saída é var de pacote inicializada, init 1, init 2 e só então main. A moderação tem outros motivos. Uma conexão aberta no init é aberta em todo programa e em todo teste que importe o pacote, mesmo os que não precisam dela, e uma falha ali só pode virar panic ou log.Fatal, porque init não devolve erro. Além disso, o init não recebe parâmetros, então não há como trocar a conexão real por uma de teste. Uma função Conectar(cfg) (*DB, error) chamada no main resolve os três problemas.

Exercício 5

Para evitar registrar o mesmo handler duas vezes, alguém tenta guardar funções num map[func()]bool e, em outro ponto, comparar duas funções com ==. Nenhuma das ideias compila. Por que o Go proíbe isso, e o que usar no lugar?

Ver resposta

✓ Resposta: Funções em Go só podem ser comparadas com nil. O compilador recusa g == f com func can only be compared to nil e o map com invalid map key type func(), já que chave de map precisa ser comparável. A proibição é deliberada, porque não existe uma resposta boa para "estas duas funções são iguais": dois closures criados pela mesma fábrica têm o mesmo código mas estados diferentes, e o compilador pode tanto duplicar quanto fundir funções durante a otimização. Qualquer regra de igualdade daria resultados surpreendentes. O que se usa no lugar é uma identidade explícita: registrar cada handler com um nome, como map[string]func(), ou devolver ao chamador um identificador, seja um número ou uma função de cancelamento, que permita removê-lo depois. É o padrão de bibliotecas de eventos e de middlewares em Go, e ele deixa o código mais legível do que teria ficado com a comparação direta.

Comentários

Mais em Golang

gRPC e Protocol Buffers: comunicação eficiente entre serviços
gRPC e Protocol Buffers: comunicação eficiente entre serviços

gRPC em Go com Protocol Buffers: contrato e código gerado, interceptors com…

Variáveis de ambiente e configuração com Viper
Variáveis de ambiente e configuração com Viper

Configuração em Go com os.LookupEnv, godotenv e Viper: arquivo YAML, variáveis…

Introdução ao database/sql e drivers em Go
Introdução ao database/sql e drivers em Go

O pacote database/sql em Go: drivers e import em branco, o pool de conexões e…