Go : Les bases pour développeurs Java/Python en 2026

Apprenez Go rapidement en partant de vos acquis Java ou Python. Goroutines, channels, interfaces et patterns essentiels expliqués pour une transition fluide.

Guide Go pour développeurs Java et Python

Go (ou Golang) s'est imposé comme le langage de choix pour les microservices, les CLI et les systèmes distribués. Créé par Google en 2009, il combine la simplicité de Python avec les performances du C. Pour un développeur venant de Java ou Python, la transition vers Go est étonnamment fluide une fois les concepts clés assimilés.

Pourquoi Go en 2026 ?

Go alimente Docker, Kubernetes, Terraform et de nombreuses infrastructures cloud. Sa compilation rapide, sa gestion native de la concurrence et son déploiement en binaire unique en font un choix idéal pour le backend moderne.

Installation et configuration de Go

L'installation de Go est simple et uniforme sur toutes les plateformes. L'outil go intègre compilation, gestion des dépendances et tests.

bash
# install.sh
# Installation sur macOS avec Homebrew
brew install go

# Installation sur Linux (Ubuntu/Debian)
sudo apt update && sudo apt install golang-go

# Vérification de l'installation
go version
# go version go1.22.0 linux/amd64

La structure d'un projet Go suit des conventions strictes mais simples. Le fichier go.mod définit le module et ses dépendances.

bash
# project-setup.sh
# Création d'un nouveau projet
mkdir mon-projet && cd mon-projet
go mod init github.com/user/mon-projet

# Structure générée :
# mon-projet/
# ├── go.mod    # Manifeste du module
# └── main.go   # Point d'entrée

# Commandes essentielles
go build          # Compile le projet
go run main.go    # Compile et exécute
go test ./...     # Lance tous les tests
go fmt ./...      # Formate le code automatiquement

Premier programme Go

Voici un programme simple qui illustre la syntaxe de base de Go. La comparaison avec Java et Python aide à visualiser les différences.

main.gogo
package main

import "fmt"

// Point d'entrée du programme
func main() {
    // Déclaration avec inférence de type
    message := "Hello, Go!"
    fmt.Println(message)

    // Déclaration explicite
    var count int = 42
    fmt.Printf("Count: %d\n", count)
}

Ce qui frappe immédiatement : pas de point-virgule, pas de parenthèses autour des conditions, et l'inférence de type avec :=. Go privilégie la concision sans sacrifier la lisibilité.

Variables et types fondamentaux

Go est statiquement typé mais offre une excellente inférence de types. Les types de base couvrent la majorité des besoins.

types.gogo
package main

import "fmt"

func main() {
    // Déclaration courte (dans les fonctions uniquement)
    name := "Alice"        // string
    age := 30              // int
    height := 1.75         // float64
    active := true         // bool

    // Déclaration explicite
    var score int = 100
    var rate float64 = 3.14

    // Déclaration multiple
    var (
        firstName string = "Bob"
        lastName  string = "Smith"
        points    int    = 0
    )

    // Zéro-valeurs (valeurs par défaut)
    var count int      // 0
    var text string    // "" (chaîne vide)
    var flag bool      // false
    var ptr *int       // nil

    fmt.Println(name, age, height, active)
}
Zéro-valeurs en Go

Contrairement à Java ou Python, Go initialise automatiquement les variables à leur "zéro-valeur" : 0 pour les nombres, "" pour les strings, false pour les bools, nil pour les pointeurs et slices.

Fonctions et retours multiples

Go permet de retourner plusieurs valeurs, une fonctionnalité utilisée massivement pour la gestion des erreurs.

functions.gogo
package main

import (
    "errors"
    "fmt"
)

// Fonction simple avec paramètres typés
func add(a, b int) int {
    return a + b
}

// Retours multiples (pattern idiomatique pour les erreurs)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Retours nommés
func getUser(id int) (name string, age int, err error) {
    if id <= 0 {
        err = errors.New("invalid ID")
        return
    }
    name = "Alice"
    age = 30
    return
}

// Fonction variadique
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

func main() {
    // Appel simple
    result := add(5, 3)
    fmt.Println("5 + 3 =", result)

    // Gestion d'erreur explicite
    quotient, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("10 / 3 = %.2f\n", quotient)

    // Ignorer une valeur retournée avec _
    name, _, _ := getUser(1)
    fmt.Println("User:", name)

    // Appel variadique
    total := sum(1, 2, 3, 4, 5)
    fmt.Println("Sum:", total)
}

Structures et méthodes

Les structs sont les blocs de construction des types personnalisés en Go. Les méthodes s'attachent aux types via des receivers.

structs.gogo
package main

import "fmt"

