公司动态

Go学习笔记:错误处理——没有异常,只有 error

📅 2026/8/21 22:16:57
Go学习笔记:错误处理——没有异常,只有 error
本文首发于 栏轩·阁欢迎访问阅读原文获取更好的阅读体验。一、Go 的错误哲学Go 和其他语言最大的不同没有异常Exception没有try-catch-finally。错误在 Go 里就是一个普通的值value和int、string没什么区别// 错误就是一个返回值和普通值一样处理result,err:doSomething()iferr!nil{// 处理错误returnerr} 这种设计的核心理念错误是预期的一部分不是意外。文件可能不存在、网络可能断开——这些不是异常是正常业务流程的一部分。二、创建错误errors.New最基础的创建方式importerrorsvarErrNotFounderrors.New(资源未找到)命名规范导出的错误变量用Err开头其他包能引用// 标准库中的例子varos.ErrNotExisterrors.New(file does not exist)varsql.ErrNoRowserrors.New(sql: no rows in result set)// 你自己的包varErrNotFounderrors.New(资源未找到)varErrAuthFailerrors.New(认证失败)varErrInvaliderrors.New(无效参数)这些叫哨兵错误Sentinel Error——类似常量用来比较错误类型。三、判断错误errors.Is不能用比较错误因为包装后会不同要用errors.IsvarErrNotFounderrors.New(资源未找到)// 创建错误err:findUser(999)// 返回 ErrNotFound// ❌ 不要这样iferrErrNotFound{...}// ✅ 正确方式iferrors.Is(err,ErrNotFound){fmt.Println(没找到用户)}errors.Is会沿着错误链一路找即使错误被包装过也能识别。四、自定义错误类型 errors.As有时候光一个字符串不够用需要携带更多信息比如哪个文件、什么操作typeFileErrorstruct{Pathstring// 哪个文件Opstring// 什么操作Errerror// 原始错误}// 实现 error 接口——只要实现这个方法*FileError 就是 errorfunc(e*FileError)Error()string{returnfmt.Sprintf(文件操作失败 [%s] %s: %v,e.Op,e.Path,e.Err)}用errors.As提取自定义错误_,err:readFile(secret.txt)varfileErr*FileErroriferrors.As(err,fileErr){// 现在可以访问 FileError 特有的字段了fmt.Printf(文件 %s 在 %s 操作中出错,fileErr.Path,fileErr.Op)}errors.Is vs errors.As函数用途类似errors.Is(err, target)判断是否是某个特定错误errors.As(err, target)提取某种类型的错误类型断言instanceof五、错误包装%w实际项目中错误极少直接返回通常需要加上上下文信息// 不包装原始错误直接传上去调用者不知道在哪出的问题returnerr// ✅ 包装加上上下文同时保留原始错误returnfmt.Errorf(读取配置文件失败: %w,err)%w的作用给错误增加描述信息上下文保留原始错误errors.Is和errors.As依然能找到它funcreadConfig()error{err:readFile(config.yaml)iferr!nil{returnfmt.Errorf(读取配置失败: %w,err)}returnnil}err:readConfig()iferrors.Is(err,os.ErrNotExist){// ✅ 仍然能判断尽管被包装了fmt.Println(文件不存在)}不加 %w 的情况如果不想暴露内部错误细节用%v而不是%w// 这样上层只能用 errors.Is 判断 ErrPermission看不到原始错误returnfmt.Errorf(无权访问: %v,err)六、错误处理三大原则原则1每个错误都必须处理// ❌ 绝对不能这样result,_:doSomething()// ✅ 必须检查result,err:doSomething()iferr!nil{// 处理或返回}原则2错误只记录一次错误不要层层打印否则日志里一堆重复信息// ❌ 每一层都打印funca(){err:b()iferr!nil{log.Println(err)// 打印returnerr}}funcb(){err:c()iferr!nil{log.Println(err)// 又打印returnerr}}// ✅ 只在最终处理的地方打印funca(){err:b()iferr!nil{log.Println(err)// 只在这里打印returnerr}}funcb(){err:c()iferr!nil{returnfmt.Errorf(b 执行失败: %w,err)// 不打印只包装}}原则3出错后程序继续跑实际项目中处理一批任务时某个出错不应该让全部崩溃for_,item:rangeitems{err:process(item)iferr!nil{log.Printf(处理 %v 出错: %v,item,err)continue// 跳过这个处理下一个}}七、完整示例packagemainimport(errorsfmt)// 哨兵错误var(ErrNotFounderrors.New(资源未找到)ErrInvaliderrors.New(无效参数))// 自定义错误类型typeBizErrorstruct{CodeintMessagestring}func(e*BizError)Error()string{returnfmt.Sprintf([%d] %s,e.Code,e.Message)}// 业务函数funcgetUser(idint)(string,error){ifid0{return,ErrInvalid}ifid100{return,fmt.Errorf(查询用户失败: %w,ErrNotFound)}ifid50{return,BizError{Code:403,Message:该用户已被禁用}}returnpc,nil}funcmain(){testCases:[]int{1,-1,999,50}for_,id:rangetestCases{name,err:getUser(id)iferrnil{fmt.Printf([%d] ✅ 用户: %s\n,id,name)continue}// 判断具体错误类型switch{caseerrors.Is(err,ErrInvalid):fmt.Printf([%d] ⚠️ 无效参数\n,id)caseerrors.Is(err,ErrNotFound):fmt.Printf([%d] ❌ 未找到\n,id)default:// 自定义错误varbizErr*BizErroriferrors.As(err,bizErr){fmt.Printf([%d] 业务错误: %s\n,id,bizErr.Message)}else{fmt.Printf([%d] ❓ 未知错误: %v\n,id,err)}}}}输出[1] ✅ 用户: pc [-1] ⚠️ 无效参数 [999] ❌ 未找到 [50] 业务错误: 该用户已被禁用八、小结知识点一句话errors.New(msg)创建简单错误fmt.Errorf(...: %w, err)包装错误保留链errors.Is(err, target)判断是否为目标错误errors.As(err, target)提取特定类型的错误自定义错误实现Error() string即可成为 error接口本质error是一个接口任何实现了Error() string的类型都是 error