理论说完了,让我们来写真正有用的东西:一个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 运行和测试 #
运行服务器:
go run main.go # 输出: API server listening on :8080
使用
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的基本设计原则
在实际项目中,你可能会使用像Gin
、Echo
或Chi
这样的第三方Web框架来简化路由和中间件处理,但底层原理都与此类似。