Comprehensive Project (Part 2)

Lesson 30: Comprehensive Project (Part 2)

Real-World Analogy

In the previous lesson we completed the requirements analysis, directory structure design, data models, and core business logic for the "Task Management System TaskFlow" — like building a house with the foundation laid and frame erected. In this lesson, we'll complete the finishing and delivery:

After this lesson, you'll have a deployable, testable, documented complete Go project.


Project Review

In the previous lesson we built TaskFlow's core skeleton. This lesson continues to refine it. If you haven't read Lesson 29: Comprehensive Project (Part 1), it's recommended to complete the first half first.

Final project structure:

TEXT
taskflow/
├── cmd/
│   └── server/
│       └── main.go            # Program entry point
├── internal/
│   ├── model/
│   │   ├── task.go            # Data model
│   │   └── task_test.go       # Model tests
│   ├── store/
│   │   ├── store.go           # Storage interface
│   │   ├── memory.go          # In-memory implementation (previous lesson)
│   │   └── sqlite.go          # SQLite implementation (this lesson)
│   ├── service/
│   │   ├── task.go            # Business logic
│   │   └── task_test.go       # Unit tests
│   ├── handler/
│   │   ├── task.go            # HTTP handler
│   │   └── task_test.go       # Handler tests
│   └── middleware/
│       ├── auth.go            # Authentication middleware
│       ├── logging.go         # Logging middleware
│       └── cors.go            # CORS middleware
├── docs/
│   └── api.md                 # API documentation
├── Dockerfile                 # Container packaging
├── docker-compose.yml         # Orchestration config
├── go.mod
├── go.sum
└── README.md

1. REST API Layer Completion

1.1 Route Design

API routes designed based on RESTful conventions:

Method Path Description
GET /api/tasks Get task list
GET /api/tasks/{id} Get single task
POST /api/tasks Create task
PUT /api/tasks/{id} Update task
DELETE /api/tasks/{id} Delete task
GET /health Health check

1.2 Complete Handler Implementation

GO
// internal/handler/task.go
package handler

import (
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"strings"

	"taskflow/internal/model"
	"taskflow/internal/service"
)

// TaskHandler task HTTP handler
type TaskHandler struct {
	svc *service.TaskService
}

// NewTaskHandler creates a handler instance
func NewTaskHandler(svc *service.TaskService) *TaskHandler {
	return &TaskHandler{svc: svc}
}

// ErrorResponse unified error response format
type ErrorResponse struct {
	Error   string `json:"error"`
	Code    int    `json:"code"`
	Message string `json:"message,omitempty"`
}

// SuccessResponse unified success response format
type SuccessResponse struct {
	Data    interface{} `json:"data"`
	Message string      `json:"message,omitempty"`
}

// writeJSON writes a JSON response
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(data)
}

// writeError writes an error response
func writeError(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, ErrorResponse{
		Error: msg,
		Code:  status,
	})
}

// extractID extracts the ID parameter from the URL path
func extractID(r *http.Request) (int, error) {
	// Path format: /api/tasks/{id}
	parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
	if len(parts) < 3 {
		return 0, errors.New("missing ID parameter")
	}
	return strconv.Atoi(parts[2])
}

// ListTasks handles GET /api/tasks
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
	// Parse query parameters
	query := r.URL.Query()
	filter := service.TaskFilter{
		Status: model.TaskStatus(query.Get("status")),
	}

	// Parse pagination parameters
	page, _ := strconv.Atoi(query.Get("page"))
	pageSize, _ := strconv.Atoi(query.Get("page_size"))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 || pageSize > 100 {
		pageSize = 20
	}

	tasks, total, err := h.svc.ListTasks(r.Context(), filter, page, pageSize)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "Query failed: "+err.Error())
		return
	}

	writeJSON(w, http.StatusOK, map[string]interface{}{
		"data":      tasks,
		"total":     total,
		"page":      page,
		"page_size": pageSize,
	})
}

// GetTask handles GET /api/tasks/{id}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
	id, err := extractID(r)
	if err != nil {
		writeError(w, http.StatusBadRequest, "Invalid task ID")
		return
	}

	task, err := h.svc.GetTask(r.Context(), id)
	if err != nil {
		if errors.Is(err, service.ErrTaskNotFound) {
			writeError(w, http.StatusNotFound, "Task not found")
			return
		}
		writeError(w, http.StatusInternalServerError, "Query failed")
		return
	}

	writeJSON(w, http.StatusOK, SuccessResponse{Data: task})
}

// CreateTask handles POST /api/tasks
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
	var req model.CreateTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "Invalid request format")
		return
	}

	task, err := h.svc.CreateTask(r.Context(), &req)
	if err != nil {
		if errors.Is(err, service.ErrValidation) {
			writeError(w, http.StatusBadRequest, err.Error())
			return
		}
		writeError(w, http.StatusInternalServerError, "Creation failed")
		return
	}

	writeJSON(w, http.StatusCreated, SuccessResponse{
		Data:    task,
		Message: "Task created successfully",
	})
}

