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