Project Deployment and Optimization

Lesson 28: Project Deployment and Optimization

Real-World Analogy

Imagine you've opened a restaurant. The recipes are written (code is done), but to actually start business, you need:

Core Concepts

Concept Description
Cross-compilation Compiling executables for one platform on another platform
Multi-stage build Docker build separating compilation and runtime environments, reducing image size
Environment variables Configuring program behavior without modifying code
Structured logging Using log/slog to output machine-parseable log formats
Performance profiling Using pprof to identify CPU and memory bottlenecks
Graceful shutdown After receiving a termination signal, waiting for in-progress requests to complete before exiting

Basic Syntax and Usage

1. Cross-Compilation

Go natively supports cross-compilation, just set GOOS and GOARCH environment variables:

BASH
# Compile Linux amd64 version (run on Windows/Mac)
GOOS=linux GOARCH=amd64 go build -o myapp-linux .

# Compile Windows version (run on Linux/Mac)
GOOS=windows GOARCH=amd64 go build -o myapp.exe .

# Compile macOS ARM (M1/M2) version
GOOS=darwin GOARCH=arm64 go build -o myapp-darwin .
💡 Tip: During cross-compilation, CGO is disabled by default. If you depend on C libraries, you need to install cross-compilation toolchains.

2. Docker Multi-Stage Build

DOCKERFILE
# ---- Build Stage ----
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# ---- Runtime Stage ----
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
💡 Tip: The final image only contains the compiled binary and minimal runtime environment, reducing size from 1GB to under 20MB.

3. Environment Variable Configuration

GO
package main

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

func main() {
	// Read environment variables with default values
	port := getEnv("APP_PORT", "8080")
	dbHost := getEnv("DB_HOST", "localhost")
	debug := getEnv("DEBUG", "false")

	fmt.Printf("Startup config: port=%s, db=%s, debug=%s\n", port, dbHost, debug)
}

// getEnv gets an environment variable, returns default value if not set
func getEnv(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}
💡 Tip: For production, it's recommended to use .env files with the godotenv library, but don't commit .env files to version control.

4. Structured Logging (log/slog)

Go 1.21 introduced the standard library log/slog, supporting JSON format output:

GO
package main

import (
	"log/slog"
	"os"
)

func main() {
	// Create JSON format handler
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	// Use structured logging
	logger.Info("Service started",
		"port", 8080,
		"env", "production",
	)

	logger.Error("Database connection failed",
		"host", "db.example.com",
		"error", "connection refused",
	)
}
💡 Tip: slog supports setting a global default logger via slog.SetDefault(), which can be used uniformly across the entire project.

5. Performance Profiling (pprof)

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // Import registers pprof routes
	"time"
)

func main() {
	// Start pprof service on a separate port
	go func() {
		fmt.Println("pprof listening on :6060")
		http.ListenAndServe(":6060", nil)
	}()

	// Your main business logic
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(10 * time.Millisecond)
		fmt.Fprintln(w, "Hello, World!")
	})

	http.ListenAndServe(":8080", nil)
}
💡 Tip: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 can collect 30 seconds of CPU data.

6. Graceful Shutdown

GO
package main

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

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second) // Simulate long-running request
		fmt.Fprintln(w, "Processing complete")
	})

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

	// Start server in a goroutine
	go func() {
		fmt.Println("Server listening on :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			fmt.Printf("Server exited abnormally: %v\n", err)
		}
	}()

	// Wait for interrupt signal
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	fmt.Println("\nReceived shutdown signal, gracefully exiting...")

	// Give in-progress requests up to 10 seconds to complete
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		fmt.Printf("Forced shutdown: %v\n", err)
	} else {
		fmt.Println("Server shut down gracefully")
	}
}
💡 Tip: server.Shutdown() stops accepting new connections and waits for existing connections to finish processing before returning.


Example: Basic Cross-Compilation Script (Difficulty ⭐)

Create a script that compiles binaries for multiple platforms in one go:

GO
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// Target platform list
type Target struct {
	OS   string
	Arch string
}

func main() {
	targets := []Target{
		{"linux", "amd64"},
		{"linux", "arm64"},
		{"darwin", "amd64"},
		{"darwin", "arm64"},
		{"windows", "amd64"},
	}

	outputDir := "dist"
	os.MkdirAll(outputDir, 0755)

	for _, t := range targets {
		outputName := fmt.Sprintf("myapp-%s-%s", t.OS, t.Arch)
		if t.OS == "windows" {
			outputName += ".exe"
		}

		outputPath := filepath.Join(outputDir, outputName)
		fmt.Printf("Compiling: %s/%s -> %s\n", t.OS, t.Arch, outputPath)

		cmd := exec.Command("go", "build", "-o", outputPath, ".")
		cmd.Env = append(os.Environ(),
			"GOOS="+t.OS,
			"GOARCH="+t.Arch,
			"CGO_ENABLED=0",
		)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		if err := cmd.Run(); err != nil {
			fmt.Printf("Compilation failed %s/%s: %v\n", t.OS, t.Arch, err)
		}
	}

	fmt.Println("All compilations complete!")
}
▶ Try it Yourself

