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
*/
关键点:
- 闭包会持有捕获变量的引用,不是拷贝值;
- 每次调用外层函数,生成一组独立变量环境;
- 外层函数执行完毕,局部变量不会被 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
}
}
优缺点总结
✅ 优点
- 轻量,无需定义结构体即可持有状态;
- 天然支持函数回调、装饰器;
- 简化代码,避免大量全局变量;
- 状态私有,外部无法直接篡改捕获变量。
❌ 缺点 & 注意事项
- 循环中 goroutine / 闭包极易出现变量引用陷阱;
- 长期运行闭包持有大对象,容易造成内存泄漏(变量无法 GC);
- 复杂状态场景可读性不如 struct;
- 捕获大量变量时,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("程序继续运行")
}
print /println 内置打印
标准库外内置打印,不推荐生产,仅调试使用,正式代码用 fmt.Print
func main() {
print("内置print\n")
println("内置println")
}
常见易错点小结
make不能用于数组、结构体;只能 slice/map/chanappend必须接收返回值,切片底层可能扩容recover只能在 defer 函数内生效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
*/