// Définition d'une struct
type User struct {
    ID       int
    Username string
    Email    string
    Active   bool
}

// Constructeur (convention : NewTypeName)
func NewUser(id int, username, email string) *User {
    return &User{
        ID:       id,
        Username: username,
        Email:    email,
        Active:   true,
    }
}

// Méthode avec receiver valeur (copie)
func (u User) FullInfo() string {
    status := "inactive"
    if u.Active {
        status = "active"
    }
    return fmt.Sprintf("%s <%s> (%s)", u.Username, u.Email, status)
}

// Méthode avec receiver pointeur (modification possible)
func (u *User) Deactivate() {
    u.Active = false
}

// Méthode avec receiver pointeur pour modification
func (u *User) UpdateEmail(newEmail string) {
    u.Email = newEmail
}

func main() {
    // Création avec constructeur
    user := NewUser(1, "alice", "alice@example.com")
    fmt.Println(user.FullInfo())

    // Modification via méthode
    user.Deactivate()
    fmt.Println(user.FullInfo())

    // Création directe
    user2 := User{
        ID:       2,
        Username: "bob",
        Email:    "bob@example.com",
    }
    fmt.Println(user2.FullInfo())
}
Receiver valeur vs pointeur

Utiliser un receiver pointeur (*User) quand la méthode modifie l'état ou quand la struct est volumineuse. Utiliser un receiver valeur (User) pour les méthodes en lecture seule sur des structs légères.

Interfaces : polymorphisme implicite

Les interfaces Go sont implémentées implicitement. Un type satisfait une interface s'il implémente toutes ses méthodes, sans déclaration explicite.

interfaces.gogo
package main

import (
    "fmt"
    "math"
)

// Définition d'une interface
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle implémente Shape implicitement
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle implémente aussi Shape
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Fonction acceptant l'interface
func PrintShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}

    // Polymorphisme via interface
    PrintShapeInfo(rect)
    PrintShapeInfo(circle)

    // Slice d'interfaces
    shapes := []Shape{rect, circle}
    for _, shape := range shapes {
        PrintShapeInfo(shape)
    }
}

Cette approche est radicalement différente de Java où implements est obligatoire. En Go, la conformité est structurelle et non nominale.

Prêt à réussir tes entretiens Go ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Slices et Maps : collections dynamiques

Les slices sont des vues dynamiques sur des arrays, et les maps sont des tables de hachage natives.

collections.gogo
package main

import "fmt"

func main() {
    // Slice : tableau dynamique
    numbers := []int{1, 2, 3, 4, 5}

    // Ajouter des éléments
    numbers = append(numbers, 6, 7)

    // Slicing (similaire à Python)
    subset := numbers[1:4]  // [2, 3, 4]
    fmt.Println("Subset:", subset)

    // Créer un slice avec make
    scores := make([]int, 0, 10)  // len=0, cap=10
    scores = append(scores, 100, 95, 88)

    // Itération avec range
    for index, value := range numbers {
        fmt.Printf("numbers[%d] = %d\n", index, value)
    }

    // Map : table de hachage
    users := map[string]int{
        "alice": 30,
        "bob":   25,
    }

    // Ajouter/Modifier
    users["charlie"] = 35

    // Vérifier l'existence
    age, exists := users["alice"]
    if exists {
        fmt.Println("Alice's age:", age)
    }

    // Supprimer
    delete(users, "bob")

    // Itération sur map
    for name, age := range users {
        fmt.Printf("%s is %d years old\n", name, age)
    }
}

Gestion des erreurs idiomatique

Go n'a pas d'exceptions. Les erreurs sont des valeurs retournées explicitement, ce qui force une gestion rigoureuse.

errors.gogo
package main

import (
    "errors"
    "fmt"
    "os"
)

// Erreur sentinelle (pour comparaison)
var ErrNotFound = errors.New("resource not found")
var ErrInvalidInput = errors.New("invalid input")

// Erreur custom avec contexte
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}

// Fonction retournant différents types d'erreurs
func GetUser(id int) (string, error) {
    if id <= 0 {
        return "", &ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }
    if id > 1000 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

// Wrapping d'erreurs (Go 1.13+)
func ReadConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("reading config %s: %w", path, err)
    }
    return data, nil
}

func main() {
    // Pattern de base
    user, err := GetUser(-1)
    if err != nil {
        fmt.Println("Error:", err)

        // Type assertion pour erreur custom
        var valErr *ValidationError
        if errors.As(err, &valErr) {
            fmt.Printf("Field: %s\n", valErr.Field)
        }

        // Comparaison avec erreur sentinelle
        if errors.Is(err, ErrNotFound) {
            fmt.Println("User not found")
        }
    } else {
        fmt.Println("User:", user)
    }
}
errors.Is et errors.As

