最简 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 main
<p>import (
"fmt"
"log"
"net/http"
)</p><p>func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, Go Web!")
})</p><pre class='brush:php;toolbar:false;'>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 main
<p>import (
"fmt"
"log"
"net/http"
)</p><p>func main() {
mux := http.NewServeMux()</p><pre class='brush:php;toolbar:false;'>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 main
<p>import (
"encoding/json"
"log"
"net/http"
)</p><p>type User struct {
Name string <code>json:"name"</code>
Email string <code>json:"email"</code>
}</p><p>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
}</p><pre class='brush:php;toolbar:false;'>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。










