DevToolBoxGRATUIT
Blog

Guide Go (Golang): Goroutines, Canaux, Génériques et APIs REST

14 min de lecturepar DevToolBox

Go (Golang) Guide: Complete Tutorial for Backend Development

Master Go programming from fundamentals to production-ready backend development. Learn goroutines, channels, REST APIs, testing, generics, and concurrency patterns with real code examples.

TL;DR — Go in 60 Seconds
  • Go is a compiled, statically typed language optimized for simplicity and performance
  • Goroutines and channels enable elegant, efficient concurrency
  • Built-in tooling: go build, go test, go fmt, go vet
  • Explicit error handling — no exceptions, just (value, error) returns
  • Interfaces are satisfied implicitly — duck typing without the overhead
  • Generics available since Go 1.18 for type-safe reusable code
  • Ideal for REST APIs, microservices, CLI tools, and systems programming

Introduction to Go

Go (often called Golang) was designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson and released publicly in 2009. It was created to solve real engineering problems: slow compilation, complex dependency management, and the difficulty of writing safe concurrent programs in C++ and Java.

Today, Go powers some of the most critical infrastructure on the internet — Docker, Kubernetes, Terraform, InfluxDB, CockroachDB, and countless microservices at Google, Cloudflare, Dropbox, and Uber. Its combination of performance close to C, developer ergonomics close to Python, and a world-class concurrency model makes it the language of choice for modern backend development.

Key Takeaways
  • Go compiles to native binaries — no runtime required for deployment
  • The language spec fits on a single webpage and changes rarely
  • Goroutines are ~1000x cheaper than OS threads
  • The standard library covers HTTP servers, JSON, crypto, SQL, and much more
  • Table-driven tests and the testing package make testing idiomatic
  • Go modules provide reproducible, hermetic builds

1. Go Fundamentals: Packages, Imports, and the Main Function

Every Go program is organized into packages. The main package is special — it defines a standalone executable. Packages are imported using their module path, and unused imports cause a compile error, keeping codebases clean.

package main

import (
	"fmt"
	"math"
	"strings"
)

func main() {
	// fmt is the format package — print, scan, sprintf
	fmt.Println("Hello, Go!")

	// math functions
	fmt.Printf("Pi: %.4f\n", math.Pi)
	fmt.Printf("Sqrt(16): %.0f\n", math.Sqrt(16))

	// strings package
	s := strings.ToUpper("golang")
	fmt.Println(s) // GOLANG

	// Multiple return values
	q, r := divide(17, 5)
	fmt.Printf("17 / 5 = %d remainder %d\n", q, r)
}

func divide(a, b int) (int, int) {
	return a / b, a % b
}

Package Naming Conventions

Package names should be short, lowercase, and describe what the package provides. Avoid generic names like util or common. The package name is the last element of the import path by convention — import "net/http" uses http.Get.

2. Variables and Types: var, :=, and Zero Values

Go is statically typed with strong type inference. There are two ways to declare variables: the verbose var form (useful at package scope or when the type must be explicit) and the short declaration operator := (most common inside functions).

package main

import "fmt"

// Package-level variables
var (
	appName = "myapp"
	version = "1.0.0"
	debug   bool // zero value: false
)

func main() {
	// Short declaration (most common)
	name := "Alice"
	age  := 30
	score := 98.6

	// Explicit type declaration
	var count int = 10
	var pi float64 = 3.14159

	// Multiple assignment
	x, y := 1, 2
	x, y = y, x // swap!

	// Zero values (Go initializes everything)
	var i int     // 0
	var f float64 // 0.0
	var b bool    // false
	var s string  // ""

	fmt.Println(name, age, score)
	fmt.Println(count, pi)
	fmt.Println(x, y)
	fmt.Printf("Zeros: %d %f %v %q\n", i, f, b, s)

	// Constants
	const MaxRetries = 3
	const (
		StatusOK    = 200
		StatusNotFound = 404
		StatusError = 500
	)

	// iota for enumerations
	const (
		Small = iota // 0
		Medium       // 1
		Large        // 2
	)
	fmt.Println(Small, Medium, Large)
	_ = MaxRetries // suppress unused warning
	_ = StatusOK
	_ = StatusNotFound
	_ = StatusError
}

Basic Types Reference

