Go: 电商 API(上):模型与 CRUD

最后更新:2026-08-26

理论到此为止。现在开始——用 Go 从零构建一个真正的电商 API。

Bob 决定创业做电商平台。三个星期,三个故事,一个完整的 Go backend。

1. 你将学到


2. 故事:Bob 的创业第一天

(1) 痛点:"我先写个 MVP,两周后上线"

Bob 辞职创业,要做电商平台:

"第一天我搭了一个 monolith——所有逻辑都在 main.go 里。到了第七天,代码 3000 行,我分不清哪些是订单逻辑,哪些是用户逻辑。我想加一个'优惠券'功能,改了 6 个文件还没改完。合伙人说'两周上线',我看两个月都悬。"

但他冷静下来回忆 Go 的 4 层架构,决定重构:

GO
// 重构计划:
// cmd/server/        → 入口
// internal/handler/  → HTTP 处理器
// internal/service/  → 业务逻辑
// internal/repository/ → 数据访问
// pkg/model/         → 领域模型

(2) 本课目标:用户 + 商品 + 订单基础 CRUD

第一周目标:搭建完整骨架,实现三个核心资源的 CRUD。

TEXT 📖 仅展示
POST /api/v1/register         → 用户注册
POST /api/v1/login            → 用户登录
GET  /api/v1/products         → 商品列表
GET  /api/v1/products/{id}    → 商品详情
POST /api/v1/orders           → 创建订单
GET  /api/v1/orders/{id}      → 订单详情
GET  /api/v1/orders           → 我的订单列表

3. 项目结构

TEXT 📖 仅展示
ecommerce/
├── cmd/
│   └── server/
│       └── main.go              # 入口 + 依赖注入
├── internal/
│   ├── handler/                  # HTTP 层
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── response.go          # 统一响应工具
│   ├── service/                  # 业务层
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── errors.go            # 业务错误定义
│   └── repository/              # 数据层
│       ├── user.go
│       ├── product.go
│       └── order.go
├── pkg/
│   └── model/                   # 领域模型
│       ├── user.go
│       ├── product.go
│       └── order.go
├── go.mod
└── go.sum

4. 完整实现

▶ 示例:Model 层

GO
// pkg/model/user.go
package model

type User struct {
    ID        int     `json:"id"`
    Email     string  `json:"email"`
    Password  string  `json:"-"`       // 不返回给客户端
    Name      string  `json:"name"`
}

type RegisterRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    Name     string `json:"name"`
}

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}
▶ 试一试
GO
// pkg/model/product.go
package model

type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Description string  `json:"description"`
    Price       float64 `json:"price"`
    Stock       int     `json:"stock"`
}
GO
// pkg/model/order.go
package model

type Order struct {
    ID         int       `json:"id"`
    UserID     int       `json:"user_id"`
    ProductID  int       `json:"product_id"`
    Quantity   int       `json:"quantity"`
    TotalPrice float64   `json:"total_price"`
    Status     string    `json:"status"`
}

type CreateOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

▶ 示例:Repository 层

GO 📖 仅展示
// internal/repository/user.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, error)
    FindByEmail(email string) (*model.User, error)
    Create(user *model.User) error
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, error) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE id = ?", id).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) FindByEmail(email string) (*model.User, error) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE email = ?", email).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) Create(user *model.User) error {
    result, err := r.db.Exec(
        "INSERT INTO users (email, password, name) VALUES (?, ?, ?)",
        user.Email, user.Password, user.Name,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    user.ID = int(id)
    return nil
}
逻辑代码 46 行(超过 40 行限制,仅展示)
GO
// internal/repository/product.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type ProductRepository interface {
    FindByID(id int) (*model.Product, error)
    List() ([]*model.Product, error)
    Create(product *model.Product) error
    UpdateStock(id int, stock int) error
}

type productRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) ProductRepository {
    return &productRepository{db: db}
}

