函数
- 函数是一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。
- 函数支持多值返回
- 支持闭包
- 函数支持可变参数
基本概念
函数定义
func funcName(param-list) (result-list) {
function-body
}
函数的特点
1、函数可以没有输入, 也可以没有返回值(默认返回0)
2、多个相邻的相同类型的参数可以使用简写模式:
func add(a, b int) int { //a int, b int 简写为 a,b int
return a + b
}
3、支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,
最后的return可以不带参数名直接返回
func add(a, b int) (sum int) {
sum = a + b
return //return sum的简写模式
// sum := a + b // 如果是 sum:=a + b, 则相当于新声明一个sum变量命名返回变量sum覆盖
// return sum //最后需要显示的调用return sum
}
4、不支持默认值参数
5、不支持函数重载
6、不支持函数嵌套,严格地说是不支持命名函数的嵌套定义,但支持嵌套匿名函数
func add(a, b int) (sum int) {
anonymouse := function(x, y int) int {
return x + y
}
return anonymouse(a, b)
}
多值返回
func swap(a, b int) (int, int) {
return b, a
}
实参到形参的传递
package main
import "fmt"
func chvalue(a int) int {
a = a + 1
return a
}
func chpointer(a *int) {
*a = *a + 1
return
}
func main() {
a := 10
chvalue(a) //实参传递给形参是值拷贝
fmt.Println(a)
chpointer(&a) // 实参传递给形参仍然是值拷贝,只不过复制的是a的地址值
fmt.Println(a)
}
不定参数
go 函数支持不定数目的形式参数, 不定参数声明使用param ...type的语法形式
函数的不定参数有如下几个特点:
1、所有的不定参数类型必须是相同的
2、不定参数必须是函数的最后一个参数
3、不定参数名在函数体内相当于切片,对切片的操作同同样适合对不定参数的操作:
例如:
func sum(arr ...int) (sum int) {
for _, v := range arr { //此时arr就相当于切片,可以使用range访问
sum += v
}
return
}
4、切片可以作为参数传递给不定参数,切片名后要加上"..."。
例如:
func sum(arr ...int) (sum int) {
sum += v
}
func main() {
slice := []int{1,2,3,4}
array := [...]int{1,2,3,4}
sum(slice...)
}
5、形参不定参数的函数和形参为切片的函数类型不相同.
例如:
func suma(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}
func sumb(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
// suma和sumb的类型并不一样
fmt.Printf("%T\n", suma) //func(...int)
fmt.Printf("%T\n", sumb) //func([]int)
函数签名和匿名函数
函数签名
函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名,参数名和{, 可以使用fmt.Printf的%T
格式化参数打印函数的类型。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
fmt.Printf("%T\n", add) // func(int, int) int
}
两个函数类型相同的条件是: 拥有相同的形参列表和返回值列表(列表元素的次序、个数和类型都相同), 形参名可以不同。
以下2个函数的参数类型完全一样.
func add(a, b int) int {return a + b}
func sub(x int, y int) (c int) {c = x -y; return c}
可以使用type定义函数类型, 函数类型变量可以作为函数的参数或返回值.
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
type Op func(int, int) int // 定义一个函数类型,输入的是两个int类型,返回值是一个int类型
func do(f Op, a, b int) int { // 定义一个函数,第一个参数是函数类型Op
return f(a, b) //函数类型变量可以直接用来进行函数调用
}
func main() {
a := do(add, 1, 2) // 函数名add可以当做相同函数类型形象,不需要强制类型转换
fmt.Println(a) // 3
s := do(sub, 1, 3)
fmt.Println(s) // -1
}
函数类型和map, slice, chan一样, 实际函数类型变量和函数名都可以当做指针变量,该指针执行函数代码的
开始位置. 通常说函数类型变量是一种引用类型,未初始化的函数类型的变量默认值是nil
Go中函数是"第一公民", 有名函数的函数名可以看做函数类型的常量,可以直接使用函数名调用函数,也可以直接
赋值给函数类型变量, 后续通过该变量来调用该函数.
package main
func sum(a, b int) int {
return a + b
}
func main() {
sum(3, 4) // 直接调用
f := sum //有名函数可以直接赋值给变量
f(1, 3)
}
匿名函数
package main
import "fmt"
// 匿名函数被直接复制函数变量
var sum = 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 int, b int) int {
return a - b
}
default:
return nil
}
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
sum(1, 2)
// 匿名函数作为实参
doinput(func(x int, y int) int {
return x + y
}, 1, 2)
opFunc := wrap("add")
re := opFunc(2, 3)
fmt.Printf("%d\n", re)
}
defer
defer关键字, 可以注册多个延迟调用
package main
func main() {
// 先进后出
defer func() {
println("firts")
}()
defer func() {
println("second")
}()
println("function body")
}
defer后面必须是函数或方法, 不能是语句。
defer函数的实参在注册时通过值拷贝传递进去.
主动调用os.Exit(int)退出进程时,defer将不再被执行(即使defer已经提前注册)
package main
import "os"
func main() {
defer func() {
println("defer") //这种情况 不执行
}()
println("func body")
os.Exit(1)
}
defer的好处是可以在一定程度上避免资源泄露, 特定是在有很多return语句, 有多个资源需要关闭的场景中,很
容易漏掉资源的关闭操作.
defer语句的位置不当, 有可能导致panic, 一般defer语句放在错误检查语句之后
defer也有明显的副作用; defer会推迟资源的释放,defer尽量不要放到循环语句里面, 将大函数内部的defer
语句单独拆分成一个小函数是一种很好的实践方式. 另外,defer相当于普通的函数调用需要间接的数据结构的支持。
相对于普通函数调用有一定的性能耗损。
defer中最好不要对有名返回值参数进行操作,否则会引发匪夷所思的结果
闭包
闭包是由函数及其相关引用环境组合而成的实体, 一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量)
(1) 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每个调用函数都会为局部变量分配内存。
(2) 用一个闭包函数多次, 如果该闭包修改了其引用的外部变量。则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。
package main
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa(1) // f引用的外部的闭包环境包括本次函数调用的形参a的值1
g := fa(1) //g引用的外部的闭包环境包括本次函数调用的形参a的值1
println(f(1)) //多次调用f引用的是同一个副本a
println(f(1))
// g中a的值仍然是1
println(g(1))
println(g(1))
}
f和g引用的是不同的a
如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
如果函数返回的闭包引用的是全局变量a, 则多次调用该函数返回的多个闭包引用的都是同一个a。同理,调用一个闭包
多次引用的也是同一个a。此时如果闭包中修改了a值的逻辑,则每次闭包调用都会影响全局变量a的值。
使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。