📅 : 2024年4月30日

📦 使用版本为 1.21.5

函数

1️⃣ GO语言函数介绍

⭐️ 在 go语言中,函数是基本代码块

⭐️ Go是一门编译型语言,函数的位置没有像 C语言那样卡那么死

⭐️ Goretrun语句可以返回多个值,也可以用来结束一个 for循环或者一个协程

⭐️ Go语言的函数类型

  • 普通带有名字的函数
  • 匿名函数和 lambda函数(JavaPython也有)
  • 方法(Methods)

⭐️ 函数签名就是函数的参数,语句它们的类型

函数的签名(Function Signature)是函数在编程中的一种标识方式,它包含了函数的名称、参数类型、参数个数以及参数顺序。对于C++这样的编程语言,函数的签名还包含了函数所在的类和命名空间的信息。函数签名的主要作用是唯一标识一个函数,以便在编译和链接时进行区分。

值得注意的是,函数的返回值并不属于函数签名的一部分。也就是说,如果两个函数只有返回值不同,那么编译器是无法通过函数签名来区分它们的,这会导致语法错误。

此外,C++编译器在将源代码编译成目标文件时,会对函数和变量的名字进行修饰,形成符号名,这是为了确保在编译后的目标文件中,每个函数和变量都有唯一的标识。这种修饰后的名称就是函数的“修饰后名称”(Decorated Name),它也是函数签名的一部分。

函数签名在C++编程中有许多重要的应用,特别是在函数重载和模板实例化中。在函数重载时,编译器使用函数签名来判断应该调用哪个函数;在模板实例化时,函数签名可以帮助确定应该实例化哪个模板。此外,名称修饰也有助于链接器正确地区分和链接不同的函数实现。

总的来说,函数的签名是函数在编程中的一个重要概念,它对于理解函数的性质、实现函数的重载和模板实例化、以及确保编译和链接的正确性都具有重要的意义。

⭐️ 函数的创建

这是在同一个包下

func main() {
    a := 1
	echo(a); //同一包内下函数的调用
}

func echo(a int){
	fmt.Println(a);
}

不同包,在不同包下函数名首字母就必须写为大写,如果小写就表示不公开

package main

import (
	"var/Test"
)

func main() {
	a := 1
	Test.Echo(a) //Echo在不同包下
}

括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。

⭐️ 函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序
与调用函数所需求的实参是一致的

假设 f1 需要 3 个参数 f1(a, b, c int) ,同时 f2 返回 3 个参数 f2(a, b int) (int, int, int) ,就可以这样调用 f1: f1(f2(a, b))

package main

import "fmt"

func main() {
	f1(f2()) //f1直接调用f2作为参数
}

func f1(a, b, c int) { //f1作用输出传入的参数
	fmt.Println(a, b, c)
}

func f2() (int, int, int) { //返回3个参数
	return 1, 2, 3
}

⭐️ 如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体,这种情况下在接口中用的比较多,(和Java类似)

func f2() (int, int, int) 

还有一种情况是在声明的时候,函数也可以以申明的方式被使用,作为一个函数类型,这时候也不需要函数体

type f2 func() in

⭐️ 在 Go语言中函数是一个一等值

在编程语言中,"一等公民"(First-class citizen)是一个术语,用来描述语言中某些实体可以被无限制地使用,就像基本类型(如整数、布尔值)一样。在Go语言中,当谈论某个特性或构造是一等公民时,这意味着它们可以:

  1. 被赋值给变量。
  2. 作为参数传递给函数。
  3. 从函数中返回。
  4. 存储在数据结构中(如数组、切片、映射等)。

2️⃣ 函数的参数和返回值

⭐️ Go语言支持多返回值,可以使用 return或者是 panic

func f2() (int, int, int) { //返回3个参数
	return 1, 2, 3
}

⭐️ 在函数块里面,return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return 语句

在下面这个代码里面,只在 for循环内部有 return代码,但是它在for循环外部并没有 return代码,会导致编译不通过

func (st *Stack) Pop() int {
    v := 0
    for ix := len(st) - 1; ix >= 0; ix-- {
        if v = st[ix]; v != 0 {
            st[ix] = 0
            return v
        }
    }
}  

