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
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Python的函数可以分成几种 纯函数 递归函数 内置函数 匿名函数 偏函数 闭包 纯函数 官方文档:https:...
    猪儿打滚阅读 48评论 0 1
  • 第7天 添加函数功能 基本的函数定义与调用执行、引入闭包使Stone语言可以将变量赋值为函数,或将函数作为参数传递...
    余生如意阅读 1,735评论 0 0
  • 基于网络课程《Python全栈开发专题》 记录笔记,请支持正版课程。 斐波那契数列 斐波那契数列科普:https:...
    WESTWALL阅读 1,183评论 0 0
  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 10,094评论 0 5
  • 本章内容 函数表达式的特征 使用函数实现递归 使用闭包定义私有变量 定义函数的方式有两种:一种是函数声明,另一种就...
    闷油瓶小张阅读 2,880评论 0 0