Blog

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

Funções em Go podem retornar vários valores, carregar variáveis como closures e agendar a limpeza com defer. Veja como cada um funciona, incluindo a ordem em que as chamadas adiadas rodam e quando os argumentos delas são fixados.

Funções em Go parecem funções de qualquer outra linguagem, até você notar três coisas. Elas podem retornar mais de um valor. Elas mesmas são valores, então você pode guardá-las e passá-las adiante. E defer deixa uma função agendar trabalho para o momento em que ela termina.

Este post cobre as três, e as closures que surgem quando você trata funções como valores. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.

Declarando uma função

Uma função em Go lista os parâmetros com o tipo depois do nome, e o tipo do resultado depois da lista de parâmetros. Quando dois parâmetros seguidos têm o mesmo tipo, você pode escrever o tipo uma vez só.

package main

import (
	"fmt"
	"strconv"
)

func add(a, b int) int {
	return a + b
}

func minMax(nums []int) (int, int) {
	lo, hi := nums[0], nums[0]
	for _, n := range nums[1:] {
		lo = min(lo, n)
		hi = max(hi, n)
	}
	return lo, hi
}

func main() {
	fmt.Println(add(2, 3))

	lo, hi := minMax([]int{7, 2, 9, 4})
	fmt.Println(lo, hi)

	n, err := strconv.Atoi("42")
	fmt.Println(n, err)

	n, err = strconv.Atoi("forty-two")
	fmt.Println(n, err)
}

Ele imprime:

5
2 9
42 <nil>
0 strconv.Atoi: parsing "forty-two": invalid syntax

add(a, b int) é a forma curta de add(a int, b int). minMax retorna dois valores, então os tipos do resultado vão entre parênteses, e quem chama recebe os dois com lo, hi :=. (min e max são builtins desde o Go 1.21.)

strconv.Atoi mostra o formato mais comum em Go: um valor e um erro. Quando a conversão funciona, err é nil. Quando falha, você recebe um zero value e um erro dizendo o que deu errado. Go não usa exceções para isso. O erro é só o último valor de retorno, e você confere com if err != nil. A parte sobre erros trata disso a fundo.

Você não ignora um segundo resultado sem querer

Uma função que retorna dois valores não cabe numa variável só:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n := strconv.Atoi("42")
	fmt.Println(n)
}

O compilador diz:

./main.go:9:7: assignment mismatch: 1 variable but strconv.Atoi returns 2 values

Se você realmente não quer o erro, precisa dizer isso com o identificador em branco: n, _ := strconv.Atoi("42"). Esse _ aparece no code review, e é justamente essa a ideia. Jogar um erro fora é uma decisão que alguém consegue ver.

Resultados nomeados

Go deixa você dar nomes aos valores de resultado, do mesmo jeito que nomeia parâmetros. Resultados nomeados são declarados no topo da função, começam no zero value, e um return sozinho retorna o que eles guardam.

package main

import (
	"fmt"
	"strings"
)

func splitPair(s string) (key, value string, ok bool) {
	i := strings.Index(s, "=")
	if i < 0 {
		return
	}
	key = s[:i]
	value = s[i+1:]
	ok = true
	return
}

func main() {
	k, v, ok := splitPair("host=example.com")
	fmt.Printf("%q %q %v\n", k, v, ok)

	k, v, ok = splitPair("no equals sign")
	fmt.Printf("%q %q %v\n", k, v, ok)
}

Ele imprime:

"host" "example.com" true
"" "" false

O return antecipado devolve "", "" e false, porque nada foi atribuído ainda e esses são os zero values. (A biblioteca padrão já tem essa função, como strings.Cut. Escrever na mão mostra a mecânica.)

Os nomes ajudam mais como documentação. (key, value string, ok bool) diz a quem lê o que volta. (string, string, bool) obriga a pessoa a adivinhar qual string é qual.

Um return sozinho assim se chama naked return. Numa função curta como essa, tudo bem. Numa longa, evite. Quem lê precisa rolar para cima para descobrir o que está sendo retornado, e uma variável declarada num bloco interno pode esconder um resultado sem você perceber. Go pega a pior versão disso:

package main

import (
	"fmt"
	"strconv"
)

func parsePort(s string) (port int, err error) {
	if s != "" {
		port, err := strconv.Atoi(s)
		if err != nil {
			return
		}
		fmt.Println("parsed", port)
	}
	return
}

func main() {
	fmt.Println(parsePort("8080"))
}

O compilador diz:

./main.go:12:4: result parameter port not in scope at return
./main.go:12:4: result parameter err not in scope at return

