目 录CONTENT

文章目录
go

【Go】函数

hxuanyu
2026-02-02 / 0 评论 / 0 点赞 / 14 阅读 / 0 字

函数是基于特定输入执行特定任务、并可返回执行结果的代码块(Go 中的方法本质上也是函数,只是额外绑定了接收者)。

Go 函数基础

在编程语言中,函数用于将大问题拆分为若干具有特定功能或职责的小任务,从而提升代码复用性与可维护性。

函数声明

函数的声明方式如下:

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

例如:

func min(a int, b int) (result int, err error) {
	if a < b {
		result = a
	} else {
		result = b
	}
	err = nil
	return
}
  • func 为关键字,表示函数声明开始。
  • min 为函数名。在同一个包中函数名必须唯一,并遵守 Go 的导出规则:以大写字母开头的函数可被其他包导入使用;以小写字母开头的函数仅包内可见。
  • 函数名后的括号内是参数列表。参数的声明方式与变量一致。Go 支持变长参数:在参数类型前加 ... 表示可接收数量不定的实参。
  • 参数列表之后是返回值列表。多数情况下只需按顺序写返回值类型;示例中为返回值命名,这称为具名返回值。
  • 大括号内是函数体,包含具体实现代码。函数声明也可以没有函数体,用于声明在 Go 之外实现的函数(常见于汇编实现,通过链接器完成绑定)。

函数声明在语义上可视为将一个函数字面值赋给同名变量,例如:

var min = func(a int, b int) (result int, err error) {
	if a < b {
		result = a
	} else {
		result = b
	}
	err = nil
	return
}

这种写法符合 Go 语法规则,因此可以理解为:声明了一个函数类型的变量。func 关键字、参数列表与返回值列表共同构成函数类型;参数与返回值的组合也称为函数签名(function signature),在比较函数类型是否一致时起决定性作用。

在描述函数签名时,通常会省略参数名与具名返回值的名字;因此即使参数名或具名返回值名不同,只要参数与返回值的类型序列一致,它们仍属于同一函数类型。

每个函数声明定义的函数,是对应函数类型的一个实例。

使用变量形式时,func(){} 被称为函数字面值,也常被称为匿名函数。

函数参数

在函数声明阶段,参数列表中的参数称为​形式参数(形参) ​;函数调用时传入的参数称为​实际参数(实参) 。调用时实参与形参按位置绑定,编译器会检查实参的类型与数量是否匹配,否则会产生编译错误。

Go 中函数参数的传递形式是​值传递​:实参会被复制后传入形参。对于整型、数组、结构体等类型,复制的是其完整数据,开销与数据大小成正比;对于字符串、切片、map、chan、func 等类型,复制的是其内部“描述符/引用”(例如切片头、字符串头、map 头等),因此复制开销通常是固定的。这种仅复制描述符而非底层数据的行为常被称为​浅拷贝。需要注意:即便是“引用类型”,它们仍然是值传递,只是值本身包含了对底层数据的引用。

在参数传递中有两种常见的“转换/包装”行为:

  • 形参为接口类型:实参会被赋值到接口值中(接口值由动态类型与动态值组成)。
  • 形参为变长参数:调用点会把零个或多个实参打包为一个对应类型的切片传入(也可用 slice... 将已有切片展开为变长实参)。

示例:

package main

import "fmt"

// 演示 Go 中的函数参数传递方式
func main() {
	commonPass(1, 2)
	interfacePass("%d\n", 1)
	variadic(1, 2, 3, 4)
}

func commonPass(x, y int) int {
	fmt.Println("x + y =", x+y)
	return x + y
}

func interfacePass(format string, i interface{}) {
	fmt.Printf(format, i)
}

func variadic(args ...int) {
	fmt.Printf("%T\n", args)
	fmt.Printf("args len: %d\n", len(args))
	for _, v := range args {
		fmt.Println(v)
	}
}

输出:

x + y = 3
1
[]int
args len: 4
1
2
3
4

函数返回值

Go 支持多返回值,错误处理也常依赖多返回值实现。返回值常见三种形式:

func foo()                 // 无返回值
func foo() error           // 单返回值
func foo() (int, string, error) // 多返回值
  • 无返回值:省略返回值列表
  • 单返回值:返回值列表可省略括号
  • 两个及以上返回值:返回值列表必须用括号包围

示例:

func returnOneValue() int {
	result := 1
	return result
}

func returnTwoValue() (int, int) {
	result1 := 1
	result2 := 2
	return result1, result2
}

func returnThreeValue() (int, int, int) {
	result1 := 1
	result2 := 2
	result3 := 3
	return result1, result2, result3
}

带名字的返回值称为具名返回值。具名返回值会在函数开始时被声明并初始化为零值,作用域覆盖整个函数体,因此在函数体内不需要(也不应)再次用 := 重新声明同名变量,否则会导致编译错误或变量遮蔽问题。若具名返回值能显著提升可读性(尤其是多返回值场景),则可以使用。

