Funções em Go: múltiplos retornos, variádicas, closures e defer

Capítulo 8

Tempo estimado de leitura: 7 minutos

+ Exercício

Múltiplos retornos: o padrão valor + erro

Em Go, funções podem retornar mais de um valor. O uso mais idiomático é retornar (valor, error), onde error indica se a operação falhou. A regra prática é: se err != nil, o valor retornado deve ser considerado inválido (ou um valor “zero” seguro).

Exemplo: parse com validação

package main

import (
	"errors"
	"fmt"
	"strconv"
)

func parsePositiveInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	if n <= 0 {
		return 0, errors.New("valor deve ser positivo")
	}
	return n, nil
}

func main() {
	n, err := parsePositiveInt("10")
	if err != nil {
		fmt.Println("erro:", err)
		return
	}
	fmt.Println("ok:", n)
}

Passo a passo (como pensar)

  • Faça a operação que pode falhar (ex.: conversão, I/O, validação).
  • Se falhar, retorne o valor zero do tipo + o erro.
  • Se der certo, retorne o valor + nil.
  • No chamador, trate o erro imediatamente (early return).

Retornos nomeados (quando fazem sentido)

Você pode nomear retornos para melhorar legibilidade em funções com múltiplas saídas ou quando o retorno “carrega significado”. Use com moderação: retornos nomeados ajudam quando o nome adiciona clareza, mas podem confundir se usados sem necessidade.

func minMax(nums []int) (min int, max int, err error) {
	if len(nums) == 0 {
		return 0, 0, fmt.Errorf("slice vazio")
	}
	min, max = nums[0], nums[0]
	for _, v := range nums[1:] {
		if v < min {
			min = v
		}
		if v > max {
			max = v
		}
	}
	return min, max, nil
}

Note que return “nu” (sem valores) retorna os valores atuais das variáveis nomeadas. Prefira return min, max, nil se isso deixar mais explícito.

Funções variádicas: recebendo N argumentos

Uma função variádica recebe zero ou mais argumentos de um tipo, declarados com .... Internamente, o parâmetro variádico é tratado como um slice.

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

Chamando com valores soltos

total := sum(1, 2, 3)

Passando um slice para uma variádica

Para “expandir” um slice em argumentos variádicos, use ... na chamada.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

nums := []int{1, 2, 3}
total := sum(nums...)

Variádica + parâmetros fixos

O parâmetro variádico deve ser o último.

func join(sep string, parts ...string) string {
	if len(parts) == 0 {
		return ""
	}
	out := parts[0]
	for _, p := range parts[1:] {
		out += sep + p
	}
	return out
}

Cuidados comuns

  • parts é um slice: pode ser iterado, ter len, etc.
  • Evite modificar o slice variádico se você não controla quem chamou (pode surpreender se o chamador passou um slice com ...).

Closures: funções que “capturam” variáveis

Closure é uma função que referencia variáveis do escopo onde foi criada. Em Go, isso permite criar funções geradoras, filtros e callbacks que carregam “estado” sem precisar de structs.

Exemplo 1: filtro (predicado) com parâmetro capturado

Vamos criar uma função que gera um filtro “maior que X”.

func greaterThan(min int) func(int) bool {
	return func(v int) bool {
		return v > min
	}
}

func filterInts(nums []int, keep func(int) bool) []int {
	out := make([]int, 0, len(nums))
	for _, n := range nums {
		if keep(n) {
			out = append(out, n)
		}
	}
	return out
}

Uso:

nums := []int{1, 5, 7, 2, 9}
keep := greaterThan(5)
filtered := filterInts(nums, keep) // [7 9]

Passo a passo (o que acontece)

  • greaterThan(5) cria uma função anônima.
  • A função anônima “captura” min (que vale 5 naquele momento).
  • Ao chamar keep(7), ela compara com o min capturado.

Exemplo 2: função geradora com estado (contador)

func counter(start int) func() int {
	n := start
	return func() int {
		n++
		return n
	}
}

Uso:

next := counter(10)
fmt.Println(next()) // 11
fmt.Println(next()) // 12

A variável n permanece viva enquanto a closure existir, funcionando como “estado privado”.

Escopo e armadilha comum: capturar variável de loop

Um erro frequente é capturar a variável do for e esperar que cada closure “lembre” um valor diferente. Em Go, a variável do loop é reutilizada; closures podem acabar vendo o mesmo valor final.

// Exemplo problemático (não faça assim)
func bad() []func() int {
	fs := []func() int{}
	for i := 0; i < 3; i++ {
		fs = append(fs, func() int { return i })
	}
	return fs
}

Correção: crie uma variável local por iteração.