Depuis Go 1.13, utiliser errors.Is() pour comparer avec des erreurs sentinelles et errors.As() pour extraire un type d'erreur spécifique d'une chaîne d'erreurs wrappées.

Goroutines : concurrence légère

Les goroutines sont des threads légers gérés par le runtime Go. Lancer une goroutine coûte seulement quelques Ko de mémoire.

goroutines.gogo
package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // Décrémente le compteur à la fin

    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    // Lancer 5 goroutines
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)  // Préfixe 'go' lance la goroutine
    }

    // Attendre la fin de toutes les goroutines
    wg.Wait()
    fmt.Println("All workers completed")
}

Le sync.WaitGroup permet d'attendre la fin de plusieurs goroutines. C'est le pattern de base pour le parallélisme en Go.

Channels : communication entre goroutines

Les channels sont des conduits typés pour la communication entre goroutines. Ils permettent de synchroniser et d'échanger des données de manière sûre.

channels.gogo
package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int) {
    for i := 1; i <= 5; i++ {
        fmt.Println("Producing:", i)
        ch <- i  // Envoie sur le channel
        time.Sleep(100 * time.Millisecond)
    }
    close(ch)  // Ferme le channel quand terminé
}

func consumer(ch <-chan int, done chan<- bool) {
    for value := range ch {  // Itère jusqu'à fermeture
        fmt.Println("Consuming:", value)
    }
    done <- true
}

func main() {
    ch := make(chan int)     // Channel non bufferisé
    done := make(chan bool)

    go producer(ch)
    go consumer(ch, done)

    <-done  // Attend la fin du consumer
    fmt.Println("All done")
}

Channels bufferisés et select

channels_advanced.gogo
package main

import (
    "fmt"
    "time"
)

func main() {
    // Channel bufferisé (capacité 3)
    buffered := make(chan int, 3)
    buffered <- 1
    buffered <- 2
    buffered <- 3
    // buffered <- 4  // Bloquerait car buffer plein

    fmt.Println(<-buffered)  // 1

    // Select : multiplexage de channels
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    // Attend le premier message disponible
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received:", msg2)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("Timeout!")
        }
    }
}
Philosophie Go : CSP

Go suit le modèle CSP (Communicating Sequential Processes) : "Ne communiquez pas en partageant la mémoire, partagez la mémoire en communiquant." Les channels évitent les race conditions.

Tests en Go

Go intègre un framework de test minimaliste mais efficace. Les fichiers de test se terminent par _test.go.

calculator.gogo
package calculator

func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
calculator_test.gogo
package calculator

import (
    "testing"
)

// Test de base
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// Tests table-driven (pattern recommandé)
func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// Test d'erreur
func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("Expected error for division by zero")
    }
}

// Benchmark
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(100, 200)
    }
}

Les tests s'exécutent avec go test ./... et les benchmarks avec go test -bench=..

HTTP : serveur web minimaliste

Go excelle dans la création de serveurs HTTP performants grâce à sa bibliothèque standard.

server.gogo
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Route simple
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, Go!"))
    })

    // Route JSON
    http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
        users := []User{
            {ID: 1, Name: "Alice"},
            {ID: 2, Name: "Bob"},
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(users)
    })

    // Route avec méthode
    http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case "GET":
            w.Write([]byte("Get user"))
        case "POST":
            w.Write([]byte("Create user"))
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Ce serveur gère des milliers de connexions simultanées grâce aux goroutines. Chaque requête est traitée dans sa propre goroutine automatiquement.

Conclusion

Go offre une approche pragmatique du développement backend : syntaxe simple, compilation rapide, concurrence native et excellent outillage. Pour les développeurs Java ou Python, la transition demande d'accepter quelques conventions différentes (gestion explicite des erreurs, pas de génériques complexes avant Go 1.18), mais les bénéfices en performance et maintenabilité sont immédiats.

Checklist pour bien démarrer

  • ✅ Installer Go via le site officiel ou gestionnaire de paquets
  • ✅ Maîtriser les commandes go build, go run, go test, go fmt
  • ✅ Comprendre la différence entre slices et arrays
  • ✅ Adopter le pattern if err != nil pour la gestion des erreurs
  • ✅ Utiliser goroutines et channels pour la concurrence
  • ✅ Écrire des tests table-driven avec le package testing

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

L'écosystème Go est mature avec des frameworks populaires comme Gin, Echo et Fiber pour le web, et des outils comme Cobra pour les CLI. Avec ces bases solides, l'exploration de sujets avancés comme les génériques (Go 1.18+), le context package et les patterns de concurrence devient accessible.

Tags

#go
#golang
#concurrency
#goroutines
#backend

Partager

Articles similaires