Retour au blog

Le mot-clé struct en Go : champs, tags, embedding et méthodes

Comment fonctionne le mot-clé struct de Go : déclaration des champs, valeurs zéro et littéraux composites, champs exportés et JSON, struct tags avec omitempty et omitzero, récepteurs valeur ou pointeur, embedding, comparaison et copie, struct vide et ordre des champs.

Le mot-clé struct en Go : champs, tags, embedding et méthodes

Le mot-clé struct regroupe des champs nommés dans un seul type. En Go, c'est avec une déclaration type Order struct { ... } que vous décrivez une commande, un utilisateur, le corps d'une requête HTTP ou un fichier de configuration. Les structs n'ont ni constructeurs ni héritage. Vous ajoutez du comportement avec des méthodes, réutilisez du code en intégrant un struct dans un autre et contrôlez l'encodage avec des struct tags (spécification Go).

En bref

  • type Order struct { ... } déclare un type struct. Chaque champ a un nom et un type, et les champs de même type peuvent partager une ligne.
  • Dans la valeur zéro d'un struct, chaque champ vaut sa propre valeur zéro, donc var o Order est utilisable sans aucune initialisation.
  • Écrivez les littéraux composites avec les noms de champs, comme Order{ID: 1001, Total: 4999}. go vet signale les littéraux sans noms de champs pour les types d'autres packages.
  • Seuls les champs exportés (avec une majuscule) sont visibles par encoding/json et les autres packages. Un champ en minuscules est ignoré sans erreur.
  • Les struct tags comme json:"email,omitempty" contrôlent l'encodage. Utilisez omitzero (Go 1.24) pour time.Time et les autres structs, car omitempty ne les omet jamais.
  • Les méthodes à récepteur pointeur peuvent modifier le struct. Les méthodes à récepteur valeur travaillent sur une copie.
  • Intégrer un type promeut ses champs et ses méthodes vers le struct englobant. C'est ainsi que Go réutilise du code sans héritage.
  • Les structs ne sont comparables avec == que si tous leurs champs le sont. Affecter un struct le copie, mais les slices et les maps qu'il contient restent partagées.

Comment déclarer un struct en Go ?

Écrivez type, le nom, struct puis les champs entre accolades. Chaque champ est un nom suivi d'un type :

example.gogo
package main

import (
	"fmt"
	"time"
)

type Order struct {
	ID         int64
	CustomerID int64
	Status     string
	Total      int64 // cents
	CreatedAt  time.Time
}

type Address struct {
	Street, City, Country string
}

func main() {
	var o Order
	fmt.Printf("%+v\n", o)
	fmt.Println(o.Status == "", o.CreatedAt.IsZero())
}
example.texttext
{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC}
true true

Address montre la forme courte. Les champs de même type peuvent partager une ligne, séparés par des virgules. La plupart des codebases gardent malgré tout un champ par ligne, car les diffs restent petits et il reste de la place pour un tag ou un commentaire.

var o Order ne demande aucun constructeur. Chaque champ démarre à sa valeur zéro : 0 pour les nombres, "" pour les chaînes, nil pour les pointeurs, les slices et les maps, et le time.Time zéro pour CreatedAt. Le mot-clé var en Go traite les valeurs zéro en détail.

Comme tout type, un struct peut être déclaré dans une fonction quand seule cette fonction en a besoin. Le mot-clé type en Go montre un struct local loginRequest utilisé pour décoder un corps JSON.

Comment créer une valeur de struct ?

Vous utiliserez surtout la valeur zéro obtenue avec var, un littéral composite avec noms de champs, ou un pointeur vers un littéral avec & :

example.gogo
keyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999}
partial := Order{ID: 1002, Status: "pending"}
positional := Order{1003, 42, "paid", 1250}
p := &Order{ID: 1004}
p.Status = "shipped"
example.texttext
{ID:1001 CustomerID:42 Status:paid Total:4999}
{ID:1002 CustomerID:0 Status:pending Total:0}
{ID:1003 CustomerID:42 Status:paid Total:1250}
{ID:1004 CustomerID:0 Status:shipped Total:0}

Un littéral avec noms de champs peut lister n'importe quel sous-ensemble de champs dans n'importe quel ordre, et les autres prennent leur valeur zéro. Un littéral positionnel doit lister tous les champs dans l'ordre de déclaration, ce qui le rend fragile. Ajoutez un champ Currency à Order et tous les littéraux positionnels cessent de compiler, alors que ceux avec noms de champs continuent de fonctionner.

