Comme beaucoup d'autres langages, Go possède des pointeurs. Si tu débutes avec les pointeurs, ils peuvent te sembler un peu mystérieux, mais une fois que tu t'y es habitué, ils sont assez simples. Ils sont un élément crucial de Go, alors prends le temps de bien les comprendre.
Avant d'entrer dans les détails, il vaut la peine de comprendre à quoi servent les pointeurs. Les pointeurs sont un moyen de partager de la mémoire avec d'autres parties de notre programme, ce qui est utile pour deux raisons principales :
Imaginons qu'on ait une variable entière ordinaire a :
var a int
Quand on déclare une variable, Go doit trouver un endroit en mémoire pour stocker sa valeur. Tout cela est largement abstrait pour nous : quand on doit récupérer la valeur stockée dans cette portion de mémoire, on peut simplement s'y référer par le nom de la variable.
Par exemple, quand on écrit a + 2, on récupère en fait la valeur stockée dans la mémoire associée à la variable a et on y ajoute 2.
De même, quand on a besoin de modifier la valeur dans la portion de mémoire de a, on peut utiliser le nom de la variable pour faire une affectation :
a = 3
La portion de mémoire associée à a stocke maintenant la valeur 3.
Alors que les variables nous permettent de faire référence à des valeurs en mémoire, il est parfois utile de connaître l'adresse mémoire vers laquelle pointe la variable. Les pointeurs contiennent les adresses mémoire de ces valeurs. On déclare une variable de type pointeur en préfixant le type sous-jacent par un astérisque :
var p *int // 'p' contains the memory address of an integer
Ici, on déclare une variable p de type « pointeur vers un int » (*int).
Cela signifie que p contiendra l'adresse mémoire d'un entier.
La valeur zéro des pointeurs est nil, car un pointeur nil ne contient aucune adresse mémoire.
Pour trouver l'adresse mémoire de la valeur d'une variable, on peut utiliser l'opérateur &.
Par exemple, si on veut trouver et stocker l'adresse mémoire de la variable a dans le pointeur p, on peut faire ceci :
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Quand on a un pointeur, on peut vouloir connaître la valeur stockée à l'adresse mémoire que le pointeur représente.
On peut le faire avec l'opérateur * :
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
L'opération *p récupère la valeur stockée à l'adresse mémoire contenue dans p.
Cette opération est souvent appelée « déréférencement ».
On peut aussi utiliser l'opérateur de déréférencement pour affecter une nouvelle valeur à l'adresse mémoire référencée par le pointeur :
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!
Affecter à *pa changera la valeur stockée à l'adresse mémoire que contient pa.
Comme pa contient l'adresse mémoire de a, en affectant à *pa, on modifie en fait la valeur de a !
Une mise en garde, toutefois : vérifie toujours qu'un pointeur n'est pas nil avant de le déréférencer.
Déréférencer un pointeur nil fera planter le programme à l'exécution !
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Jusqu'ici, on n'a vu que des pointeurs vers des valeurs primitives. On peut aussi créer des pointeurs vers des structures :
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
On aurait aussi pu créer un nouveau Person et stocker immédiatement un pointeur vers lui :
var p *Person
p = &Person{Name: "Peter", Age: 22}
Quand on a un pointeur vers une structure, on n'a pas besoin de déréférencer le pointeur avant d'accéder à l'un de ses champs :
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
Les slices et les maps sont des types particuliers, car leur implémentation contient déjà des pointeurs. Cela signifie que, la plupart du temps, on n'a pas besoin de créer des pointeurs pour ces types afin de partager l'adresse mémoire de leurs valeurs. Imaginons une fonction qui incrémente la valeur d'une clé dans une map :
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Si on crée une map et qu'on appelle cette fonction, les changements que la fonction a faits à la map subsistent après la fin de la fonction. C'est un comportement similaire à celui qu'on obtient avec un pointeur, mais remarque que dans cet exemple, on n'utilise ni référence, ni déréférencement, ni aucune autre syntaxe de pointeur :
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!
Il en va de même quand on modifie un élément existant dans un slice.
Cependant, les actions qui renvoient un nouveau slice, comme append, sont un cas particulier et peuvent ne pas modifier le slice en dehors de la fonction.
Cela vient de la façon dont les slices fonctionnent en interne, mais on ne rentrera pas dans les détails dans cet exercice, car c'est un sujet plus avancé.
Si tu es vraiment curieux, tu peux en lire plus à ce sujet dans Go Blog: Mechanics of 'append'
Une école locale près de chez toi possède une association d'élèves très active. L'association d'élèves est gérée par un président et, tous les 2 ans, des élections sont organisées pour élire un nouveau président.
Lors de l'élection de cette année, il a été décidé qu'un nouveau système numérique de comptage des votes était nécessaire. L'école a besoin de ton aide pour construire ce nouveau système.
L'une des premières choses dont le nouveau système de vote a besoin est un compteur de votes. Ce compteur permet de suivre les votes d'un candidat en particulier.
Crée une fonction NewVoteCounter qui accepte le nombre de votes initiaux d'un candidat et renvoie un pointeur vers un int, initialisé avec le nombre de votes initiaux donné.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
Tu disposes maintenant d'un moyen de créer de nouveaux compteurs ! Mais tu réalises que le nouveau système aura aussi besoin d'un moyen de récupérer le nombre de votes d'un compteur.
Crée une fonction VoteCount qui prend un compteur (*int) en argument et renvoie le nombre de votes du compteur.
Si le compteur vaut nil, tu dois considérer qu'il n'a aucun vote :
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
C'est enfin le moment de compter les votes ! Tu as maintenant besoin d'un moyen d'incrémenter les votes d'un compteur.
Crée une fonction IncrementVoteCount qui prend un compteur (*int) en argument et un nombre de votes, et incrémente le compteur de ce nombre de votes.
Tu peux supposer que le pointeur passé ne sera jamais nil.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Maintenant que tous les votes sont comptés, il est temps de préparer l'annonce des résultats pour toute l'école. Pour cela, tu remarques qu'avoir uniquement des compteurs de votes ne suffit pas. Il faut un moyen d'associer le nombre de votes à un candidat en particulier.
Crée une fonction NewElectionResult qui reçoit le nom d'un candidat et son nombre de votes et renvoie un nouveau résultat d'élection.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
La structure de résultat d'élection est déjà créée pour toi et est définie ainsi :
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
Il est temps d'annoncer le nouveau président à l'école !
Le président sera annoncé sur les petits panneaux d'affichage numériques de l'école.
Le message doit indiquer le nom du nouveau président et les votes qu'il a obtenus, au format suivant : <candidate_name> (<votes>).
Voici un exemple d'un tel message : "Peter (51)".
Crée une fonction DisplayResult qui recevra un *ElectionResult en argument et renverra une string avec le message à afficher.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Pour s'assurer que les résultats finaux étaient exacts, les votes ont été recomptés. Lors du recomptage, il s'est avéré que le nombre de votes de certains candidats était décalé de un.
Crée une fonction DecrementVotesOfCandidate qui reçoit les résultats finaux et le nom d'un candidat dont tu dois décrémenter le nombre de votes.
Les résultats finaux sont donnés sous la forme d'une map[string]int, où les clés sont les noms des candidats et les valeurs leur nombre total de votes.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Inscris-toi sur Exercism pour apprendre et maîtriser Go avec 34 concepts165 exercices, et un vrai mentorat humain, le tout gratuitement.