下载工作台
Go 编程实战

错误处理与 defer

试读上半部分 · 解锁后可读全文

第 7 章 · 错误处理与 defer

本章目标:掌握 error 接口、fmt.Errorf%w 包装errors.Is / errors.As;熟练使用 defer 关闭资源;理解 panic/recover 仅用于不可恢复错误;在 toolkit-go 中安全读写日志文件;对照 java-dev ch07 异常机制与 python-dev try/except。

学时建议:3~4 小时(含 1.5 小时跟练)

前置:完成 go-dev ch06;建议已学 java-dev ch07python-dev 异常章节。


7.1 场景说明:读取 access 日志

toolkit-go~/learn-go/toolkit-go,模块 example.com/toolkit-go)从磁盘读取 JSON Lines 或管道分隔日志。文件可能不存在、权限不足或行格式损坏——Go 用 error 返回值 逐层向上传递,而非 try/catch。

internal/logparser/
├── parser.go
├── file_reader.go
└── errors.go          # 哨兵错误
JavaGoPython
try/catch/finallyif err != nil { return err } + defer
受检/非受检异常统一 error 接口
throw new XxxExceptionreturn fmt.Errorf(...)
finally { close }defer f.Close()

7.2 error 接口与创建错误

// error 是内置接口
type error interface {
	Error() string
}
// internal/logparser/errors.go
package logparser

import "errors"

var (
	ErrEmptyLog = errors.New("empty log file")
	ErrBadLine  = errors.New("invalid log line")
)
// internal/logparser/file_reader.go
package logparser

import (
	"fmt"
	"os"
)

func OpenLog(path string) (*os.File, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open %s: %w", path, err)
	}
	return f, nil
}
创建方式示例用途
errors.New哨兵错误errors.Is 判断
fmt.Errorf格式化消息添加上下文
fmt.Errorf("...: %w", err)包装保留错误链
自定义类型type MyErr struct{}errors.As 提取

7.3 错误处理惯用法

func Process(path string) error {
	f, err := OpenLog(path)
	if err != nil {
		return err // 或再包装一层
	}
	defer f.Close()

	data, err := readAll(f)
	if err != nil {
		return fmt.Errorf("read %s: %w", path, err)
	}
	if len(data) == 0 {
		return ErrEmptyLog
	}
	return nil
}

不要

// 反模式:panic 处理普通 IO 失败
if err != nil {
	panic(err)
}

7.4 defer 释放资源

defer 将函数调用推迟到当前函数返回前执行,LIFO(后进先出)顺序。

func ReadAllLines(path string) (lines []string, err error) {
	f, err := OpenLog(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scan: %w", err)
	}
	if len(lines) == 0 {
		return nil, ErrEmptyLog
	}
	return lines, nil
}
defer 用途示例
关闭文件defer f.Close()
解锁defer mu.Unlock()
记录耗时defer func(t time.Time) { log.Printf("took %v", time.Since(t)) }(time.Now())
恢复 panicdefer func() { recover() }()

defer 执行时机:在 return 之后、函数真正退出之前;多个 defer 按声明逆序执行。


7.5 errors.Is 与 errors.As

import (
	"errors"
	"fmt"
	"os"
)

func HandleOpen(path string) {
	_, err := OpenLog(path)
	if err == nil {
		return
	}
	if errors.Is(err, os.ErrNotExist) {
		fmt.Println("文件不存在,请检查路径")
		return
	}
	if errors.Is(err, ErrEmptyLog) {
		fmt.Println("日志为空")
		return
	}
	fmt.Println("其他错误:", err)
}

// errors.As 提取具体类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
	fmt.Println("路径:", pathErr.Path)
}
函数用途
errors.Is(err, target)判断错误链中是否包含 target
errors.As(err, &ptr)提取链中某具体类型
%wfmt.Errorf 包装,保留链
errors.Unwrap手动 unwrap(少用)

7.6 完整示例:ParseFile

// internal/logparser/parse_file.go
package logparser

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

type AccessLog struct {
	Path       string
	DurationMs int
	Status     int
}

func parseLine(line string) (AccessLog, error) {
	parts := strings.Split(strings.TrimSpace(line), "|")
	if len(parts) != 3 {
		return AccessLog{}, fmt.Errorf("%w: %q", ErrBadLine, line)
	}
	ms, err := strconv.Atoi(parts[1])
	if err != nil {
		return AccessLog{}, fmt.Errorf("%w: bad duration", ErrBadLine)
	}
	status, err := strconv.Atoi(parts[2])
	if err != nil {
		return AccessLog{}, fmt.Errorf("%w: bad status", ErrBadLine)
	}
	return AccessLog{Path: parts[0], DurationMs: ms, Status: status}, nil
}

func ParseFile(path string) ([]AccessLog, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("parse file: %w", err)
	}
	defer f.Close()

	var logs []AccessLog
	scanner := bufio.NewScanner(f)
	lineNo := 0
	for scanner.Scan() {
		lineNo++
		entry, err := parseLine(scanner.Text())
		if err != nil {
			return nil, fmt.Errorf("line %d: %w", lineNo, err)

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。