Run:

BASH
go run build.go

Output directory structure:

TEXT
dist/
├── myapp-darwin-amd64
├── myapp-darwin-arm64
├── myapp-linux-amd64
├── myapp-linux-arm64
└── myapp-windows-amd64.exe

Example: HTTP Service with Environment Variable Configuration (Difficulty ⭐⭐)

Demonstrates a complete environment variable configuration scheme:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"
)

// Config application configuration struct
type Config struct {
	Port        int    `json:"port"`
	Env         string `json:"env"`
	LogLevel    string `json:"log_level"`
	DBHost      string `json:"db_host"`
	DBPort      int    `json:"db_port"`
	IdleTimeout int    `json:"idle_timeout"` // seconds
}

// LoadConfig loads configuration from environment variables
func LoadConfig() Config {
	return Config{
		Port:        getEnvInt("APP_PORT", 8080),
		Env:         getEnvStr("APP_ENV", "development"),
		LogLevel:    getEnvStr("LOG_LEVEL", "info"),
		DBHost:      getEnvStr("DB_HOST", "localhost"),
		DBPort:      getEnvInt("DB_PORT", 5432),
		IdleTimeout: getEnvInt("IDLE_TIMEOUT", 30),
	}
}

func getEnvStr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func getEnvInt(key string, fallback int) int {
	if v := os.Getenv(key); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			return n
		}
	}
	return fallback
}

func (c Config) String() string {
	data, _ := json.MarshalIndent(c, "", "  ")
	return string(data)
}

func main() {
	cfg := LoadConfig()

	// Configure log level based on environment
	var level slog.Level
	switch cfg.LogLevel {
	case "debug":
		level = slog.LevelDebug
	case "warn":
		level = slog.LevelWarn
	case "error":
		level = slog.LevelError
	default:
		level = slog.LevelInfo
	}

	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: level,
	}))
	slog.SetDefault(logger)

	logger.Info("Application config loaded", "config", cfg.String())

	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"status": "ok",
			"env":    cfg.Env,
		})
	})

	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("Request received", "method", r.Method, "path", r.URL.Path)
		fmt.Fprintf(w, "Hello from %s environment!", cfg.Env)
	})

	server := &http.Server{
		Addr:        fmt.Sprintf(":%d", cfg.Port),
		Handler:     mux,
		IdleTimeout: time.Duration(cfg.IdleTimeout) * time.Second,
	}

	go func() {
		logger.Info("Service started", "port", cfg.Port, "env", cfg.Env)
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			logger.Error("Service error", "error", err)
			os.Exit(1)
		}
	}()

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

	logger.Info("Shutting down service...")
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	server.Shutdown(ctx)
	logger.Info("Service shut down")
}
▶ Try it Yourself

Start:

BASH
# Use default configuration
go run main.go

# Custom configuration
APP_PORT=3000 APP_ENV=production LOG_LEVEL=debug go run main.go

Test requests:

BASH
# Health check
curl http://localhost:8080/health

# Homepage
curl http://localhost:8080/

Example: Complete Docker Deployment Solution (Difficulty ⭐⭐⭐)

A complete deployment solution including Dockerfile, docker-compose, and Makefile:

Project structure:

TEXT
myproject/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── Makefile
└── .env.example
▶ Try it Yourself

main.go:

GO
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"syscall"
	"time"
)

type AppConfig struct {
	Port      string
	Env       string
	LogLevel  string
	PprofPort string
}

func loadConfig() AppConfig {
	return AppConfig{
		Port:      getEnv("APP_PORT", "8080"),
		Env:       getEnv("APP_ENV", "production"),
		LogLevel:  getEnv("LOG_LEVEL", "info"),
		PprofPort: getEnv("PPROF_PORT", "6060"),
	}
}

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

func setupLogger(level string) *slog.Logger {
	var lvl slog.Level
	switch level {
	case "debug":
		lvl = slog.LevelDebug
	case "warn":
		lvl = slog.LevelWarn
	case "error":
		lvl = slog.LevelError
	default:
		lvl = slog.LevelInfo
	}
	return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
}

