Funções do Go
Em Go, funções são cidadãos de primeira classe, funções são o componente mais básico do Go, e também o núcleo do Go.
Declaração
O formato de declaração de uma função é o seguinte
func nomeDaFunção([lista de parâmetros]) [valor de retorno] {
corpo da função
}Existem duas formas de declarar funções, uma é através da palavra-chave func para declaração direta, outra é através da palavra-chave var para declaração, como mostrado abaixo
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}A assinatura da função é composta pelo nome da função, lista de parâmetros e valor de retorno, abaixo está um exemplo completo, o nome da função é Sum, tem dois parâmetros a e b do tipo int, o tipo do valor de retorno é int.
func Sum(a int, b int) int {
return a + b
}Há também um ponto muito importante, ou seja, Go não suporta sobrecarga de funções, código como o abaixo não pode compilar
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}A filosofia do Go é que se as assinaturas são diferentes, então são duas funções completamente diferentes, então não deveriam ter o mesmo nome, sobrecarga de funções torna o código confuso e difícil de entender. Se essa filosofia está correta ou não é questão de opinião, pelo menos em Go você pode saber o que uma função faz apenas pelo nome, sem precisar descobrir qual sobrecarga é.
Parâmetros
Em Go, os nomes dos parâmetros podem não ter nome, geralmente isso é usado ao declarar interfaces ou tipos de função, mas para legibilidade geralmente é recomendado adicionar nomes aos parâmetros
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Para parâmetros do mesmo tipo, basta declarar o tipo uma vez, desde que sejam adjacentes
func Log(format string, a1, a2 any) {
...
}Parâmetros variáveis podem receber 0 ou mais valores, devem ser declarados no final da lista de parâmetros, o exemplo mais típico é a função fmt.Printf.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Vale mencionar que os parâmetros de função em Go são passados por valor, ou seja, ao passar parâmetros o valor do argumento é copiado. Se você está preocupado que passar slices ou maps vai copiar muita memória, posso dizer que não precisa se preocupar, porque esses dois tipos de dados são essencialmente ponteiros.
Valor de Retorno
Abaixo está um exemplo simples de valor de retorno de função, a função Sum retorna um valor do tipo int.
func Sum(a, b int) int {
return a + b
}Quando a função não tem valor de retorno, não precisa de void, basta não ter valor de retorno.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go permite que funções tenham múltiplos valores de retorno, neste caso precisa-se usar parênteses para envolver os valores de retorno.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 não pode ser divisor")
}
return a / b, nil
}Go também suporta valores de retorno nomeados, não podem ter o mesmo nome dos parâmetros, ao usar valores de retorno nomeados, a palavra-chave return não precisa especificar quais valores retornar.
func Sum(a, b int) (ans int) {
ans = a + b
return
}Assim como os parâmetros, quando há múltiplos valores de retorno nomeados do mesmo tipo, pode-se omitir a declaração de tipo repetida
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}Independente de como os valores de retorno nomeados são declarados, os valores após a palavra-chave return sempre têm a prioridade mais alta.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d não serão retornados
return a + b, a * b
}Funções Anônimas
Funções anônimas são funções sem assinatura, por exemplo a função func(a, b int) int abaixo, ela não tem nome, então só podemos chamá-la colocando parênteses logo após seu corpo.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}Ao chamar uma função, quando seu parâmetro é um tipo de função, o nome não importa mais, pode-se passar diretamente uma função anônima, como mostrado abaixo
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}Este é um exemplo de classificação com regras personalizadas, slices.SortFunc aceita dois parâmetros, um é o slice, outro é a função de comparação, se não considerar reutilização, pode-se passar diretamente uma função anônima.
Closure
Closure, em algumas linguagens também é chamado de expressão Lambda, usado junto com funções anônimas, closure = função + referência de ambiente, veja o exemplo abaixo
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}Saída
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512O valor de retorno da função Exp é uma função, aqui será chamada de função grow, cada vez que é chamada, a variável e cresce exponencialmente. A função grow referencia duas variáveis da função Exp - e e n, elas nasceram no escopo da função Exp, em circunstâncias normais, com o fim da chamada da função Exp, a memória dessas variáveis seria recuperada com o desempilhamento. Mas como a função grow as referencia, elas não podem ser recuperadas, em vez disso escapam para o heap, mesmo que o ciclo de vida da função Exp tenha terminado, o ciclo de vida das variáveis e e n não terminou, dentro da função grow ainda é possível modificar diretamente essas duas variáveis, a função grow é uma função closure.
Usando closure, pode-se implementar de forma muito simples uma função para calcular a sequência de Fibonacci, o código é o seguinte
func main() {
// 10 números de Fibonacci
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}A saída é
0
1
1
2
3
5
8
13
21
34Chamada Deferida
A palavra-chave defer pode fazer com que uma função seja chamada após um certo atraso, antes da função retornar todas as funções descritas por defer serão executadas uma por uma, veja o exemplo abaixo
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Saída
2
1Como defer é executado antes da função retornar, você também pode modificar o valor de retorno da função no defer
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Quando há múltiplas funções descritas por defer, elas serão executadas na ordem de último a entrar, primeiro a sair, como uma pilha.
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1Chamadas deferidas geralmente são usadas para liberar recursos de arquivos, fechar conexões de rede, etc., outro uso é capturar panic, mas isso será abordado na seção de tratamento de erros.
Loop
Embora não seja explicitamente proibido, geralmente não é recomendado usar defer em loops for, como mostrado abaixo
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}A saída é a seguinte
4
3
2
1
0O resultado deste código está correto, mas o processo talvez não esteja. Em Go, cada vez que um defer é criado, precisa-se alocar um espaço de memória na goroutine atual. Supondo que no exemplo acima não seja um simples loop for n, mas um fluxo de processamento de dados mais complexo, quando as requisições externas aumentam subitamente, um grande número de defers será criado em pouco tempo, quando o número de loops é grande ou incerto, isso pode levar a um aumento repentino no uso de memória, isso geralmente é chamado de vazamento de memória.
Pré-cálculo de Parâmetros
Para chamadas deferidas há alguns detalhes contra-intuitivos, por exemplo o caso abaixo
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}Esta armadilha é bastante sutil, o autor já passou meio dia sem conseguir descobrir a causa por causa disso, pode tentar adivinhar qual é a saída, a resposta é
2
3
1Muitas pessoas podem achar que a saída seria
3
2
1De acordo com a intenção do usuário, fmt.Println(Fn1()) deveria ser executado após o corpo da função terminar, fmt.Println de fato é executado por último, mas Fn1() é inesperado, o exemplo abaixo deixa isso mais claro.
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}Sua saída certamente será 3 e não 7, se usar closure em vez de chamada deferida, o resultado é diferente
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}A saída do closure é 7, e se combinar chamada deferida com closure?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Agora está correto, a saída é 7. Vamos modificar novamente, sem closure
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}A saída volta a ser 3. Através da comparação dos exemplos acima, pode-se perceber que este código
defer fmt.Println(sum(a,b))Na verdade é equivalente a
defer fmt.Println(3)Go não vai esperar até o último momento para chamar a função sum, a função sum já foi chamada antes da chamada deferida ser executada, e passada como parâmetro para fmt.Println. Em resumo, para a função diretamente afetada por defer, seus parâmetros são pré-calculados, isso causa o fenômeno estranho no primeiro exemplo, para este caso, especialmente quando o valor de retorno de uma função é usado como parâmetro em uma chamada deferida, deve-se ter atenção especial.