Dentro do if, o := declarou um port e um err novinhos, que escondem os resultados nomeados. Um naked return ali devolveria os de fora, que nunca foram preenchidos, então Go se recusa a compilar. Escrever return port, err explicitamente faz o código dizer o que quer dizer.

Funções variádicas

Uma função variádica aceita qualquer quantidade de argumentos de um tipo. Você marca o último parâmetro com ..., e dentro da função ele é um slice.

package main

import "fmt"

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

func main() {
	fmt.Println(sum())
	fmt.Println(sum(1, 2, 3))

	scores := []int{10, 20, 30}
	fmt.Println(sum(scores...))
}

Ele imprime:

0
6
60

sum() recebe um nums vazio, então o total é 0. sum(1, 2, 3) recebe um slice de três.

Quando você já tem um slice, não pode passá-lo como sum(scores), porque um []int não é um int. Escrever scores... espalha o slice na chamada. Go não copia os elementos quando você faz isso. O nums dentro da função é o mesmo slice que scores, e os dois usam o mesmo array. A parte sobre slices mostra por que isso importa.

Você já vinha chamando uma função variádica o tempo todo. fmt.Println é declarada como func Println(a ...any) (n int, err error).

Funções são valores

Uma função em Go é um valor com um tipo, como um int ou uma string. Você pode atribuí-la a uma variável, passá-la para outra função e retorná-la de uma.

package main

import (
	"fmt"
	"strings"
)

func apply(words []string, f func(string) string) []string {
	out := make([]string, 0, len(words))
	for _, w := range words {
		out = append(out, f(w))
	}
	return out
}

func exclaim(s string) string {
	return s + "!"
}

func main() {
	words := []string{"go", "is", "fun"}

	shout := strings.ToUpper
	fmt.Println(shout("hello"))

	fmt.Println(apply(words, exclaim))
	fmt.Println(apply(words, strings.ToUpper))
	fmt.Println(apply(words, func(s string) string {
		return "<" + s + ">"
	}))

	fmt.Printf("%T\n", exclaim)
}

Ele imprime:

HELLO
[go! is! fun!]
[GO IS FUN]
[<go> <is> <fun>]
func(string) string

shout := strings.ToUpper não tem parênteses depois de ToUpper, então não chama a função. Guarda a função. apply aceita qualquer função do tipo func(string) string e a chama uma vez por palavra. O seu exclaim, o strings.ToUpper da biblioteca e uma função literal escrita direto na chamada servem todos, porque todos têm esse tipo.

A última linha imprime o próprio tipo. O tipo de uma função são os tipos dos parâmetros e do resultado. O nome não faz parte dele.

Closures

Uma função literal pode usar variáveis da função em volta dela. Quando faz isso, ela se chama closure, e mantém essas variáveis vivas enquanto a closure existir.

package main

import "fmt"

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

func main() {
	a := newCounter()
	b := newCounter()

	fmt.Println(a(), a(), a())
	fmt.Println(b())
	fmt.Println(a())
}

Ele imprime:

1 2 3
1
4

newCounter retorna, e normalmente o count local dela sumiria. Mas a função que ela retornou ainda usa count, então count fica. Cada chamada a a soma um a ele.

b vem de outra chamada a newCounter, então ganhou o próprio count, começando do 0. Por isso b() imprime 1, e por isso a segue até 4 depois. Os dois contadores não compartilham nada.

Explicado como se você tivesse dez anos

Pense numa closure como uma função que carrega uma mochila.

Quando newCounter cria a funçãozinha de contar, ela coloca count na mochila dessa função antes de mandá-la embora. Aonde a função for, a mochila vai junto. Toda vez que você a chama, ela abre a mochila, soma um ao número lá dentro e diz o número novo.

Chame newCounter de novo e você ganha uma segunda função com uma segunda mochila. O que tem numa mochila não tem nada a ver com a outra.

A versão precisa

Uma função literal que se refere a uma variável de uma função externa captura a própria variável, não uma cópia do valor dela. A closure e a função externa compartilham essa única variável. Se qualquer uma das duas mudar a variável, a outra vê a mudança.

Como a closure pode viver mais que a chamada que declarou a variável, o compilador normalmente coloca essa variável no heap, onde ela vive enquanto algo apontar para ela. A parte sobre memória mostra como o compilador decide isso.

Onde a analogia falha: uma mochila dá a ideia de uma cópia particular, arrumada no momento em que a função foi criada. Não é uma cópia. Se a função externa mudar count depois de criar a closure, a closure vê o valor novo, porque as duas compartilham uma variável.

Closures num laço

