Przejdź do treści
Backend

Go (Golang) - wydajne API i serwisy backendowe

Opublikowano:
·5 min czytania·Autor: MDS Software Solutions Group

Go (Golang) wydajne

backend

Go (Golang) - wydajne API i serwisy backendowe

Go, znany również jako Golang, to język programowania stworzony przez Google w 2009 roku. Zaprojektowany przez Roberta Griesemera, Roba Pike'a i Kena Thompsona, Go powstał jako odpowiedź na frustracje związane z istniejącymi językami używanymi w infrastrukturze Google. Celem było stworzenie języka, który łączy prostotę Pythona z wydajnością C++, jednocześnie oferując natywne wsparcie dla współbieżności i szybką kompilację.

Dziś Go jest jednym z najpopularniejszych języków do budowy mikroserwisów, API, narzędzi CLI i systemów rozproszonych. Używają go firmy takie jak Google, Uber, Dropbox, Twitch, Docker i Kubernetes - a właściwie sam Kubernetes jest napisany w Go.

Dlaczego Go zyskał popularność?#

Go wyróżnia się kilkoma kluczowymi cechami:

  • Prostota składni - minimalna ilość słów kluczowych, brak dziedziczenia klas
  • Szybka kompilacja - duże projekty kompilują się w sekundach
  • Statyczne typowanie - błędy wykrywane na etapie kompilacji
  • Wbudowana współbieżność - goroutines i kanały jako elementy języka
  • Pojedynczy binark - brak zależności runtime, łatwy deployment
  • Garbage collector - automatyczne zarządzanie pamięcią z niskimi opóźnieniami
  • Bogata biblioteka standardowa - serwer HTTP, JSON, kryptografia, testowanie

Goroutines i kanały - model współbieżności#

Goroutines to lekkie wątki zarządzane przez runtime Go. Są znacznie tańsze niż wątki systemowe - można uruchomić setki tysięcy goroutines na zwykłym serwerze. Każda goroutine zaczyna z zaledwie 2 KB stosu, który dynamicznie rośnie w miarę potrzeb.

Podstawowe goroutines#

package main

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

func fetchData(source string, wg *sync.WaitGroup) {
    defer wg.Done()

    // Symulacja pobierania danych
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Pobrano dane z: %s\n", source)
}

func main() {
    var wg sync.WaitGroup

    sources := []string{
        "https://api.users.example.com",
        "https://api.orders.example.com",
        "https://api.products.example.com",
        "https://api.payments.example.com",
    }

    start := time.Now()

    for _, source := range sources {
        wg.Add(1)
        go fetchData(source, &wg)
    }

    wg.Wait()
    fmt.Printf("Wszystkie zapytania zakończone w: %v\n", time.Since(start))
    // ~100ms zamiast ~400ms sekwencyjnie
}

Kanały (Channels)#

Kanały umożliwiają bezpieczną komunikację między goroutines. Zamiast współdzielenia pamięci, goroutines komunikują się przez przesyłanie wiadomości - zgodnie z mottem Go: „Nie komunikuj się przez współdzielenie pamięci; współdziel pamięć przez komunikację".

package main

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

type Result struct {
    Source string
    Data   interface{}
    Error  error
}

func fetchAPI(url string, ch chan<- Result) {
    client := &http.Client{Timeout: 5 * time.Second}

    resp, err := client.Get(url)
    if err != nil {
        ch <- Result{Source: url, Error: err}
        return
    }
    defer resp.Body.Close()

    var data interface{}
    json.NewDecoder(resp.Body).Decode(&data)
    ch <- Result{Source: url, Data: data}
}

func aggregateData(urls []string) []Result {
    ch := make(chan Result, len(urls))

    for _, url := range urls {
        go fetchAPI(url, ch)
    }

    results := make([]Result, 0, len(urls))
    for i := 0; i < len(urls); i++ {
        results = append(results, <-ch)
    }

    return results
}

func main() {
    urls := []string{
        "https://jsonplaceholder.typicode.com/posts/1",
        "https://jsonplaceholder.typicode.com/users/1",
        "https://jsonplaceholder.typicode.com/comments/1",
    }

    results := aggregateData(urls)
    for _, r := range results {
        if r.Error != nil {
            fmt.Printf("Błąd z %s: %v\n", r.Source, r.Error)
        } else {
            fmt.Printf("Sukces z %s\n", r.Source)
        }
    }
}

