Go错误与日志处理—推荐实践
错误的分类
在 Go 语言中,错误是通过实现 error
接口的类型表示的,但不同场景下的错误可以按性质和用途进行分类。以下是 Go 语言错误的常见分类,以及每类错误的解释和示例:
标准错误类型
标准库中定义了许多常见的错误类型,用于表示各种常见的错误场景。以下是一些 Go 标准库中常见的错误类型和相关包:
errors.New
和 fmt.Errorf
-
用于创建自定义的错误。
-
标准库提供的最基础的错误类型。
示例:
import (
"errors"
"fmt"
)
err1 := errors.New("this is an error")
err2 := fmt.Errorf("formatted error: %d", 42)
IO 相关错误
io
包
-
包含基础 I/O 操作的错误类型。
常见错误:
-
io.EOF
:表示流结束(End Of File)。 -
io.ErrUnexpectedEOF
:在读取流时遇到意外的 EOF。 -
io.ErrClosedPipe
:操作已关闭的管道。
示例:
import "io"
if err == io.EOF {
fmt.Println("Reached end of file")
}
文件操作相关错误
os
包
-
处理文件系统相关的错误。
常见错误:
-
os.ErrNotExist
:文件或目录不存在。 -
os.ErrExist
:文件或目录已经存在。 -
os.ErrPermission
:权限不足。 -
os.ErrInvalid
:无效操作。
示例:
import "os"
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
}
网络相关错误
net
包
-
网络操作相关的错误。
常见错误:
-
net.InvalidAddrError
:无效地址错误。 -
net.UnknownNetworkError
:未知网络类型错误。 -
net.AddrError
:地址解析错误。 -
net.DNSError
:域名解析错误。
示例:
import "net"
_, err := net.LookupHost("invalid_domain")
if dnsErr, ok := err.(*net.DNSError); ok {
fmt.Println("DNS error:", dnsErr)
}
JSON 相关错误
encoding/json
包
-
JSON 编码和解码的错误。
常见错误:
-
json.InvalidUnmarshalError
:解码到无效的目标。 -
json.UnmarshalTypeError
:JSON 与目标类型不匹配。
示例:
import "encoding/json"
var data interface{}
err := json.Unmarshal([]byte("invalid json"), &data)
if syntaxErr, ok := err.(*json.SyntaxError); ok {
fmt.Println("JSON Syntax Error at offset:", syntaxErr.Offset)
}
HTTP 相关错误
net/http
包
-
HTTP 请求与响应相关的错误。
常见错误:
-
http.ErrHandlerTimeout
:HTTP 处理程序超时。 -
http.ErrBodyNotAllowed
:HTTP 请求体不被允许。
示例:
import "net/http"
if errors.Is(err, http.ErrHandlerTimeout) {
fmt.Println("HTTP handler timeout")
}
时间解析相关错误
time
包
-
处理时间解析或格式化错误。
常见错误:
-
time.ErrBad
:时间字符串格式错误。
示例:
import "time"
_, err := time.Parse("2006-01-02", "invalid-date")
if err != nil {
fmt.Println("Time parsing error:", err)
}
数据库相关错误
database/sql
包
-
数据库操作相关的错误。
常见错误:
-
sql.ErrNoRows
:查询未返回结果。 -
sql.ErrTxDone
:事务已完成,不能再执行操作。
示例:
import "database/sql"
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("No rows found")
}
压缩解压相关错误
compress/gzip
包
-
用于处理 gzip 格式的错误。
常见错误:
-
gzip.ErrHeader
:gzip 文件头错误。
加密解密相关错误
crypto
和 crypto/x509
包
-
加密或证书解析相关错误。
常见错误:
-
x509.IncorrectPasswordError
:密码错误。 -
x509.UnknownAuthorityError
:未知的证书颁发机构。
按错误来源分类
应用级错误
应用程序逻辑中定义的错误,如输入验证失败、业务规则不满足等。这些错误通常由程序员明确定义。
示例:
type ValidationError struct {
Field string
Msg string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %s: %s", e.Field, e.Msg)
}
系统级错误
系统资源相关的错误,包括文件访问、网络问题等。 示例:
func readConfig(filename string) error {
_, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}
第三方库错误
使用第三方库时返回的错误,需要通过文档或代码了解这些错误的含义,并采取适当措施。 示例:
func sendMessageToKafka() error {
err := producer.SendMessage(message)
if err != nil {
return fmt.Errorf("kafka producer error: %w", err)
}
return nil
}
按错误处理方式分类
可恢复错误
可以通过重新尝试或特定逻辑处理恢复的错误。 示例:
func retryOperation(attempts int) error {
for i := 0; i < attempts; i++ {
err := doSomething()
if err == nil {
return nil
}
time.Sleep(1 * time.Second) // 等待后重试
}
return fmt.Errorf("operation failed after %d attempts", attempts)
}
不可恢复错误
表示程序的逻辑或系统的严重错误,无法通过重新尝试解决,如非法状态、编程错误等。
示例:
func mustDivide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
按错误语义分类
用户输入错误
用户提供的输入不满足预期导致的错误。
示例:
func validateInput(input string) error {
if input == "" {
return fmt.Errorf("input cannot be empty")
}
return nil
}
数据处理错误
数据格式、解析、转换等问题。
示例:
func parseInt(value string) (int, error) {
num, err := strconv.Atoi(value)
if err != nil {
return 0, fmt.Errorf("failed to parse integer: %w", err)
}
return num, nil
}
网络/IO 错误
网络连接失败、超时、文件系统操作失败等问题。
示例:
func fetchData(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch data: %w", err) } defer resp.Body.Close() return io.ReadAll(resp.Body) }
业务逻辑错误
业务逻辑不满足需求导致的错误。
示例:
func checkAccountBalance(balance, withdrawAmount float64) error {
if withdrawAmount > balance {
return fmt.Errorf("insufficient balance")
}
return nil
}
按错误表现分类
明确错误
明确的错误通过 error
接口表示,并具有清晰的语义。
示例:
return fmt.Errorf("unable to connect to database: %w", err)
模糊错误
返回的错误缺乏上下文信息,不利于调试。
示例:
return errors.New("something went wrong") // 不清楚具体问题是什么
总结
Go 中的错误分类可以帮助开发者更清晰地理解错误的来源和性质,从而制定合理的处理策略。推荐:
-
使用明确的错误上下文。
-
尽量细化错误类型,尤其是应用级错误。
-
使用
errors.Is
和errors.As
对错误进行分类处理。 -
在必要的场景下记录日志,但不要重复记录错误信息。
错误处理规范
错误检查与优先处理
-
及时检查错误:不要忽略返回的错误值。
-
优先处理错误:如果发生错误,尽快中止当前流程或采取修复措施。
好的示例:
func readFile(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filename, err)
}
return data, nil
}
坏的示例:
func readFile(filename string) ([]byte, error) {
data, _ := os.ReadFile(filename) // 忽略错误,可能导致不可预见的问题
return data, nil
}
使用错误包装提供上下文信息
-
使用
fmt.Errorf
和%w
包装错误,保留错误链路。 -
错误信息应清晰表明发生错误的上下文。
好的示例:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", filename, err)
}
defer file.Close() // 文件处理逻辑... return nil
}
坏的示例:
func processFile(filename string) error {
_, err := os.Open(filename)
if err != nil {
return err // 丢失了错误上下文,难以追踪来源
}
return nil
}
自定义错误类型
-
针对特定业务场景,创建自定义错误类型以提供丰富的上下文。
好的示例:
type ValidationError struct {
Field string Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %s: %s", e.Field, e.Message)
}
func validateInput(input string) error {
if input == "" {
return ValidationError{"input", "cannot be empty"}
}
return nil
}
坏的示例:
func validateInput(input string) error {
if input == "" {
return fmt.Errorf("invalid input") // 错误信息缺乏上下文
}
return nil
}
使用 errors.Is
和 errors.As
检查错误
-
使用
errors.Is
检查错误是否是某种特定类型。 -
使用
errors.As
提取并处理特定的错误类型。
对比
特性 | errors.Is | errors.As |
用途 | 检查错误值是否相等或包装目标错误 | 检查错误是否为特定类型 |
参数 | 错误和目标错误值 | 错误和目标错误类型的指针 |
返回值 | 布尔值 | 布尔值,目标指针可能会被赋值 |
支持链式错误 | 是 | 是 |
适用场景 | 判断是否是某个特定错误 | 判断是否属于某个特定类型的错误 |
好的示例:
func handleError(err error) {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("路径错误: %s\n", pathErr.Path)
}
}
避免滥用 panic
,使用显式错误返回
-
panic
仅用于不可恢复的错误,普通错误应返回error
。 -
提供有意义的错误信息。
好的示例:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
坏的示例:
func divide(a, b int) int {
if b == 0 {
panic("division by zero") // 滥用 panic,不建议用于常规错误处理
}
return a / b
}
日志与错误分离
-
错误和日志分层:日志应由调用方处理,库函数仅返回错误。
-
日志通常在服务层或调用者处理,库函数不应记录日志。
好的示例:
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch data from %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
坏的示例:
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
log.Printf("error: %v", err) // 不必要的日志记录
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
使用 defer
简化资源清理
-
使用
defer
保证资源在函数退出时被正确释放。
好的示例:
func processLargeFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close() // 确保资源释放
// 文件处理逻辑...
return nil
}
坏的示例:
func processLargeFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
} // 如果忘记关闭文件,会导致资源泄露
file.Close()
return nil
}
分层处理错误
-
在业务逻辑层返回错误,允许调用方决定是否记录日志。
-
在顶层捕获错误并进行统一处理。
好的实践:
// 库函数
func queryDatabase(query string) ([]Record, error) {
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
defer rows.Close() // 解析数据...
return records, nil
}
// 应用层
func handleRequest(query string) {
records, err := queryDatabase(query)
if err != nil {
log.Printf("query error: %v", err)
return
}
fmt.Println(records)
}
错误与日志处理的推荐实践
不同层级对错误和日志的处理
数据访问层(DAL/Repository)
职责:直接与数据库或其他持久化存储交互。
-
错误处理:
-
返回具体的、易于处理的错误。例如:SQL 执行失败、数据未找到等。
-
尽量使用错误包装 (
fmt.Errorf
),为上层提供上下文。 -
不要记录日志,交由上层决定是否需要记录。
-
-
示例:
func GetUserByID(id int) (*User, error) {
user := &User{}
err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user with ID %d not found: %w", id, err)
}
if err != nil {
return nil, fmt.Errorf("failed to fetch user: %w", err)
}
return user, nil
}
服务层(Service/Use Case)
职责:实现业务逻辑。
-
错误处理:
-
捕获底层错误并添加业务语义上下文。
-
根据需要返回特定业务错误或通用错误。
-
可对一些关键错误进行日志记录(如影响业务流程的错误)。
-
-
日志记录:
-
记录错误可能对调试或审计有价值的信息。
-
日志应包含业务上下文(如用户 ID、请求参数等)。
-
-
示例:
func ProcessOrder(orderID int) error {
order, err := repo.GetOrderByID(orderID)
if err != nil {
return fmt.Errorf("failed to process order %d: %w", orderID, err)
}
if order.Status != "pending" {
return fmt.Errorf("order %d is not in a pending state", orderID)
}
// 业务逻辑...
return nil
}
控制器层(Controller/Handler)
职责:处理用户请求并返回响应。
-
错误处理:
-
将服务层的错误转换为用户友好的消息(HTTP 状态码或自定义响应)。
-
不应暴露底层实现细节。
-
-
日志记录:
-
在请求入口处记录重要的请求信息。
-
在错误返回时记录错误上下文和请求相关信息。
-
-
示例:
func OrderHandler(w http.ResponseWriter, r *http.Request) {
orderID, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "invalid order ID", http.StatusBadRequest)
return
}
err = service.ProcessOrder(orderID)
if err != nil {
log.Printf("failed to process order: %v", err)
http.Error(w, "failed to process order", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
错误与日志的推荐实践
错误返回层级
-
底层(如 DAL):
-
返回详细的上下文错误,方便上层理解问题。
-
不记录日志,避免重复记录。
-
-
中间层(如 Service):
-
包装底层错误,提供业务相关的上下文。
-
根据需要选择是否记录关键日志。
-
-
顶层(如 Controller):
-
转换错误为用户友好的消息。
-
记录完整的请求上下文和错误信息。
-
日志记录层级
-
入口点(Controller/Handler):
-
记录请求相关的信息(URL、参数、用户身份等)。
-
记录最终的响应状态。
-
-
服务层:
-
记录对业务有重要影响的错误或状态变化。
-
-
数据层:
-
尽量不记录日志,避免暴露内部实现细节。
-
常见反模式与改进
-
重复记录日志:
-
底层记录错误,上层再次记录相同错误,导致日志冗余。
-
改进:仅在一个明确的层级记录日志。
-
-
暴露内部错误细节:
-
直接将数据库错误返回到用户端。
-
改进:在顶层捕获并转换为用户友好的消息。
-
-
忽略日志上下文:
-
日志中缺乏关键信息(如用户 ID、操作参数等)。
-
改进:在日志中包含足够的上下文信息。
-
总结
错误处理应遵循逐层封装的原则,每一层专注于自身职责,避免信息泄漏或日志冗余。日志记录应关注调试和审计价值,并在错误信息中添加业务或操作上下文,从而提高系统的可维护性和可观测性。