在 Go 语言中,异常处理与传统的面向对象语言有所不同,主要通过返回错误值的方式来处理程序中的异常情况。虽然这种方式简洁明了,但在实际应用中,开发者常常会忽视错误处理的重要性,导致程序在运行时出现潜在问题或不易察觉的漏洞。

本模块将探讨 Go 语言中常见的异常处理错误,包括错误值的忽略、错误包装的误用以及错误的判断逻辑等问题。通过分析这些错误,帮助开发者理解如何有效地处理错误,避免因忽略异常情况而引发的 bug 或系统崩溃。掌握良好的错误处理习惯,不仅能提升代码的健壮性,还能提高系统的稳定性和可靠性。

错误四十八:Panicking (#48)

示例代码:

package main

import (
    "fmt"
    "os"
)

func LoadFunTesterConfig() {
    config, err := os.Open("FunTester.conf")
    if err != nil {
        panic(fmt.Sprintf("FunTester: 配置文件加载失败: %v", err))
    }
    defer config.Close()
    // 读取配置文件内容
    fmt.Println("FunTester: 配置文件已加载")
}

func main() {
    LoadFunTesterConfig()
    fmt.Println("FunTester: 程序继续运行")
}

错误说明:
在 Go 语言中,panic 用于处理不可恢复的错误,如程序无法继续执行下去的严重问题。然而,滥用 panic 会导致程序异常终止,难以维护和测试。就像在小问题上就大喊 “救命”,不可取。

可能的影响:
使用 panic 处理可恢复的错误会导致程序意外中断,影响用户体验和程序的稳定性。另外,过度使用 panic 会使得错误处理逻辑难以追踪和维护,增加了调试难度。

最佳实践:
仅在遇到无法恢复的错误时使用 panic,例如初始化时的重要资源失败。另外,优先考虑使用错误返回值进行错误处理,以便调用者能够根据需要决定如何应对错误。仅在不可挽回的情况才使用 panic,并确保在可能的情况下使用 recover 恢复程序的正常运行。

改进后的代码:

使用错误返回值而不是 panic

package main

import (
    "fmt"
    "os"
)

func LoadFunTesterConfig() error {
    config, err := os.Open("FunTester.conf")
    if err != nil {
        return fmt.Errorf("FunTester: 配置文件加载失败: %w", err)
    }
    defer config.Close()
    // 读取配置文件内容
    fmt.Println("FunTester: 配置文件已加载")
    return nil
}

func main() {
    if err := LoadFunTesterConfig(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("FunTester: 程序继续运行")
}

输出结果:

FunTester: 配置文件已加载
FunTester: 程序继续运行

错误四十九:未考虑何时才应该包装 error (#49)

示例代码:

package main

import (
    "fmt"
)

func ReadFunTesterFile(filename string) error {
    // 模拟读取文件错误
    return fmt.Errorf("FunTester: 无法读取文件 %s", filename)
}

func main() {
    err := ReadFunTesterFile("FunTester.txt")
    if err != nil {
        fmt.Printf("FunTester: 发生错误: %v\n", err)
        // 错误被进一步包装
        err = fmt.Errorf("FunTester: 处理文件时出错: %w", err)
    }
    fmt.Println("FunTester: 程序结束")
}

错误说明:
在 Go 语言中,错误包装(Wrapping error)能够为错误提供上下文信息,有助于定位问题。然而,过度或不必要地包装错误会引入潜在的耦合,使得原始错误对调用者可见,增加了代码的复杂性,就像在信封上贴了太多标签,难以辨认信件内容。

可能的影响:
包装错误可能导致调用者对错误的理解加深,但如果滥用,可能使得错误链变得混乱,难以追踪真实错误源头。此外,过度包装错误会增加代码的复杂性,影响性能和可读性。

最佳实践:
只在需要新增上下文信息时进行错误包装。避免无意义或重复地包装错误,保持错误链的清晰和简洁。使用 fmt.Errorf 搭配 %w 进行包装时,确保包装的意义明确且有助于错误定位。

改进后的代码:

在需要提供更多上下文时进行包装:

package main

import (
    "fmt"
)

func ReadFunTesterFile(filename string) error {
    // 模拟读取文件错误
    return fmt.Errorf("不可恢复的错误")
}

func OpenFunTesterFile(filename string) error {
    err := ReadFunTesterFile(filename)
    if err != nil {
        return fmt.Errorf("FunTester: 处理文件 %s 时出错: %w", filename, err)
    }
    return nil
}

func main() {
    err := OpenFunTesterFile("FunTester.txt")
    if err != nil {
        fmt.Printf("FunTester: 发生错误: %v\n", err)
        // 不进一步包装,保持错误链清晰
        // err = fmt.Errorf("FunTester: 处理文件时出错: %w", err)
    }
    fmt.Println("FunTester: 程序结束")
}

输出结果:

FunTester: 发生错误: FunTester: 处理文件 FunTester.txt 时出错: 不可恢复的错误
FunTester: 程序结束

错误五十:不正确的错误类型比较 (#50)

示例代码:

package main

import (
    "errors"
    "fmt"
)

var ErrFunTesterNotFound = errors.New("FunTester: 未找到")

func GetFunTester(id int) error {
    if id != 1 {
        return fmt.Errorf("FunTester: 获取FunTester失败: %w", ErrFunTesterNotFound)
    }
    return nil
}

func main() {
    err := GetFunTester(2)
    if err != nil {
        // 错误比较不正确
        if err == ErrFunTesterNotFound {
            fmt.Println("FunTester: FunTester未找到")
        } else {
            fmt.Println("FunTester: 其他错误", err)
        }
    }
}

错误说明:
在 Go 1.13 及以上版本,使用 fmt.Errorf 搭配 %w 进行错误包装时,直接使用 == 运算符比较无法正确判断错误是否是特定类型。需要使用 errors.Iserrors.As 来进行比较。这种误用就像试图用放大镜看显微镜里的细节,自然无效。

可能的影响:
错误比较不正确会导致错误处理逻辑失效,可能无法正确识别和响应特定错误类型。这会导致程序无法按照预期处理错误,影响程序的稳定性和可靠性。

最佳实践:
在进行错误比较时,使用 errors.Iserrors.As 来判断包装后的错误是否为特定错误类型。这确保了错误比较的正确性和健壮性,特别是在处理嵌套或包装错误时。

改进后的代码:

使用 errors.Is 进行正确的错误比较:

package main

import (
    "errors"
    "fmt"
)

var ErrFunTesterNotFound = errors.New("FunTester: 未找到")

func GetFunTester(id int) error {
    if id != 1 {
        return fmt.Errorf("FunTester: 获取FunTester失败: %w", ErrFunTesterNotFound)
    }
    return nil
}

func main() {
    err := GetFunTester(2)
    if err != nil {
        // 使用 errors.Is 进行正确的错误比较
        if errors.Is(err, ErrFunTesterNotFound) {
            fmt.Println("FunTester: FunTester未找到")
        } else {
            fmt.Println("FunTester: 其他错误", err)
        }
    }
}

输出结果:

FunTester: 其他错误 FunTester: 获取FunTester失败: FunTester: 未找到

错误五十一:不正确的错误对象值比较 (#51)

示例代码:

package main

import (
    "errors"
    "fmt"
)

var ErrFunTesterNotFound = errors.New("FunTester: 未找到")

func GetFunTester(id int) error {
    if id != 1 {
        return fmt.Errorf("FunTester: 获取FunTester失败: %w", ErrFunTesterNotFound)
    }
    return nil
}

func main() {
    err := GetFunTester(2)
    if err != nil {
        // 错误对象值比较不正确
        if err.Error() == "FunTester: 未找到" {
            fmt.Println("FunTester: FunTester未找到")
        } else {
            fmt.Println("FunTester: 其他错误", err)
        }
    }
}

错误说明:
即使错误信息与预期相符,直接通过 err.Error() == "FunTester: 未找到" 进行比较也是不正确的。这不仅效率低下,还容易因为错误信息的微小变化而导致比较失败。就像是用拼音代替汉字来比较,既不准确又不高效。

可能的影响:
错误对象值比较不正确,会导致错误处理逻辑无法正确判断特定错误类型,进而影响程序的稳定性和正确性。这可能会导致处理某些错误时,无法执行正确的响应措施。

最佳实践:
始终使用 errors.Iserrors.As 进行错误比较,避免通过字符串比较错误信息。这样不仅更准确,还能保持代码的健壮性和可维护性。

改进后的代码:

使用 errors.Is 进行正确的错误比较,避免通过字符串进行比较:

package main

import (
    "errors"
    "fmt"
)

var ErrFunTesterNotFound = errors.New("FunTester: 未找到")

func GetFunTester(id int) error {
    if id != 1 {
        return fmt.Errorf("FunTester: 获取FunTester失败: %w", ErrFunTesterNotFound)
    }
    return nil
}

func main() {
    err := GetFunTester(2)
    if err != nil {
        // 使用 errors.Is 进行正确的错误比较
        if errors.Is(err, ErrFunTesterNotFound) {
            fmt.Println("FunTester: FunTester未找到")
        } else {
            fmt.Println("FunTester: 其他错误", err)
        }
    }
}

输出结果:

FunTester: 其他错误 FunTester: 获取FunTester失败: FunTester: 未找到

错误五十二:两次处理同一个错误 (#52)

示例代码:

package main

import (
    "fmt"
)

func processFunTester() error {
    return fmt.Errorf("FunTester: 发生错误")
}

func main() {
    err := processFunTester()
    if err != nil {
        fmt.Println("FunTester: 错误:", err)
        // 再次处理同一个错误
        fmt.Println("FunTester: 再次处理错误:", err)
    }
}

错误说明:
在 Go 语言中,错误在函数内部处理后,可能会被重复处理,如打印日志和再次返回。两次处理同一个错误会导致日志冗余,增加维护难度,甚至混淆错误来源,就像同一个问题被反复提及,却没有解决方案。

可能的影响:
两次处理同一个错误会导致日志中出现重复的错误信息,混淆问题的实际来源,增加调试难度。此外,重复处理错误可能会干扰正常的错误处理流程,导致错误响应不一致或不完整。

最佳实践:
在处理错误时,应明确责任,决定由函数内处理错误还是传递给调用方处理。避免在函数内部同时打印错误日志和返回错误给调用方,让错误的处理逻辑清晰且不重复。包装错误时,只提供额外的上下文信息,而不进行实际的处理。

改进后的代码:

选择由调用方负责处理错误,函数内仅返回错误:

package main

import (
    "fmt"
)

func processFunTester() error {
    return fmt.Errorf("FunTester: 发生错误")
}

func main() {
    err := processFunTester()
    if err != nil {
        // 仅由调用方处理错误
        fmt.Println("FunTester: 错误:", err)
        // 调用方决定是否进一步处理
    }
}

如果需要在调用方进一步处理,可以传递或记录错误,而不重复打印:

package main

import (
    "fmt"
)

func processFunTester() error {
    return fmt.Errorf("FunTester: 发生错误")
}

func main() {
    err := processFunTester()
    if err != nil {
        // 记录错误日志
        fmt.Println("FunTester: 错误:", err)
        // 再次处理错误,例如返回或上报
        // fmt.Println("FunTester: 再次处理错误:", err) // 避免重复处理
    }
}

输出结果:

FunTester: 错误: FunTester: 发生错误

错误五十三:不处理错误 (#53)

示例代码:

package main

import (
    "fmt"
    "os"
)

func main() {
    // 忽略读取文件时的错误
    data, _ := os.ReadFile("FunTester.txt")
    fmt.Println("FunTester: 文件内容 =", string(data))
}

错误说明:
在 Go 语言中,忽略错误处理是一个常见的错误,尤其是在函数调用或 defer 语句执行时。未能处理错误可能导致程序忽略关键的问题,继续执行不安全的逻辑,进而引发更严重的问题。就像在尝试修理机器时,没有检查是否安全,可能导致机器损坏甚至人身危险。

可能的影响:
不处理错误会导致程序在遇到问题时无法及时响应和修复,导致数据错误、资源泄露或程序崩溃。特别是在关键操作(如文件读取、网络通信等)中,忽略错误会导致严重的后果,影响程序的稳定性和可靠性。

最佳实践:
每次调用可能返回错误的函数时,都要检查并适当处理错误。即使当前不需要对错误进行特别处理,也应至少记录错误日志,以便后续调查和修复。此外,在 defer 语句中执行的函数,如果返回错误,也应处理或记录,避免错过关键问题。

改进后的代码:

显式处理错误,确保程序在错误发生时能够正确响应:

package main

import (
    "fmt"
    "os"
)

func main() {
    data, err := os.ReadFile("FunTester.txt")
    if err != nil {
        fmt.Printf("FunTester: 读取文件失败: %v\n", err)
        return
    }
    fmt.Println("FunTester: 文件内容 =", string(data))
}

或者在 defer 函数中处理错误:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("FunTester_output.txt")
    if err != nil {
        fmt.Println("FunTester: 无法创建文件")
        return
    }
    defer func() {
        if cerr := file.Close(); cerr != nil {
            fmt.Printf("FunTester: 关闭文件时出错: %v\n", cerr)
        }
    }()

    _, err = file.WriteString("FunTester: 写入内容")
    if err != nil {
        fmt.Printf("FunTester: 写入文件时出错: %v\n", err)
        return
    }
}

输出结果:

FunTester: 文件内容 = FunTester演示内容

错误五十四:不处理 defer 中的错误 (#54)

示例代码:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("FunTester_output.txt")
    if err != nil {
        fmt.Println("FunTester: 无法创建文件")
        return
    }
    defer file.Close()

    _, err = file.WriteString("FunTester: 写入内容")
    if err != nil {
        fmt.Println("FunTester: 写入时发生错误")
    }
}

错误说明:
在 Go 语言中,defer 语句用于在函数退出前执行清理操作,如关闭文件。然而,defer 函数执行时的错误往往被忽略,可能导致资源释放不完全或未记录的重要错误。就像是在清理家务时,不检查是否所有东西都已整理干净,可能留下隐患。

可能的影响:
未处理 defer 中执行的错误,会导致资源泄漏(如未关闭的文件、未释放的锁等),影响程序的稳定性和资源管理。此外,遗漏的错误信息会增加调试难度,导致潜在的问题被忽略。

最佳实践:
defer 中执行的函数,应该检查并处理返回的错误。可以使用匿名函数(闭包)来捕获并处理错误,或者在 defer 内部记录错误日志,确保不会遗漏重要的错误信息。

改进后的代码:

使用匿名函数在 defer 中捕获和处理错误:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("FunTester_output.txt")
    if err != nil {
        fmt.Println("FunTester: 无法创建文件")
        return
    }
    defer func() {
        if cerr := file.Close(); cerr != nil {
            fmt.Printf("FunTester: 关闭文件时出错: %v\n", cerr)
        }
    }()

    _, err = file.WriteString("FunTester: 写入内容")
    if err != nil {
        fmt.Printf("FunTester: 写入时发生错误: %v\n", err)
    }
}

或者记录错误:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("FunTester_output.txt")
    if err != nil {
        fmt.Println("FunTester: 无法创建文件")
        return
    }
    defer func() {
        if cerr := file.Close(); cerr != nil {
            fmt.Printf("FunTester: 关闭文件时出错: %v\n", cerr)
        }
    }()

    _, err = file.WriteString("FunTester: 写入内容")
    if err != nil {
        fmt.Printf("FunTester: 写入时发生错误: %v\n", err)
    }
}

输出结果文件 FunTester_output.txt 内容:

FunTester: 写入内容

说明:
通过在 defer 中使用匿名函数,可以捕获并处理文件关闭时可能发生的错误,确保资源得以正确释放,并且错误信息不会被忽略。

FunTester 原创精华
【免费合集】从 Java 开始性能测试
故障测试与 Web 前端
服务端功能测试
性能测试专题
Java、Groovy、Go
白盒、工具、爬虫、UI 自动化
理论、感悟、视频


↙↙↙阅读原文可查看相关链接,并与作者交流