Select - multipleksowanie kanałów#

Instrukcja select pozwala oczekiwać na wiele operacji kanałowych jednocześnie:

func fetchWithTimeout(url string, timeout time.Duration) (*http.Response, error) {
    ch := make(chan *http.Response, 1)
    errCh := make(chan error, 1)

    go func() {
        resp, err := http.Get(url)
        if err != nil {
            errCh <- err
            return
        }
        ch <- resp
    }()

    select {
    case resp := <-ch:
        return resp, nil
    case err := <-errCh:
        return nil, err
    case <-time.After(timeout):
        return nil, fmt.Errorf("timeout po %v", timeout)
    }
}

Serwer HTTP z biblioteki standardowej#

Go ma wbudowany, produkcyjny serwer HTTP. W przeciwieństwie do wielu języków, nie potrzebujesz zewnętrznego frameworka, aby uruchomić wydajny serwer webowy:

package main

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

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

type UserStore struct {
    mu    sync.RWMutex
    users map[int]User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) Create(user User) User {
    s.mu.Lock()
    defer s.mu.Unlock()

    user.ID = s.nextID
    s.nextID++
    s.users[user.ID] = user
    return user
}

func (s *UserStore) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()

    users := make([]User, 0, len(s.users))
    for _, u := range s.users {
        users = append(users, u)
    }
    return users
}

func main() {
    store := NewUserStore()

    mux := http.NewServeMux()

    mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")

        switch r.Method {
        case http.MethodGet:
            json.NewEncoder(w).Encode(store.GetAll())

        case http.MethodPost:
            var user User
            if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
                http.Error(w, `{"error":"nieprawidłowe dane"}`, http.StatusBadRequest)
                return
            }
            created := store.Create(user)
            w.WriteHeader(http.StatusCreated)
            json.NewEncoder(w).Encode(created)

        default:
            http.Error(w, `{"error":"metoda niedozwolona"}`, http.StatusMethodNotAllowed)
        }
    })

    // Middleware logujący
    handler := loggingMiddleware(mux)

    log.Println("Serwer uruchomiony na :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
        next.ServeHTTP(w, r)
    })
}

Popularne frameworki: Gin, Echo, Fiber#

Choć biblioteka standardowa jest wystarczająca, frameworki dodają wygodne funkcje jak routing z parametrami, walidację, middleware i generowanie dokumentacji.

Gin - najpopularniejszy framework#

Gin jest najczęściej używanym frameworkiem webowym Go. Oferuje doskonałą wydajność i intuicyjne API:

package main

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
)

type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name" binding:"required,min=2"`
    Description string  `json:"description"`
    Price       float64 `json:"price" binding:"required,gt=0"`
    Category    string  `json:"category" binding:"required"`
}

var products = []Product{
    {ID: 1, Name: "Laptop", Description: "Laptop programistyczny", Price: 4999.99, Category: "elektronika"},
    {ID: 2, Name: "Klawiatura", Description: "Mechaniczna", Price: 399.99, Category: "akcesoria"},
}

func main() {
    r := gin.Default()

    // Middleware CORS
    r.Use(func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
        c.Next()
    })

    api := r.Group("/api/v1")
    {
        api.GET("/products", getProducts)
        api.GET("/products/:id", getProduct)
        api.POST("/products", createProduct)
        api.PUT("/products/:id", updateProduct)
        api.DELETE("/products/:id", deleteProduct)
    }

    r.Run(":8080")
}

func getProducts(c *gin.Context) {
    category := c.Query("category")

    if category != "" {
        filtered := make([]Product, 0)
        for _, p := range products {
            if p.Category == category {
                filtered = append(filtered, p)
            }
        }
        c.JSON(http.StatusOK, gin.H{"data": filtered, "count": len(filtered)})
        return
    }

    c.JSON(http.StatusOK, gin.H{"data": products, "count": len(products)})
}

func getProduct(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "nieprawidłowe ID"})
        return
    }

    for _, p := range products {
        if p.ID == id {
            c.JSON(http.StatusOK, gin.H{"data": p})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "produkt nie znaleziony"})
}