// UpdateTask handles PUT /api/tasks/{id}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
	id, err := extractID(r)
	if err != nil {
		writeError(w, http.StatusBadRequest, "Invalid task ID")
		return
	}

	var req model.UpdateTaskRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "Invalid request format")
		return
	}

	task, err := h.svc.UpdateTask(r.Context(), id, &req)
	if err != nil {
		switch {
		case errors.Is(err, service.ErrTaskNotFound):
			writeError(w, http.StatusNotFound, "Task not found")
		case errors.Is(err, service.ErrValidation):
			writeError(w, http.StatusBadRequest, err.Error())
		default:
			writeError(w, http.StatusInternalServerError, "Update failed")
		}
		return
	}

	writeJSON(w, http.StatusOK, SuccessResponse{
		Data:    task,
		Message: "Task updated successfully",
	})
}

// DeleteTask handles DELETE /api/tasks/{id}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
	id, err := extractID(r)
	if err != nil {
		writeError(w, http.StatusBadRequest, "Invalid task ID")
		return
	}

	if err := h.svc.DeleteTask(r.Context(), id); err != nil {
		if errors.Is(err, service.ErrTaskNotFound) {
			writeError(w, http.StatusNotFound, "Task not found")
			return
		}
		writeError(w, http.StatusInternalServerError, "Deletion failed")
		return
	}

	writeJSON(w, http.StatusOK, SuccessResponse{Message: "Task deleted successfully"})
}

// HealthCheck health check endpoint
func (h *TaskHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
	writeJSON(w, http.StatusOK, map[string]string{
		"status": "ok",
		"service": "taskflow",
	})
}

1.3 Route Registration and Server Startup

GO
// cmd/server/main.go
package main

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

	"taskflow/internal/handler"
	"taskflow/internal/middleware"
	"taskflow/internal/service"
	"taskflow/internal/store"
)

func main() {
	// Initialize logging
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	// Initialize storage layer (default to SQLite)
	dbPath := getEnv("DB_PATH", "taskflow.db")
	st, err := store.NewSQLiteStore(dbPath)
	if err != nil {
		slog.Error("Database initialization failed", "error", err)
		os.Exit(1)
	}
	defer st.Close()

	// Initialize business layer
	svc := service.NewTaskService(st)

	// Initialize handlers
	h := handler.NewTaskHandler(svc)

	// Register routes
	mux := http.NewServeMux()

	// Health check
	mux.HandleFunc("GET /health", h.HealthCheck)

	// API routes
	mux.HandleFunc("GET /api/tasks", h.ListTasks)
	mux.HandleFunc("POST /api/tasks", h.CreateTask)
	mux.HandleFunc("GET /api/tasks/{id}", h.GetTask)
	mux.HandleFunc("PUT /api/tasks/{id}", h.UpdateTask)
	mux.HandleFunc("DELETE /api/tasks/{id}", h.DeleteTask)

	// Assemble middleware chain: CORS → Logging → Auth → Business handler
	apiKey := getEnv("API_KEY", "dev-secret-key")
	handlerChain := middleware.Chain(
		mux,
		middleware.CORS(),
		middleware.Logging(),
		middleware.Auth(apiKey),
	)

	// Create HTTP server
	addr := ":" + getEnv("PORT", "8080")
	srv := &http.Server{
		Addr:         addr,
		Handler:      handlerChain,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	// Start server (non-blocking)
	go func() {
		slog.Info("TaskFlow service started", "addr", addr)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			slog.Error("Server exited abnormally", "error", err)
			os.Exit(1)
		}
	}()

	// Graceful shutdown: listen for system signals
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	slog.Info("Received shutdown signal, gracefully shutting down...")

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

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

	slog.Info("TaskFlow service stopped")
}

// getEnv gets an environment variable with default value support
func getEnv(key, defaultVal string) string {
	if val := os.Getenv(key); val != "" {
		return val
	}
	return defaultVal
}
💡 Go 1.22+ Route Enhancement: Starting from Go 1.22, net/http supports method matching ("GET /path") and path parameters ({id}), allowing you to build RESTful APIs without third-party routing libraries.


2. Middleware Integration

2.1 Middleware Chain Pattern

Middleware are "interceptors" in the HTTP request processing pipeline that can execute common logic before and after requests reach the handler:

TEXT
Request → [CORS] → [Logging] → [Auth] → [Handler] → Response
           ↓         ↓          ↓
       Cross-origin  Record    Verify
       control       duration  identity

2.2 Middleware Infrastructure

GO
// internal/middleware/middleware.go
package middleware

import "net/http"

// Middleware type definition
type Middleware func(http.Handler) http.Handler

// Chain combines multiple middleware into a chain, first passed = first executed
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
	// Wrap from back to front, ensuring execution order is left to right
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

2.3 CORS Middleware

GO
// internal/middleware/cors.go
package middleware

import "net/http"

