Premiers pas
Hello World
Chaque programme Go commence dans la fonction main() du package main. Utilisez 'go run' pour exécuter, 'go build' pour compiler. gofmt formate automatiquement le code (tabulations, espacement). L'instruction import apporte des packages — fmt gère les E/S formatées.
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}
// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.goModules Go (go.mod)
Les modules Go (go.mod) gèrent les dépendances depuis Go 1.11. 'go mod init' crée le fichier de module. 'go get' ajoute des dépendances. 'go mod tidy' supprime les imports inutilisés et ajoute les manquants. Le chemin du module est le chemin d'import pour votre package.
// Initialize a new module
// $ go mod init github.com/user/project
// go.mod file:
module github.com/user/project
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
golang.org/x/sync v0.3.0
)
// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy // clean up unused depsStructure des packages
Go organise le code en packages — un package par répertoire. Les noms capitalisés (Println, Add) sont exportés (publics) ; les noms minuscules (privés) sont privés au package. Le répertoire 'internal' restreint les imports au module parent. Le nom du package devrait correspondre au nom du répertoire.
// project structure:
// myproject/
// go.mod
// main.go // package main
// utils/
// helper.go // package utils
// math.go
// internal/
// api/ // private (only importable within parent)
// main.go
package main
import (
"fmt"
"github.com/user/project/utils"
)
func main() {
fmt.Println(utils.Add(1, 2))
}
// Exported names start with uppercase letter
// unexported names start with lowercase letterVariables & Constantes
Utilisez 'var name type = value' pour les déclarations explicites, 'name := value' pour les déclarations courtes (fonctions uniquement). Go a des valeurs zéro : 0 pour les nombres, '' pour les chaînes, false pour les booléens, nil pour les pointeurs/slices/maps. Les constantes (const) sont à la compilation et ne peuvent pas utiliser :=.
package main
import "fmt"
// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159
func main() {
// var with type
var name string = "Alice"
var age int = 30
// Short declaration (only inside functions)
city := "NYC"
// Multiple assignment
a, b, c := 1, 2, 3
// Zero values (default)
var x int // 0
var s string // ""
var ok bool // false
var p *int // nil
fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}Types de base & Conversion
Go nécessite une conversion de type explicite — il n'y a pas de conversion implicite comme en C. rune est un alias pour int32 (représente un point de code Unicode). byte est un alias pour uint8. La conversion entre types numériques peut perdre en précision (float vers int tronque). string(65) convertit un point de code en son caractère.
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793
// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias
// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n) // int -> float64
var s string = string(65) // 65 -> "A" (rune to string)
var i2 int = int(f) // float64 -> int (truncates)
// Type inference
var x = 42 // int
var y = 3.14 // float64
var z = len("hi") // intChaînes & Formatage
Bases des chaînes
Les chaînes Go sont des séquences d'octets immuables. len() renvoie le nombre d'octets, pas le nombre de caractères — utilisez utf8.RuneCountInString() pour Unicode. Les chaînes sont comparées lexicographiquement avec ==, <, >. Le range sur une chaîne itère par rune (point de code Unicode), gérant correctement les caractères multi-octets.
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, World"
// Length (bytes, not runes!)
fmt.Println(len(s)) // 12
// Concatenation
s1 := "Hello" + " " + "World"
s2 := fmt.Sprintf("%s = %d", "age", 30)
// Comparison
fmt.Println("abc" == "abc") // true
fmt.Println("a" < "b") // true (lexicographic)
// Iteration (by rune)
for i, r := range "Go语" {
fmt.Printf("%d: %c\n", i, r)
}
}Package strings
Le package strings fournit des opérations courantes sur les chaînes. Contains/HasPrefix/HasSuffix vérifient les sous-chaînes. Split divise par délimiteur ; Join combine. Replace prend un compte (-1 pour tout). TrimSpace supprime les espaces de tête/de fin. Toutes les fonctions renvoient de nouvelles chaînes (les chaînes sont immuables).
import (
"fmt"
"strings"
)
s := "Hello, World"
// Case
strings.ToUpper(s) // "HELLO, WORLD"
strings.ToLower(s) // "hello, world"
strings.Title("hello world") // "Hello World"
// Search
strings.Contains(s, "World") // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World") // 7 (-1 if not found)
strings.Count(s, "l") // 3
// Split & Join
parts := strings.Split("a,b,c", ",") // ["a", "b", "c"]
joined := strings.Join(parts, "-") // "a-b-c"
// Replace
strings.Replace(s, "l", "L", 1) // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L") // "HeLLo, WorLd"
strings.TrimSpace(" hi ") // "hi"strconv & Formatage
strconv convertit entre chaînes et nombres — vérifiez toujours le retour d'erreur. fmt.Printf formate la sortie : %d (int), %f (float), %s (chaîne), %t (bool), %T (type), %x (hex), %q (entre guillemets). Utilisez %.2f pour 2 décimales, %05d pour le remplissage par zéro. Sprintf renvoie la chaîne au lieu d'imprimer.
import (
"fmt"
"strconv"
)
// String to number
n, err := strconv.Atoi("42") // string -> int
f, err := strconv.ParseFloat("3.14", 64)
// Number to string
s1 := strconv.Itoa(42) // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42) // int
fmt.Printf("Hex: %x\n", 255) // ff
fmt.Printf("Pad: %05d\n", 42) // 00042
fmt.Printf("Quote: %q\n", "hi") // "hi"
// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)Chaînes brutes & Multilignes
Les chaînes brutes (backticks) préservent tout littéralement — pas de séquences d'échappement, peuvent s'étendre sur plusieurs lignes. Utilisez pour regex, SQL, modèles HTML. Les chaînes interprétées (guillemets doubles) traitent \n, \t, etc. Pour une concaténation de chaîne efficace dans les boucles, utilisez strings.Builder (évite l'allocation O(n²)).
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`
// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"
// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`
// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("line\n")
}
result := sb.String()Unicode & Runes
Les chaînes Go sont des séquences d'octets encodées en UTF-8. len() donne les octets ; utf8.RuneCountInString() donne les caractères. Le range sur une chaîne décode l'UTF-8 automatiquement. Pour modifier une chaîne, convertissez en []rune, modifiez et reconvertissez. C'est essentiel pour le traitement de texte internationalisé.
import (
"fmt"
"unicode/utf8"
)
s := "Hello, 世界"
// Byte length vs rune length
fmt.Println(len(s)) // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)
// Decode runes manually
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%c ", r)
i += size
}
// Range automatically decodes runes
for i, r := range s {
fmt.Printf("%d:%c ", i, r)
}
// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"Flux de contrôle
If / Else
Le if de Go n'a pas besoin de parenthèses autour des conditions, mais les accolades sont obligatoires (même pour les corps d'une ligne). L'instruction d'initialisation (if x := f(); x > 0) est limitée au bloc if/else — courante pour la vérification d'erreur. Ce motif garde la portée des variables serrée.
// Basic if/else
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// If with initialization statement
if n := computeValue(); n > 100 {
fmt.Println("big:", n)
} else {
fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)
// No parentheses needed, but braces are requiredBoucles for
Go n'a qu'un seul mot-clé de boucle : 'for'. Il gère le style C, le style while (for cond), infini (for) et l'itération (for range). Range fonctionne avec les slices, maps, chaînes et canaux. Utilisez _ pour ignorer l'index ou la valeur. L'ordre d'itération des maps est aléatoire par conception.
// C-style for
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style (condition only)
n := 10
for n > 0 {
n--
}
// Infinite loop
for {
break
}
// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
fmt.Printf("%d: %d\n", index, value)
}
// Skip index or value with _
for _, value := range nums {
fmt.Println(value)
}
// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
fmt.Println(key, val)
}Switch
Le switch de Go ne passe pas au cas suivant par défaut (contrairement à C/Java) — chaque cas est une branche séparée. Utilisez fallthrough pour le forcer. Plusieurs valeurs par cas utilisent des virgules. Un switch sans expression agit comme une chaîne if/else plus propre. Un switch avec instruction d'initialisation limite la variable au switch.
// Basic switch
day := 3
switch day {
case 1:
fmt.Println("Mon")
case 2, 3, 4, 5:
fmt.Println("Weekday")
case 6, 7:
fmt.Println("Weekend")
default:
fmt.Println("Invalid")
}
// Switch with no expression (like if/else chain)
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
default:
grade = "C"
}
// Switch with init
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Linux")
case "darwin":
fmt.Println("macOS")
}
// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two") // executes even though value is 1
}Defer
defer planifie un appel de fonction à exécuter lorsque la fonction englobante revient — dernier entré premier sorti (LIFO). Utilisez-le pour le nettoyage (fermer les fichiers, libérer les verrous, fermer les connexions). Les arguments des appels différés sont évalués immédiatement, mais l'appel s'exécute au retour. Les defers s'exécutent même si la fonction panique.
// Defer runs when function returns (LIFO order)
func main() {
defer fmt.Println("third")
defer fmt.Println("second")
fmt.Println("first")
}
// Output: first, second, third
// Common: resource cleanup
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when readFile returns
// ... use f ...
return nil
}
// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2Goto, Break, Continue
break sort de la boucle la plus interne ; continue passe à l'itération suivante. Les étiquettes (break outer) sortent des boucles imbriquées. goto existe mais est rarement utilisé — préférez le flux de contrôle structuré. Les étiquettes peuvent aussi être utilisées avec continue pour passer à l'itération suivante d'une boucle externe.
// Break and continue
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // skip even numbers
}
if i > 7 {
break // exit loop
}
fmt.Println(i)
}
// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // exits both loops
}
}
}
// Goto (rarely used, avoid)
i := 0
loop:
if i < 5 {
fmt.Println(i)
i++
goto loop
}Select (Opérations de canal)
select est comme un switch pour les canaux — il attend sur plusieurs opérations de canal et choisit la première prête. Le cas default le rend non bloquant. Utilisez select dans une boucle pour les motifs pilotés par événements. time.After() crée un canal de délai. select choisit au hasard si plusieurs cas sont prêts, empêchant la privation.
// Select waits on multiple channel operations
select {
case msg := <-messages:
fmt.Println("received:", msg)
case <-timeout:
fmt.Println("timed out")
default:
fmt.Println("no activity") // non-blocking
}
// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
}
// Select in a loop (event loop pattern)
for {
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(5 * time.Second):
return // timeout after 5s of inactivity
}
}Fonctions
Définir & Retours multiples
Les fonctions Go peuvent renvoyer plusieurs valeurs — le motif canonique est (résultat, erreur). Les retours nommés agissent comme des variables déclarées initialisées aux valeurs zéro ; un retour 'nu' les utilise. Les retours nommés améliorent la lisibilité pour les fonctions complexes mais peuvent être confus s'ils sont surutilisés. Vérifiez toujours les erreurs immédiatement.
// Function with multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil
}
// Named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // "naked" return (uses named values)
}
// Usage
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
x, y := split(100)
fmt.Println(x, y)Variadique & Fermetures
Les fonctions variadiques (...T) acceptent un nombre quelconque d'arguments, reçus comme slice. Déployez un slice avec .... Les fermetures capturent les variables par référence depuis leur portée englobante — la fonction counter() renvoie une fermeture qui se souvient de 'count'. Les fermetures sont utiles pour les rappels, itérateurs et fonctions avec état.
// Variadic function (variable arguments)
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6
// Closure (function that captures variables)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3Fonctions comme valeurs
Les fonctions en Go sont des valeurs de première classe — elles peuvent être affectées à des variables, passées en arguments et stockées dans des structures de données. Définissez des types de fonction avec 'type Name func(params) returns'. Les maps de fonctions sont utiles pour les tables de distribution, les gestionnaires de commandes et les patrons de stratégie.
// Functions are first-class values
func apply(f func(int) int, x int) int {
return f(x)
}
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25
// Function type
type MathFunc func(int) int
var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11
// Map of functions
ops := map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
"mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7init() & Fonctions anonymes
Les fonctions init() s'exécutent automatiquement avant main(), dans l'ordre où elles sont déclarées. Utilisez pour la configuration (chargement de config, validation, enregistrement). Plusieurs fonctions init() par fichier sont autorisées. Les fonctions anonymes peuvent être invoquées immédiatement (IIFE) ou affectées à des variables. Elles sont aussi utilisées pour les goroutines.
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main
import "fmt"
var config string
func init() {
// Setup code: config, connections, validation
config = "production"
fmt.Println("init 1")
}
func init() {
fmt.Println("init 2, config:", config)
}
func main() {
fmt.Println("main")
}
// Output: init 1, init 2, main
// Anonymous function (IIFE)
result := func(x int) int {
return x * 2
}(5) // immediately invoked, result = 10
// Goroutine with anonymous function
go func(msg string) {
fmt.Println(msg)
}("async")Génériques (Go 1.18+)
Go 1.18+ prend en charge les génériques avec des paramètres de type [T any]. Les contraintes de type (interfaces) limitent les types autorisés — utilisez 'any' pour aucune contrainte, 'comparable' pour les opérateurs == / !=. Les contraintes d'union (int | float64) autorisent des types spécifiques. Les génériques permettent des structures de données et algorithmes réutilisables et de type sûr sans duplication de code.
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]
strs := Map(nums, func(n int) string {
return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]
// Type constraints
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}Méthodes
Les méthodes sont des fonctions avec un argument récepteur. Les récepteurs de valeur (r Rectangle) travaillent sur une copie — ne peuvent pas modifier l'original. Les récepteurs de pointeur (r *Rectangle) peuvent modifier et éviter la copie de grandes structures. La cohérence compte : si une méthode utilise un récepteur de pointeur, toutes devraient. Les méthodes peuvent être définies sur tout type du même package.
type Rectangle struct {
Width, Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// Method on non-struct types
type MyString string
func (s MyString) Shout() string {
return strings.ToUpper(string(s)) + "!"
}
// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width) // 20
s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!Structures de données
Tableaux & Slices
Les tableaux ont une longueur fixe ; les slices sont dynamiques (soutenus par des tableaux). append() ajoute des éléments, augmentant la capacité au besoin. make([]T, len, cap) pré-alloue pour l'efficacité. Le découpage crée une vue (partage le tableau sous-jacent) — utilisez copy() pour des données indépendantes. Vérifiez toujours len/cap lors de l'optimisation.
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred
// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4) // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10
// Slicing
sub := nums[1:4] // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:] // [4, 5, 6, 7]
// Copy
dst := make([]int, len(nums))
copy(dst, nums)
// Length and capacity
fmt.Println(len(nums), cap(nums))Maps
Les maps sont les tables de hachage de Go — paires clé/valeur non ordonnées. Le motif comma-ok (val, ok := m[key]) vérifie si une clé existe. delete() supprime une clé. L'ordre d'itération des maps est aléatoire par conception. Les maps sont des types de référence — les passer à des fonctions partage les données sous-jacentes. Les maps nil ne peuvent pas être écrites (utilisez make()).
// Create a map
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Using make
ages := make(map[string]int)
ages["Charlie"] = 35
// Access
fmt.Println(m["Alice"]) // 30
// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
fmt.Println("not found")
}
// Delete
delete(m, "Bob")
// Iterate (random order)
for name, age := range m {
fmt.Printf("%s: %d\n", name, age)
}
// Length
fmt.Println(len(m))
// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}Structs
Les structs regroupent des champs liés. Utilisez l'initialisation nommée (Person{Name: ...}) pour la clarté. new() renvoie un pointeur avec des valeurs zéro. Les structs anonymes sont utiles pour les formes de données uniques. L'intégration de struct (sans nom de champ) promeut les champs et méthodes du struct intégré — l'alternative de Go à l'héritage.
// Define a struct
type Person struct {
Name string
Age int
Address string // field names are exported (capitalized)
}
// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person) // returns *Person, zero values
p3.Name = "Charlie"
// Anonymous struct (one-off)
config := struct {
Port int
Host string
}{
Port: 8080,
Host: "localhost",
}
// Struct embedding (composition)
type Employee struct {
Person // embedded (promotes fields)
Salary float64
}
emp := Employee{
Person: Person{Name: "Dave", Age: 40},
Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)Pointeurs
Les pointeurs Go (*T) détiennent des adresses mémoire. & prend l'adresse, * déréférence. Contrairement à C, Go n'a pas d'arithmétique de pointeur (plus sûr). Les pointeurs de struct permettent le raccourci (u.Name au lieu de (*u).Name). Les pointeurs nil causent des paniques lors du déréférencement. Go a le garbage collection — pas de free() manuel nécessaire.
// Pointer basics
x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 (dereference)
*p = 100 // modify x through pointer
fmt.Println(x) // 100
// new() allocates and returns pointer
p2 := new(int) // *int, value 0
*p2 = 42
// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob" // (*u).Name shorthand
fmt.Println(u.Name) // Bob
// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference
// No pointer arithmetic (unlike C)
// p++ is illegalOpérations de slice
Go n'a pas de filter/map/reduce intégrés — écrivez-les ou utilisez le package slices (Go 1.21+). Insert/remove nécessitent de décaler des éléments avec append+copy. Attention à l'aliasing de slice : s[:i] et s[i+1:] partagent le tableau sous-jacent. Pour les slices 2D, allouez chaque ligne séparément.
// Filter
func filter(nums []int, pred func(int) bool) []int {
result := []int{}
for _, n := range nums {
if pred(n) {
result = append(result, n)
}
}
return result
}
evens := filter([]int{1,2,3,4,5}, func(n int) bool {
return n%2 == 0
}) // [2, 4]
// Insert at index
func insert(s []int, i, v int) []int {
s = append(s, 0)
copy(s[i+1:], s[i:])
s[i] = v
return s
}
// Remove at index
func remove(s []int, i int) []int {
return append(s[:i], s[i+1:]...)
}
// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
matrix[i] = make([]int, 4)
}Tri & Recherche
sort.Ints/Strings/Float64s trient sur place. sort.Slice avec un comparateur gère les types personnalisés. sort.Search* fait une recherche binaire sur les slices triées. Go 1.21+ ajoute le package slices avec des fonctions de tri génériques. Tout le tri est sur place — faites une copie d'abord si vous avez besoin de l'ordre original.
import "sort"
// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)
// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]
// Sort with custom comparator
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age // by age ascending
})
// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4
// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp funcMéthodes & Interfaces
Définir des interfaces
Les interfaces définissent des signatures de méthode. Go utilise le typage structurel — un type implémente une interface automatiquement s'il a toutes les méthodes requises (pas de déclaration 'implements' explicite). Cela permet un découplage de conception : définissez les interfaces où vous les utilisez, pas où vous les implémentez.
// Interface: a set of method signatures
type Shape interface {
Area() float64
Perimeter() float64
}
// Implement implicitly (no 'implements' keyword)
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
}
// Use the interface
func describe(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
s.Area(), s.Perimeter())
}
c := Circle{Radius: 5}
describe(c) // works! Circle implements ShapeInterface vide & Assertions de type
interface{} (ou 'any' en Go 1.18+) détient toute valeur. L'assertion de type (v.(T)) extrait le type concret — panique si mauvais type, utilisez donc le motif comma-ok. Le switch de type (switch v.(type)) gère plusieurs types proprement. L'interface vide est utile pour les conteneurs génériques mais perd la sécurité de type — préférez les génériques.
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
fmt.Println(v)
}
printAny(42)
printAny("hello")
printAny([]int{1, 2, 3})
// Type assertion
var i any = "hello"
s := i.(string) // panics if not string
fmt.Println(s)
// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
fmt.Println("string:", s)
}
// Type switch
func describe(v any) {
switch x := v.(type) {
case int:
fmt.Printf("int: %d\n", x)
case string:
fmt.Printf("string: %s\n", x)
case []int:
fmt.Printf("int slice: %v\n", x)
default:
fmt.Printf("unknown type: %T\n", x)
}
}Composition d'interfaces
Les interfaces peuvent intégrer d'autres interfaces (composition). io.Reader et io.Writer sont les interfaces les plus importantes de Go — implémentées par les fichiers, connexions réseau, tampons, etc. Cela permet de puissantes abstractions : les fonctions prenant io.Reader fonctionnent avec toute source lisible. Les interfaces petites et focalisées (1-3 méthodes) sont idiomatiques.
// Compose interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter combines Reader and Writer
type ReadWriter interface {
Reader
Writer
}
// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn
func copyData(r io.Reader, w io.Writer) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if n > 0 {
if _, err := w.Write(buf[:n]); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}Récepteurs pointeur vs valeur
Les récepteurs de pointeur peuvent modifier le struct et éviter la copie de grandes valeurs. Les récepteurs de valeur sont sûrs (ne peuvent pas modifier) et permettent la méthode à la fois sur les valeurs et les pointeurs. Si une méthode a un récepteur de pointeur, toutes devraient (pour la cohérence). Les méthodes à récepteur de pointeur satisfont les interfaces pour T et *T ; les méthodes à récepteur de valeur uniquement pour T.
type Counter struct {
count int
}
// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
return c.count
}
// Pointer receiver: can modify the original
func (c *Counter) Increment() {
c.count++
}
c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2
// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring bothInterface Stringer
L'interface Stringer (String() string) contrôle comment un type apparaît dans Print/Printf — comme toString() en Java/JS. %v utilise String() ; %+v affiche les noms de champs ; %#v affiche la syntaxe Go. L'interface error fonctionne de la même manière : implémentez Error() string pour faire de tout type une erreur. Ce sont les interfaces intégrées les plus courantes de Go.
// Stringer interface (like toString() in other languages)
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
// Implement Stringer
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}
// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an errorIntégration de type & Composition
Go utilise la composition au lieu de l'héritage. L'intégration d'un struct promeut ses champs et méthodes vers le struct externe. Vous pouvez remplacer les méthodes promues en définissant une méthode du même nom sur le type externe. L'intégration d'une interface permet la décoration/délégation — le type externe satisfait l'interface et peut transférer les appels.
// Embed a struct (composition over inheritance)
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " makes a sound"
}
type Dog struct {
Animal // embedded — promotes Name and Speak()
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" (promoted)
fmt.Println(d.Speak()) // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works
// Override a promoted method
func (d Dog) Speak() string {
return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"
// Embed an interface
type Logger struct {
io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded WriterGestion des erreurs
Bases des erreurs
Go gère les erreurs comme des valeurs, pas des exceptions. Les fonctions renvoient (résultat, erreur) — vérifiez toujours err != nil immédiatement. errors.New() crée des erreurs simples ; fmt.Errorf() ajoute du formatage. Les erreurs sentinelles (var ErrX = errors.New()) permettent la comparaison avec ==. N'ignorez jamais les erreurs (utilisez _ uniquement lorsque c'est intentionnel).
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
log.Fatal(err) // or handle gracefully
}
fmt.Println(result)
// fmt.Errorf for formatted errors
func validate(age int) error {
if age < 0 {
return fmt.Errorf("invalid age: %d (must be positive)", age)
}
return nil
}
// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
// handle not found
}Types d'erreur personnalisés
Les types d'erreur personnalisés implémentent l'interface error (Error() string). Ils transportent des données structurées (champs, codes, contexte) au-delà d'un simple message. Utilisez l'assertion de type (*ValidationError) pour accéder aux données. C'est essentiel pour la gestion d'erreurs spécifique au domaine — ex. codes de statut HTTP, détails de validation, logique de réessai.
// Custom error type (implements error interface)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}
// Use the custom error
func validateUser(name string) error {
if name == "" {
return &ValidationError{
Field: "name",
Message: "cannot be empty",
}
}
return nil
}
// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
fmt.Println("Field:", ve.Field) // "name"
fmt.Println("Message:", ve.Message) // "cannot be empty"
}errors.Is & errors.As (Go 1.13+)
Go 1.13+ a ajouté l'enveloppement d'erreur avec %w (fmt.Errorf). errors.Is() vérifie si une erreur correspond à une sentinelle (en déballant la chaîne). errors.As() extrait un type d'erreur spécifique de la chaîne. Cela permet une gestion d'erreur en couches : erreurs de bas niveau enveloppées avec du contexte, vérifiées à haut niveau. Enveloppez toujours avec %w (pas %v) pour préserver la chaîne.
import "errors"
// Wrapped errors with %w
var ErrNotFound = errors.New("not found")
func getUser(id int) error {
if id == 0 {
return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
}
return nil
}
// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("user not found")
}
// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field)
}
// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)Panic & Recover
panic() est pour les erreurs non récupérables (bugs, violations d'invariant) — pas pour la gestion d'erreurs normale. recover() capture les paniques mais ne fonctionne que dans les fonctions différées. Utilisez panic/recover pour : les erreurs de programmation (index hors plage), les échecs d'initialisation de package et la protection des goroutines contre le plantage du programme. Préférez renvoyer des erreurs pour les échecs attendus.
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("invalid number: %s", s))
}
return n
}
// Recover: catch a panic (only in deferred functions)
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("something went wrong")
}
// Practical: safe goroutine
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("goroutine panic:", r)
}
}()
fn()
}()
}Patrons d'enveloppement d'erreur
Enveloppez les erreurs avec du contexte à chaque couche en utilisant fmt.Errorf avec %w. Cela crée une chaîne d'erreur : le niveau supérieur voit le chemin complet (getUserProfile → fetchUser → erreur sql). Le contexte (nom de fonction, paramètres) aide au débogage. Utilisez errors.Is/As pour vérifier les erreurs spécifiques dans la chaîne. Évitez d'envelopper la même erreur plusieurs fois avec le même contexte.
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ... WHERE id = ?", id)
var u User
if err := row.Scan(&u.Name, &u.Age); err != nil {
return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
}
return &u, nil
}
func getUserProfile(id int) (*Profile, error) {
user, err := fetchUser(id)
if err != nil {
return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
}
// ...
}
// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
log.Printf("error: %v", err)
// Output: getUserProfile(42): fetchUser(42): sql: no rows
}
// golang.org/x/xerrors or pkg/errors for stack tracesDefer, Panic, Recover ensemble
Combinez defer (nettoyage), panic (erreurs fatales) et recover (capturer les paniques) pour une gestion robuste des ressources. Les defers s'exécutent dans l'ordre LIFO, même pendant les paniques. La valeur de retour nommée (err error) peut être définie dans un recover différé. Ce patron garantit que les fichiers/connexions sont fermés et les transactions annulées, même si le code panique.
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
file, e := os.Open(path)
if e != nil {
return e
}
defer func() {
// Close file regardless of panic
file.Close()
// Recover from panic and convert to error
if r := recover(); r != nil {
err = fmt.Errorf("panic in processFile: %v", r)
}
}()
// Do work that might panic
data := parseFile(file) // might panic
return saveData(data)
}
// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
return fn(tx)
}Concurrence
Goroutines
Les goroutines sont les threads légers de Go — démarrez avec 'go'. Elles sont bon marché (~2KB de pile) et gérées par l'ordonnanceur d'exécution de Go (ordonnancement M:N). La fonction main n'attend pas les goroutines — utilisez sync.WaitGroup ou des canaux pour la synchronisation. N'utilisez jamais time.Sleep pour la synchronisation en production (utilisez WaitGroup).
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 3; i++ {
fmt.Println(name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// Launch goroutine with 'go' keyword
go sayHello("Alice")
go sayHello("Bob")
// Anonymous goroutine
go func() {
fmt.Println("anonymous goroutine")
}()
// Wait for goroutines (simple approach)
time.Sleep(1 * time.Second)
fmt.Println("done")
}
// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrentlyCanaux
Les canaux sont des conduits typés pour la communication entre goroutines. Non tamponnés (make(chan T)) bloquent jusqu'à ce que l'expéditeur et le récepteur soient prêts (synchrone). Tamponnés (make(chan T, n)) bloquent uniquement lorsque pleins (asynchrone). L'expéditeur devrait fermer les canaux, jamais le récepteur. Range sur un canal jusqu'à ce qu'il soit fermé. Les canaux permettent 'partager la mémoire en communiquant'.
// Unbuffered channel (synchronous)
ch := make(chan string)
// Send and receive
go func() {
ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)
// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1
// Close a channel (sender closes, never receiver)
close(buf)
// Range over channel (until closed)
for v := range buf {
fmt.Println(v)
}
// Check if closed
v, ok := <-buf
if !ok {
fmt.Println("channel closed")
}Instruction Select
select permet à une goroutine d'attendre sur plusieurs opérations de canal — il choisit la première prête (au hasard si plusieurs). Le cas default le rend non bloquant. time.After() crée des canaux de délai. Select est le cœur du Go concurrent : boucles d'événements, fan-in/fan-out, délais. Incluez toujours des délais pour éviter les blocages.
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
// Wait for first to arrive
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received:", msg1)
case msg2 := <-ch2:
fmt.Println("received:", msg2)
}
}
// Timeout with select
select {
case result := <-slowOperation():
fmt.Println(result)
case <-time.After(3 * time.Second):
fmt.Println("timeout!")
}
// Non-blocking receive (default case)
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("no message") // runs if no data
}sync.WaitGroup
sync.WaitGroup attend qu'un groupe de goroutines se termine. Add(n) incrémente le compteur, Done() le décrémente (utilisez defer), Wait() bloque jusqu'à zéro. Passez toujours les variables de boucle en paramètres aux goroutines pour éviter les bugs de capture de fermeture (corrigé dans Go 1.22 mais toujours recommandé). WaitGroup est plus simple que les canaux pour la concurrence fire-and-forget.
import "sync"
func main() {
var wg sync.WaitGroup
// Launch 5 goroutines
for i := 0; i < 5; i++ {
wg.Add(1) // increment counter
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
fmt.Printf("Worker %d done\n", id)
}(i)
}
wg.Wait() // block until all Done() called
fmt.Println("all workers finished")
}
// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)Mutex & Sync
sync.Mutex protège l'état partagé de l'accès concurrent — Lock/Unlock avec defer. RWMutex permet plusieurs lecteurs ou un écrivain (mieux pour les charges de travail à forte lecture). sync.Once garantit que l'initialisation se produit exactement une fois (patron singleton). Préférez les canaux pour la communication, les mutex pour protéger l'état partagé. 'Partagez la mémoire en communiquant ; ne communiquez pas en partageant la mémoire.'
import "sync"
// Mutex: mutual exclusion lock
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: multiple readers, one writer
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple allowed)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}
// sync.Once: run exactly once
var (
once sync.Once
instance *Database
)
func GetDB() *Database {
once.Do(func() {
instance = &Database{}
})
return instance
}Patrons de concurrence
Pool de workers : un nombre fixe de goroutines traitent des tâches depuis un canal, envoyant les résultats vers un autre. Cela limite la concurrence et empêche l'épuisement des ressources. Fan-out/fan-in : distribue le travail entre les goroutines, puis fusionne les résultats. L'idée clé : fermez les canaux lorsqu'aucune donnée supplémentaire ne sera envoyée, pour que les boucles range se terminent. Ces patrons sont la fondation du Go concurrent.
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
time.Sleep(time.Second) // simulate work
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
}
// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
output := make(chan int)
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for v := range input {
output <- v * 2
}
}()
}
go func() { wg.Wait(); close(output) }()
return output
}E/S de fichiers & OS
Lire des fichiers
os.ReadFile() lit un fichier entier en mémoire (simple mais pas pour les gros fichiers). Pour les gros fichiers, ouvrez avec os.Open(), lisez par morceaux, et defer toujours Close(). bufio.Scanner lit ligne par ligne — idéal pour le traitement de texte. Vérifiez toujours les erreurs, surtout io.EOF pour savoir quand la lecture est terminée.
import (
"io"
"os"
)
// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Read in chunks
buf := make([]byte, 1024)
for {
n, err := file.Read(buf)
if n > 0 {
// process buf[:n]
}
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
}
// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}Écrire des fichiers
os.WriteFile() crée/tronque et écrit atomiquement (simple). os.OpenFile() avec des indicateurs donne le contrôle : O_APPEND (ajouter à la fin), O_CREATE (créer si manquant), O_TRUNC (tronquer). Utilisez bufio.Writer pour de nombreuses petites écritures (tamponne en mémoire, flush à la fin). Mode de fichier 0644 : le propriétaire peut lire/écrire, les autres peuvent lire.
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
log.Fatal(err)
}
// Open for writing (with options)
file, err := os.OpenFile("log.txt",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Write
if _, err := file.WriteString("log entry\n"); err != nil {
log.Fatal(err)
}
// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!
// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNCOpérations de fichier
os.Mkdir/MkdirAll créent des répertoires. os.ReadDir liste les entrées efficacement. filepath.WalkDir parcourt les répertoires récursivement. os.Stat donne des infos de fichier (taille, heure de modification, permissions). os.IsNotExist vérifie si un fichier manque. os.Remove supprime un fichier ; RemoveAll supprime les répertoires récursivement. Vérifiez toujours les erreurs.
// Create directory
os.Mkdir("mydir", 0755) // one level
os.MkdirAll("a/b/c", 0755) // nested directories
// List directory
entries, err := os.ReadDir(".")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println(entry.Name(), entry.IsDir())
}
// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Println(path)
return nil
})
// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size()) // bytes
fmt.Println(info.ModTime()) // last modified
fmt.Println(info.IsDir())
// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
fmt.Println("file does not exist")
}
// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursiveEnvironnement & Ligne de commande
os.Getenv/LookupEnv/Setenv gèrent les variables d'environnement. os.Args donne les arguments bruts de ligne de commande. Le package flag fournit des indicateurs analysés avec des valeurs par défaut et du texte d'aide. LookupEnv distingue entre les valeurs non définies et vides. Les variables d'environnement sont la méthode standard pour configurer les apps 12-factor (clés API, URLs de base de données).
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set
// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
fmt.Println("HOME:", val)
}
// All environment variables
for _, e := range os.Environ() {
fmt.Println(e)
}
// Command line arguments
// args := os.Args // []string, args[0] is program name
// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)
// Run: go run main.go -port 3000 -host 0.0.0.0Exécuter des commandes
os/exec exécute des commandes externes. Command() crée la commande ; Output() capture stdout ; Run() exécute avec Stdout/Stderr personnalisés. Utilisez CommandContext pour les délais (tue le processus). Vérifiez toujours les erreurs — exec.ExitError indique les codes de sortie non nuls. Attention à l'entrée utilisateur pour empêcher l'injection de commande.
import "os/exec"
// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(output))
// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())
// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"
// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5sJSON & Encodage
Marshal & Unmarshal JSON
encoding/json convertit entre structs Go et JSON. Marshal (struct → JSON), Unmarshal (JSON → struct). Les tags de struct (json:"name") contrôlent le nommage et la visibilité des champs. omitempty saute les valeurs zéro/vides. json:"-" exclut un champ entièrement. C'est la méthode standard pour gérer les requêtes/réponses d'API en Go.
import "encoding/json"
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omit if empty
Password string `json:"-"` // never serialize
Age int `json:"age,omitempty"`
}
// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}
// Pretty print
pretty, _ := json.MarshalIndent(user, "", " ")
// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"
// Struct tags control field names:
// json:"name" -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-" -> skip entirely
// json:",omitempty" -> keep name, skip if emptyJSON avec maps & slices
Pour le JSON dynamique (structure inconnue), unmarshal dans map[string]any (ou interface{}). Les nombres JSON deviennent float64 — assert de type pour accéder. json.Decoder/Encoder travaillent avec des flux (fichiers, corps HTTP) efficacement. Utilisez l'unmarshaling de struct lorsque vous connaissez le schéma ; utilisez les maps pour les données flexibles/dynamiques.
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`
var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)
// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
fmt.Println(s.(float64)) // JSON numbers are float64
}
// Encode a map
m := map[string]any{
"count": 42,
"items": []string{"a", "b", "c"},
"meta": map[string]any{"page": 1},
}
data, _ := json.Marshal(m)
// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)
// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(user)Marshaling JSON personnalisé
Implémentez MarshalJSON/UnmarshalJSON pour la sérialisation personnalisée. C'est utile pour : les champs calculés, les formats alternatifs (Money comme '99.99 USD'), la gestion des données sensibles et le formatage du temps. Le récepteur de méthode pour UnmarshalJSON doit être un pointeur pour modifier le struct. time.Time sérialise automatiquement en chaînes RFC 3339.
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
Amount float64
Currency string
}
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"amount": m.Amount,
"currency": m.Currency,
"display": fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
})
}
func (m *Money) UnmarshalJSON(data []byte) error {
var v struct {
Amount float64
Currency string
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
m.Amount = v.Amount
m.Currency = v.Currency
return nil
}
money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}
// Time formatting
type Event struct {
Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by defaultServeur HTTP
Le package net/http construit des serveurs HTTP. http.HandleFunc enregistre les gestionnaires. http.ResponseWriter écrit la réponse ; *http.Request lit la requête. Pour les APIs JSON, définissez Content-Type et utilisez json.NewEncoder(w).Encode(data). La bibliothèque standard est prête pour la production — pas de framework nécessaire pour les APIs simples. Utilisez des routeurs mux (gorilla/mux, chi) pour le routage complexe.
import (
"encoding/json"
"net/http"
)
type Response struct {
Status string `json:"status"`
Message string `json:"message"`
}
func main() {
// Simple handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
// JSON API endpoint
http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
})
// Start server
fmt.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Request methods:
// r.Method -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body -> request body (io.ReadCloser)Client HTTP
http.Get/Post sont des raccourcis pratiques. Pour les en-têtes, méthodes ou corps personnalisés, utilisez http.NewRequest + client.Do(). defer toujours resp.Body.Close() pour éviter les fuites de connexion. Définissez un délai sur le client (par défaut, aucun délai — dangereux). En production, réutilisez http.Client (il gère le pool de connexions) et utilisez le contexte pour l'annulation.
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))
// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewBuffer(body),
)
defer resp.Body.Close()
// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")Tests & Benchmarking
Tests unitaires
Les fichiers de test se terminent par _test.go, les fonctions de test commencent par Test. Les tests basés sur des tables sont le patron idiomatique : définissez les cas de test dans un slice, bouclez avec t.Run pour les sous-tests (nommés, exécutables individuellement). Utilisez t.Errorf pour les échecs (continue), t.Fatalf pour les échecs fatals (arrête). Exécutez avec 'go test -v' pour une sortie détaillée.
// math_test.go (file must end with _test.go)
package math
import "testing"
// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"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)
}
})
}
}
// Run: go test -v
// Run specific: go test -run TestAddTable/positiveBenchmarks
Les fonctions de benchmark commencent par Benchmark et utilisent *testing.B. La boucle b.N est ajustée par l'exécution pour obtenir des mesures stables. Exécutez avec 'go test -bench=.'. b.ResetTimer() exclut le temps de configuration. b.ReportAllocs() affiche les allocations mémoire. Comparez les implémentations avec 'benchstat' pour vérifier que les améliorations sont significatives.
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < 100; j++ {
s += "a"
}
}
}
// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8 300 4234567 ns/op
// BenchmarkStringConcat-8 200 6789012 ns/op 100 B/op 1 allocs/op
// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
data := setup() // setup not measured
b.ResetTimer() // start timing here
for i := 0; i < b.N; i++ {
process(data)
}
}
// Memory allocations
func BenchmarkAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
make([]int, 100)
}
}Assistants de test & Mocking
t.Helper() améliore les messages d'erreur en sautant les fonctions d'assistance dans les traces de pile. Mock en implémentant des interfaces (l'approche de Go du mocking — pas de framework mock nécessaire). t.Cleanup() enregistre les fonctions de nettoyage (comme defer, mais pour la portée de test). Pour le mocking complexe, utilisez testify/assert et mockery ou gomock pour générer des mocks depuis des interfaces.
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// Interfaces for mocking
type DataStore interface {
Get(key string) (string, error)
}
type MockStore struct {
data map[string]string
}
func (m *MockStore) Get(key string) (string, error) {
if v, ok := m.data[key]; ok {
return v, nil
}
return "", errors.New("not found")
}
func TestService(t *testing.T) {
store := &MockStore{data: map[string]string{"key": "value"}}
svc := NewService(store)
result := svc.GetValue("key")
assertEqual(t, result, "value")
}
// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
t.Cleanup(func() {
// runs after test (LIFO order)
os.Remove("tempfile")
})
// test code
}Points forts de la bibliothèque standard
La bibliothèque standard de Go est complète. time gère les dates/heures (note : le format utilise l'heure de référence 2006-01-02). regexp pour la correspondance de motifs. context pour l'annulation/les délais à travers les goroutines. sync.Pool pour la réutilisation d'objets (réduit la pression du GC). Go 1.21+ ajoute les packages slices et maps avec des utilitaires génériques.
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")
// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)
// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork
// sync.Pool (reuse objects)
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)Goroutines approfondi
Démarrer des goroutines & WaitGroup
Les goroutines sont des threads légers gérés par l'exécution Go (pas des threads OS) — vous pouvez en lancer des centaines de milliers. sync.WaitGroup coordonne l'achèvement des goroutines : Add(1) avant de démarrer, Done() lorsque terminé (utilisez defer), et Wait() pour bloquer jusqu'à ce que le compteur atteigne zéro. Passez toujours un pointeur au WaitGroup pour que toutes les goroutines partagent le même compteur.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // signal completion when function returns
fmt.Printf("Worker %d started\n", id)
// ... do work ...
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // increment counter before starting goroutine
go worker(i, &wg)
}
wg.Wait() // block until all goroutines call Done()
fmt.Println("All workers finished")
}GOMAXPROCS & Ordonnancement
GOMAXPROCS contrôle combien de threads OS exécutent les goroutines simultanément — il est par défaut au nombre de cœurs CPU et est presque toujours optimal. Go utilise l'ordonnancement M:N : de nombreuses goroutines sur peu de threads OS. Les E/S bloquantes ou opérations de canal font que l'ordonnanceur exécute d'autres goroutines sur le même thread. runtime.Gosched() cède explicitement. Vous avez rarement besoin de régler GOMAXPROCS.
package main
import (
"fmt"
"runtime"
)
func main() {
// GOMAXPROCS: number of OS threads available to run goroutines
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores
// Set it manually (rarely needed — default is optimal)
runtime.GOMAXPROCS(2)
// Goroutines are multiplexed onto OS threads (M:N scheduling)
// - Blocking syscalls (I/O) don't block other goroutines
// - The scheduler steals work across threads
// - A goroutine blocked on channel/IO yields its thread
// Number of goroutines currently running
go func() { runtime.Gosched() }() // yield to other goroutines
fmt.Println("Goroutines:", runtime.NumGoroutine())
}Fuites de goroutines & Prévention
Une fuite de goroutine se produit lorsqu'une goroutine bloque pour toujours (ex. envoyer sur un canal non tamponné que personne ne lit) — elle n'est jamais collectée par le GC. Prévenez les fuites en : tamponnant les canaux, utilisant select avec un canal/contexte d'annulation, et fournissant toujours un chemin de sortie. Les goroutines fuites accumulent la mémoire et le CPU. Utilisez runtime.NumGoroutine() et pprof pour détecter les fuites en production.
package main
// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
ch := make(chan int)
go func() {
ch <- 42 // blocks forever if nobody reads from ch
}()
return ch
}
// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
ch := make(chan int, 1) // buffered — sender never blocks
go func() {
select {
case ch <- 42:
case <-ctx.Done(): // exit when context is cancelled
return
}
}()
return ch
}
// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle controlsync.Once (Initialisation unique)
sync.Once garantit qu'une fonction s'exécute exactement une fois à travers toutes les goroutines — la méthode standard pour implémenter des singletons thread-safe et l'initialisation paresseuse. C'est plus efficace que la vérification d'indicateur protégée par mutex. La méthode Do bloque les appelants concurrents jusqu'à ce que la première invocation se termine. sync.Once est aussi utilisé en interne dans de nombreux patrons de la bibliothèque standard comme sync.OnceValue (Go 1.21+).
package main
import (
"fmt"
"sync"
)
var (
instance *Database
once sync.Once
)
type Database struct{ name string }
func GetDB() *Database {
// sync.Once guarantees the function runs exactly once,
// even if called from many goroutines simultaneously
once.Do(func() {
instance = &Database{name: "production"}
fmt.Println("Database initialized")
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
GetDB() // "Database initialized" prints only once
}()
}
wg.Wait()
}Pool de workers avec goroutines
Le patron de pool de workers limite la concurrence à un nombre fixe de goroutines traitant des tâches depuis un canal. Cela empêche l'épuisement des ressources dû au lancement de goroutines non bornées. Les workers utilisent 'for job := range jobs' qui sort lorsque le canal est fermé. close(jobs) signale à tous les workers de s'arrêter. C'est le patron canonique de concurrence Go pour le parallélisme borné.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs { // exits when jobs channel is closed
results <- job * job // process and send result
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // signal workers to stop (range loop exits)
wg.Wait()
close(results)
for r := range results {
fmt.Println("Result:", r)
}
}Canaux & Select
Bases des canaux (Non tamponné & Tamponné)
Les canaux non tamponnés (make(chan T)) synchronisent l'expéditeur et le récepteur — l'envoi bloque jusqu'à ce qu'un récepteur soit prêt. Les canaux tamponnés (make(chan T, n)) permettent n envois sans récepteur, découplant le timing expéditeur/récepteur. Seul l'expéditeur devrait fermer un canal (pour signaler 'plus de valeurs'). Recevoir d'un canal fermé renvoie la valeur zéro avec ok=false. Les boucles range sortent lorsque le canal est fermé.
package main
import "fmt"
func main() {
// Unbuffered: send blocks until someone receives (synchronous)
ch := make(chan int)
go func() {
ch <- 42 // blocks until main reads
}()
fmt.Println(<-ch) // 42
// Buffered: send blocks only when buffer is full
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block — buffer full
fmt.Println(<-buf) // 1 (FIFO)
// Close a channel (sender closes, never the receiver)
close(buf)
// Check if closed (ok is false when closed and empty)
val, ok := <-buf
fmt.Println(val, ok) // 2 true
// Range over channel until closed
nums := make(chan int, 3)
nums <- 10; nums <- 20; nums <- 30
close(nums)
for n := range nums {
fmt.Println(n) // 10, 20, 30
}
}Instruction Select
select permet à une goroutine d'attendre sur plusieurs opérations de canal simultanément — il choisit le premier cas prêt (au hasard si plusieurs sont prêts). Le cas default rend select non bloquant. time.After crée un canal de délai. select est le cœur de la coordination de concurrence de Go : le multiplexage, les délais, l'annulation et les patrons fan-in/fan-out se construisent dessus.
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()
// select waits on multiple channel operations
// picks the first one that's ready (random if multiple ready)
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
// Non-blocking receive with default
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("no message ready") // runs immediately
}
// Timeout with time.After
select {
case msg := <-ch1:
fmt.Println(msg)
case <-time.After(500 * time.Millisecond):
fmt.Println("timed out")
}
}Patrons Fan-In & Fan-Out
Fan-out distribue le travail à travers plusieurs goroutines pour le parallélisme ; fan-in fusionne plusieurs canaux en un. Ensemble ils forment un pipeline parallèle : les workers fan-out traitent indépendamment, puis fan-in collecte les résultats. Le fan-in utilise un WaitGroup pour fermer le canal fusionné uniquement après que tous les canaux d'entrée sont épuisés. Ces patrons sont fondamentaux au traitement de données concurrent de Go.
package main
import (
"fmt"
"sync"
)
// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
outputs := make([]<-chan int, n)
for i := 0; i < n; i++ {
outputs[i] = process(input) // each worker reads from same input
}
return outputs
}
func process(input <-chan int) <-chan int {
output := make(chan int)
go func() {
defer close(output)
for v := range input {
output <- v * v // transform
}
}()
return output
}
// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
output := func(c <-chan int) {
defer wg.Done()
for v := range c {
merged <- v
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}Canaux directionnels (Émission/Réception uniquement)
Les types de canaux directionnels (<-chan T pour réception uniquement, chan<- T pour émission uniquement) appliquent l'utilisation du canal à la compilation. Un canal bidirectionnel se convertit implicitement en type directionnel lorsqu'il est passé à une fonction. Cela documente l'intention et empêche les bugs — une fonction productrice ne peut littéralement pas recevoir de son propre canal de sortie. Utilisez des types directionnels dans les signatures de fonction pour rendre les contrats clairs.
package main
import "fmt"
// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
for i := 0; i < 3; i++ {
out <- i
}
close(out)
}
func consumer(in <-chan int) { // can only receive
for v := range in {
fmt.Println("consumed:", v)
}
}
func main() {
ch := make(chan int)
// Directional restrictions are enforced at compile time
go producer(ch) // bidirectional chan converts to send-only
consumer(ch) // bidirectional chan converts to receive-only
// This prevents bugs: producer can't accidentally read,
// consumer can't accidentally send or close
}Canaux Ticker & Timer
time.Ticker se déclenche de façon répétée à intervalles — utilisez ticker.C comme canal dans select pour les tâches périodiques. time.Timer se déclenche une fois après une durée. Appelez toujours Stop() sur les tickers et timers pour libérer les ressources et éviter les fuites. time.After est un minuteur unique pratique qui renvoie un canal (mais ne peut pas être annulé, préférez donc NewTimer dans les boucles select pour éviter l'accumulation). Reset vous permet de reprogrammer un minuteur.
package main
import (
"fmt"
"time"
)
func main() {
// Ticker: fires repeatedly at intervals
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // always stop to release resources
go func() {
for t := range ticker.C { // ticker.C is a channel
fmt.Println("Tick at", t)
}
}()
// Timer: fires once after a duration
timer := time.NewTimer(2 * time.Second)
<-timer.C // blocks until timer fires
fmt.Println("Timer fired!")
// time.After: one-shot timer as a channel (no Stop needed)
select {
case <-time.After(1 * time.Second):
fmt.Println("1 second elapsed")
}
// Reset a timer (cancel and reschedule)
timer2 := time.NewTimer(5 * time.Second)
timer2.Reset(100 * time.Millisecond)
<-timer2.C
}Package Context
context.WithCancel & WithTimeout
context.Context transporte l'annulation, les délais et les valeurs à portée de requête à travers les frontières de goroutine. WithCancel renvoie un contexte et une fonction d'annulation ; WithTimeout annule automatiquement après une durée. Les goroutines vérifient ctx.Done() (un canal) dans un select pour savoir quand s'arrêter. Appelez TOUJOURS la fonction d'annulation (utilisez defer) pour libérer les ressources, même si le délai se déclenche — sinon le contexte fuit.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done(): // cancelled or timed out
fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("Worker %d working...\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
// WithCancel: manual cancellation
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, 1)
time.Sleep(2 * time.Second)
cancel() // stop the worker
time.Sleep(500 * time.Millisecond)
// WithTimeout: auto-cancel after duration
ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
defer cancel2() // always call cancel to release resources
go worker(ctx2, 2)
time.Sleep(2 * time.Second) // worker stops after 1.5s
}Propager le contexte à travers les appels
Le contexte devrait être le premier paramètre de chaque fonction qui fait des E/S, et il devrait être propagé à travers toute la chaîne d'appels. http.Request.Context() est automatiquement annulé lorsque le client se déconnecte. Passer ctx aux opérations de base de données/HTTP (QueryRowContext, NewRequestWithContext) garantit qu'elles avortent lorsque le contexte est annulé — empêchant le travail gaspillé et les fuites de ressources. Ne stockez jamais de contextes dans des structs.
package main
import (
"context"
"database/sql"
"net/http"
)
// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
// Pass ctx to all blocking operations so they cancel together
var name string
err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
return name, err
}
func handler(w http.ResponseWriter, r *http.Request) {
// r.Context() is cancelled when the client disconnects
ctx := r.Context()
name, err := GetUser(ctx, db, 42)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Write([]byte(name))
}
// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)Valeurs de contexte (Données à portée de requête)
context.WithValue stocke des données à portée de requête (comme IDs utilisateur, IDs de trace, jetons d'auth) qui circulent à travers la chaîne d'appels. Utilisez un type de clé personnalisé (pas une chaîne) pour éviter les collisions de clés. Les valeurs devraient être des données dont la requête a besoin, pas des paramètres de fonction — l'équipe Go recommande de l'utiliser avec parcimonie, principalement pour les préoccupations transversales comme le tracing/auth. Assertez toujours le type lors de la récupération des valeurs.
package main
import (
"context"
"fmt"
"net/http"
)
// Define a custom key type to avoid collisions
type contextKey string
const userIDKey contextKey = "userID"
// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := 42 // authenticate...
// WithValue creates a new context with the value
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
if v, ok := ctx.Value(userIDKey).(int); ok {
return v, true
}
return 0, false
}
func handler(w http.ResponseWriter, r *http.Request) {
if uid, ok := getUserID(r.Context()); ok {
fmt.Fprintf(w, "User ID: %d", uid)
}
}Arrêt gracieux avec contexte
L'arrêt gracieux permet aux requêtes en cours de se terminer avant que le serveur ne sorte. signal.Notify capture les signaux OS (Ctrl+C, SIGTERM des orchestrateurs de conteneurs). server.Shutdown(ctx) arrête d'accepter de nouvelles connexions et attend que les actives se terminent (jusqu'au délai du contexte). C'est essentiel pour les serveurs de production — sans cela, les requêtes actives sont brutalement terminées, causant des erreurs et de la corruption de données.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080", Handler: mux}
// Listen for OS interrupt signals (Ctrl+C, SIGTERM)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
<-stop // block until signal received
log.Println("Shutting down...")
// Give in-flight requests 10 seconds to finish
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Forced shutdown: %v", err)
}
log.Println("Server stopped gracefully")
}Échéance de contexte & Gestion d'erreur
context.WithDeadline annule à une heure absolue ; WithTimeout annule après une durée relative (WithTimeout est juste WithDeadline(now+timeout)). ctx.Err() renvoie context.DeadlineExceeded ou context.Canceled pour que vous puissiez distinguer pourquoi il s'est arrêté. Utilisez errors.Is() pour vérifier les erreurs de contexte. Vérifiez toujours ctx.Err() au début des opérations longues et utilisez select avec ctx.Done() pendant les attentes bloquantes.
package main
import (
"context"
"errors"
"fmt"
"time"
)
func slowOperation(ctx context.Context) (string, error) {
// Check if already cancelled before starting
if err := ctx.Err(); err != nil {
return "", err
}
select {
case <-time.After(3 * time.Second): // simulate slow work
return "result", nil
case <-ctx.Done(): // cancelled or deadline exceeded
return "", ctx.Err()
}
}
func main() {
// Deadline: cancel at a specific time
deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
result, err := slowOperation(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Operation timed out")
} else if errors.Is(err, context.Canceled) {
fmt.Println("Operation was cancelled")
}
return
}
fmt.Println(result)
}Serveur HTTP & Client
Serveur HTTP (net/http)
net/http fournit un serveur HTTP prêt pour la production. http.HandleFunc enregistre les gestionnaires par chemin. Utilisez json.NewEncoder(w).Encode() pour écrire des réponses JSON et json.NewDecoder(r.Body).Decode() pour analyser les corps de requête. Définissez toujours les en-têtes Content-Type et vérifiez r.Method. http.Error envoie un statut d'erreur. ListenAndServe démarre le serveur ; enveloppez avec log.Fatal pour capturer les erreurs.
package main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUser(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
user.ID = 99
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/users/1", getUser)
http.HandleFunc("/users", createUser)
log.Println("Server on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Client HTTP & Requêtes
Le http.Client par défaut n'a AUCUN délai — définissez-en toujours un pour éviter de bloquer indéfiniment sur des serveurs non réactifs. Utilisez http.NewRequestWithContext pour attacher un contexte pour l'annulation/les délais. defer TOUJOURS resp.Body.Close() pour éviter les fuites de connexion. En production, réutilisez un seul http.Client (il gère le pool de connexions). http.Get est un raccourci mais manque de délai et de personnalisation.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
panic(err)
}
defer resp.Body.Close() // ALWAYS close the body
var users []map[string]any
json.NewDecoder(resp.Body).Decode(&users)
fmt.Println(users)
// Custom client with timeout (default has NO timeout!)
client := &http.Client{Timeout: 30 * time.Second}
// POST with JSON body and context
body, _ := json.Marshal(map[string]string{"name": "Bob"})
req, _ := http.NewRequestWithContext(context.Background(),
"POST", "https://api.example.com/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err = client.Do(req)
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}Patron Middleware
Le middleware enveloppe les gestionnaires pour ajouter des préoccupations transversales (journalisation, auth, CORS, limitation de débit) sans modifier le gestionnaire lui-même. La signature func(http.Handler) http.Handler est le type de middleware standard. Chain les applique dans l'ordre (le plus externe en premier). Ce patron est la base de frameworks comme Chi, Echo et Gin. Le ResponseWriter peut être enveloppé pour capturer les codes de statut pour la journalisation.
package main
import (
"log"
"net/http"
"time"
)
// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // call the wrapped handler
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello!"))
})
// Apply middlewares: Auth -> Logging -> handler
http.Handle("/", Chain(handler, Logging, Auth))
http.ListenAndServe(":8080", nil)
}Servir des fichiers statiques & Modèles
http.FileServer sert des fichiers statiques depuis un répertoire ; http.StripPrefix supprime le préfixe de chemin pour que les chemins de fichiers se résolvent correctement. html/template rend le HTML de manière sûre (auto-échappe pour empêcher XSS). template.Must panique sur les erreurs d'analyse (correct pour le démarrage). Les modèles utilisent {{.Field}} pour les données et {{range}} pour l'itération. En production, envisagez d'intégrer les fichiers avec go:embed au lieu de lire depuis le disque.
package main
import (
"html/template"
"net/http"
)
func main() {
// Serve static files (CSS, JS, images)
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// HTML templates
tmpl := template.Must(template.ParseFiles("templates/index.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := struct {
Title string
Items []string
}{
Title: "My Page",
Items: []string{"Apple", "Banana", "Cherry"},
}
tmpl.Execute(w, data)
})
http.ListenAndServe(":8080", nil)
}
// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>Arrêt gracieux & go:embed
go:embed regroupe des fichiers dans le binaire compilé au moment du build — permettant des déploiements à binaire unique véritable sans dépendances de fichiers externes. Utilisez les directives //go:embed au-dessus d'une déclaration var. embed.FS est un système de fichiers virtuel en lecture seule. fs.Sub crée un sous-système de fichiers (supprime le préfixe de répertoire). Cela fonctionne pour les assets statiques, modèles HTML, migrations SQL, fichiers de config, et plus.
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// Embed static files into the binary (no external files needed)
// Access the embedded filesystem
sub, _ := fs.Sub(staticFiles, "static")
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
// go:embed benefits:
// - Single binary deployment (no external files)
// - Files are compiled into the executable
// - Works with templates, configs, migrations, etc.
// Embed a single file
//go:embed config.json
// var configData []byte
// Embed with pattern
//go:embed templates/*.html
// var templates embed.FS
http.ListenAndServe(":8080", nil)
}Package Sync
sync.Mutex & sync.RWMutex
sync.Mutex fournit un verrouillage exclusif — une seule goroutine peut le détenir à la fois. sync.RWMutex permet plusieurs lecteurs simultanés OU un écrivain exclusif — utilisez-le lorsque les lectures surpassent largement les écritures. Associez toujours Lock avec defer Unlock pour empêcher les blocages si la fonction panique. Intégrez le mutex dans le struct (mu minuscule) pour le garder privé. Ne copiez jamais un mutex (utilisez toujours des pointeurs).
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock() // always unlock with defer
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock() // read lock (multiple readers OK)
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *SafeCache) Set(key, val string) {
c.mu.Lock() // write lock (exclusive)
defer c.mu.Unlock()
c.data[key] = val
}sync.Map (Map concurrent)
sync.Map est une map sécurisée pour la concurrence optimisée pour des scénarios spécifiques : écriture-une-fois-lecture-multiple (caches) ou accès à clés disjointes à travers les goroutines. Elle évite les verrous sur les lectures via des opérations atomiques. Cependant, pour les maps concurrentes à usage général, une map régulière protégée par sync.RWMutex est souvent plus rapide et plus ergonomique. L'API de sync.Map utilise any (interface{}) pour les clés et valeurs, perdant la sécurité de type.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Store and Load (thread-safe, no locks needed)
m.Store("name", "Alice")
m.Store("age", 30)
val, ok := m.Load("name")
fmt.Println(val, ok) // Alice true
// LoadOrStore: atomic get-or-set
actual, loaded := m.LoadOrStore("name", "Bob")
fmt.Println(actual, loaded) // Alice true (already existed)
// Delete
m.Delete("age")
// Range over all entries
m.Range(func(key, value any) bool {
fmt.Printf("%v = %v\n", key, value)
return true // continue; return false to stop
})
// Use sync.Map when:
// 1. Keys are written once, read many times (caches)
// 2. Multiple goroutines read/write disjoint keys
// For most cases, a regular map + Mutex is simpler and faster
}sync.Cond (Variables de condition)
sync.Cond implémente des variables de condition — les goroutines attendent qu'une condition devienne vraie. Wait() libère atomiquement le verrou et dort ; Signal() réveille un attendant, Broadcast() réveille tous. Utilisez toujours une boucle for autour de Wait() (pas if) pour gérer les réveils parasites. Cond est utile pour les files producteur-consommateur et l'attente de changements d'état, bien que les canaux fournissent souvent une alternative plus simple.
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
items []int
mu sync.Mutex
cond *sync.Cond
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
return q
}
func (q *Queue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // wake one waiting goroutine
}
func (q *Queue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
}
item := q.items[0]
q.items = q.items[1:]
return item
}
func main() {
q := NewQueue()
go func() {
time.Sleep(1 * time.Second)
q.Put(42)
}()
fmt.Println("Waiting for item...")
fmt.Println("Got:", q.Get()) // blocks until item available
}sync.Pool (Réutilisation d'objets)
sync.Pool réutilise des objets pour réduire les allocations et la pression du GC — idéal pour les objets à vie courte fréquemment alloués comme bytes.Buffer. Get() renvoie un objet du pool (appelant New si vide) ; Put() le renvoie. Les pools sont vidés pendant le GC, ne comptez donc pas sur eux pour la persistance. Réinitialisez toujours les objets avant réutilisation. La bibliothèque standard utilise sync.Pool largement (http, json, fmt).
package main
import (
"bytes"
"sync"
)
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer) // create when pool is empty
},
}
func Process(data []byte) string {
// Get a buffer from the pool (or create via New)
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf) // return it when done
buf.Reset() // clear before reuse
buf.Write(data)
buf.WriteString("-processed")
return buf.String()
}
// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictableerrgroup (Groupe avec propagation d'erreur)
errgroup (de golang.org/x/sync) est un WaitGroup qui renvoie la première erreur et peut annuler les goroutines restantes via le contexte. g.Go() démarre une goroutine ; g.Wait() bloque et renvoie la première erreur non nulle. WithContext crée un contexte qui est annulé lorsqu'une goroutine renvoie une erreur — pour que les autres goroutines s'arrêtent tôt. C'est la méthode idiomatique pour exécuter des opérations parallèles qui devraient toutes réussir ou échouer ensemble.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"net/http"
)
// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
g, ctx := errgroup.WithContext(context.Background())
results := make([]*http.Response, len(urls))
for i, url := range urls {
i, url := i, url // capture loop variables
g.Go(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // cancels ctx, stopping other goroutines
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // first error returned
}
return results, nil
}
func main() {
urls := []string{
"https://example.com",
"https://golang.org",
}
resps, err := fetchAll(urls)
fmt.Println(len(resps), err)
}Génériques (Go 1.18+)
Fonctions génériques & Paramètres de type
Les génériques Go (1.18+) utilisent des paramètres de type entre crochets : [T any] déclare T comme tout type. Le compilateur infère les types depuis les arguments, donc vous les spécifiez rarement explicitement. Les génériques permettent des fonctions réutilisables et de type sûr comme Map/Filter/Reduce sans interface{} et assertions de type. 'any' est un alias pour interface{} introduit avec les génériques.
package main
import "fmt"
// Type parameters in brackets: [T any] means T can be any type
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
squared := Map(nums, func(n int) int { return n * n })
fmt.Println(squared) // [1 4 9 16 25]
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
// Works with any type
words := []string{"go", "rust", "python"}
lengths := Map(words, func(s string) int { return len(s) })
fmt.Println(lengths) // [2 4 6]
}Contraintes de type & comparable
Les contraintes de type limitent les types qu'un générique accepte. 'comparable' est une contrainte intégrée pour les types prenant en charge == (nécessaire pour les clés de map et les comparaisons). Les contraintes personnalisées utilisent des unions de type (int | float64) pour autoriser des types spécifiques. Le package golang.org/x/exp/constraints fournit Ordered (pour <, >, etc.). Les contraintes peuvent combiner des ensembles de type et des méthodes.
package main
import "fmt"
// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Custom constraint with a type set
type Number interface {
int | int64 | float64 | float32
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
return struct{ First T; Second U }{first, second}
}
func main() {
fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
fmt.Println(Contains([]int{1, 2, 3}, 5)) // false
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4
p := Pair("Alice", 30)
fmt.Println(p) // {Alice 30}
}Structures de données génériques
Les types génériques (Stack[T any]) créent des structures de données de type sûr qui fonctionnent avec tout type sans boxing interface{} ni assertions de type. Le paramètre de type T fait partie du type, donc Stack[int] et Stack[string] sont des types distincts, vérifiés à la compilation. Cela élimine toute une classe d'erreurs de type à l'exécution. Les structs, méthodes et interfaces génériques prennent tous en charge les paramètres de type.
package main
import "fmt"
// Generic Stack — works with any type, type-safe
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
// Type-safe stack of ints
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop()
fmt.Println(val) // 2
// Type-safe stack of strings
strStack := &Stack[string]{}
strStack.Push("hello")
s, _ := strStack.Pop()
fmt.Println(s) // hello
// intStack.Push("oops") // compile error! type-safe
}Contraintes génériques avec ~ (Type sous-jacent)
Le préfixe ~ dans une contrainte correspond à tout type dont le type sous-jacent est le type nommé — donc ~string correspond à la fois à string et au type MyString string. Sans ~, une contrainte ne correspond qu'au type nommé exact, ce qui est rarement utile pour les types personnalisés. Utilisez ~ lorsque vous voulez que les génériques fonctionnent avec les alias de type et les types nommés dérivés de primitives (courant dans la modélisation de domaine).
package main
import "fmt"
// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
~string
}
func Join[S StringLike](items []S, sep string) string {
result := ""
for i, item := range items {
if i > 0 {
result += sep
}
result += string(item) // convert underlying type to string
}
return result
}
// Numeric constraint with ~
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func Double[T Integer](v T) T {
return v * 2
}
// Custom type based on int
type Score int
func main() {
type MyString string
items := []MyString{"a", "b", "c"}
fmt.Println(Join(items, "-")) // a-b-c
s := Double(Score(21))
fmt.Println(s) // 42
}Génériques vs Interfaces (Quand utiliser)
Choisissez les génériques lorsque l'algorithme est identique à travers les types (collections, maths, transformations) — ils fournissent une sécurité de type à la compilation avec zéro surcoût à l'exécution. Choisissez les interfaces lorsque différents types nécessitent différentes implémentations (polymorphisme) ou lorsque vous avez besoin de distribution à l'exécution (injection de dépendances, mocking). Ils sont complémentaires : les génériques peuvent être contraints par des interfaces (T Stringer) pour le meilleur des deux mondes.
package main
import "fmt"
// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
String() string
}
type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }
type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }
func Print[T Stringer](v T) {
fmt.Println(v.String())
}
func main() {
fmt.Println(Max(3, 7)) // 7
fmt.Println(Max(3.14, 2.71)) // 3.14
Print(Dog{"Rex"}) // Dog: Rex
Print(Cat{"Whiskers"}) // Cat: Whiskers
}
// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> GenericsRéflexion
reflect.Type & reflect.Value
reflect.TypeOf renvoie le type à l'exécution ; reflect.ValueOf renvoie la valeur à l'exécution. Depuis un Type, vous pouvez inspecter les champs de struct, méthodes et tags. Depuis une Value, vous pouvez lire et (avec CanSet) modifier les champs. La réflexion est puissante mais lente (10-100x plus lente que l'accès direct) et contourne la sécurité de type à la compilation — utilisez-la avec parcimonie, principalement pour la sérialisation, les ORMs et les frameworks.
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Alice", Age: 30}
// Get type and value at runtime
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
fmt.Println("Type:", t.Name()) // User
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("NumFields:", t.NumField())
// Iterate struct fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
tag := field.Tag.Get("json")
fmt.Printf("%s (%s) = %v [json:%s]\n",
field.Name, field.Type, value, tag)
}
// Name (string) = Alice [json:name]
// Age (int) = 30 [json:age]
}Modifier des valeurs avec réflexion
Pour modifier une valeur via réflexion, vous devez passer un pointeur et appeler .Elem() pour la déréférencer. CanSet() indique si un champ est affectable (champs exportés d'une valeur adressable). Les champs non exportés (minuscules) peuvent être lus mais pas définis. SetInt/SetString/Set sont vérifiés au type à l'exécution et paniquent en cas de non-correspondance. La mutation basée sur la réflexion est la base des analyseurs de config et des ORMs.
package main
import (
"fmt"
"reflect"
)
func main() {
type Config struct {
Port int
Host string
}
cfg := Config{Port: 8080, Host: "localhost"}
// MUST pass a pointer to modify (ValueOf of a struct is a copy)
v := reflect.ValueOf(&cfg).Elem()
// Check if a field is settable
portField := v.FieldByName("Port")
fmt.Println("Settable:", portField.CanSet()) // true
// Modify fields by name
portField.SetInt(9090)
v.FieldByName("Host").SetString("0.0.0.0")
fmt.Println(cfg) // {9090 0.0.0.0}
// Set via interface (type-checked at runtime)
v.FieldByName("Port").Set(reflect.ValueOf(7070))
fmt.Println(cfg) // {7070 0.0.0.0}
// Unexported fields are NOT settable (CanSet returns false)
}Appeler des méthodes dynamiquement
La réflexion peut appeler des méthodes dynamiquement par nom — utile pour les frameworks RPC, les systèmes de plugins et le routage. MethodByName renvoie une Value ; Call() l'invoque avec un slice d'arguments reflect.Value et renvoie un slice de résultats. NumMethod/Method énumèrent les méthodes. Les appels de méthode via réflexion sont lents et contournent la sécurité de type, utilisez-les uniquement lorsque le nom de la méthode n'est pas connu à la compilation.
package main
import (
"fmt"
"reflect"
)
type Calculator struct{}
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }
func main() {
calc := Calculator{}
v := reflect.ValueOf(calc)
// Find a method by name
method := v.MethodByName("Add")
fmt.Println("Found:", method.IsValid()) // true
// Call with arguments (each arg must be a reflect.Value)
args := []reflect.Value{
reflect.ValueOf(3),
reflect.ValueOf(4),
}
results := method.Call(args)
fmt.Println("3 + 4 =", results[0].Int()) // 7
// List all methods
t := reflect.TypeOf(calc)
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
}
// Method: Add, Type: func(main.Calculator, int, int) int
// Method: Mul, Type: func(main.Calculator, int, int) int
}Cas d'usage pratique : Struct vers Map (basé sur les tags)
Ce patron — itérer les champs de struct et lire les tags — est comment encoding/json, les analyseurs YAML, les ORMs et les bibliothèques de validation fonctionnent sous le capot. reflect permet d'écrire du code générique qui traite tout struct basé sur ses tags. La convention de tag json:'-' (sauter ce champ) est standard. C'est l'un des usages les plus légitimes de la réflexion en Go.
package main
import (
"fmt"
"reflect"
)
// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
result := make(map[string]any)
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
// Dereference pointers
if v.Kind() == reflect.Ptr {
v = v.Elem()
t = t.Elem()
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Use the tag value if present, else the field name
key := field.Tag.Get(tag)
if key == "" {
key = field.Name
}
// Skip fields tagged with "-"
if key == "-" {
continue
}
result[key] = v.Field(i).Interface()
}
return result
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Pass string `json:"-"` // excluded
}
func main() {
u := User{Name: "Alice", Age: 30, Pass: "secret"}
m := StructToMap(u, "json")
fmt.Println(m) // map[age:30 name:Alice]
}Build & Outils
go build, go run & go install
go build compile en un exécutable ; go run compile vers un fichier temp et l'exécute (excellent pour le développement) ; go install place le binaire dans $GOPATH/bin pour un accès global. La fonctionnalité phare de Go est la compilation croisée facile via GOOS/GOARCH — pas de chaîne d'outils nécessaire. -ldflags='-s -w' supprime les infos de débogage pour des binaires ~30% plus petits. -X injecte des valeurs (comme des chaînes de version) au moment du build pour CI/CD.
# Build an executable (outputs to current directory)
go build -o myapp # custom output name
go build -o bin/myapp ./cmd # build a specific package
# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac # Apple Silicon
# Run without building an executable (compiles to temp, runs, deletes)
go run main.go # run single file
go run ./cmd/server # run a package
# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp # now 'myapp' is on your PATH
# Build flags
go build -v # verbose: print packages being compiled
go build -race # enable race detector (for testing)
go build -ldflags="-s -w" # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0" # inject version at build timego mod (Gestion de module)
go mod gère les dépendances via go.mod (liste de dépendances) et go.sum (sommes de contrôle pour la sécurité). go get ajoute/met à niveau les deps ; go mod tidy synchronise les fichiers (exécutez avant les commits). Go utilise le versionnement sémantique d'import : v2+ nécessite un suffixe de chemin /v2. Le cache de module est partagé à $GOPATH/pkg/mod. go mod vendor crée un répertoire vendor/ pour des builds reproductibles, hors ligne ou audités.
# Initialize a new module
go mod init github.com/user/myproject
# This creates go.mod:
# module github.com/user/myproject
# go 1.21
# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected] # specific version
# Tidy: add missing deps, remove unused ones
go mod tidy
# Download dependencies to local cache
go mod download
# Verify dependencies haven't been modified
go mod verify
# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor
# Upgrade dependencies
go get -u github.com/lib/pq # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected] # pin specific version
# View dependency graph
go mod graph | headgo test & Benchmarking
go test exécute les fichiers _test.go ; les fonctions nommées TestXxx(t *testing.T) sont des tests, BenchmarkXxx(b *testing.B) sont des benchmarks. -race active le détecteur de course de données (essentiel pour le code concurrent). -cover affiche la couverture de test ; -coverprofile génère un rapport détaillé. Les benchmarks exécutent b.N itérations où N est auto-ajusté. Les tests basés sur des tables (slices d'entrée/attendu) sont le style de test idiomatique de Go.
# Run all tests in the current package
go test ./...
# Verbose output
go test -v ./...
# Run a specific test
go test -run TestAdd -v
# Run benchmarks
go test -bench=. -benchmem
# Race detector (find data races)
go test -race ./...
# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # view in browser
# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
# got := Add(2, 3)
# if got != 5 {
# t.Errorf("Add(2,3) = %d, want 5", got)
# }
# }
#
# func BenchmarkAdd(b *testing.B) {
# for i := 0; i < b.N; i++ {
# Add(2, 3)
# }
# }go fmt, go vet & golangci-lint
go fmt/gofmt impose un seul format canonique — Go n'a pas de débats de formatage. go vet capture les bugs courants (inadéquations printf, copies de verrou, mauvais tags de struct). golangci-lint agrège des dizaines de linters et est la norme de l'industrie pour CI. Exécutez gofmt et go vet avant chaque commit ; ajoutez golangci-lint à CI pour une analyse plus approfondie. Des outils cohérents sont une raison majeure pour laquelle les codebases Go semblent uniformes.
# go fmt: format code (the ONE true Go style)
go fmt ./... # format all files
gofmt -d main.go # show diff without changing
gofmt -w main.go # write changes in place
# go vet: static analysis for common mistakes
go vet ./... # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables
# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run # run all enabled linters
golangci-lint run --enable=gosec,goconst # enable specific linters
# Common linters:
# errcheck - check unchecked errors
# gosec - security issues
# govet - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused - find unused code
# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.goProfilage & pprof
pprof est le profileur intégré de Go — profilage CPU, mémoire, goroutine et mutex. Pour les serveurs de longue durée, importez _ 'net/http/pprof' pour exposer un endpoint /debug/pprof/ pour le profilage en direct sans redémarrages. go tool pprof donne un shell interactif (top, list, web) ou une UI web. Le traceur d'exécution (go tool trace) visualise l'ordonnancement des goroutines et le blocage. Le profilage est essentiel pour le code Go critique en performance.
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)
# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof
# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30
# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...
# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out # opens in browserBase de données (SQL)
Bases de database/sql
database/sql est l'interface standard pour les bases de données SQL. Le pilote est importé pour ses effets secondaires (s'enregistrant lui-même). sql.Open n'établit pas de connexion ; utilisez Ping pour vérifier. defer toujours db.Close().
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()Interroger des lignes
Query renvoie plusieurs lignes. defer toujours rows.Close() pour libérer les ressources. Scan copie les valeurs de colonnes dans des variables. Vérifiez rows.Err() après la boucle pour les erreurs d'itération.
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
var id int; var name string
if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
fmt.Printf("%d: %s\n", id, name)
}Interroger une seule ligne
QueryRow renvoie une seule ligne. Scan renvoie sql.ErrNoRows si aucune ligne ne correspond. Plus propre que Query pour les recherches à ligne unique. Gérez toujours ErrNoRows explicitement.
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}Instructions préparées
Prepare crée une instruction réutilisable, améliorant les performances pour les requêtes répétées. Empêche l'injection SQL. defer toujours stmt.Close(). Utilisez pour les requêtes exécutées plusieurs fois.
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")Transactions
Begin démarre une transaction. Toutes les opérations dans tx sont atomiques. defer Rollback est sûr : c'est un no-op après Commit. Si une opération échoue, Rollback annule toutes les modifications.
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback() // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()Tests approfondis
Test de base
Les fonctions de test commencent par Test et prennent *testing.T. t.Errorf journalise l'échec et continue. t.Fatalf journalise et arrête. Exécutez avec go test. Utilisez testify/assert pour des assertions plus propres.
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d; want %d", got, want)
}
}Tests basés sur des tables
Les tests basés sur des tables sont idiomatiques en Go. Définissez les cas de test comme un slice de structs. Bouclez et exécutez chacun. t.Run crée des sous-tests pour les échecs individuels.
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
}
}
}Sous-tests
Les sous-tests utilisent t.Run avec un nom et une fonction. Exécutez des sous-tests spécifiques avec le motif -run. Fournit une meilleure organisation et sortie de test.
func TestAdd(t *testing.T) {
t.Run("positive", func(t *testing.T) {
if Add(1, 2) != 3 { t.Error("failed") }
})
t.Run("negative", func(t *testing.T) {
if Add(-1, -2) != -3 { t.Error("failed") }
})
}
// Run specific: go test -run TestAdd/negativeTest Main
TestMain s'exécute une fois pour le package, remplaçant le runner de test par défaut. Utilisez pour la configuration/démontage global. Doit appeler m.Run() pour exécuter les tests. os.Exit propage le code de sortie.
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }Mocking d'interfaces
Le mocking de Go repose sur les interfaces. Définissez une interface, implémentez un mock, et injectez-le. Des outils comme mockery et mockgen auto-génèrent des mocks. Permet les tests unitaires sans dépendances externes.
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
if v, ok := m.data[id]; ok { return v, nil }
return "", errors.New("not found")
}
// Use in tests by injecting MockStoreBenchmarking
Benchmark de base
Les fonctions de benchmark commencent par Benchmark et prennent *testing.B. b.N est ajusté par le framework pour obtenir des mesures stables. Exécutez avec go test -bench. La sortie affiche ns/op (nanosecondes par opération).
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8 1000000000 0.3 ns/opSous-benchmarks
Les sous-benchmarks utilisent b.Run. ResetTimer exclut le temps de configuration. ReportAllocs affiche les allocations mémoire. Comparez les performances à travers les tailles d'entrée.
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
data := generateData(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sort(data)
}
})
}
}Allocations mémoire
ReportAllocs affiche les allocations mémoire par opération. Réduire les allocations est clé pour la performance de Go. Utilisez sync.Pool, pré-allouez les slices et évitez la concaténation de chaînes inutile.
func BenchmarkConcat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
s := "a" + "b" + "c"
_ = s
}
}
// Output includes allocs/op and B/opBenchmarks parallèles
RunParallel exécute les benchmarks de manière concurrente à travers plusieurs goroutines. Utile pour tester le code thread-safe. pb.Next() itère la distribution de travail. Mesure le débit sous concurrence.
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(2, 3)
}
})
}Comparaison de benchmarks
benchstat compare les résultats de benchmark statistiquement. Exécutez les benchmarks plusieurs fois avec -count=10 pour une comparaison fiable. Aide à vérifier les améliorations de performance et détecter les régressions.
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name old time/op new time/op delta
# Add-8 2.3ns 1.8ns -21.7%Profilage (pprof)
Profilage CPU
StartCPUProfile écrit le profil CPU dans un fichier. StopCPUProfile vide les données. Analysez avec go tool pprof. Utilisez les commandes top, list, web dans pprof. Concentrez-vous sur les fonctions consommant le plus de CPU.
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.profProfilage mémoire
WriteHeapProfile capture l'état courant du tas. Appelez runtime.GC() d'abord pour des résultats précis. Définissez MemProfileRate = 1 pour profiler chaque allocation (plus lent mais précis).
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocationsProfilage HTTP
net/http/pprof enregistre les endpoints de profilage sur le mux par défaut. Accédez aux profils via HTTP sans redémarrer. Utile pour les diagnostics en production. Sécurisez l'endpoint en production.
import _ "net/http/pprof"
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profileCommandes pprof
top affiche les fonctions consommant le plus de ressources. list affiche le code source annoté. web ouvre un graphe d'appels SVG. tree affiche la hiérarchie d'appels. Utilisez focus pour filtrer.
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10 # Top functions by CPU
(pprof) list Add # Show source with timings
(pprof) web # Open graph in browser
(pprof) tree # Call tree
(pprof) png > out.png # Save graph as PNGTrace
runtime/trace capture les traces d'exécution : ordonnancement des goroutines, GC, blocage de syscall. go tool trace ouvre une UI web. Utile pour diagnostiquer les problèmes de latence et de concurrence.
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.outBases de CGO
CGO de base
CGO permet d'appeler C depuis Go. Le code C est dans les commentaires au-dessus de import "C". L'import doit être immédiatement après le commentaire. CGO ralentit les builds et empêche la compilation croisée.
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
func main() {
C.hello()
}Passer des chaînes
C.CString alloue une chaîne C (doit libérer avec C.free). CString copie les données. Utilisez unsafe.Pointer pour la conversion. Libérez toujours la mémoire allouée par C pour éviter les fuites.
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)Appeler des bibliothèques C
#cgo LDFLAGS lie les bibliothèques C. #cgo CFLAGS définit les indicateurs du compilateur. Incluez les en-têtes système avec #include. CGO fait le pont entre Go et les bibliothèques C existantes comme libm, libcrypto.
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result) // 4Rappel de C vers Go
Les fonctions Go exportées avec //export peuvent être appelées depuis C. La fonction doit être dans le package main. Permet aux bibliothèques C de rappeler Go. Utilisé dans les liaisons FFI.
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
fmt.Printf("Called with %d\n", n)
}Notes de performance
Les appels CGO ont un surcoût significatif par rapport aux appels de fonction Go. Évitez dans le code critique en performance. Groupez les opérations pour réduire les appels transfrontaliers. Définissez CGO_ENABLED=0 pour des builds Go purs.
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go buildFrameworks Web
Serveur net/http
net/http est le serveur HTTP de la bibliothèque standard. HandleFunc enregistre les gestionnaires. ListenAndServe démarre le serveur. Le mux par défaut convient pour les apps simples ; utilisez un mux personnalisé pour la production.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))Framework Gin
Gin est un framework HTTP haute performance. Param extrait les paramètres d'URL. JSON sérialise les réponses. Gin fournit le routage, le middleware et la validation JSON. Plus rapide que net/http grâce à httprouter.
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")Middleware
Le middleware enveloppe les gestionnaires pour ajouter des préoccupations transversales : journalisation, auth, CORS. c.Next() appelle le gestionnaire suivant. gin.Recovery() empêche les plantages dus aux paniques. L'ordre compte.
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
}
}
r := gin.New()
r.Use(Logger(), gin.Recovery())Framework Echo
Echo est un autre framework populaire, similaire à Gin. Les gestionnaires renvoient des erreurs pour une gestion centralisée des erreurs. Middleware intégré pour CORS, JWT, limitation de débit. Conception d'API propre.
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")Fichiers statiques
FileServer sert des fichiers statiques. StripPrefix ajuste le chemin URL. Utile pour servir HTML, CSS, JS et images. En production, utilisez un CDN ou nginx pour les assets statiques.
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")gRPC
Définir le Proto
Protocol Buffers définissent le contrat de service. proto3 est la dernière syntaxe. service définit les méthodes RPC. message définit les structures de données. Générez le code Go avec protoc.
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }Générer le code
protoc génère le code Go depuis les fichiers .proto. --go_out génère les types de message. --go-grpc_out génère les stubs de service. Le code généré n'est pas édité manuellement.
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfacesImplémentation du serveur
Intégrez UnimplementedGreeterServer pour la compatibilité ascendante. Implémentez les méthodes du service. grpc.NewServer crée le serveur. Enregistrez le service avant de servir.
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)Client
grpc.Dial établit une connexion. WithInsecure désactive TLS (utilisez credentials.NewTLS pour la production). Le stub client fournit des méthodes typées. Les connexions sont poolées et réutilisées.
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)Streaming
gRPC prend en charge trois patrons de streaming. Le mot-clé stream marque le streaming. Streaming serveur : une requête, plusieurs réponses. Bidirectionnel : les deux côtés streament. Utile pour les données en temps réel.
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);Enveloppement d'erreur
Envelopper des erreurs
Utilisez le verbe %w pour envelopper les erreurs, préservant l'originale. Cela crée une chaîne d'erreur. Évitez %v pour l'enveloppement car il perd la chaîne. L'enveloppement ajoute du contexte sans perdre la cause racine.
if err != nil {
return fmt.Errorf("failed to open config: %w", err)
}Déballer
errors.Is vérifie si une erreur dans la chaîne correspond. errors.As extrait un type d'erreur spécifique de la chaîne. Utilisez Is pour les valeurs sentinelles, As pour les erreurs typées. Les deux traversent la chaîne d'enveloppement.
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}Erreurs personnalisées
Les types d'erreur personnalisés implémentent l'interface error. Ils transportent des données structurées pour la gestion d'erreur. Utilisez errors.As pour extraire le type personnalisé. Préférez les erreurs typées aux comparaisons de chaînes.
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
return nil
}Erreurs sentinelles
Les erreurs sentinelles sont des variables d'erreur au niveau du package. Utilisez pour les conditions d'erreur attendues. Vérifiez avec errors.Is, jamais avec ==. Exportez-les pour que les utilisateurs puissent vérifier contre elles.
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
if id > 100 { return nil, ErrNotFound }
return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }Patrons de gestion d'erreur
Gérez les erreurs immédiatement lorsque possible. Pour les opérations différées comme Close, capturez l'erreur. Les valeurs de retour nommées permettent aux fonctions différées de modifier la valeur de retour. Vérifiez toujours les erreurs de Close.
// Immediate handling
if err := doSomething(); err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
f, e := os.Open("file.txt")
if e != nil { return e }
defer func() {
if cerr := f.Close(); err == nil { err = cerr }
}()
// ...
}Pièges courants
Fuites de goroutines
Les goroutines fuient lorsqu'elles bloquent pour toujours. Fournissez toujours un chemin de sortie : annulation de contexte, fermeture de canaux ou canaux tamponnés. Utilisez runtime.NumGoroutine() pour détecter les fuites.
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
go func() { ch <- 1 }() // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
go func() {
select {
case ch <- 1:
case <-ctx.Done():
}
}()
}Fermeture de canal
Seul l'expéditeur devrait fermer un canal, jamais le récepteur. La fermeture signale plus de valeurs. Recevoir d'un canal fermé renvoie la valeur zéro. Envoyer à un canal fermé panique.
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
defer close(ch) // Sender closes
for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }Capture de variable de boucle
Avant Go 1.22, les variables de boucle étaient partagées à travers les itérations. Les goroutines les capturant voient la valeur finale. Go 1.22+ corrige cela en créant une nouvelle variable par itération. Passez en paramètre pour les versions antérieures.
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }() // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
go func(i int) { fmt.Println(i) }(i)
}Concurrence de map
Les maps ne sont pas sûres pour un usage concurrent. Les lectures et écritures concurrentes causent une panique à l'exécution. Utilisez sync.Mutex pour un verrouillage explicite ou sync.Map pour un accès concurrent à forte lecture.
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()Interface nil
Un pointeur nil enveloppé dans une interface n'est pas nil. L'interface a un type même si la valeur est nil. Renvoyez toujours nil directement, pas un pointeur typé nil. Vérifiez avec reflect ou renvoyez nil explicitement.
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
var err *MyError = nil
return err // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
return nil
}Snippets Go associés
Copy-paste ready code for common tasks.
goroutine
Implémenter la concurrence avec des goroutines.
channel
Communiquer entre goroutines avec des channels.
select
Multiplexer des channels avec select.
mutex Mutex
Protéger les données partagées avec sync.Mutex.
defer Appel différé
Utilisation et ordre d'exécution de defer.
Gestion d'erreurs
Modèle de gestion d'erreurs de Go.
interface
Définir et implémenter des interfaces.
Incorporation de struct
Implémenter la composition par incorporation.
Génériques
Utilisation des génériques en Go 1.18+.
context
Contrôler le timeout et l'annulation avec context.
Opérations sur fichiers
Opérations de lecture et écriture de fichiers.
Serveur HTTP
Créer un serveur HTTP.
Client HTTP
Envoyer des requêtes HTTP.
Encodage/décodage JSON
Convertir entre structs et JSON.
Traitement de chaînes
Opérations courantes dans le package strings.
Opérations sur slice
Opérations courantes sur les slices.
Opérations sur map
Opérations CRUD sur map.
Gestion du temps
Opérations courantes dans le package time.
Expressions régulières
Utilisation du package regexp.
Tests
Écrire des tests unitaires.
Was this helpful?