【学习笔记】Go语言函数详解

149 阅读7分钟

Go语言函数详解

摘要: 本文讲解了Go语言函数的定义、参数简写、可变参数、命名返回值等基础用法,以及函数作为变量、参数与返回值的高级应用。同时介绍匿名函数、自执行函数、闭包的概念及案例。最后详细说明了defer语句的执行顺序和错误处理机制,包括panic/recover的使用。内容配以代码示例,清晰展示了Go函数的核心特性和灵活性(由ChatGPT生成)

函数是组织好的,可重复使用的,用于执行指定任务的代码块

函数的定义

func 函数名(参数1 数据类型, ...) 返回值类型 {
    函数执行流程
}

例如,定义一个进行两数相加的函数,可以这样定义:

package main
import "fmt"
// 执行两个int类型整数相加,返回值也是整数类型
func add(a int, b int) int {
	return a + b
}

func main() {
	c := add(5, 6)
	fmt.Printf("%v - %T", c, c)
}
2.1 函数参数之类型简写

如果相邻的两个或多个参数的类型一致,可以在最后一个参数中注明类型,前面相同数据类型的参数后面可以省略。

package main
import "fmt"
// 这里a和b类型都是int, c、d、e类型都是float
func add(a, b int, c, d, e float64) int {
	fmt.Printf("a:(%v, %T)\n", a, a)
	fmt.Printf("b:(%v, %T)\n", b, b)
	fmt.Printf("c:(%v, %T)\n", c, c)
	fmt.Printf("d:(%v, %T)\n", d, d)
	fmt.Printf("e:(%v, %T)\n", e, e)
	return a + b
}
func main() {
	c := add(10, 15, 22, 33, 42)
	fmt.Printf("%v - %T", c, c)
}
2.2 函数参数之可变参数

有些时候,我们可能并不确定参数个数(但是确定参数类型),这个时候我们就可以使用可变参数,用...来标识。注意:可变参数一定在最后

package main
import "fmt"
// d 是一个int类型的切片
func printD(a, b int, c string, d ...int) {
	fmt.Printf("a(%v, %T)\n", a, a)
	fmt.Printf("b(%v, %T)\n", b, b)
	fmt.Printf("c(%v, %T)\n", c, c)
	fmt.Printf("d(%v, %T)\n", d, d)
}
func main() {
	printD(10, 15, "你好", 1, 2, 3)
	fmt.Println("------------------------")
	printD(10, 15, "hello")
}
2.3 函数之返回值命名

在我们定义函数时,可以直接定义出相关的返回值,这样做的好处时:无需在代码块中进行声明,和可以直接通过return返回,如果没有给该变量重新赋值,则使用默认值

import "fmt"
func add(a, b int) (c, d int) {
	// 一旦在外面定义了返回值,则不能在创建其他值
	//d: = a + b
	//return d
	a += b
	// c = a + b  // 此时返回值为 6 0
	return
}
func main() {
	e, f := add(1, 5)
	fmt.Println(e, f)  // 此时返回值为 0 0
}

函数类型和变量

package main

import "fmt"


func sumA(a, b int) int {
	return a + b
}
func sumB(a int, b string) {
	return
}
func sumC(a, b int) string {
	return "hello"
}
func sumD(c, d int) int {
	return c + d + 10
}
func sumE(c, d, e int) int {
	return c + d + e
}
// 定义了一个calc函数类型,它接收两个int类型的参数,返回一个int类型的值
type calc func(int, int) int
func main() {
	var c calc
	//c = sumA  // 可以
	c = sumD // 可以
	fmt.Println(c(1, 3))
	//c = sumB  // 不可以 参数类型 返回值类型不一致
	//c = sumC  // 不可以 返回值类型不一致
	//c = sumE  // 不可以 参数数量不一致
}

这样做的好处是,可以在定义某个变量时,通过自定义类型,来确定那些函数可以赋值给变量,有利于我们通过某个变量操作不同的函数,同时,在后面将函数作为参数中,也会用到这个方法。

高阶函数

4.1 函数可以作为参数
package main
import "fmt"
// 定义了一个calc函数类型,它接收两个int类型的参数,返回一个int类型的值
type calc func(int, int) int
func ca(a, b int, op calc) int {
	return op(a, b)
}
func sum(a, b int) int {
	return a + b
}
func sub(a, b int) int {
	return a - b
}
func main() {
	fmt.Println(ca(1, 2, sub))
	fmt.Println(ca(10, 3, sum))
}

4.2 函数可以作为返回值

package main

import "fmt"

type calc func(int, int) int

func sum(a, b int) int {
	return a + b
}
func sub(a, b int) int {
	return a - b
}
// 也可以这样定义 func do(s string) calc {}
func do(s string) func(int, int) int {
	switch s {
	case "+":
		return sum
	case "-":
		return sub
	default:
		return nil
	}
}
func main() {
	a1 := do("+")
	fmt.Println(a1(10, 5))
	a2 := do("-")
	fmt.Println(a2(10, 5))
}

