Structures en Go
Une structure peut stocker un ensemble de données de types différents, c'est un type composé. Go a abandonné les classes et l'héritage, ainsi que les constructeurs, et a délibérément affaibli les fonctionnalités orientées objet. Go n'est pas un langage OOP traditionnel, mais il conserve des traces de la POO. Il est possible de simuler une classe via des structures et des méthodes. Voici un exemple simple de structure :
type Programmer struct {
Name string
Age int
Job string
Language []string
}Déclaration
La déclaration d'une structure est très simple. Voici un exemple :
type Person struct {
name string
age int
}La structure elle-même et ses champs internes suivent tous les règles de nommage avec majuscules/minuscules pour l'exposition. Pour les champs adjacents de même type, il n'est pas nécessaire de répéter le type. Voici :
type Rectangle struct {
height, width, area int
color string
}TIP
Lors de la déclaration des champs d'une structure, le nom du champ ne peut pas être identique au nom d'une méthode.
Instanciation
Go n'a pas de constructeur. Dans la plupart des cas, on utilise la méthode suivante pour instancier une structure. Lors de l'initialisation, on spécifie le nom du champ puis on initialise la valeur du champ, comme pour une map :
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}Cependant, on peut également omettre les noms des champs. Lorsqu'on omet les noms des champs, il est nécessaire d'initialiser tous les champs. Cette méthode n'est généralement pas recommandée car la lisibilité est mauvaise.
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}Si le processus d'instanciation est complexe, vous pouvez également écrire une fonction pour instancier la structure, comme ci-dessous. Vous pouvez la considérer comme un constructeur :
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}Cependant, Go ne prend pas en charge la surcharge de fonctions et de méthodes. Vous ne pouvez donc pas définir différents paramètres pour la même fonction ou méthode. Si vous souhaitez instancier une structure de plusieurs manières, soit vous créez plusieurs constructeurs, soit il est recommandé d'utiliser le modèle options.
Modèle options
Le modèle options est un modèle de conception très courant en langage Go. Il permet d'instancier une structure de manière plus flexible, avec une forte extensibilité, et sans modifier la signature de la fonction du constructeur. Supposons qu'il y ait une structure comme ci-dessous :
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}Déclarez un type PersonOptions qui accepte un paramètre de type *Person. Il doit s'agir d'un pointeur, car nous devons assigner une valeur à Person dans la fermeture.
type PersonOptions func(p *Person)Ensuite, créez des fonctions options. Elles commencent généralement par With, et leur valeur de retour est une fonction de fermeture.
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}La signature de la fonction constructeur réellement déclarée est la suivante. Elle accepte un paramètre de type PersonOptions de longueur variable.
func NewPerson(options ...PersonOptions) *Person {
// Appliquer d'abord les options
p := &Person{}
for _, option := range options {
option(p)
}
// Traitement des valeurs par défaut
if p.Age < 0 {
p.Age = 0
}
......
return p
}Ainsi, pour différents besoins d'instanciation, un seul constructeur suffit. Il suffit de passer différentes fonctions Options :
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}Le modèle d'options fonctionnelles peut être vu dans de nombreux projets open source. L'instanciation du serveur gRPC utilise également ce modèle de conception. Le modèle d'options fonctionnelles convient uniquement aux instanciations complexes. Si les paramètres ne sont que quelques-uns simples, il est recommandé d'utiliser un constructeur ordinaire.
Composition
En Go, la relation entre les structures est représentée par la composition. Elle peut être explicite ou anonyme. Cette dernière est plus similaire à l'héritage, mais essentiellement rien ne change. Par exemple :
Méthode de composition explicite :
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}Lors de l'utilisation, il est nécessaire de spécifier explicitement le champ p :
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)La composition anonyme permet de ne pas spécifier explicitement le champ :
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}Le nom du champ anonyme est par défaut le nom du type. L'appelant peut accéder directement aux champs et méthodes de ce type. Mais à part être plus pratique, il n'y a aucune différence avec la première méthode.
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)Pointeurs
Pour un pointeur de structure, il n'est pas nécessaire de le déréférencer pour accéder directement au contenu de la structure. Voici un exemple :
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)Lors de la compilation, cela sera converti en (*p).name, (*p).age. En réalité, il faut toujours déréférencer, mais on peut l'omettre lors du codage. C'est une sorte de sucre syntaxique.
Tags
Les tags de structure sont une forme de métaprogrammation. Combinés avec la réflexion, ils permettent de créer de nombreuses fonctionnalités merveilleuses. Le format est le suivant :
`key1:"val1" key2:"val2"`Les tags sont sous forme de paires clé-valeur, séparées par des espaces. La tolérance aux erreurs des tags de structure est très faible. Si le tag n'est pas écrit dans le format correct, il ne pourra pas être lu normalement. Cependant, aucune erreur ne sera signalée lors de la compilation. Voici un exemple d'utilisation :
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}L'utilisation la plus courante des tags de structure est la définition d'alias dans divers formats de sérialisation. L'utilisation des tags doit être combinée avec la réflexion pour充分发挥 leurs fonctionnalités.
Alignement mémoire
La distribution mémoire des champs de structure en Go suit les règles d'alignement mémoire. Cela permet de réduire le nombre d'accès mémoire du CPU. En contrepartie, plus de mémoire est occupée. C'est une méthode d'échange d'espace contre du temps. Supposons qu'il y ait la structure suivante :
type Num struct {
A int64
B int32
C int16
D int8
E int32
}Connaissant le nombre d'octets occupés par ces types :
int64occupe 8 octetsint32occupe 4 octetsint16occupe 2 octetsint8occupe 1 octet
L'occupation mémoire totale de la structure semble être de 8+4+2+1+4=19 octets ? Bien sûr que non. Selon les règles d'alignement mémoire, la longueur d'occupation mémoire d'une structure doit être au moins un multiple du champ le plus grand. Les octets manquants sont complétés. Le plus grand dans cette structure est int64 qui occupe 8 octets, donc la distribution mémoire est comme illustré ci-dessous :

