Go 语言函数

基本概念

函数(Function)是执行特定任务的代码块,可以接受输入参数并返回运行结果。一个程序由一到多个函数组成。

Go 语言中函数不支持嵌套(nested,函数内定义命名函数)、重载(overload,一个函数名用于不同函数实现)、命名实参(named arguments,调用函数时指定参数名字)和默认参数(default parameter,定义函数时为参数提供默认值)。但支持可变参数、多返回值和延迟语句等特性。

函数声明

声明函数使用 func 关键字:

func functionName(paramsList) returnType {
    // 函数体
}
  • functionName:函数名称。
  • parameterList:参数列表,代表函数从外部接受的输入数据,可选。
  • returnType:函数返回的数据类型,可选。
  • {}:大括号内是函数内容,函数遇到返回语句或执行到结尾时结束。

func 关键字所在函数定义行也叫函数签名(signature)。

函数调用

根据函数从属包类型,调用方式不同:

  • 内置函数:共有 15 个内置函数,函数名均为小写,可以在任意位置直接调用。例如 panic()
  • 标准函数:需要导入所属标准包后使用,函数名首字母大写。例如 fmt.Println()
  • 自定义函数:调用同个包内函数无需要导入,直接通过函数名调用 functionName(parameList)。调用外部包函数需要先导入包,调用时需要带上包名 pkgName.FunctionName(paramsList)

调用函数传参顺序必须与函数签名一致。接受函数返回的变量数也要与函数签名一致,但可以不赋值来忽略全部返回值。

函数参数

Go 语言中函数可以有零到多个参数,每个参数名后跟着其类型,参数之间用逗号 , 分隔:

func functionName(param1 type1, param2 type2...)
  • parame1param2:函数参数名,遵循标识符命名规则。
  • type1type2:参数类型,可以是任何有效类型。

如果多个相邻参数类型相同,可采用简写:

func functionName(param1, param2 type12, param3 type3...)
  • type12:参数 param1param2 类型相同,只需要在 param2 后声明类型。
  • type3:参数 param3 的类型。

函数没有参数时,括号不能省略:

func functionName()

函数参数名可以忽略,只保留参数类型,效果等同于将空白标识符作为参数名:

func functionName(param1 type1, type2...)

参数传递

为描述函数参数状态,参数可分为实参(Actual Parameter)和形参(Formal Parameter)。实参是实际参数,指传入函数的外部数据,形参是在函数签名中定义的形式参数。调用函数时,形参会在函数内部自动初始化,调用结束后销毁,作用域仅限于函数体内。

函数参数传递是指将实参副本赋值给形参的过程,严格来说属于值传递。但由于实参可能为引用类型,引用类型的副本(指针)依然指向原始数据,因此这类传参被称为引用传递。总之,参数传递方式由参数类型决定:

package main

import "fmt"

// 函数接受两个整型参数和一个字符串指针参数
func f(m, n int, s *string) {
	m += 7 + n            // 值传递,没副作用
	*s += "go"            // 引用传递,同时会修改外部实参
	fmt.Println(m, n, *s) // 输出:10 2 hello go
}

func main() {
	a, b, c := 1, 2, "hello " // 外部实参:a, b, c
	f(a, b, &c)               // 实参赋值给行参:m, n, s
	fmt.Println(a, b, c)      // c 被函数修改,输出:1 2 hello go
}

如果函数需要参数太多,可以整合到一个结构体中来传递。

可变参数

Go 语言中函数支持可变数量的参数,也叫不定参数(数量不定的参数),通过在参数类型前加 ... 来指定:

func functionName(params ...paramsType)
  • params:变参名。在函数内部使用时是个 []paramsType 类型切片。
  • ...paramsType:类型同样不限,所有变参必须同一类型。

在使用变参时,可以使用任何切片操作方法:

package main

import "fmt"

// 对任意多个传入整数求和
func sum(numbers ...int) (total int) {
	fmt.Printf("%T\n", numbers) // 类型为:[]int
	fmt.Println(len(numbers))   // 长度为:4

	for _, n := range numbers { // 遍历参数切片
		total += n
	}
	return total
}

func main() {
	result := sum(1, 2, 3, 4)     // 如果不传参数,则得到默认返回值 0
	fmt.Printf("求和结果:%v", result) // 返回运算结果:10
}

