【Golang】深入解读Go语言中的错误(error)与异常(panic)

✨✨ 欢迎大家来到景天科技苑✨✨

🎈🎈 养成好习惯,先赞后看哦~🎈🎈

🏆 作者简介:景天科技苑

🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。

🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。

所属的专栏: Go语言开发零基础到高阶实战
景天的主页: 景天科技苑

文章目录

Go语言中的错误与异常

在Go语言编程中,错误和异常处理是非常重要的一部分。Go语言通过其独特的错误处理机制和异常处理模型,提供了一种既清晰又高效的方式来处理程序运行时的错误和异常情况。

错误:指的是程序中预期会发生的结果,预料之中

打开一个文件:文件正在被占用,可知的。

异常:不该出现问题的地方出现了问题,预料之外

调用一个对象,发现这个对象是个空指针对象,发生错误。

错误是业务的一部分,而异常不是。

go语言开发过程中遇到最多的代码,就是error。

需要将所有的错误情况都考虑到,并写到你的代码中。

1. Go语言错误 error

鼓励工程师在代码中显式的检查错误,而非忽略错误。

错误是开发中必须要思考的问题

  • 某些系统错误 ,文件被占用,网络有延迟
  • 人为错误:核心就是一些不正常的用户会怎么来给你传递参数,sql注入

在实际工程项目中,

我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。

Go语言没有提供像Java. c#语言中的try...catch异常处理方式,

而是通过函数返回值逐层往上抛, 如果没有人处理这个错误,程序就终止 panic

这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误。

好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码繁琐。

Go中的错误也是一种类型。错误用内置的error类型表示。就像其他类型的,如int, float64。

错误值可以存储在变量中,从函数中返回,传递参数 等等。

代码示例:

go 复制代码
package main

import (
    "fmt"
    "os"
)

// 错误是开发中必须要思考的问题
// - 某些系统错误 ,文件被占用,网络有延迟
// - 人为错误:核心就是一些不正常的用户会怎么来给你传递参数,sql注入
func main() {
    //工作目录是指在执行程序时,操作系统认为当前正在工作的目录。
    //一般情况下,我们通过在终端或文件浏览器中打开文件时所处的目录就是当前工作目录。
    //os.Getwd()只能获取当前项目的工作目录,并不是当前文件所在路径
    path, _ := os.Getwd()
    fmt.Println("Path:", path)
    err := os.Chdir("F:\\goworks\\src\\jingtian\\yufa\\错误与异常")
    if err != nil {
        return
    }
    //再次查看路径
    path2, _ := os.Getwd()
    fmt.Println("再次查看Path:", path2)
    //打开一个文件 os 系统包,所有可以用鼠标和键盘能执行的事件,都可以用程序实现
    //os.Open()返回 一个文件对象和错误error
    // func Open(name string) (*File, error)
    file, err2 := os.Open("aaa.txt")
    // 在开发中,我们需要思考这个错误的类型  PathError
    // 1、文件不存在 err
    // 2、文件被占用 err
    // 3、文件被损耗 err
    // 调用方法后,出现错误,需要解决
    if err2 != nil {
        fmt.Println(err2)
        return
    }

    fmt.Println(file.Name())
}

// 在实际工程项目中,
// 我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。
// Go语言没有提供像Java. c#语言中的try...catch异常处理方式,
// 而是通过函数返回值逐层往上抛, 如果没有人处理这个错误,程序就终止 panic

// 这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误。
// 好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码繁琐。

// Go中的错误也是一种类型。错误用内置的error类型表示。就像其他类型的,如int, float64。
// 错误值可以存储在变量中,从函数中返回,传递参数 等等。

如果文件不存在,运行就会捕获错误

文件存在,就可以正常打印文件名称

(1)自定义错误

1. 使用errors.New()

errors.New()函数是最简单的创建错误的方式。它接受一个字符串作为参数,并返回一个实现了error接口的错误值。