⭐️ 函数定义是,形参可以没有名字

func f2(int, int, int) {

}

⭐️ 没有参数的函数通常被称为 niladic函数

🌟 按值传递和按引用传递

⭐️ 在 go中默认使用按值传递,也就是传递参数副本

func main() {
	a := 2
	b := 3
	f1(a,b)
	fmt.Println(a,b) //输出发现并没有修改还是2 3
}

func f1(a, b int) { //交换a和b的值
	tmp := a
	a = b
	b = tmp
}

⭐️ 使用引用传递,就是传递一个指向参数的一个指针,通过这个指针可以修改指向地址参数的值(和C语言有点类似,也是需要传递指针,也就是地址值),和C语言一样也是使用 &符号,但是在函数中,这个指针的值会被复制一份,但是它指向的值还是指向源参数的值,使用指针传递在任何时候都要比副本传递消耗少

如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。

指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递

func main() {
	a := 2
	b := 3
	f1(&a, &b) //传入地址值
	fmt.Println(a, b)
}

func f1(a, b *int) { //使用指针类型
	tmp := *a //使用指针来修改
	*a = *b
	*b = tmp
}

⭐️ 在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)

⭐️ 如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

//这里先演示切片,等我学了函数体在回头来写
func main() {
	f1()
	fmt.Println()
}

func f1() []int {
	a := 1
	b := 2
	c := 3
	return []int{a, b, c}
}

🌟 给返回值命名

⭐️ 不给函数值命名,可以直接使用类型符号,如果有多个需要使用 ()括起来

func main() {
	a := 2
	b := 3
	f1(a, b)
	fmt.Println(a, b)
}

func f1(a, b int) (int, int) {
	tmp := &a
	tmp2 := &b
	return *tmp, *tmp2 //返回会两个int类型的值,如果直接tmp,那么返回的就是一个*int类型的值
}

⭐️ 给函数返回值命名,如果有多个返回值,也需要使用括号,不同的是,如果给返回值命名了,这个被命名的返回值,就会被初始化为相应类型的零值,且 return不需要加任何返回值名

func main() {
	f1()
}

func f1() (tmp int, tmp2 int) {
	fmt.Println(tmp) //输出0
	fmt.Println(tmp2) //输出0
	return
}

⚠️ 不能返回赋值表达式比如 return tmp,tmp2 = 1,2

🌟 空白符

⭐️ 也就是匿名符号 _,之前应该学过,当你不需要函数返回值某个值时,但是不赋值又编译不过就可以使用

package main

import "fmt"

// 返回两个值的函数,一个整数和一个字符串
func getValues() (int, string) {
    return 42, "meaning of life"
}

func main() {
    // 只关心整数值,不关心字符串,但是函数又retrun了一个字符串
    number, _ := getValues()
  
    fmt.Println(number) // 输出: 42
}

🌟 改变外部变量

⭐️ 传递指针可以减少内存开销,而且还可以通过指针来修改外部变量(重复哈哈哈)

⭐️ 在一个函数中修改的如果是指针变量,那么就不需要使用 return语句了,可以,创建一个指针指向需要被修改的参数,这样可以更加的优化性能

func main() {
	s := 0;  //存储最后的结果
	sum := &s //使用指针指向它
	f1(3,2,sum)
	fmt.Println(*sum) //输出指针指向的值
}

func f1(a,b int, sum *int){
	*sum = a + b
}

3️⃣ 传递变长参数

⭐️ 在切片中讲了 ...,当你需要传入多个同一个类型的参数时就可以使用,实际上就是讲它们加入到了切片内

func main() {
	f1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}

func f1(a ...int) { //传入多个参数
	sum := 0
	for i := range a {
		sum += i
	}
	fmt.Println(sum)
}

⭐️ 如果多个参数类型都不一样传入需要使用到

  • 结构体(后面学),先了解一下:

  • 定义一个结构类型,假设它叫 Options,用以存储所有可能的参数:

    type Options struct {
        par1 type1,
        par2 type2,
        ...
    }
    

    函数 F1 可以使用正常的参数 a 和 b,以及一个没有任何初始化的 Options 结构: F1(a, b, Options {})。如果需要对选项进行初始化,则可以使用 F1(a, b, Options {par1:val1, par2:val2})

  • 空接口(后面学):

  • 如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{},这样就可以接受任何类型的参数(详见第 11.9 节)。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:

    func typecheck(..,..,values … interface{}) {
        for _, value := range values {
            switch v := value.(type) {
                case int: …
                case float: …
                case string: …
                case bool: …
                default: …
            }
        }
    }
    

