Go 零基础教程

Go 函数参数与返回值

本章将深入探讨函数中极其关键的两个环节:参数 (Parameters)返回值 (Return Values),研究如何高效地将数据传入函数,并把结果接收回来。

1. 定义函数参数

函数参数就是列在函数定义括号里的那些变量。它们允许你把外部数据传递到函数内部,使得函数每次被调用时都能处理不同的输入。

1.1 基础参数

定义参数最直白的方式,就是在函数的括号内依次写明参数的名称和数据类型。

package main

import "fmt"

// add 接收两个整数参数 a 和 b,并返回它们的和。
func add(a int, b int) int {
	return a + b
}

func main() {
	result := add(5, 3) // 使用参数 5 和 3 调用 add 函数
	fmt.Println(result) // 输出: 8
}

在这个例子中,add 函数接收两个参数 ab,它们的类型都是 int。当调用 add(5, 3) 时,数值 5 被赋给了 a,数值 3 被赋给了 b

1.2 参数类型简写 (Shorthand)

如果连续的几个参数拥有相同的数据类型,你不需要每个都写一遍类型。你只需要在这一组参数的最后一个后面写一次类型即可。

package main

import "fmt"

// multiply 接收两个整数参数 a 和 b,并返回它们的乘积。
// 请注意相同类型参数的简写声明方式。
func multiply(a, b int) int {
	return a * b
}

func main() {
	product := multiply(4, 6) // 使用参数 4 和 6 调用 multiply 函数
	fmt.Println(product)      // 输出: 24
}

在这里,a, b int 完全等价于 a int, b int。这种简写大大提高了代码的可读性,尤其是在处理一大串同类型参数时。

1.3 值传递机制 (Passing by Value)

在 Go 语言中,参数是通过**“值传递”的方式传入的。这意味着当你把一个变量传给函数时,Go 会在内存中复制一份**该变量的值,并在函数内部使用这个副本。因此,在函数内部对参数进行的任何修改,都绝对不会影响到函数外部的原始变量。

package main

import "fmt"

// modifyValue 试图修改传入参数的值。
func modifyValue(x int) {
	x = x * 2 // 在函数内部修改 x 的值
	fmt.Println("modifyValue 内部:", x) // 输出: modifyValue 内部: 20
}

func main() {
	num := 10
	modifyValue(num) // 将 num 传给 modifyValue 函数
	fmt.Println("modifyValue 外部:", num) // 输出: modifyValue 外部: 10
}

在这个例子中,num 被传给了 modifyValue。在函数内部,x 只是 num 的一个副本。修改 x 的值并不会改变 main 函数中 num 的值。理解这种数据处理方式对掌握 Go 函数至关重要。

1.4 忽略参数 (Ignoring Parameters)

有时候,你定义了一个函数,它的签名要求必须有某个参数,但在函数体内你压根用不到它。在这种情况下,你可以使用空白标识符 _ (下划线) 来忽略这个参数。

package main

import "fmt"

// greet 忽略了传入的 name 参数,但依然满足了函数签名的要求。
func greet(_ string) {
	fmt.Println("你好呀!")
}

func main() {
	greet("Alice") // 传入了一个参数,但它在函数里被忽略了
}

在这里,名字参数被声明了但没被使用。使用空白标识符 _ 可以防止 Go 编译器报错(Go 对声明了却不使用的变量非常严格)。这在实现某些强制要求特定函数签名的接口 (Interfaces) 时非常有用。

2. 定义函数返回值

函数可以将执行结果返回给调用者。返回值的类型需要在函数参数列表之后进行声明。

2.1 单一返回值

最常见的情况就是函数只返回一个值。

package main

import "fmt"

// square 计算一个整数的平方并返回结果。
func square(x int) int {
	return x * x
}

func main() {
	result := square(7) // 用参数 7 调用 square 函数
	fmt.Println(result) // 输出: 49
}

在这个例子中,square 接收一个整数 x,并返回它的平方(同样也是一个整数)。

2.2 多返回值 (Multiple Return Values)

Go 语言拥有一个极其强大的特性:允许函数同时返回多个值。这在 Go 中最常用于同时返回一个“结果”和一个“错误状态”。

package main

import (
	"fmt"
	"errors"
)

// divide 执行整数除法,返回商和余数。
// 如果除数为零,它还会返回一个错误。
func divide(numerator, denominator int) (int, int, error) {
	if denominator == 0 {
		return 0, 0, errors.New("不能除以零")
	}
	quotient := numerator / denominator
	remainder := numerator % denominator
	
	// 返回多个值:商、余数、以及代表没有错误的 nil
	return quotient, remainder, nil 
}

func main() {
	// 调用 divide 函数并接收多个返回值
	quotient, remainder, err := divide(10, 3) 
	if err != nil {
		fmt.Println("发生错误:", err)
		return
	}
	fmt.Println("商:", quotient)   // 输出: 商: 3
	fmt.Println("余数:", remainder) // 输出: 余数: 1
	
	// 使用空白标识符 _ 忽略商和余数,只关心是否报错
	_, _, err = divide(5, 0)
	if err != nil {
		fmt.Println("发生错误:", err) // 输出: 发生错误: 不能除以零
		return
	}
}

