使用Golang构建基础但设计周全的RESTful API指南
使用Golang构建基础但设计周全的RESTful API指南 我看到了Go语言的诸多优点。它"简单",标准库也很棒。但似乎所有的内容都停留在"看……我可以用标准库启动一个Web服务器并提供单个端点!"这种表面层次。要找到一篇描述如何组织(Web)应用程序的好文章或书籍似乎极为罕见。
任何构建过具有一定规模(中小型)项目的开发者都知道,在构建软件时通常会有一些不想做的事情。我想知道这些是否是我在以往语言经历中学到的"坏"习惯?也许在Go中我需要换一种思维方式?
让我们举一个简单的例子。我主要来自Web应用程序背景。我学到的一点是不要在控制器中填充大量业务逻辑。相反,我倾向于将大量逻辑推回到模型中。我通过多种方式实现这一点,最近最喜欢的方式是使用CQRS模式。然而,Go很少提及CQRS或DDD相关内容。基本上所有内容都展示如何创建"控制器"(HandlerFunc)、查询数据库并返回数据。这对其他Web开发者来说可以接受吗?在Go中这样做合适吗?
另一个例子……假设我正在构建一个允许用户创建待办事项的简单应用程序。在我熟悉的语言中,我通常不习惯通过API直接暴露我的领域对象。
例如,如果我有一个如下所示的领域对象:
public class TodoItem {
public int Id { get; set; } // 数据库专用(内部)
public string Name { get; set; }
public bool IsProcessed { get; set; } // 内部属性
public bool IsCompleted { get; set;
}
根据我的经验,我宁愿不向外界暴露Id和IsProcessed。通常在C#中,这是通过ViewModel来管理的,或者当仅通过API暴露时,我更喜欢称之为ResourceModel。
我的控制器通常负责将领域模型转换为资源模型。通过使用某种映射库(AutoMapper)或简单地手动映射它们。
然而,我从未在Go教程中看到过这方面的提及?我甚至找不到任何人讨论类似的内容。这在Go中不是问题吗?向外界暴露整个领域对象被认为是可接受的吗?这似乎不是语言特定的问题,而是API设计和保持领域封装的问题。所以当我在Go社区中看不到任何人讨论这些事情时,我感到担忧。
有人能帮我理解我遗漏了什么吗?我应该努力按照DDD或CQRS类型的模式进行组织吗?如何构建比"在控制器中从数据库获取数据并直接返回"更复杂的东西?有人能提供一个他们构建的RESTful API链接供我学习吗?
非常感谢任何反馈。
更多关于使用Golang构建基础但设计周全的RESTful API指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html
Kat Zien 已经多次就此主题进行演讲。你可以在 YouTube 上找到相关视频,例如 https://youtu.be/oL6JBUk6tj0。幻灯片和示例代码位于 https://github.com/katzien/talks/blob/master/how-do-you-structure-your-go-apps/。
更多关于使用Golang构建基础但设计周全的RESTful API指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在Go中构建结构良好的RESTful API时,确实需要考虑架构模式和组织方式。虽然Go社区倾向于简洁,但分层架构和关注点分离同样重要。以下是一个基于你需求的实现示例:
项目结构示例
todo-api/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ └── todo.go
│ ├── handlers/
│ │ └── todo.go
│ ├── services/
│ │ └── todo.go
│ ├── repositories/
│ │ └── todo.go
│ └── dto/
│ └── todo.go
└── go.mod
领域模型与DTO分离
// internal/domain/todo.go
package domain
type TodoItem struct {
ID int `json:"-"`
Name string `json:"name"`
IsProcessed bool `json:"-"`
IsCompleted bool `json:"isCompleted"`
}
// internal/dto/todo.go
package dto
type CreateTodoRequest struct {
Name string `json:"name" validate:"required"`
}
type TodoResponse struct {
ID int `json:"id"`
Name string `json:"name"`
IsCompleted bool `json:"isCompleted"`
}
服务层处理业务逻辑
// internal/services/todo.go
package services
import (
"context"
"todo-api/internal/domain"
"todo-api/internal/dto"
"todo-api/internal/repositories"
)
type TodoService struct {
repo repositories.TodoRepository
}
func NewTodoService(repo repositories.TodoRepository) *TodoService {
return &TodoService{repo: repo}
}
func (s *TodoService) CreateTodo(ctx context.Context, req dto.CreateTodoRequest) (*dto.TodoResponse, error) {
todo := &domain.TodoItem{
Name: req.Name,
IsProcessed: false,
IsCompleted: false,
}
err := s.repo.Create(ctx, todo)
if err != nil {
return nil, err
}
return &dto.TodoResponse{
ID: todo.ID,
Name: todo.Name,
IsCompleted: todo.IsCompleted,
}, nil
}
func (s *TodoService) GetTodo(ctx context.Context, id int) (*dto.TodoResponse, error) {
todo, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return &dto.TodoResponse{
ID: todo.ID,
Name: todo.Name,
IsCompleted: todo.IsCompleted,
}, nil
}
仓储层抽象数据访问
// internal/repositories/todo.go
package repositories
import (
"context"
"database/sql"
"todo-api/internal/domain"
)
type TodoRepository interface {
Create(ctx context.Context, todo *domain.TodoItem) error
GetByID(ctx context.Context, id int) (*domain.TodoItem, error)
Update(ctx context.Context, todo *domain.TodoItem) error
Delete(ctx context.Context, id int) error
}
type todoRepository struct {
db *sql.DB
}
func NewTodoRepository(db *sql.DB) TodoRepository {
return &todoRepository{db: db}
}
func (r *todoRepository) Create(ctx context.Context, todo *domain.TodoItem) error {
query := `INSERT INTO todos (name, is_processed, is_completed) VALUES ($1, $2, $3) RETURNING id`
return r.db.QueryRowContext(ctx, query, todo.Name, todo.IsProcessed, todo.IsCompleted).Scan(&todo.ID)
}
func (r *todoRepository) GetByID(ctx context.Context, id int) (*domain.TodoItem, error) {
query := `SELECT id, name, is_processed, is_completed FROM todos WHERE id = $1`
row := r.db.QueryRowContext(ctx, query, id)
var todo domain.TodoItem
err := row.Scan(&todo.ID, &todo.Name, &todo.IsProcessed, &todo.IsCompleted)
if err != nil {
return nil, err
}
return &todo, nil
}
处理器层处理HTTP请求
// internal/handlers/todo.go
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"todo-api/internal/dto"
"todo-api/internal/services"
)
type TodoHandler struct {
service *services.TodoService
}
func NewTodoHandler(service *services.TodoService) *TodoHandler {
return &TodoHandler{service: service}
}
func (h *TodoHandler) CreateTodo(w http.ResponseWriter, r *http.Request) {
var req dto.CreateTodoRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
todo, err := h.service.CreateTodo(r.Context(), req)
if err != nil {
http.Error(w, "Failed to create todo", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(todo)
}
func (h *TodoHandler) GetTodo(w http.ResponseWriter, r *http.Request) {
idStr := r.URL.Query().Get("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid todo ID", http.StatusBadRequest)
return
}
todo, err := h.service.GetTodo(r.Context(), id)
if err != nil {
http.Error(w, "Todo not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(todo)
}
应用入口点
// cmd/api/main.go
package main
import (
"database/sql"
"log"
"net/http"
"todo-api/internal/handlers"
"todo-api/internal/repositories"
"todo-api/internal/services"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "postgres://user:pass@localhost/todo_db?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
todoRepo := repositories.NewTodoRepository(db)
todoService := services.NewTodoService(todoRepo)
todoHandler := handlers.NewTodoHandler(todoService)
http.HandleFunc("/todos", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
todoHandler.CreateTodo(w, r)
case http.MethodGet:
todoHandler.GetTodo(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
这种架构实现了:
- 领域模型与API响应的分离
- 清晰的关注点分离(处理器、服务、仓储)
- 内部属性(ID、IsProcessed)不暴露给外部
- 可测试的独立组件
Go社区确实倾向于更简单的解决方案,但对于中等规模的项目,这种分层架构提供了更好的可维护性和可测试性。