4️⃣ defer和追踪

🌟 defer使用

⭐️ defer关键字,可以使得某个语句在函数返回之前,任意位置执行return后才会去执行那个语句,和 Javatry catch finally中的 finally很像

func main() {
	sum := f1(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
	fmt.Println(sum)
}

func f1(a ...int) int {
	fmt.Println("程序开始....")
	defer fmt.Println("程序结束成功")
	sum := 0
	for i := range a {
		sum += i
	}
	fmt.Println("程序结束")
	return sum
}


//输出
程序开始....
程序结束
程序结束成功
45

⭐️ 如果有多个 defer语句,遵循后进先出,先进后出,(栈的操作)

func f1(a ...int) int {
	fmt.Println("程序开始....")
	defer fmt.Println("程序真的真的结束成功了(爱信不信)")
	defer fmt.Println("程序真的结束成功了")
	defer fmt.Println("程序结束成功")
	sum := 0
	for i := range a {
		sum += i
	}
	fmt.Println("程序结束")
	return sum
}
//输出:
程序开始....
程序结束
程序结束成功
程序真的结束成功了
程序真的真的结束成功了(爱信不信)

⭐️ 在 java中避免流使用文件为关闭,使用 finally做后序操作(当然后面还是可以节约掉它),那么 defer也可以做收尾工作如

  • 关闭文件流
  • 解锁一个有锁的资源
  • 打印最终报告
  • 关闭数据连接

🌟 defer实现代码追踪

⭐️ 可以使用它来判断在进入和离开某个函数时打印相关消息

package main

import "fmt"

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}


//输出:
entering: b
in b
entering: a
in a
leaving: a
leaving: b

⭐️ 使用 defer 语句来记录函数的参数与返回值

package main

import (
    "io"
    "log"
)

