Structures & Pointeurs
Définition
Simplement
Type personnalisé représentant une collection de champs
Syntaxe
type <NomStruct> struct {
var1 int
var2 string
var3 float64
}
Exemple
type User struct {
Name string
Email string
Age int
}
Déclaration
Il y a 3 types de déclaration possible
type Person struct {
Name string
Age int
}
func main() {
// 1
var p1 Person
p1.Name = "Bob"
p1.Addr.city = "Lyon"
// 2
p2 := Person{"Paul","Abibi"}
// 3
p3 := Person{Name: "Swann"}
}
Règles
- Une structure ne peut contenir que des variables
- La règle de visibilité de package s'applique pour :
- la structure elle-même
- les variables de la structure
Exercice player
- Définir 2 structure :
- Avatar
- Url
- Player
- Name
- Age
- Avatar
- password
- Avatar
- Le mot de passe doit avoir un scope privé.
- Créer une fonction de création d’un player, qui ne prend que son nom en argument et initialise la structure avec ce dernier, ainsi qu’un mot de passe par défaut.
Solution
type Avatar struct {
Url string
}
type Player struct {
Name string
Age int
Avatar Avatar
password string
}
func New(name string) Player {
return Player{
Name: name,
password: "defaultpassword",
}
}
Embedded struct
type Avatar struct {
Url string
}
type Player struct {
Name string
Avatar Avatar
}
Un Player a un Avatar
Parfois, on veut exprimer un autre type de relation → Un XXX est un YYY
Dans d’autres langages, cette relation est exprimée par l’héritage
Go préfère la composition avec l’embedded struct :
type Avatar struct {
Url string
}
type Player struct {
Name string
Avatar // Pas de nom de variables
}
var p Player
p.Url = "https://photodemoi.jpg"
Dans ce code, Avatar est embarqué dans le type Player
Player est un Avatar
Receiver function
Grâce à ce fonctionnent, on peut enfin reproduire quasiment à l’identique le comportement d’un objet tel qu’en Java par exemple.
type User struct {
Name string
}
func (u User) SayHello() {
fmt.Printf("Hello %v!\n", u.Name)
}
-
u est un value receiver pour la méthode SayHello()
-
Les champs de u sont accessibles pour la fonction
-
C’est bien beau ça, mais à quoi ça nous sert ?
func main() { u := User{"Paul"} u.SayHello() }
Lorsqu’on utilise cette technique, notre struct User passé en argument est en réalité “copiée” pour la méthode.
Conséquence : Une méthode avec un value receiver ne peut pas modifier la structure originale. Cela peut permettre de favoriser l’immutabilité en renvoyant une nouvelle instance de notre structure avec les propriétés mise à jour !
Exercice rectangle
- Définir une structure rectangle qui contient
- Longueur
- Largeur
- Créer 3 receiver functions pour cette structure :
Area()
: renvoie l’air du rectangleString()
: Affiche les informations de la structure bien formatéesDoubleSize()
: Renvoie une nouvelle structure du rectangle avec sa taille doublée
Astuce
Définir la receiver function String()
d’une structure viendra surcharger l’affichage par défaut de cette dernière !
Solution
package main
import (
"fmt"
)
type Rect struct {
Width, Height int
}
func (r Rect) Area() int {
return r.Width * r.Height
}
func (r Rect) String() string {
return fmt.Sprintf("Rect ==> width=%v, height=%v", r.Width, r.Height)
}
func (r Rect) DoubleSize() Rect {
r.Width *= 2
r.Height *= 2
return r
}
func main() {
r := Rect{2, 4}
fmt.Printf("Rect area=%v\n", r.Area())
fmt.Println(r)
r2 := r.DoubleSize()
fmt.Println("r", r)
fmt.Println("r2", r2)
}
Pointeurs
En Go, lorsque qu’on passe un paramètre à une fonction, on passe en réalité une copie de cette dernière,
Les pointeurs en Go fonctionnent presque comme en C, à l’exception que nous n’avons pas à gérer l’allocation et la libération de la mémoire.
x := -42
s := "Bob"
p := &x // Création d'un pointer vers la variable x
i := *p // Déréférencement de p pour récupérer la valeur de x
Manipulations
func UpdateVal(val string) {
val = "value"
}
func UpdatePtr(ptr *string) {
*ptr = "pointer"
}
func main() {
i := 1
var p *int = &i
fmt.Printf("i=%v\n", i)
fmt.Printf("p=%v\n", p)
fmt.Printf("*p=%v\n", *p)
fmt.Println("---------------")
s := "Paul"
sPtr := &s
s2 := *sPtr
fmt.Println("String pointer")
fmt.Printf("*s=%v\n", s)
fmt.Printf("*sPtr=%v\n", *sPtr)
fmt.Printf("s2=%v\n", s2)
fmt.Println("---------------")
*sPtr = "Clément"
fmt.Println("Dereference and update")
fmt.Printf("s=%v\n", s)
fmt.Printf("*sPtr=%v\n", *sPtr)
fmt.Printf("s2=%v\n", s2)
fmt.Println("---------------")
UpdateVal(s)
fmt.Println("Func UpdateVal()")
fmt.Printf("s=%v\n", s)
fmt.Printf("*sPtr=%v\n", *sPtr)
fmt.Println("---------------")
UpdatePtr(&s)
UpdatePtr(sPtr)
fmt.Println("Func UpdatePtr()")
fmt.Printf("s=%v\n", s)
fmt.Printf("*sPtr=%v\n", *sPtr)
fmt.Println("---------------")
}
Pointer Receiver
Comme dit plus tôt, les paramètres de fonctions sont des copies des objets originaux.
Ça vaut aussi pour les fonctions value receiver
sur les structures. Elles ne peuvent donc que faire de la lecture simple, et ne peuvent pas modifier la structure d’origine.
Grâce aux pointeurs, on peut régler ce problème et le couplant à des receiver functions
.
type Post struct {
Title string
Text string
published bool
}
func (p Post) Headline() string {
return fmt.Sprintf("%v - %v", p.Title, p.Text[:50])
}
func (p *Post) Publish() {
p.published = true
}
func (p *Post) Unpublish() {
p.published = true
}
func main() {
p := Post{
Title: "Go release",
Text: "Go is a programming language...",
}
fmt.Println(p.Headline())
fmt.Printf("Post published? %v\n", p.Published())
p.Publish()
fmt.Printf("Post published? %v\n", p.Published())
}
Enfin, si on souhaite créer une structure directement sous la forme d’un pointeur, on peut faire autrement que :
p := Post{
Title: "Go release",
Text: "Go is a programming language...",
}
pointer := &p
Comme ceci :
pythonPost := &Post{
Title: "Python Intro",
Text: "Python is an interpreted high-level programming language",
}
pythonPost
est un pointeur.