函数式编程
大约 6 分钟
函数式编程
原理:与其他语言相似,每个函数调用时会在栈中创建栈帧,并有自己的资源,结束时销毁
与其他语言不同:
- 不支持函数重载
- 基本类似和数组都是值传递,其他的数组多是引用传递
声明定义、使用
声明定义、使用
package main
import "fmt"
// 两数相加
func add(num1 int, num2 int) (int) { // 如果返回值类型就一个,那么()可以省略不写。如果无返回值,那么返回值类型可以不写
var sum int = 0
sum += num1
sum += num2
return sum
}
func main() {
sum1 := add(10, 20)
sum2 := add(20, 30)
sum3 := add(30, 50)
}
函数变量
函数也是一种数据类型,类似于 js 的 let a = function
、C 的普通函数地址指针。 注意:C++的类成员指针和Lambda函数,则是使用auto、泛型、std::function 等工具来定义函数指针
func test (num int) {
fmt.Println(num)
}
func main() {
a := test // 等价于 a func(int) := test
fmt.Print("a类型 %T, test类型 %T", a, test) // func(int) func(int)
a(10) // 等价于 test(10)
}
函数作为形参
func test (num int) {
fmt.Println(num)
}
func test2 (num1 int, testFunc func(int)) {
fmt.Println("--- test02")
testFunc(num1)
}
func main() {
a := test
a(12) // 输出:12
test2(23, test) // 也可写:test2(23, a),等价的
// 输出:--- test02\n 23
}
(详见语言区别) 定义顺序、与向前声明问题
与其他语言不同:
- Go
- 不可分离声明和定义
- 不考虑顺序,编译时会先解析所有包和函数,原理类似于全部默认向前声明
返回值
多返回值
扩展
// 若需返回多个
func fn(num1 int, num2 int) (int, int) {
return num1, num2
}
忽略某个返回值
// 可忽略某个返回值
sum1, _ := fn(1, 2)
返回值名
func add_and_sub(num1 int, num2 int)(sum int, sub int) {
sub := num1 - num2 // 注意:Go的1.20版本后,这里将 `:=` 要修改成 `=`,应该是默认已定义过sum和sub了
sum := sum1 + sum2
return // 自动按顺序对应好并返回,和 `return sum, sub` 没有区别
}
参数
可变数量参数
一般说可变参数特指可变数量
和python或typeScript的写法比较像
func test (args ...int) { // 这里的args本质是切片
for i:=0; i<len(args); i++ {
fmt.Println(args[i])
}
}
func main() {
test(1, 3, 5, 7, 9)
}
提供可变实参
除了传递不同数量的参数外,还可以传递一个切片的展开
slice3 := append(slice, slice2...) // ^ 这里三个点类似TypeScript的用法,必须写
可变类型参数 (重载或泛型)
Go不支持函数重载,但支持泛型
func myFunction(args ...interface{}) {
for _, arg := range args {
fmt.Println(arg)
}
}
func main() {
// 调用函数,传入不同数量和类型的参数
myFunction(1, 2, 3, "a", "b", "c")
}
地址传参
地址传参,不是类似 Java 那种引用传参
func test (num *int) {
*num = 30
}
func main() {
var num int = 10
test(&num)
fmt.Println(num)
}
特殊函数
main函数
略,main包里的main函数,作为可执行程序的入口函数
init函数
每个源文件都可以包含一个init函数,该函数会在main函数执行前被Go框架调用
与其他函数不同:其他语言没有这种函数,若其他函数想达到类似的效果:
- 这点类似全局变量定义,
g_object = fun1()
(Go语言也行,但init函数比全局变量更慢) 特别地,C++的全局静态对象在main函数前调用构造函数的原理,和这个是一样的 - 或类似于js的各种 加载钩子执行函数
执行顺序:全局变量 -> init函数 -> main函数
package main
import "fmt"
import "testUtils" // 一般init函数和包的导入搭配使用
var num init = test()
func test() int {
fmt.Println("test函数")
return 10
}
func init() {
fmt.Println("init函数")
}
func main() {
fmt.Println("main函数")
}
// 最终打印顺序:
// test.test函数
// test.init函数
// test函数
// init函数
// main函数
高级函数
匿名函数
类似于 js 的匿名函数,我很喜欢用这种用法,能做到类似空{}
以及goto的效果而又不用goto
使用场景1:如果某个函数只希望使用一次时使用。定义时直接调用
package main
import "fmt"
func main() {
result := func (num1 int, num2 int) int {
return num1 + num2
}(10, 20)
fmt.Println(result)
}
使用场景2:使用函数变量存储
sub := func (num1 int, num2 int) int {
return num1 + num2
}
// 似乎等价于下面的语句,但:可以在局部定义,可以传递 (?不太确定原来的地方方法是不是类似C语言那样,函数名本身也是指针),特殊的生命周期 (除非赋值给全局变量)
// 如果赋值给全局变量,那么应该是没什么区别的 (?也不太确定不用匿名函数能不能局部定义)
func (num1 int, num2 int) int {
return num1 + num2
}
闭包 (Closure)
更多概念推荐去看笔记 “语言区别” 里的 “函数式变成/闭包”,那里的很详细
- FAQ
- 什么是闭包:闭包就是一个函数和其相关的引用环境组成的一个整体
- 闭包的本质:本质依然是一个匿名函数,只是这个函数引入外界的变量/参数。匿名函数+引用的变量/参数 = 闭包
- 具名函数可以闭包吗:
- 特点
- 返回的是一个匿名函数,但该匿名函数…………………………
- 闭包中使用的变量/参数会一直保存在内存中,并一直使用。这意味着闭包不可滥用,容易内存泄露
例如:
func getSum fun (int) int {
var sum int = 0 // 这里的sum变量会一直保存在内存中
return func (num int) int {
sum = sum + num // 会发现,这里的sum后面
return sum
}
}
func main() {
f := getSum()
fmt.Println(f(1)) // 1 = 0 + 1
fmt.Println(f(2)) // 3 = 1 + 2
fmt.Println(f(3)) // 6 = 3 + 3
fmt.Println(f(3)) // 10= 6 + 4
}
资源释放 —— defer关键字
与其他语言不同:Go使用defer关键字用于处理函数的资源释放,以及异常处理等(关于defer的更多信息,详见Go语言的 “异常” 部分。结合 “异常” 部分看这个语法还好一些,否则从其他语言转过来是真的不适应)
直接作用:遇到defer会将后面的代码压入栈中,也会将相关值拷贝入栈
package main
import "fmt"
func main() {
fmt.Println("main get ", add(30, 60))
}
func add(num1 int, num2 int) int {
// Go中,程序遇到defer关键字,不会立即执行defer后的语句,而是将defer后的语句压入栈,先执行函数后的值然后出栈。需要注意栈的先进后出
defer fmt.Println("num1 = ", num1)
defer fmt.Println("num2 = ", num2)
num1 += 10
num2 += 10
var sum int = num1 + num2
fmt.Println("sum = ", sum)
return sum
}
// 打印顺序:
sum = 110
num2 = 30 // 先进先出,先出num2
num1 = 30 // 并且defer会将变量会值传递到新栈帧中,所以值不受后续改变的影响
main get 110
应用场景:
- 在函数执行完毕后,及时释放函数中创建的资源。
- QA:有GC语言为什么还要手动释放?例如打开文件函数并想函数结束时释放掉
- QA:在函数末尾释放资源不行吗?不用这个的话,提前返回或抛出异常就很麻烦。要重复写一堆资源释放语句,并且每个return处所需要释放的资源还可能不同