也可以直接传入切片作为可变参数,需要在切片后加上 ... 来展开:

package main

import "fmt"

func f(p ...int) {
	fmt.Println(p)
}

func main() {
	sl := []int{1, 2, 3}
	// 展开后,切片每个元素都作为独立参数传递
	f(sl...)
}

可变参数与常规参数组合使用时,需要把可变参数放在参数列表最后,因此一个函数签名中只能有一个可变参数:

package main

import "fmt"

func greet(msg string, names ...string) {
	for _, name := range names {
		fmt.Println(msg, name)
	}
}

func main() {
	greet("Hello", "Alice", "Bob", "Charlie")
}

函数返回值

Go 语言中函数可以有零到多个返回值,有返回值函数必须包含终止语句,即 returnpanic 语句。

单返回值

单返回值函数最常见,在函数签名中,返回类型紧随参数列表之后:

package main

import "fmt"

// 简单函数支持写在一行
func add(a, b int) int { return a + b }

func main() {
	fmt.Println(add(3, 4))
}

无返回值

函数不返回任何值时,在函数签名中不指定返回类型:

func functionName(paramsList)

此时在函数体中的 return 语句用来提前退出函数:

package main

func f(s string) {
	if s == "" {
		return // 满足条件则提前退出
	}
	// 正常流程代码
}

func main() {
	// 不能获取函数返回值,报错:f("test") (no value) used as value
	a := f("test")
}

多返回值

Go 语言函数支持多返回值,在函数签名中使用小括号 () 将多返回值括起来,返回值之间用逗号 , 分隔:

func functionName(paramsList) (returnType1, returnType2...)

返回值类型 returnType1returnType2 必须分别指定。多返回值常用于错误处理,让函数同时返回结果和错误信息:

package main

import (
	"errors"
	"fmt"
)

// 用多返回值处理除数为 0
func f(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("除数为零")
	}

	return a / b, nil
}

func main() {
	m, err := f(10, 0)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(m)
}

在调用有返回值函数时,可以使用空白标识符 _ 来忽略某个返回值,也可以隐式地忽略函数所有返回值,即不把返回值赋给变量:

package main

func f() (int, error) { return 1, nil }

func main() {
	// 使用空白标识符忽略错误返回值
	m, _ := f()

	// 直接忽略所有返回值,等价于 _, _ = f()
	f()

	// 不能隐式忽略部分值,报错赋值计数不匹配: 1 = 2
	n := f()
}

命名返回值

Go 语言中可以在函数签名部分给返回值命名,以增强函数签名的可读性:

func functionName(paramsList) (returnValue returnType)

此外,有命名的返回值和参数一样,会在函数调用时自动初始化为类型零值,配合「裸 return」语句自动返回:

package main

import "fmt"

func f(a, b float32) (c, d int) {
	fmt.Println(c, d) // 自动初始化为类型零值:0 0

	c, d = int(a), int(b)
	return // 不需要带上返回值
}

func main() {
	fmt.Println(f(1.1, 0.9)) // 输出:1 0
}

当然,在 return 语句中带上其他值或变量都可以,会自动赋予给命名返回变量:

package main

import "fmt"

// 隐式返回
func f() (c, d float32) { return }

// 显式返回命名返回值
func g() (c, d float32) { return c, d }

// 显式返回其他值,类型约束还在
func h() (c, d float32) { return 0.5e-05, 8.123e2 }

func main() {
	fmt.Println(h()) // 输出:5e-06 812.3
}

虽然命名返回值看起来很方便,但会增加代码复杂度,一般不使用。

函数应用

Go 语言中函数是「头等公民(first-class citizens)」,可以像操作和使用其他数据类型(如整型、结构体等)一样操作函数。具体来说:

  • 函数变量:函数可以赋值给变量,通过变量来调用和传递函数。
  • 函数类型:函数可以作为独立类型,用在需要指定类型的地方。
  • 传递函数:函数可以作为其他函数的参数或返回,以实现高阶函数(Higher-Order Functions)。
  • 储存函数:函数可以储存在数组、切片、映射等数据结构中。

这些函数特性也是函数式编程的特性。

函数变量

将已声明函数赋值给变量标识符,则变量的值是函数本身:

package main

