DEV Community

Putra Prima A
Putra Prima A

Posted on

Golang For PHP Developer : Testing & Deployment

Day 7: Testing & Deployment

Goal: Write tests and deploy your API to production

Time: ~4-5 hours

What you'll build: Tested, production-ready API with deployment


1. Testing Basics in Go (1 hour)

Go's Built-in Testing

Go has testing built into the language - no PHPUnit needed!

Test file naming: *_test.go

Your First Test

Create utils/password_test.go:

package utils

import "testing"

func TestHashPassword(t *testing.T) {
    password := "mySecretPassword123"

    hash, err := HashPassword(password)
    if err != nil {
        t.Fatalf("HashPassword failed: %v", err)
    }

    if hash == "" {
        t.Error("Hash should not be empty")
    }

    if hash == password {
        t.Error("Hash should not equal plain password")
    }
}

func TestCheckPassword(t *testing.T) {
    password := "mySecretPassword123"
    hash, _ := HashPassword(password)

    // Test correct password
    if !CheckPassword(password, hash) {
        t.Error("CheckPassword should return true for correct password")
    }

    // Test wrong password
    if CheckPassword("wrongPassword", hash) {
        t.Error("CheckPassword should return false for wrong password")
    }
}

func TestHashPasswordTooShort(t *testing.T) {
    password := "short"

    _, err := HashPassword(password)
    if err == nil {
        t.Error("Expected error for short password")
    }
}
Enter fullscreen mode Exit fullscreen mode

Running Tests

# Run all tests
go test ./...

# Run tests in specific package
go test ./utils

# Verbose output
go test -v ./utils

# Run specific test
go test -v -run TestHashPassword ./utils

# With coverage
go test -cover ./...

# Coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Enter fullscreen mode Exit fullscreen mode

