05、函数

一、函数基础定义

语法格式

func 函数名(参数列表) 返回值列表 {
    函数体
}

**函数名:**标识符,首字母大写包外可调用,小写仅包内私有

**返回值:**可单返回、多返回、命名返回、无返回

基础示例:

// 无参无返回
func hello() {
    fmt.Println("hello go")
}

// 单参单返回
func add(a int, b int) int {
    return a + b
}

// 参数类型简写:同类型只写一次
func sub(a, b int) int {
    return a - b
}

二、函数参数详解

1. 值传递(默认)

Go 所有参数默认值拷贝,函数内修改不会影响外部原变量

func changeNum(x int) {
    x = 100
}
func main() {
    a := 10
    changeNum(a)
    fmt.Println(a) // 输出10,原值不变
}

2. 指针参数(引用修改)

传入内存地址,内部解引用修改外部变量

func changeNum2(x *int) {
    *x = 100
}
func main() {
    a := 10
    changeNum2(&a)
    fmt.Println(a) // 输出100

/*
& 在变量前面 → 拿地址,得到指针内存地址;
* 在指针前面 → 顺着指针内存地址取出里面的数据值。
*/
    var a int = 10
    // &a:获取变量a的内存地址,类型是 *int(int指针)
    p := &a

    fmt.Println(a)    // 10,变量本身的值
    fmt.Println(&a)   // 0xc000018088,a的内存地址
    fmt.Println(*(&a)) // 等价 a,输出10
    fmt.Println(p)    // 0xc000018088,p保存a的地址
    fmt.Println(*p)   // 10,解引用:通过地址取出a里面的值
    
}

示例:

package main

import "fmt"

func multiply(a, b int, res *int){
    *res = a * b
}

func main(){
    n := 0
    res := &n
    multiply(2, 4, res)
    fmt.Println("Result:", *res) // Result: 8
    
}

3. 可变参数 ...T

接收任意个同类型参数,本质是切片

规则:可变参数必须放在参数列表最后

func sum(nums ...int) int {
    total := 0
    for _, v := range nums {
        total += v
    }
    return total
}
func main() {
    fmt.Println(sum(1,2,3))      //6
    fmt.Println(sum(1,2,3,4,5))  //15
    // 切片解包传入
    arr := []int{10,20}     
    fmt.Println(sum(arr...))     //30
}

示例:

package main

import "fmt"

func ageMinOrMax(m string, a ...int) int {
	if len(a) == 0 {
		return 0 // 如果没有传入参数,返回0
	}
	if m == "max" {
		max := a[0]
		for _, v := range a {
			if max < v {
				max = v
			}
		}
		return max
	} else if m == "min" {
		min := a[0]
		for _, v := range a {
			if min > v {
				min = v
			}
		}
		return min
	} else {
		e := -1 // 如果没有传入参数,返回-1
		return e
	}
}
func main() {
	//手动填写参数
	age := ageMinOrMax("min", 1, 3, 2, 0)
	fmt.Printf("最小年龄是: %d\n", age) // 输出:最小年龄是: 0
	ageArr := []int{7, 9, 3, 5, 1}
	age = ageMinOrMax("max", ageArr...)
	fmt.Printf("最大年龄是: %d\n", age) // 输出:最大年龄是: 9
}

4.函数作为参数(高阶函数)

函数是一等公民,可当作参数 / 返回值

// 函数作为类型回调函数类型
type calc func(int, int) int

func compute(a, b int, f calc) int {
    a = a * 2
    return f(a, b)
}

func add(x, y int) int {
	return x + y
}//2选1

func main() {
    //add := func(x,y int)int{return x+y}  //2选1
    fmt.Println(compute(3,4,add))       // 输出:10
}

函数作为类型示例1:

package main

import "fmt"

func isOdd(v int) bool {
	if v%2 == 0 {
		return false
	}
	return true
}
func isEven(v int) bool {
	if v%2 == 0 {
		return true
	}
	return false
}

type boolFunc func(int) bool //声明一个函数类型,参数为int,返回值为bool

