7. 函数

7. 函数

7.1 函数定义

    在Go语言中,函数是若干语句组成的语句块、函数名称、参数列表、返回值构成,是组织代码的最单元。使用函数的主要作用如下所示:

  • 结构化编程是对代码的最基本的封装,一般按照功能组织一段代码
  • 使用函数封装,可提高代码复用,减少冗余代码
  • 使用代码可使代码简洁美观,增加代码的可读性

    在Go语言中函数大致可以按以下分类进行划分:

  • 内置函数,例如:make、new等
  • 库函数,例如:math.Ceil等
  • 自定义函数,使用func定义

    Go语言中函数定义的基本语法如下所示:

func name(parametersA,parametersB)(returnValue){
	语句块

	return value1,value2
}
  • func:关键字,用于定义函数或方法
  • name:函数名称
  • parameters: 函数参数,参数个数无要求,多个参数之间使用逗号分隔
  • returnValue: 函数返回值,若函数有返回值,则必须设置,且需要注意返回值的数据类型,若无返回值,可以不用设置
  • retrun: 函数返回关键字,若无返回值,可以不用设置
  • value1和value2: 函数的返回值,数据类型需要与returnValue一致

由于Go语言属于编译型语言,因此函数编写顺序不影响程序运行。

    根据定义以上基本语法,定义函数的示例代码如下所示:

// 无参数无返回值
func name() {

}

// 无参数有返回值
func name() int {
	return 1
}

// 有参数无返回值
func name(n int) {

}

// 有参数有返回值
func name(n int) int {
	return n + 1
}

// 多个返回值
func name(n int) (int, error) {
	return n, nil
}

// 多个返回值
func name(n int) (ret int, err error) {
	ret, err = n+1, nil
	return
}

    在实际开发中,很多函数都会设置一个error类型的返回值,用于表示函数运行的结果,如果返回为nil,则代表函数运行成功,否则,则说明函数运行失败。error类型是Go语言定义的接口,主要用于记录程序运行中出现的异常,因此在很多Go源码中会看到类型的代码格式:

	if f, err := os.Open("ret.txt"); err != nil {
		fmt.Printf("调用Open函数出现异常:%v\n", err)
	}

7.2 可变参数

    在实际开发过程,会出现事先无法确定参数个数的情况,针对这种情况,Go语言中函数也允许对函数设置不固定参数。即不限制参数数量,但限制参数的数据类型,不固定参数数量使用...表示,最终转换为切片,这种方式称这为可变参数。基本语法如下所示:

func name (n ...int){
	语句块
}

    如果传入的参数数据类型不确定,则可以使用接口类型interface{}any,示例代码如下所示:

package main

import (
	"fmt"
)

func PrintNumber(n ...int) {
	for idx, val := range n {
		fmt.Printf("不固定参数的索引:%d,值为:%+v\n", idx, val)
	}
}

func PrintMessageA(m ...any) {
	for _, v := range m {
		fmt.Printf("参数类型不固定-A:%v\n", v)
	}
}

func PrintMessageB(m ...interface{}) {
	for _, v := range m {
		fmt.Printf("参数类型不固定-B:%v\n", v)
	}
}

func main() {
	PrintNumber(1, 2, 3, 4, 5, 6)
	PrintMessageA(1, "Surpass", "Kevin", []string{"abc"})
	PrintMessageB(2, "Surpass", "Kevin", []int{1})
}

    最终代码运行结果如下所示:

不固定参数的索引:0,值为:1
不固定参数的索引:1,值为:2
不固定参数的索引:2,值为:3
不固定参数的索引:3,值为:4
不固定参数的索引:4,值为:5
不固定参数的索引:5,值为:6
参数类型不固定-A:1
参数类型不固定-A:Surpass
参数类型不固定-A:Kevin
参数类型不固定-A:[abc]
参数类型不固定-B:2
参数类型不固定-B:Surpass
参数类型不固定-B:Kevin
参数类型不固定-B:[1]

7.3 函数变量

    函数本质上也是一种数据类型,也可以用来定义一种函数类型的变量,示例代码如下所示:

package main

import "fmt"

func HelloA(name string) {
	fmt.Printf("函数HelloA:Hello,%s\n", name)
}

func HelloB(name string) string {
	return fmt.Sprintf("函数HelloB:Hello,%s", name)
}

func main() {
	// 定义函数变量
	var f1 func(string)
	var f2 func(string) string
	// 将函数作为变量进行赋值
	f1 = HelloA
	f1("Surpass")

	f2=HelloB
	fmt.Println(f2("Surpass"))
}

    运行结果如下所示:

函数HelloA:Hello,Surpass
函数HelloB:Hello,Surpass

