最简 HTTP 服务需用 net/http 启动并必须检查 http.ListenAndServe 错误;监听地址应写为 ":8080";路由可用 http.ServeMux;处理 JSON 时直接解码 r.Body 且只读一次;热重载推荐 air 工具。

用 net/http 启一个最简 HTTP 服务
Go 自带 net/http 包,不用装第三方库就能跑起一个可访问的 HTTP 服务。关键不是“怎么写”,而是“别漏掉 http.ListenAndServe 的错误处理”——很多人本地跑起来没报错,部署后服务静默退出,就是这里没检查返回值。
常见错误现象:go run main.go 看似运行了,但 curl localhost:8080 返回 connection refused;或者程序启动后立刻退出,终端没任何提示。
-
http.ListenAndServe在端口被占用或权限不足时会直接返回 error,**不会 panic**,必须显式判断 - 监听地址建议写成
":8080"(冒号开头),而非"localhost:8080",后者在某些容器或远程环境可能绑定失败 - 如果想复用已关闭的端口,加一行
http.Server{Addr: ":8080", ...}.ListenAndServe()并设置ReusePort: true(需 Go 1.19+)
package mainimport ( "fmt" "log" "net/http" )
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, Go Web!") })
log.Println("Server starting on :8080") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) // ← 这行不能省 }}
路由不够用?别急着换 Gin,先试试 http.ServeMux
新手常以为“没路由就用框架”,其实
http.ServeMux已经支持前缀匹配、子路径注册和基本的 404 控制。Gin 的优势在中间件、结构化参数解析、性能优化,不是“有没有路由”。过早引入框架反而掩盖了 Go HTTP 模型的本质。使用场景:API 分组(如
/api/v1/users)、静态文件托管、健康检查端点(/healthz)。
-
http.Handle和http.HandleFunc底层都用默认的http.DefaultServeMux,但自定义http.ServeMux更利于测试和隔离 - 注册路径以
/结尾(如/static/)会自动匹配子路径;不加斜杠(如/api)只精确匹配 - 404 不是自动返回的——如果请求路径没匹配到任何 handler,
DefaultServeMux才返回 404;自定义 mux 需手动设置mux.NotFoundHandler
package mainimport ( "fmt" "log" "net/http" )
func main() { mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") }) mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "API path: %s", r.URL.Path) }) // 手动控制 404 mux.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) }) log.Println("Server starting on :8080") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatal(err) }}
接收 JSON 请求体时,为什么 r.Body 总是空的?
不是代码写错了,大概率是没调用
r.ParseForm()或没读取r.Body。Go 的http.Request不会自动解析 body 内容——它把原始字节流交给你自己处理,这是设计选择,不是 bug。常见错误现象:打印
r.FormValue("key")为空;json.NewDecoder(r.Body).Decode(&v)报EOF或invalid character;Postman 发 JSON,服务端收不到字段。
- 如果 Content-Type 是
application/json,直接读r.Body,**不要调用r.ParseForm()**(它只处理application/x-www-form-urlencoded和multipart/form-data) -
r.Body是io.ReadCloser,只能读一次;后续再读会得到空内容,必要时用io.ReadAll先缓存 - 记得设响应头:
w.Header().Set("Content-Type", "application/json; charset=utf-8"),否则前端可能解析失败
package mainimport ( "encoding/json" "log" "net/http" )
type User struct { Name string
json:"name"Email stringjson:"email"}func createUser(w http.ResponseWriter, r *http.Request) { var u User if err := json.NewDecoder(r.Body).Decode(&u); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return }
w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(map[string]string{ "message": "created", "name": u.Name, })}
func main() { http.HandleFunc("/api/user", createUser) log.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
开发阶段热重载怎么做?别碰 go install -buildmode=plugin
Go 官方不支持运行时重载代码,所谓“热更新”本质都是进程级重启。新手容易被各种插件、构建脚本绕晕,结果本地调试时改一行代码要等 5 秒,还以为是 Go 慢。
真正轻量、稳定、无依赖的做法只有两个:
- 用
air:安装简单(curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin),配置一个.air.toml就能监听.go文件变化并重启 - 用 Makefile +
inotifywait(Linux/macOS)或fswatch(macOS):比写 shell 脚本更可控,适合后期接入 CI - 绝对避免
go:generate或plugin做热重载——它们有平台限制、符号冲突风险,且无法 reload 全局变量和 init 函数
复杂点在于:HTTP 服务器 shutdown 需要优雅等待(比如正在处理的请求完成),否则并发请求可能被中断。用 http.Server.Shutdown 配合 context 是标准解法,但 air 默认不支持,得自己写 wrapper。










