【GoLang #5】:错误处理机制 & 包管理

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
一、Go 语言的错误处理机制
先来看一段代码,试判断下面输出什么 (10 / 0)
func test() {
n1 := 10
n2 := 0
res := n1 / n2
fmt.Printf("res= %d", res)
}
func main() {
test()
fmt.Println("下面的代码和逻辑")
}
结果如下:
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.test()
D:/C++Learning/go_learning/funcdemo/Error/main.go:8 +0x9
main.main()
D:/C++Learning/go_learning/funcdemo/Error/main.go:13 +0x13
exit status 2
- 可以发现
test()出异常就直接终止了,没有打印 “下面的代码和逻辑” - 当发送错误(panic)的时候,程序就会退出(崩溃)
1. 错误机制概述
Go 语言采用了一种独特而高效的错误处理机制,由 Go 的设计哲学是"显式优于隐式",错误处理应该显式地处理,而不是通过异常机制隐式传播。这种设计哲学使代码更加健壮且易于理解。
核心设计理念
- 显式错误处理:错误作为普通值返回,而非通过异常机制
- 无异常机制:没有
try/catch/finally结构 - 错误即值:错误是普通的值,实现了
error接口 - 轻量级:不依赖调用栈展开,性能开销小
错误接口
Go 的核心错误机制基于内置的 error 接口:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都可以作为错误类型使用。
基本说明
- Go语言追求简洁优雅,所以,Go语言不支持传统的 try … catch … finally 这种处理
- Go中引入的处理方式为:defer,panic,recover
- Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
- 异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
比如:对开头的 10/0 进行处理
func test() {
defer func() {
err := recover() // recover 捕获异常
if err != nil {
fmt.Println("err=", err)
}
}()
n1 := 10
n2 := 0
res := n1 / n2
fmt.Printf("res= %d", res)
}
func main() {
test()
for {
fmt.Println("下面的代码和逻辑")
time.Sleep(time.Second)
}
}
输出
err= runtime error: integer divide by zero
下面的代码和逻辑
下面的代码和逻辑
- 这个时候就没有异常退出了
抛异常和处理异常
① 系统抛异常
// 系统抛
func test() {
a := [5]int{0, 1, 2, 3, 4}
a[1] = 123
fmt.Println(a)
// 下标越界
//a[10] = 11
index := 10
a[index] = 10
fmt.Println(a)
}
func main() {
test()
}
// 输出
[0 123 2 3 4]
panic: runtime error: index out of range [10] with length 5
goroutine 1 [running]:
main.test()
/tmp/sandbox2456950132/prog.go:13 +0x9f
main.main()
/tmp/sandbox2456950132/prog.go:19 +0xf
② 自己抛异常
package main
import "fmt"
func getCircleArea(radius float32) (area float32) {
if radius < 0 {
// 自己抛
panic("半径不能为负")
}
return 3.14 * radius * radius
}
func test() {
getCircleArea(-5)
}
func test1() {
// 延时执行匿名函数
// 延时到何时?(1)程序正常结束 (2)发生异常时
defer func() {
// recover() 复活 恢复
// 会返回程序为什么挂了
if err := recover(); err != nil {
fmt.Println(err)
}
}()
getCircleArea(-5)
fmt.Println("这里有没有执行")
}
func test2() {
test1()
fmt.Println("test04")
}
func main() {
test2()
test() // 程序直接挂了 发生异常, 后续不执行
fmt.Println("test")
}
// 输出
半径不能为负
test04
panic: 半径不能为负
goroutine 1 [running]:
main.getCircleArea(...)
/tmp/sandbox1905482954/prog.go:8
main.test(...)
/tmp/sandbox1905482954/prog.go:14
main.main()
/tmp/sandbox1905482954/prog.go:38 +0x28
③ 返回异常
func getCircleArea(radius float32) (area float32, err error) {
if radius < 0 {
// 构建个异常对象
err = errors.New("半径不能为负")
return
}
area = 3.14 * radius * radius
return
}
func main() {
area, err := getCircleArea(-5)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(area)
}
}
2. 关于 panic
panic 是 Go 语言中用于处理不可恢复错误的机制,它会中断程序的正常执行流程并触发程序崩溃。与传统的异常机制不同,panic 被设计为一种"最后手段",仅在遇到真正无法继续执行的严重错误时使用。
2.1 核心概念
定义:一种运行时恐慌机制,表示程序遇到了无法继续执行的状态,会导致当前 goroutine 立即停止执行,触发栈展开(stack unwinding)过程,最终导致程序崩溃退出(除非被 recover 捕获)
何时会发生 panic?
- 显式触发:开发者主动调用
panic(value) - 隐式触发:运行时检测到严重错误,例如:
- 数组/切片越界访问
- 空指针解引用(nil pointer dereference)
- 向已关闭的 channel 发送数据
- 类型断言失败(
v.(T)当 v 不是 T 类型时) - 除数为零
- 死锁检测
panic 特点:
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
2.2 Panic 工作原理
执行流程 如下:
- 触发 panic:
panic()被调用或运行时错误发生 - 停止当前函数:当前函数的执行立即停止
- 执行 defer 函数:按后进先出顺序执行当前 goroutine 的所有延迟函数(defer)
- 栈展开:向上层调用栈传播 panic
- 崩溃或恢复:
- 如果到达 main 函数仍未被捕获 → 程序崩溃
- 如果在栈展开过程中被
recover捕获 → 恢复执行
示例代码
func main() {
fmt.Println("程序开始")
riskyFunction()
fmt.Println("这行不会执行") // 不会到达这里
}
func riskyFunction() {
defer fmt.Println("defer 在 panic 前执行")
panic("发生严重错误!")
defer fmt.Println("这行不会执行") // 不会到达这里
}
// 输出如下:
程序开始
defer 在 panic 前执行
panic: 发生严重错误!
goroutine 1 [running]:
main.riskyFunction()
/tmp/sandbox123456/main.go:12
main.main()
/tmp/sandbox123456/main.go:6
2.4 使用场景
虽然 Go 不鼓励过度使用 panic,但在以下情况是合适的:
-
不可恢复的错误
data, err := os.ReadFile(path) if err != nil { panic("配置文件不存在,程序无法启动") } -
程序初始化失败
db, err := sql.Open("mysql", dsn) if err != nil { panic("数据库连接失败: " + err.Error()) } -
防御性编程(保护不变量)
if amt < 0 { panic("交易金额不能为负") } -
测试中的失败标记
result := criticalOperation() if !result.IsValid() { panic("关键功能测试失败") }
2.5 注意事项
- 避免常规错误处理:
- 常规错误应使用
error返回值 - 保留 panic 给真正灾难性的情况
- 常规错误应使用
- 资源清理:使用
defer确保资源释放 - recover 的位置:在包边界或 goroutine 顶层进行 recover
- 避免嵌套 panic:在 defer 函数中谨慎操作,避免引发二次 panic
- 提供有意义的 panic 值:使用字符串或自定义类型提供上下文信息
3. recover
recover 特点:
1、用来控制一个 goroutine 的 panicking 行为,捕获panic,从而影响应用的行为
2、一般的调用建议
a). 在defer函数中,通过 recover 来终止一个 goroutine 的 panicking 过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error
recover 是 Go 提供的唯一能捕获并处理 panic 的内置函数,必须与 defer 配合使用,如下:
func safeFunction() {
// 延迟函数中捕获 panic
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
// 可在此处进行错误恢复处理
}
}()
// 可能引发 panic 的操作
riskyOperation()
}
举例:
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("安全捕获:", r)
}
}()
fmt.Println("安全操作开始")
// 即使有 panic 也会被捕获
causePanic()
fmt.Println("安全操作结束") // 会执行
}
func dangerousCall() {
fmt.Println("危险操作开始")
causePanic()
fmt.Println("危险操作结束") // 不会执行
}
func causePanic() {
panic("测试 panic")
}
func main() {
fmt.Println("=== 安全操作示例 ===")
safeCall()
fmt.Println("=== 危险操作示例 ===")
dangerousCall()
}
// 输出
=== 安全操作示例 ===
安全操作开始
安全捕获: 测试 panic
安全操作结束
=== 危险操作示例 ===
危险操作开始
panic: 测试 panic
... [堆栈跟踪信息] ...
注意事项:
- 利用
recover处理panic指令,defer 必须放在 panic 之前定义,另外recover只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。 recover处理异常后,逻辑并不会恢复到panic那个点去,函数跑到 defer 之后的那个点。- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
举例:
func main() {
test() // panic error!
}
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象,如下:
func panic(v interface{})
func recover() interface{}
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test() // defer panic
}
注意:捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
func test() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
// 输出:
defer inner
<nil>
test panic
使用延迟匿名函数或下面这样都是有效的,如下:
func except() {
fmt.Println(recover())
}
func test() {
defer except()
panic("test panic")
}
func main() {
test()
}
如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z)
}
func main() {
test(2, 1)
}
区别 panic 与 error
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
| 特性 | panic | error |
|---|---|---|
| 用途 | 不可恢复的严重错误 | 预期内的常规错误 |
| 处理方式 | 中断执行流 | 通过返回值处理 |
| 传播 | 自动向上传播 | 需要显式传递 |
| 恢复 | 可通过 recover 捕获 | 直接处理 |
| 适用场景 | 程序无法继续执行的情况 | 可预测的可处理错误 |
| 性能 | 栈展开开销较大 | 几乎无额外开销 |
| 推荐度 | 尽量避免,最后手段 | 首选错误处理机制 |
4. 自定义 error
Go程序中,也支持自定义错误,使用errors.New 和 panic 内置函数。
- errors.New(“错误说明”):会返回一个error类型的值,表示一个错误
- panic 内置函数:接收一个
interface(}类型的值(也就是任何值)作为参数。可以接收error类型的变量,输出错误信息,并退出程序.
示例如下:
// 函数读取配置文件 init.conf 信息 -- 如果文件名不正确就返回自定义错误
func readConf(name string) (err error) {
if name == "config.ini" {
// 读取
return nil
} else {
return errors.New("读取文件错误")
}
}
func main() {
err := readConf("config.ini")
if err != nil {
panic(err) // 读取文件错误
}
fmt.Println("继续执行")
}
// 输出
panic: 读取文件错误
goroutine 1 [running]:
main.main()
D:/C++Learning/go_learning/funcdemo/Error/main.go:47 +0x45
exit status 2
小结
panic 是 Go 语言中处理不可恢复错误的机制:
- 用于严重错误导致程序无法继续执行的情况
- 会中断当前执行流并触发栈展开
- 必须与
defer和recover配合使用进行安全处理 - 不应替代常规错误处理(使用 error 返回值)
- 在程序初始化、保护不变量等场景下适用
- 在公共 API 边界应转换为 error 返回
Go 的错误处理哲学强调:“Errors are values”,建议开发者优先使用 error 返回值处理可预期的错误,将 panic 保留给真正灾难性的、无法恢复的程序状态。
二、Golang 中 包介绍和定义
包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言为我们提供了很多内置包,如fmt、strconv、strings、sort、errors、time、encoding/json、os、io 等。
说明:Go 的每个文件都是属于包的,也就是说 Go 是以包的形式来管理文件和项目目录结构的
包的三大作用
- 区分相同名字的函数、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
Golang 中的包可以分为以下三种
- 系统内置包:Golang 语言给我们提供的内置包,引入后可以直接使用,如 fmt、strconv、strings、sort、errors、time、encoding/ison、os、io 等。
- 自定义包:开发者自己写的包
- 第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的"github.com/shopspring/decimal"包解决 float 精度丢失问题。
相关说明
- 打包基本语法:package util
- 引入包的基本语法:import "包的路径”
1. 包管理工具 go mod
在 Golang1.11 版本之前如果我们要自定义包的话必须把项目放在 GOPATH 目录。Go1.11 版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,Go1.13 以后可以彻底不要 GPPATH了。
初始化项目
实际项目开发中我们首先要在我们项目目录中用 go mod init 命令生成一个 go.mod 文件管理我们项目的依赖。
-
比如我们的 golang 项目文件要放在了 myapp 这个文件夹,这个时候我们需要在 myapp 文件夹里面使用如下命令
go mod init myapp
此时生成一个 go.mod 文件代码如下:
module myapp
go 1.24.5
2. 自定义包
上面通过 import 导入的方式还是太慢了,如果我们的 calc 下面有多个文件又该怎么办
- 包(package)是多个 Go 源码的集合,一个包可以简单理解为一个存放多个
.go文件的文件夹。该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包。
package 包名
注意事项:
- 一个文件夹下面直接包含的文件只能归属一个 package,同样一个 package 的文件不能在多个文件夹下。
- 包名可以不和文件夹的名字一样,包名不能包含
-符号。 - 包名为 main 的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含 main 包的源代码则不会得到可执行文件。
而且 同一个目录下的所有 .go 文件,只要 package 声明相同,就会被编译器视为同一个包。而一个 package main 只能有 一个 func main() 作为程序入口。
比如:现在 tcp/ 目录下同时存在:
server.go→package main+func main()client.go→package main+func main()
当执行 go run . 或 go build 时,Go 会尝试把这两个文件一起编译成一个可执行文件,自然就会报:
main redeclared in this block
(main 函数在此包中被重复声明了)
🛠️ 怎么解决?
正式项目中,服务端和客户端属于两个独立的可执行程序,应该放在不同目录:
tcp_project/
├── go.mod
├── cmd/
│ ├── server/
│ │ └── main.go # package main + func main()
│ └── client/
│ └── main.go # package main + func main()
└── internal/ # 放共享逻辑(如协议解析、工具函数)
运行时:
go run ./cmd/server
go run ./cmd/client
匿名导入包
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:
import_"包的路径"
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。自定义包名
自定义包名
在导入包名的时候,我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。
具体语法格式如下:
import 别名"包的路径"
示例如下:
在当前文件夹新建一个 calc 文件夹,然后在 calc 文件夹下建立 calc.go 文件,如下:
package calc
// 首字母大写表示公有方法
func Add(x, y int) int{
return x + y
}
func Sub(x, y int) int{
return x * y
}
var age int // 小写无法跨包访问
var Name string // 大写可以跨包访问
然后回到 myapp 文件夹下,在 main.go 文件调用如下:
package main
import (
"myapp/calc"
"fmt"
)
func main() {
sum := calc.Add(1, 2) // 3
fmt.Println(sum)
}
3. 包的使用细节
- 在给文件打包时,该包对应文件夹(如 utils 文件夹对应的包名就是utils),文件的包名 通常 和文件所在的文件夹名一致,一般为 小写字母。
- 当一个文件要使用其它包函数或变量时,需要先引入对应的包
- 引入方式:import 包名
- package 指令在 文件第一行,然后是 import 指令。
- 在import包时,路径从
$GOPATH的 src下开始(不用带src,编译器会 自动从src下开始引入)
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问。比如 utils.go 的
func Cal() 函数 - 在访问其它包函数时,其语法是 包名.函数名(如调用 utils 包中的 Cal 方法:
utils.Cal()) - 如果包名较长,Go支持给包取别名(注意:取别名后,原来的包名就不能使用了)
- 在同一个包下,不能有相同的函数名(也不能有相同的全局变量名),否则会报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main,即
package main。这个就是一个语法规范,如果是写一个库,包名可以自定义。
样例如下:
目录结构
├───src
│ ├───main.go
│ └───utils.go
├───test
│ └───test.go
└───go.mod
编译后生成一个默认名的可执行文件 main.exe,在 $GOPATH 目录下,但是可能会出现如下问题:
package myapp/main is not in std (D:\C++\Go\src\myapp\main)
- 说明 Go 工具链 误以为你正在使用 GOPATH 模式,并试图在
$GOPATH/src下查找包myapp/main,但你的项目实际上是一个 Go Modules(module-aware)项目,或者路径结构不符合 GOPATH 要求。
原因:我这里使用 Go 命令为:go build myapp\main,表示 构建包 myapp/main,而不是构建某个 .go 文件。
Go 会:
- 在 模块模式(module mode) 下:查找
myapp/main是否是当前模块的子包。 - 在 GOPATH 模式 下:去
$GOPATH/src/myapp/main找源码。
由于窝的项目是启用了 Go Moudules(有 go.mod 文件),因此直接在模块根目录(myapp) 下构建即可
go build
4. 模块模式 和 GOPATH 模式的区别
1. 依赖管理方式:
- GOPATH 模式:
- 依赖的包集中存储在
$GOPATH/src目录下,所有项目共享这些依赖。 - 没有内置的、可靠的版本管理机制。如果两个项目需要同一个依赖的不同版本,就会出现冲突或问题。
- 项目结构严格依赖
$GOPATH的设置。
- 依赖的包集中存储在
- 模块模式 (Go Modules):
- 引入了版本控制。每个模块(项目)都有一个
go.mod文件,明确声明其所需的依赖及其精确版本。 - 依赖包的版本信息与项目本身关联,存储在本地的模块缓存中,而不是共享的
$GOPATH/src。 - 解决了不同项目依赖同一库的不同版本时的冲突问题。
- 引入了版本控制。每个模块(项目)都有一个
2. 代码位置和项目结构:
- GOPATH 模式:
- 项目代码必须放在
$GOPATH/src/目录下,其路径必须和导入路径(import path)匹配。例如,一个项目如果导入路径是github.com/user/project,那么项目代码必须在$GOPATH/src/github.com/user/project。 - 项目结构受限于
$GOPATH。
- 项目代码必须放在
- 模块模式 (Go Modules):
- 项目不再需要放在
$GOPATH/src目录下。你可以在任何文件系统位置初始化和开发你的 Go 项目。 - 一个项目就是一个模块,由其根目录下的
go.mod文件定义。
- 项目不再需要放在
3. 对 vendor 目录的支持:
- GOPATH 模式:
- 在 Go 1.5 引入
vendor机制后,项目可以选择将依赖的副本放在项目内的vendor目录下,Go 工具链会优先使用这个目录下的依赖。
- 在 Go 1.5 引入
- 模块模式 (Go Modules):
- 默认不使用
vendor目录。依赖项从模块缓存中获取。 - 但可以通过命令(如
go mod vendor)生成vendor目录,并在需要时(例如在受限的网络环境中构建)配置工具链使用它。
- 默认不使用
总的来说,模块模式是现代、推荐的 Go 项目管理方式,它解决了 GOPATH 模式中依赖版本管理混乱、项目位置受限等核心问题。 Go 语言社区和工具链目前都推荐和默认使用模块模式。
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【GoLang】的内容,请持续关注我 !!

更多推荐



所有评论(0)