//函数作为一个参数使用
func filter(slice []int, f boolFunc) []int {
	var result []int
	for _, value := range slice {
		if f(value) {
			result = append(result, value)
		}
	}
	return result
}
func main() {

    slice := []int{3, 1, 4, 5, 9, 2}
	fmt.Println("slice = ", slice)
	odd := filter(slice, isOdd)   //函数当作值来传递
	fmt.Println("odd = ", odd)    // 输出: odd =  [3 1 5 9]
	even := filter(slice, isEven) //函数当作值来传递
	fmt.Println("even = ", even)  // 输出:even =  [4 2]

}

函数作为类型示例2:

package main

import "fmt"

// 【定义函数类型】CalcFunc 是一种函数类型
type CalcFunc func(int, int) int

// 参数使用自定义函数类型 CalcFunc
func operate(a, b int, calc CalcFunc) int {
	return calc(a, b)
}

// 还可以用函数类型做返回值
func getOperator(op string) CalcFunc {
	switch op {
	case "+":
		return func(x, y int) int { return x + y }
	case "-":
		return func(x, y int) int { return x - y }
	default:
		return nil
	}
}

func main() {
	var add CalcFunc = func(x, y int) int { return x + y }
	fmt.Println(operate(20, 6, add))  //26

	mul := CalcFunc(func(x, y int) int { return x * y })
	fmt.Println(operate(20, 6, mul))  //120

	// 从函数获取CalcFunc类型变量
	sub := getOperator("-")
	fmt.Println(operate(20, 6, sub))  //14
}

三、返回值四种写法

1.单个返回值

func mul(a,b int) int {
    return a * b
}

2.多返回值(Go 特色,常用于返回结果 + 错误)

func div(a,b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("除数不能为0")
    }
    return a/b, nil
}
// 调用
res, err := div(10, 2)
if err != nil {
    log.Fatal(err)
}

3.命名返回值

提前定义返回变量,可裸 return

func rect(w, h int) (area, perimeter int) {
    area = w * h
    perimeter = 2 * (w + h)
    // 裸return,自动返回命名变量
    return
}
//注意:命名返回会在函数初始化时赋零值,复杂逻辑建议显式 return

4.忽略返回值 _

package main

import "fmt"

func div(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("除数不能为0")
	}
	return a / b, nil
}

func main() {
	// 接收两个返回值
	res, err := div(10, 2)
	fmt.Println(res, err)   // 5 <nil>

	// 忽略第二个返回值 error
	val, _ := div(20, 4)
	fmt.Println(val)       // 5 

	// ❌ 错误:_ 不能读取
	// fmt.Println(_)
}

四、匿名函数(闭包)

1. 基础匿名函数

匿名函数:没有函数名的函数

两种使用方式:立即执行、赋值变量

// 1. 立即执行
func() {
    fmt.Println("匿名函数直接运行")
}()
// 定义并立即执行
func(a, b int) {
     fmt.Println(a + b)
}(3, 5)
// 2. 赋值给变量
f := func(x int) int {
    return x * 2
}
fmt.Println(f(5))

2. 闭包:捕获外部变量

闭包:匿名函数捕获外部作用域变量,持有变量引用(不是拷贝)

重点:闭包保存变量引用,不是当时的值

package main

import "fmt"

func main() {
    num := 10

    // 闭包捕获外部变量 num
    f := func() {
        num++
        fmt.Println("num:", num)
    }

    f()  // num:11
    f()  // num:12
    num = 100
    f()  // num:101
}

闭包修改外部指针变量(对比值捕获)

package main

import "fmt"

func main() {
    a := 1
    // 捕获值
    f1 := func(x int) {
        x = 100
    }
    f1(a)
    fmt.Println(a) //1

    // 捕获引用
    f2 := func(p *int) {
        *p = 200
    }
    f2(&a)
    fmt.Println(a) //200
}

示例1:

func counter() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}
func main() {
    c1 := counter()
    fmt.Println(c1()) //1
    fmt.Println(c1()) //2
    c2 := counter()
    fmt.Println(c2()) //1 独立计数
}