CategoryTypesZero Value
Integersint, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint640
Floatsfloat32, float640.0
Complexcomplex64, complex1280+0i
Booleanboolfalse
Stringstring"" (empty string)
Byte/Runebyte (=uint8), rune (=int32)0
Pointer*Tnil
Slice[]Tnil
Mapmap[K]Vnil
Channelchan Tnil
Interfaceinterface{}nil
Functionfunc(...)nil

3. Functions: Multiple Returns, Named Returns, Variadic, and Defer

Go functions are first-class values. Key features include multiple return values (eliminating the need for out-parameters or exceptions), named return values for documentation, variadic functions, and the powerful defer statement for cleanup logic.

package main

import (
	"errors"
	"fmt"
)

// Multiple return values
func minMax(nums []int) (int, int) {
	min, max := nums[0], nums[0]
	for _, n := range nums {
		if n < min {
			min = n
		}
		if n > max {
			max = n
		}
	}
	return min, max
}

// Named return values (useful for documentation)
func divide(a, b float64) (result float64, err error) {
	if b == 0 {
		err = errors.New("division by zero")
		return // naked return uses named values
	}
	result = a / b
	return
}

// Variadic function
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

// Defer: runs when surrounding function returns
func readFile(path string) error {
	// file, err := os.Open(path)
	// if err != nil { return err }
	// defer file.Close() // always runs, even on panic
	fmt.Println("Opening:", path)
	defer fmt.Println("Closing:", path)
	fmt.Println("Reading:", path)
	return nil
}

// Function as value
func apply(nums []int, fn func(int) int) []int {
	result := make([]int, len(nums))
	for i, n := range nums {
		result[i] = fn(n)
	}
	return result
}

func main() {
	min, max := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
	fmt.Printf("min=%d max=%d\n", min, max)

	res, err := divide(10, 3)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Printf("10/3 = %.4f\n", res)
	}

	fmt.Println(sum(1, 2, 3, 4, 5)) // 15
	nums := []int{1, 2, 3}
	fmt.Println(sum(nums...)) // spread slice

	_ = readFile("data.txt")

	doubled := apply([]int{1, 2, 3, 4}, func(n int) int {
		return n * 2
	})
	fmt.Println(doubled) // [2 4 6 8]
}

4. Structs and Interfaces: Embedding, Methods, and Duck Typing

Go uses composition over inheritance. Structs group related data, methods attach behavior to types, and interfaces define contracts satisfied implicitly. Struct embedding provides a form of inheritance without the complexity.

package main

import (
	"fmt"
	"math"
)

// Struct definition
type Point struct {
	X, Y float64
}

// Method on Point (pointer receiver for mutation)
func (p *Point) Scale(factor float64) {
	p.X *= factor
	p.Y *= factor
}

// Value receiver (does not mutate)
func (p Point) Distance(q Point) float64 {
	dx := p.X - q.X
	dy := p.Y - q.Y
	return math.Sqrt(dx*dx + dy*dy)
}

func (p Point) String() string {
	return fmt.Sprintf("(%.2f, %.2f)", p.X, p.Y)
}

// Embedding: Circle embeds Point
type Circle struct {
	Point  // embedded — promotes X, Y, Scale, Distance
	Radius float64
}

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

// Interface: implicitly satisfied
type Shape interface {
	Area() float64
	String() string
}

// Rectangle also satisfies Shape
type Rectangle struct {
	Width, Height float64
}

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

func (r Rectangle) String() string {
	return fmt.Sprintf("Rect(%.1f x %.1f)", r.Width, r.Height)
}

func printShape(s Shape) {
	fmt.Printf("%s has area %.2f\n", s, s.Area())
}

func main() {
	p := Point{3, 4}
	p.Scale(2)
	fmt.Println(p) // (6.00, 8.00)

	c := Circle{Point: Point{0, 0}, Radius: 5}
	fmt.Printf("Circle area: %.2f\n", c.Area())
	fmt.Println(c.Distance(Point{3, 4})) // uses embedded Point method

	r := Rectangle{10, 5}

	// Both satisfy Shape without explicit declaration
	shapes := []Shape{c, r}
	for _, s := range shapes {
		printShape(s)
	}

	// Empty interface accepts any value
	var any interface{} = 42
	any = "hello"
	any = p
	_ = any
}

5. Goroutines and Channels: Concurrency the Go Way