Table-Driven Tests (Best Practice)

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "john@example.com", true},
        {"valid with subdomain", "john@mail.example.com", true},
        {"missing @", "johnexample.com", false},
        {"missing domain", "john@", false},
        {"empty string", "", false},
        {"spaces", "john @example.com", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ValidateEmail(tt.email)
            if got != tt.want {
                t.Errorf("ValidateEmail(%q) = %v, want %v", 
                    tt.email, got, tt.want)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Test Assertions Helper

Create testutils/assertions.go:

package testutils

import "testing"

func AssertEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func AssertNotEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if got == want {
        t.Errorf("got %v, should not equal %v", got, want)
    }
}

func AssertNil(t *testing.T, got interface{}) {
    t.Helper()
    if got != nil {
        t.Errorf("expected nil, got %v", got)
    }
}

func AssertNotNil(t *testing.T, got interface{}) {
    t.Helper()
    if got == nil {
        t.Error("expected not nil, got nil")
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Testing HTTP Handlers (1 hour)

HTTP Test Basics

package handlers

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHealthCheck(t *testing.T) {
    // Create request
    req := httptest.NewRequest(http.MethodGet, "/health", nil)

    // Create response recorder
    w := httptest.NewRecorder()

    // Call handler
    healthCheckHandler(w, req)

    // Check status code
    if w.Code != http.StatusOK {
        t.Errorf("Expected status 200, got %d", w.Code)
    }

    // Check response body
    var response map[string]string
    json.NewDecoder(w.Body).Decode(&response)

    if response["status"] != "healthy" {
        t.Errorf("Expected healthy status, got %s", response["status"])
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing JSON APIs

func TestRegisterUser(t *testing.T) {
    // Setup test database
    db := setupTestDB(t)
    defer teardownTestDB(db)

    handler := NewAuthHandler(repository.NewUserRepository(db))

    // Prepare request body
    reqBody := map[string]string{
        "name":     "John Doe",
        "email":    "john@example.com",
        "password": "password123",
    }
    bodyBytes, _ := json.Marshal(reqBody)

    // Create request
    req := httptest.NewRequest(
        http.MethodPost,
        "/api/v1/register",
        bytes.NewBuffer(bodyBytes),
    )
    req.Header.Set("Content-Type", "application/json")

    // Create response recorder
    w := httptest.NewRecorder()

    // Call handler
    handler.Register(w, req)

    // Check status
    if w.Code != http.StatusCreated {
        t.Fatalf("Expected status 201, got %d", w.Code)
    }

    // Parse response
    var response struct {
        Token string      `json:"token"`
        User  models.User `json:"user"`
    }
    json.NewDecoder(w.Body).Decode(&response)

    // Assertions
    if response.Token == "" {
        t.Error("Expected token in response")
    }
    if response.User.Email != "john@example.com" {
        t.Errorf("Expected email john@example.com, got %s", response.User.Email)
    }
}
Enter fullscreen mode Exit fullscreen mode

Test Database Setup

package handlers

import (
    "testing"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "your-project/models"
)

func setupTestDB(t *testing.T) *gorm.DB {
    // Use in-memory SQLite for tests
    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        t.Fatal("Failed to connect to test database:", err)
    }

    // Auto migrate
    err = db.AutoMigrate(&models.User{}, &models.Post{})
    if err != nil {
        t.Fatal("Failed to migrate test database:", err)
    }

    return db
}

func teardownTestDB(db *gorm.DB) {
    sqlDB, _ := db.DB()
    sqlDB.Close()
}

// Helper to create test user
func createTestUser(t *testing.T, db *gorm.DB) *models.User {
    user := &models.User{
        Name:     "Test User",
        Email:    "test@example.com",
        Password: "hashed_password",
    }

    if err := db.Create(user).Error; err != nil {
        t.Fatal("Failed to create test user:", err)
    }

    return user
}
Enter fullscreen mode Exit fullscreen mode

Testing Protected Endpoints

func TestGetProfile_Authenticated(t *testing.T) {
    db := setupTestDB(t)
    defer teardownTestDB(db)

    // Create test user
    user := createTestUser(t, db)

    // Generate token
    token, _ := utils.GenerateToken(user.ID, user.Email)

    handler := NewAuthHandler(repository.NewUserRepository(db))

    // Create request with auth header
    req := httptest.NewRequest(http.MethodGet, "/api/v1/profile", nil)
    req.Header.Set("Authorization", "Bearer "+token)

    // Add user to context (normally done by middleware)
    claims := &utils.Claims{UserID: user.ID, Email: user.Email}
    ctx := context.WithValue(req.Context(), middleware.UserContextKey, claims)
    req = req.WithContext(ctx)

    w := httptest.NewRecorder()
    handler.GetProfile(w, req)

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

func TestGetProfile_Unauthenticated(t *testing.T) {
    db := setupTestDB(t)
    defer teardownTestDB(db)

    handler := NewAuthHandler(repository.NewUserRepository(db))

    // Request without auth
    req := httptest.NewRequest(http.MethodGet, "/api/v1/profile", nil)
    w := httptest.NewRecorder()

    handler.GetProfile(w, req)

    if w.Code != http.StatusUnauthorized {
        t.Errorf("Expected status 401, got %d", w.Code)
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Middleware

func TestAuthMiddleware(t *testing.T) {
    // Create a test handler
    nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    // Wrap with auth middleware
    handler := middleware.AuthMiddleware(nextHandler)

    t.Run("Valid Token", func(t *testing.T) {
        token, _ := utils.GenerateToken(1, "test@example.com")

        req := httptest.NewRequest(http.MethodGet, "/test", nil)
        req.Header.Set("Authorization", "Bearer "+token)

        w := httptest.NewRecorder()
        handler.ServeHTTP(w, req)

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

    t.Run("Missing Token", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/test", nil)
        w := httptest.NewRecorder()

        handler.ServeHTTP(w, req)

        if w.Code != http.StatusUnauthorized {
            t.Errorf("Expected status 401, got %d", w.Code)
        }
    })

    t.Run("Invalid Token", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/test", nil)
        req.Header.Set("Authorization", "Bearer invalid-token")

        w := httptest.NewRecorder()
        handler.ServeHTTP(w, req)

        if w.Code != http.StatusUnauthorized {
            t.Errorf("Expected status 401, got %d", w.Code)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Benchmark Tests

func BenchmarkHashPassword(b *testing.B) {
    password := "mySecretPassword123"

    for i := 0; i < b.N; i++ {
        HashPassword(password)
    }
}

func BenchmarkCheckPassword(b *testing.B) {
    password := "mySecretPassword123"
    hash, _ := HashPassword(password)

    for i := 0; i < b.N; i++ {
        CheckPassword(password, hash)
    }
}

// Run benchmarks
// go test -bench=. -benchmem ./utils
Enter fullscreen mode Exit fullscreen mode

3. Building for Production (1 hour)

Building Your Application

# Build for current platform
go build -o blog-api

# Build for Linux
GOOS=linux GOARCH=amd64 go build -o blog-api-linux

# Build for Windows
GOOS=windows GOARCH=amd64 go build -o blog-api.exe

# Build with optimization (smaller binary)
go build -ldflags="-s -w" -o blog-api

# Run the binary
./blog-api
Enter fullscreen mode Exit fullscreen mode

Dockerfile

Create Dockerfile:

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build application
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o blog-api .

# Run stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy binary from builder
COPY --from=builder /app/blog-api .

# Expose port
EXPOSE 8080

# Run
CMD ["./blog-api"]
Enter fullscreen mode Exit fullscreen mode

Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/blog_db?sslmode=disable
      - JWT_SECRET=your-super-secret-key
      - PORT=8080
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=blog_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

Run with Docker

# Build and run
docker-compose up -d

# View logs
docker-compose logs -f api

# Stop
docker-compose down

# Rebuild
docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

.dockerignore

Create .dockerignore:

.env
.git
.gitignore
*.md
*.test
coverage.out
blog-api
blog-api-linux
blog-api.exe
Enter fullscreen mode Exit fullscreen mode

4. Environment Configuration (30 min)

Production-Ready Config

Update config/config.go:

package config

import (
    "fmt"
    "os"
    "strconv"
)

type Config struct {
    // Server
    Port        string
    Environment string // development, staging, production

    // Database
    DatabaseURL string

    // JWT
    JWTSecret     string
    JWTExpiration int // hours

    // CORS
    AllowedOrigins []string

    // Rate Limiting
    RateLimitEnabled bool
    RateLimit        int
}

func Load() *Config {
    return &Config{
        Port:             getEnv("PORT", "8080"),
        Environment:      getEnv("ENVIRONMENT", "development"),
        DatabaseURL:      getEnv("DATABASE_URL", ""),
        JWTSecret:        getEnv("JWT_SECRET", ""),
        JWTExpiration:    getEnvAsInt("JWT_EXPIRATION", 24),
        AllowedOrigins:   getEnvAsSlice("ALLOWED_ORIGINS", []string{"*"}),
        RateLimitEnabled: getEnvAsBool("RATE_LIMIT_ENABLED", true),
        RateLimit:        getEnvAsInt("RATE_LIMIT", 100),
    }
}

func (c *Config) Validate() error {
    if c.DatabaseURL == "" {
        return fmt.Errorf("DATABASE_URL is required")
    }
    if c.JWTSecret == "" {
        return fmt.Errorf("JWT_SECRET is required")
    }
    if c.Environment == "production" && c.JWTSecret == "default-secret" {
        return fmt.Errorf("JWT_SECRET must be changed in production")
    }
    return nil
}

func (c *Config) IsProduction() bool {
    return c.Environment == "production"
}

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getEnvAsInt(key string, defaultValue int) int {
    if value := os.Getenv(key); value != "" {
        if intVal, err := strconv.Atoi(value); err == nil {
            return intVal
        }
    }
    return defaultValue
}

func getEnvAsBool(key string, defaultValue bool) bool {
    if value := os.Getenv(key); value != "" {
        if boolVal, err := strconv.ParseBool(value); err == nil {
            return boolVal
        }
    }
    return defaultValue
}

func getEnvAsSlice(key string, defaultValue []string) []string {
    if value := os.Getenv(key); value != "" {
        return strings.Split(value, ",")
    }
    return defaultValue
}
Enter fullscreen mode Exit fullscreen mode

Multiple Environment Files

Create .env.development:

ENVIRONMENT=development
PORT=8080
DATABASE_URL=postgres://postgres:postgres@localhost:5432/blog_db_dev?sslmode=disable
JWT_SECRET=dev-secret-key
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
Enter fullscreen mode Exit fullscreen mode

Create .env.production:

ENVIRONMENT=production
PORT=8080
DATABASE_URL=postgres://user:password@prod-db:5432/blog_db?sslmode=require
JWT_SECRET=super-secret-production-key-change-this
ALLOWED_ORIGINS=https://yourapp.com,https://www.yourapp.com
RATE_LIMIT_ENABLED=true
RATE_LIMIT=100
Enter fullscreen mode Exit fullscreen mode

5. Deployment Options (1 hour)

Option 1: Deploy to Railway (Easiest)

  1. Create account at https://railway.app
  2. Install Railway CLI:
   npm install -g @railway/cli
   # or
   brew install railway
Enter fullscreen mode Exit fullscreen mode
  1. Login and deploy:
   railway login
   railway init
   railway up
Enter fullscreen mode Exit fullscreen mode
  1. Add PostgreSQL:
   railway add
   # Select PostgreSQL
Enter fullscreen mode Exit fullscreen mode
  1. Set environment variables in Railway dashboard

  2. Railway automatically:

    • Detects Go project
    • Builds your app
    • Provides HTTPS URL
    • Auto-deploys on git push

Option 2: Deploy to Fly.io

  1. Install flyctl:
   # Mac
   brew install flyctl

   # Linux
   curl -L https://fly.io/install.sh | sh
Enter fullscreen mode Exit fullscreen mode
  1. Login:
   fly auth login
Enter fullscreen mode Exit fullscreen mode
  1. Initialize:
   fly launch
Enter fullscreen mode Exit fullscreen mode
  1. Create fly.toml:
   app = "your-blog-api"
   primary_region = "sin"

   [build]

   [http_service]
     internal_port = 8080
     force_https = true
     auto_stop_machines = true
     auto_start_machines = true
     min_machines_running = 0

   [[vm]]
     cpu_kind = "shared"
     cpus = 1
     memory_mb = 256
Enter fullscreen mode Exit fullscreen mode
  1. Add PostgreSQL:
   fly postgres create
   fly postgres attach --app your-blog-api
Enter fullscreen mode Exit fullscreen mode
  1. Set secrets:
   fly secrets set JWT_SECRET=your-secret-key
Enter fullscreen mode Exit fullscreen mode
  1. Deploy:
   fly deploy
Enter fullscreen mode Exit fullscreen mode

Option 3: Deploy to VPS (DigitalOcean/Linode)

  1. Create droplet/VPS

  2. Install Go on server:

   ssh root@your-server-ip

   wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
   tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
   echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
   source ~/.bashrc
Enter fullscreen mode Exit fullscreen mode
  1. Install PostgreSQL:
   sudo apt update
   sudo apt install postgresql postgresql-contrib
   sudo -u postgres psql
   CREATE DATABASE blog_db;
   CREATE USER blog_user WITH PASSWORD 'secure_password';
   GRANT ALL PRIVILEGES ON DATABASE blog_db TO blog_user;
   \q
Enter fullscreen mode Exit fullscreen mode
  1. Clone and build:
   git clone your-repo
   cd your-blog-api
   go build -o blog-api
Enter fullscreen mode Exit fullscreen mode
  1. Create systemd service /etc/systemd/system/blog-api.service:
   [Unit]
   Description=Blog API
   After=network.target

   [Service]
   Type=simple
   User=root
   WorkingDirectory=/root/blog-api
   ExecStart=/root/blog-api/blog-api
   Restart=on-failure
   Environment="DATABASE_URL=postgres://blog_user:secure_password@localhost:5432/blog_db"
   Environment="JWT_SECRET=your-secret-key"
   Environment="PORT=8080"

   [Install]
   WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode
  1. Start service:
   sudo systemctl daemon-reload
   sudo systemctl enable blog-api
   sudo systemctl start blog-api
   sudo systemctl status blog-api
Enter fullscreen mode Exit fullscreen mode
  1. Setup Nginx reverse proxy /etc/nginx/sites-available/blog-api:
   server {
       listen 80;
       server_name your-domain.com;

       location / {
           proxy_pass http://localhost:8080;
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection 'upgrade';
           proxy_set_header Host $host;
           proxy_cache_bypass $http_upgrade;
       }
   }
Enter fullscreen mode Exit fullscreen mode
  1. Enable site:
   sudo ln -s /etc/nginx/sites-available/blog-api /etc/nginx/sites-enabled/
   sudo nginx -t
   sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode
  1. Setup SSL with Let's Encrypt:
   sudo apt install certbot python3-certbot-nginx
   sudo certbot --nginx -d your-domain.com
Enter fullscreen mode Exit fullscreen mode

Option 4: Deploy with Docker

# Build image
docker build -t blog-api .

# Run container
docker run -d \
  -p 8080:8080 \
  -e DATABASE_URL="postgres://..." \
  -e JWT_SECRET="your-secret" \
  --name blog-api \
  blog-api

# Or use docker-compose
docker-compose -f docker-compose.prod.yml up -d
Enter fullscreen mode Exit fullscreen mode

6. Production Checklist (30 min)

Security Checklist

// ✅ HTTPS only in production
if config.IsProduction() {
    // Redirect HTTP to HTTPS
    go http.ListenAndServe(":80", http.HandlerFunc(redirectToHTTPS))
}

func redirectToHTTPS(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, "https://"+r.Host+r.RequestURI, 
        http.StatusMovedPermanently)
}

// ✅ Secure headers middleware
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Strict-Transport-Security", 
            "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

// ✅ Rate limiting
r.Use(middleware.Throttle(100)) // Chi built-in

// ✅ Request size limit
r.Use(middleware.RequestSize(10 * 1024 * 1024)) // 10MB max

// ✅ Timeout
r.Use(middleware.Timeout(60 * time.Second))
Enter fullscreen mode Exit fullscreen mode

Monitoring & Logging

// Structured logging
import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

logger.Info("Server starting",
    "port", config.Port,
    "environment", config.Environment,
)

logger.Error("Database connection failed",
    "error", err,
)
Enter fullscreen mode Exit fullscreen mode

Health Check Endpoint

func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request) {
    // Check database
    sqlDB, _ := s.db.DB()
    if err := sqlDB.Ping(); err != nil {
        s.sendJSON(w, http.StatusServiceUnavailable, map[string]string{
            "status": "unhealthy",
            "database": "down",
        })
        return
    }

    s.sendJSON(w, http.StatusOK, map[string]string{
        "status": "healthy",
        "database": "up",
        "version": "1.0.0",
    })
}
Enter fullscreen mode Exit fullscreen mode

Graceful Shutdown

func main() {
    // ... setup code ...

    server := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }

    // Start server in goroutine
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal("Server failed:", err)
        }
    }()

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

    log.Println("Shutting down server...")

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exited")
}
Enter fullscreen mode Exit fullscreen mode

Final Project Structure

blog-api/
├── .env
├── .env.production
├── .gitignore
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── README.md
├── config/
│   └── config.go
├── models/
│   ├── user.go
│   ├── user_test.go
│   ├── post.go
│   └── post_test.go
├── handlers/
│   ├── auth.go
│   ├── auth_test.go
│   ├── user.go
│   ├── user_test.go
│   ├── post.go
│   └── post_test.go
├── middleware/
│   ├── auth.go
│   ├── auth_test.go
│   ├── logging.go
│   └── security.go
├── repository/
│   ├── user_repository.go
│   ├── user_repository_test.go
│   ├── post_repository.go
│   └── post_repository_test.go
├── utils/
│   ├── jwt.go
│   ├── jwt_test.go
│   ├── password.go
│   ├── password_test.go
│   ├── validation.go
│   └── validation_test.go
├── routes/
│   └── routes.go
└── testutils/
    ├── database.go
    └── assertions.go
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

Testing

  • Test files end with _test.go
  • Use table-driven tests
  • Test with in-memory SQLite
  • Use httptest for HTTP handlers
  • Aim for >70% coverage

Building

  • go build creates single binary
  • Cross-compile with GOOS/GOARCH
  • Docker for containerized deployment
  • No runtime dependencies needed!

Deployment

  • Railway/Fly.io: Easiest, free tier
  • VPS: Full control, requires setup
  • Docker: Portable, consistent
  • Always use environment variables
  • Enable HTTPS in production

Production

  • Graceful shutdown
  • Health checks
  • Rate limiting
  • Security headers
  • Structured logging
  • Request timeouts

🎉 Congratulations!

You've completed the 7-day Go learning journey!

What you've learned:

  • ✅ Go basics and syntax
  • ✅ Structs, interfaces, and OOP
  • ✅ Error handling
  • ✅ HTTP servers and REST APIs
  • ✅ Chi router and middleware
  • ✅ Database with GORM
  • ✅ JWT authentication
  • ✅ Testing and deployment

You can now:

  • Build production-ready REST APIs
  • Implement secure authentication
  • Work with databases
  • Test your code
  • Deploy to production

Next Steps

  1. Add more features:

    • Pagination with cursor
    • File uploads
    • Email notifications
    • WebSockets
    • GraphQL
  2. Learn advanced topics:

    • Goroutines and channels (concurrency)
    • gRPC
    • Microservices
    • Event-driven architecture
  3. Practice:

    • Build a real project
    • Contribute to open source
    • Read others' Go code
  4. Resources:

You're now a Go developer! 🚀

Top comments (0)