示例2:

package main

import "fmt"

func main(){
	j := 5
	a := func() func() {
        i :=8
			return func() {
					fmt.Printf("i = %d j = %d\n", i, j)
			}
	}()
	a()
	j = 12
	a()
}
/*
i = 8 j = 5
i = 8 j = 12
*/

关键点:

  1. 闭包会持有捕获变量的引用,不是拷贝值
  2. 每次调用外层函数,生成一组独立变量环境;
  3. 外层函数执行完毕,局部变量不会被 GC 回收,被闭包持有。

闭包在业务开发中的主要用途:

1.封装状态,实现轻量 “对象”(无 struct)

不需要定义结构体,利用闭包持久保存私有状态,实现数据封装。

典型场景:限流器、计数器、序列号生成器

func NewCounter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}
//对比 struct:适合单一职责、少量状态;复杂状态优先 struct。

2.延迟处理、回调函数(最常用)

http.HandleFunc 处理器本质就是闭包,捕获请求上下文、配置、日志对象

cfg := loadConfig()
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
    // 闭包捕获cfg,直接使用配置
    handleRequest(w, r, cfg)
})

gRPC 中间件、回调、事件处理器大量使用。

3.函数式编程:过滤器、映射、遍历处理

封装通用遍历逻辑,外部传入自定义逻辑

package main

import "fmt"

// FilterInts 通用整数切片过滤器
// predicate: 断言函数,由外部传入,可以是普通函数/闭包
func FilterInts(list []int, predicate func(int) bool) []int {
	var res []int
	for _, v := range list {
		if predicate(v) {
			res = append(res, v)
		}
	}
	return res
}

func main() {
	data := []int{2, 8, 12, 15, 3, 20, 7}

	// 外部变量 threshold,闭包会捕获这个变量的引用
	threshold := 10

	// 匿名闭包捕获外层 threshold
	greaterThan10 := FilterInts(data, func(x int) bool {
		return x > threshold
	})
	fmt.Println("大于10的数字:", greaterThan10)  //[12 15 20]

	// 修改阈值,再次调用(演示捕获的是引用)
	threshold = 15
	greaterThan15 := FilterInts(data, func(x int) bool {
		return x > threshold
	})
	fmt.Println("大于15的数字:", greaterThan15) // [20]

	// 多个不同条件的过滤器复用同一个FilterInts
	min := 5
	max := 18
	between := FilterInts(data, func(x int) bool {
		return x >= min && x <= max
	})
	fmt.Println("5~18之间数字:", between) // [8 12 15 7]
}
/*
大于10的数字: [12 15 20]
大于15的数字: [20]
5~18之间数字: [8 12 15 7]
func(x int)bool{} 是匿名闭包,捕获外层 threshold/min/max;
捕获的是变量引用:后续修改threshold,新创建的闭包读取最新值;
FilterInts 是高阶函数,接收函数作为参数,实现逻辑解耦;
不用修改FilterInts函数,只传不同闭包就能实现各种筛选条件。
*/

拓展升级版(泛型,支持任意类型切片 Go1.18+)

日常业务更通用,不限于 int:

package main

import "fmt"

// Filter 泛型通用过滤器
func Filter[T any](slice []T, predicate func(T) bool) []T {
	var result []T
	for _, item := range slice {
		if predicate(item) {
			result = append(result, item)
		}
	}
	return result
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6}
	limit := 3
	out := Filter(nums, func(n int) bool {
		return n > limit
	})
	fmt.Println(out) // [4 5 6]
}

4.中间件、装饰器模式(Go Web 重中之重)

对原有函数增强:增加日志、耗时统计、panic 捕获、权限校验

package main

import (
	"fmt"
	"time"
)

// WithTrace 装饰器函数
// 入参:被包装的业务函数 fn func()
// 返回:包装后的新函数 func()
func WithTrace(fn func()) func() {
	// 返回一个闭包
	return func() {
		start := time.Now()
		// defer 在函数退出时执行,打印耗时
		defer func() {
			cost := time.Since(start)
			fmt.Printf("【耗时统计】执行耗时 = %v\n", cost)
		}()

		// 执行原始业务逻辑
		fn()
	}
}