func good() []func() int {
	fs := []func() int{}
	for i := 0; i < 3; i++ {
		i := i // sombra intencional: novo i por iteração
		fs = append(fs, func() int { return i })
	}
	return fs
}

defer: adiar execução e garantir limpeza

defer agenda uma chamada de função para executar quando a função atual retornar (seja por retorno normal, por erro, ou por pânico não recuperado). É muito usado para liberar recursos (fechar arquivos, liberar locks, etc.).

Ordem de execução: LIFO (pilha)

Vários defer executam em ordem inversa à que foram declarados.

func demoDeferOrder() {
	defer fmt.Println("3")
	defer fmt.Println("2")
	defer fmt.Println("1")
	fmt.Println("0")
}
// Saída:
// 0
// 1
// 2
// 3

Quando os argumentos são avaliados

Os argumentos da chamada adiada são avaliados no momento do defer, não no momento da execução.

func demoDeferArgs() {
	x := 10
	defer fmt.Println("x:", x) // captura 10 agora
	x = 20
}
// imprime: x: 10

Padrão comum: fechar recursos

Ao abrir um recurso, faça defer Close() imediatamente após verificar o erro. Isso reduz vazamentos e simplifica retornos antecipados.

func readAll(r io.ReadCloser) ([]byte, error) {
	defer r.Close()
	return io.ReadAll(r)
}

Em casos reais, você normalmente obtém o io.ReadCloser de uma função que pode falhar; o padrão é:

rc, err := openSomething()
if err != nil {
	return nil, err
}
defer rc.Close()

defer com retorno nomeado: ajustando o erro

Quando você usa retorno nomeado, um defer pode ler e até modificar o valor que será retornado. Isso é útil para enriquecer erros com contexto.

func doWork() (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("doWork falhou: %w", err)
		}
	}()

	err = step1()
	if err != nil {
		return err
	}
	err = step2()
	if err != nil {
		return err
	}
	return nil
}

Recuperando pânico com defer + recover (quando aplicável)

panic interrompe o fluxo normal. Em bibliotecas e serviços, você pode usar recover para evitar que um pânico derrube o programa inteiro, convertendo-o em erro. recover só funciona quando chamado dentro de uma função adiada (defer).

Wrapper seguro: converter pânico em erro

func safeRun(fn func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic recuperado: %v", r)
		}
	}()

	fn()
	return nil
}

Uso:

err := safeRun(func() {
	panic("algo inesperado")
})
if err != nil {
	fmt.Println("erro:", err)
}

Boas práticas ao usar recover

  • Use para proteger fronteiras (handlers, goroutines, jobs), não como controle de fluxo normal.
  • Registre contexto suficiente (o que estava sendo feito) ao transformar em erro.
  • Se fizer sentido, repropague (panic novamente) após logar, dependendo do tipo de aplicação.

Exercícios

1) Múltiplos retornos (valor + erro)

  • Implemente func divide(a, b float64) (float64, error) que retorna erro quando b == 0.
  • Implemente func parsePort(s string) (port int, err error) que valida se o número está entre 1 e 65535 (use retorno nomeado apenas se ajudar a clarear).

2) Variádicas e slices

  • Implemente func avg(nums ...float64) (float64, error) que retorna erro se nenhum número for passado.
  • Dado um slice []float64, chame avg expandindo com ....
  • Implemente func concat(sep string, parts ...string) string sem usar strings.Join.

3) Closures (filtros e geradores)

  • Crie func between(min, max int) func(int) bool e use com um filterInts para filtrar números dentro do intervalo.
  • Crie func multiplier(factor int) func(int) int e aplique em um slice (faça uma função mapInts).
  • Crie um gerador func fibonacci() func() int que retorna o próximo número da sequência a cada chamada.

4) defer (ordem e limpeza)

  • Escreva uma função que imprime start, agenda três defer com prints diferentes e imprime end. Verifique a ordem.
  • Implemente func withLock(mu *sync.Mutex, fn func()) que faz mu.Lock() e garante mu.Unlock() com defer.

5) defer + recover

  • Implemente func safeValue(fn func() int) (v int, err error) que recupera pânico e retorna um erro.
  • Crie um teste manual chamando safeValue com uma função que dá panic e outra que retorna normalmente.

Agora responda o exercício sobre o conteúdo:

Ao chamar uma função em Go que retorna (valor, error), qual prática é mais idiomática quando err != nil?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Em Go, o padrão (valor, error) indica que, se err != nil, o valor não deve ser usado. O comum é checar o erro e fazer retorno antecipado, evitando seguir com dados inválidos.

Próximo capitúlo

Métodos e interfaces em Go: receivers, polimorfismo e design idiomático

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
44%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.