Pour les types struct d'autres packages, go vet signale les littéraux positionnels via son analyseur composites :

example.gogo
addr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttext
main.go:9:10: net.TCPAddr struct literal uses unkeyed fields

p.Status fonctionne sur un pointeur sans écrire (*p).Status. Go déréférence automatiquement un pointeur vers un struct quand vous accédez à un champ.

Comment renseigner un champ pointeur optionnel en une ligne ?

Utilisez new avec une valeur, ce qui fonctionne depuis Go 1.26. Les champs pointeurs sont la façon habituelle de distinguer « non envoyé » de « envoyé à zéro » dans le corps d'une requête PATCH, et avant Go 1.26 il fallait une variable temporaire ou une fonction utilitaire pour chacun d'eux :

example.gogo
type UpdateUser struct {
	Name  *string `json:"name,omitempty"`
	Email *string `json:"email,omitempty"`
	Age   *int    `json:"age,omitempty"`
}

func main() {
	patch := UpdateUser{
		Email: new("[email protected]"),
		Age:   new(31),
	}
	out, _ := json.Marshal(patch)
	fmt.Println(string(out))
}
example.texttext
{"email":"[email protected]","age":31}

new(31) alloue un int, y stocke 31 et renvoie le pointeur (notes de version de Go 1.26). Avec go 1.25 dans go.mod, le compilateur le refuse :

example.texttext
./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)

Pourquoi les champs en minuscules d'un struct manquent-ils dans le JSON ?

Parce qu'ils ne sont pas exportés, et que les autres packages ne voient pas les champs non exportés. encoding/json est un autre package, il les ignore donc sans erreur :

example.gogo
type User struct {
	ID       int64
	Email    string
	password string
	role     string
}

func main() {
	u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"}
	out, err := json.Marshal(u)
	fmt.Println(string(out), err)

	var in User
	err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in)
	fmt.Printf("%+v %v\n", in, err)
}
example.texttext
{"ID":7,"Email":"[email protected]"} <nil>
{ID:8 Email:[email protected] password: role:} <nil>

Les deux appels renvoient une erreur nil, et le role présent en entrée disparaît sans le moindre message. Quand une API Go renvoie {} ou qu'un champ reste vide après le décodage, vérifiez ce point en premier. La correction consiste à mettre une majuscule au champ et à utiliser un tag pour conserver le nom JSON en minuscules.

La même règle s'applique à tous les packages qui lisent les champs par réflexion : encoding/xml, les scanners de database/sql, GORM, les bibliothèques YAML et l'exécution des templates. Vous pouvez aussi en tirer parti volontairement. Un champ non exporté est un endroit sûr pour des données qui ne doivent jamais être encodées, comme un handle de base de données ou un cache.

Que sont les struct tags en Go ?

Un struct tag est une chaîne littérale placée après le type d'un champ. Go lui-même l'ignore, et les packages le lisent par réflexion pour décider comment traiter ce champ. Le tag JSON est celui que vous écrirez le plus souvent :

example.gogo
type User struct {
	ID           int64     `json:"id"`
	Email        string    `json:"email"`
	Nickname     string    `json:"nickname,omitempty"`
	PasswordHash string    `json:"-"`
	DeletedAt    time.Time `json:"deleted_at,omitempty"`
	LastLoginAt  time.Time `json:"last_login_at,omitzero"`
}

func main() {
	u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	enc.Encode(u)
}
example.texttext
{
  "id": 7,
  "email": "[email protected]",
  "deleted_at": "0001-01-01T00:00:00Z"
}

Reprenons la sortie champ par champ :

  • json:"id" renomme le champ dans la sortie.
  • omitempty supprime le champ quand il vaut false, 0, "", nil ou une slice ou une map vide. Nickname est vide, il disparaît donc.
  • json:"-" n'encode jamais le champ. PasswordHash reste absent de toutes les réponses.
  • omitempty ne fonctionne pas sur les structs. DeletedAt est un time.Time zéro, et il apparaît quand même sous la forme 0001-01-01T00:00:00Z, que les clients ont tendance à interpréter comme une vraie date.
  • omitzero, ajouté dans Go 1.24, supprime un champ quand il contient la valeur zéro de son type. Pour les types dotés d'une méthode IsZero() bool, comme time.Time, il appelle cette méthode. LastLoginAt a disparu (notes de version de Go 1.24).