import "fmt"

// 两个函数签名相同
func add(a, b int) int { return a + b }
func sub(c, d int) int { return c - d }

func main() {
	// 把函数赋值给变量,等价于 var x func(int, int) int = add
	x := add
	fmt.Printf("%T\n", x) // 输出:func(int, int) int
	fmt.Println(x(1, 2))  // 等于调用 add(1, 2),输出:3

	// 函数签名相同,所以可以赋值
	x = sub
	fmt.Println(x(1, 2)) // 输出:-1
	//x = cap              // 不可赋值,函数类型不匹配
}

函数变量声明后,不能将不同函数类型(参数或返回值类型不同)赋值给变量。

函数类型

Go 语言中可以自定义函数类型,函数类型需要指明函数参数和返回值类型:

type FunctionType func(paramType1, paramType2, ...) returnType
  • FunctionType:函数类型名称。
  • paramType1, paramType2:函数参数类型,不需要写上参数名。
  • returnType:返回值类型。

函数类型是高阶函数实现的基础:

package main

import "fmt"

// 声明函数类型
type Op func(int, int) int

// 函数属于 Op 函数类型
func add(a, b int) int { return a + b }

// 函数类型作为参数
func fp(op Op) {}

// 函数类型作为返回值
func fr() Op { return add }

func main() {
	// 使用函数类型声明变量
	var f Op              // 初始化零值 nil
	f = add               // 将函数赋值
	fmt.Printf("%T\n", f) // 输出:main.Op
}

函数组合

函数组合(Function Composition)指将一个函数的输出直接作为另一个函数的输入。只要被调用函数的返回值数量、类型和顺序与调用函数参数一致,就可以把这个函数调用当作其他函数的调用参数:

package main

import "fmt"

// 函数 f1 返回两个整数
func f1(a, b int) (int, int) { return a + b, a - b }

// 函数 f2 接受两个整型参数
func f2(x, y int) int { return x * y }

func main() {
	// 直接嵌套调用,不需要先对 f1 结果赋值
	fmt.Println(f2(f1(1, 2)))

	// 对等效果代码
	sum, diff := f1(1, 2)
	fmt.Println(f2(sum, diff))
}

递归函数

递归函数(Recursive Functions)是在函数体内直接或间接地调用自身的函数,抽象概念中包括两个部分:

  • 基本情形(Base Case):递归调用终止条件,满足条件时停止递归。缺少基本情形会造成无限递归。
  • 递归调用(Recursive Call):通过调用自身以解决部分问题,减小问题规模,直到达到基本情况。

递归函数常见于遍历树结构、排序算法和计算数学序列等。例如阶乘定义为:n! = n × (n-1) × (n-2) × ... × 10! = 1,使用递归函数实现非常简洁:

package main

import "fmt"

// factorial 函数使用方式计算 n 的阶乘
func factorial(n int) int {
	// 基本情形:0 的阶乘为 1
	if n == 0 {
		return 1
	}
	// 递归调用:n 的阶乘是 n 乘以 n-1 的阶乘
	return n * factorial(n-1)
}

func main() {
	fmt.Println("5! =", factorial(5)) // 等同于:5*4*3*2*1 输出 5! = 120
}

遍历二叉树时,递归调用不在函数返回中:

package main

import "fmt"

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

// preOrder 前序遍历二叉树
func preOrder(node *TreeNode) {
	// 基本情形
	if node == nil {
		return
	}
	fmt.Print(node.Value, " ")

	// 递归调用:分别遍历左右子树
	preOrder(node.Left)
	preOrder(node.Right)
}

func main() {
	// 构建一个简单的二叉树
	root := &TreeNode{1, &TreeNode{2, nil, &TreeNode{4, nil, nil}}, &TreeNode{3, nil, nil}}
	preOrder(root) // 输出:1 2 4 3
}

大多情况下可以使用迭代来代替递归,以减小递归深度大时的函数调用栈开销。例如使用迭代方法计算阶乘:

package main

import "fmt"

func factorial(n int) int {
	result := 1
	for i := 1; i <= n; i++ {
		result *= i
	}
	return result
}

func main() {
	fmt.Println("5! =", factorial(5)) // 输出:120
}

匿名函数