func createProduct(c *gin.Context) {
    var product Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    product.ID = len(products) + 1
    products = append(products, product)
    c.JSON(http.StatusCreated, gin.H{"data": product})
}

func updateProduct(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))

    var input Product
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    for i, p := range products {
        if p.ID == id {
            input.ID = id
            products[i] = input
            c.JSON(http.StatusOK, gin.H{"data": input})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "produkt nie znaleziony"})
}

func deleteProduct(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))

    for i, p := range products {
        if p.ID == id {
            products = append(products[:i], products[i+1:]...)
            c.JSON(http.StatusOK, gin.H{"message": "usunięto"})
            return
        }
    }

    c.JSON(http.StatusNotFound, gin.H{"error": "produkt nie znaleziony"})
}

Echo - minimalistyczny i wydajny#

package main

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    e := echo.New()

    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))

    e.GET("/api/health", func(c echo.Context) error {
        return c.JSON(http.StatusOK, map[string]string{
            "status": "healthy",
            "version": "1.0.0",
        })
    })

    e.Logger.Fatal(e.Start(":8080"))
}

Fiber - zainspirowany Express.js#

Fiber bazuje na fasthttp i jest przyjazny dla programistów przechodzących z Node.js:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/limiter"
    "time"
)

func main() {
    app := fiber.New(fiber.Config{
        Prefork:       true,
        CaseSensitive: true,
        ServerHeader:  "MDS-API",
    })

    app.Use(cors.New())
    app.Use(limiter.New(limiter.Config{
        Max:        100,
        Expiration: 1 * time.Minute,
    }))

    app.Get("/api/status", func(c *fiber.Ctx) error {
        return c.JSON(fiber.Map{
            "status": "ok",
            "uptime": time.Since(time.Now()).String(),
        })
    })

    app.Listen(":3000")
}

Modelowanie danych za pomocą struktur#

Go nie ma klas w tradycyjnym rozumieniu. Zamiast tego wykorzystuje struktury (structs) z metodami i interfejsy do polimorfizmu:

package models

import "time"

type User struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Email     string    `json:"email" gorm:"uniqueIndex;not null" validate:"required,email"`
    Name      string    `json:"name" gorm:"not null" validate:"required,min=2,max=100"`
    Password  string    `json:"-" gorm:"not null"` // "-" ukrywa pole w JSON
    Role      string    `json:"role" gorm:"default:user"`
    Active    bool      `json:"active" gorm:"default:true"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
    Orders    []Order   `json:"orders,omitempty" gorm:"foreignKey:UserID"`
}

type Order struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    UserID    uint      `json:"user_id" gorm:"index;not null"`
    Total     float64   `json:"total" gorm:"not null"`
    Status    string    `json:"status" gorm:"default:pending"`
    Items     []Item    `json:"items" gorm:"foreignKey:OrderID"`
    CreatedAt time.Time `json:"created_at"`
}

type Item struct {
    ID        uint    `json:"id" gorm:"primaryKey"`
    OrderID   uint    `json:"order_id"`
    ProductID uint    `json:"product_id"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

// Interfejs definiujący kontrakt repozytorium
type UserRepository interface {
    FindByID(id uint) (*User, error)
    FindByEmail(email string) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id uint) error
    List(page, limit int) ([]User, int64, error)
}

Obsługa błędów w Go#

Go przyjmuje jawne podejście do obsługi błędów. Zamiast wyjątków, funkcje zwracają wartość błędu, którą trzeba sprawdzić:

package service

import (
    "errors"
    "fmt"
)

// Definiowanie własnych typów błędów
var (
    ErrNotFound      = errors.New("zasób nie znaleziony")
    ErrAlreadyExists = errors.New("zasób już istnieje")
    ErrUnauthorized  = errors.New("brak autoryzacji")
    ErrValidation    = errors.New("błąd walidacji")
)

type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Detail  string `json:"detail,omitempty"`
    Err     error  `json:"-"`
}