Pour du nouveau code, utilisez omitzero sur les champs de type struct et time.Time. omitempty reste adapté aux chaînes, aux nombres, aux slices et aux maps.

Un champ peut porter des tags pour plusieurs packages, séparés par des espaces : `json:"email" db:"email" validate:"required,email"`. Chaque package ne lit que sa propre clé avec reflect.StructTag.Get :

example.gogo
f, _ := reflect.TypeFor[User]().FieldByName("Nickname")
fmt.Println(f.Tag.Get("json"))
example.texttext
nickname,omitempty

Pourquoi mon struct tag est-il ignoré ?

En général à cause d'un espace ou d'un guillemet manquant. Le format est strict : key:"value", sans espace après les deux-points. json: "email" semble correct à un humain, mais Tag.Get("json") renvoie une chaîne vide, et le champ est donc encodé sous le nom Email. Le compilateur accepte n'importe quelle chaîne comme tag, il ne dira donc rien. go vet, lui, le signale via son analyseur structtag, et il détecte aussi deux champs portant le même nom JSON :

example.gogo
type User struct {
	Email string `json: "email"`
	Name  string `json:"name" db:"name"`
	ID    int64  `json:"id"`
	Ref   int64  `json:"id"`
}
example.texttext
main.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6

Les éditeurs qui exécutent gopls affichent les mêmes avertissements pendant la saisie.

Comment fonctionnent les méthodes sur les structs ?

Une méthode est une fonction dotée d'un récepteur, écrit avant le nom. Le récepteur est soit une valeur (c Cart), soit un pointeur (c *Cart), et ce choix détermine si la méthode peut modifier le struct :

example.gogo
type Cart struct {
	Items []string
	Total int64
}

func (c Cart) AddValue(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func (c *Cart) Add(item string, price int64) {
	c.Items = append(c.Items, item)
	c.Total += price
}

func main() {
	var c Cart
	c.AddValue("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)

	c.Add("keyboard", 4999)
	fmt.Println(len(c.Items), c.Total)
}
example.texttext
0 0
1 4999

AddValue compile et s'exécute, mais le panier reste vide. Un récepteur valeur est une copie du struct : la méthode met donc à jour la copie, puis la jette. Ce bug revient souvent dans du code Go écrit par des débutants, car aucune erreur ni aucun avertissement ne le signale. Add reçoit un pointeur et modifie le panier de l'appelant.

Vous n'avez pas besoin d'écrire (&c).Add(...). Quand c est adressable, Go prend l'adresse à votre place. L'inverse fonctionne aussi : un pointeur peut appeler des méthodes à récepteur valeur.

Utilisez un récepteur pointeur quand la méthode modifie le struct, quand le struct est volumineux ou quand il contient un sync.Mutex ou un champ similaire qui ne doit pas être copié. Utilisez un récepteur valeur pour de petites valeurs immuables comme un Money ou un Point. Si une méthode d'un type a besoin d'un récepteur pointeur, donnez un récepteur pointeur à toutes. Mélanger les deux rend l'ensemble de méthodes plus difficile à cerner, car seul *Cart possède alors toutes les méthodes, et seul *Cart satisfait les interfaces qui les exigent toutes (spécification Go).

Comment fonctionne l'embedding de structs en Go ?

Un champ écrit avec seulement un type, sans nom, est un champ intégré (embedded field). Ses champs et ses méthodes sont promus, ce qui signifie que vous pouvez les utiliser comme s'ils étaient déclarés sur le struct englobant :

example.gogo
type Timestamps struct {
	CreatedAt time.Time
	UpdatedAt time.Time
}

func (t *Timestamps) Touch(now time.Time) {
	if t.CreatedAt.IsZero() {
		t.CreatedAt = now
	}
	t.UpdatedAt = now
}

type Order struct {
	ID int64
	Timestamps
}

type User struct {
	ID    int64
	Email string
	Timestamps
}

func main() {
	now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC)
	var o Order
	o.Touch(now)
	fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen))

	u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}}
	fmt.Println(u.CreatedAt.Year())
}
example.texttext
2026-09-26 9:00AM
2026