// CORS handles Cross-Origin Resource Sharing
func CORS() Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Set CORS response headers
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
			w.Header().Set("Access-Control-Max-Age", "86400") // Preflight cache 24 hours

			// Preflight request returns directly
			if r.Method == http.MethodOptions {
				w.WriteHeader(http.StatusNoContent)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

2.4 Logging Middleware

GO
// internal/middleware/logging.go
package middleware

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

// responseWriter wraps ResponseWriter to capture status code
type responseWriter struct {
	http.ResponseWriter
	statusCode int
	written    int64
}

func newResponseWriter(w http.ResponseWriter) *responseWriter {
	return &responseWriter{
		ResponseWriter: w,
		statusCode:     http.StatusOK,
	}
}

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

func (rw *responseWriter) Write(b []byte) (int, error) {
	n, err := rw.ResponseWriter.Write(b)
	rw.written += int64(n)
	return n, err
}

// Logging records processing logs for each request
func Logging() Middleware {
	return func(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 := newResponseWriter(w)

			// Call next handler
			next.ServeHTTP(rw, r)

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

2.5 Authentication Middleware

GO
// internal/middleware/auth.go
package middleware

import (
	"net/http"
	"strings"
)

// Auth validates API key
func Auth(apiKey string) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Health check endpoint doesn't need authentication
			if r.URL.Path == "/health" {
				next.ServeHTTP(w, r)
				return
			}

			// Extract token from request header
			authHeader := r.Header.Get("Authorization")
			if authHeader == "" {
				w.Header().Set("WWW-Authenticate", `Bearer realm="taskflow"`)
				http.Error(w, `{"error":"Missing authentication","code":401}`, http.StatusUnauthorized)
				return
			}

			// Validate Bearer token format
			parts := strings.SplitN(authHeader, " ", 2)
			if len(parts) != 2 || parts[0] != "Bearer" {
				http.Error(w, `{"error":"Invalid auth format, should be Bearer <token>","code":401}`, http.StatusUnauthorized)
				return
			}

			// Validate token value
			if parts[1] != apiKey {
				http.Error(w, `{"error":"Authentication failed: invalid API key","code":403}`, http.StatusForbidden)
				return
			}

			// Authentication passed, continue processing
			next.ServeHTTP(w, r)
		})
	}
}

3. Database Integration (SQLite)

3.1 Why SQLite?

Feature SQLite MySQL/PostgreSQL
Installation Zero config, single file Requires separate service
Use case Small-medium projects, embedded Large production environments
Concurrency Read concurrent, write serial High concurrency
Migration Self-implemented needed Rich tooling
💡 For learning projects and small-to-medium applications, SQLite is the best starting choice. When traffic grows, you can seamlessly switch to PostgreSQL.

3.2 SQLite Storage Implementation

GO
// internal/store/sqlite.go
package store

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	_ "github.com/mattn/go-sqlite3" // SQLite driver, anonymous import triggers initialization

	"taskflow/internal/model"
)

// SQLiteStore SQLite storage implementation
type SQLiteStore struct {
	db *sql.DB
}

// NewSQLiteStore creates and initializes SQLite storage
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
	db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
	if err != nil {
		return nil, fmt.Errorf("failed to open database: %w", err)
	}

	// Verify connection
	if err := db.Ping(); err != nil {
		return nil, fmt.Errorf("database connection failed: %w", err)
	}

	// Configure connection pool
	db.SetMaxOpenConns(1) // SQLite single write, limit concurrent writes
	db.SetMaxIdleConns(1)
	db.SetConnMaxLifetime(0) // Connections don't expire

	// Execute table migration
	if err := migrate(db); err != nil {
		return nil, fmt.Errorf("database migration failed: %w", err)
	}

	return &SQLiteStore{db: db}, nil
}

// migrate executes database migration
func migrate(db *sql.DB) error {
	query := `
	CREATE TABLE IF NOT EXISTS tasks (
		id          INTEGER PRIMARY KEY AUTOINCREMENT,
		title       TEXT    NOT NULL,
		description TEXT    NOT NULL DEFAULT '',
		status      TEXT    NOT NULL DEFAULT 'pending',
		priority    TEXT    NOT NULL DEFAULT 'medium',
		due_date    DATETIME,
		created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
		updated_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
	);

	CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
	CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
	CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
	`
	_, err := db.Exec(query)
	return err
}

// Close closes the database connection
func (s *SQLiteStore) Close() error {
	return s.db.Close()
}

// Create creates a task
func (s *SQLiteStore) Create(ctx context.Context, task *model.Task) error {
	query := `
		INSERT INTO tasks (title, description, status, priority, due_date, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?)
	`
	now := time.Now()
	result, err := s.db.ExecContext(ctx, query,
		task.Title, task.Description, task.Status, task.Priority,
		task.DueDate, now, now,
	)
	if err != nil {
		return fmt.Errorf("failed to insert task: %w", err)
	}

	id, err := result.LastInsertId()
	if err != nil {
		return fmt.Errorf("failed to get insert ID: %w", err)
	}

	task.ID = int(id)
	task.CreatedAt = now
	task.UpdatedAt = now
	return nil
}

