Go语言没有类似Java的异常机制,而是采用显式的错误返回值来处理错误。这种方式鼓励开发者显式地检查和处理每个可能出错的操作,代码更加健壮和可预测。核心是内置的error接口,开发者可以自定义错误类型,并在函数中返回错误值。
error是Go内置的接口类型,只有一个方法:Error() string。任何实现了该方法的类型都可以作为错误使用。
type error interface {
Error() string
}
函数通常返回error作为最后一个返回值,调用方通过检查err != nil判断是否出错。
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)
}
import "errors"
err := errors.New("自定义错误信息")
支持格式化错误信息,并提供%w动词用于错误包装(后面讲解)。
import "fmt"
err := fmt.Errorf("用户 %s 不存在,ID: %d", username, id)
可以定义结构体实现error接口,携带额外上下文信息。
type ValidationError struct {
Field string
Value interface{}
Msg string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("验证失败: %s=%v (%s)", e.Field, e.Value, e.Msg)
}
func validateAge(age int) error {
if age < 0 {
return ValidationError{Field: "age", Value: age, Msg: "年龄不能为负数"}
}
if age > 150 {
return ValidationError{Field: "age", Value: age, Msg: "年龄过大"}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println(err)
// 可以类型断言获取额外字段
if vErr, ok := err.(ValidationError); ok {
fmt.Printf("问题字段: %s\n", vErr.Field)
}
}
}
使用fmt.Errorf配合%w动词可以将一个错误包装到另一个错误中,形成错误链,便于保留原始错误信息。
func readConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 包装原始错误
return fmt.Errorf("打开配置文件失败: %w", err)
}
return nil
}
func main() {
err := readConfigFile("/nonexist.conf")
if err != nil {
fmt.Println(err) // 打开配置文件失败: open /nonexist.conf: no such file or directory
}
}
标准库errors包提供了两个函数来检查错误链:
errors.Is(err, target) —— 判断错误链中是否包含特定错误(值比较)。errors.As(err, target) —— 判断错误链中是否有类型匹配的错误,并将第一个匹配的类型赋值给目标。var ErrNotFound = errors.New("not found")
func findUser(id int) error {
if id != 1 {
return fmt.Errorf("查找用户失败: %w", ErrNotFound)
}
return nil
}
func main() {
err := findUser(2)
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在(通过Is检测)")
}
}
// 使用 errors.As 获取自定义错误类型
var valErr ValidationError
if errors.As(err, &valErr) {
fmt.Printf("自定义错误字段: %s\n", valErr.Field)
}
panic用于不可恢复的错误(如程序逻辑错误、数组越界等),会导致程序崩溃。在极少情况下,可以使用recover捕获panic,通常只在顶层goroutine中使用。
func safeDivide(a, b int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到panic:", r)
}
}()
if b == 0 {
panic("除零错误")
}
fmt.Println(a / b)
}
func main() {
safeDivide(10, 2) // 5
safeDivide(4, 0) // 捕获到panic: 除零错误
fmt.Println("程序继续运行")
}
panic。错误处理应优先使用返回error,只在程序无法继续运行的情况下(如初始化失败)使用panic。
_忽略错误(除非明确无关紧要)。%w包装错误,保留堆栈线索,方便定位问题。var ErrNotFound = errors.New("not found")),供外部判断。panic。defer+recover保护goroutine,避免单个panic导致整个服务崩溃。package main
import (
"errors"
"fmt"
"os"
)
var ErrConfigMissing = errors.New("配置文件缺失")
func readFile(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("读取文件失败 %s: %w", path, err)
}
return nil
}
func loadConfig(path string) error {
if path == "" {
return fmt.Errorf("无效的配置路径: %w", ErrConfigMissing)
}
if err := readFile(path); err != nil {
return fmt.Errorf("加载配置失败: %w", err)
}
return nil
}
func main() {
err := loadConfig("/not/exist.conf")
if err != nil {
fmt.Println("错误详情:", err)
// 判断是否为 ErrConfigMissing
if errors.Is(err, ErrConfigMissing) {
fmt.Println("根因:配置路径缺失")
}
// 获取最底层的 os.PathError
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("底层文件错误: %s\n", pathErr.Err)
}
}
}
error接口和函数返回值实现了简单而强大的错误处理。开发者应养成良好的错误检查习惯,使用包装和解包函数构建清晰的错误链。下一章我们将学习Go并发模型的核心——goroutine和channel。