最简http服务用http.ListenAndServe(“:8080”, nil),默认使用http.DefaultServeMux;需自定义路由时应创建http.ServeMux实例并显式传入server.Handler,注意其仅支持前缀匹配且同路径后注册覆盖前注册。

go 的 net/http 标准库足够轻量、稳定,直接用它写生产级 HTTP 服务完全可行,不需要立刻上框架。
怎么快速启动一个 HTTP 服务
最简方式是调用 http.ListenAndServe,传入监听地址和处理器。默认使用 http.DefaultServeMux,也就是全局的多路复用器。
常见写法:
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) }) http.ListenAndServe(":8080", nil) // nil 表示用默认多路复用器 }
注意:http.ListenAndServe 默认启用 HTTP/1.1,不支持 https(需用 http.ListenAndServeTLS);端口被占用时会直接 panic,建议加错误处理。
立即学习“go语言免费学习笔记(深入)”;
- 监听地址写
":8080"表示绑定所有网卡,写"127.0.0.1:8080"更安全(仅本地访问) - 如果处理器逻辑稍复杂,别把业务逻辑全塞进匿名函数里,应拆成独立函数,便于测试和复用
-
nil作为第二个参数虽方便,但隐藏了路由控制权——后续想换多路复用器或加中间件时,得改这里
如何自定义路由和处理器
直接操作 http.ServeMux 实例,能明确控制路由注册行为,也利于单元测试(比如传入 mock 的 *http.ServeMux)。
示例:
func main() { mux := http.NewServeMux() mux.HandleFunc("/api/users", usersHandler) mux.HandleFunc("/health", healthHandler) server := &http.Server{ Addr: ":8080", Handler: mux, } server.ListenAndServe() }
关键点:
-
http.ServeMux只支持前缀匹配(/api会匹配/api/users和/api/foo/bar),不支持通配符或正则——需要路径参数或 REST 风格路由时,得自己解析r.URL.Path或换第三方路由器(如gorilla/mux、chi) - 处理器函数签名必须是
func(http.ResponseWriter, *http.Request),否则编译报错:cannot use ... (type func()) as type func(http.ResponseWriter, *http.Request) in argument to mux.HandleFunc - 多个
HandleFunc注册相同路径,后注册的会覆盖前一个,无警告
怎么读取请求参数和解析 jsON
GET 查询参数用 r.URL.Query().Get("key"),POST 表单用 r.FormValue("key")(自动调用 ParseForm),json 请求体需手动解码。
典型 JSON 处理片段:
func apiPostHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var data struct { Name string `json:"name"` Age int `json:"age"` } if err := json.NewDecoder(r.Body).Decode(&data); err != nil { http.Error(w, "Invalid JSON", http.StatusbadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) }
容易踩的坑:
-
r.Body是io.ReadCloser,只可读一次;若中间件已读过(如日志中间件调用了r.ParseForm()或io.ReadAll(r.Body)),后续再读就会得到空内容 - 没设
Content-Type: application/json头时,json.Decode仍可能成功(只要内容合法),但前端发错类型时服务端难以区分意图 -
http.Error会立即写响应并返回,但不会终止 handler 函数执行——后面代码仍会运行,可能导致重复写响应(触发http: multiple response.WriteHeader calls错误)
怎么安全关闭 HTTP 服务
http.Server.Shutdown 是唯一推荐的优雅关机方式,它会等待活跃连接完成处理后再退出。直接杀进程或用 server.Close() 会导致正在处理的请求被中断。
示例:
server := &http.Server{Addr: ":8080", Handler: mux} go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatal(err) } }() // 收到 SIGINT/SIGTERM 后触发关机 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server.Shutdown(ctx)
注意点:
-
Shutdown不会关闭监听 socket 直到所有连接完成,超时时间要结合业务响应时长设(例如有长轮询或流式接口,就得设更长) - 若 handler 中有阻塞操作(如未设超时的数据库查询、外部 HTTP 调用),需在对应上下文中传入取消信号,否则
Shutdown会等满超时时间 - 标准库不提供内置的请求超时控制,需在
http.Server中配置ReadTimeout、WriteTimeout或用context手动控制 handler 内部逻辑
真正难的不是写出能跑的 HTTP 服务,而是让每个 handler 在高并发下不泄露 goroutine、不阻塞主线程、不忽略错误返回、不滥用全局状态——这些细节不会出现在入门示例里,但线上出问题时,往往就卡在这几步。