跳至主要內容

函数式编程

LincZero大约 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处所需要释放的资源还可能不同