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...)
parame1
,param2
:函数参数名,遵循标识符命名规则。type1
,type2
:参数类型,可以是任何有效类型。
如果多个相邻参数类型相同,可采用简写:
func functionName(param1, param2 type12, param3 type3...)
type12
:参数param1
和param2
类型相同,只需要在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 语言中函数可以有零到多个返回值,有返回值函数必须包含终止语句,即 return
或 panic
语句。
单返回值
单返回值函数最常见,在函数签名中,返回类型紧随参数列表之后:
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...)
返回值类型 returnType1
和 returnType2
必须分别指定。多返回值常用于错误处理,让函数同时返回结果和错误信息:
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) × ... × 1
,0! = 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
语句中修改返回值。