Tal como muitas outras linguagens, o Go tem ponteiros. Se és novo no que toca a ponteiros, podem parecer um pouco misteriosos, mas assim que te habituares a eles vão parecer-te bastante simples. São uma parte crucial do Go, por isso dedica algum tempo a compreendê-los bem.
Antes de entrar nos detalhes, vale a pena perceber para que servem os ponteiros. Os ponteiros são uma forma de partilhar memória com outras partes do nosso programa, o que é útil por duas grandes razões:
Digamos que temos uma variável inteira normal a:
var a int
Quando declaramos uma variável, o Go tem de encontrar um lugar na memória para guardar o seu valor. Isto está em grande parte abstraído de nós: quando precisamos de obter o valor guardado nesse pedaço de memória, basta-nos usar o nome da variável.
Por exemplo, quando escrevemos a + 2, estamos efetivamente a obter o valor guardado na memória associada à variável a e a somar-lhe 2.
Do mesmo modo, quando precisamos de alterar o valor no pedaço de memória de a, podemos usar o nome da variável para fazer uma atribuição:
a = 3
O pedaço de memória associado a a passa agora a guardar o valor 3.
Embora as variáveis nos permitam referir valores na memória, por vezes é útil saber o endereço de memória para o qual a variável aponta. Os ponteiros guardam os endereços de memória desses valores. Declaras uma variável com um tipo ponteiro ao colocar um asterisco antes do tipo subjacente:
var p *int // 'p' contains the memory address of an integer
Aqui declaramos uma variável p do tipo "ponteiro para int" (*int).
Isto significa que p vai guardar o endereço de memória de um inteiro.
O valor zero dos ponteiros é nil, porque um ponteiro nil não guarda nenhum endereço de memória.
Para encontrar o endereço de memória do valor de uma variável, podemos usar o operador &.
Por exemplo, se quisermos encontrar e guardar o endereço de memória da variável a no ponteiro p, podemos fazer o seguinte:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Quando temos um ponteiro, podemos querer saber o valor guardado no endereço de memória que o ponteiro representa.
Podemos fazê-lo com o operador *:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
A operação *p obtém o valor guardado no endereço de memória que está em p.
A esta operação dá-se muitas vezes o nome de "desreferenciação".
Também podemos usar o operador de desreferenciação para atribuir um novo valor ao endereço de memória referenciado pelo ponteiro:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
Atribuir a *pa altera o valor guardado no endereço de memória que pa contém.
Como pa contém o endereço de memória de a, ao atribuir a *pa estamos efetivamente a alterar o valor de a!
Um aviso, no entanto: verifica sempre se um ponteiro não é nil antes de o desreferenciares.
Desreferenciar um ponteiro nil faz o programa rebentar em tempo de execução!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Até agora só vimos ponteiros para valores primitivos. Também podemos criar ponteiros para structs:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
Também podíamos ter criado um novo Person e guardado logo um ponteiro para ele:
var p *Person
p = &Person{Name: "Peter", Age: 22}
Quando temos um ponteiro para uma struct, não precisamos de desreferenciar o ponteiro antes de aceder a um dos campos:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
Os slices e os mapas são tipos especiais porque já têm ponteiros na sua implementação. Isto significa que, na maioria das vezes, não precisamos de criar ponteiros para estes tipos para partilhar o endereço de memória dos seus valores. Imagina que temos uma função que incrementa o valor de uma chave num mapa:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Se criarmos um mapa e chamarmos esta função, as alterações que a função fez ao mapa mantêm-se depois de a função terminar. É um comportamento semelhante ao que teríamos se usássemos um ponteiro, mas repara que neste exemplo não estamos a usar qualquer referência/desreferenciação nem qualquer outra sintaxe de ponteiros:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
O mesmo se aplica quando alteramos um elemento já existente num slice.
No entanto, as ações que devolvem um novo slice, como append, são um caso especial e podem não modificar o slice fora da função.
Isto deve-se à forma como os slices funcionam internamente, mas não vamos abordar isto em detalhe neste exercício, porque é um tópico mais avançado.
Se estiveres mesmo curioso, podes ler mais sobre isto em Go Blog: Mechanics of 'append'
Uma escola local perto de ti tem uma associação de estudantes muito ativa. A associação de estudantes é gerida por um presidente e, de 2 em 2 anos, realizam-se eleições para eleger um novo presidente.
Nas eleições deste ano, decidiu-se que era necessário um novo sistema digital para contar os votos. A escola precisa da tua ajuda para construir este novo sistema.
Uma das primeiras coisas de que o novo sistema de votação precisa é um contador de votos. Este contador serve para acompanhar os votos que um determinado candidato tem.
Cria uma função NewVoteCounter que aceita o número de votos iniciais de um candidato e devolve um apontador para um int, inicializado com o número de votos iniciais indicado.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
Já tens forma de criar novos contadores! Mas agora reparas que o novo sistema também vai precisar de uma forma de obter o número de votos de um contador.
Cria uma função VoteCount que recebe um contador (*int) como argumento e devolve o número de votos do contador.
Se o contador for nil, deves assumir que o contador não tem votos:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
É finalmente a altura de contar os votos! Agora precisas de uma forma de incrementar os votos de um contador.
Cria uma função IncrementVoteCount que recebe um contador (*int) como argumento e um número de votos, e incrementa o contador com esse número de votos.
Podes assumir que o apontador recebido nunca será nil.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Com todos os votos contados, é altura de preparar o anúncio dos resultados para toda a escola. Para isso, reparas que ter apenas contadores de votos não é suficiente. É preciso uma forma de associar o número de votos a um determinado candidato.
Cria uma função NewElectionResult que recebe o nome de um candidato e o seu número de votos e devolve um novo resultado eleitoral.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
A struct do resultado eleitoral já foi criada para ti e está definida assim:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
É altura de anunciar o novo presidente à escola!
O presidente será anunciado nos pequenos painéis de mensagens digitais que a escola tem.
A mensagem deve mostrar o nome do novo presidente e os votos que teve, no seguinte formato: <candidate_name> (<votes>).
Este é um exemplo dessa mensagem: "Peter (51)".
Cria uma função DisplayResult que recebe um *ElectionResult como argumento e devolve uma string com a mensagem a apresentar.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Para garantir que os resultados finais estavam corretos, os votos foram recontados. Na recontagem, descobriu-se que o número de votos de alguns candidatos estava errado por um.
Cria uma função DecrementVotesOfCandidate que recebe os resultados finais e o nome de um candidato cujo número de votos deves decrementar.
Os resultados finais são dados na forma de um map[string]int, em que as chaves são os nomes dos candidatos e os valores são o seu total de votos.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Inscreve-te no Exercism para aprenderes e dominares Go com 34 conceitos165 exercícios, e mentoria humana real, tudo grátis.