func main() {
	cfg := loadConfig()
	logger := setupLogger(cfg.LogLevel)
	slog.SetDefault(logger)

	// Start pprof (only in non-production or when explicitly enabled)
	if cfg.Env != "production" || os.Getenv("ENABLE_PPROF") == "true" {
		go func() {
			logger.Info("pprof service started", "port", cfg.PprofPort)
			http.ListenAndServe(":"+cfg.PprofPort, nil)
		}()
	}

	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"status":    "ok",
			"env":       cfg.Env,
			"goroutine": runtime.NumGoroutine(),
			"uptime":    time.Since(startTime).String(),
		})
	})

	mux.HandleFunc("GET /api/info", func(w http.ResponseWriter, r *http.Request) {
		logger.Info("info request", "remote", r.RemoteAddr)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"app":      "myproject",
			"version":  version,
			"go":       runtime.Version(),
			"env":      cfg.Env,
		})
	})

	mux.HandleFunc("POST /api/data", func(w http.ResponseWriter, r *http.Request) {
		// Simulate data processing
		time.Sleep(100 * time.Millisecond)
		logger.Info("Data processing complete", "content_length", r.ContentLength)
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(map[string]string{"result": "created"})
	})

	server := &http.Server{
		Addr:         ":" + cfg.Port,
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// Graceful shutdown logic
	done := make(chan struct{})
	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
		sig := <-quit
		logger.Info("Received shutdown signal", "signal", sig)

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		// Stop pprof
		// Note: standard library pprof has no Shutdown, relies on process exit

		if err := server.Shutdown(ctx); err != nil {
			logger.Error("Server shutdown failed", "error", err)
		}
		close(done)
	}()

	logger.Info("Service started",
		"port", cfg.Port,
		"env", cfg.Env,
		"version", version,
	)

	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		logger.Error("Server exited abnormally", "error", err)
		os.Exit(1)
	}

	<-done
	logger.Info("Service fully shut down")
}

var (
	startTime = time.Now()
	version   = "1.0.0"
)

Dockerfile:

DOCKERFILE
# ========== Build Stage ==========
FROM golang:1.22-alpine AS builder

# Install git (go mod may need it)
RUN apk add --no-cache git

WORKDIR /build

# Copy dependency files first, leveraging Docker cache layers
COPY go.mod go.sum ./
RUN go mod download

# Copy source and compile
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o /app/server .

# ========== Runtime Stage ==========
FROM alpine:3.19

# Install CA certificates (for HTTPS requests) and timezone data
RUN apk --no-cache add ca-certificates tzdata

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# Copy binary from build stage
COPY --from=builder /app/server .
COPY --from=builder /build/.env.example .env.example

# Run as non-root user
USER appuser

EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

CMD ["./server"]

docker-compose.yml:

YAML
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - APP_ENV=production
      - LOG_LEVEL=info
      - APP_PORT=8080
      - ENABLE_PPROF=false
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Makefile:

MAKEFILE
APP_NAME := myproject
VERSION  := 1.0.0

.PHONY: build run clean docker docker-run docker-stop

# Local build
build:
	CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME) .

# Local run
run: build
	./bin/$(APP_NAME)

# Clean
clean:
	rm -rf bin/

# Build Docker image
docker:
	docker build -t $(APP_NAME):$(VERSION) .

# Start Docker container
docker-run:
	docker-compose up -d

# Stop Docker container
docker-stop:
	docker-compose down

# Cross-compile all platforms
cross-build:
	GOOS=linux   GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-linux-amd64 .
	GOOS=darwin  GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-darwin-amd64 .
	GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/$(APP_NAME)-windows-amd64.exe .

.env.example:

TEXT
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
ENABLE_PPROF=false
PPROF_PORT=6060

Build and deploy workflow:

BASH
# 1. Local test
make run

# 2. Build Docker image
make docker

# 3. Start container
make docker-run

# 4. Test service
curl http://localhost:8080/health
curl http://localhost:8080/api/info

# 5. View logs
docker-compose logs -f

# 6. Stop service
make docker-stop

Scenario 1: Production Logging and Monitoring

In a real web service, logs need to be collected into a centralized logging system:

GO
package main

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

// RequestLogger middleware: records detailed information for each request
func RequestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()

		// Wrap ResponseWriter to capture status code
		rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
		next.ServeHTTP(rw, r)

		slog.Info("HTTP request",
			"method", r.Method,
			"path", r.URL.Path,
			"status", rw.statusCode,
			"duration_ms", time.Since(start).Milliseconds(),
			"remote_addr", r.RemoteAddr,
			"user_agent", r.UserAgent(),
		)
	})
}

type responseWriter struct {
	http.ResponseWriter
	statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
	rw.statusCode = code
	rw.ResponseWriter.WriteHeader(code)
}

// RecoveryMiddleware catches panics and logs them
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				slog.Error("Request processing panic",
					"error", err,
					"path", r.URL.Path,
					"method", r.Method,
				)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	// Production uses JSON format for easy log collection system parsing
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	mux := http.NewServeMux()
	mux.HandleFunc("GET /api/users", func(w http.ResponseWriter, r *http.Request) {
		// Simulate business logic
		ctx := r.Context()
		slog.DebugContext(ctx, "Querying user list", "page", 1)
		// ...
	})

	// Middleware chain: Recovery -> Logger -> Router
	handler := RecoveryMiddleware(RequestLogger(mux))

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

	slog.Info("Service started", "addr", server.Addr)
	server.ListenAndServe()
}