匿名函数(Anonymous Functions)没有函数名,用于实现闭包和一次性功能。和命名函数不同,匿名函数可以定义在任何地方。

可以将匿名函数可赋值给变量,通过变量名对函数进行调用和传递:

package main

import "fmt"

func main() {
	// 匿名函数赋值给变量,通过变量调用
	f := func() { fmt.Println("匿名函数") }
	f()
}

也可以定义同时调用匿名函数,只需在定义后用括号传入函数参数:

package main

import "fmt"

func main() {
	// 原地调用匿名函数
	fmt.Println(func(x, y int) int { return x + y }(1, 2))
}

闭包函数

闭包(Closure)是指在匿名函数内部封装外部变量。外部变量在闭包创建时被捕获,生命周期被延长至闭包存在期间。简单来说,通过闭包能使函数访问另一个函数作用域中的局部变量,常用于封装功能和数据:

package main

import "fmt"

// 函数 f 接受一个整型,返回一个函数
func f(a int) func(int) int {
	// 返回的匿名函数依赖于外部变量 a,所以形成闭包
	return func(b int) int {
		// a 被传透到闭包内部,闭包需要维持 a 的状态
		return a + b
	}
}

func main() {
	// 返回的闭包函数保留着调用 f 时传入的参数
	c := f(30)

	// 调用闭包,传入不同加数,被加数不变。结果输出:31 32
	fmt.Println(c(1), c(2))
}

由于闭包没有经过参数传递而是直接引用外部变量,外部变量对闭包来说像个全局变量,因此在闭包内可以修改外部变量值:

package main

import "fmt"

func f(a int) func() int {
	// 在闭包内修改外部变量 a 的值
	return func() int {
		a++
		return a
	}
}

func main() {
	c := f(1)
	// 闭包没引用新参数,但每调用一次,闭包内保存的传参值被加一
	fmt.Println(c()) // 输出:2
	fmt.Println(c()) // 输出:3

	// 新建闭包 d,每个闭包内都有各自独立的 a 变量
	d := f(10)
	fmt.Println(d()) // 输出:11
}

此外需要注意,闭包中捕获的是外部变量引用,而不是变量的值,在调用闭包时才对变量取值:

package main

import (
	"fmt"
	"time"
)

func f(v *int) {
	// goroutine 中运行的函数是个闭包
	go func() {
		// 循环 5 次,每秒打印一次外部变量 v 的值
		for range 5 {
			time.Sleep(1 * time.Second)
			// 在第 2 次循环后,外部修改了 v 的值,输出跟着改变
			fmt.Println(*v)
		}
	}()
}

func main() {
	v := 10
	// 传递指针给给异步函数,好在主函数修改
	f(&v)

	// 等待 3 秒后修改变量 v 的值
	time.Sleep(3 * time.Second)
	v = 20
	time.Sleep(3 * time.Second)
}

特殊函数

特殊函数是指在程序或包中有特定用途的函数。

主函数

在 Go 语言中,main 函数是程序唯一运行入口,程序会在主函数执行完毕后结束:

func main() {
    // 函数体
}

主函数有下面特性:

  • 必须位于 main 包中。
  • 没有参数和返回值。
  • 自动运行,不能手动调用。
  • 不能导入导出。

虽然 main 函数没有返回值,但可以用 os.Exit 来结束程序并给操作系统返回一个自定义状态码:

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("Hello, World!")

	// 自定义退出码
	if true {
		fmt.Println("自定义退出码为 1")
		os.Exit(1) // 非零状态码表示错误
	}

	// 正常退出
	fmt.Println("正常退出码为 0")
	os.Exit(0) // 用 0 表示成功执行
}

在使用命令运行时可以看到自定义退出码:

D:\Software\Programming\Go\new>go run main.go
Hello, World!
自定义退出码为 1
exit status 1 

初始化函数

init 函数用于包级别初始化设置:

func init() {
    // 初始化代码
}

初始化函数特性:

  • 自动执行,不能手动调用。
  • 没有参数和返回值。
  • 不能导入导出。
  • 在主函数之前执行。
  • 在包首次导入时执行,仅执行一次。如果导入多个包,包初始化函数执行顺序和包导入顺序一致。
  • 一个源文件中能有多个初始化函数,执行顺序和声明顺序一致。