func (e *AppError) Error() string {
    return fmt.Sprintf("[%d] %s: %s", e.Code, e.Message, e.Detail)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

func NewNotFoundError(resource string, id interface{}) *AppError {
    return &AppError{
        Code:    404,
        Message: "nie znaleziono",
        Detail:  fmt.Sprintf("%s o ID %v nie istnieje", resource, id),
        Err:     ErrNotFound,
    }
}

func NewValidationError(detail string) *AppError {
    return &AppError{
        Code:    400,
        Message: "błąd walidacji",
        Detail:  detail,
        Err:     ErrValidation,
    }
}

// Użycie w serwisie
type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUser(id uint) (*User, error) {
    user, err := s.repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("pobieranie użytkownika %d: %w", id, err)
    }

    if user == nil {
        return nil, NewNotFoundError("użytkownik", id)
    }

    return user, nil
}

func (s *UserService) CreateUser(user *User) error {
    // Sprawdzenie czy email już istnieje
    existing, err := s.repo.FindByEmail(user.Email)
    if err != nil {
        return fmt.Errorf("sprawdzanie emaila: %w", err)
    }
    if existing != nil {
        return &AppError{
            Code:    409,
            Message: "konflikt",
            Detail:  "użytkownik z tym emailem już istnieje",
            Err:     ErrAlreadyExists,
        }
    }

    if err := s.repo.Create(user); err != nil {
        return fmt.Errorf("tworzenie użytkownika: %w", err)
    }

    return nil
}

Dostęp do bazy danych - GORM i sqlx#

GORM - ORM dla Go#

GORM to najpopularniejszy ORM w ekosystemie Go, oferujący auto-migracje, relacje i query builder:

package database

import (
    "fmt"
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func Connect(dsn string) (*gorm.DB, error) {
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        return nil, fmt.Errorf("połączenie z bazą danych: %w", err)
    }

    // Connection pooling
    sqlDB, _ := db.DB()
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetMaxOpenConns(100)

    // Auto-migracja
    err = db.AutoMigrate(&User{}, &Order{}, &Item{})
    if err != nil {
        return nil, fmt.Errorf("migracja: %w", err)
    }

    log.Println("Połączono z bazą danych")
    return db, nil
}

// Implementacja repozytorium z GORM
type GormUserRepository struct {
    db *gorm.DB
}

func NewGormUserRepository(db *gorm.DB) *GormUserRepository {
    return &GormUserRepository{db: db}
}

func (r *GormUserRepository) FindByID(id uint) (*User, error) {
    var user User
    result := r.db.Preload("Orders").First(&user, id)
    if result.Error != nil {
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            return nil, nil
        }
        return nil, result.Error
    }
    return &user, nil
}

func (r *GormUserRepository) List(page, limit int) ([]User, int64, error) {
    var users []User
    var total int64

    r.db.Model(&User{}).Count(&total)

    offset := (page - 1) * limit
    result := r.db.Offset(offset).Limit(limit).
        Order("created_at DESC").
        Find(&users)

    return users, total, result.Error
}

func (r *GormUserRepository) Create(user *User) error {
    return r.db.Create(user).Error
}

sqlx - rozszerzenie database/sql#

Dla lepszej kontroli nad zapytaniami SQL, sqlx jest lekką alternatywą:

package database

import (
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

type SqlxUserRepository struct {
    db *sqlx.DB
}

func NewSqlxConnection(dsn string) (*sqlx.DB, error) {
    return sqlx.Connect("postgres", dsn)
}

func (r *SqlxUserRepository) FindByID(id uint) (*User, error) {
    var user User
    err := r.db.Get(&user,
        `SELECT id, email, name, role, active, created_at
         FROM users WHERE id = $1`, id)
    if err != nil {
        return nil, err
    }
    return &user, nil
}

func (r *SqlxUserRepository) Search(query string, limit int) ([]User, error) {
    var users []User
    err := r.db.Select(&users,
        `SELECT id, email, name, role, created_at
         FROM users
         WHERE name ILIKE $1 OR email ILIKE $1
         ORDER BY created_at DESC
         LIMIT $2`,
        "%"+query+"%", limit)
    return users, err
}

gRPC i Protocol Buffers#

Go jest jednym z najlepszych języków do budowy serwisów gRPC. Protocol Buffers zapewniają wydajną serializację, a gRPC - szybką komunikację między serwisami:

// user.proto
syntax = "proto3";
package user;
option go_package = "./pb";

message User {
    int64 id = 1;
    string name = 2;
    string email = 3;
    string role = 4;
}

message GetUserRequest {
    int64 id = 1;
}

message CreateUserRequest {
    string name = 1;
    string email = 2;
    string role = 3;
}

message UserListResponse {
    repeated User users = 1;
    int64 total = 2;
}

message ListUsersRequest {
    int32 page = 1;
    int32 limit = 2;
}

service UserService {
    rpc GetUser(GetUserRequest) returns (User);
    rpc CreateUser(CreateUserRequest) returns (User);
    rpc ListUsers(ListUsersRequest) returns (UserListResponse);
}

Implementacja serwera gRPC w Go:

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    pb "myapp/pb"
)

