Go propose deux façons de créer une variable, var et make, et choisir la mauvaise est à l'origine de nombreux bugs de débutant. Le grand classique est une map qui provoque un panic dès sa première écriture. Juste derrière vient un channel qui bloque indéfiniment. Cet article montre quand utiliser chacun et pourquoi les slices, les maps et les channels sont des cas particuliers. Tous les exemples sont exécutables.
Table des matières
- Quelle est la différence entre var et make en Go ?
- Quand utiliser var ?
- Quand utiliser make ?
- Pourquoi écrire dans une map nil provoque un panic alors qu'append sur un slice nil fonctionne ?
- Et new ? (var vs make vs new)
- Une règle de décision rapide
- Questions fréquentes
- Sources
Quelle est la différence entre var et make en Go ?
var déclare une variable et lui affecte la valeur zéro de son type. Cela fonctionne pour tous les types du langage. make n'accepte qu'un slice, une map ou un channel, les trois types intégrés qui ont besoin d'une préparation au runtime, et renvoie une valeur capable de contenir des données immédiatement. La valeur zéro de ces trois types est nil, donc var saute la préparation que fait make.
Concrètement, make alloue la table de hachage qui se trouve derrière une map, met en place les files d'envoi et de réception d'un channel, et fournit un tableau sous-jacent à un slice. Si vous déclarez l'un de ces trois types avec var, vous obtenez nil à la place.
La même map, déclarée des deux façons :
example.gogopackage main import "fmt" func main() { var counts map[string]int // nil map, no hash table yet made := make(map[string]int) // initialized, ready to write fmt.Println(counts == nil) // true fmt.Println(made == nil) // false made["/health"] = 1 // fine, made is initialized fmt.Println(made["/health"]) // 1 // counts["/health"] = 1 // panic: assignment to entry in nil map }
On s'attend souvent à ce qu'une allocation renvoie un pointeur. Ce n'est pas le cas de make. Il alloue, puis vous donne directement la valeur.
Ainsi, make([]int, 3) renvoie un simple []int, pas un *[]int, et vous l'utilisez sans rien déréférencer. make(map[string]int) renvoie une map dans laquelle vous pouvez écrire immédiatement. La fonction intégrée qui renvoie bien un pointeur est new, présentée plus bas.
var x T | make(T, ...) | |
|---|---|---|
| Fonctionne pour | tout type | slices, maps et channels uniquement |
| Vous donne | la valeur zéro | une valeur initialisée et utilisable |
| Résultat pour un slice | slice nil (len 0, cap 0) | slice non nil avec tableau sous-jacent |
| Résultat pour une map | map nil (l'écriture provoque un panic) | map accessible en écriture |
| Résultat pour un channel | channel nil (bloque indéfiniment) | channel utilisable |
| Renvoie | la valeur | la valeur, jamais un pointeur |
Quand utiliser var ?
Utilisez var pour tout type dont la valeur zéro est déjà utile : nombres, booléens, chaînes, structs, pointeurs et interfaces. Go est conçu pour que la valeur zéro soit un état initial valide, ce qui signifie que vous avez rarement besoin d'initialiser quoi que ce soit au préalable.
bytes.Buffer en est un bon exemple. Il n'a pas de constructeur. Vous le déclarez avec var et vous commencez à écrire :
example.gogopackage main import ( "bytes" "fmt" ) func main() { var buf bytes.Buffer // zero value is ready to use buf.WriteString("status=") buf.WriteString("ok") fmt.Println(buf.String()) // status=ok }
sync.Mutex fonctionne de la même manière. Sa valeur zéro est un mutex déverrouillé, et il n'existe pas de constructeur NewMutex parce que vous n'en avez pas besoin.
var convient aussi pour un slice, à condition de ne le faire grandir qu'avec append. Un slice nil est un slice vide valide, de longueur 0 et de capacité 0, et append alloue un tableau sous-jacent lorsque vous ajoutez le premier élément. En revanche, vous ne pouvez pas y accéder par index. ids[0] = 3 provoque un panic index out of range, car la longueur vaut toujours 0. Si vous devez affecter par index, utilisez make([]int, n) pour que le slice ait une vraie longueur dès le départ. Pour collecter des éléments avec append, un slice nil convient très bien :
example.gogopackage main import "fmt" func main() { var ids []int // nil slice, but append-ready for i := 1; i <= 3; i++ { ids = append(ids, i*10) } fmt.Println(ids) // [10 20 30] fmt.Println(ids == nil) // false, append allocated it }
Écrire ids := make([]int, 0) ici ne vous apporte rien. Le slice nil est plus court à écrire et se comporte de la même façon dès le premier append. Si vous voulez vous entraîner sur ce point, le cours Composite Types propose une série complète d'exercices sur le comportement des slices et des maps.
Quand utiliser make ?
Utilisez make quand un slice, une map ou un channel doit contenir des données avant que quoi que ce soit n'y soit ajouté avec append. En pratique, il s'agit d'une map dans laquelle vous allez écrire des clés, d'un channel sur lequel des goroutines vont envoyer, ou d'un slice que vous voulez pré-dimensionner pour que son tableau sous-jacent existe dès le départ.
Les maps sont le cas le plus fréquent, car il n'existe aucun équivalent d'append qui en alloue une pour vous. Si vous comptez affecter une clé, quelle qu'elle soit, vous devez d'abord créer la map avec make. Compter les requêtes par chemin dans un handler HTTP en est un cas typique :
example.gogopackage main import "fmt" func main() { hits := make(map[string]int) // must initialize before writing for _, path := range []string{"/api", "/api", "/health"} { hits[path]++ // safe: map is initialized } fmt.Println(hits["/api"]) // 2 fmt.Println(hits["/health"]) // 1 }
Les channels suivent la même logique. Un channel sur lequel vous envoyez doit être créé avec make, car un channel nil bloque indéfiniment (nous y revenons plus bas). Un fan-out de workers commence par un channel créé avec make :
example.gogopackage main import "fmt" func main() { jobs := make(chan int, 3) // buffered channel, capacity 3 jobs <- 1 jobs <- 2 close(jobs) for j := range jobs { fmt.Println("processing job", j) } }
Pour les slices, make existe sous deux formes. make([]T, n) vous donne un slice de longueur n rempli de valeurs zéro, auquel vous accédez directement par index. make([]T, 0, n) vous donne un slice vide avec de la place pour n éléments, dans lequel vous faites des append sans réallocation. Utilisez la première forme quand vous allez remplir chaque case par index, par exemple pour lire un nombre fixe d'octets :
example.gogopackage main import "fmt" func main() { // length 4, index into it directly buf := make([]byte, 4) copy(buf, []byte("data")) fmt.Println(string(buf)) // data // length 0, capacity 8, append without regrowing sums := make([]int, 0, 8) for i := 0; i < 8; i++ { sums = append(sums, i*i) } fmt.Println(sums) // [0 1 4 9 16 25 36 49] }
La seconde forme est une question de performances. À mesure qu'un slice grandit, append le recopie sans cesse dans des tableaux plus grands. Si vous savez à peu près combien d'éléments vont arriver, fixer la capacité dès le départ évite ces copies. Le comportement du programme ne change pas, seule sa vitesse change.
Pourquoi écrire dans une map nil provoque un panic alors qu'append sur un slice nil fonctionne ?
Les deux n'ont pas été conçus de la même façon. append sait gérer un slice nil. Au premier appel, il alloue un tableau sous-jacent et renvoie un vrai slice, donc écrire via append fonctionne toujours. Les maps n'ont rien de comparable. Une écriture va directement dans une table de hachage, une map nil n'en a pas, et le runtime provoque un panic plutôt que de deviner ce que vous vouliez faire.
La plupart des développeurs tombent sur ce problème dès leur première semaine avec Go. Voici ce que fait chaque type nil, en commençant par le slice nil, que vous pouvez lire et étendre avec append sans problème :
example.gogopackage main import "fmt" func main() { var s []int // nil slice fmt.Println(len(s), cap(s)) // 0 0 fmt.Println(s == nil) // true s = append(s, 42) // works: append allocates fmt.Println(s) // [42] }
Une map nil se comporte différemment. La lire renvoie la valeur zéro du type des valeurs, et la parcourir avec range ne fait rien, mais la première écriture provoque un panic :
example.gogopackage main import "fmt" func main() { var m map[string]int // nil map fmt.Println(m["missing"]) // 0, reading nil map is safe fmt.Println(len(m)) // 0, also safe m["key"] = 1 // panic: assignment to entry in nil map fmt.Println(m) }
Ce contraste entre des lectures sans risque et des écritures qui provoquent un panic explique pourquoi ce bug passe facilement inaperçu. Une map nil réussit tous les tests qui se contentent de la lire, puis provoque un panic la première fois que de vraies données arrivent. La correction ne change pas : initialisez-la avec make avant la première écriture, ou acceptez une map déjà créée par l'appelant.
Les channels nil ont le comportement le plus étrange des trois. Ils ne provoquent pas de panic. Un envoi ou une réception sur un channel nil ne progresse tout simplement jamais, ce qui, dans une seule goroutine, donne un deadlock :
example.gogopackage main func main() { var ch chan int // nil channel ch <- 1 // blocks forever: fatal deadlock }
Cela ressemble à un piège, mais c'est utile dans un select. Un channel nil n'est jamais prêt, donc affecter nil à une variable de channel désactive le case correspondant, ce qui permet de désactiver une branche au runtime. Attention toutefois à close : fermer un channel nil provoque un panic, donc ne fermez que les channels que vous avez créés avec make. Le cours Concurrency Fundamentals traite en détail le pattern select avec des channels nil. Le duo map nil et slice nil figure aussi dans notre liste des erreurs courantes à éviter en Go, car les développeurs expérimentés s'y font prendre eux aussi.
Et new ? (var vs make vs new)
new est la troisième fonction intégrée d'allocation, et elle ne remplit pas le même rôle que make. new(T) alloue un espace mis à zéro pour un T et renvoie un *T. make ne gère que les slices, les maps et les channels, et renvoie la valeur. new fonctionne pour tout type et renvoie toujours un pointeur.
La raison habituelle de l'appeler est d'obtenir un pointeur vers une nouvelle valeur zéro sans déclarer au préalable une variable nommée :
example.gogopackage main import "fmt" func main() { p := new(int) // *int pointing at a zeroed int fmt.Println(*p) // 0 *p = 7 fmt.Println(*p) // 7 }
Depuis Go 1.26, new accepte aussi une expression. new(expr) alloue de l'espace, y stocke la valeur de l'expression et renvoie un pointeur vers celle-ci. Les anciens helpers du type intPtr, qui n'existaient que pour obtenir un pointeur vers un littéral, deviennent donc inutiles :
example.gogopackage main import "fmt" func main() { port := new(8080) // *int pointing at 8080 (Go 1.26) enabled := new(true) // *bool pointing at true fmt.Println(*port) // 8080 fmt.Println(*enabled) // true }
C'est utile pour les champs de struct optionnels, où un pointeur nil signifie « non défini » et un pointeur non nil signifie que quelqu'un a choisi une valeur. Le cours Pointers & Memory propose un exercice de valeurs par défaut de configuration construit sur ce pattern. Voici les trois fonctions intégrées côte à côte :
| Fonction intégrée | Fonctionne pour | Renvoie | Le résultat est-il utilisable ? |
|---|---|---|---|
var x T | tout type | la valeur zéro | utilisable si la valeur zéro l'est (pas pour écrire dans une map) |
make(T, ...) | slice, map, channel | la valeur T | oui, initialisé |
new(T) | tout type | *T, un pointeur | pointe vers un T mis à zéro |
new(expr) (Go 1.26) | toute expression | un pointeur vers cette valeur | pointe vers la valeur donnée |
Pour un slice, une map ou un channel que vous allez remplir, c'est presque toujours make qu'il vous faut. Utilisez new quand vous avez spécifiquement besoin d'un pointeur, le plus souvent un pointeur vers un littéral pour un champ optionnel.
Une règle de décision rapide
Si vous hésitez, parcourez cette liste et arrêtez-vous à la première ligne qui correspond :
- Une map dans laquelle vous allez écrire des clés ? Utilisez
make(map[K]V). Une map nil provoque un panic à la première écriture. - Un channel sur lequel des goroutines vont envoyer ? Utilisez
make(chan T)oumake(chan T, n). Un channel nil bloque indéfiniment. - Un slice que vous voulez pré-dimensionner, pour la vitesse ou pour affecter par index ? Utilisez
make([]T, n)oumake([]T, 0, n). - Un slice que vous ne ferez grandir qu'avec
append? Utilisezvar s []T. Un slice nil fonctionne avecappend. - Un pointeur vers une nouvelle valeur ? Utilisez
new(T), ounew(expr)en Go 1.26 pour pointer vers un littéral. - Autre chose (int, string, bool, struct, la plupart des types de la bibliothèque standard) ? Utilisez
var, car la valeur zéro est prête à l'emploi.
S'il ne fallait retenir qu'une chose, ce serait celle-ci : avant d'écrire une clé dans une map, vérifiez d'où vient cette map. Une map reçue en paramètre a vraisemblablement été initialisée par l'appelant. Une map que vous avez déclarée vous-même avec var est nil, et la première écriture provoquera un panic.
Questions fréquentes
Peut-on utiliser var pour une map ?
Vous pouvez déclarer une map avec var m map[string]int. La lire, la parcourir avec range et appeler len dessus sont des opérations sans risque. Écrire une clé ne l'est pas : m["x"] = 1 provoque un panic avec assignment to entry in nil map. Si vous ne faites que lire, var convient. Si vous écrivez la moindre clé, initialisez d'abord la map avec make ou prenez-en une déjà construite par l'appelant.
make([]int, 0) est-il équivalent à var s []int ?
Les deux ont une longueur de 0 et se comportent de la même façon dès que vous faites un append. La différence apparaît quand vous comparez avec nil. var s []int vaut nil, alors que make([]int, 0) est un slice vide non nil. Cela compte si votre code ou un test vérifie s == nil, ou si vous encodez en JSON, où nil devient null et le slice vide devient []. Pour de simples append, préférez var.
make fonctionne-t-il avec les structs ?
Non. make n'accepte que les slices, les maps et les channels, donc make(MyStruct) est une erreur de compilation. Un struct n'a pas besoin de la préparation au runtime que fait make. Créez-en un avec un littéral composite comme MyStruct{}, déclarez-le avec var s MyStruct pour obtenir la valeur zéro, ou utilisez new(MyStruct) quand vous voulez un *MyStruct.
make ou new : lequel pour obtenir un pointeur ?
Utilisez new quand vous voulez un pointeur. new(T) renvoie un *T qui pointe vers une valeur mise à zéro, et en Go 1.26, new(expr) renvoie un pointeur vers la valeur de cette expression. make ne renvoie jamais de pointeur. Il renvoie un slice, une map ou un channel initialisé. Les deux ne se recoupent pas : make vous donne une collection utilisable et new vous donne un pointeur.
Sources
Références principales citées dans cet article (dernière vérification le 3 juillet 2026) :
- The Go Programming Language Specification: Making slices, maps and channels, consulté le 2026-07-03
- The Go Programming Language Specification: Allocation (new), consulté le 2026-07-03
- The Go Programming Language Specification: The zero value, consulté le 2026-07-03
- Effective Go: Allocation with make and new, consulté le 2026-07-03
- Go 1.26 Release Notes: changes to the language, consulté le 2026-07-03
