第四周 函数与错误处理深度解析
以下是第4周函数基础的深度教程,包含两个完整案例和详细实现细节:
第四周:函数与错误处理深度解析
一、函数定义与参数传递
1. 基础函数结构
// 基本语法
func 函数名(参数列表) 返回值类型 {
// 函数体
}
// 示例:计算圆面积
func circleArea(radius float64) float64 {
return math.Pi * radius * radius
}
2. 参数传递方式
类型 | 语法 | 特点 | 适用场景 |
---|---|---|---|
值传递 | func(a int) |
创建副本 | 小型数据 |
指针传递 | func(a *int) |
操作原值 | 大型结构体 |
引用类型 | func(s []int) |
共享底层数组 | 切片/map |
3. 参数类型详解
案例1:值传递 vs 指针传递
// 值传递示例
func addValue(n int) {
n += 10
}
// 指针传递示例
func addPointer(n *int) {
*n += 10
}
func main() {
num := 5
addValue(num) // 不影响原值
addPointer(&num) // 修改原值
fmt.Println(num) // 输出15
}
二、返回值与错误处理
1. 多返回值
// 返回商和余数
func div(a, b int) (int, int) {
return a/b, a%b
}
// 使用
q, r := div(17, 5) // q=3, r=2
2. 错误处理规范
// 标准错误返回格式
func sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("负数不能求平方根")
}
return math.Sqrt(x), nil
}
3. 错误处理实践
案例2:安全除法函数
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零 (a=%d, b=%d)", a, b)
}
return a / b, nil
}
// 使用示例
result, err := safeDivide(10, 0)
if err != nil {
log.Printf("计算失败: %v", err)
// 输出:计算失败: 除数不能为零 (a=10, b=0)
}
三、素数判断任务实现
需求分析
- 函数
isPrime
接收整数参数 - 返回:
true
:质数false
:非质数error
:输入非法(n < 2)
- 使用优化算法(试除法到平方根)
版本1:基础实现
func isPrime(n int) (bool, error) {
if n < 2 {
return false, fmt.Errorf("无效输入:%d 必须大于1", n)
}
// 单独处理2(唯一的偶质数)
if n == 2 {
return true, nil
}
// 排除偶数
if n%2 == 0 {
return false, nil
}
// 试除到平方根
max := int(math.Sqrt(float64(n)))
for i := 3; i <= max; i += 2 {
if n%i == 0 {
return false, nil
}
}
return true, nil
}
版本2:性能优化(预计算小质数)
var smallPrimes = []int{2, 3, 5, 7, 11, 13}
func optimizedIsPrime(n int) (bool, error) {
if n < 2 {
return false, fmt.Errorf("invalid input: %d", n)
}
// 先检查小质数
for _, p := range smallPrimes {
if n == p {
return true, nil
}
if n%p == 0 {
return false, nil
}
}
// 从17开始检查(大于预存小质数的下一个奇数)
max := int(math.Sqrt(float64(n)))
for i := 17; i <= max; i += 2 {
if n%i == 0 {
return false, nil
}
}
return true, nil
}
四、测试驱动开发
1. 表格驱动测试
func TestIsPrime(t *testing.T) {
tests := []struct {
name string
input int
want bool
wantErr bool
}{
{"负数测试", -5, false, true},
{"0测试", 0, false, true},
{"1测试", 1, false, true},
{"最小质数", 2, true, false},
{"偶合数", 4, false, false},
{"大质数", 9973, true, false},
{"平方数", 25, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := isPrime(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("isPrime() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("isPrime() = %v, want %v", got, tt.want)
}
})
}
}
2. 性能基准测试
func BenchmarkIsPrime(b *testing.B) {
for i := 0; i < b.N; i++ {
isPrime(104729) // 第10000个质数
}
}
五、错误处理进阶
1. 错误包装
func process(n int) error {
if valid, err := isPrime(n); err != nil {
return fmt.Errorf("处理失败: %w", err) // 使用%w包装错误
} else if !valid {
return errors.New("需要质数输入")
}
return nil
}
2. 错误类型断言
func handleError(err error) {
if pe, ok := err.(*PrimeError); ok {
fmt.Printf("质数错误代码%d: %s\n", pe.Code, pe.Message)
} else {
fmt.Println("通用错误:", err)
}
}
// 自定义错误类型
type PrimeError struct {
Code int
Message string
}
func (e *PrimeError) Error() string {
return fmt.Sprintf("错误%d: %s", e.Code, e.Message)
}
六、扩展练习
质因数分解函数:
func primeFactors(n int) ([]int, error) { // 返回所有质因数 // 示例:输入12 → [2, 2, 3] }
并发素数筛选:
// 实现并发版本的埃拉托斯特尼筛法 func sieve(max int) []int { // 返回所有小于max的质数 }
七、学习检查清单
- 能正确声明带多个返回值的函数
- 会使用指针参数修改外部变量
- 理解error接口的使用场景
- 能编写表格驱动测试用例
- 会处理负数输入的边界情况
- 能解释试除法的优化原理
- 理解自定义错误类型的优势
通过这两个完整案例(安全除法与素数判断),你已掌握Go函数的定义、参数传递、错误处理和测试方法。建议:
- 实际运行测试用例观察结果
- 使用
go test -bench .
查看性能差异 - 尝试为优化版本添加更多预计算质数
- 实现扩展练习中的质因数分解功能
以下同扩展部分的代码与分析
以下是两个扩展练习的完整实现代码,包含详细注释说明每个关键步骤:
扩展练习1:质因数分解函数
// primeFactors 返回给定整数的质因数分解结果
// 输入:n - 需要分解的正整数(n >= 2)
// 返回:质因数切片(升序排列)和可能的错误
func primeFactors(n int) ([]int, error) {
// 输入验证
if n < 2 {
return nil, fmt.Errorf("无效输入:%d 必须大于1", n)
}
factors := make([]int, 0)
original := n
// 处理偶数因子
for n%2 == 0 {
factors = append(factors, 2)
n /= 2
}
// 从3开始检查奇数因子(步长2)
for i := 3; i*i <= n; i += 2 {
// 当i是因子时持续除尽
for n%i == 0 {
factors = append(factors, i)
n /= i
}
}
// 处理剩余的大质数因子
if n > 2 {
factors = append(factors, n)
}
// 验证分解结果
product := 1
for _, f := range factors {
product *= f
}
if product != original {
return nil, fmt.Errorf("分解验证失败:%d != %d", product, original)
}
return factors, nil
}
/* 使用示例:
factors, err := primeFactors(360)
if err != nil {
log.Fatal(err)
}
fmt.Println(factors) // 输出:[2 2 2 3 3 5]
*/
扩展练习2:并发素数筛选(埃拉托斯特尼筛法)
// sieve 使用并发筛法返回所有小于max的质数
func sieve(max int) []int {
if max < 2 {
return []int{}
}
// 创建初始数字生成器(2到max)
ch := generateNumbers(2, max)
primes := make([]int, 0)
for {
// 从当前通道获取候选质数
p, ok := <-ch
if !ok {
break
}
primes = append(primes, p)
// 创建新的过滤通道
ch = filter(ch, p)
}
return primes
}
// generateNumbers 生成连续整数序列
func generateNumbers(start, end int) <-chan int {
ch := make(chan int)
go func() {
for i := start; i <= end; i++ {
ch <- i
}
close(ch)
}()
return ch
}
// filter 创建新的通道过滤特定质数的倍数
func filter(in <-chan int, prime int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
// 过滤掉能被当前质数整除的数
if n%prime != 0 {
out <- n
}
}
close(out)
}()
return out
}
/* 使用示例:
primes := sieve(30)
fmt.Println(primes) // 输出:[2 3 5 7 11 13 17 19 23 29]
*/
代码关键点解析
质因数分解函数
- 输入验证:确保输入符合质因数分解的基本要求
- 分阶段处理:
- 先处理所有偶数因子(2的幂次)
- 再处理奇数因子(从3开始,步长2)
- 验证机制:最终乘积验证确保分解的正确性
- 性能优化:
- 循环上限设置为√n(i*i <= n)
- 跳过偶数检查(通过步长2)
并发素数筛法
- 通道链式结构:
- 动态过滤:
- 每个新发现的质数都会创建一个新的过滤通道
- 前序通道处理完成后自动关闭
- 并发控制:
- 每个filter操作在独立的goroutine中执行
- 通过通道关闭机制自然终止循环
测试用例
质因数分解测试
func TestPrimeFactors(t *testing.T) {
tests := []struct {
input int
expect []int
}{
{12, []int{2, 2, 3}},
{360, []int{2, 2, 2, 3, 3, 5}},
{9973, []int{9973}}, // 大质数
{1024, []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2}},
}
for _, tt := range tests {
result, _ := primeFactors(tt.input)
if !reflect.DeepEqual(result, tt.expect) {
t.Errorf("输入%d 期望%v 实际%v", tt.input, tt.expect, result)
}
}
}
并发筛法测试
func TestSieve(t *testing.T) {
expected := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
result := sieve(30)
if !reflect.DeepEqual(result, expected) {
t.Errorf("期望%v 实际%v", expected, result)
}
}
性能对比
实现方式 | 时间复杂度 | 空间复杂度 | 适合场景 |
---|---|---|---|
传统筛法 | O(n log log n) | O(n) | 大数据量 |
并发筛法 | O(n)(理论) | O(k)(k为goroutine数量) | 学习并发原理 |
实际测试显示,当max超过1,000,000时,传统实现效率更高。并发版本更适合理解通道和goroutine的协作机制。
扩展方向
质因数分解优化:
- 预生成小质数列表加速试除过程
- 添加Pollard’s Rho算法处理大数分解
并发筛法改进:
- 限制最大goroutine数量
- 添加超时控制
- 实现分片并行处理
这两个练习展示了Go语言在处理数学问题和并发编程方面的优势,建议通过实际调试观察通道数据的流动过程,可以更深入理解并发筛法的工作原理。