Golang网络编程中的错误处理规范

Go HTTP handler 中错误不可 panic,须转为对应HTTP状态码;net.DialContext超时需用errors.Is区分DeadlineExceeded与Canceled;io.ReadFull/ReadAtLeast必须显式检查io

.ErrUnexpectedEOF;自定义net.Error应实现Timeout()和Temporary()。

Go HTTP handler 中的错误不能直接 panic

HTTP handler 函数签名是 func(http.ResponseWriter, *http.Request),它不返回 error,也不接受 recover 机制自动兜底。一旦在 handler 内部 panic,默认会触发 http.DefaultServeMux 的兜底逻辑:打印堆栈、返回 500,并可能暴露敏感路径信息。

正确做法是把错误转为 HTTP 状态码和响应体:

  • 业务校验失败(如参数缺失)→ http.StatusBadRequest(400)
  • 资源未找到 → http.StatusNotFound(404)
  • 权限不足 → http.StatusForbidden(403)或 http.StatusUnauthorized(401)
  • 后端服务不可用 → http.StatusServiceUnavailable(503)
func myHandler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "missing 'id' parameter", http.StatusBadRequest)
        return
    }
    // ... 处理逻辑
}

net.DialContext 超时错误要区分 timeout vs. cancelled

net.DialContext 返回的 error 可能是 context.DeadlineExceeded(超时)或 context.Canceled(主动取消),二者语义不同,影响重试策略和日志分级。

直接用 errors.Is(err, context.DeadlineExceeded) 判断超时;用 errors.Is(err, context.Canceled) 区分是否由上层主动中断。

  • 超时通常需记录为 warn 级别,并考虑降级或重试
  • Cancel 多数来自 client 断连或 request context cancel,一般不报警
  • 避免用 strings.Contains(err.Error(), "timeout") —— 不可靠,且无法兼容自定义 net.Conn 实现

io.ReadFull 和 io.ReadAtLeast 的错误类型必须显式检查

这两个函数在读取不足时返回 io.ErrUnexpectedEOF,而不是 io.EOF。若与普通 io.Read 混淆处理,容易误判协议帧完整性。

例如解析固定头长的二进制协议时:

  • io.ReadFull(conn, header[:]) 失败 → 一定是连接提前关闭或数据截断,应关闭 conn 并记录 error
  • err == io.EOF 表示流正常结束,可安全退出
  • errors.Is(err, io.ErrUnexpectedEOF) 表示协议违规,大概率是客户端 bug 或中间设备干扰
header := make([]byte, 8)
_, err := io.ReadFull(conn, header)
if err != nil {
    if errors.Is(err, io.ErrUnexpectedEOF) {
        log.Warn("incomplete header received")
        return
    }
    if errors.Is(err, io.EOF) {
        log.Info("connection closed gracefully")
        return
    }
    log.Error("read header failed", "err", err)
    return
}

自定义 net.Error 需实现 Timeout() 和 Temporary() 方法

当封装底层网络调用(如 TLS 握手、DNS 解析)并返回自定义 error 时,若未实现 Timeout()Temporary(),上层库(如 http.Transport)无法判断是否可重试。

典型后果:本该重试的临时 DNS 错误被当作永久失败,导致请求直接失败而非 fallback。

  • 网络抖动、连接拒绝(ECONNREFUSED)、TLS handshake timeout → 应返回 Temporary() == true
  • 证书过期、SNI 不匹配、协议不支持 → Temporary() == false
  • 所有超时类错误必须返回 Timeout() == true

Go 标准库中 net.OpError 已正确实现这两方法,优先复用而非自己造轮子。