在这里插入图片描述

📃个人主页: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 的设计哲学是"显式优于隐式",错误处理应该显式地处理,而不是通过异常机制隐式传播。这种设计哲学使代码更加健壮且易于理解。

核心设计理念

  1. 显式错误处理:错误作为普通值返回,而非通过异常机制
  2. 无异常机制:没有 try/catch/finally 结构
  3. 错误即值:错误是普通的值,实现了 error 接口
  4. 轻量级:不依赖调用栈展开,性能开销小

错误接口

Go 的核心错误机制基于内置的 error 接口:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误类型使用。

基本说明

  1. Go语言追求简洁优雅,所以,Go语言不支持传统的 try … catch … finally 这种处理
  2. Go中引入的处理方式为:defer,panic,recover
    • Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
  3. 异常的使用场景简单描述: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 特点:

  1. 内置函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误
2.2 Panic 工作原理

执行流程 如下:

  1. 触发 panicpanic() 被调用或运行时错误发生
  2. 停止当前函数:当前函数的执行立即停止
  3. 执行 defer 函数:按后进先出顺序执行当前 goroutine 的所有延迟函数(defer)
  4. 栈展开:向上层调用栈传播 panic
  5. 崩溃或恢复
    • 如果到达 main 函数仍未被捕获 → 程序崩溃
    • 如果在栈展开过程中被 recover 捕获 → 恢复执行

示例代码

func main() {
    fmt.Println("程序开始")
    riskyFunction()
    fmt.Println("这行不会执行") // 不会到达这里
}

func riskyFunction() {
    defer fmt.Println("defer 在 panic 前执行")
    panic("发生严重错误!")
    defer fmt.Println("这行不会执行") // 不会到达这里
}

// 输出如下: 
程序开始
deferpanic 前执行
panic: 发生严重错误!

goroutine 1 [running]:
main.riskyFunction()
        /tmp/sandbox123456/main.go:12
main.main()
        /tmp/sandbox123456/main.go:6
2.4 使用场景

虽然 Go 不鼓励过度使用 panic,但在以下情况是合适的:

  1. 不可恢复的错误

    data, err := os.ReadFile(path)
    if err != nil {
        panic("配置文件不存在,程序无法启动")
    }
    
  2. 程序初始化失败

    db, err := sql.Open("mysql", dsn)
    if err != nil {
        panic("数据库连接失败: " + err.Error())
    }
    
  3. 防御性编程(保护不变量)

    if amt < 0 {
        panic("交易金额不能为负")
    }
    
  4. 测试中的失败标记

    result := criticalOperation()
    if !result.IsValid() {
        panic("关键功能测试失败")
    }
    
2.5 注意事项
  1. 避免常规错误处理
    • 常规错误应使用 error 返回值
    • 保留 panic 给真正灾难性的情况
  2. 资源清理:使用 defer 确保资源释放
  3. recover 的位置:在包边界或 goroutine 顶层进行 recover
  4. 避免嵌套 panic:在 defer 函数中谨慎操作,避免引发二次 panic
  5. 提供有意义的 panic 值:使用字符串或自定义类型提供上下文信息

3. recover

recover 特点

1、用来控制一个 goroutinepanicking 行为,捕获panic,从而影响应用的行为
2、一般的调用建议
a). 在defer函数中,通过 recover 来终止一个 goroutinepanicking 过程,从而恢复正常代码的执行
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
... [堆栈跟踪信息] ...

注意事项

  1. 利用 recover 处理 panic 指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic 时,recover 无法捕获到panic,无法防止 panic 扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 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 语言中处理不可恢复错误的机制:

  • 用于严重错误导致程序无法继续执行的情况
  • 中断当前执行流并触发栈展开
  • 必须与 deferrecover 配合使用进行安全处理
  • 不应替代常规错误处理(使用 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.gopackage main + func main()
  • client.gopackage 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. 包的使用细节

  1. 在给文件打包时,该包对应文件夹(如 utils 文件夹对应的包名就是utils),文件的包名 通常 和文件所在的文件夹名一致,一般为 小写字母
  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包
    • 引入方式:import 包名
    • package 指令在 文件第一行,然后是 import 指令。
    • 在import包时,路径从 $GOPATH 的 src下开始(不用带src,编译器会 自动从src下开始引入
  3. 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问。比如 utils.go 的 func Cal() 函数
  4. 在访问其它包函数时,其语法是 包名.函数名(如调用 utils 包中的 Cal 方法:utils.Cal()
  5. 如果包名较长,Go支持给包取别名(注意:取别名后,原来的包名就不能使用了
  6. 在同一个包下,不能有相同的函数名(也不能有相同的全局变量名),否则会报重复定义
  7. 如果你要编译成一个可执行程序文件,就需要将这个包声明为 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 Modules):
    • 默认不使用 vendor 目录。依赖项从模块缓存中获取。
    • 但可以通过命令(如 go mod vendor)生成 vendor 目录,并在需要时(例如在受限的网络环境中构建)配置工具链使用它。

总的来说,模块模式是现代、推荐的 Go 项目管理方式,它解决了 GOPATH 模式中依赖版本管理混乱、项目位置受限等核心问题。 Go 语言社区和工具链目前都推荐和默认使用模块模式。

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【GoLang】的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