跟着做完这篇,你能得到一个可直接 go run 启动、带分层目录、统一响应格式和优雅关闭的 Go RESTful API 项目骨架,后面加业务只需往固定位置填代码。
第一步:确认 Go 版本并初始化模块
先确认版本,路由部分要用到 Go 1.22 引入的「方法 + 路径参数」写法:
go version # 需要 go1.22 及以上
mkdir myapi && cd myapi
go mod init github.com/yourname/myapi
注意:go.mod 里的 go 行必须 ≥ 1.22。如果写了 go 1.21,GET /users/{id} 这种路由模式会在运行时 panic,报 conflicts with pattern。
第二步:建立目录骨架
mkdir -p cmd/server internal/{config,handler,router,service,repository,middleware} pkg/response
touch cmd/server/main.go
约定如下:
cmd/server:程序入口,只负责组装和启动
internal/:业务代码,Go 的访问规则保证外部模块无法 import
pkg/response:可复用的通用工具(统一响应、错误码)
handler 只做参数解析和返回,service 写业务逻辑,repository 管数据访问
注意:internal 是 Go 的保留目录名,外部项目 import 会直接编译失败。别把希望被复用的包放进去。
第三步:写统一响应和错误码
pkg/response/response.go:
package response
import (
"encoding/json"
"net/http"
)
type Envelope struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
func JSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(Envelope{Code: 0, Message: "ok", Data: data})
}
func Error(w http.ResponseWriter, status int, code int, msg string) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(Envelope{Code: code, Message: msg})
}
先把响应外壳固定下来,后面所有接口只调这两个函数,前端对接成本会低很多。
第四步:写一个示例 handler
internal/handler/user.go:
package handler
import (
"net/http"
"github.com/yourname/myapi/pkg/response"
)
type UserHandler struct{}
func NewUserHandler() *UserHandler { return &UserHandler{} }
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
response.JSON(w, http.StatusOK, map[string]string{"id": id})
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
response.JSON(w, http.StatusCreated, map[string]string{"status": "created"})
}
注意:r.PathValue("id") 是标准库自带的取值方式。如果你之前用过 chi/gin,别写成 chi.URLParam 或 c.Param,混用会拿到空字符串。
第五步:注册路由
internal/router/router.go:
package router
import (
"net/http"
"github.com/yourname/myapi/internal/handler"
"github.com/yourname/myapi/internal/middleware"
"github.com/yourname/myapi/pkg/response"
)
func New() http.Handler {
mux := http.NewServeMux()
uh := handler.NewUserHandler()
mux.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) {
response.JSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
mux.HandleFunc("GET /api/v1/users/{id}", uh.Get)
mux.HandleFunc("POST /api/v1/users", uh.Create)
return middleware.Recover(middleware.Logger(mux))
}
路径统一带 /api/v1 前缀,未来升 v2 时新旧接口可以并存,不用一刀切。
第六步:加两个最必要的中间件
internal/middleware/middleware.go:
package middleware
import (
"log"
"net/http"
"time"
)
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v", err)
http.Error(w, `{"code":500,"message":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
注意:中间件顺序是「外层包内层」。Recover 放在最外层,才能兜住 Logger 和业务里的 panic;反过来写的话,业务 panic 会直接打断进程。
第七步:入口与优雅关闭
cmd/server/main.go:
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/yourname/myapi/internal/router"
)
func main() {
addr := os.Getenv("HTTP_ADDR")
if addr == "" {
addr = ":8080"
}
srv := &http.Server{
Addr: addr,
Handler: router.New(),
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Printf("listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("shutdown: %v", err)
}
log.Println("bye")
}
跑一下验证:
go run ./cmd/server
curl http://localhost:8080/api/v1/health
curl http://localhost:8080/api/v1/users/42
两个请求都应返回 {"code":0,"message":"ok",...}。
注意:ReadHeaderTimeout 建议必填,否则慢速攻击(Slowloris)可以直接把连接池拖住。生产环境再加 ReadTimeout 和 WriteTimeout。