Go: Funções em Go
As funções são os “blocos de Lego” do código Go — o Go leva o conceito de “funções de primeira classe” ao extremo, simplificando a programação combinatória.
A filosofia de design de funções do Go: Maximizar a reutilização com sintaxe mínima. Vários valores de retorno, valores de retorno nomeados, argumentos variáveis, closures e passagem de funções por valor — recursos que exigem uma “estrutura” ou “biblioteca” em outras linguagens fazem parte da sintaxe nativa do Go.
1. Você aprenderá
- Os 4 elementos da definição da função
func - Vários valores de retorno e valores de retorno nomeados
- Utilizando o argumento variável
... - Funções anônimas e closures
- A função
inite a inicialização do pacote - Funções como argumentos e valores de retorno (funções de ordem superior)
- Criação de pipelines de processamento de dados com funções
2. A história real de um engenheiro de dados
(1) Problema: as funções em Java só podem retornar um único valor
Bob é um engenheiro de dados que recentemente precisou escrever uma função de processamento de dados:
“Preciso escrever uma função
parseUserDataque retorne tanto o objeto de usuário analisado quanto um código de erro; ela também precisa registrar o tempo de análise e retornar mensagens de aviso — mas, como o Java permite apenas um único valor de retorno, sou obrigado a enfiar tudo em um único objetoResult, fazendo com que o código pareça um emaranhado de espaguete.”
Ele abriu o código Java:
// Java: Can only return one value, so it must be packaged
public class ParseResult {
public User user;
public int errorCode;
public long durationMs;
public List<String> warnings;
}
public ParseResult parseUserData(String raw) {
// All 5 return values are listed here
return new ParseResult(...);
}
Durante uma revisão, um colega reclamou: “Sua função é como um conjunto de bonecas russas — se eu precisar de cinco campos, tenho que ir descascando camada por camada”.
(2) Solução em Go
As funções do Go oferecem suporte nativo a vários valores de retorno:
// user_parser.go
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
// Multiple return values: user + errorCode + durationMs + warnings
func parseUserData(raw string) (User, int, time.Duration, []string) {
start := time.Now()
var warnings []string
parts := strings.Split(raw, ",")
if len(parts) != 3 {
return User{}, 400, time.Since(start), []string{"Format error: 3 fields required"}
}
age, err := strconv.Atoi(parts[1])
if err != nil {
return User{}, 400, time.Since(start), []string{"Invalid age format"}
}
if age < 0 || age > 150 {
warnings = append(warnings, "Age Anomaly")
}
user := User{Name: parts[0], Age: age, City: parts[2]}
return user, 200, time.Since(start), warnings
}
type User struct {
Name string
Age int
City string
}
func main() {
user, code, duration, warnings := parseUserData("Alice,28,Shanghai")
fmt.Printf("Status: %d\n", code)
fmt.Printf("User: %+v\n", user)
fmt.Printf("Duration: %v\n", duration)
fmt.Printf("Warnings: %v\n", warnings)
}
Resultado:
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []
(3) Desempenho: Funções em Go x outras linguagens
| Recurso | C | Java | Python | Go |
|---|---|---|---|---|
| Vários valores de retorno | ❌ Requer uma estrutura de envoltório | ❌ Requer uma classe de envoltório | ✅ Tupla | ✅ Nativo |
| Nome do valor de retorno | ❌ | ❌ | ❌ | ✅ Nativo |
| Funções como valores | Ponteiros de função | Lambda | Elementos de primeira classe | ✅ Elementos de primeira classe |
| Fechamentos | ✅ (Complexo) | Lambda | ✅ | ✅ Conciso |
| Argumentos de comprimento variável | ✅ (stdarg) | ✅ varargs | ✅ *args | ✅ ... |
func foo() (T, error)). Essa é a pedra angular da filosofia de tratamento de erros do Go.
sequenceDiagram
participant Caller
participant Function as parseUserData()
participant Parser as Internal Logic
Caller->>Function: parseUserData(raw)
Function->>Parser: Split fields
Parser-->>Function: name, age, city
Function-->>Caller: user + 200 + duration + warnings
Note over Caller: Multiple return values: receive<br/>user/status/time/warnings simultaneously
3. Definição da função func
(1) Os quatro elementos da definição de uma função
func functionName(param1 type1, param2 type2) returnType {
// Function Body
return value
}
| Elemento | Palavra-chave | Obrigatório? |
|---|---|---|
| Nome da função | funcName |
Sim |
| Lista de parâmetros | (param type, ...) |
Sim |
| Tipo de retorno | returnType |
Não (pode ser omitido se não houver valor de retorno) |
| Corpo da função | { ... } |
Sim |
(2) ▶ Exemplo: As funções básicas são definidas de quatro maneiras
package main
import "fmt"
// Form 1: No parameters, no return value
func sayHello() {
fmt.Println("Hello!")
}
// Form 2: With parameters but no return value
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// Form 3: With parameters and a return value
func add(a, b int) int {
return a + b
}
// Form 4: Multiple Parameters and Multiple Return Values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
sayHello() // Hello!
greet("Alice") // Hello, Alice!
fmt.Println(add(2, 3)) // 5
result, err := divide(10.0, 2.0)
fmt.Printf("%.2f, err=%v\n", result, err) // 5.00, err=<nil>
}
Saída:
Hello!
Hello, Alice!
5
5.00, err=<nil>
(3) Abreviações de parâmetros
Parâmetros contínuos do mesmo tipo podem ser combinados em um único tipo:
func add(a, b int) int // equivalent to a int, b int
func rect(w, h int) (int, int) // both parameters are int
4. Vários valores de retorno
(1) Definição de vários valores de retorno
package main
import "fmt"
func swap(a, b string) (string, string) {
return b, a
}
func main() {
x, y := swap("hello", "world")
fmt.Println(x, y) // world hello
}
(2) ▶ Exemplo: Valor + Erro (padrão de assinatura do Go)
package main
import (
"errors"
"fmt"
)
func findUser(id int) (string, error) {
if id <= 0 {
return "", errors.New("invalid id")
}
if id == 999 {
return "", fmt.Errorf("user %d not found", id)
}
return fmt.Sprintf("User-%d", id), nil
}
func main() {
user, err := findUser(1)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Found: %s\n", user)
// To ignore a return value: use _
_, err = findUser(999)
fmt.Printf("Ignored: err=%v\n", err)
}
Saída:
Found: User-1
Ignored: err=user 999 not found
(3) Como nomear o valor de retorno
Os valores de retorno nomeados são declarados no início da função; a instrução return retorna automaticamente essas variáveis:
package main
import "fmt"
func calc(a, b int) (sum, diff, product int) {
sum = a + b
diff = a - b
product = a * b
return // bare return, automatically returns sum/diff/product
}
func main() {
s, d, p := calc(10, 3)
fmt.Printf("sum=%d, diff=%d, product=%d\n", s, d, p)
}
Saída:
sum=13, diff=7, product=30
(4) Valores de retorno múltiplos versus valores de retorno nomeados
| Cenário | Recomendação |
|---|---|
| Retorna 1 valor | Valor de retorno padrão |
| Retorna 2 valores (valor + erro) | Valor de retorno padrão |
| Retorna 3 ou mais valores | Nomeie os valores de retorno (para maior clareza) |
O valor de retorno precisa ser modificado em defer |
O valor de retorno deve ser nomeado |
defer (conforme mostrado no exemplo completo desta lição).
5. Parâmetros variádicos
(1) Sintaxe de argumentos variáveis ...
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20)) // 30
fmt.Println(sum()) // 0
// Break the slice apart and pass it in: nums...
nums := []int{1, 2, 3, 4, 5}
fmt.Println(sum(nums...)) // 15
}
Saída:
6
30
0
15
(2) ▶ Exemplo: Argumentos variáveis + cadeias formatadas
package main
import "fmt"
// Similar to `fmt.Printf`: The first argument is fixed, and the rest are variable.
func logMessage(level string, args ...interface{}) {
fmt.Printf("[%s] ", level)
fmt.Println(args...) // spread slice and pass to Println
}
func main() {
logMessage("INFO", "Server started", "on port", 8080)
logMessage("ERROR", "Database connection failed:", "timeout 5s")
logMessage("DEBUG")
}
Saída:
[INFO] Server started on port 8080
[ERROR] Database connection failed: timeout 5s
[DEBUG]
(3) Restrições a argumentos variáveis
| Restrição | Descrição |
|---|---|
| Até 1 argumento variável | func foo(a int, b ...int) ✅ |
| Os parâmetros variádicos devem ser os últimos | func foo(a ...int, b int) ❌ |
| Os tipos devem ser consistentes | Para permitir vários tipos, use ...interface{} |
6. Funções anônimas e closures
(1) Funções anônimas (literais de função)
As funções anônimas não têm nome e podem ser atribuídas a uma variável ou chamadas diretamente:
package main
import "fmt"
func main() {
// Assign to a variable
add := func(a, b int) int {
return a + b
}
fmt.Println(add(2, 3)) // 5
// Direct Call
func(x int) {
fmt.Printf("Anonymous function: x=%d\n", x)
}(42)
}
Saída:
5
Anonymous function: x=42
(2) Closures: Captura de variáveis externas
Um closure = uma função + as variáveis externas às quais ela faz referência. Os closures permitem que as funções “lembrem” do ambiente em que foram criadas:
(3) ▶ Exemplo: Implementação de um contador usando closures
package main
import "fmt"
// Returns a closure: increments by 1 on each call
func makeCounter() func() int {
count := 0 // variable captured by closure
return func() int {
count++
return count
}
}
func main() {
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3
// Each counter is an independent closure.
another := makeCounter()
fmt.Println(another()) // 1 (restarts counting)
}
Saída:
1
2
3
1
(4) Closures x Funções comuns
| Dimensão | Função regular | Fechamento |
|---|---|---|
| Com estado | Sem estado | Com estado (variáveis capturadas) |
| Memória | Estática | Sempre que uma nova instância é criada |
| Casos de uso | Computação pura | Fábricas, decoradores, callbacks |
7. A função init e a inicialização do pacote
(1) Características da função init
| Recurso | Descrição |
|---|---|
| Sem parâmetros, sem valor de retorno | func init() |
| Invocação automática | Executada automaticamente quando o pacote é importado |
| Múltiplas | Um pacote pode ter várias funções de inicialização (executadas na ordem em que são declaradas) |
| Antes da função main | Executado antes da função main() |
(2) ▶ Exemplo: Inicialização de pacote (Registro)
// registry.go
package main
import "fmt"
var registry = make(map[string]int)
func init() {
registry["version"] = 1
registry["max_connections"] = 100
fmt.Println("[init] registry initialized")
}
func init() {
registry["debug"] = 1
fmt.Println("[init] debug enabled")
}
func main() {
fmt.Printf("Registry: %+v\n", registry)
}
Saída:
[init] registry initialized
[init] debug enabled
Registry: map[debug:1 max_connections:100 version:1]
(3) init x main
| Função | Quando é chamada | Finalidade |
|---|---|---|
init() |
Executado automaticamente quando o pacote é importado | Inicializa variáveis globais e registra drivers |
main() |
Executado quando o programa é iniciado (uma vez) | Ponto de entrada do programa |
8. Funções como argumentos e valores de retorno (funções de ordem superior)
As funções em Go são cidadãos de primeira classe — elas podem ser atribuídas a variáveis, passadas como argumentos e retornadas como valores.
(1) Funções como argumentos (callbacks)
package main
import "fmt"
// The second parameter is the function type: it accepts an `int` and returns an `int`.
func process(nums []int, callback func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = callback(n)
}
return result
}
func double(n int) int { return n * 2 }
func square(n int) int { return n * n }
func main() {
nums := []int{1, 2, 3, 4, 5}
doubled := process(nums, double)
fmt.Println("doubled:", doubled)
squared := process(nums, square)
fmt.Println("squared:", squared)
}
Saída:
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]
(2) Funções como valores de retorno (fábrica)
package main
import "fmt"
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add10 := makeAdder(10)
add100 := makeAdder(100)
fmt.Println(add10(5)) // 15
fmt.Println(add100(5)) // 105
}
Saída:
15
105
(3) ▶ Exemplo: Tipos de função + funções de ordem superior (map/filter/reduce)
package main
import "fmt"
func mapFunc(nums []int, f func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = f(n)
}
return result
}
func filterFunc(nums []int, predicate func(int) bool) []int {
var result []int
for _, n := range nums {
if predicate(n) {
result = append(result, n)
}
}
return result
}
func reduceFunc(nums []int, initial int, f func(int, int) int) int {
acc := initial
for _, n := range nums {
acc = f(acc, n)
}
return acc
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 1. Map: Each element *2
doubled := mapFunc(nums, func(n int) int { return n * 2 })
// 2. Filter: Keep even numbers
evens := filterFunc(nums, func(n int) bool { return n%2 == 0 })
// 3. Reduce: Sum
sum := reduceFunc(nums, 0, func(acc, n int) int { return acc + n })
fmt.Printf("doubled: %v\n", doubled)
fmt.Printf("evens: %v\n", evens)
fmt.Printf("sum: %d\n", sum)
}
Saída:
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55
9. Exemplo completo: Pipeline de processamento de dados
Combine todos os recursos da função para criar um pipeline de dados ETL (Extract-Transform-Load):
// pipeline.go
package main
import (
"fmt"
"strings"
"time"
)
// Data Sources
func extract() []string {
return []string{
" Alice,28,Shanghai ",
"Bob,32,Beijing",
"CHARLIE,45,Guangzhou",
"", // empty data
"Dave,abc,ErrorCity", // anomalous data
}
}
// Step 1: Remove Spaces + Split Fields
func trim(s string) []string {
return strings.Split(strings.TrimSpace(s), ",")
}
// Step 2: Convert to uppercase
func upper(s []string) []string {
for i, v := range s {
s[i] = strings.ToUpper(v)
}
return s
}
// Step 3: Verify the number of fields
func validate(s []string) (string, bool) {
if len(s) != 3 || s[0] == "" {
return "", false
}
return strings.Join(s, "|"), true
}
// Pipeline Function: Combining 3 Steps + Defer Report
func processPipeline(name string, data []string) (valid int, errors int) {
defer func() {
// Name the return value so that `defer` can modify the result
fmt.Printf("[%s] Completed: valid=%d errors=%d duration=%v\n",
name, valid, errors, time.Since(startTime))
}()
for _, raw := range data {
s := trim(raw)
if len(s) < 3 {
errors++
continue
}
s = upper(s)
if result, ok := validate(s); ok {
valid++
fmt.Printf(" -> %s\n", result)
} else {
errors++
}
}
return valid, errors
}
var startTime = time.Now()
func main() {
fmt.Println("=== Start of Data Pipeline ===")
// Combining Steps Using Functions as Arguments
data := extract()
valid, errors := processPipeline("ETL-1", data)
fmt.Printf("\nSummary: valid=%d, errors=%d\n", valid, errors)
}
Resultado esperado:
=== Start of Data Pipeline ===
-> ALICE|28|SHANGHAI
-> BOB|32|BEIJING
-> CHARLIE|45|GUANGZHOU
-> DAVE|ABC|ERRORCITY
[ETL-1] Completed: valid=4 errors=1 duration=2.5µs
Summary: valid=4, errors=1
startTime deve ser declarado no nível do arquivo (não é possível usar := dentro de init). Essa é a diferença sutil entre a função init e as declarações var — var está no nível do pacote, enquanto init está no nível da função.
❓ Perguntas Frequentes
P: As funções do Go suportam sobrecarga? R: Não, não suportam. O Go optou deliberadamente por não introduzir a sobrecarga de funções — para evitar a complexidade da “ambiguidade de sobrecarga” encontrada em C++ e Java. Se você precisar de um comportamento semelhante, use argumentos variáveis
func foo(a ...int)ou nomes de função diferentes.
P: Qual é a diferença entre retornar vários valores e retornar uma estrutura? R: O retorno de vários valores é adequado para cenários que envolvem de 2 a 3 valores (especialmente um valor mais um erro), pois resulta em um código mais conciso; para mais de 3 valores, recomenda-se usar uma estrutura, já que as estruturas possuem nomes de campos, o que melhora a legibilidade.
P: Quais são as utilidades práticas de nomear valores de retorno? R: (1) As instruções automáticas
returnreduzem o código; (2) Os valores de retorno podem ser modificados emdefer(Importante! Isso é usado no exemplo completo desta lição); (3) Documentação — as assinaturas de função servem como documentação.
P: Os closures podem causar vazamentos de memória? R: Sim. O tempo de vida das variáveis capturadas por um closure é prolongado até que o próprio closure termine. Manter um closure por um longo período (como em um cache ou dentro de uma goroutine) impede que as variáveis sejam coletadas pelo coletor de lixo.
P: A função
initpode ser chamada manualmente? R: Não. A funçãoinitsó pode ser chamada automaticamente pelo runtime do Go, e a funçãoinitde cada pacote é executada apenas uma vez. Tentar chamar a funçãoinitmanualmente resultará em um erro de compilação.
P: Qual é a diferença entre argumentos variádicos e argumentos de fatia? R: Um argumento variádico
nums ...inté, essencialmente, uma[]intfatia dentro da função, mas as convenções de chamada diferem:foo(1,2,3)versusfoo([]int{1,2,3}...). O primeiro é um recurso sintático.
P: O uso de funções como valores acarreta uma perda de desempenho? R: Os compiladores modernos otimizam os valores das funções (chamando-os diretamente), portanto, praticamente não há sobrecarga. No entanto, armazenar uma função em um mapa ou fatia e chamá-la com frequência resultará em uma leve sobrecarga devido ao endereçamento indireto — o que geralmente é insignificante.
📖 Resumo
- A definição de uma função consiste em 4 elementos:
func+ nome + lista de parâmetros + tipo de retorno - Os valores de retorno múltiplos são uma característica marcante da linguagem Go e são comumente usados no padrão de tratamento de erros
(value, error) - Atribuir nomes aos valores de retorno torna as instruções
returnmais concisas e permite quedefermodifique o valor de retorno - Argumentos variádicos
...como alternativa à sobrecarga; a fatia é definida dentro da função - Os closures capturam variáveis externas, permitindo a implementação de funções com estado (como contadores e funções de fábrica)
inité executado automaticamente quando o pacote é importado e é usado para inicializar o estado global- As funções são entidades de primeira classe: podem ser atribuídas a variáveis, passadas como argumentos e retornadas; suportam funções de ordem superior (map/filter/reduce)
📝 Exercícios
-
Problema básico (Dificuldade ⭐): Escreva uma função
max(nums ...int) intque retorne o valor máximo entre todos os argumentos. A função deve usar argumentos variáveis; ao chamarmax(1, 5, 3, 9, 2), o resultado deve ser 9. -
Problema avançado (Dificuldade ⭐⭐): Implemente um
makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int)para simular uma conta bancária:depositaumenta o saldo e retorna o novo saldo;withdrawdeduz fundos (retornafalsese o saldo for insuficiente);balanceconsulta o saldo. Você deve usar closures. -
Problema de Desafio (Dificuldade ⭐⭐⭐): Implemente um pipeline de dados funcional: Dada uma entrada
[]string(uma sequência de números), use funções de ordem superior para executar sequencialmente as quatro etapasparse -> filter(>10) -> map(*2) -> sum. Cada etapa deve ser uma função independente, e as funções combinadas devem produzir o resultado final. Por exemplo:["1", "15", "3", "20"]→parse [1,15,3,20]→filter>10 [15,20]→map*2 [30,40]→sum 70.