返回值的类型为 *errors.errorString,可以使用Error()方法,将err转化为字符串

看下源码

go 复制代码
package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("这是一个错误")
    if err != nil {
        fmt.Println(err)
        fmt.Printf("错误类型%T\n", err)
        //可以使用Error()方法,将err转化为字符串
                // 注意,如果err为空,调用Error()方法会报错,所以该方法一定要在if err != nil条件中执行

        fmt.Printf("错误类型%T\n", err.Error())
    }
}
2.使用fmt.Errorf()

fmt.Errorf()函数比errors.New()更灵活,它允许你使用格式化字符串来创建错误。这对于需要动态构建错误信息的场景非常有用。

go 复制代码
package main

import (
    "fmt"
)

func main() {
    err := fmt.Errorf("错误代码: %d, 错误信息: %s", 1001, "操作失败")
    if err != nil {
        fmt.Println(err.Error())
    }
}
3. 综合案例
go 复制代码
package main

import (
    "errors"
    "fmt"
)

// 自己定义一个错误
// 1、errors.New("xxxxx")
// 2、fmt.Errorf()
// 都会返回  error 对象, 本身也是一个类型

func main() {
    //根据用户传参数据,用我们定义好的函数处理
    age_err := setAge(-3)
    if age_err != nil {
        fmt.Println(age_err)
    }
    fmt.Printf("%T\n", age_err) // *errors.errorString
    //如果age_err为空,调用Error()方法会报错
    fmt.Printf("%T\n", age_err.Error()) // string

    // 方式二
    errInfo1 := fmt.Errorf("我是一个错误信息:%d\n", 500)
    //errInfo2 := fmt.Errorf("我是一个错误信息:%d\n", 404)
    if errInfo1 != nil {
        // 处理这个错误
        fmt.Println(errInfo1)
        fmt.Printf("%T\n", errInfo1.Error())
    }

}

// 设置年龄的函数,一定需要处理一些非正常用户的请求
// 返回值为 error 类型  这个是内置类型
// 作为一个开发需要不断思考的事情,代码的健壮性和安全性
func setAge(age int) error {
    if age < 0 {
        // 给出一个默认值
        age = 3
        // 抛出一个错误 errors 包
        return errors.New("年龄不合法")
    }
    // 程序正常的结果,给这个用户赋值
    fmt.Println("年龄设置成功:age=", age)
    //年龄合法,就返回nil
    return nil
}

(2)自定义错误类型(难点)

虽然errors.New()和fmt.Errorf()很方便,但在某些情况下,你可能需要更丰富的错误信息。这时,可以通过自定义类型来实现go内置的error接口。

自定义错误类型步骤

1、定义一个结构体
go 复制代码
// JingTian 定义一个结构体
type JingTian struct {
    msg  string
    code int
}
2、为该结构体创建个方法,实现go内置error接口中的Error方法

内置接口

go 复制代码
// 注意,Error()方法开头大写 string
// 实现内置error接口的Error方法
// 注意,方法的调用者用指针类型,因为结构体是值类型,不同函数里面的操作是互相独立的
func (e *JingTian) Error() string {
    //  fmt.Sprintf() 返回string
    return fmt.Sprintf("错误信息:%s,错误代码:%d\n", e.msg, e.code)
}
3、创建个方法测试
go 复制代码
// 使用错误测试
func test(i int) (int, error) {
    // 需要编写的错误
    if i != 0 {
        //自带的 errors.New()和fmt.Errorf() 只能返回字符串类型的错误信息,不常用,信息太少了
        // 更多的时候我们会使用自定义类型的错误
        //注意,返回的错误用内存地址,因为结构体是值类型
        return i, &JingTian{msg: "非预期数据", code: 500}
    }
    // 正常结果
    return i, nil
}
4、main方法中写逻辑
go 复制代码
func main() {

    i, err := test(1)

    if err != nil {
        fmt.Println(err)
        //查看错误类型
        fmt.Printf("%T\n", err)
        ks_err, ok := err.(*JingTian)
        if ok {
            if ks_err.print() {
                // 处理这个错误中的子错误的逻辑
            }
            fmt.Println(ks_err.msg)
            fmt.Println(ks_err.code)
        }
    }
    fmt.Println(i)

}

