Hostinger

Pointeurs en Go (Golang) 😰

Qu'est-ce qu'une variable ?

On a déjà parlé des variables, mais nous allons les revoir, car pour bien comprendre les pointeurs, il est essentiel de bien maîtriser les variables.

Une variable est un espace mémoire qui stocke une valeur et qui est accessible via un nom.

Variable en Go

Chaque variable possède également une adresse.

Adresse d'une variable en Go

Qu’est-ce qu’un pointeur en Go ?

Un pointeur en Go est une variable. Elle contient l’adresse mémoire d’une autre variable au lieu de contenir directement sa valeur.

En Go, les pointeurs sont utilisés pour partager des données entre des fonctions, modifier des valeurs directement en mémoire et éviter la copie de grandes structures. Les pointeurs permettent une gestion plus fine de la mémoire, mais il faut être attentif à leur utilisation pour éviter certains pièges. 😰

Syntaxe des pointeurs en Go

Déclaration et opérateur &

Pour déclarer un pointeur en Go, on utilise l’opérateur * devant un type, indiquant que c’est un pointeur vers ce type. Pour obtenir l’adresse mémoire d’une variable, on utilise l’opérateur &.

Voici un exemple simple :

package main

import "fmt"

func main() {
    x := 10
    var ptr *int     // Déclaration d'un pointeur vers un int
    ptr = &x         // ptr pointe vers l'adresse mémoire de x

    fmt.Println("Valeur de x      :", x)
    fmt.Println("Adresse de x     :", &x)
    fmt.Println("Valeur de ptr    :", ptr)
    fmt.Println("Valeur pointée   :", *ptr)
}

Si on lance le programme avec go run main.go, le résultat sera le suivant :

Valeur de x      : 10
Adresse de x     : 0x14000102020
Valeur de ptr    : 0x14000102020
Valeur pointée   : 10

🚨 Je vous encourage à bien analyser le code et le résultat afin de bien comprendre la mécanique des pointeurs en Go.

Accéder à la valeur pointée avec l’opérateur *

Pour accéder à la valeur pointée par un pointeur, Go utilise également l’opérateur *.

⚠️ Attention, la signification de * varie selon le contexte : lors de la déclaration, * indique un type pointeur; lors de l’utilisation, * permet de déréférencer le pointeur, c’est-à-dire accéder à la valeur dans la mémoire.

Dans notre exemple, *ptr (ligne 13) est la valeur de x. Si on modifie *ptr, on modifie en fait x directement, car ptr pointe vers x.

Schéma pour bien comprendre * et & ! 😉

Golang pointeur go

Exemples pratiques : modifications via pointeur

Passer un pointeur en paramètre de fonction

L’un des cas d’usage majeurs des pointeurs est de permettre à une fonction de modifier la valeur d’une variable passée en paramètre. Sans pointeur, Go copie la valeur à chaque appel de fonction, ce qui ne modifie pas l’original :

package main

import "fmt"

func increment(ptr *int) {
    *ptr = *ptr + 1
}

func main() {
    val := 10
    fmt.Println("Avant l'appel :", val)

    increment(&val)  // On passe l'adresse de val à la fonction
    fmt.Println("Après l'appel :", val)
}

Dans ce bout de code, la fonction increment reçoit un pointeur vers un entier, et incrémente la valeur pointée de 1. Dans main, on passe l’adresse de val à increment pour permettre à la fonction de modifier val directement. Sans ce pointeur, la fonction ne pourrait pas affecter val en dehors de son scope.

Partage de structures plus complexes

Les pointeurs sont également très utiles pour partager des structures plus complexes sans copier toute la structure. Par exemple, imaginez un User avec de nombreux champs. Passer un pointeur est plus efficace et permet des modifications directes :

type User struct {
    Name string
    Age  int
}

func celebrateBirthday(u *User) {
    u.Age++
}

func main() {
    bob := User{Name: "Bob", Age: 30}
    celebrateBirthday(&bob) // Passe l'adresse de bob
    fmt.Println(bob)        // Bob a désormais 31 ans
}

Réviser pour bien comprendre * et &

Golang pointeur go

Les pointeurs et les slices, maps, channels

Références internes

Notez bien que les slices, maps et channels en Go sont déjà des types qui contiennent des références internes.

👉 Cela signifie que si vous modifiez leur contenu dans une fonction, les changements seront visibles à l’extérieur, même sans utiliser de pointeurs.

package main

import "fmt"

func modifierSlice(s []int) {
    s[0] = 100 // Modification directe du premier élément
}

func main() {
    nombres := []int{1, 2, 3}
    modifierSlice(nombres) // Pas besoin de pointeur !
    fmt.Println(nombres)   // Affiche : [100 2 3]
}

Quand utiliser les pointeurs ?

L’usage des pointeurs est surtout nécessaire pour :

  1. Les types primitifs (int, bool, float, etc.), car ils sont passés par valeur.
  2. Les structs volumineuses, pour éviter de copier de gros objets en mémoire.

Dernier exemple pour bien comprendre les pointeurs ! 😊

Nous allons écrire deux fonctions dans deux morceaux de code distincts : l’une utilisera un pointeur, tandis que l’autre n'utilisera pas le pointeur.

Fonction : avec pointeur

Code : fonction avec le pointeur

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func whoIs(p *Person) string {
	p.Name = "John"
	return p.Name
}

func main() {

	p := Person{
		Name: "David",
		Age:  25,
	}

	fmt.Println(whoIs(&p))

}

Résultat

John

Fonction : sans pointeur

Code : fonction sans le pointeur

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func whoIs(p Person) string {
	p.Name = "John"
	return p.Name
}

func main() {

	p := Person{
		Name: "David",
		Age:  25,
	}

	fmt.Println(whoIs(p))

}

Résultat

John

Analyser les deux codes

Vous avez sûrement remarqué que le résultat affiché est le même John !

Cependant, une différence existe ! Si vous avez bien compris les pointeurs, vous devriez l’avoir devinée. 😋

Première version (avec pointeur)

La fonction reçoit un pointeur. Cela évite de copier l’intégralité de la structure et permet de modifier directement l’objet original.

Deuxième version (sans pointeur)

La fonction reçoit une copie. La structure passée ne peut pas être modifiée dans la fonction, ce qui garantit l’immuabilité de l’objet original. Mais si la structure est grande, copier toutes ses données peut être moins performant.

Preuve qu'il y a bien une copie sans le pointeur.

Je vais ajouter le même morceau de code aux deux versions. En observant attentivement le résultat, vous comprendrez la différence.

Avec le pointeur

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func whoIs(p *Person) string {
	p.Name = "John"
	return p.Name
}

func main() {

	p := Person{
		Name: "David",
		Age:  25,
	}

	fmt.Println("Fonction : " + whoIs(&p))
	fmt.Println("Direct : " + p.Name)

}
Fonction : John
Direct : John

Sans le pointeur

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func whoIs(p Person) string {
	p.Name = "John"
	return p.Name
}

func main() {

	p := Person{
		Name: "David",
		Age:  25,
	}

	fmt.Println("Fonction : " + whoIs(p))
	fmt.Println("Direct : " + p.Name)

}
Fonction : John
Direct : David

C’est limpide, non ? 😎