在上面的代码中,divide 返回了三个值:商、余数和一个 error。如果除法成功,错误值就是 nil;如果除数为零,就会返回一个错误对象。这让调用者能非常方便地检查并处理错误。

2.3 命名返回值 (Named Return Values)

Go 允许你在函数签名中直接给返回值命名。这能提升代码的可读性,特别是当返回多个值时。
命名返回值会被当作在函数顶部声明的局部变量。随后,你可以使用一个“裸返回” (Naked return) 语句(即 return 后面不跟任何东西),它会自动把你命名的这些变量的当前值返回出去。

package main

import "fmt"

// calculateAreaAndPerimeter 计算矩形的面积和周长。
// 为了清晰起见,使用了命名返回值。
func calculateAreaAndPerimeter(length, width float64) (area, perimeter float64) {
	area = length * width
	perimeter = 2 * (length + width)
	return // 裸返回语句;自动返回 area 和 perimeter 的当前值
}

func main() {
	a, p := calculateAreaAndPerimeter(5.0, 4.0)
	fmt.Println("面积:", a) // 输出: 面积: 20
	fmt.Println("周长:", p) // 输出: 周长: 18
}

虽然命名返回值在某些情况下能提高可读性,但如果函数逻辑很复杂,滥用它们反而会让人搞不清到底返回了什么。使用时需要看情况把握分寸。

2.4 忽略返回值

和忽略参数一样,如果你调用了一个多返回值的函数,但只对其中某几个结果感兴趣,你可以使用空白标识符 _ 来丢弃不需要的返回值。

package main

import "fmt"

// getCoordinates 返回 x 和 y 坐标。
func getCoordinates() (int, int) {
	return 10, 20
}

func main() {
	x, _ := getCoordinates() // 忽略了第二个返回值 (y 坐标)
	fmt.Println("X 坐标:", x) // 输出: X 坐标: 10
}

3. 实战案例演示

让我们通过几个实战案例,来看看在不同场景下如何运用函数参数和返回值。

3.1 案例 1:计算一组数字的平均值

这个例子演示了如何将一个切片 (Slice) 作为参数传给函数,并返回计算后的平均值。

package main

import "fmt"

// calculateAverage 计算一个浮点数切片的平均值。
func calculateAverage(numbers []float64) float64 {
	if len(numbers) == 0 {
		return 0 // 如果切片为空,返回 0 以防止除以零的错误
	}
	sum := 0.0
	for _, number := range numbers {
		sum += number
	}
	return sum / float64(len(numbers))
}

func main() {
	values := []float64{10.0, 20.0, 30.0, 40.0, 50.0}
	average := calculateAverage(values) // 传入切片进行调用
	fmt.Println("平均值:", average)      // 输出: 平均值: 30
}

3.2 案例 2:温度单位转换

这个例子演示了在摄氏度和华氏度之间互相转换。

package main

import "fmt"

// celsiusToFahrenheit 将摄氏度转换为华氏度。
func celsiusToFahrenheit(celsius float64) float64 {
	return (celsius * 9 / 5) + 32
}

// fahrenheitToCelsius 将华氏度转换为摄氏度。
func fahrenheitToCelsius(fahrenheit float64) float64 {
	return (fahrenheit - 32) * 5 / 9
}

func main() {
	celsius := 25.0
	fahrenheit := celsiusToFahrenheit(celsius) 
	fmt.Printf("%.2f°C 等于 %.2f°F\n", celsius, fahrenheit) // 输出: 25.00°C 等于 77.00°F
	
	fahrenheit = 77.0
	celsius = fahrenheitToCelsius(fahrenheit) 
	fmt.Printf("%.2f°F 等于 %.2f°C\n", fahrenheit, celsius) // 输出: 77.00°F 等于 25.00°C
}

3.3 案例 3:模拟基础计算器

这个例子展示了如何创建一个简单的计算器,其中除法函数利用了多返回值来进行错误处理。

package main

import (
	"fmt"
	"errors"
)

// add 执行加法。
func add(a, b float64) float64 { return a + b }

// subtract 执行减法。
func subtract(a, b float64) float64 { return a - b }

// multiply 执行乘法。
func multiply(a, b float64) float64 { return a * b }

// divide 执行除法,如果除数为零则返回错误。
func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("除数不能为零")
	}
	return a / b, nil
}

func main() {
	num1 := 10.0
	num2 := 5.0
	
	fmt.Println("加法结果:", add(num1, num2))       // 输出: 加法结果: 15
	fmt.Println("减法结果:", subtract(num1, num2))  // 输出: 减法结果: 5
	fmt.Println("乘法结果:", multiply(num1, num2))  // 输出: 乘法结果: 50
	
	quotient, err := divide(num1, num2) 
	if err != nil {
		fmt.Println("错误:", err)
	} else {
		fmt.Println("除法结果:", quotient) // 输出: 除法结果: 2
	}
	
	// 尝试除以零
	_, err = divide(num1, 0) 
	if err != nil {
		fmt.Println("错误:", err) // 输出: 错误: 除数不能为零
	}
}