// GetByID queries a task by ID
func (s *SQLiteStore) GetByID(ctx context.Context, id int) (*model.Task, error) {
	query := `
		SELECT id, title, description, status, priority, due_date, created_at, updated_at
		FROM tasks WHERE id = ?
	`
	var task model.Task
	var dueDate sql.NullTime

	err := s.db.QueryRowContext(ctx, query, id).Scan(
		&task.ID, &task.Title, &task.Description,
		&task.Status, &task.Priority, &dueDate,
		&task.CreatedAt, &task.UpdatedAt,
	)
	if err == sql.ErrNoRows {
		return nil, nil // Not found returns nil
	}
	if err != nil {
		return nil, fmt.Errorf("failed to query task: %w", err)
	}

	if dueDate.Valid {
		task.DueDate = &dueDate.Time
	}
	return &task, nil
}

// List queries task list (supports filtering and pagination)
func (s *SQLiteStore) List(ctx context.Context, filter TaskFilter, page, pageSize int) ([]model.Task, int, error) {
	// Build query conditions
	where := "1=1"
	args := []interface{}{}

	if filter.Status != "" {
		where += " AND status = ?"
		args = append(args, filter.Status)
	}
	if filter.Priority != "" {
		where += " AND priority = ?"
		args = append(args, filter.Priority)
	}

	// Query total count
	countQuery := fmt.Sprintf("SELECT COUNT(*) FROM tasks WHERE %s", where)
	var total int
	if err := s.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
		return nil, 0, fmt.Errorf("failed to query total: %w", err)
	}

	// Paginated query
	offset := (page - 1) * pageSize
	query := fmt.Sprintf(`
		SELECT id, title, description, status, priority, due_date, created_at, updated_at
		FROM tasks WHERE %s
		ORDER BY created_at DESC
		LIMIT ? OFFSET ?
	`, where)
	args = append(args, pageSize, offset)

	rows, err := s.db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, 0, fmt.Errorf("failed to query list: %w", err)
	}
	defer rows.Close()

	var tasks []model.Task
	for rows.Next() {
		var task model.Task
		var dueDate sql.NullTime
		if err := rows.Scan(
			&task.ID, &task.Title, &task.Description,
			&task.Status, &task.Priority, &dueDate,
			&task.CreatedAt, &task.UpdatedAt,
		); err != nil {
			return nil, 0, fmt.Errorf("failed to scan row: %w", err)
		}
		if dueDate.Valid {
			task.DueDate = &dueDate.Time
		}
		tasks = append(tasks, task)
	}

	return tasks, total, nil
}

// Update updates a task
func (s *SQLiteStore) Update(ctx context.Context, task *model.Task) error {
	query := `
		UPDATE tasks
		SET title = ?, description = ?, status = ?, priority = ?, due_date = ?, updated_at = ?
		WHERE id = ?
	`
	result, err := s.db.ExecContext(ctx, query,
		task.Title, task.Description, task.Status, task.Priority,
		task.DueDate, time.Now(), task.ID,
	)
	if err != nil {
		return fmt.Errorf("failed to update task: %w", err)
	}

	rows, _ := result.RowsAffected()
	if rows == 0 {
		return fmt.Errorf("task not found")
	}
	return nil
}

// Delete deletes a task
func (s *SQLiteStore) Delete(ctx context.Context, id int) error {
	query := "DELETE FROM tasks WHERE id = ?"
	result, err := s.db.ExecContext(ctx, query, id)
	if err != nil {
		return fmt.Errorf("failed to delete task: %w", err)
	}

	rows, _ := result.RowsAffected()
	if rows == 0 {
		return fmt.Errorf("task not found")
	}
	return nil
}

// TaskFilter task filter conditions
type TaskFilter struct {
	Status   model.TaskStatus
	Priority model.TaskPriority
}

3.3 Unified Storage Interface

GO
// internal/store/store.go
package store

import (
	"context"

	"taskflow/internal/model"
)

// TaskStore defines the task storage interface
// All storage implementations (in-memory, SQLite, PostgreSQL) must implement this interface
type TaskStore interface {
	Create(ctx context.Context, task *model.Task) error
	GetByID(ctx context.Context, id int) (*model.Task, error)
	List(ctx context.Context, filter TaskFilter, page, pageSize int) ([]model.Task, int, error)
	Update(ctx context.Context, task *model.Task) error
	Delete(ctx context.Context, id int) error
	Close() error
}
💡 Dependency Injection: The business layer only depends on the TaskStore interface, not caring whether the underlying storage is SQLite or in-memory. Inject mock for testing, real implementation for production.


4. Integration Testing

4.1 Handler Integration Tests

Integration tests verify whether multiple components work together correctly, using httptest to simulate real HTTP requests:

GO
// internal/handler/task_test.go
package handler_test

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

	"taskflow/internal/handler"
	"taskflow/internal/middleware"
	"taskflow/internal/model"
	"taskflow/internal/service"
	"taskflow/internal/store"
)

// setupTestServer creates a test server
func setupTestServer(t *testing.T) *httptest.Server {
	t.Helper()

	// Use in-memory storage
	memStore := store.NewMemoryStore()
	svc := service.NewTaskService(memStore)
	h := handler.NewTaskHandler(svc)

	// Register routes
	mux := http.NewServeMux()
	mux.HandleFunc("GET /health", h.HealthCheck)
	mux.HandleFunc("GET /api/tasks", h.ListTasks)
	mux.HandleFunc("POST /api/tasks", h.CreateTask)
	mux.HandleFunc("GET /api/tasks/{id}", h.GetTask)
	mux.HandleFunc("PUT /api/tasks/{id}", h.UpdateTask)
	mux.HandleFunc("DELETE /api/tasks/{id}", h.DeleteTask)

	// Add middleware
	apiKey := "test-key"
	handlerChain := middleware.Chain(
		mux,
		middleware.CORS(),
		middleware.Logging(),
		middleware.Auth(apiKey),
	)

	return httptest.NewServer(handlerChain)
}