具名返回值的常见场景:

  • 多返回值:将返回值含义直接体现在签名中,可在末尾直接 return 返回当前变量值。
  • 需要在 defer 中修改外层函数返回值时:具名返回值更直观。

使用惯例:

  • 通常将 error 作为最后一个返回值
  • 返回值数量不宜过多:通常 1~2 个,尽量不超过 3 个
func returnValueWithName() (v1 int, v2 int, v3 int) {
	v1 = 1
	v2 = 2
	v3 = 3
	return
}

一等公民

如果一门语言允许某种元素像普通值一样被创建、赋值、作为参数传递、在函数中生成并作为返回值返回,那么该元素就是这门语言的“一等公民”。

特征 1:函数可以存储在变量中

package main

import "fmt"

var f1 = func(x int) int { return x + 1 }

func main() {
	fmt.Printf("%T\n", f1) // func(int) int
	fmt.Println(f1(5))     // 6
}

函数可以保存在变量中,并具有明确的函数类型。

特征 2:支持在函数内创建并通过返回值返回

package main

import "fmt"

func main() {
	removeHandler := setupListener()
	defer removeHandler()
}

func setupListener() func() {
	fmt.Println("setup listener")
	removeListener := func() {
		fmt.Println("remove listener")
	}
	return removeListener
}

返回的函数可以访问 setupListener​ 中的局部变量,这类捕获外部变量的函数字面值称为​闭包(closure) 。只要闭包仍可达,被捕获的变量就可能继续存活。

特征 3:函数可以作为参数传入函数

package main

import "fmt"

func funcAsParam(a int, b int, f func(int, int) int) int {
	return f(a, b)
}

func main() {
	fmt.Println(funcAsParam(1, 2, func(a int, b int) int { return a + b }))
}

特征 4:函数拥有自己的类型

每个函数声明定义的是对应函数类型的一个实例,也可以基于函数类型定义新的类型:

package main

import "fmt"

type MathOperation func(int, int) int

func applyOperation(x int, y int, op MathOperation) int {
	return op(x, y)
}

func main() {
	fmt.Println(applyOperation(1, 2, func(a, b int) int { return a * b }))
}

defer 简化函数实现

在 Go 中访问 IO、网络连接、锁、文件等资源时,使用完毕应及时释放。若同时使用多个资源,手动在多分支路径上关闭会增加复杂度并降低可读性,Go 提供 defer 机制用于统一收尾。

defer 机制

  • defer 是一种延迟调用机制。
  • 只能在函数或方法内部使用。
  • defer 后必须是一次函数/方法调用(该调用的返回值若有,会在语义上被丢弃)。
  • defer​ 注册的调用会绑定到当前 goroutine,并在当前函数返回前按**后进先出(LIFO)**顺序执行。
  • 无论函数是正常返回还是中途 return​,已注册的 defer​ 都会执行;若发生 panic​,在展开栈的过程中也会执行已注册的 defer(除非进程直接终止等极端情况)。

使用defer的注意事项

package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		defer deferTest(i)
	}
}

func deferTest(order int) {
	fmt.Printf("Defer test %d completed\n", order)
}

输出:

Defer test 2 completed
Defer test 1 completed
Defer test 0 completed

使用 defer 的注意事项

明确哪些调用可以直接 defer

  • 自定义函数/方法:可以直接 defer(其返回值会被丢弃)。
  • 内置函数中,可直接作为 defer​ 调用的常见有:close​、copy​、delete​、panic​、print​、println​、recover​、clear(Go 1.21+)。
  • 不能直接 defer​ 的内置函数:append​、cap​、len​、make​、new​、real​、imag​、complex​、min​、max 等(它们不是“可调用的函数值”,而是编译器内建指令/特殊形式)。
  • 对于不能直接 defer 的内置能力,通常通过包裹一层匿名函数实现延迟执行:
defer func() {
	_ = append(s, 1) // 示例:仅演示语法;实际要使用返回值
}()

注意表达式的求值时机

defer​ 语句在注册时就会对被调用函数值以及实参进行求值并保存;真正的调用发生在函数返回前。

package main

import "fmt"

func main() {
	for i := 0; i < 3; i++ {
		defer func(order int) {
			fmt.Printf("Defer %d executed\n", order)
		}(i)
	}
	fmt.Println("Main function completed")
}

输出:

Main function completed
Defer 2 executed
Defer 1 executed
Defer 0 executed

以上例子中,匿名函数在注册 defer​ 时就把当次循环的 i​ 作为参数值保存下来;由于 defer 采用栈结构管理,因此按后进先出顺序执行。

性能损耗

早期版本的 Go 中 defer​ 有较明显开销;从 Go 1.14 起引入了更激进的优化(如 open-coded defer 等),在很多常见场景下开销已显著降低。但在极端高频、极致性能敏感的路径中,仍应结合基准测试决定是否使用 defer

0
博主关闭了所有页面的评论