After running, each request outputs structured logs:

JSON
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"HTTP request","method":"GET","path":"/api/users","status":200,"duration_ms":5,"remote_addr":"127.0.0.1","user_agent":"curl/7.68.0"}

Scenario 2: Using pprof to Locate Performance Bottlenecks

When service response slows down, use pprof for diagnosis:

GO
package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"runtime"
	"strings"
	"time"
)

func main() {
	// Main service
	mux := http.NewServeMux()

	// Simulate an endpoint with performance issues
	mux.HandleFunc("GET /api/search", func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query().Get("q")
		result := slowSearch(query) // Intentionally slow function
		fmt.Fprint(w, result)
	})

	// pprof registers on default http.DefaultServeMux, needs separate port
	go func() {
		fmt.Println("pprof: http://localhost:6060/debug/pprof/")
		http.ListenAndServe(":6060", nil)
	}()

	fmt.Println("Main service: http://localhost:8080")
	http.ListenAndServe(":8080", mux)
}

// slowSearch simulates a CPU-intensive function with memory allocation issues
func slowSearch(query string) string {
	var results []string
	// Intentionally allocate memory repeatedly in a loop
	for i := 0; i < 100000; i++ {
		data := fmt.Sprintf("item_%d_%s", i, query) // Allocate new string each iteration
		if strings.Contains(data, query) {
			results = append(results, data)
		}
	}
	// Simulate additional delay
	time.Sleep(50 * time.Millisecond)
	return fmt.Sprintf("Found %d results", len(results))
}

Using pprof for analysis:

BASH
# 1. Install pprof tool (if not already installed)
go install github.com/google/pprof@latest

# 2. Collect 30 seconds of CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# 3. View in pprof interactive interface
(pprof) top          # View most CPU-intensive functions
(pprof) list slowSearch  # View specific code line costs
(pprof) web          # Generate visualization (requires graphviz)

# 4. View memory allocation
go tool pprof http://localhost:6060/debug/pprof/heap

# 5. View goroutine count (detect leaks)
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 6. Use browser to view all metrics
# Open http://localhost:6060/debug/pprof/

❓ FAQ

Q1: What to do when encountering cgo: exec gcc: exec: "gcc": not found during cross-compilation?

This is because the code uses CGO. If you don't need C library dependencies, set CGO_ENABLED=0:

BASH
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp .

If you do need CGO, you need to install cross-compilation toolchains for the target platform (like gcc-aarch64-linux-gnu).

Q2: Docker image is too large, how to further reduce size?

Use scratch as the base image (no system tools at all):

DOCKERFILE
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
CMD ["/server"]

Combined with removing debug info during compilation: go build -ldflags="-s -w". -s removes the symbol table, -w removes DWARF debug info, typically reducing size by 20-30%.

Q3: How does log/slog compare to third-party logging libraries (like zap, zerolog)?

log/slog's advantage is being a standard library with no additional dependencies, and its performance is sufficient for most scenarios. If your application has extremely high log volume (tens of thousands per second), zap's zero-allocation design will be faster. For new projects, start with slog; consider switching only if you encounter performance bottlenecks.

Q4: During graceful shutdown, how to ensure WebSocket connections are handled correctly?

http.Server.Shutdown() only handles HTTP requests. For long-lived connections (WebSocket), you need to manage the connection pool yourself:

GO
var connections sync.Map // Store active connections

// Register when new connection arrives
connections.Store(conn, true)

// On shutdown, iterate all connections and send close frames
connections.Range(func(key, value any) bool {
    wsConn := key.(*websocket.Conn)
    wsConn.WriteMessage(websocket.CloseMessage, closeMsg)
    wsConn.Close()
    return true
})

📖 Summary

In this lesson we learned the complete workflow from development to deployment for Go projects:

After mastering these skills, you can deploy Go projects to production environments safely and efficiently.


📝 Exercises

Exercise 1: Write a Cross-Compilation Script

Write a Go program that automatically compiles the current project for linux/amd64, darwin/arm64, windows/amd64 platforms, with output filenames containing version and platform information.

Exercise 2: Add Request Logging Middleware

Add logging middleware to an HTTP service that records for each request: method, path, status code, response time, client IP. Use log/slog for JSON format output.

Exercise 3: Docker Deploy Your Project

Write a complete Dockerfile and docker-compose.yml for any project you've written in previous lessons (e.g., Todo API), with the following requirements:


Next Lesson

After completing this lesson, you've mastered the core skills for Go project deployment. Next we'll enter the practical project phase: Lesson 29: Practical Project (Part 1)

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