// 业务函数1
func BusinessFuncA() {
	fmt.Println("开始执行业务A")
	time.Sleep(200 * time.Millisecond)
	fmt.Println("业务A执行完成")
}

// 业务函数2
func BusinessFuncB() {
	fmt.Println("开始执行业务B")
	time.Sleep(500 * time.Millisecond)
	fmt.Println("业务B执行完成")
}

func main() {
	fmt.Println("===== 使用装饰器包装函数 =====")
	// 用WithTrace包装原始函数,得到新函数
	wrappedA := WithTrace(BusinessFuncA)
	wrappedB := WithTrace(BusinessFuncB)

	// 调用包装后的函数
	wrappedA()
	fmt.Println()
	wrappedB()
}
/*

===== 使用装饰器包装函数 =====
开始执行业务A
业务A执行完成
【耗时统计】执行耗时 = 200.319352ms
开始执行业务B
业务B执行完成
【耗时统计】执行耗时 = 500.601365ms

进入闭包内部
start := time.Now() 记录开始时间
注册 defer(先注册,后执行,函数退出前触发)
fn() → 执行原始 BusinessFuncA()
原始函数执行完毕,触发 defer
计算 time.Since(start),打印耗时
*/

Gin/Echo 中间件底层思想大量依赖闭包。

5.资源捕获 + 延迟释放(循环、协程场景)

捕获资源句柄,在函数退出、协程内统一关闭;

搭配 goroutine 使用(注意循环变量陷阱)。

6.构造可配置函数(工厂模式)

通过外层函数传入参数,生成定制化函数

func MakeMultiplier(base int) func(int) int {
    return func(x int) int {
        return base * x
    }
}
double := MakeMultiplier(2)
triple := MakeMultiplier(3)
fmt.Println(double(5)) //10

创建不同超时时间的 http 客户端请求函数。

7.协程携带上下文数据

启动 goroutine 时,用闭包携带当前请求 traceID、用户信息,避免传递大量参数。

⚠️ 注意:循环启动 goroutine 务必拷贝循环变量!

8.实现迭代器(简易)

持续产出数据,保存迭代游标

func Iterator(max int) func() (int, bool) {
    i := 0
    return func() (int, bool) {
        if i >= max {
            return 0, false
        }
        res := i
        i++
        return res, true
    }
}

优缺点总结

✅ 优点

  1. 轻量,无需定义结构体即可持有状态;
  2. 天然支持函数回调、装饰器;
  3. 简化代码,避免大量全局变量;
  4. 状态私有,外部无法直接篡改捕获变量。

❌ 缺点 & 注意事项

  1. 循环中 goroutine / 闭包极易出现变量引用陷阱;
  2. 长期运行闭包持有大对象,容易造成内存泄漏(变量无法 GC);
  3. 复杂状态场景可读性不如 struct;
  4. 捕获大量变量时,GC 负担增加。

选型建议

  • 少量私有状态、回调、装饰器、临时处理器 → 优先闭包
  • 多个方法、大量状态、需要序列化、对外暴露方法 → 使用 struct

五、递归函数

函数内部调用自身,必须设置终止条件

// 阶乘
func fact(n int) int {
    if n == 1 {
        return 1
    }
    return n * fact(n-1)
}

示例:斐波那契数列(朴素递归,效率差,仅教学)

func Fib(n int) int {
    if n <= 2 {
        return 1
    }
    return Fib(n-1) + Fib(n-2)
}
// 优化方案:记忆化递归,避免重复计算
var memo = map[int]int{}
func FibMemo(n int) int {
    if n <= 2 {
        return 1
    }
    if v, ok := memo[n]; ok {
        return v
    }
    memo[n] = FibMemo(n-1) + FibMemo(n-2)
    return memo[n]
}