// TestIntegration_CRUD complete CRUD flow integration test
func TestIntegration_CRUD(t *testing.T) {
	srv := setupTestServer(t)
	defer srv.Close()

	client := srv.Client()
	authHeader := "Bearer test-key"

	// 1. Health check
	t.Run("Health check", func(t *testing.T) {
		resp, err := client.Get(srv.URL + "/health")
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			t.Errorf("Status code = %d, expected 200", resp.StatusCode)
		}
	})

	// 2. Create task
	var createdTask model.Task
	t.Run("Create task", func(t *testing.T) {
		body := `{"title":"Integration test task","description":"Testing CRUD flow","priority":"high"}`
		req, _ := http.NewRequest(http.MethodPost, srv.URL+"/api/tasks", bytes.NewBufferString(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusCreated {
			t.Errorf("Status code = %d, expected 201", resp.StatusCode)
		}

		var result struct {
			Data    model.Task `json:"data"`
			Message string     `json:"message"`
		}
		json.NewDecoder(resp.Body).Decode(&result)
		createdTask = result.Data

		if createdTask.Title != "Integration test task" {
			t.Errorf("Title = %q, expected %q", createdTask.Title, "Integration test task")
		}
	})

	// 3. Query single task
	t.Run("Query task", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks/1", nil)
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			t.Errorf("Status code = %d, expected 200", resp.StatusCode)
		}
	})

	// 4. Update task
	t.Run("Update task", func(t *testing.T) {
		body := `{"title":"Updated task","status":"completed"}`
		req, _ := http.NewRequest(http.MethodPut, srv.URL+"/api/tasks/1", bytes.NewBufferString(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			t.Errorf("Status code = %d, expected 200", resp.StatusCode)
		}
	})

	// 5. Query list
	t.Run("Query list", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks?page=1&page_size=10", nil)
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			t.Errorf("Status code = %d, expected 200", resp.StatusCode)
		}

		var result struct {
			Data  []model.Task `json:"data"`
			Total int          `json:"total"`
		}
		json.NewDecoder(resp.Body).Decode(&result)

		if result.Total != 1 {
			t.Errorf("Total = %d, expected 1", result.Total)
		}
	})

	// 6. Delete task
	t.Run("Delete task", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/api/tasks/1", nil)
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			t.Errorf("Status code = %d, expected 200", resp.StatusCode)
		}
	})

	// 7. Verify query after deletion returns 404
	t.Run("Query after deletion should return 404", func(t *testing.T) {
		req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks/1", nil)
		req.Header.Set("Authorization", authHeader)

		resp, err := client.Do(req)
		if err != nil {
			t.Fatalf("Request failed: %v", err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusNotFound {
			t.Errorf("Status code = %d, expected 404", resp.StatusCode)
		}
	})
}

// TestIntegration_Auth authentication middleware integration test
func TestIntegration_Auth(t *testing.T) {
	srv := setupTestServer(t)
	defer srv.Close()

	client := srv.Client()

	tests := []struct {
		name       string
		authHeader string
		wantCode   int
	}{
		{"No auth header", "", http.StatusUnauthorized},
		{"Wrong format", "Basic abc123", http.StatusUnauthorized},
		{"Wrong key", "Bearer wrong-key", http.StatusForbidden},
		{"Correct auth", "Bearer test-key", http.StatusOK},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/tasks", nil)
			if tt.authHeader != "" {
				req.Header.Set("Authorization", tt.authHeader)
			}

			resp, err := client.Do(req)
			if err != nil {
				t.Fatalf("Request failed: %v", err)
			}
			defer resp.Body.Close()

			if resp.StatusCode != tt.wantCode {
				t.Errorf("Status code = %d, expected %d", resp.StatusCode, tt.wantCode)
			}
		})
	}
}

// TestIntegration_CORS cross-origin test
func TestIntegration_CORS(t *testing.T) {
	srv := setupTestServer(t)
	defer srv.Close()

	client := srv.Client()

	// OPTIONS preflight request
	req, _ := http.NewRequest(http.MethodOptions, srv.URL+"/api/tasks", nil)
	req.Header.Set("Origin", "http://example.com")
	req.Header.Set("Access-Control-Request-Method", "POST")

	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("Request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusNoContent {
		t.Errorf("Status code = %d, expected 204", resp.StatusCode)
	}

	if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
		t.Error("Missing CORS response header")
	}
}

4.2 Running Integration Tests

BASH
# Run all tests
go test ./... -v

# Only run integration tests
go test -run TestIntegration -v

# View coverage
go test ./... -cover -coverprofile=coverage.out

# Generate HTML coverage report
go tool cover -html=coverage.out -o coverage.html

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