func (r *productRepository) FindByID(id int) (*model.Product, error) {
    p := &model.Product{}
    err := r.db.QueryRow("SELECT id, name, description, price, stock FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *productRepository) List() ([]*model.Product, error) {
    rows, err := r.db.Query("SELECT id, name, description, price, stock FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*model.Product
    for rows.Next() {
        p := &model.Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

func (r *productRepository) Create(product *model.Product) error {
    result, err := r.db.Exec(
        "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
        product.Name, product.Description, product.Price, product.Stock,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    product.ID = int(id)
    return nil
}

func (r *productRepository) UpdateStock(id int, stock int) error {
    _, err := r.db.Exec("UPDATE products SET stock = ? WHERE id = ?", stock, id)
    return err
}
GO
// internal/repository/order.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type OrderRepository interface {
    FindByID(id int) (*model.Order, error)
    ListByUserID(userID int) ([]*model.Order, error)
    Create(order *model.Order) error
}

type orderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) OrderRepository {
    return &orderRepository{db: db}
}

func (r *orderRepository) FindByID(id int) (*model.Order, error) {
    o := &model.Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

func (r *orderRepository) ListByUserID(userID int) ([]*model.Order, error) {
    rows, err := r.db.Query(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE user_id = ?",
        userID,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var orders []*model.Order
    for rows.Next() {
        o := &model.Order{}
        if err := rows.Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status); err != nil {
            return nil, err
        }
        orders = append(orders, o)
    }
    return orders, rows.Err()
}

func (r *orderRepository) Create(order *model.Order) error {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total_price, status) VALUES (?, ?, ?, ?, ?)",
        order.UserID, order.ProductID, order.Quantity, order.TotalPrice, order.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    order.ID = int(id)
    return nil
}

(3) Service 层

GO
// internal/service/errors.go
package service

import "errors"

var (
    ErrEmailTaken      = errors.New("email already registered")
    ErrInvalidCreds    = errors.New("invalid email or password")
    ErrProductNotFound = errors.New("product not found")
    ErrInsufficientStock = errors.New("insufficient stock")
    ErrOrderNotFound   = errors.New("order not found")
    ErrUnauthorized    = errors.New("unauthorized")
)
GO
// internal/service/user.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
    "errors"
    "strings"
)

type UserService struct {
    userRepo repository.UserRepository
}

func NewUserService(userRepo repository.UserRepository) *UserService {
    return &UserService{userRepo: userRepo}
}

func (s *UserService) Register(req model.RegisterRequest) (*model.User, error) {
    if strings.TrimSpace(req.Email) == "" {
        return nil, errors.New("email is required")
    }
    if strings.TrimSpace(req.Password) == "" {
        return nil, errors.New("password is required")
    }
    if strings.TrimSpace(req.Name) == "" {
        return nil, errors.New("name is required")
    }

    existing, _ := s.userRepo.FindByEmail(req.Email)
    if existing != nil {
        return nil, ErrEmailTaken
    }

    user := &model.User{
        Email:    req.Email,
        Password: req.Password, // 生产环境用 bcrypt
        Name:     req.Name,
    }

    if err := s.userRepo.Create(user); err != nil {
        return nil, err
    }

    return user, nil
}

func (s *UserService) Login(req model.LoginRequest) (*model.User, error) {
    user, err := s.userRepo.FindByEmail(req.Email)
    if err != nil {
        return nil, err
    }
    if user == nil || user.Password != req.Password {
        return nil, ErrInvalidCreds
    }
    return user, nil
}

func (s *UserService) GetUserByID(id int) (*model.User, error) {
    return s.userRepo.FindByID(id)
}
GO
// internal/service/product.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
)

type ProductService struct {
    productRepo repository.ProductRepository
}

func NewProductService(productRepo repository.ProductRepository) *ProductService {
    return &ProductService{productRepo: productRepo}
}

func (s *ProductService) ListProducts() ([]*model.Product, error) {
    return s.productRepo.List()
}

func (s *ProductService) GetProduct(id int) (*model.Product, error) {
    product, err := s.productRepo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    return product, nil
}
GO
// internal/service/order.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
    "errors"
)

type OrderService struct {
    orderRepo   repository.OrderRepository
    productRepo repository.ProductRepository
    userRepo    repository.UserRepository
}

func NewOrderService(
    orderRepo repository.OrderRepository,
    productRepo repository.ProductRepository,
    userRepo repository.UserRepository,
) *OrderService {
    return &OrderService{
        orderRepo:   orderRepo,
        productRepo: productRepo,
        userRepo:    userRepo,
    }
}