Go's concurrency model is based on Communicating Sequential Processes (CSP). The mantra: "Don't communicate by sharing memory; share memory by communicating." Goroutines are cheap lightweight threads, and channels are typed pipes for safe data transfer between them.

package main

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

// Unbuffered channel: sender blocks until receiver is ready
func pingPong() {
	ch := make(chan string)

	go func() {
		ch <- "ping"
	}()

	msg := <-ch
	fmt.Println(msg) // ping
}

// Buffered channel: sender only blocks when buffer is full
func buffered() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	// ch <- 4 // would block! buffer full

	fmt.Println(<-ch) // 1
	fmt.Println(<-ch) // 2
}

// WaitGroup for synchronization
func fanOut() {
	var wg sync.WaitGroup
	results := make(chan int, 5)

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			time.Sleep(time.Millisecond * 10)
			results <- id * id
		}(i)
	}

	// Close channel when all goroutines done
	go func() {
		wg.Wait()
		close(results)
	}()

	// Range over closed channel
	for r := range results {
		fmt.Println(r)
	}
}

// Select: handle multiple channels
func selectDemo() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	timeout := time.After(100 * time.Millisecond)

	go func() { time.Sleep(10 * time.Millisecond); ch1 <- "one" }()
	go func() { time.Sleep(20 * time.Millisecond); ch2 <- "two" }()

	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-ch1:
			fmt.Println("Received:", msg1)
		case msg2 := <-ch2:
			fmt.Println("Received:", msg2)
		case <-timeout:
			fmt.Println("Timed out")
			return
		}
	}
}

func main() {
	pingPong()
	buffered()
	fanOut()
	selectDemo()
}

6. Error Handling: The error Interface, Wrapping, and Sentinel Errors

Go treats errors as values. The built-in error interface has a single method: Error() string. This simplicity, combined with explicit error returns, makes error handling highly visible and forces developers to think about failure cases.

package main

import (
	"errors"
	"fmt"
)

// Sentinel errors: predefined, comparable
var (
	ErrNotFound   = errors.New("not found")
	ErrUnauthorized = errors.New("unauthorized")
)

// Custom error type
type ValidationError struct {
	Field   string
	Message string
}

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

// Wrapping errors with %w (Go 1.13+)
func findUser(id int) (string, error) {
	if id <= 0 {
		return "", &ValidationError{Field: "id", Message: "must be positive"}
	}
	if id > 100 {
		return "", fmt.Errorf("findUser(%d): %w", id, ErrNotFound)
	}
	return fmt.Sprintf("user_%d", id), nil
}

func getProfile(id int) (string, error) {
	user, err := findUser(id)
	if err != nil {
		// Wrap with additional context
		return "", fmt.Errorf("getProfile: %w", err)
	}
	return "Profile of " + user, nil
}

func main() {
	// Normal error check
	profile, err := getProfile(42)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println(profile)
	}

	// errors.Is: unwraps error chain
	_, err = getProfile(999)
	if errors.Is(err, ErrNotFound) {
		fmt.Println("Resource not found")
	}

	// errors.As: type assertion through chain
	_, err = getProfile(-1)
	var valErr *ValidationError
	if errors.As(err, &valErr) {
		fmt.Printf("Field %q: %s\n", valErr.Field, valErr.Message)
	}

	// Multi-error (Go 1.20+)
	err1 := errors.New("first error")
	err2 := errors.New("second error")
	combined := errors.Join(err1, err2)
	fmt.Println(combined)
}

7. Standard Library: net/http, encoding/json, os, io, context

Go's standard library is one of its greatest strengths. You can build production-ready HTTP servers, parse JSON, interact with the filesystem, and manage cancellation — all without any third-party dependencies.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

