第七章:实战演练——构建一个简单的REST API

理论说完了,让我们来写真正有用的东西:一个Web API。我们将使用Go强大的标准库net/http来构建一个简单的CRUD API。

7.1 项目结构 #

/my-api
  ├── go.mod
  └── main.go

7.2 代码实现 #

我们将创建一个内存中的“数据库”(一个map),并实现以下API端点:

  • GET /books: 获取所有图书列表
  • POST /books: 添加一本新书
  • GET /books/{id}: 获取指定ID的图书
// main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"sync"
)

// Book 结构体,代表我们的数据模型
type Book struct {
	ID     int    `json:"id"`
	Title  string `json:"title"`
	Author string `json:"author"`
}

// === 内存数据库 ===
var (
	// 使用map存储图书,键是ID,值是Book
	store = make(map[int]Book)
	// 读写锁,用于在并发访问map时保证安全
	mu sync.RWMutex
	// 用于生成自增ID
	nextID = 1
)

// === 处理器函数 (Handlers) ===

func booksHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		getBooks(w, r)
	case http.MethodPost:
		createBook(w, r)
	default:
		// 如果是其他HTTP方法,返回405 Method Not Allowed
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func getBooks(w http.ResponseWriter, r *http.Request) {
	mu.RLock() // 加读锁
	defer mu.RUnlock() // 函数结束时解锁

	// 将map的值(所有Book)放入一个切片中
	books := make([]Book, 0, len(store))
	for _, book := range store {
		books = append(books, book)
	}

	w.Header().Set("Content-Type", "application/json")
	// 使用json.NewEncoder将数据结构编码为JSON并写入http.ResponseWriter
	json.NewEncoder(w).Encode(books)
}

func createBook(w http.ResponseWriter, r *http.Request) {
	var book Book
	// 从请求体中解码JSON到book结构体
	if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	mu.Lock() // 加写锁
	defer mu.Unlock()

	book.ID = nextID
	nextID++
	store[book.ID] = book

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated) // 设置HTTP状态码为201 Created
	json.NewEncoder(w).Encode(book)
}

// bookHandler 处理带ID的路由
func bookHandler(w http.ResponseWriter, r *http.Request) {
	// 从URL路径中提取ID
	// 期望的路径是 /books/123
	idStr := r.URL.Path[len("/books/"):]
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid book ID", http.StatusBadRequest)
		return
	}

	switch r.Method {
	case http.MethodGet:
		getBookByID(w, r, id)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func getBookByID(w http.ResponseWriter, r *http.Request, id int) {
	mu.RLock()
	defer mu.RUnlock()

	book, ok := store[id]
	if !ok {
		http.Error(w, "Book not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(book)
}


func main() {
	// === 路由设置 ===
	// Go 1.22+ 引入了更方便的路由模式匹配
	// http.HandleFunc("GET /books", getBooks)
	// http.HandleFunc("POST /books", createBook)
	// http.HandleFunc("GET /books/{id}", getBookByID)

	// 为了兼容旧版本和更清晰地展示,我们使用http.ServeMux
	mux := http.NewServeMux()
	mux.HandleFunc("/books", booksHandler) // /books 同时处理GET和POST
	mux.HandleFunc("/books/", bookHandler) // /books/ 后面的部分将由handler处理

	// 启动HTTP服务器
	fmt.Println("API server listening on :8080")
	// log.Fatal 会在出错时记录日志并退出程序
	log.Fatal(http.ListenAndServe(":8080", mux))
}

7.3 运行和测试 #

  1. 运行服务器:

    go run main.go
    # 输出: API server listening on :8080
    
  2. 使用curl或其他API工具测试:

    • 创建一本书:
      curl -X POST http://localhost:8080/books -d '{"title":"The Go Programming Language","author":"Alan Donovan"}' -H "Content-Type: application/json"
      # 响应: {"id":1,"title":"The Go Programming Language","author":"Alan Donovan"}
      
    • 再创建一本书:
      curl -X POST http://localhost:8080/books -d '{"title":"Concurrency in Go","author":"Katherine Cox-Buday"}' -H "Content-Type: application/json"
      # 响应: {"id":2,"title":"Concurrency in Go","author":"Katherine Cox-Buday"}
      
    • 获取所有书:
      curl http://localhost:8080/books
      # 响应: [{"id":1,"title":"The Go Programming Language","author":"Alan Donovan"},{"id":2,"title":"Concurrency in Go","author":"Katherine Cox-Buday"}]
      
    • 获取ID为1的书:
      curl http://localhost:8080/books/1
      # 响应: {"id":1,"title":"The Go Programming Language","author":"Alan Donovan"}
      
    • 获取不存在的书:
      curl -i http://localhost:8080/books/99
      # 响应头会包含 HTTP/1.1 404 Not Found
      # 响应体: Book not found
      

这个例子虽然简单,但它涵盖了:

  • 启动HTTP服务器
  • 路由处理
  • JSON的编码与解码
  • 并发安全(使用sync.RWMutex
  • RESTful API的基本设计原则

在实际项目中,你可能会使用像GinEchoChi这样的第三方Web框架来简化路由和中间件处理,但底层原理都与此类似。