Retour au blog

var vs make en Go : quand utiliser l'un ou l'autre (avec exemples)

var donne la valeur zéro, make initialise les slices, les maps et les channels. Quand utiliser chacun en Go, et pourquoi écrire dans une map nil provoque un panic alors qu'un append sur un slice nil fonctionne.

var vs make en Go : quand utiliser l'un ou l'autre (avec exemples)

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 ?

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.gogo
package 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 Tmake(T, ...)
Fonctionne pourtout typeslices, maps et channels uniquement
Vous donnela valeur zéroune valeur initialisée et utilisable
Résultat pour un sliceslice nil (len 0, cap 0)slice non nil avec tableau sous-jacent
Résultat pour une mapmap nil (l'écriture provoque un panic)map accessible en écriture
Résultat pour un channelchannel nil (bloque indéfiniment)channel utilisable
Renvoiela valeurla 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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éeFonctionne pourRenvoieLe résultat est-il utilisable ?
var x Ttout typela valeur zéroutilisable si la valeur zéro l'est (pas pour écrire dans une map)
make(T, ...)slice, map, channella valeur Toui, initialisé
new(T)tout type*T, un pointeurpointe vers un T mis à zéro
new(expr) (Go 1.26)toute expressionun pointeur vers cette valeurpointe 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 :

  1. 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.
  2. Un channel sur lequel des goroutines vont envoyer ? Utilisez make(chan T) ou make(chan T, n). Un channel nil bloque indéfiniment.
  3. Un slice que vous voulez pré-dimensionner, pour la vitesse ou pour affecter par index ? Utilisez make([]T, n) ou make([]T, 0, n).
  4. Un slice que vous ne ferez grandir qu'avec append ? Utilisez var s []T. Un slice nil fonctionne avec append.
  5. Un pointeur vers une nouvelle valeur ? Utilisez new(T), ou new(expr) en Go 1.26 pour pointer vers un littéral.
  6. 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) :

É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