7.4 匿名函数

    匿名函数即无函数名称的函数,通过将整个函数作为变量形式使用。通常在临时需要使用某个功能,但其功能又不需要单独定义成一个函数时使用。示例代码如下所示:

package main

import (
	"fmt"
)

func main() {
	// 方式一:定义函数并传参
	sum := func(a, b int) int {
		return a + b
	}(10, 50)
	fmt.Printf("匿名函数 ret 返回结果为:%d\n", sum)

	// 方式二:仅定义函数不传参
	div := func(a, b float64) (float64, error) {
		if b == 0 {
			return 0.00, fmt.Errorf("除数为0")
		}
		return a / b, nil

	}
	if ret, err := div(10, 30); err == nil {
		fmt.Printf("匿名函数 div 返回结果为:%f\n", ret)
	}

	if ret, err := div(10, 0); err == nil {
		fmt.Printf("匿名函数 div 返回结果为:%f\n", ret)
	} else {
		fmt.Println("匿名函数 div 计算异常", err)
	}
}

    运行结果如下所示:

匿名函数 ret 返回结果为:60
匿名函数 div 返回结果为:0.333333
匿名函数 div 计算异常 除数为0

7.5 作用域

    函数会开启一个局部作用域,其中定义的标识符仅能在函数之中使用,也称之为标识符在函数中的可见范围,这种对标识符的约束的可见范围,称之为作用域

7.5.1 语句块作用域

    if、for、switch等语句中使用短格式定义的变量,称之为该语句块中的变量,作用域仅在该语句块中。

  • 示例1:
s := []int{1, 2, 3}
for idx, val := range s {
	// idx和val仅在for语句块中可见
	fmt.Println(idx, val)
}
// 在这里会报错,超出idx和val的作用域
fmt.Println(idx, val)
  • 示例2:
if f, err := os.Open("surpass.txt"); err != nil {
	// 仅在if中可见
	fmt.Println(f, err)
}
// 会报错,超出作用域
fmt.Println(f, err)

7.5.2 显式块作用域

    在任何一个大括号定义的标识符,其作用域只能在大括号中

{
	// 块作用域
	const a = 100
	var b = 200
	c := 300
	// 在大括号之内,是可见的
	fmt.Println(a, b, c)
}
// 会报错,超出作用域
fmt.Println(a,b,c)

7.5.3 宇宙块

    宇宙块是指全局块,通常是指语言内置的。例如,预定义标识符。因此像bool、int、nil、true、iota等标识符都是全局可见,随处可用。

7.5.4 包块

    每一个package里面包含该包所有源文件,从而形成的包级别的作用域。在包中顶层代码定义的标识符,称之为包中全局标识符。所有包内定义的全局标识符,在包内可见。包外若想使用这些标识符,则需要首字母大写,在使用时还需要添加包名。

7.5.5 函数块

    函数声明的时候使用了大括号,所以整个函数就是一个显式的代码块,而函数则形成了一个块作用域。

    下面我们通过一个示例来理解作用域,如下所示:

package main

import (
	"fmt"
)

// 包级别的常量、变量定义,只能使用const和var定义,不能使用短格式

const a = 100

var b, c, d = 200, 300, 400

func ShowB() int {
	return b
}

func main() {
	// 常量不可寻址,是对常量的保护
	fmt.Printf("a 的值为:%v\n", a)
	var a = 500
	fmt.Printf("a 的值为:%v,内存地址:%v\n", a, &a)

	fmt.Printf("b 的值为:%v,内存地址:%v\n", b, &b)
	// 这里仅仅是重新赋值,不改变b的内存地址
	b = 600
	fmt.Printf("b 的值为:%v,内存地址:%v\n", b, &b)

	// 这里相当于重新定义了一个变量b,因此内存地址发生变更
	b := 700
	fmt.Printf("b 的值为:%v,内存地址:%v\n", b, &b)
	// b的值已经被重新赋值,因此结果为 600
	fmt.Printf("b 的值为:%v\n", ShowB())

	{
		const j = 'A'
		var k = "Surpass"
		t := true
		a = 900
		b := 1000
		fmt.Printf("j:%v,k:%v,t:%v,a:%v,b:%v\n", j, k, t, a, b)
		{
			l := 1200
			fmt.Printf("j:%v,k:%v,t:%v,a:%v,b:%v,l:%v\n", j, k, t, a, b, l)
		}
		// 报错中,超出作用域
		// fmt.Printf("l:v%",l)
	}
	// 报错中,超出作用域
	// fmt.Printf("j:%v,k:%v,t:%v\n", j, k, t)

	for idx,val:=range []int{10,20,30}{
		fmt.Printf("idx:%v,val:%v\n", idx,val)
	}
	// 报错中,超出作用域
	// fmt.Printf("idx:%v,val:%v\n", idx,val)
}

    代码运行结果如下所示:

