Go不是一门纯函数式的编程语言,但是函数在Go中是“第一公民”,表现在:
- 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。
- 函数支持多值返回
- 支持闭包
- 函数支持可变参数。
Go是通过编译本地代码且基于“堆栈”式执行的。
1. 基本概念
1.1 函数定义
函数是Go程序源代码的基本构造单位,一个函数的定义包括如下几个部分:
- 函数声明关键字func
- 函数名
- 参数列表
- 返回列表和函数体
注意:函数名的首字母大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包才可以访问。函数的此参数和返回值需要用“()”包裹,如果只有一个返回值,而且使用的是非命名的函数,则返回参数的“()”可以省略。函数体使用“{}”包裹,并且“{"必须位于函数返回值同行的行尾。
func funcName(param-list) (result-list){
function-body
}
函数的特性
(1)函数可以没有输入参数,也可以没有返回值(默认返回0)
(2)多个相邻的相同类型的参数可以使用简写模式
(3)支持有名的返回值,参数名就相当于函数体内最外层局部变量,命名返回值变量会被初始化为类型零值,最后return可以不带参数名直接返回。
import "fmt"
func main() {
a := 10
b := 20
fmt.Println(add(a, b))
}
func add(a, b int) (sum int) {
sum = a + b
return
}
(4)不支持默认值参数
(5)不支持函数重载
(6)不支持函数嵌套,严格地说是不支持命名函数的嵌套定义,但支持嵌套匿名函数。
import "fmt"
func main() {
a := 10
b := 20
fmt.Println(add(a, b))
}
func add(a, b int) (sum int) {
anonymous := func(x, y int) int {
return x + y
}
return anonymous(a, b)
}
1.2 多值返回
Go函数支持多值返回,定义多值返回的返回参数列表时要使用“()”包裹,支持命名参数的返回。
func swap(a,b int) (int ,int){
return b, a
}
习惯用法:如果多值返回值有错误类型,则一般将错误类型作为最后一个返回值。
1.3 实参到形参的传递
Go函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生了变化,那是因为参数传递的是指针值的拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址,本质上参数传递仍然是值拷贝。
import "fmt"
func main() {
a := 10
chvalue(a)
fmt.Println(a)
chpointer(&a)
fmt.Println(a)
}
func chvalue(a int) int {
a = a + 1
return a
}
func chpointer(a *int) {
*a = *a + 1
return
}
1.4 不定参数
Go函数支持不定数目的形式参数,不定参数声明使用param …type的语法格式。
函数的不定参数有如下几个特定:
(1)所有的不定参数类型必须是相同的;
(2)不定参数必须是函数的最后一个参数;
(3)不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作;
func sum2(arr ...int) (sum int) {
for _, v := range arr {
sum += v
}
return
}
(4)切片可以作为参数传递给不定参数,但切片名后要加上“…”;
slice := []int{1, 2, 3, 4}
//array := [...]int{1,2,3,4} // 数组不可以作为实参传递给不定参数的函数
fmt.Println(sum2(slice...))
(5)形参为不定参数的函数和形参为切片的函数类型不相同;
func sum2(arr ...int) (sum int) {
for _, v := range arr {
sum += v
}
return
}
func sum3(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
fmt.Printf("%T\n", sum2) //func(...int) int
fmt.Printf("%T\n", sum3) //func([]int) int
2. 函数签名和匿名函数
2.1 函数签名
- 函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名、参数名和{,可以使用fmt.Printf的“%T”格式化参数打印函数的类型。
- 两个函数类型相同的条件是:拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同),形参名可以不同。
- 可以使用type定义函数类型,函数类型变量可以作为函数的参数或返回值。
package main
import "fmt"
func merge(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
// 定义一个函数类型,输入的是两个int类型,返回值是一个int类型
type Op func(int, int) int
// 定义一个函数,第一个参数是函数类型Op
func do(f Op, a, b int) int {
return f(a, b) // 函数类型变量可以直接用来进行函数调用
}
func main() {
a := do(merge, 1, 2)
fmt.Println(a)
s := do(sub, 1, 2)
fmt.Println(s)
}
- 函数类型和map、slice、chan一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。
- 通常说函数类型变量是一种引用类型,未初始化的函数类型的变量的默认值是nil。
- Go中函数是“第一公民”(first class)。有名函数的函数名可以看作函数类型的常量,可以直接使用函数名调用函数,也可以直接赋值给函数类型变量,后续通过该变量来调用该函数。
func combine(a, b int) int {
return a + b
}
combine(3, 4)
f := combine
fmt.Println(f(1, 2))
2.2 匿名函数
Go语言提供两种函数:有名函数和匿名函数。匿名函数可以看作函数字面量,所有直接使用函数类型变量的地方都可以由匿名函数代替。匿名函数可以直接赋值给函数变量,可以当作实参、也可以作为返回值,还可以直接被调用。
package main
import "fmt"
// 匿名函数黑直接赋值函数变量
var sumsum = func(a, b int) int {
return a + b
}
func doinput(f func(int, int) int, a, b int) int {
return f(a, b)
}
//匿名函数作为返回值
func wrap(op string) func(int, int) int {
switch op {
case "add":
return func(a, b int) int {
return a + b
}
case "sub":
return func(a, b int) int {
return a - b
}
default:
return nil
}
}
func main() {
// 匿名函数直接被调用
defer func() {
if err := recover(); err != nil {
println(err)
}
}()
sumsum(1, 2)
// 匿名函数作为实参
doinput(func(x, y int) int {
return x + y
}, 1, 2)
opFunc := wrap("add")
re := opFunc(2, 3)
fmt.Printf("%d\n", re)
}
3. defer
- Go函数里提供了defer关键字,可以注册多个延迟调用,这些调用以先进后出(FILO)的顺序在函数返回前被执行。
package main
func main() {
//后进先出
defer func() {
println("first")
}()
defer func() {
println("second")
}()
println("functionBody")
}
- defer常用于保证一些资源最终一定能够得到回收和释放。
- 主动调用os.Exit(int)退出进程时,defer将不再被执行(即使defer已经提前注册)
- defer后面必须是函数或方法的调用,不能是语句。
- defer函数的实参在注册时通过值拷贝传递进去。
func f() int {
a := 0
defer func(i int) {
println("defer i=", i)
}(a)
a++
return a
}
- defer语句必须先注册后才能执行,如果defer位于return之后,则defer因为没有注册,不会执行。
- defer的好处是可以在一定程度上避免资源泄露,特别是在有很多return语句,有多个资源需要关闭的场景中,很容易漏掉资源的关闭操作。
- 在打开资源无报错后直接调用defer关闭资源。
- defer语句的位置不当,有可能导致panic,一般defer语句放在错误检查语句之后
- defer也有明显的副作用: defer会推出资源的释放,defer尽量不要放到循环语句里面;
- defer相对于普通的函数调用需要间接的数据结构的支持,相对于普通函数调用有一定的性能损耗
- defer中最好不要对有名返回值参数进行操作,否则可能会引起莫名其妙的结果。
至此,我们已经基本了解了Go语言中的函数及其用法,下一节我们将继续了解Go语言中的闭包。