Esse compartilhamento causava um bug famoso com variáveis de laço. Desde o Go 1.22, cada iteração de um laço for ganha uma variável nova, então uma closure criada no laço captura o valor daquela iteração:

package main

import "fmt"

func main() {
	var printers []func()
	for i := range 3 {
		printers = append(printers, func() {
			fmt.Println("i is", i)
		})
	}
	for _, p := range printers {
		p()
	}
}

Ele imprime:

i is 0
i is 1
i is 2

Antes do Go 1.22, o laço inteiro compartilhava um só i, e isso imprimia i is 3 três vezes. Você ainda vai ver código antigo que contorna o problema com uma linha como i := i dentro do laço. No Go 1.22 e depois, essa linha não faz nada, e você pode apagá-la.

defer: rode isto quando a função terminar

Uma instrução defer agenda uma chamada de função para rodar quando a função em volta retornar, e não antes. Quando há várias, elas rodam em ordem inversa.

package main

import "fmt"

func main() {
	fmt.Println("start")
	defer fmt.Println("deferred 1")
	defer fmt.Println("deferred 2")
	defer fmt.Println("deferred 3")
	fmt.Println("end of body")
}

Ele imprime:

start
end of body
deferred 3
deferred 2
deferred 1

O corpo roda de cima para baixo e imprime start e end of body. As três chamadas adiadas esperam. Quando main retorna, elas rodam, e a última adiada roda primeiro. Veja passo a passo:

código func main() { fmt.Println("start") defer fmt.Println("deferred 1") defer fmt.Println("deferred 2") defer fmt.Println("deferred 3") fmt.Println("end of body") } pilha defer deferred 1 deferred 2 deferred 3 (vazia) saída start end of body deferred 3 deferred 2 deferred 1 o corpo roda de cima para baixo, e a 1ª linha imprime "start" cada defer empilha sua chamada, e nada é impresso ainda a última linha do corpo roda e imprime "end of body" main retorna, e a chamada do topo da pilha roda: deferred 3 roda a chamada de baixo: deferred 2 o 1º defer roda por último, a pilha esvazia e main acaba

A pilha de defer do programa acima. O corpo imprime start, e depois cada defer empilha uma chamada sem rodá-la. O corpo imprime end of body e main retorna. As chamadas adiadas então saem do topo da pilha uma de cada vez, e por isso imprimem deferred 3, deferred 2 e deferred 1.

Aqui estão esses passos em palavras, caso a animação não rode para você:

  1. O corpo começa no topo e imprime start.
  2. Cada linha defer coloca sua chamada numa pilha. deferred 1 entra primeiro, depois deferred 2, depois deferred 3 no topo. Nada é impresso.
  3. A última linha do corpo imprime end of body.
  4. main retorna. A chamada do topo da pilha, deferred 3, roda primeiro.
  5. Depois roda deferred 2.
  6. Depois deferred 1, a primeira adiada, roda por último. A pilha fica vazia, e main termina.

Explicado como se você tivesse dez anos

Imagine uma pilha de pratos ao lado da pia. Toda vez que você diz defer, você escreve uma tarefa num prato e o coloca no topo da pilha. Você ainda não faz a tarefa. Só continua com o que estava fazendo.

Quando termina e vai sair da cozinha, você faz as tarefas dos pratos. Você só consegue pegar o prato de cima, então a última tarefa que anotou é a primeira que faz.

A versão precisa

Cada vez que uma instrução defer roda, Go avalia o valor da função e os argumentos dela na hora, e guarda a chamada. Quando a função retorna, seja por uma instrução return, por chegar à chave de fechamento ou por causa de um panic, as chamadas guardadas rodam na ordem último a entrar, primeiro a sair. Elas rodam depois que os valores de resultado são definidos e antes de a função devolver o controle a quem a chamou.

Um defer dentro de um laço guarda uma chamada por iteração, e nenhuma delas roda até a função inteira retornar. Vale saber disso antes de usar defer num laço que roda um milhão de vezes.

Onde a analogia falha: você escreveria a tarefa no prato em palavras e resolveria os detalhes depois. Go resolve os argumentos no momento em que você diz defer, como mostra a próxima seção.

Os argumentos são avaliados no defer, não quando a chamada roda

Uma chamada adiada espera até a função terminar, mas os argumentos dela não esperam. Eles são calculados na linha do defer, e isso surpreende quase todo mundo na primeira vez.

package main

import "fmt"

func main() {
	x := 1
	defer fmt.Println("deferred sees x =", x)
	defer func() {
		fmt.Println("closure sees x =", x)
	}()
	x = 2
	fmt.Println("main sets x =", x)
}

