Uma função permite-te agrupar código numa unidade reutilizável.
É composta pela palavra-chave func, pelo nome da função e por uma lista de zero ou mais parâmetros e tipos entre parênteses, separados por vírgulas.
Todos os parâmetros têm de ter um tipo explícito; não existe inferência de tipos para os parâmetros. Não existem valores predefinidos para os parâmetros, por isso todos os parâmetros de função são obrigatórios.
import "fmt"
// No parameters
func PrintHello() {
fmt.Println("Hello")
}
// One parameter
func PrintHelloName(name string) {
fmt.Println("Hello " + name)
}
Os parâmetros do mesmo tipo podem ser declarados em conjunto, seguidos de uma única declaração de tipo.
import "fmt"
func PrintGreetingName(greeting, name string) {
fmt.Println(greeting + " " + name)
}
Vamos ver rapidamente dois termos que são frequentemente confundidos: parameters e arguments.
Os parâmetros de função são os nomes definidos na assinatura da função, como greeting e name na função PrintGreetingName acima.
Os argumentos de função são os valores concretos passados aos parâmetros da função quando invocamos a função.
Por exemplo, no exemplo abaixo, "Hello" e "Katrina" são os argumentos passados aos parâmetros greeting e name:
PrintGreetingName("Hello", "Katrina")
Aos parâmetros da função seguem-se zero ou mais valores devolvidos, que também têm de ter um tipo explícito.
Os valores devolvidos únicos são apresentados sem parênteses; os valores devolvidos múltiplos são envolvidos em parênteses.
Os valores são devolvidos ao código que fez a chamada através da palavra-chave return.
Pode haver várias instruções return numa função.
A execução da função termina assim que encontra uma dessas instruções return.
Se uma função tiver de devolver vários valores, estes são separados por vírgulas.
Podes encontrar mais informações sobre o uso idiomático de múltiplos valores devolvidos no conceito associado.
func Hello(name string) string {
return "Hello " + name
}
func HelloAndGoodbye(name string) (string, string) {
return "Hello " + name, "Goodbye " + name
}
Invocar uma função consiste em indicar o nome da função e passar argumentos para cada um dos parâmetros da função, entre parênteses.
import "fmt"
// No parameters, no return value
func PrintHello() {
fmt.Println("Hello")
}
// Called like this:
PrintHello()
// One parameter, one return value
func Hello(name string) string {
return "Hello " + name
}
// Called like this:
greeting := Hello("Dave")
// Multiple parameters, multiple return values
func SumAndMultiply(a, b int) (int, int) {
return a+b, a*b
}
// Called like this:
aplusb, atimesb := SumAndMultiply(a, b)
Tal como os parâmetros, os valores devolvidos podem ter um nome, opcionalmente.
Se forem usados valores devolvidos com nome, uma instrução return sem argumentos devolve esses valores.
A isto dá-se o nome de devolução sem argumentos.
func SumAndMultiplyThenMinus(a, b, c int) (sum, mult int) {
sum, mult = a+b, a*b
sum -= c
mult -= c
return
}
Também é importante clarificar o conceito de passagem por valor e de passagem por referência.
Primeiro, vamos clarificar a passagem por valor.
No exemplo abaixo, quando passamos a variável val à função MultiplyByTwo, passamos uma cópia de val.
Por isso, newVal tem o valor atualizado 4, mas a variável original val continua a ser 2.
Nos bastidores, o Go faz essencialmente uma cópia do valor original, para que apenas essa cópia, ou seja, v, seja modificada pela função.
val := 2
func MultiplyByTwo(v int) int {
v = v * 2
return v
}
newVal := MultiplyByTwo(val)
// newval is 4, val is still 2 because only a copy of its value was passed into the function
Em rigor, no Go todos os argumentos são passados por valor, ou seja, é feita uma cópia do valor ou dos dados fornecidos à função.
Mas se não quiseres fazer uma cópia dos dados passados a uma função e quiseres alterar esses dados dentro da função, deves usar pointers como argumentos, ou seja, a passagem por referência.
Usamos um pointer para conseguir a passagem por referência.
Ao passar argumentos pointer para uma função, podemos modificar os dados subjacentes passados à função em vez de operar apenas sobre uma cópia desses dados.
Por agora, basta saber que os tipos de ponteiro se reconhecem pelo * à frente do tipo na assinatura da função.
func HandlePointers(x, y *int) {
// Some logic to handle integer values referenced by pointers x and y
}
Se o conceito de pointer te parecer confuso, não há problema.
Temos uma secção dedicada mais à frente no programa de estudos para te ajudar a dominar os ponteiros.
Repara que slices e maps são exceções à regra acima mencionada.
Quando passamos um slice ou um map como argumentos para uma função, são tratados como tipos de ponteiro, mesmo que não haja um * explícito no tipo.
Isto significa que, se passarmos um slice ou um map para uma função e modificarmos os seus dados subjacentes, as alterações refletem-se no slice ou map original.
As funções em Go são consideradas cidadãos de primeira classe, o que as torna muito poderosas. Existem vários outros conceitos à volta das funções, como métodos e funções anónimas, que vais encontrar mais à frente no teu percurso.