5. API Documentation (Swagger/OpenAPI Overview)

5.1 OpenAPI Specification Overview

OpenAPI (formerly Swagger) is the standard specification for describing RESTful APIs, defining interfaces in YAML/JSON format:

YAML
# docs/api.yaml (simplified version)
openapi: "3.0.3"
info:
  title: TaskFlow API
  description: Task management system RESTful API
  version: "1.0.0"
  contact:
    name: Development Team
    email: dev@taskflow.example.com

servers:
  - url: http://localhost:8080
    description: Local development environment

paths:
  /api/tasks:
    get:
      summary: Get task list
      operationId: listTasks
      tags: [Task Management]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, in_progress, completed]
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: page_size
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Successfully returned task list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Task"
                  total:
                    type: integer
                  page:
                    type: integer

    post:
      summary: Create new task
      operationId: createTask
      tags: [Task Management]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
      responses:
        "201":
          description: Task created successfully
        "400":
          description: Invalid request parameters

  /api/tasks/{id}:
    get:
      summary: Get single task
      operationId: getTask
      tags: [Task Management]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Success
        "404":
          description: Task not found

    put:
      summary: Update task
      operationId: updateTask
      tags: [Task Management]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Updated successfully
        "404":
          description: Task not found

    delete:
      summary: Delete task
      operationId: deleteTask
      tags: [Task Management]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Deleted successfully
        "404":
          description: Task not found

components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: integer
          example: 1
        title:
          type: string
          example: "Learn Go"
        description:
          type: string
          example: "Complete Lesson 30 comprehensive project"
        status:
          type: string
          enum: [pending, in_progress, completed]
        priority:
          type: string
          enum: [low, medium, high, urgent]
        due_date:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CreateTaskRequest:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
        priority:
          type: string
          enum: [low, medium, high, urgent]
          default: medium
        due_date:
          type: string
          format: date-time
          nullable: true

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer

security:
  - BearerAuth: []

5.2 Auto-generating Documentation with Swaggo

For Go projects, it's recommended to use swaggo/swag to auto-generate Swagger documentation from code comments:

GO
// Add comments above handler functions

// ListTasks godoc
// @Summary      Get task list
// @Description  Supports filtering by status and pagination
// @Tags         Task Management
// @Accept       json
// @Produce      json
// @Param        status   query    string  false  "Task status"
// @Param        page     query    int     false  "Page number"     default(1)
// @Param        page_size query   int     false  "Items per page"  default(20)
// @Success      200  {object}  map[string]interface{}
// @Failure      500  {object}  ErrorResponse
// @Security     BearerAuth
// @Router       /api/tasks [get]
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
    // ...
}

Generation command:

BASH
# Install swag tool
go install github.com/swaggo/swag/cmd/swag@latest

# Generate documentation in project root directory
swag init -g cmd/server/main.go

# Generated files are in docs/ directory
# docs/swagger.json, docs/swagger.yaml, docs/docs.go

6. README Writing Guidelines

An excellent README should include the following sections:

MARKDOWN
# TaskFlow - Task Management System

> A lightweight task management RESTful API service built with Go

## Features

- Complete task CRUD operations
- Filter by status and priority
- Pagination support
- API key authentication
- CORS cross-origin support
- Structured logging
- SQLite persistent storage
- One-click Docker deployment

## Quick Start

### Prerequisites

- Go 1.22+
- SQLite3 (development environment)

### Installation and Running

```bash
# Clone project
git clone https://github.com/yourname/taskflow.git
cd taskflow

# Install dependencies
go mod tidy

# Run service
go run cmd/server/main.go

# Service listens on http://localhost:8080 by default

Environment Variables

Variable Default Description
PORT 8080 Service port
DB_PATH taskflow.db SQLite database file path
API_KEY dev-secret-key API authentication key

API Usage Examples

BASH
# Create task
curl -X POST http://localhost:8080/api/tasks \
  -H "Authorization: Bearer dev-secret-key" \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn Go","priority":"high"}'

# Query list
curl http://localhost:8080/api/tasks \
  -H "Authorization: Bearer dev-secret-key"

# Health check (no auth needed)
curl http://localhost:8080/health

Project Structure

TEXT
taskflow/
├── cmd/server/         # Program entry point
├── internal/
│   ├── model/          # Data models
│   ├── store/          # Data storage layer
│   ├── service/        # Business logic layer
│   ├── handler/        # HTTP handler layer
│   └── middleware/     # Middleware
├── docs/               # API documentation
├── Dockerfile
└── README.md

Testing

BASH
# Run all tests
go test ./... -v

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

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

Deployment

Docker Deployment

BASH
# Build image
docker build -t taskflow:latest .

# Run container
docker run -d \
  -p 8080:8080 \
  -e API_KEY=your-production-key \
  -e DB_PATH=/data/taskflow.db \
  -v taskflow-data:/data \
  --name taskflow \
  taskflow:latest

License

MIT License


> 💡 README Writing Principle: Get others running in 30 seconds first, then gradually expand details.

---

## 7. Dockerfile Packaging and Release

### 7.1 Multi-Stage Build

```dockerfile
# Dockerfile

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