o.Touch(now) est un raccourci pour o.Timestamps.Touch(now). Le champ intégré existe toujours sous le nom de son type, et c'est aussi ainsi que vous le renseignez dans un littéral : Timestamps: Timestamps{...}. encoding/json aplatit également les structs intégrés, donc l'Order ci-dessus est encodé en {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"}.

L'embedding n'est pas de l'héritage. Un Order n'est pas un Timestamps, et vous ne pouvez pas passer un Order à une fonction qui attend un Timestamps. Quand Touch s'exécute, son récepteur est le Timestamps interne. Il n'a aucun moyen d'atteindre l'Order qui l'entoure ni d'appeler des méthodes définies par Order. Une sous-classe en Java ou en Python le peut, et c'est de là que viennent les questions du type « quelle redéfinition s'exécute ? ». En Go, elles ne se posent pas.

Quand le struct englobant déclare un champ ou une méthode du même nom, c'est lui qui l'emporte, et l'élément intégré reste accessible via le nom du type :

example.gogo
type Base struct{ ID int64 }

func (Base) Describe() string { return "base" }

type Order struct {
	Base
	ID string
}

func (Order) Describe() string { return "order" }

func main() {
	o := Order{Base: Base{ID: 42}, ID: "ord_42"}
	fmt.Println(o.ID, o.Base.ID)
	fmt.Println(o.Describe(), o.Base.Describe())
}
example.texttext
ord_42 42
order base

Peut-on intégrer une interface dans un struct ?

Oui, et c'est une façon courante d'envelopper un type tout en redéfinissant une seule méthode. Un middleware HTTP qui enregistre le code de statut intègre http.ResponseWriter, obtient Header et Write sans rien écrire et ne remplace que WriteHeader :

example.gogo
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

func logStatus(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, req)
		slog.Info("request", "path", req.URL.Path, "status", rec.status)
	})
}
example.texttext
2026/09/26 09:04:44 INFO request path=/orders/99 status=404

Comme *statusRecorder possède les trois méthodes, il satisfait http.ResponseWriter et peut être passé au handler suivant. Si l'interface intégrée vaut nil, appeler l'une de ses méthodes provoque un panic, donc renseignez-la toujours dans le littéral.

Faut-il intégrer sync.Mutex dans un struct ?

Pas dans un type exporté. L'embedding promeut Lock et Unlock dans l'API publique du type, et n'importe quel appelant peut alors verrouiller votre cache depuis l'extérieur :

example.gogo
type Cache struct {
	sync.Mutex
	items map[string]string
}

Un champ nommé et non exporté fait du verrou un détail d'implémentation :

example.gogo
type SafeCache struct {
	mu    sync.Mutex
	items map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	v, ok := c.items[key]
	return v, ok
}

La valeur zéro de sync.Mutex est un mutex déverrouillé, donc SafeCache n'a besoin d'aucune initialisation pour le verrou. Sa map doit quand même être créée avant la première écriture.

Peut-on comparer et copier des structs en Go ?

Vous pouvez comparer deux structs avec == quand tous leurs champs sont comparables. La comparaison se fait champ par champ. Les petits structs font ainsi de bons types valeur et de bonnes clés de map :

example.gogo
type Money struct {
	Amount   int64
	Currency string
}

type RouteKey struct {
	Method string
	Path   string
}

func main() {
	fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"})

	hits := map[RouteKey]int{}
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"GET", "/orders"}]++
	hits[RouteKey{"POST", "/orders"}]++
	fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits))
}
example.texttext
true
2 2

Une clé composite comme RouteKey est plus propre que des chaînes collées sous la forme "GET /orders", et elle ne peut pas entrer en collision quand un chemin contient un espace. Les littéraux positionnels conviennent ici, car RouteKey est déclaré dans le même package et possède deux champs évidents.

Les slices, les maps et les fonctions ne sont pas comparables, donc un struct qui en contient ne l'est pas non plus. Le compilateur vous le signale :

example.gogo
type Order struct {
	ID    int64
	Items []string
}

func main() {
	a := Order{ID: 1}
	b := Order{ID: 1}
	fmt.Println(a == b)
}
example.texttext
./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)

Comparez ces structs champ par champ, avec slices.Equal pour la slice, ou avec reflect.DeepEqual dans les tests.

L'affectation d'un struct le copie-t-elle ?

Oui, mais la copie est superficielle. Chaque champ est copié, et un champ slice ou map est copié sous la forme d'un en-tête qui pointe toujours vers les mêmes données :

example.gogo
type Order struct {
	ID    int64
	Items []string
	Meta  map[string]string
}

func main() {
	original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}}
	copied := original
	copied.ID = 2
	copied.Items[0] = "mouse"
	copied.Meta["source"] = "api"
	fmt.Println(original.ID, original.Items, original.Meta)
}
example.texttext
1 [mouse] map[source:api]