Ele imprime:

main sets x = 2
closure sees x = 2
deferred sees x = 1

O primeiro defer avaliou x na hora, quando ainda valia 1, e guardou esse 1 como argumento. Mudar x depois não fez diferença para ele.

O segundo defer guardou uma closure que não recebe argumentos. Não havia nada para avaliar antes. Quando ela finalmente rodou, leu x, e a essa altura x valia 2. Ela compartilha a variável, exatamente como o contador fazia.

Então, se você quer que uma chamada adiada veja o valor mais recente, adie uma closure. Se quer congelar o valor, passe-o como argumento.

O uso do dia a dia: limpeza

A maioria das instruções defer em código Go de verdade libera algo logo depois de adquiri-lo: fechar um arquivo, fechar o corpo de uma resposta, destravar um mutex. Escrever a limpeza na linha seguinte garante que você não vai esquecê-la num caminho de retorno que adicionar depois.

package main

import (
	"errors"
	"fmt"
)

type resource struct {
	name string
}

func open(name string) *resource {
	fmt.Println("open", name)
	return &resource{name: name}
}

func (r *resource) Close() error {
	fmt.Println("close", r.name)
	return nil
}

func process(name string) error {
	r := open(name)
	defer r.Close()

	if name == "broken.txt" {
		fmt.Println("bad data, returning early")
		return errors.New("bad data")
	}
	fmt.Println("processing", name)
	return nil
}

func main() {
	fmt.Println(process("notes.txt"))
	fmt.Println(process("broken.txt"))
}

Ele imprime:

open notes.txt
processing notes.txt
close notes.txt
<nil>
open broken.txt
bad data, returning early
close broken.txt
bad data

Aqui resource faz o papel de um arquivo, para o programa não mexer no disco. O método Close dele só imprime. (Métodos ganham uma parte própria. Por enquanto, leia r.Close() como “chame Close em r“.)

As duas chamadas fecharam o recurso, e as duas fecharam antes de process devolver o erro para main. O retorno antecipado não precisou de um Close próprio, porque o defer cobre todas as saídas da função.

O mesmo padrão protege um lock. Você vai escrever isto o tempo todo quando chegar à concorrência:

mu.Lock()
defer mu.Unlock()

Uma coisa que esse programa pula sem avisar: Close retorna um erro, e defer r.Close() joga esse erro fora. Para um arquivo que você só lê, é normal. Para um arquivo em que você escreveu, um close que falha pode significar dados perdidos. A próxima seção mostra um jeito de pegar isso.

Mudando o resultado com uma closure adiada

Uma closure adiada roda depois que os valores de retorno são definidos, e se esses resultados têm nome, ela pode lê-los e mudá-los. É o único lugar em que resultados nomeados são mais que documentação.

package main

import (
	"errors"
	"fmt"
)

func save(name string) (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("save %q: %w", name, err)
		}
	}()

	if name == "" {
		return errors.New("empty name")
	}
	return nil
}

func main() {
	fmt.Println(save("notes"))
	fmt.Println(save(""))
}

Ele imprime:

<nil>
save "": empty name

return errors.New("empty name") define err. Depois a closure adiada roda, vê um err diferente de nil e o troca por um erro embrulhado que acrescenta o nome do arquivo. Quem chamou recebe o valor trocado.

Com um resultado sem nome, a closure não teria um nome para alcançar o valor de retorno, e não conseguiria fazer isso. O mesmo truque deixa um Close adiado reportar o erro dele: if cerr := f.Close(); cerr != nil && err == nil { err = cerr }.

defer também roda quando uma função entra em panic, e é isso que torna recover possível. Os dois são tratados por completo na parte sobre erros.

O que lembrar

  • Parâmetros do mesmo tipo podem compartilhar um nome de tipo, e uma função pode retornar vários valores. O último costuma ser um error, conferido com if err != nil.
  • Resultados nomeados começam no zero value e documentam o que volta. Guarde os naked returns para funções curtas.
  • Um parâmetro variádico ...T é um slice dentro da função. Espalhe um slice existente nele com s....
  • Funções são valores com um tipo como func(string) string. Você pode guardá-las, passá-las e retorná-las.
  • Uma closure compartilha as variáveis que usa da função em volta, e as mantém vivas. Desde o Go 1.22, cada iteração do laço ganha a própria variável.
  • defer roda chamadas quando a função termina, a última a entrar sai primeiro. Os argumentos são avaliados na linha do defer. Uma closure adiada lê as variáveis quando roda, e pode mudar resultados nomeados.

Coloque a limpeza na linha logo depois da coisa que precisa ser limpa.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.