匿名函数

故名思意,就是没有名称的函数

在Go语言中,我们无法在一个函数中在定义另外一个函数,例如这样:

package main
import "fmt"
func do(a, b int)(int, int){
	func sum(a, b int) int{
		return a+b
	}
	func sub(a, b int) int{
		return a - b
	}
	return sum(a, b), sub(a, b)
}
func main() {
	fmt.Println(do(10, 5))
}

这样的代码逻辑在Go中时不被支持的,所以才有了匿名函数这一实现。

func(参数1 参数类型, ...)(返回值1类型, 返回值2类型){}
// 如果只有一个返回值,后面不用加括号

通过匿名函数,我们可以改良上面的代码,使其可以正常运行:

package main
import "fmt"
func do(a, b int) (int, int) {
	sum := func(a, b int) int {
		return a + b
	}
	sub := func(a, b int) int {
		return a - b
	}
	return sum(a, b), sub(a, b)
}
func main() {
	fmt.Println(do(10, 5))
}

匿名函数有两种使用方式,上面这种属于将匿名函数与某个变量绑定,通过变量来使用匿名函数

5.1 匿名自执行函数

除了将匿名函数交给某个变量外,我们还可以让其自执行,方法如下:

package main
import "fmt"
func do(a, b int) (c int, d int) {
	func(a, b int) int {
		c = a + b
		return c
	}(a, b)
	func(a, b int) int {
		d = a - b
		return d
	}(a, b) // 该函数将在执行到这个位置时,自动创建并运行
	return
}
func main() {
	fmt.Println(do(10, 5))
}

闭包

简单理解就是: 一个函数中嵌套了另外一个函数

举一个常见的例子,当用户点赞时,我们运行一个up函数,将原有的点赞量加1,这种情况用闭包来实现就再好不过了。

package main
import "fmt"
func up() func() int {
	num := 0
	return func() int {
		num += 1
		return num
	}
}
func main() {
	v1 := up()
	fmt.Println(v1())
	fmt.Println(v1())
	v2 := up()
	fmt.Println(v2())
	fmt.Println(v2())
	fmt.Println(v2())
}

每次运行一次v1,num则自动增加1,且该变量能在内存中长期保存。而且相对于全局变量而言,每次将up绑定到一个新的变量时,该变量对应的num都是初始值0,不会受到其他函数的影响。

defer 语句

关于defer要了解的是:

  1. defer标记的函数执行流程要在正常函数之后
  2. defer标记了多个函数,则根据defer的标记顺序从后往前执行
  3. defer在函数注册时,其参数就已经确定了

关于第一和第二点,可以参考以下代码。

package main
import "fmt"
func stop() {
	fmt.Println("程序运行完毕...")
}
func clean() {
	fmt.Println("程序执行完毕,正在清除占用内存...")
}
func main() {
	stop()
	clean()
	fmt.Println("程序开始运行")
	fmt.Println(5 + 10)
}

这段代码正常运行的结果是这样的:

程序运行完毕...
程序执行完毕,正在清除占用内存...
程序开始运行
15

但是当我们通过defer标记stop和clean函数时,将会有不一样的效果

package main
import "fmt"
func stop() {
	fmt.Println("程序运行完毕...")
}
func clean() {
	fmt.Println("程序执行完毕,正在清除占用内存...")
}
func main() {
	defer stop()
	defer clean()
	fmt.Println("程序开始运行")
	fmt.Println(5 + 10)
}

此时运行效果如下:

程序开始运行
15
程序执行完毕,正在清除占用内存...
程序运行完毕...

关于第三条,可以分析该代码得到答案:

package main
import "fmt"
func add(a, b int) int {
	return a + b
}
func main() {
	a, b := 10, 15
	defer fmt.Println("defer:", a, b)
	a = 20
	b = 30
	fmt.Println(add(a, b))
}

这段代码的运行结果是:

50
defer: 10 15

之所以会出现这样的结果,是因为当代码运行到

defer fmt.Println("defer:", a, b)

注册Println这个函数时,此时的参数a、参数b是多少,传参就是多少,后面在改变也不会影响到Println函数的参数。

Go语言的错误处理

在Go中,目前还没有try .. catch,这对项目的稳定性是有一定影响的,但是不用担心,Go语言中也是提供了panic/recover来实现对错误的捕获,注意recover只能在defer调用的函数中才有效果。

package main
import "fmt"
func div(a, b int) int {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
			return
		}
	}()
	return a / b
}
func main() {
	fmt.Println(div(10, 5))
	fmt.Println(div(10, 0))
}

上面这样的方式属于被动捕获函数运行过程中的异常,我们也可以主动设置异常。

package main
import "fmt"
func div(a, b int) int {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println(err)
			return
		}
	}()
	if b == 0 {
		panic("分母不能为0")
	}
	return a / b
}
func main() {
	fmt.Println(div(10, 5))
	fmt.Println(div(10, 0))
}