# Install SQLite compilation dependencies
RUN apk add --no-cache gcc musl-dev

WORKDIR /app

# 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=1 GOOS=linux go build \
    -ldflags="-s -w" \
    -o /app/taskflow \
    ./cmd/server/main.go

# ========== Stage 2: Run ==========
FROM alpine:3.19

# Install runtime dependencies
RUN apk add --no-cache ca-certificates tzdata

# Create non-root user
RUN adduser -D -g '' appuser

WORKDIR /app

# Copy binary from build stage
COPY --from=builder /app/taskflow .

# Switch to non-root user
USER appuser

# Expose port
EXPOSE 8080

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

# Startup command
ENTRYPOINT ["./taskflow"]

7.2 Docker Compose Configuration

YAML
# docker-compose.yml
version: "3.8"

services:
  taskflow:
    build: .
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - DB_PATH=/data/taskflow.db
      - API_KEY=${API_KEY:-change-me-in-production}
      - TZ=Asia/Shanghai
    volumes:
      - taskflow-data:/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3

volumes:
  taskflow-data:

7.3 Build and Run

BASH
# Build Docker image
docker build -t taskflow:latest .

# View image size
docker images taskflow

# Start with Docker Compose
export API_KEY=your-secret-key
docker compose up -d

# View logs
docker compose logs -f

# Stop service
docker compose down

# Stop and remove data volume
docker compose down -v

7.4 Cross-Compilation (Without Docker Environment)

BASH
# Compile Linux binary (on Windows/macOS)
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
    CC=x86_64-linux-musl-gcc \
    go build -ldflags="-s -w" -o taskflow-linux ./cmd/server/main.go

# Compile CGO-free version (doesn't support SQLite, needs pure Go database driver)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o taskflow-linux ./cmd/server/main.go

8. Deployment Guide

8.1 Deployment Checklist

TEXT
✅ Security Checks
   □ Production API_KEY changed to strong password
   □ CORS config restricts allowed origin domains
   □ Logs don't contain sensitive information
   □ Using HTTPS (configure reverse proxy)

✅ Performance Checks
   □ Database connection pool configured
   □ HTTP timeouts set (Read/Write/Idle)
   □ Request body size limited
   □ Paginated queries have maximum limits

✅ Reliability Checks
   □ Health check endpoint available
   □ Graceful shutdown logic implemented
   □ Log format is structured JSON
   □ Docker health check configured

8.2 Using Nginx Reverse Proxy

NGINX
# /etc/nginx/conf.d/taskflow.conf
server {
    listen 80;
    server_name api.taskflow.example.com;

    # Force HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.taskflow.example.com;

    ssl_certificate     /etc/ssl/certs/taskflow.crt;
    ssl_certificate_key /etc/ssl/private/taskflow.key;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeout settings
        proxy_connect_timeout 10s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }
}

❓ FAQ

Q1: SQLite reports "database is locked" in Docker?

A: SQLite has limitations on concurrent writes. Ensure:

GO
// 1. Set connection pool to 1 (single connection)
db.SetMaxOpenConns(1)

// 2. Enable WAL mode (allows read/write concurrency)
db.Exec("PRAGMA journal_mode=WAL")

// 3. Set busy wait timeout
// Add ?_busy_timeout=5000 to connection string

If concurrency is high, consider switching to PostgreSQL.

Q2: Does middleware execution order matter?

A: Yes. Recommended order: CORS → Logging → Auth → Business handler. Reasons:

GO
// In Chain, the first passed middleware executes first
handler := middleware.Chain(mux,
    middleware.CORS(),    // 1. Executes first
    middleware.Logging(), // 2. Second
    middleware.Auth(key), // 3. Executes last
)

Q3: How to manage API keys in production?

A: Never hardcode keys or commit them to the code repository. Recommended approaches:

BASH
# Method 1: Environment variables
export API_KEY=$(openssl rand -hex 32)

# Method 2: .env file (add to .gitignore)
echo "API_KEY=$(openssl rand -hex 32)" > .env

# Method 3: Docker Secrets (Swarm mode)
echo "my-secret-key" | docker secret create api_key -

Q4: Should I use SQLite or PostgreSQL for my project?

A: Choose based on scenario:

Scenario Recommended Reason
Learning/prototyping SQLite Zero config, single file
Internal tools/low traffic SQLite Sufficient and easy to maintain
Multiple services sharing data PostgreSQL Network access, centralized management
High concurrency writes PostgreSQL SQLite writes are serial
Need JSON queries PostgreSQL Powerful JSONB support

📖 Course Summary

Congratulations on completing all 30 Go language tutorial lessons! Let's review the entire knowledge system:

Phase 1 — Go Fundamentals (Lessons 1-6)

Lesson Core Takeaways
Go Introduction Go's positioning: system-level language for the cloud-native era
Variables and Types := short variables, zero value mechanism, iota enums
Control Flow for is the only loop, defer delayed execution, switch without fallthrough
Functions Multiple return values, closures, init function, functions as first-class citizens
Arrays and Slices Slices are reference types, append expansion mechanism, underlying principles
Map comma ok pattern, unordered nature, selection criteria with slice