全局变量和常量声明会先于初始化函数执行,初始化函数也能调用其他自定义函数:

package main

import (
	"fmt"
	"os"
)

// 先于主函数获取到 GOROOT 值
func init() { GOROOT = os.Getenv("GOROOT") }
// 初始化函数中调用其他函数
func init() { f("初始化函数 2") }

// 虽然定义顺序在后面,但初始化函数中能调用
var GOROOT string
func f(s string) { fmt.Println(s) }

func main() {
	fmt.Println(GOROOT)
}

延迟语句

Go 语言函数拥有独特的延迟语句,常用于函数执行完毕后及时地释放资源,例如关闭连接、释放锁和关闭文件等。

语句声明

延迟语句在函数内使用关键字 defer 声明:

defer functionName(paramsList)

defer 后必须是个函数调用,但会等到包含 defer 的函数执行完毕后才真正执行:

package main

import "fmt"

func main() {
	func() {
		fmt.Println("开始")
		defer fmt.Println("结束") // 匿名函数中最后打印
		fmt.Println("处理中")
		return
	}()

	func() {
		fmt.Println("开始")
		defer fmt.Println("结束") // 发生异常时也会运行
		panic("发生异常")
	}()
}

使用 defer 语句清理资源时,尽可能紧接打开资源后立即声明:

package main

import (
	"log"
	"os"
)

func main() {
	// 正常打开文件逻辑
	file, err := os.Open("app.log")
	if err != nil {
		log.Fatal(err)
	}
	// 必须在文件正确打开后再声明
	defer file.Close()

	// 执行文件读取等操作
	// ...
}

多个声明

函数中可以定义多个 defer 语句,它们会被压入专门栈中,按照后进先出顺序(LIFO)执行:

package main

import "fmt"

func main() {
	defer fmt.Println("最先声明,最后执行")
	defer fmt.Println("最后声明,最先执行")
	fmt.Println("正常代码先行")
}

立即求值

与闭包中捕获变量不同,defer 语句可以经过函数传参,捕获定义时外部变量的值。之后外部函数对变量修改,不会影响 defer 语句中保存的值:

package main

import "fmt"

func main() {
	x := 10

	// defer 中函数经过传参,捕获 x 的值
	defer func(x int) {
		fmt.Println(x) // 后执行,输出:10
	}(x)

	// defer 中闭包直接引用 x,捕获变量而非值
	defer func() {
		fmt.Println(x) // 先执行,输出:20
	}()

	x = 20
}

特殊情况

defer 语句中修改函数返回变量值需要谨慎,可能有意外结果:

package main

import "fmt"

// 返回不受 defer 影响,返回 0
func f0() int {
	var i int
	defer func() { i++ }()
	return i
}

// 返回被 defer 修改后的值 1
func f1() (i int) {
	defer func() { i++ }()
	return
}

// 返回引用类型受 defer 影响,返回 [2]
func f2() []int {
	var i = []int{1}
	defer func() { i[0]++ }()
	return i
}

// 指明返回变量 a,但实际返回的还是 i。经过 defer 修改后返回 3
func f3() (i int) {
	i = 100 // 在 return 时被重新赋值
	a := 2
	defer func() { i++ }()
	return a
}

// i 在返回前赋值为 3 但不返回,经过 defer 修改,返回 4
func f4() (i int) {
	defer func() { i++ }()
	return 3
}

func main() {
	fmt.Println(f0(), f1(), f2(), f3(), f4()) // 输出:0 1 [2] 3 4
}

实际上正常 return 语句和裸 return 语句在逻辑上是一致的:

  • 正常返回(匿名返回值):函数内部会初始化一个隐藏局部变量储存返回值,在运行到 return 时,这个隐藏变量被赋予 i 的字面量值。defer 语句中修改 i 的值不会影响到隐藏返回变量。当然,如果 i 的类型为引用型(例如切片),那么赋值给隐藏返回变量时,是引用传递,defer 语句中的修改依然会体现到返回值上。
  • 具名返回(命名返回值):要把函数返回动作分为三步。先给具名返回变量 i 赋值,如果 return 后带有值(或变量)则赋给 i;然后执行 defer 语句,里面可能修改 i 的值;最后将 i 的最终值返回。

如果不想考虑那么多,那么记住别在 defer 语句中修改返回值。