完整代码

go 复制代码
package main

import (
    "fmt"
)

// JingTian 定义一个结构体
type JingTian struct {
    msg  string
    code int
}

// 注意,Error()方法开头大写 string
// 实现内置error接口的Error方法
// 注意,方法的调用者用指针类型,因为结构体是值类型,不同函数里面的操作是互相独立的
func (e *JingTian) Error() string {
    //  fmt.Sprintf() 返回string
    return fmt.Sprintf("错误信息:%s,错误代码:%d\n", e.msg, e.code)
}

// 自定义错误,里面还可以写一些方法
// 处理error的逻辑
func (e *JingTian) print() bool {
    fmt.Println("hello,world")
    return true
}

// 使用错误测试
func test(i int) (int, error) {
    // 需要编写的错误
    if i != 0 {
        //自带的 errors.New()和fmt.Errorf() 只能返回字符串类型的错误信息,不常用,信息太少了
        // 更多的时候我们会使用自定义类型的错误
        //注意,返回的错误用内存地址,因为结构体是值类型
        return i, &JingTian{msg: "非预期数据", code: 500}
    }
    // 正常结果
    return i, nil
}

func main() {

    i, err := test(1)

    if err != nil {
        fmt.Println(err)
        //查看错误类型
        fmt.Printf("%T\n", err)
        ks_err, ok := err.(*JingTian)
        if ok {
            if ks_err.print() {
                // 处理这个错误中的子错误的逻辑
            }
            fmt.Println(ks_err.msg)
            fmt.Println(ks_err.code)
        }
    }
    fmt.Println(i)

}

go语言中,业务逻辑其实代码量并不大,大多数是在处理错误逻辑

比如下购物商城代码:
购物

下单:

1、查询商品信息 (FindGoodsError: msg、code | fun-商品不在了 ,fun-商品失效 ,fun-网络错误 )

2、查询库存 (FindxxxError: msg、code | fun-库存为0 ,fun-库存>0)

3、查询物流是否可以到达 (xxxError: msg、code | fun ,fun)

4、查询价格生成订单 (xxxError: msg、code | fun ,fun)

5、支付成功与否 (xxxError: msg、code | fun ,fun)

6、成功就产生一个订单,推送到商家和物流 (xxxError: msg、code | fun ,fun)

2. Go语言的异常处理机制

panic 和 recover

与错误处理不同,Go语言中的异常处理是通过panic和recover关键字来实现的。panic用于表示程序中的严重错误,它会导致程序中断执行;recover用于捕获panic,并恢复程序的正常执行流程。

(1)使用panic

什么时候会发生恐慌panic,我们不知道什么时候会报错

程序运行的时候会发生panic

手动抛出panic。我们在一些能够预知到危险的情况下,可以主动抛出

panic可以接受任何类型的值作为参数,通常是一个字符串或错误对象,用于描述发生的异常。

使用 panic() 程序就会终止,停在这里强制结束

看下源码

go 复制代码
package main

import "fmt"

func main() {
    panic("发生了异常")
    //panic后面的代码,程序不会执行
    fmt.Println("hello world")
}

在上面的例子中,程序会在执行到panic时中断,并打印出"发生了异常"这条消息。

如果有panic发生,我们尽可能接收它,并处理

出现了panic之后,如果panic语句前面有defer语句,先执行所有的defer语句。panic语句后面的defer语句不再执行

go 复制代码
package main

import "fmt"

