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
语句中修改返回值。