示例:二叉树遍历(面试高频)

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// 前序遍历
func PreOrder(root *TreeNode) {
    if root == nil { // 基线:空节点终止
        return
    }
    println(root.Val)
    PreOrder(root.Left)
    PreOrder(root.Right)
}

// 求二叉树最大深度
func MaxDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }
    l := MaxDepth(root.Left)
    r := MaxDepth(root.Right)
    if l > r {
        return l + 1
    }
    return r + 1
}

示例:目录递归遍历

import "os"

func WalkDir(path string) error {
    entries, err := os.ReadDir(path)
    if err != nil {
        return err
    }
    for _, e := range entries {
        fullPath := path + "/" + e.Name()
        if e.IsDir() {
            // 递归进入子目录
            if err := WalkDir(fullPath); err != nil {
                return err
            }
        } else {
            println("文件:", fullPath)
        }
    }
    return nil
}

示例:数组全排列(回溯递归)

func Permute(nums []int) [][]int {
    var res [][]int
    var backtrack func(path []int, used []bool)
    backtrack = func(path []int, used []bool) {
        if len(path) == len(nums) {
            tmp := make([]int, len(path))
            copy(tmp, path)
            res = append(res, tmp)
            return
        }
        for i := range nums {
            if used[i] {
                continue
            }
            used[i] = true
            path = append(path, nums[i])
            backtrack(path, used)
            path = path[:len(path)-1] // 回溯撤销
            used[i] = false
        }
    }
    backtrack([]int{}, make([]bool, len(nums)))
    return res
}

示例:二分查找递归版

func BinarySearch(arr []int, l, r, target int) int {
    if l > r {
        return -1 // 找不到
    }
    mid := (l + r) / 2
    if arr[mid] == target {
        return mid
    } else if arr[mid] > target {
        return BinarySearch(arr, l, mid-1, target)
    } else {
        return BinarySearch(arr, mid+1, r, target)
    }
}

示例:普通递归(自上而下)

func Recur(param T) Result {
    // 基线条件
    if stopCondition {
        return baseResult
    }
    // 拆分问题,递归调用
    subRes := Recur(transform(param))
    // 合并结果
    return combine(subRes, param)
}

示例:闭包递归(Go 最常用!内部递归,捕获外部变量)

适合回溯、遍历、需要共享结果切片 /map 场景

func Demo() {
    var dfs func(x int)
    dfs = func(x int) {
        if x > 10 {
            return
        }
        println(x)
        dfs(x + 1)
    }
    dfs(1)
}

示例:记忆化递归(解决重复计算)

套路:全局 / 传入 map 缓存结果

var cache = make(map[输入类型]返回类型)
func dfs(入参) 返回值 {
    if v, ok := cache[入参]; ok {
        return v
    }
    // 基线
    // 递归计算
    res := ...
    cache[入参] = res
    return res
}

Go 递归避坑重点

Go 不支持尾递归优化

下面这种尾递归写法不会自动转循环,深度太大依然栈溢出:

// 尾递归,Go无效优化
func factTail(n, acc int) int {
    if n == 0 {
        return acc
    }
    return factTail(n-1, acc*n)
}

栈溢出风险

默认 goroutine 栈不大,递归上万层直接 panic;

处理深度不确定的场景优先迭代。

递归中切片、指针共享问题

回溯递归一定要copy或者回溯撤销,防止结果被覆盖。

并发不要滥用递归

递归 + goroutine 极易瞬间创建大量协程。

快速选用场景对照表

场景推荐递归写法
树、文件夹遍历闭包 dfs 递归
排列组合、子集、回溯闭包回溯递归
存在大量重复子问题记忆化递归
简单数学计算(阶乘、求和)基础递归
深度未知、层级上万❌不要递归,改用循环

后续学习:子集、组合、爬楼梯、汉诺塔经典递归代码。

六、内置函数

len () 获取长度

package main

import "fmt"

func main() {
	s := "hello"
	arr := [3]int{1,2,3}
	sl := []int{10,20}
	m := map[string]int{"a":1}

	fmt.Println(len(s))     // 5 字节长度
	fmt.Println(len(arr))   // 3
	fmt.Println(len(sl))    // 2
	fmt.Println(len(m))     // 1
}