type userServer struct {
    pb.UnimplementedUserServiceServer
    users map[int64]*pb.User
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, ok := s.users[req.Id]
    if !ok {
        return nil, status.Errorf(codes.NotFound, "użytkownik %d nie znaleziony", req.Id)
    }
    return user, nil
}

func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
    id := int64(len(s.users) + 1)
    user := &pb.User{
        Id:    id,
        Name:  req.Name,
        Email: req.Email,
        Role:  req.Role,
    }
    s.users[id] = user
    return user, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("nie udało się nasłuchiwać: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterUserServiceServer(s, &userServer{
        users: make(map[int64]*pb.User),
    })

    log.Println("Serwer gRPC uruchomiony na :50051")
    log.Fatal(s.Serve(lis))
}

Testowanie w Go#

Go ma wbudowany framework testowy - nie potrzebujesz zewnętrznych bibliotek. Testy tworzy się w plikach _test.go:

package service_test

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/gin-gonic/gin"
)

func setupRouter() *gin.Engine {
    gin.SetMode(gin.TestMode)
    r := gin.Default()
    r.GET("/api/products", getProducts)
    r.POST("/api/products", createProduct)
    return r
}

func TestGetProducts(t *testing.T) {
    router := setupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/api/products", nil)
    router.ServeHTTP(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("oczekiwano status 200, otrzymano %d", w.Code)
    }

    if !strings.Contains(w.Body.String(), "data") {
        t.Error("odpowiedź nie zawiera klucza 'data'")
    }
}

func TestCreateProduct(t *testing.T) {
    router := setupRouter()

    body := `{"name":"Test Product","price":29.99,"category":"test"}`

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("POST", "/api/products", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    router.ServeHTTP(w, req)

    if w.Code != http.StatusCreated {
        t.Errorf("oczekiwano status 201, otrzymano %d", w.Code)
    }
}

// Testy tabelaryczne - idiomatyczny wzorzec Go
func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        isValid bool
    }{
        {"prawidłowy email", "user@example.com", true},
        {"bez domeny", "user@", false},
        {"bez @", "userexample.com", false},
        {"pusty string", "", false},
        {"z subdomeną", "user@sub.example.com", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := ValidateEmail(tt.email)
            if result != tt.isValid {
                t.Errorf("ValidateEmail(%q) = %v, oczekiwano %v",
                    tt.email, result, tt.isValid)
            }
        })
    }
}

// Benchmark
func BenchmarkGetProducts(b *testing.B) {
    router := setupRouter()

    for i := 0; i < b.N; i++ {
        w := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", "/api/products", nil)
        router.ServeHTTP(w, req)
    }
}

Uruchamianie testów:

# Wszystkie testy
go test ./...

# Z verbose output
go test -v ./...

# Z pokryciem kodu
go test -cover ./...

# Benchmarki
go test -bench=. ./...

# Race detector
go test -race ./...

Cross-kompilacja i deployment#

Jedną z największych zalet Go jest możliwość kompilacji na dowolną platformę z dowolnej platformy:

# Kompilacja na Linux z Windowsa/Maca
GOOS=linux GOARCH=amd64 go build -o server-linux ./cmd/server

# Kompilacja na Windows
GOOS=windows GOARCH=amd64 go build -o server.exe ./cmd/server