Phase 2 — Structs and Interfaces (Lessons 7-12)

Lesson Core Takeaways
Structs Value types vs pointers, struct tags, anonymous fields
Methods Value receivers vs pointer receivers, composition over inheritance
Interfaces Implicit implementation (duck typing), type assertions, interface composition
Error Handling error interface, errors.Is/As, custom errors, explicit error handling philosophy
Packages and Modules go mod, export rules, internal packages
Practice Comprehensive use of structs + interfaces + error handling

Phase 3 — Concurrent Programming (Lessons 13-18)

Lesson Core Takeaways
Goroutine Lightweight coroutines, sync.WaitGroup, leak prevention
Channel Unbuffered vs buffered, close/range, direction restrictions
Select Multiplexing, timeout control, fan-in/fan-out patterns
sync Package Mutex/RWMutex, Once, sync.Map, race detection
Task Scheduler context.Context, concurrent collaboration, timeout retry
Web Crawler Rate limiting, deduplication, error retry, graceful exit

Phase 4 — Standard Library and Practical Skills (Lessons 19-24)

Lesson Core Takeaways
String Processing strings/strconv packages, strings.Builder
File IO os package, bufio efficient read/write, directory traversal
JSON Processing Marshal/Unmarshal, struct tags, streaming processing
HTTP Programming net/http server, Handler interface, middleware pattern
Testing Table-driven tests, httptest, benchmarks, coverage
Regex and Date regexp package, time formatting and parsing, timers

Phase 5 — Comprehensive Projects (Lessons 25-30)

Lesson Core Takeaways
CLI Tools flag package, cobra library, argument validation
REST API RESTful design, JSON handling, middleware chain
Database database/sql, SQLite driver, CRUD encapsulation
Deployment and Optimization Cross-compilation, Docker builds, graceful shutdown
Comprehensive Project (Part 1) Architecture design, directory structure, business logic, unit tests
Comprehensive Project (Part 2) API completion, middleware integration, integration testing, containerized release

Core Competency Checklist

After 30 lessons, you've mastered:

TEXT
✅ Go language basic syntax and type system
✅ Object-oriented programming (structs + interfaces)
✅ Concurrent programming (Goroutine + Channel + sync)
✅ Standard library core packages (net/http, encoding/json, testing, etc.)
✅ RESTful API design and development
✅ Database operations (SQL drivers, CRUD encapsulation)
✅ Test-driven development (unit tests, integration tests, benchmarks)
✅ Project engineering (directory structure, dependency management, configuration management)
✅ Containerized deployment (Docker, multi-stage builds, health checks)

📝 Further Learning Suggestions

Advanced Directions

Direction Recommended Resources Description
Web Framework Gin, Echo Production-grade HTTP frameworks
ORM GORM, sqlx Simplified database operations
Microservices go-kit, Kratos, go-zero Microservice frameworks
Config Management Viper Multi-format config reading
Logging zap, zerolog High-performance logging libraries
API Gateway Kong, Traefik Traffic management
Cloud Native Kubernetes, Docker Compose, Helm Container orchestration
Message Queue NATS, Kafka, RabbitMQ Asynchronous communication
  1. "The Go Programming Language" — Donovan & Kernighan
  2. "Go in Action" — William Kennedy
  3. "Concurrency in Go" — Katherine Cox-Buday
  4. "100 Go Mistakes and How to Avoid Them" — Teiva Harsanyi

Project Practice Suggestions

Difficulty Project Practice Focus
⭐⭐ URL Shortener HTTP, JSON, SQLite
⭐⭐⭐ Chat Room WebSocket, Goroutine, Channel
⭐⭐⭐ Personal Blog System Template engine, Session, File upload
⭐⭐⭐⭐ Distributed Crawler Multi-node collaboration, Message queue, Deduplication
⭐⭐⭐⭐ API Gateway Middleware chain, Load balancing, Rate limiting and circuit breaking

🎉 Conclusion

"Less is more" — Go's design philosophy

30 lessons, from fmt.Println("Hello, World!") to building a deployable RESTful API service, you've completed the entire Go language learning journey.

Go's charm lies in its simplicity — it doesn't give you too many choices, but every choice is carefully considered. When facing concurrency problems, think of goroutine and channel; when handling errors, think "explicit over implicit"; when designing interfaces, think "duck typing" and "composition over inheritance."

Programming is a craft, and the best way to learn is to write code. Type out all 30 lessons' examples, complete all the exercises, and then find yourself a real small project to practice — you'll discover that Go is simpler and more powerful than you imagined.

TEXT
  ╔══════════════════════════════════════════════════╗
  ║   Congratulations on completing the 30-lesson    ║
  ║   Go language tutorial!                          ║
  ║   You now have the foundation to develop         ║
  ║   production-grade Go applications.              ║
  ║   Go build something amazing!                    ║
  ╚══════════════════════════════════════════════════╝

Next Lesson

This is the final lesson. If you'd like to revisit the course starting point, go back to Lesson 1: Go Introduction.

If you have questions or feedback, feel free to submit them on GitHub Issues.

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%

🙏 帮我们做得更好

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

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