// JSON serialization
type User struct {
	ID        int    `json:"id"`
	Name      string `json:"name"`
	Email     string `json:"email,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

func jsonDemo() {
	user := User{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()}

	// Marshal to JSON
	data, err := json.Marshal(user)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(data))

	// Unmarshal from JSON
	jsonStr := `{"id":2,"name":"Bob","created_at":"2024-01-01T00:00:00Z"}`
	var u2 User
	if err := json.Unmarshal([]byte(jsonStr), &u2); err != nil {
		panic(err)
	}
	fmt.Printf("User: %+v\n", u2)
}

// HTTP server with context
func httpDemo() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		// Go 1.22+ pattern matching
		id := r.PathValue("id")
		user := User{ID: 1, Name: "Alice"}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(user)
		fmt.Println("Served user:", id)
	})

	server := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	fmt.Println("Server on :8080")
	_ = server // server.ListenAndServe()
}

// Context for cancellation
func contextDemo() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	ch := make(chan string, 1)
	go func() {
		time.Sleep(1 * time.Second)
		ch <- "result"
	}()

	select {
	case result := <-ch:
		fmt.Println("Got:", result)
	case <-ctx.Done():
		fmt.Println("Timed out:", ctx.Err())
	}
}

func main() {
	jsonDemo()
	httpDemo()
	contextDemo()
}

8. Building REST APIs: net/http vs chi vs gin

Go 1.22 significantly improved the built-in net/http mux with method-based routing and path parameters. For more complex needs, chi provides middleware composition without reflection, and gin offers a batteries-included framework with excellent performance.

// === Standard Library (Go 1.22+) ===
package main

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

type APIServer struct {
	mux *http.ServeMux
}

func NewAPIServer() *APIServer {
	s := &APIServer{mux: http.NewServeMux()}
	s.routes()
	return s
}

func (s *APIServer) routes() {
	s.mux.HandleFunc("GET /api/users", s.listUsers)
	s.mux.HandleFunc("POST /api/users", s.createUser)
	s.mux.HandleFunc("GET /api/users/{id}", s.getUser)
	s.mux.HandleFunc("PUT /api/users/{id}", s.updateUser)
	s.mux.HandleFunc("DELETE /api/users/{id}", s.deleteUser)
}

func (s *APIServer) listUsers(w http.ResponseWriter, r *http.Request) {
	users := []map[string]any{
		{"id": 1, "name": "Alice"},
		{"id": 2, "name": "Bob"},
	}
	writeJSON(w, http.StatusOK, users)
}

func (s *APIServer) getUser(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	slog.Info("Getting user", "id", id)
	writeJSON(w, http.StatusOK, map[string]string{"id": id})
}

func (s *APIServer) createUser(w http.ResponseWriter, r *http.Request) {
	var body map[string]any
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
	writeJSON(w, http.StatusCreated, body)
}

func (s *APIServer) updateUser(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	writeJSON(w, http.StatusOK, map[string]string{"updated": id})
}

func (s *APIServer) deleteUser(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusNoContent)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(v); err != nil {
		panic(err)
	}
}

func main() {
	server := NewAPIServer()
	slog.Info("Starting server", "addr", ":8080")
	http.ListenAndServe(":8080", server.mux)
}

Router Comparison: stdlib vs chi vs gin

Featurenet/http (1.22+)chigin
Path paramsYes ({id})Yes ({id})Yes (:id)
Method routingYesYesYes
MiddlewareManualExcellentExcellent
Groups/SubroutersNoYesYes
Request bindingManualManualAuto (JSON/form)
ValidationNoNoBuilt-in
Zero alloc routingNoYesYes (radix tree)
DependenciesNoneMinimalSeveral
Best forSimple APIsIdiomatic GoRapid development

9. Testing: Table-Driven Tests, Mocks, and Benchmarks

Go has built-in testing support via the testing package. The idiomatic pattern is table-driven tests — a slice of test cases run in a loop. This approach scales from simple unit tests to complex integration scenarios.

package calc_test

import (
	"testing"
	"errors"
)

// Function under test
func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

// Table-driven test
func TestDivide(t *testing.T) {
	tests := []struct {
		name    string
		a, b    float64
		want    float64
		wantErr bool
	}{
		{"positive", 10, 2, 5, false},
		{"negative divisor", -6, 2, -3, false},
		{"division by zero", 5, 0, 0, true},
		{"decimal result", 1, 3, 0.333, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := divide(tt.a, tt.b)
			if (err != nil) != tt.wantErr {
				t.Errorf("divide() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !tt.wantErr && abs(got-tt.want) > 0.001 {
				t.Errorf("divide() = %v, want %v", got, tt.want)
			}
		})
	}
}

func abs(x float64) float64 {
	if x < 0 {
		return -x
	}
	return x
}

// Interface for mocking
type UserStore interface {
	FindByID(id int) (string, error)
}

// Mock implementation
type MockUserStore struct {
	users map[int]string
}

func (m *MockUserStore) FindByID(id int) (string, error) {
	if user, ok := m.users[id]; ok {
		return user, nil
	}
	return "", errors.New("not found")
}

func TestGetUser(t *testing.T) {
	store := &MockUserStore{
		users: map[int]string{1: "Alice", 2: "Bob"},
	}

	user, err := store.FindByID(1)
	if err != nil {
		t.Fatal(err)
	}
	if user != "Alice" {
		t.Errorf("got %q, want %q", user, "Alice")
	}
}

// Benchmark
func BenchmarkDivide(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, _ = divide(1000.0, 3.0)
	}
}
// Run: go test ./... -bench=. -benchmem -race

10. Modules and Packages: go.mod, go.sum, and Workspaces

Go modules provide reproducible, hermetic builds. Every module has a go.mod file declaring its module path and dependencies, and a go.sum file with cryptographic checksums for security.

# Initialize a new module
go mod init github.com/yourname/myapp

# Add a dependency
go get github.com/go-chi/chi/v5@latest
go get github.com/lib/pq@v1.10.9

# Remove unused dependencies, add missing ones
go mod tidy

# Download all dependencies to local cache
go mod download

# Verify integrity
go mod verify

# Replace a dependency (local dev or fork)
# In go.mod:
# replace github.com/original/pkg => ../local/pkg

# === go.mod example ===
# module github.com/yourname/myapp
#
# go 1.22
#
# require (
#     github.com/go-chi/chi/v5 v5.1.0
#     github.com/lib/pq v1.10.9
#     golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
# )

# === Workspace (multi-module development) ===
# go work init ./moduleA ./moduleB
# go work use ./newmodule
# go work sync

# === Build and run ===
go run ./cmd/server          # run without building
go build -o bin/server ./cmd/server  # compile binary
go install github.com/tools/cmd@latest  # install tool

# Cross-compilation
GOOS=linux GOARCH=amd64 go build -o bin/server-linux ./cmd/server
GOOS=windows GOARCH=amd64 go build -o bin/server.exe ./cmd/server

11. Generics (Go 1.18+): Type Parameters and Constraints

Go generics use type parameters in square brackets with constraint interfaces. The golang.org/x/exp/constraints package and the built-in comparable constraint are commonly used. Generics are most valuable for data structures and functional utilities.

package main

import "fmt"

// Number constraint
type Number interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64
}

// Generic sum function
func Sum[T Number](nums []T) T {
	var total T
	for _, n := range nums {
		total += n
	}
	return total
}

// Generic Map (functional)
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
}

// Generic Filter
func Filter[T any](slice []T, pred func(T) bool) []T {
	var result []T
	for _, v := range slice {
		if pred(v) {
			result = append(result, v)
		}
	}
	return result
}

// Generic stack data structure
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) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	n := len(s.items) - 1
	item := s.items[n]
	s.items = s.items[:n]
	return item, true
}

func (s *Stack[T]) Len() int { return len(s.items) }

// Generic Set
type Set[T comparable] struct {
	items map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
	return &Set[T]{items: make(map[T]struct{})}
}

func (s *Set[T]) Add(v T)      { s.items[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.items[v]; return ok }
func (s *Set[T]) Len() int     { return len(s.items) }

func main() {
	// Generic functions
	fmt.Println(Sum([]int{1, 2, 3, 4, 5}))       // 15
	fmt.Println(Sum([]float64{1.1, 2.2, 3.3}))   // 6.6

	doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
	fmt.Println(doubled) // [2 4 6]

	evens := Filter([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 })
	fmt.Println(evens) // [2 4]

	// Generic stack
	s := &Stack[string]{}
	s.Push("a")
	s.Push("b")
	if v, ok := s.Pop(); ok {
		fmt.Println("Popped:", v) // b
	}

	// Generic set
	set := NewSet[int]()
	set.Add(1)
	set.Add(2)
	set.Add(1) // duplicate ignored
	fmt.Println("Set size:", set.Len()) // 2
	fmt.Println("Has 1:", set.Has(1))   // true
}

12. Concurrency Patterns: Worker Pools, Fan-Out/Fan-In, Context Cancellation

Beyond basic goroutines and channels, Go enables elegant higher-level concurrency patterns. These patterns handle real-world concerns like backpressure, graceful shutdown, and coordinating many concurrent operations.

package main

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

// Worker Pool Pattern
func workerPool(ctx context.Context, jobs <-chan int, numWorkers int) <-chan int {
	results := make(chan int, numWorkers)
	var wg sync.WaitGroup

	for i := 0; i < numWorkers; i++ {
		wg.Add(1)
		go func(workerID int) {
			defer wg.Done()
			for {
				select {
				case job, ok := <-jobs:
					if !ok {
						return // channel closed
					}
					// simulate work
					time.Sleep(time.Millisecond)
					results <- job * job
				case <-ctx.Done():
					return // cancelled
				}
			}
		}(i)
	}

	go func() {
		wg.Wait()
		close(results)
	}()

	return results
}

// Fan-Out: one input, multiple processors
func fanOut[T any](ctx context.Context, input <-chan T, n int) []<-chan T {
	outputs := make([]<-chan T, n)
	for i := 0; i < n; i++ {
		ch := make(chan T)
		outputs[i] = ch
		go func(out chan<- T) {
			defer close(out)
			for v := range input {
				select {
				case out <- v:
				case <-ctx.Done():
					return
				}
			}
		}(ch)
	}
	return outputs
}

// Fan-In: multiple inputs, one output
func fanIn[T any](ctx context.Context, inputs ...<-chan T) <-chan T {
	merged := make(chan T)
	var wg sync.WaitGroup

	for _, ch := range inputs {
		wg.Add(1)
		go func(c <-chan T) {
			defer wg.Done()
			for v := range c {
				select {
				case merged <- v:
				case <-ctx.Done():
					return
				}
			}
		}(ch)
	}

	go func() {
		wg.Wait()
		close(merged)
	}()

	return merged
}

// Pipeline stage
func generate(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for _, n := range nums {
			out <- n
		}
	}()
	return out
}

func square(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for n := range in {
			out <- n * n
		}
	}()
	return out
}

func main() {
	// Worker pool demo
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	jobs := make(chan int, 10)
	for i := 1; i <= 5; i++ {
		jobs <- i
	}
	close(jobs)

	results := workerPool(ctx, jobs, 3)
	for r := range results {
		fmt.Println("Result:", r)
	}

	// Pipeline demo
	nums := generate(1, 2, 3, 4, 5)
	squares := square(nums)
	for s := range squares {
		fmt.Println("Square:", s)
	}
}

13. Go vs Node.js vs Python: Which to Choose?

CriterionGoNode.jsPython
PerformanceExcellent (near C)Good (V8 JIT)Moderate (CPython)
ConcurrencyGoroutines (true parallel)Event loop (single-threaded)GIL limited (asyncio)
Memory usageVery lowModerateHigh
Cold startInstant (compiled)FastSlow (imports)
Type safetyStatic, strictOptional (TypeScript)Optional (type hints)
Learning curveLow-moderateLowVery low
EcosystemGrowingMassive (npm)Massive (PyPI)
DeploymentSingle binaryNode runtime neededPython runtime needed
Best forAPIs, microservices, systemsReal-time, full-stackData science, ML, scripting
Error handlingExplicit (values)try/catch (exceptions)try/except (exceptions)
GenericsYes (1.18+)TypeScript genericsType hints only
Standard libraryExcellentGoodExcellent

14. Production Tips and Best Practices

Project Structure

myapp/
├── cmd/
│   └── server/
│       └── main.go          # entry point
├── internal/
│   ├── handler/             # HTTP handlers
│   ├── service/             # business logic
│   ├── repository/          # data access layer
│   └── model/               # domain types
├── pkg/
│   ├── logger/              # reusable packages
│   └── middleware/
├── migrations/              # SQL migrations
├── config/
│   └── config.go
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── go.mod
└── go.sum

# Makefile targets
# make build   → go build -o bin/server ./cmd/server
# make test    → go test ./... -race -cover
# make lint    → golangci-lint run
# make docker  → docker build -t myapp .

Graceful Shutdown Pattern

package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	srv := &http.Server{
		Addr:    ":8080",
		Handler: http.DefaultServeMux,
	}

	// Start server in goroutine
	go func() {
		if err := srv.ListenAndServe(); err != http.ErrServerClosed {
			slog.Error("Server error", "err", err)
			os.Exit(1)
		}
	}()
	slog.Info("Server started", "addr", srv.Addr)

	// Wait for interrupt signal
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	slog.Info("Shutting down...")
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		slog.Error("Shutdown error", "err", err)
	}
	slog.Info("Server stopped")
}

Conclusion

Go has earned its place as one of the premier languages for backend development. Its philosophy of simplicity — orthogonal features, a small spec, and one idiomatic way to do most things — pays dividends in maintainability as teams and codebases grow.

The goroutine model makes concurrency approachable without sacrificing performance. The standard library reduces external dependencies. And the compiler catches errors that would only surface at runtime in dynamic languages.

Whether you are building a high-throughput microservice, a CLI tool, or a data pipeline, Go gives you the tools to write correct, fast, and maintainable code. Start with the standard library, follow idiomatic patterns, and reach for third-party packages only when genuinely needed.

Quick Reference: Essential Go Commands

go run ./cmd/serverRun without building
go build -o bin/app .Compile to binary
go test ./... -raceRun tests with race detector
go test -bench=.Run benchmarks
go fmt ./...Format all code
go vet ./...Static analysis checks
go mod tidyClean up dependencies
go generate ./...Run code generators

Frequently Asked Questions

What is Go (Golang) and why should I use it for backend development?

Go is a statically typed, compiled language created by Google in 2009. It excels at backend development due to its built-in concurrency model (goroutines), fast compilation, simple syntax, and excellent standard library. It is ideal for microservices, APIs, CLI tools, and high-performance systems.

What is the difference between goroutines and threads?

Goroutines are lightweight user-space threads managed by the Go runtime. They start with only ~2KB of stack space (vs ~1MB for OS threads), can number in the millions, and are multiplexed onto OS threads automatically. This makes concurrent Go programs extremely memory-efficient.

How does error handling work in Go?

Go uses explicit error returns rather than exceptions. Functions return (value, error) pairs. Callers check if error != nil and handle it. Use fmt.Errorf with %w to wrap errors, errors.Is for sentinel error comparison, and errors.As for type assertion on errors.

What are Go interfaces and how does duck typing work?

Go interfaces are satisfied implicitly — if a type implements all methods of an interface, it automatically satisfies it without explicit declaration. This is called structural typing (duck typing). It promotes loose coupling and makes testing with mocks very easy.

How do Go channels work?

Channels are typed conduits for communication between goroutines. Unbuffered channels block until both sender and receiver are ready. Buffered channels (make(chan T, n)) can hold n values before blocking. Use select to handle multiple channels. Close channels to signal completion.

What is the Go module system and how do I use it?

Go modules (introduced in Go 1.11) manage dependencies using go.mod and go.sum files. Initialize with "go mod init module-name", add dependencies with "go get package@version", and tidy with "go mod tidy". Modules replace the old GOPATH-based workflow.

When should I use Go generics?

Use Go generics (introduced in 1.18) when you need type-safe data structures or algorithms that work across multiple types without code duplication. Common use cases include generic collections (stacks, queues, maps), utility functions (Map, Filter, Reduce), and constraint-based APIs.

How does Go compare to Node.js and Python for backend development?

Go is faster and more memory-efficient than both Node.js and Python, with true parallelism via goroutines. Node.js has a larger ecosystem and is better for real-time apps. Python excels in data science and ML. Go is the best choice for high-performance APIs, microservices, and systems programming.

𝕏 Twitterin LinkedIn
Cet article vous a-t-il aidé ?

Restez informé

Recevez des astuces dev et les nouveaux outils chaque semaine.

Pas de spam. Désabonnez-vous à tout moment.

Essayez ces outils associés

{ }JSON Formatter#Hash GeneratorB→Base64 Encoder

Articles connexes

Guide Bases Rust: Propriété, Emprunt, Durées de Vie et Programmation Système

Maîtrisez Rust. Guide avec système de propriété, emprunt, durées de vie, structs, enums, pattern matching, gestion d'erreurs, traits, itérateurs et concurrence.

Guide Node.js: Tutoriel Complet pour le Développement Backend

Maîtrisez Node.js backend. Guide avec event loop, Express.js, REST API, JWT, intégration BDD, tests Jest, déploiement PM2 et comparaison Node.js vs Deno vs Bun.

Guide CI/CD: GitHub Actions, GitLab CI, Docker et Pipelines de Déploiement

Maîtrisez les pipelines CI/CD. Guide avec GitHub Actions, GitLab CI, Docker, stratégies de déploiement, secrets, monorepo CI et optimisation de pipeline.