func (s *OrderService) CreateOrder(userID int, req model.CreateOrderRequest) (*model.Order, error) {
    if req.Quantity <= 0 {
        return nil, errors.New("quantity must be positive")
    }

    product, err := s.productRepo.FindByID(req.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    if product.Stock < req.Quantity {
        return nil, ErrInsufficientStock
    }

    totalPrice := product.Price * float64(req.Quantity)

    order := &model.Order{
        UserID:     userID,
        ProductID:  req.ProductID,
        Quantity:   req.Quantity,
        TotalPrice: totalPrice,
        Status:     "pending",
    }

    if err := s.orderRepo.Create(order); err != nil {
        return nil, err
    }

    // 扣减库存
    newStock := product.Stock - req.Quantity
    if err := s.productRepo.UpdateStock(product.ID, newStock); err != nil {
        return nil, err
    }

    return order, nil
}

func (s *OrderService) GetOrder(orderID, userID int) (*model.Order, error) {
    order, err := s.orderRepo.FindByID(orderID)
    if err != nil {
        return nil, err
    }
    if order == nil {
        return nil, ErrOrderNotFound
    }
    if order.UserID != userID {
        return nil, ErrUnauthorized
    }
    return order, nil
}

func (s *OrderService) ListUserOrders(userID int) ([]*model.Order, error) {
    return s.orderRepo.ListByUserID(userID)
}

(4) Handler 层

GO
// internal/handler/response.go
package handler

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

type APIResponse struct {
    Data  interface{} `json:"data,omitempty"`
    Error string      `json:"error,omitempty"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Data: data})
}

func writeError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Error: msg})
}
GO
// internal/handler/user.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type UserHandler struct {
    userSvc *service.UserService
}

func NewUserHandler(userSvc *service.UserService) *UserHandler {
    return &UserHandler{userSvc: userSvc}
}

func (h *UserHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/register", h.RegisterUser)
    mux.HandleFunc("POST /api/v1/login", h.LoginUser)
}

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    var req model.RegisterRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Register(req)
    if errors.Is(err, service.ErrEmailTaken) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

func (h *UserHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
    var req model.LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Login(req)
    if errors.Is(err, service.ErrInvalidCreds) {
        writeError(w, http.StatusUnauthorized, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "login failed")
        return
    }

    writeJSON(w, http.StatusOK, user)
}
GO
// internal/handler/product.go
package handler

import (
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
)

type ProductHandler struct {
    productSvc *service.ProductService
}

func NewProductHandler(productSvc *service.ProductService) *ProductHandler {
    return &ProductHandler{productSvc: productSvc}
}

func (h *ProductHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/products", h.ListProducts)
    mux.HandleFunc("GET /api/v1/products/{id}", h.GetProduct)
}

func (h *ProductHandler) ListProducts(w http.ResponseWriter, r *http.Request) {
    products, err := h.productSvc.ListProducts()
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list products")
        return
    }
    writeJSON(w, http.StatusOK, products)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid product ID")
        return
    }

    product, err := h.productSvc.GetProduct(id)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get product")
        return
    }

    writeJSON(w, http.StatusOK, product)
}
GO
// internal/handler/order.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type OrderHandler struct {
    orderSvc *service.OrderService
}

func NewOrderHandler(orderSvc *service.OrderService) *OrderHandler {
    return &OrderHandler{orderSvc: orderSvc}
}

func (h *OrderHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/orders", h.CreateOrder)
    mux.HandleFunc("GET /api/v1/orders", h.ListOrders)
    mux.HandleFunc("GET /api/v1/orders/{id}", h.GetOrder)
}

// 模拟从 Context 获取 userID(第二课实现 JWT)
func getUserID(r *http.Request) int {
    if id, ok := r.Context().Value("user_id").(int); ok {
        return id
    }
    return 1 // 默认用户(第一课临时)
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

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

    order, err := h.orderSvc.CreateOrder(userID, req)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrInsufficientStock) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid order ID")
        return
    }

    order, err := h.orderSvc.GetOrder(id, userID)
    if errors.Is(err, service.ErrOrderNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrUnauthorized) {
        writeError(w, http.StatusForbidden, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get order")
        return
    }

    writeJSON(w, http.StatusOK, order)
}

func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    orders, err := h.orderSvc.ListUserOrders(userID)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list orders")
        return
    }
    writeJSON(w, http.StatusOK, orders)
}

(5) Main(依赖注入)

⚙️ 前置安装:运行 go get github.com/mattn/go-sqlite3 ⚠️ 注意:go-sqlite3 需要 CGO,Windows 需安装 gcc(MinGW-w64),macOS/Linux 自带

GO
package main

import (
    "database/sql"
    "log"
    "net/http"

    "ecommerce/internal/handler"
    "ecommerce/internal/repository"
    "ecommerce/internal/service"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./ecommerce.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    initDB(db)

    // 依赖注入
    userRepo := repository.NewUserRepository(db)
    productRepo := repository.NewProductRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    userSvc := service.NewUserService(userRepo)
    productSvc := service.NewProductService(productRepo)
    orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)

    userHandler := handler.NewUserHandler(userSvc)
    productHandler := handler.NewProductHandler(productSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    userHandler.Register(mux)
    productHandler.Register(mux)
    orderHandler.Register(mux)

    log.Println("电商 API 启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func initDB(db *sql.DB) {
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT UNIQUE NOT NULL,
        password TEXT NOT NULL,
        name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        description TEXT,
        price REAL NOT NULL,
        stock INTEGER NOT NULL DEFAULT 0
    );
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        product_id INTEGER NOT NULL,
        quantity INTEGER NOT NULL,
        total_price REAL NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        FOREIGN KEY (user_id) REFERENCES users(id),
        FOREIGN KEY (product_id) REFERENCES products(id)
    );`
    if _, err := db.Exec(schema); err != nil {
        log.Fatal("initDB:", err)
    }
}

(6) 单元测试(mock Repository)

GO
// internal/service/order_test.go
package service

import (
    "testing"
    "ecommerce/pkg/model"
)

// mock Repository
type mockProductRepo struct {
    products map[int]*model.Product
}

func (m *mockProductRepo) FindByID(id int) (*model.Product, error) {
    return m.products[id], nil
}

func (m *mockProductRepo) List() ([]*model.Product, error) {
    var result []*model.Product
    for _, p := range m.products {
        result = append(result, p)
    }
    return result, nil
}

func (m *mockProductRepo) Create(product *model.Product) error {
    product.ID = len(m.products) + 1
    m.products[product.ID] = product
    return nil
}

func (m *mockProductRepo) UpdateStock(id int, stock int) error {
    if p, ok := m.products[id]; ok {
        p.Stock = stock
    }
    return nil
}

type mockOrderRepo struct {
    orders []*model.Order
}

func (m *mockOrderRepo) FindByID(id int) (*model.Order, error) {
    for _, o := range m.orders {
        if o.ID == id {
            return o, nil
        }
    }
    return nil, nil
}

func (m *mockOrderRepo) ListByUserID(userID int) ([]*model.Order, error) {
    var result []*model.Order
    for _, o := range m.orders {
        if o.UserID == userID {
            result = append(result, o)
        }
    }
    return result, nil
}

func (m *mockOrderRepo) Create(order *model.Order) error {
    order.ID = len(m.orders) + 1
    m.orders = append(m.orders, order)
    return nil
}

func TestCreateOrder_Success(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 10},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    order, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 2})
    if err != nil {
        t.Fatalf("expected no error, got %v", err)
    }
    if order.TotalPrice != 1999.98 {
        t.Errorf("expected total 1999.98, got %.2f", order.TotalPrice)
    }
}