Donc en réalité, 24 octets sont occupés, dont 5 octets sont inutiles.
Examinons maintenant la structure suivante :
type Num struct {
A int8
B int64
C int8
}Après avoir compris les règles ci-dessus, on peut rapidement comprendre que son occupation mémoire est également de 24 octets, bien qu'elle n'ait que trois champs, ce qui gaspille jusqu'à 14 octets.

Mais nous pouvons ajuster les champs dans l'ordre suivant :
type Num struct {
A int8
C int8
B int64
}Ainsi, la mémoire occupée devient 16 octets, avec 6 octets gaspillés, réduisant le gaspillage de mémoire de 8 octets.

Théoriquement, faire en sorte que les champs de la structure soient distribués dans un ordre raisonnable peut réduire leur occupation mémoire. Cependant, dans le processus de codage réel, il n'est pas nécessaire de le faire pour de bonnes raisons. Cela n'apportera pas nécessairement une amélioration substantielle de la réduction de l'occupation mémoire, mais augmentera certainement la pression et la charge cognitive des développeurs. Surtout dans les activités commerciales, certains champs de structure peuvent atteindre plusieurs dizaines ou centaines. Il suffit donc de le comprendre.
TIP
Si vous souhaitez vraiment économiser de la mémoire avec cette méthode, vous pouvez consulter ces deux bibliothèques :
Elles vérifieront les structures dans votre code source, calculeront et réorganiseront les champs de structure pour minimiser la mémoire occupée par la structure.
Structure vide
Une structure vide n'a pas de champs et n'occupe pas d'espace mémoire. Nous pouvons utiliser la fonction unsafe.Sizeof pour calculer la taille en octets occupée :
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Sortie :
0Les cas d'utilisation des structures vides sont nombreux. Comme mentionné précédemment, en tant que type de valeur d'une map, une map peut être utilisée comme set. Ou encore comme type de canal, indiquant un canal de type notification uniquement.