cap () 获取容量

只能用于map / 切片 / 通道

func main() {
	sl := make([]int, 2, 5)
	fmt.Println(len(sl), cap(sl)) // 2 5

	ch := make(chan int, 4)
	fmt.Println(cap(ch)) //4
}

make /new 内存分配

new (T) 返回 *T,分配零值内存,并返回其地址,也就是指向类型T 的指针

func main() {
	p := new(int)
	fmt.Println(*p) // 0
	*p = 100
	fmt.Println(*p)
}

make () 仅用于 slice、map、channel,初始化内部结构

func main() {
	sl := make([]int, 0, 10)
	m := make(map[string]bool)
	ch := make(chan int, 2)

	m["ok"] = true
	ch <- 1
	fmt.Println(sl, m, ch)
}

append 切片追加

func main() {
	a := []int{1,2}
	a = append(a, 3,4)
	a = append(a, []int{5,6}...) // 打散切片
	fmt.Println(a) // [1 2 3 4 5 6]
}

copy 复制

func main() {
	src := []int{10,20,30}
	dst := make([]int, 2)
	copy(dst, src) // 按较短长度复制
	fmt.Println(dst) // [10 20]
}

delete 删除map

delete(map, key)

func main() {
	m := map[string]int{"x":1, "y":2}
	delete(m, "x")
	fmt.Println(m)
}

close 关闭通道

close(chan)

func main() {
	ch := make(chan int, 2)
	ch <- 1
	close(ch)
	val, ok := <-ch
	fmt.Println(val, ok) //1 true
	val2, ok2 := <-ch
	fmt.Println(val2, ok2) //0 false
}

complex /real/imag 复数

func main() {
	c := complex(2, 3)   // 构造复数 2+3i
	fmt.Println(real(c)) // 实部:2
	fmt.Println(imag(c)) // 虚部:3
}
/*
complex(a, b):创建复数 \(a+bi\)
real(z):取出复数 z 的实部 a
imag(z):取出复数 z 的虚部 b
i 虚数单位
*/

panic & recover 错误捕获

func test() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获panic:", err)
		}
	}()
	panic("出错啦")
}

func main() {
	test()
	fmt.Println("程序继续运行")
}

标准库外内置打印,不推荐生产,仅调试使用,正式代码用 fmt.Print

func main() {
	print("内置print\n")
	println("内置println")
}

常见易错点小结

  1. make 不能用于数组、结构体;只能 slice/map/chan

  2. append 必须接收返回值,切片底层可能扩容

  3. recover 只能在 defer 函数内生效

  4. len(string) 返回字节数,不是字符数,中文要用 utf8.RuneCountInString

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "你好Go"

	// 1. len():获取字符串字节长度
	fmt.Println("len(s) =", len(s)) // 6,"你""好"各3字节 + Go两个英文字符 3+3+2=8
	// 拆解:你(3) + 好(3) + G(1) + o(1) = 8

	// 2. utf8.RuneCountInString():统计字符数量(中文、字母都算1个)
	fmt.Println("字符数量 =", utf8.RuneCountInString(s)) // 4

	// 3. 转 []rune 再 len(),等价 utf8.RuneCountInString
	runes := []rune(s)
	fmt.Println("len([]rune(s)) =", len(runes)) // 4

	// 4. 遍历演示差异
	fmt.Println("=== 按字节遍历 ===")
	for i := 0; i < len(s); i++ {
		fmt.Printf("%d: %x\n", i, s[i])
	}

	fmt.Println("=== 按rune遍历 ===")
	for idx, r := range []rune(s) {
		fmt.Printf("%d: %c\n", idx, r)
	}
}
/*
len(s) = 8
字符数量 = 4
len([]rune(s)) = 4
=== 按字节遍历 ===
0: e4
1: bd
2: a0
3: e5
4: a5
5: bd
6: 47
7: 6f
=== 按rune遍历 ===
0: 你
1: 好
2: G
3: o
*/