Il y a quelques années, j'ai ouvert un projet Go pour la première fois. Ma réaction initiale :
"Pourquoi est-ce qu'il n'y a pas de classes ? Pourquoi je dois écrire if err != nil
partout ? Pourquoi le compilateur me gueule dessus parce que j'ai importé un package sans l'utiliser ?"
Trois semaines plus tard, je débuggais un problème en production et je réalisais que je comprenais exactement ce que le code faisait, ligne par ligne, sans surprise. Pas de magie, pas de middleware mystérieux, pas de comportement implicite. C'est à ce moment-là que j'ai compris pourquoi Go est conçu comme ça.
Cet article est le guide que j'aurais voulu lire quand j'ai commencé. Pas la doc officielle reformulée — les vrais conseils, les vraies ressources, et ce qui se passe dans la tête quand on vient d'un autre langage.
Pourquoi Go en 2026
Oublie le pitch marketing "Go est rapide, Go est concurrent, Google l'a fait". Ce n'est pas pour ça qu'on adopte Go.
Go est ennuyeux. Et c'est sa force. Un langage où tu ouvres un fichier que tu n'as jamais vu, écrit par quelqu'un d'autre, et tu comprends ce qu'il fait en 30 secondes. Pas de meta-programming. Pas de 15 façons de faire la même chose. Pas de DSL imbriqué dans un DSL. Pas de décorateurs magiques qui transforment le comportement d'une classe à l'exécution.
Après des années de PHP/Symfony avec ses 200 classes de DI, ou de Node.js avec 47 dépendances
dans node_modules, ou de Python où chaque projet a ses propres conventions
non-dites — Go est libérateur. Le code Go ressemble à du code Go. Peu importe qui l'a écrit.
En pratique en 2026 : Go domine l'infrastructure (Kubernetes, Docker, Terraform, Prometheus sont tous écrits en Go), les APIs haute performance, les services backend qui ont besoin de concurrence sérieuse, et les CLIs. Si tu travailles dans ce périmètre, apprendre Go est un bon investissement.
1. Les ressources qui marchent vraiment
Il y a beaucoup de contenu Go sur internet. La majorité est soit trop basique, soit mal foutu. Voici ce qui vaut vraiment le temps.
Les incontournables
A Tour of Go — Le point d'entrée officiel. Interactif, bien structuré, 2-3 heures en le faisant sérieusement. Le faire en entier, pas en diagonale. Chaque exercice est là pour une raison.
Go by Example — Chaque concept du langage illustré avec du code minimal et fonctionnel. Parfait comme référence rapide après le Tour, quand on cherche "comment on fait un channel avec timeout en Go déjà ?".
Effective Go — Le document le plus important qui existe sur Go. C'est la philosophie du langage. Pas juste la syntaxe — pourquoi les interfaces sont implicites, comment penser la composition, pourquoi les erreurs sont des valeurs. Le lire une fois au début (on comprend 50%), puis le relire après 2-3 mois (on comprend tout). Vraiment.
Le blog officiel Go — Articles de fond, écrits par les créateurs du langage. Quelques incontournables : "Go Concurrency Patterns", "Error handling and Go", "Go Slices: usage and internals", "The Go Memory Model". Pas à lire d'un coup — les bookmarker et les lire quand le sujet devient pertinent.
Les livres qui valent leur prix
Let's Go de Alex Edwards — Le meilleur livre pour construire une vraie application web en Go. Payant, mais c'est l'investissement le plus rentable de cette liste. Il explique les patterns production : middleware, sessions, CSRF, tests d'intégration, déploiement. Pas du code jouet. Et pour ceux qui ont un budget serré : une recherche rapide sur GitHub permet de trouver le PDF sans trop de difficulté. Mais si vous pouvez vous le permettre, payez-le — le travail d'Alex Edwards le mérite.
Learning Go de Jon Bodner (O'Reilly) — Bon choix si tu viens d'un langage objet et que tu veux comprendre les "pourquoi" derrière les choix de design de Go. Moins axé web, plus axé fondamentaux du langage.
Ce qu'on oublie souvent : la stdlib
En Go, la bibliothèque standard est incroyablement complète. net/http,
encoding/json, database/sql, testing,
context, sync, io... Avant de chercher une
dépendance externe, vérifier si la stdlib ne fait pas déjà le job. 90% du temps,
elle le fait.
Ce qui est une perte de temps
Les tutoriels YouTube "Build a REST API in Go in 30 minutes" — souvent des mauvaises pratiques, pas de gestion d'erreur, architectures incompréhensibles. Les cours Udemy — trop lents pour un dev expérimenté, souvent outdatés. Essayer d'apprendre Go en lisant du code Kubernetes — c'est comme apprendre le français en lisant Proust. Techniquement correct, mais ce n'est pas le bon point d'entrée.
2. Les 5 changements mentaux à faire rapidement
Ce sont les trucs qui surprennent le plus quand on vient d'un autre langage. Autant les identifier et les accepter dès le départ plutôt que de passer deux semaines à se battre contre le langage.
Il n'y a pas de classes
Go a des structs et des méthodes sur ces structs. Pas d'héritage. La composition remplace l'héritage. Au début ça frustre, après on comprend pourquoi c'est mieux.
// Pas de classe "Animal" avec une méthode "Speak" héritée par "Dog"
// À la place : composition via embedding
type Animal struct {
Name string
}
func (a Animal) Describe() string {
return "Je suis " + a.Name
}
type Dog struct {
Animal // Embedding : Dog "hérite" des méthodes de Animal
Breed string
}
// Dog a maintenant la méthode Describe() sans qu'on ait rien déclaré
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(d.Describe()) // "Je suis Rex"
Les erreurs sont des valeurs, pas des exceptions
Le fameux if err != nil. Oui, c'est verbeux. Non, il n'y a pas de
try/catch. C'est un choix de design : chaque erreur est gérée explicitement à
l'endroit où elle se produit. Après quelques semaines en production, on réalise
qu'on a beaucoup moins de surprises. L'erreur ne remonte pas silencieusement la
call stack pour éclater ailleurs.
Le pattern standard pour wrapper les erreurs avec du contexte :
func getUser(ctx context.Context, id string) (*User, error) {
row := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id)
var u User
if err := row.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("getUser %s: %w", id, err)
// %w permet d'unwrapper l'erreur plus tard avec errors.Is() / errors.As()
}
return &u, nil
}
Chaque couche ajoute du contexte avec fmt.Errorf("contexte: %w", err).
Au final, le message d'erreur se lit comme une trace : "handler > service > store >
erreur SQL".
Les interfaces sont implicites
Pas besoin de déclarer implements. Si un type a les bonnes méthodes,
il satisfait l'interface automatiquement. C'est le concept le plus puissant de Go
et le plus déroutant au début.
// L'interface io.Reader de la stdlib :
type Reader interface {
Read(p []byte) (n int, err error)
}
// Un fichier os.File satisfait Reader.
// Un bytes.Buffer satisfait Reader.
// Un net.Conn satisfait Reader.
// Ta propre struct satisfait Reader si elle a la méthode Read.
// Aucune déclaration "implements" nulle part.
func processData(r io.Reader) error {
// Cette fonction accepte n'importe quoi qui sait lire
data, err := io.ReadAll(r)
// ...
}
// Utilisable avec un fichier, un buffer, une connexion réseau, un mock de test...
// Sans changer une ligne de processData.
Le formatage n'est pas un débat
gofmt formate le code. Point. Pas de config, pas d'options, pas de
guerre tabs vs spaces. Tous les projets Go ont le même style. C'est libérateur.
Configurer l'extension VS Code pour formater à la sauvegarde et ne plus jamais y
penser.
go fmt ./...
# Formate tous les fichiers du projet. Rien à configurer.
Les generics existent, mais avec modération
Les generics sont arrivés en Go 1.18 (2022). La communauté les utilise avec
parcimonie. La philosophie Go : si tu peux faire sans generics, fais sans. Les
interfaces et le type any suffisent dans 90% des cas. Ne pas
commencer par les generics. Apprendre les bases, construire quelque chose qui
fonctionne, et introduire les generics quand on en a un vrai besoin.
3. L'outillage — Ce qui rend Go agréable dès le premier jour
Go a le meilleur outillage intégré de tous les langages que je connais. Tout est dans la distribution standard.
go run main.go # Compile et exécute en une commande
go build ./... # Produit un binaire statique. Un seul fichier.
go test ./... # Lance tous les tests. Testing intégré, pas de framework externe.
go fmt ./... # Formate tout le code
go vet ./... # Analyse statique basique
go mod init mon/module # Initialise un module
go mod tidy # Synchronise go.mod et go.sum avec les imports réels
Le binaire produit par go build est statique. Un seul fichier, pas de
runtime, pas de dépendances. Copier le binaire sur un serveur Linux et ça tourne.
Pas de "mais tu as la bonne version de Python installée ?".
Pour l'environnement de développement, deux outils à installer immédiatement :
-
L'extension VS Code "Go" avec
gopls(le LSP officiel) — autocomplétion, go to definition, rename, inlining des types. Tout marche out of the box après l'installation. -
golangci-lint —
Le linter à installer dès le jour 1. Combine une cinquantaine de linters.
golangci-lint run ./...trouve des bugs réels, pas juste du style.
4. Le premier projet — Quoi construire
Ne pas commencer par un microservice gRPC avec Kafka et Kubernetes. Pas une CLI ultra-compliquée. Pas "je vais réécrire mon projet PHP en Go" — trop de frustration à essayer de mapper les patterns d'un autre langage dans Go.
Le bon premier projet : une API REST simple avec une vraie base de données.
C'est suffisamment complet pour toucher tout ce qui compte : structs, interfaces,
packages, net/http, database/sql, JSON, error handling,
middleware, et tests.
Une structure de projet lisible et idiomatique :
myapp/
├── main.go # Point d'entrée : initialisation, wiring, démarrage serveur
├── go.mod
├── go.sum
├── handler/
│ └── user.go # Handlers HTTP : decode request, appelle store, encode response
├── model/
│ └── user.go # Types : struct User, struct CreateUserRequest...
└── store/
└── postgres.go # Accès BDD : queries SQL, scan dans les structs
Un handler HTTP minimaliste mais correct — avec gestion d'erreur, status codes appropriés, et réponse JSON propre :
type UserHandler struct {
store UserStore
}
// UserStore est une interface définie ici, côté consommateur
type UserStore interface {
GetByID(ctx context.Context, id string) (*model.User, error)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+ : patterns dans http.ServeMux
user, err := h.store.GetByID(r.Context(), id)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// Logguer l'erreur interne, ne pas l'exposer au client
slog.Error("GetUser failed", "id", id, "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(user); err != nil {
slog.Error("encode response failed", "error", err)
}
}
Ce n'est pas du code "avancé". C'est du Go idiomatique pour un premier projet. Chaque erreur est gérée. Les erreurs internes ne sont pas exposées au client. Le context est propagé. C'est ce niveau-là qu'il faut viser dès le départ.
5. Les pièges des premières semaines
Ce sont les erreurs que tout le monde fait. Les identifier à l'avance évite quelques semaines de mauvaises habitudes à désapprendre.
Utiliser des pointeurs partout "pour les performances"
Non. Go passe les structs par valeur efficacement. Les pointeurs servent à deux choses : modifier le receiver dans une méthode, ou éviter la copie pour les gros structs (quelques centaines d'octets). Par défaut, passer par valeur. Ajouter un pointeur quand il y a une raison concrète, pas "au cas où".
// Receiver par valeur : la méthode ne modifie pas la struct
func (u User) FullName() string {
return u.FirstName + " " + u.LastName
}
// Receiver par pointeur : la méthode modifie la struct
func (u *User) SetEmail(email string) {
u.Email = email
}
Créer des interfaces trop tôt
En Go, les interfaces se définissent côté consommateur, pas côté producteur.
Ne pas créer une interface UserService avec 15 méthodes avant d'avoir
un deuxième consommateur qui en a besoin. Le principe Go : "Accept interfaces,
return structs." Définir l'interface aussi petite que possible, seulement pour
les méthodes dont le consommateur a réellement besoin.
// ❌ Trop large : qui va vraiment utiliser ces 15 méthodes ?
type UserService interface {
Create(ctx context.Context, u User) error
GetByID(ctx context.Context, id string) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, u User) error
Delete(ctx context.Context, id string) error
// ... 10 autres méthodes
}
// ✅ Définir l'interface minimale au niveau du consommateur
type UserGetter interface {
GetByID(ctx context.Context, id string) (*User, error)
}
// Le handler n'a besoin que de GetByID : c'est tout ce qu'on expose
type UserHandler struct {
store UserGetter
}
Importer un framework web
Gin, Echo, Fiber... La stdlib net/http avec http.ServeMux
(qui supporte les patterns et les méthodes HTTP depuis Go 1.22) suffit pour 95% des
cas. Pour un peu plus de confort sur le routing,
chi
est léger et idiomatique. Les frameworks lourds ajoutent de la magie et masquent ce
que Go fait bien tout seul.
Ignorer le package context
Le contexte est partout en Go : timeouts, annulation, valeurs liées à une requête.
Commencer à l'utiliser dès le premier projet. Chaque fonction qui fait du I/O
(base de données, HTTP, fichier) doit accepter un context.Context
en premier paramètre. C'est une convention du langage, pas une option.
// ✅ Pattern standard : ctx en premier paramètre pour tout I/O
func (s *UserStore) GetByID(ctx context.Context, id string) (*User, error) {
var u User
err := s.db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&u.ID, &u.Name, &u.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("store.GetByID %s: %w", id, err)
}
return &u, nil
}
Paniquer sur la verbosité
Le code Go est plus long que du Python. C'est normal et intentionnel. Chaque ligne fait une chose claire. Après quelques semaines, on réalise qu'on lit du code Go 3x plus vite que du code Python ou JavaScript équivalent, parce qu'il n'y a pas de comportement implicite caché.
6. La concurrence — Quand s'y mettre
Pas tout de suite. Sérieusement.
Les goroutines et les channels sont la feature la plus visible de Go, et le piège le plus courant pour les débutants. Commencer par écrire du code séquentiel correct. Un code séquentiel correct vaut infiniment mieux qu'un code concurrent buggé. Introduire la concurrence quand il y a un vrai besoin : requêtes parallèles, processing de queue, fan-out sur des appels API.
L'ordre d'apprentissage qui a du sens :
- Goroutines + channels basiques (Go by Example couvre ça parfaitement)
sync.WaitGroupetsync.Mutexpour la coordinationcontextpour l'annulation et les timeoutserrgroup(golang.org/x/sync) pour les patterns production- Le blog post officiel "Go Concurrency Patterns" une fois les bases solides
Un pattern de concurrence basique correct, avec annulation via context :
func processItems(ctx context.Context, items []Item) (int, error) {
g, ctx := errgroup.WithContext(ctx)
results := make(chan Result, len(items))
for _, item := range items {
item := item // capture de variable de boucle (avant Go 1.22)
g.Go(func() error {
result, err := processOne(ctx, item)
if err != nil {
return fmt.Errorf("item %s: %w", item.ID, err)
}
results <- result
return nil
})
}
// Fermer results quand toutes les goroutines sont terminées
go func() {
g.Wait()
close(results)
}()
var count int
for range results {
count++
}
return count, g.Wait()
}
7. L'organisation du code
Quelques règles Go sur les packages qui évitent des mauvaises habitudes :
-
Un module = un repo.
go mod init github.com/user/myproject. - Un package = un dossier. Le nom du package correspond au nom du dossier.
-
Pas de package
utils,helpers,common. C'est un anti-pattern Go classique — ces packages finissent fourre-tout. Nommer les packages par ce qu'ils font :store,handler,middleware. -
Les identifiants exportés commencent par une majuscule.
Userest public,userest privé au package. Pas de mot-clépublicouprivate. -
Garder les packages petits et focalisés. Un package
userqui gère les utilisateurs, pas un packagemodelsqui fait tout.
8. Les conventions qui comptent
Go a des conventions fortes. Les adopter dès le départ, même quand elles semblent contre-intuitives.
-
Noms courts dans les portées locales :
upour un user,ctxpour context,errpour error,rpour une requête HTTP,wpour le ResponseWriter. Go préfère la brièveté quand le contexte est clair. Les noms longs sont pour les identifiants exportés. -
Pas de getters avec "Get" :
user.Name(), pasuser.GetName(). Le "Get" est implicite en Go. -
Fichiers de test à côté du code :
user_test.goà côté deuser.go. Pas de dossiertests/séparé. -
Les commentaires documentent le "pourquoi" : le code Go est
censé être lisible sans commentaires. Un commentaire qui dit "// incrémente
le compteur" devant
count++n'apporte rien. Un commentaire qui explique pourquoi on utilise un mutex ici plutôt qu'un channel — ça vaut quelque chose. -
Ignorer une erreur est un code smell :
_ = f()ouresult, _ := f()doit être rarissime et commenté. Si une fonction retourne une erreur, la gérer.
9. La suite — Après les bases
Une fois confortable avec les bases, ce qui vaut le temps :
Lire le code source de la stdlib. Sérieusement. net/http,
encoding/json, database/sql sont des exemples de code Go
idiomatique écrit par les créateurs du langage. C'est la meilleure école. La stdlib
Go est lisible — pas des milliers de fichiers de framework abstrait.
Apprendre les patterns de concurrence avancés : worker pools, fan-out
/ fan-in, semaphores avec channels bufferisés, sync.Once pour
l'initialisation paresseuse.
Comprendre les interfaces composables : io.ReadWriteCloser
est la composition de Reader + Writer + Closer.
Ce pattern de composition d'interfaces est omniprésent dans la stdlib et dans le
code Go idiomatique.
S'intéresser au profiling avec pprof quand on a un vrai
problème de performance — pas avant. Go a des outils de profiling excellents intégrés,
mais les utiliser sur un problème hypothétique ne sert à rien.
Pour aller plus loin, deux livres qui valent vraiment leur prix :
- Concurrency in Go de Katherine Cox-Buday — Le livre de référence sur la concurrence Go. Goroutines, channels, patterns avancés, pièges à éviter.
- 100 Go Mistakes de Teiva Harsanyi — Chaque chapitre est une erreur réelle avec l'explication et la correction. Plus utile qu'un livre de bonnes pratiques génériques parce que tout est ancré dans des bugs concrets.
Conclusion
Go est un langage qui récompense la patience et la simplicité. Les premières semaines
sont frustrantes quand on vient d'un langage plus expressif — on a l'impression de se
répéter, de taper trop de if err != nil, de manquer des abstractions.
Et puis un jour, on debug un problème en production et on réalise qu'on comprend exactement ce que le code fait, ligne par ligne, sans aller chercher ce que fait un décorateur magique ou ce que inject un middleware mystérieux. C'est à ce moment-là qu'on comprend pourquoi Go est conçu comme ça.
Le conseil le plus important : ne pas essayer de faire du Go comme on ferait du Java, du Python ou du PHP. Accepter les conventions Go — les interfaces implicites, les erreurs explicites, l'absence de classes, la verbosité assumée. Le langage a été designé avec des contraintes intentionnelles, et elles ont du sens une fois qu'on a assez de contexte pour les voir.
Le plan d'action concret : A Tour of Go (2-3h), puis construire une API REST simple
avec net/http et PostgreSQL, puis relire Effective Go. Dans cet ordre.
Pas de raccourcis.