函数基础
函数是 Go 语言中的核心构建单元。在 Go 中,函数是一等公民(First-class Citizen),可以作为参数传递、作为返回值返回、赋值给变量。Go 语言的程序执行从 main() 函数开始。
函数定义与调用
基本语法
func 函数名(参数列表) 返回值类型 {
函数体
}// 无参数无返回值
func sayHello() {
fmt.Println("Hello, World!")
}
// 有参数无返回值
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// 有参数有返回值
func add(a int, b int) int {
return a + b
}
// 参数类型相同时可以简写
func multiply(a, b int) int {
return a * b
}
// 多个返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
// 调用函数
func main() {
sayHello()
greet("Alice")
result := add(3, 5)
fmt.Println(result) // 8
quotient, err := divide(10, 3)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Printf("结果: %.2f\n", quotient) // 结果: 3.33
}
}Go 函数特点
- 函数参数和返回值都支持类型推导
- 相邻的同类型参数可以合并写法:
a, b int - 函数可以是导出的(首字母大写)或未导出的(首字母小写)
- Go 不支持函数重载(overloading)
多返回值
Go 支持函数返回多个值,这是 Go 处理错误的经典方式:
// 返回两个值
func divmod(a, b int) (int, int) {
return a / b, a % b
}
// 使用
quotient, remainder := divmod(17, 5)
fmt.Println(quotient, remainder) // 3 2
// 如果不关心某个返回值,使用 _ 忽略
quotient, _ = divmod(17, 5)Go 的惯用错误处理模式
Go 使用最后一个返回值返回 error 类型来表示函数是否执行成功:
func readFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("读取文件失败: %w", err)
}
return string(data), nil
}
// 调用
content, err := readFile("config.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(content)命名返回值
Go 支持为返回值命名,使代码更清晰,并支持”裸返回”(naked return):
// 命名返回值
func rectangle(width, height float64) (area float64, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // 裸返回:返回已命名的 area 和 perimeter
}
// 调用
a, p := rectangle(10, 5)
fmt.Printf("面积: %.2f, 周长: %.2f\n", a, p)
// 面积: 50.00, 周长: 30.00裸返回的使用建议
裸返回(return)虽然简洁,但如果函数较长,裸返回会降低代码可读性,因为读者需要往前查找每个返回值的赋值位置。建议仅在短函数中使用裸返回。
命名返回值在 defer 中修改返回值的场景也非常有用:
func double(n int) (result int) {
defer func() {
result *= 2 // 在函数返回前修改返回值
}()
result = n
return // 实际返回 result * 2
}
func main() {
fmt.Println(double(5)) // 10
}可变参数
使用 ...T 语法声明可变参数函数:
// 可变参数函数
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
fmt.Println(sum()) // 0
}可变参数的本质
在函数体内,可变参数 ...T 的类型是 []T(切片)。你可以直接使用切片操作来处理可变参数。
传递切片给可变参数
nums := []int{1, 2, 3, 4, 5}
// 使用 ... 将切片展开为可变参数
fmt.Println(sum(nums...)) // 15
// 如果直接传切片(不带 ...),编译错误
// sum(nums) // 编译错误:cannot use nums (type []int) as type int可变参数与其他参数结合
// 可变参数必须在参数列表的最后
func format(prefix string, values ...int) string {
result := prefix + ": "
for i, v := range values {
if i > 0 {
result += ", "
}
result += fmt.Sprintf("%d", v)
}
return result
}
func main() {
fmt.Println(format("数字", 1, 2, 3)) // 数字: 1, 2, 3
}匿名函数与闭包
匿名函数
Go 支持没有函数名的匿名函数。匿名函数可以直接调用或赋值给变量:
// 直接调用
func main() {
result := func(a, b int) int {
return a + b
}(3, 5) // 立即调用
fmt.Println(result) // 8
}
// 赋值给变量
func main() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(3, 5)) // 8
}闭包
闭包是一个函数值,它引用了其外部函数作用域中的变量。闭包不仅能访问这些变量,还能”记住”并修改它们。闭包是 Go 中实现函数式编程和状态封装的重要机制。
// 闭包:返回一个"累加器"函数
func counter() func() int {
count := 0 // 外部变量
return func() int {
count++ // 闭包引用并修改外部变量
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
c2 := counter() // 新的闭包实例,有独立的 count
fmt.Println(c2()) // 1
fmt.Println(c1()) // 4(c1 的 count 不受 c2 影响)
}闭包捕获变量的特性
闭包捕获的是变量的引用(而非值的拷贝)。这意味着如果外部变量被修改,闭包内看到的变化也会反映出来:
func makeGreeter(greeting string) func(string) string {
return func(name string) string {
return greeting + ", " + name + "!"
}
}
func main() {
hello := makeGreeter("Hello")
hi := makeGreeter("Hi")
fmt.Println(hello("World")) // Hello, World!
fmt.Println(hi("Go")) // Hi, Go!
}闭包的经典应用
// 1. 延迟计算(惰性求值)
func lazyValue() func() int {
fmt.Println("计算中...")
value := computeExpensiveValue()
return func() int {
return value // 值已经计算好了,调用时直接返回
}
}
// 2. 斐波那契生成器
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
fib := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(fib())
}
// 输出: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
}函数作为参数与返回值
Go 中函数是一等公民,可以赋值给变量、作为参数传递、作为返回值返回。
函数类型
// 定义函数类型
type Operation func(int, int) int
// 函数类型作为参数
func compute(a, b int, op Operation) int {
return op(a, b)
}
func main() {
add := func(a, b int) int { return a + b }
sub := func(a, b int) int { return a - b }
mul := func(a, b int) int { return a * b }
fmt.Println(compute(10, 3, add)) // 13
fmt.Println(compute(10, 3, sub)) // 7
fmt.Println(compute(10, 3, mul)) // 30
}函数作为返回值
// 返回不同策略的函数
func getStrategy(name string) func(int, int) int {
switch name {
case "add":
return func(a, b int) int { return a + b }
case "sub":
return func(a, b int) int { return a - b }
case "mul":
return func(a, b int) int { return a * b }
default:
return func(a, b int) int { return 0 }
}
}
func main() {
strategy := getStrategy("add")
fmt.Println(strategy(3, 5)) // 8
}实际应用:中间件模式
// 带日志的函数包装器
func withLogging(fn func()) {
fmt.Println("[开始] 调用函数")
fn()
fmt.Println("[结束] 函数返回")
}
// 带计时的函数包装器
func withTiming(fn func()) {
start := time.Now()
fn()
elapsed := time.Since(start)
fmt.Printf("执行耗时: %v\n", elapsed)
}
func main() {
work := func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("工作中...")
}
withLogging(withTiming(work))
}递归
递归是指函数直接或间接地调用自身。递归函数必须包含一个**基准情形(Base Case)**来终止递归,否则会导致栈溢出。
经典示例:阶乘
func factorial(n int) int {
if n <= 1 { // 基准情形
return 1
}
return n * factorial(n-1) // 递归调用
}
func main() {
fmt.Println(factorial(5)) // 120 (5 * 4 * 3 * 2 * 1)
fmt.Println(factorial(0)) // 1
}斐波那契数列
// 简单递归(效率低,指数时间复杂度)
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
// 使用闭包记忆化(Memoization)提高效率
func fibonacci() func(int) int {
cache := make(map[int]int)
var f func(int) int
f = func(n int) int {
if n <= 1 {
return n
}
if v, ok := cache[n]; ok {
return v
}
result := f(n-1) + f(n-2)
cache[n] = result
return result
}
return f
}
func main() {
fib := fibonacci()
fmt.Println(fib(10)) // 55
fmt.Println(fib(50)) // 12586269025(记忆化后速度快很多)
}递归的注意事项
- 必须有明确的基准情形,否则会导致无限递归和栈溢出
- Go 的默认 goroutine 栈大小较小(初始约 8KB),递归深度过大可能导致
stack overflow - 对于大问题,考虑使用迭代或尾递归优化
- 尾递归在 Go 中不会自动优化为循环
init 函数
init() 是 Go 中的一个特殊函数,它在包被导入时自动执行。每个源文件可以定义多个 init() 函数,它们在程序启动时按特定顺序执行。
init 函数的特性
- 每个源文件可以有多个
init()函数 init()函数不能被调用,也不能有参数和返回值init()在main()之前执行- 同一个文件中,多个
init()按声明顺序执行 - 不同文件中,按文件名的字典序执行
程序启动顺序
被导入的包的 init() → 当前包的 init() → main()package main
import (
"fmt"
)
var globalVar int
func init() {
fmt.Println("第一个 init 函数执行")
globalVar = 100
}
func init() {
fmt.Println("第二个 init 函数执行")
fmt.Println("globalVar =", globalVar)
}
func main() {
fmt.Println("main 函数执行")
}
// 输出:
// 第一个 init 函数执行
// 第二个 init 函数执行
// globalVar = 100
// main 函数执行init 函数的典型用途
// 1. 注册驱动(如数据库驱动)
import _ "github.com/go-sql-driver/mysql" // init 中注册 "mysql" 驱动
// 2. 初始化全局配置
var config *Config
func init() {
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatal(err)
}
config = parseConfig(data)
}
// 3. 初始化全局变量
var (
DB *sql.DB
Logger *log.Logger
)
func init() {
var err error
DB, err = sql.Open("mysql", "user:pass@/dbname")
if err != nil {
log.Fatal(err)
}
Logger = log.New(os.Stdout, "[APP] ", log.LstdFlags)
}init 的最佳实践
- 优先使用明确的初始化函数而非
init,因为init的执行顺序不透明 init应保持简单,避免复杂的依赖关系- 不要在
init中启动 goroutine(难以控制生命周期) - 测试时注意
init的副作用
练习题
练习 1:可变参数函数
编写一个函数 join(sep string, parts ...string) string,将可变参数用指定的分隔符连接成一个字符串。
解题思路:使用 strings.Join 函数,它接受一个字符串切片和一个分隔符。可变参数 parts 在函数体内就是 []string 类型。
代码:
package main
import (
"fmt"
"strings"
)
func join(sep string, parts ...string) string {
return strings.Join(parts, sep)
}
func main() {
fmt.Println(join(", ", "Alice", "Bob", "Charlie"))
// Alice, Bob, Charlie
fmt.Println(join(" | ", "Go", "is", "awesome"))
// Go | is | awesome
fmt.Println(join("-", "2024", "01", "01"))
// 2024-01-01
fmt.Println(join(",", "")) // 空字符串
}练习 2:闭包实现斐波那契生成器
使用闭包实现一个函数 newFibGenerator(),返回一个函数,每次调用时返回斐波那契数列的下一个值。
解题思路:闭包内维护两个变量 a 和 b,每次调用时更新它们的值。
代码:
package main
import "fmt"
func newFibGenerator() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
fib := newFibGenerator()
for i := 0; i < 15; i++ {
fmt.Printf("%d ", fib())
}
fmt.Println()
// 输出: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610
// 独立的生成器实例
fib2 := newFibGenerator()
fmt.Println(fib2()) // 1
fmt.Println(fib2()) // 1
fmt.Println(fib2()) // 2
}关键点:每次调用 newFibGenerator() 都会创建一个新的闭包实例,拥有独立的 a 和 b 变量。
练习 3:高阶函数实现 map 和 filter
使用函数类型实现类似函数式编程中的 map 和 filter 操作:
mapInts(nums []int, fn func(int) int) []int:对切片中每个元素应用函数filterInts(nums []int, fn func(int) bool) []int:保留满足条件的元素
解题思路:定义函数类型参数,在函数体内遍历切片并应用回调函数。
代码:
package main
import "fmt"
// mapInts 对切片中每个元素应用转换函数
func mapInts(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, v := range nums {
result[i] = fn(v)
}
return result
}
// filterInts 保留满足条件的元素
func filterInts(nums []int, fn func(int) bool) []int {
var result []int
for _, v := range nums {
if fn(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 每个元素平方
squared := mapInts(nums, func(n int) int {
return n * n
})
fmt.Println(squared) // [1 4 9 16 25 36 49 64 81 100]
// 筛选偶数
evens := filterInts(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4 6 8 10]
// 筛选大于 5 的元素
greaterThanFive := filterInts(nums, func(n int) bool {
return n > 5
})
fmt.Println(greaterThanFive) // [6 7 8 9 10]
// 组合使用:先筛选偶数,再平方
evensSquared := mapInts(
filterInts(nums, func(n int) bool { return n%2 == 0 }),
func(n int) int { return n * n },
)
fmt.Println(evensSquared) // [4 16 36 64 100]
}