a 的值为:100
a 的值为:500,内存地址:0xc0000a6068
b 的值为:200,内存地址:0xbc4358
b 的值为:600,内存地址:0xbc4358
b 的值为:700,内存地址:0xc0000a60b0
b 的值为:600
j:65,k:Surpass,t:true,a:900,b:1000
j:65,k:Surpass,t:true,a:900,b:1000,l:1200
idx:0,val:10
idx:1,val:20
idx:2,val:30

    根据以上运行结果,总结如下所示:

  • 标识符对外不可见,仅限在标识作用域内可见
  • 使用标识符,使用临近原则由近及远两个原则,即优先使用离自己最近的变量值,若没有,则向外扩展寻找
  • 标识符可以向内穿透,即对内可见,在内部局部作用域中,可以使用外部定义的标识符
  • 包级标识符:所在包内,都可见;跨包访问,包级变量和函数名首字母必须大写

7.6 函数递归

    函数递归即函数自己调用自己。一般有两种实现方式:

  • 直接在自己函数中调用自己
  • 间接在自己函数中调用的其他函数又调用了自己

    在使用函数递归时的注意事项:

  • 函数递归需要有边界条件、递归前进段、递归返回段
  • 递归一定要有边界条件
  • 当边界条件不满足时,递归前进,满足时,递归返回

7.6.1 直接递归

    直接递归是指显式在自己函数调用自己,我们以斐波那契数为例,示例如下所示:

  • 使用循环方法
// 使用循环方法
func fibA(n int) int {
	switch {
	case n < 0:
		panic("n不能小于0")
	case n == 0:
		return 0
	case n == 1 || n == 2:
		return 1
	}
	a, b := 1, 1
	for i := 0; i < n-2; i++ {
		a, b = b, a+b
	}
	return b
}
  • 使用递归公式
// 使用递归公式
func fibB(n int) int {
	if n <= 2 {
		return 1
	}
	return fibB(n-1) + fibB(n-2)
}
  • 由循环层次演变为递归函数层次
func fibC(n, a, b int) int {
	if n <= 2 {
		return b
	}
	return fibC(n-1, b, a+b)
}

    在使用递归时,注意事项如下所示:

  • 递归一定要有退出条件,否则会导致无限递归调用
  • 递归深度不宜过深
  • Go语言不会让函数无限递归,因为栈空间会耗尽

7.6.2 间接递归

    间接递归是指在函数调用的函数中再次调用自己,示例如下所示:

func fnA(){
	fnB()
}

func fnB(){
	fnA()
}

fnA()

只要是递归调用,不管是直接还是间接,都要注意边界问题。但间接递归调用时,有时不明显,代码复杂时,很难发现递归调用。因此尽量避免出现间接调用的情况。

7.7 闭包

7.7.1 函数嵌套

    函数嵌套是指在一个函数内部还包含另一个函数,示例如下所示:

package main

import "fmt"

func outer() {
	a := 100
	var inner = func() {
		a = 2000 // 这里使用的a与外部的a是同一个声明,即为同一个标识符
		fmt.Println("inner function", a)
	}
	inner()
	fmt.Println("outter function", a)
}

func main() {
	outer()
}

    代码运行结果如下所示:

inner function 2000
outter function 2000

上面代码中,函数outer中定义了另一个函数inner并调用了inner。outer是包级别的变量,main范围时可见,也可以调用。而inner是outer中的局部变量,仅在outer中可见

7.7.2 闭包

  • 自由变量

    自由变量是指未在本地作用域中定义的变量。例如定义在内层函数外面的外层函数作用域中的变量。

  • 闭包

    闭包是一个概念,通常会出现在函数嵌套中,指的是内层函数引用到了外层函数的自由变量就形成了闭包。主要特点如下所示:

  • 函数存在嵌套:函数内定义了其他函数
  • 内部函数使用了外部函数的局部变量
  • 内部函数被返回(非必须)

    示例代码如下所示:

package main

import "fmt"

func outer() func() {
	a := 100
	fmt.Printf("outer - a: %d addr: %p\n", a, &a)
	inner := func() {
		fmt.Printf("inner - a: %d addr: %p\n", a, &a)
	}
	return inner
}

func main() {
	outer()()
}

    运行结果如下所示:

outer - a: 100 addr: 0xc00000a0b8
inner - a: 100 addr: 0xc00000a0b8

本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:

posted @ 2025-08-11 23:50  Surpassme  阅读(15)  评论(0)    收藏  举报