掌握了 Go 的基础语法和并发模型后,我们正式进入 Web 开发领域。Go 的标准库 net/http 已经足够强大,无需第三方框架就能构建一个完整的 RESTful API 服务。本文从 http.Handler 接口和 http.ServeMux 路由器讲起,深入讲解如何编写 Handler 处理 HTTP 请求、如何读取请求参数和请求体、如何返回 JSON 响应,以及如何用闭包实现中间件。最后通过一个完整的“图书管理”CRUD API 实战,帮你建立 Go Web 开发的基础认知。
一、net/http 核心接口:http.Handler
net/http 包的核心是 http.Handler 接口:
typeHandlerinterface{ServeHTTP(w http.ResponseWriter,r*http.Request)}任何实现了 ServeHTTP 方法的类型都可以作为 HTTP 处理器。
1.1 函数式 Handler:http.HandlerFunc
http.HandlerFunc 是一个函数类型,它实现了 http.Handler 接口,允许将普通函数直接转换为 Handler:
packagemainimport("fmt""net/http")// 普通函数作为 HandlerfunchelloHandler(w http.ResponseWriter,r*http.Request){fmt.Fprintf(w,"Hello, World!")}funcmain(){// http.HandlerFunc 将函数转换为 Handlerhttp.HandleFunc("/hello",helloHandler)// 启动服务器http.ListenAndServe(":8080",nil)}访问 http://localhost:8080/hello,即可看到 Hello, World!。
1.2 结构体 Handler(面向对象风格)
typeGreetingHandlerstruct{Namestring}func(h*GreetingHandler)ServeHTTP(w http.ResponseWriter,r*http.Request){fmt.Fprintf(w,"Greeting from %s",h.Name)}funcmain(){handler:=&GreetingHandler{Name:"MyApp"}http.Handle("/greeting",handler)http.ListenAndServe(":8080",nil)}二、处理 HTTP 请求
2.1 读取 URL 参数(Query String)
使用 r.URL.Query() 读取查询参数:
funcgetUserHandler(w http.ResponseWriter,r*http.Request){// 获取查询参数query:=r.URL.Query()name:=query.Get("name")// /user?name=Aliceage:=query.Get("age")// /user?name=Alice&age=30fmt.Fprintf(w,"name: %s, age: %s",name,age)}2.2 读取路径参数(Path Parameters)
Go 标准库不直接支持路由参数(如 /user/123),需要自行解析路径,或使用 http.ServeMux 的路径匹配模式。更常用的是直接使用 r.URL.Path 解析,或者后续章节引入 Gin 框架来处理更复杂的路由。
funcuserHandler(w http.ResponseWriter,r*http.Request){// 解析路径:/users/123path:=r.URL.Path// 简单解析parts:=strings.Split(path,"/")iflen(parts)>=3{id:=parts[2]// 123fmt.Fprintf(w,"User ID: %s",id)}}2.3 读取请求体(POST / PUT)
对于 POST/PUT 请求,需要从请求体中读取数据:
import("encoding/json""io""net/http")typeUserstruct{Namestring`json:"name"`Ageint`json:"age"`}funccreateUserHandler(w http.ResponseWriter,r*http.Request){// 只允许 POST 方法ifr.Method!=http.MethodPost{http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)return}// 读取请求体body,err:=io.ReadAll(r.Body)iferr!=nil{http.Error(w,"读取请求体失败",http.StatusBadRequest)return}deferr.Body.Close()// 解析 JSONvaruser Useriferr:=json.Unmarshal(body,&user);err!=nil{http.Error(w,"JSON 解析失败",http.StatusBadRequest)return}fmt.Fprintf(w,"接收到用户: %s, 年龄: %d",user.Name,user.Age)}三、返回 HTTP 响应
3.1 返回 JSON 响应
使用 json.NewEncoder 可以方便地返回 JSON 格式的响应:
typeResponsestruct{Codeint`json:"code"`Messagestring`json:"message"`Datainterface{}`json:"data,omitempty"`}funcapiHandler(w http.ResponseWriter,r*http.Request){// 设置响应头w.Header().Set("Content-Type","application/json")// 构造响应数据resp:=Response{Code:200,Message:"success",Data:map[string]string{"status":"ok"},}// 返回 JSONw.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(resp)}3.2 设置状态码和响应头
funchandler(w http.ResponseWriter,r*http.Request){// 设置响应头w.Header().Set("X-Custom-Header","my-value")w.Header().Set("Content-Type","application/json")// 设置状态码w.WriteHeader(http.StatusCreated)// 201// 写入响应体w.Write([]byte(`{"status":"ok"}`))}四、中间件(Middleware)
中间件是一个高阶函数,它接收一个 http.Handler 并返回一个新的 http.Handler,可以在请求处理前后执行逻辑。net/http 标准库中没有直接定义中间件模式,但通过闭包可以非常方便地实现。
// 日志中间件funcloggerMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){fmt.Printf("[%s] %s %s\n",time.Now().Format("2006-01-02 15:04:05"),r.Method,r.URL.Path)next.ServeHTTP(w,r)// 调用下一个处理器})}// Recovery 中间件(捕获 panic)funcrecoveryMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){deferfunc(){iferr:=recover();err!=nil{log.Printf("panic 恢复: %v",err)http.Error(w,"服务器内部错误",http.StatusInternalServerError)}}()next.ServeHTTP(w,r)})}// 应用中间件funcmain(){mux:=http.NewServeMux()mux.HandleFunc("/",helloHandler)// 中间件链handler:=recoveryMiddleware(loggerMiddleware(mux))http.ListenAndServe(":8080",handler)}五、实战:图书管理 CRUD API(纯标准库)
下面是一个完整的图书管理 API,包含创建、查询、更新、删除操作,全部基于标准库实现:
packagemainimport("encoding/json""fmt""net/http""sync")// 图书模型typeBookstruct{IDstring`json:"id"`Titlestring`json:"title"`Authorstring`json:"author"`Priceint`json:"price"`}// 图书仓库typeBookStorestruct{mu sync.RWMutex booksmap[string]Book}funcNewBookStore()*BookStore{return&BookStore{books:make(map[string]Book),}}func(s*BookStore)Create(book Book){s.mu.Lock()defers.mu.Unlock()s.books[book.ID]=book}func(s*BookStore)Get(idstring)(Book,bool){s.mu.RLock()defers.mu.RUnlock()book,ok:=s.books[id]returnbook,ok}func(s*BookStore)GetAll()[]Book{s.mu.RLock()defers.mu.RUnlock()books:=make([]Book,0,len(s.books))for_,book:=ranges.books{books=append(books,book)}returnbooks}func(s*BookStore)Update(idstring,book Book)bool{s.mu.Lock()defers.mu.Unlock()if_,ok:=s.books[id];!ok{returnfalse}book.ID=id s.books[id]=bookreturntrue}func(s*BookStore)Delete(idstring)bool{s.mu.Lock()defers.mu.Unlock()if_,ok:=s.books[id];!ok{returnfalse}delete(s.books,id)returntrue}// ---- HTTP Handlers ----varstore=NewBookStore()funcrespondJSON(w http.ResponseWriter,statusint,datainterface{}){w.Header().Set("Content-Type","application/json")w.WriteHeader(status)json.NewEncoder(w).Encode(data)}funcgetAllBooks(w http.ResponseWriter,r*http.Request){books:=store.GetAll()respondJSON(w,http.StatusOK,books)}funcgetBookByID(w http.ResponseWriter,r*http.Request){// 简单路径解析:/books/123id:=r.URL.Path[len("/books/"):]ifid==""{http.Error(w,"id 不能为空",http.StatusBadRequest)return}book,ok:=store.Get(id)if!ok{http.Error(w,"图书不存在",http.StatusNotFound)return}respondJSON(w,http.StatusOK,book)}funccreateBook(w http.ResponseWriter,r*http.Request){ifr.Method!=http.MethodPost{http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)return}varbook Bookiferr:=json.NewDecoder(r.Body).Decode(&book);err!=nil{http.Error(w,"JSON 解析失败",http.StatusBadRequest)return}deferr.Body.Close()ifbook.ID==""{http.Error(w,"id 不能为空",http.StatusBadRequest)return}store.Create(book)respondJSON(w,http.StatusCreated,book)}funcupdateBook(w http.ResponseWriter,r*http.Request){ifr.Method!=http.MethodPut{http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)return}id:=r.URL.Path[len("/books/"):]ifid==""{http.Error(w,"id 不能为空",http.StatusBadRequest)return}varbook Bookiferr:=json.NewDecoder(r.Body).Decode(&book);err!=nil{http.Error(w,"JSON 解析失败",http.StatusBadRequest)return}deferr.Body.Close()if!store.Update(id,book){http.Error(w,"图书不存在",http.StatusNotFound)return}respondJSON(w,http.StatusOK,book)}funcdeleteBook(w http.ResponseWriter,r*http.Request){ifr.Method!=http.MethodDelete{http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)return}id:=r.URL.Path[len("/books/"):]ifid==""{http.Error(w,"id 不能为空",http.StatusBadRequest)return}if!store.Delete(id){http.Error(w,"图书不存在",http.StatusNotFound)return}w.WriteHeader(http.StatusNoContent)}funcmain(){// 初始化一些示例数据store.Create(Book{ID:"1",Title:"Go 入门",Author:"张三",Price:59})store.Create(Book{ID:"2",Title:"微服务架构",Author:"李四",Price:89})// 路由http.HandleFunc("/books",func(w http.ResponseWriter,r*http.Request){switchr.Method{casehttp.MethodGet:getAllBooks(w,r)casehttp.MethodPost:createBook(w,r)default:http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)}})http.HandleFunc("/books/",func(w http.ResponseWriter,r*http.Request){switchr.Method{casehttp.MethodGet:getBookByID(w,r)casehttp.MethodPut:updateBook(w,r)casehttp.MethodDelete:deleteBook(w,r)default:http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)}})fmt.Println("服务器启动在 :8080")http.ListenAndServe(":8080",nil)}测试 API:
# 获取所有图书curlhttp://localhost:8080/books# 创建图书curl-XPOST http://localhost:8080/books-H"Content-Type: application/json"-d'{"id":"3","title":"Go 并发", "author":"王五", "price":69}'# 获取单本图书curlhttp://localhost:8080/books/1# 更新图书curl-XPUT http://localhost:8080/books/1-H"Content-Type: application/json"-d'{"title":"Go 入门(第二版)", "author":"张三", "price":79}'# 删除图书curl-XDELETE http://localhost:8080/books/1六、小结
http.Handler:核心接口,ServeHTTP(w http.ResponseWriter, r *http.Request) 是 Go Web 的起点。
路由:http.ServeMux 是标准库的路由器,支持路径匹配。
请求处理:r.URL.Query() 读取查询参数,io.ReadAll(r.Body) 读取请求体,json.Unmarshal 解析 JSON。
响应返回:w.Header().Set() 设置响应头,w.WriteHeader() 设置状态码,json.NewEncoder(w).Encode() 返回 JSON。
中间件:使用闭包实现,接收 http.Handler 返回 http.Handler,可以实现日志、鉴权、恢复等功能。