func TestCreateOrder_InsufficientStock(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 1},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 5})
    if err != ErrInsufficientStock {
        t.Errorf("expected ErrInsufficientStock, got %v", err)
    }
}

func TestCreateOrder_ProductNotFound(t *testing.T) {
    productRepo := &mockProductRepo{products: make(map[int]*model.Product)}
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 999, Quantity: 1})
    if err != ErrProductNotFound {
        t.Errorf("expected ErrProductNotFound, got %v", err)
    }
}
💡 提示: 使用接口定义 Repository 层(如 type UserRepository interface{...}),就可以在测试中注入 mock 实现。这是 Go 隐式接口的巨大优势——不需要 mock 框架,手动写轻量 mock 即可。


❓ 常见问题

Q 4 层模型在真实项目怎么落地?
A 从 Model(数据结构)开始→Repository(数据库 CRUD)→Service(业务规则)→Handler(HTTP)。每个文件不超过 200 行。文件按资源组织(user/product/order)。main 函数负责组装所有依赖(依赖注入)。
Q 单元测试 mock 表怎么设计?
A 用接口定义 Repository,测试时传入 mock 实现。mock 用 map 存储内存数据。测试 Service 层时 mock Repository,测试 Handler 层时 mock Service。不需要 mock 框架——Go 的隐式接口让手动 mock 很轻量。
Q 错误处理 3 种风格?
A (1) 自定义错误类型(含 Code/Message/HTTPStatus)——适用于大型项目;(2) sentinel errors(var ErrXxx = errors.New(...))——适用于中等项目;(3) 字符串错误——仅适用于小项目。本项目使用 sentinel errors。
Q RESTful 资源设计表?
A users(注册/登录)、products(列表/详情)、orders(创建/列表/详情)。每个资源有独立的 handler/service/repository。URL 用复数名词:/api/v1/users /api/v1/products /api/v1/orders
Q validation 库对比?
A (1) 手动验证——本项目中用 strings.TrimSpace 检查——零依赖;(2) go-playground/validator——标签式验证,功能强大;(3) 自定义 Validator 结构体。小项目手动就够了。

📖 小节


📝 作业

  1. 基础题(难度⭐):运行本课完整代码,用 curl 测试注册、登录、商品列表、创建订单等端点。验证所有 CRUD 操作正常工作。

  2. 进阶题(难度⭐⭐):为本系统的 Service 层添加完整单元测试。要求:(1) 覆盖 UserService.Register 的重复邮箱场景;(2) 覆盖 ProductService.GetProduct 的未找到场景;(3) 覆盖 OrderService.GetOrder 的未授权场景;(4) 测试覆盖率 > 80%。

  3. 挑战题(难度⭐⭐⭐):为系统添加分类(Category)资源。要求:(1) Product 增加 CategoryID 字段;(2) Category CRUD(管理员功能);(3) GET /products?category_id=X 按分类筛选;(4) Service 层验证分类存在;(5) 完整的单元测试。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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