最简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、不阻塞主线程、不忽略错误返回、不滥用全局状态——这些细节不会出现在入门示例里,但线上出问题时,往往就卡在这几步。