Modifier copied.ID n'a pas touché l'original. Modifier un élément de copied.Items ou une clé de copied.Meta a aussi modifié l'original. Pour obtenir une copie indépendante, clonez vous-même ces champs avec slices.Clone et maps.Clone.

La même copie a lieu quand vous passez un struct par valeur à une fonction. Pour un struct qui contient un sync.Mutex, cette copie est un bug, car la copie obtient son propre verrou. go vet le signale via l'analyseur copylocks :

example.gogo
type Counter struct {
	mu   sync.Mutex
	hits int
}

func report(c Counter) {
	fmt.Println(c.hits)
}
example.texttext
main.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex
main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex

La correction est func report(c *Counter).

Qu'est-ce qu'un struct anonyme en Go ?

C'est un type struct écrit en ligne, sans nom. Vous l'utilisez quand une forme n'est nécessaire qu'une seule fois. Les deux endroits où vous le verrez le plus sont les tests pilotés par table et le décodage d'une partie d'une réponse JSON :

example.gogo
var resp struct {
	Data struct {
		Status string `json:"status"`
	} `json:"data"`
}
body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}`
if err := json.Unmarshal([]byte(body), &resp); err != nil {
	panic(err)
}
fmt.Println(resp.Data.Status)

tests := []struct {
	name  string
	input string
	want  string
}{
	{"trims spaces", "  [email protected] ", "[email protected]"},
	{"already clean", "[email protected]", "[email protected]"},
}
for _, tt := range tests {
	got := strings.ToLower(strings.TrimSpace(tt.input))
	fmt.Println(tt.name, got == tt.want)
}
example.texttext
shipped
trims spaces true
already clean true

Le décodeur ne remplit que les champs déclarés par le struct, donc carrier et meta sont ignorés. Vous n'avez pas besoin d'un type nommé pour une réponse dont vous ne lisez qu'un champ. Dès que la même forme apparaît à deux endroits, donnez-lui un nom.

À quoi sert struct{} ?

struct{} est un struct sans aucun champ. Sa taille est de zéro octet, c'est donc le type valeur de prédilection quand seule la présence de quelque chose compte. Les deux cas courants sont un ensemble construit sur une map et un channel qui sert uniquement à signaler :

example.gogo
seen := map[string]struct{}{}
for _, id := range []string{"evt_1", "evt_2", "evt_1"} {
	if _, dup := seen[id]; dup {
		fmt.Println("skipping duplicate", id)
		continue
	}
	seen[id] = struct{}{}
}
fmt.Println(len(seen), unsafe.Sizeof(struct{}{}))

done := make(chan struct{})
go func() {
	defer close(done)
}()
<-done
fmt.Println("worker finished")
example.texttext
skipping duplicate evt_1
2 0
worker finished

map[string]struct{} exprime « ensemble de chaînes » plus clairement que map[string]bool, où le lecteur doit se demander ce que signifie false. chan struct{} indique qu'aucune donnée ne transite sur le channel, seulement le fait qu'il a été fermé. ctx.Done() renvoie un <-chan struct{} pour la même raison.

L'ordre des champs modifie-t-il la taille d'un struct ?

Oui. Chaque champ est aligné sur sa taille, donc le compilateur insère du padding entre un petit champ et un champ plus grand qui le suit. Sur une machine 64 bits, les cinq mêmes champs occupent deux fois plus de mémoire dans le mauvais ordre :

example.gogo
type EventLoose struct {
	Active   bool
	ID       int64
	Retried  bool
	Attempts int32
	Urgent   bool
}

type EventPacked struct {
	ID       int64
	Attempts int32
	Active   bool
	Retried  bool
	Urgent   bool
}

func main() {
	fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{}))
}
example.texttext
32 16

EventLoose ajoute 7 octets de padding après Active pour que ID commence sur une frontière de 8 octets, puis encore après Retried et Urgent. EventPacked ordonne les champs du plus grand au plus petit et ne perd qu'1 octet à la fin.

Pour la plupart des structs, cela n'a pas d'importance, et regrouper les champs par sens est plus lisible. Cela commence à compter quand vous gardez des millions de valeurs dans une slice ou un cache. L'analyseur fieldalignment de golang.org/x/tools signale les structs qui pourraient être plus petits.

Struct Go vs classe : quelle différence ?

Un struct Go contient des données et peut avoir des méthodes, ce qui couvre l'essentiel de ce que fait une classe en Java, C# ou Python. Le reste diffère volontairement :

Struct GoClasse (Java, C#, Python)
ConstructeurAucun. Valeur zéro, ou une fonction NewX par conventionMéthode constructeur dédiée
RéutilisationEmbedding (composition)Héritage, plus composition
PolymorphismeInterfaces, satisfaites implicitementClasses de base et interfaces déclarées
VisibilitéPar package, selon la majuscule initialepublic, private, protected par membre
MéthodesDéclarées hors du type, n'importe où dans le packageDéclarées dans le corps de la classe
Sémantique de copieValeur par défaut, & pour un pointeurRéférence par défaut

Quand un struct a besoin de validation ou de valeurs par défaut, écrivez une simple fonction qui le renvoie. La convention est NewX, et il est normal qu'elle renvoie aussi une erreur :

example.gogo
type Client struct {
	baseURL string
	http    *http.Client
	retries int
}

func NewClient(baseURL string) (*Client, error) {
	if baseURL == "" {
		return nil, errors.New("baseURL is required")
	}
	return &Client{
		baseURL: baseURL,
		http:    &http.Client{Timeout: 10 * time.Second},
		retries: 3,
	}, nil
}

Comme les champs ne sont pas exportés, les appelants extérieurs au package ne peuvent obtenir un Client que via NewClient, avec les valeurs par défaut appliquées. Go n'ayant ni super ni redéfinition, vous ne remontez jamais une hiérarchie de classes pour savoir ce que fait une méthode. Le comportement d'un struct se compose de ses propres méthodes et de celles qu'il intègre, et vous pouvez toutes les lire dans un seul package. Cela aide beaucoup quand une codebase Go grandit.

La place de LevelUpGo

LevelUpGo enseigne Go au travers d'exercices qui exécutent du vrai code Go dans le navigateur. Composite Types construit pas à pas votre premier struct, des fonctions constructeurs, des méthodes et l'embedding, puis les utilise dans un petit projet. Pointers & Memory couvre les pointeurs vers des structs, new et les récepteurs valeur ou pointeur, et se termine par un store de feature flags qui met à jour des structs en place. Interfaces & Polymorphism montre comment les structs satisfont des interfaces sans le déclarer. Le Training Ground propose de courts exercices indépendants pour pratiquer en dehors d'un cours. Pour les 24 autres mots réservés, consultez Les mots-clés Go : les 25 expliqués.

Questions fréquentes

struct est-il un mot-clé en Go ?

Oui. struct fait partie des 25 mots-clés réservés de Go, vous ne pouvez donc pas l'utiliser comme nom de variable ou de fonction. Il introduit un type struct, presque toujours nommé avec type, comme dans type Order struct { ... }. Il apparaît aussi dans les structs anonymes et dans le struct vide struct{}.

Go a-t-il des classes ?

Non. Go propose à la place des structs avec des méthodes, des interfaces et l'embedding. Un struct contient les données, les méthodes ajoutent du comportement, les interfaces fournissent le polymorphisme et l'embedding réutilise du code par composition. Il n'y a ni héritage ni mot-clé de constructeur. Par convention, une fonction NewX joue ce rôle.

Faut-il utiliser un récepteur valeur ou pointeur ?

Utilisez un récepteur pointeur si la méthode modifie le struct, si le struct est volumineux ou s'il contient un sync.Mutex. Utilisez un récepteur valeur pour de petites valeurs qui ne changent jamais, comme un Money ou un Point. Si une méthode d'un type a besoin d'un récepteur pointeur, donnez un récepteur pointeur à toutes pour que le type se comporte de façon cohérente.

Pourquoi json.Marshal renvoie-t-il un objet vide pour mon struct ?

Parce que les champs du struct ne sont pas exportés. encoding/json ne voit que les champs qui commencent par une majuscule, et il ignore les autres sans erreur. Renommez email en Email et ajoutez `json:"email"` pour conserver le nom en minuscules dans le JSON.

Quelle est la différence entre omitempty et omitzero ?

omitempty supprime false, 0, "", nil ainsi que les slices et les maps vides, mais ne supprime jamais un struct : un time.Time zéro apparaît donc toujours sous la forme 0001-01-01T00:00:00Z. omitzero, ajouté dans Go 1.24, supprime tout champ qui contient la valeur zéro de son type, et il utilise la méthode IsZero du type quand elle existe.

Sources

Écrivez du Go comme un ingénieur senior

Des leçons interactives dans votre navigateur. Les premières sont gratuites.

Essayer une leçon gratuiteOu créer un compte gratuit