// panic  recover
//
// 出现了panic之后,如果panic语句前面有defer语句,先执行所有的defer语句。
//
// defer : 延迟函数,倒序执行,处理一些问题。
func main() {
    defer fmt.Println("main--1")
    defer fmt.Println("main--2")
    fmt.Println("main--3")
    testPanic(1) // 外部函数,执行完panic之前的所有defer
    //后面的都不执行了
    defer fmt.Println("main--4")
    fmt.Println("main--5")
}
func testPanic(num int) {
    defer fmt.Println("testPanic--1")
    defer fmt.Println("testPanic--2")
    fmt.Println("testPanic--3")
    // 如果在函数中一旦触发了 panic,会终止后面要执行的代码。
    // 如果panic语句前面存在defer,正常按照defer逻辑执行。panic语句后面的defer语句不再执行
    if num == 1 {
        panic("出现预定的异常----panic")
    }
    defer fmt.Println("testPanic--4")
    fmt.Println("testPanic--5")
}

(2)使用recover

go 语言是追求简洁的,他没有使用 try...catch 语句

如果有些情况,必须要处理异常,就需要使用panic,抛出了异常,recover,接收这个异常来处理。

recover结合defer处理 panic 恐慌

recover必须在defer语句中调用,才能捕获到panic。defer语句会延迟函数的执行,直到包含它的函数即将返回时,才执行defer语句中的函数。

一般我们在某个函数抛出的panic,就在那个函数里面解决了

recover会返回panic的参数

recover返回任意类型

1. recover案例展示
go 复制代码
package main

import (
    "fmt"
)

func main() {
    //defer语句绑定的匿名函数
    defer func() {
        r := recover()
        if r != nil {
            fmt.Println("捕获到异常:", r)
        }
    }()
    panic("发生了异常")
    fmt.Println("这行代码不会被执行")
}

在这个例子中,尽管在defer之后有panic调用,但defer中的函数仍然会执行,并捕获到panic抛出的异常。然后,程序会跳过panic之后的代码,继续执行defer之后的逻辑。

2. recover结合多个defer执行顺序
go 复制代码
package main

import "fmt"

// panic  recover
//
// 出现了panic之后,如果有defer语句,先执行所有的defer语句。
//
// defer : 延迟函数,倒序执行,处理一些问题。
func main() {
    defer fmt.Println("main--1")
    defer fmt.Println("main--2")
    fmt.Println("main--3")
    testPanic2(1) // 外部函数,如果在函数内部已经处理panic,那么程序会继续执行
    defer fmt.Println("main--4")
    fmt.Println("main--5")
}
func testPanic2(num int) {
    // 出去函数的时候处理这里面可能发生的panic
    // recover func recover() any 返回panic传递的值
    // panic   func panic(v any)
    defer func() {
        if msg := recover(); msg != nil {
            fmt.Println("recover执行了... panic msg:", msg)
            // 处理逻辑
            fmt.Println("---------程序已恢复----------")
        }
    }()

    defer fmt.Println("testPanic--1")
    defer fmt.Println("testPanic--2")
    fmt.Println("testPanic--3")
    // 如果在函数中一旦触发了 panic,会终止后面要执行的代码。
    // 如果存在defer,正常按照defer逻辑执行
    if num == 1 {
        panic("出现预定的异常----panic")
    }
    defer fmt.Println("testPanic--4")
    fmt.Println("testPanic--5")
}

执行逻辑:

1、panic 触发

2、触发panic当前函数panic前面的所有defer语句,倒序执行

3、直到遇到recover处理了这个panic...函数结束

4、main,继续向下执行。

相关推荐
懒是一种态度7 分钟前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
I_Am_Me_12 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
重生之我是数学王子23 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手24 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z28 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
神仙别闹36 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE37 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
入 梦皆星河39 分钟前
在 Ubuntu/Debian 上安装 Go
ubuntu·golang·debian
程序猿进阶43 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端