func func1(s string) (n int, err error) {
    defer func() { //这里使用了匿名函数
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}

//输出:
Output: 2011/10/04 10:46:11 func1("Go") = 7, EOF

5️⃣ 递归函数

⭐️ 字面意思

⭐️ 经典斐波那契数

func main() {
	var n int
	fmt.Scanln(&n)
	for i := 1; i <= n; i++ {
		result := fibonacci(i)
		fmt.Println(result)
	}
}

// 输出斐波那契数
func fibonacci(a int) int {
	n := 0
	if a == 1 || a == 2 {
		return 1
	} else {
		n += fibonacci(a-1) + fibonacci(a-2)
	}
	return n
}

6️⃣ 匿名函数(闭包)

⭐️ 当你不想给函数起名字,可以使用匿名函数(JAVA也有这个概念,多用于实现接口方法或者抽象方法)下面是它使用和调用方法

直接赋值给某个变量

func main() {
	a := func(x, y int) int { return x + y }
	fmt.Printf("%d", a(3, 4)) //输出7,这里的a就直接替代这个匿名函数了
}

直接对匿名函数调用,第一对 ()表示参数列表必须紧紧贴着关键词 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对 ()表示对该匿名函数的调用

func main() {
	a := func(x, y int) int { return x + y }(3, 4)//直接调用匿名函数
	fmt.Printf("%d", a)
}

⭐️ 它可以和 defer结合一起使用,这样可以用于在返回语句之后修改返回的 error使用(还没学,应该是要学到错误才可以)

func main() {
	fmt.Printf("%d", test())
}

func test() (t int) {
	defer func() { //在return语句结束后将t++
		t++
	}()
	return 1 //这里直接给t返回1了
}
  • 关键字 defer经常配合匿名函数使用,它可以用于改变函数的命名返回值。
  • 匿名函数还可以配合 go 关键字来作为 goroutine 使用(后面会学到)

匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中
被操作,直到被销毁。闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装,详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查

🌟 将函数作为返回值

这里使得函数返回一个 func的匿名函数

func main() {
	add := Adder(3)          //这里传入a
	fmt.Printf("%d", add(3)) //这里传吐参数b 输出6
}

func Adder(a int) func(b int) int {
	return func(b int) int {
		return a + b
	}
}

⭐️闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量

func main() {
	var f = Test()
    //f(1)赋值的是放回函数的值
	fmt.Print(f(1), " - ") //x=1
	fmt.Print(f(20), " - ")//x=1+20
	fmt.Print(f(300)) //x=1+20+300
}

func Test() func(int) int {
	var a int //这里的a值会一直保存
	return func(t int) int {
		a += t
		return a
	}
}


//输出:
1 - 21 - 321

⭐️ 在闭包中使用的变量可以是函数外部声明的也可以是在外部函数声明的,下面是一个计算0到1000的和

func main() {
	var t int
	go func(x int) { //这里的go关键字是Go语言中用于启动一个新的goroutine的特殊语法。(后面会学)
		s := 0
		for i := 0; i < x; i++ {
			s += i
		}
		t = s //使用外部参数
	}(1000)

📖 练习: 不使用递归来斐波那契

func main() {
	Fib := Fibonacci()
	for i := 1; i < 10; i++ {
		fmt.Print(Fib(), " ")
	}
}

func Fibonacci() func() int {
	f, a := 0, 1
	return func() int {
		f, a = a, a+f
		return a
	}
}

⭐️ 可以返回其他函数的函数和接受其他函数作为参数的函数均被称为高阶函数

🌟 使用闭包来调试

⭐️ 当您在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道
哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。您可以使用 runtimelog 包中的
特殊函数来实现这样的功能。包 runtime 中的函数 Caller() 提供了相应的信息,因此可以在需要的时候
实现一个 where() 闭包函数来打印函数执行的位置(这个后面应该要详细学学)

func main() {
	where := func() {
		_, file, line, _ := runtime.Caller(1)
		log.Printf("%s:%d", file, line)
	}
	where()
	println(1)
	where()
	println(2)
	where()
	println(3)
}

//输出
2024/05/05 17:22:48 D:/learnCode/Go/var/test1_var.go:14
1                                                  
2024/05/05 17:22:48 D:/learnCode/Go/var/test1_var.go:16
2                                                  
2024/05/05 17:22:48 D:/learnCode/Go/var/test1_var.go:18
3   

7️⃣ 计算函数执行时间

⭐️ 有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个
办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这
个计算所消耗的时间。想要实现这样的做法,可以使用 time 包中的 Now() 和 Sub 函数

// 斐波那契数: 1 1 2 3 5 8 13 21 34 55 89
package main

import (
	"fmt"
	"log"
	"time"
)

func main() {
	start := time.Now()
	Fib := Fibonacci()
	for i := 1; i < 10; i++ {
		fmt.Print(Fib(), " ")
	}
	println()
	end := time.Now()
	date := end.Sub(start)
	log.Printf("\n程序运行时间: %s", date)
}

func Fibonacci() func() int {
	f, a := 0, 1
	return func() int {
		f, a = a, a+f
		return a
	}
}


//输出:
1 2 3 5 8 13 21 34 55 
2024/05/05 17:33:47
程序运行时间: 561.8µs

8️⃣ 通过内存缓存来提升性能

⭐️ 在进行大力计算是,提升性能最直接有效的办法就是避免重复计算,可以通过内存中缓存和重复利相同计算的结果,称之为内存缓存

  • 要计算数列中第 n 个数字,需要先得到之前两个数的值,但很明显绝大多数情况下前两个数的值都是已经计算过的。即每个更后面的数都是基于之前计算结果的重复计算
  • 而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置,然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。
package main
import (
    "fmt"
    "time"
)
const LIM = 41
var fibs [LIM]uint64
func main() {
    var result uint64 = 0
    start := time.Now()
    for i := 0; i < LIM; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
    end := time.Now()
    delta := end.Sub(start)
    fmt.Printf("longCalculation took this amount of time: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
    // memoization: check if fibonacci(n) is already known in array:
    if fibs[n] != 0 {
        res = fibs[n]
        return
    }
    if n <= 1 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    fibs[n] = res
    return
}