# Kompilacja na ARM (Raspberry Pi, AWS Graviton)
GOOS=linux GOARCH=arm64 go build -o server-arm64 ./cmd/server

# Optymalizacja rozmiaru binarki
go build -ldflags="-s -w" -o server ./cmd/server

Docker i Kubernetes#

Go idealnie współgra z konteneryzacją. Dzięki statycznej kompilacji, obrazy Docker mogą być minimalne:

# Multi-stage build
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Cachowanie zależności
COPY go.mod go.sum ./
RUN go mod download

# Kopiowanie kodu i budowanie
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server

# Minimalny obraz produkcyjny
FROM scratch

COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080

ENTRYPOINT ["/server"]

Ten obraz waży zaledwie 5-15 MB - porównaj to z typowym obrazem Node.js (200+ MB) czy Javy (300+ MB).

Manifest Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: go-api
  template:
    metadata:
      labels:
        app: go-api
    spec:
      containers:
      - name: go-api
        image: registry.example.com/go-api:latest
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "32Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: go-api-service
spec:
  selector:
    app: go-api
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

Wydajność - porównanie z innymi językami#

Go oferuje wyjątkową wydajność w stosunku do swojej prostoty. Poniżej typowe porównanie dla API REST zwracającego dane z bazy:

| Metryka | Go (Gin) | Node.js (Express) | Python (FastAPI) | Java (Spring Boot) | |---|---|---|---|---| | Zapytania/s | ~45 000 | ~15 000 | ~8 000 | ~35 000 | | Opóźnienie p99 | 2 ms | 12 ms | 25 ms | 5 ms | | Zużycie RAM | 20 MB | 80 MB | 60 MB | 200 MB | | Czas startu | 10 ms | 500 ms | 2 s | 8 s | | Rozmiar Docker | 10 MB | 200 MB | 150 MB | 300 MB |

Go osiąga wydajność zbliżoną do Javy przy znacznie mniejszym zużyciu zasobów i szybszym czasie startu, co jest krytyczne w środowiskach kontenerowych i serverless.

Kiedy wybrać Go?#

Go jest idealnym wyborem, gdy:

  • Budujesz mikroserwisy - szybki start, mały footprint, łatwy deployment
  • Potrzebujesz wysokiej współbieżności - goroutines obsługują tysiące połączeń
  • Tworzysz narzędzia CLI - pojedynczy binarny plik bez zależności
  • Pracujesz z chmurą - Docker, Kubernetes, AWS Lambda, Cloud Functions
  • Zależy ci na wydajności - niskoopóźnieniowe API i przetwarzanie danych
  • Chcesz prostoty - minimalna składnia, szybkie wdrożenie nowych programistów

Go nie jest najlepszym wyborem, gdy potrzebujesz rozbudowanego ekosystemu ORM i generatorów (jak Rails czy Django), programowania funkcyjnego na zaawansowanym poziomie, lub GUI desktopowego.

Podsumowanie#

Go to język, który udowodnił swoją wartość w produkcji. Od mikroserwisów po narzędzia infrastrukturalne, od API po systemy rozproszone - Go oferuje unikalną kombinację wydajności, prostoty i niezawodności.

Kluczowe zalety Go to:

  • Goroutines zapewniające lekką współbieżność
  • Prosty model błędów wymuszający jawną obsługę
  • Szybka kompilacja i natychmiastowy start aplikacji
  • Minimalne obrazy Docker idealne dla Kubernetes
  • Wbudowane narzędzia do testowania, profilowania i formatowania kodu
  • Bogaty ekosystem z frameworkami jak Gin, Echo i Fiber

Potrzebujesz wydajnego API w Go?#

W MDS Software Solutions Group specjalizujemy się w budowie wysokowydajnych systemów backendowych:

  • Projektowanie i implementacja API w Go
  • Architektura mikroserwisów z gRPC
  • Optymalizacja wydajności istniejących systemów
  • Konteneryzacja i deployment na Kubernetes
  • Szkolenia i konsulting technologiczny

Skontaktuj się z nami, aby omówić Twój projekt i dowiedzieć się, jak Go może przyspieszyć Twoje usługi!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Go (Golang) - wydajne API i serwisy backendowe | MDS Software Solutions Group | MDS Software Solutions Group