函数

函数

​ 函数就是盛放一组代码块的”容器”,该代码块实现某个特定功能,被组织到函数内后可以重复调用

​ Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

一 函数的使用

1.1 定义

语法

func 函数名(参数)(返回值){
    函数体
}

解释:

  • 1、函数名:命名规范同变量名,同一个包内不能重复
  • 2、参数:由参数名和类型组成,可以有多个,用逗号分隔开即可
  • 3、返回值:由变量名和类型组成,可以只写返回值的类型,也可以有多个,但在定义函数时,多个返回值必须用逗号分隔开并且用()包裹
  • 4、函数体:具体的功能实现

例1

func add(x int,y int) int{
    return x + y
}

例2:参数与返回值都是可选的

func sayHi(){
    fmt.Println("hello egon")
}

1.2 调用

语法

函数名(值1,值2,值3,...)

func main()  {
    res:=add(1,2)
    fmt.Println(res)

    add(1,2) // 调用有返回值的函数时,可以不接收其返回值。
    sayHi()
}

二 参数

参数分为形参与实参,实参指的是调用函数时传入的值(相当于变量值),形参指的是定义函数是指定参数(相当于变量名),调用函数时,会将实参值赋值给形参

2.1 类型简写

函数的形参中如果相邻变量的类型相同,则可以省略类型,例如:

func add(x,y int) int{
    return x + y
}

2.2 可变长参数

可变长指的就是调用函数时,传入的实参值个数不固定,我们可以在形参名or其类型前加…,该形参就可以用来接收溢出的实参值,并把它们存成一个切片,称之为可变长参数,本质上,函数的可变参数是通过切片来实现的。

注意:可变参数通常要作为函数的最后一个参数。

func add(x ...int) int {
    fmt.Println(x)  // x是一个切片
    sum := 0
    for _, v := range x {
        sum += v
    }
    return sum
}

调用

func main() {
    res1 := add(1, 2, 3)
    res2 := add(1, 2, 3, 4, 5)
    res3 := add(1, 2, 3, 4, 5, 6)
    fmt.Println(res1, res2, res3)  // 6 15 21

}

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:

func add(x int,y ...int) int {
    fmt.Println(x,y)
    sum := 0
    for _, v := range y {
        sum += v
    }
    return sum
}

三 返回值

3.1 多个返回值

Go语言中函数支持多返回值,函数如果需要有多个返回值,我们在定义函数的返回值时,必须用()将所有返回值包裹起来。

func divmod(x, y int) (int, int) {
    res1 := x / y
    res2 := x % y
    return res1, res2
}

3.2 命名返回值

func divmod(x, y int) (res1,res2 int) {
    res1 = x / y  // 此处用的是赋值操作,而非声明res1的操作
    res2 = x % y
    return
}

3.3 返回值补充

当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

func someFunc(x string) []int {
    if x == "" {
        return nil // 没必要返回[]int{}
    }
    ...
}

四 作用域

4.1 什么是作用域

作用域指的是声明语句的作用域。声明语句如变量的声明、函数的声明等,都是将一个实体绑定给一个名字,而声明语句的作用域指的就是在源代码中可以有效使用该名字的范围

作用域 VS 声明周期

  • 1、声明语句的作用域对应的是一个源代码的文本区域,是一个编译时的属性。
  • 2、声明语句创造一个变量,该变量的生命周期指的是在程序运行时,变量存在的有效时间段,在此时间段内该变量是可以被程序的其他部分引用的,所以说声明周期是一个运行时的概念

4.2 如何区分作用域范围

(一)语法块与词法域

狭义地讲,我们通常认为用花括号{}包含的一系列语句称之为一个语法块,广义地讲,Go语言中有以下几种语法块

  • 1、整个源代码就是一个大的代码块-》全局语法块
  • 2、一个包的所有代码=》包语法块
  • 3、每个for、if和switch语句的语法决
  • 4、每个switch或select的分支也有独立的语法决;
  • 5、当然也包括显式书写的语法块(花括弧包含的语句)。

语法决定了其内部声明的名字所在的作用域,这与名字的查找优先级紧密相关,what???

声明语句所在的语法块称之为它的词法域,即名字定义的地方,也就是说,层层嵌套的词法域是以定义阶段为准生成的,而在Go语言中,采用的就是词法作用域,换句话说,定义位置决定了作用域的嵌套关系而与引用位置无关,即无论在何处引用/查找名字,都需要老老老实实地回到该名字当初定义的地方去找作用域关系,由内向外一层层查找

了解:词法域与动态作用域

编译器的第一个工作阶段叫做词法化,也叫单词化,所以说,词法作用域是在词法阶段生成的作用域,是一个编译时的概念。与词法域相对应的是动态作用域,二者对比如下
//1、词法作用域(又称静态作用域)是在书写代码或者说定义时确定的,
//2、而动态作用域是在运行时确定的。
//3、词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用,其作用域链是基于运行时的调用栈的

(二)词法作用域

词法作用域,或简称作用域,有如下几种

(1)全局作用域

对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。

(2)包级作用域

任何在在函数外部(也就是包级词法域)声明的名字是在包级作用域,可以在同一个包的任何源文件中访问

(3)源文件级作用域

对于导入的包,例如在当前文件中导入的fmt包,则是对应源文件级的作用域,因此只能在当前的文件中访问导入的fmt包,即便是当前包的其它源文件也无法访问在当前源文件导入的包,若也想使用包fmt,需要重新导入才行

(4)局部作用域

函数内部的名字,只能在函数内使用

(5)函数内嵌套的作用域

函数内可以再有{},可以是for、if、switch等{},{}内定义的名字只能在{}内使用,

一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系,例如,你可以声明一个局部变量,和包级的变量同名。当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问。

但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读,go语言不推荐这么做。

(三)隐式词法作用域与显式词法作用域

func main() {
    // 1、此处是函数体词法域=>显式的
    for i := 0; i < len(x); i++ { // 2、本行代码是初始化词法域=>隐式的
        // 3、此处是循环体词法域=>显式的
    }
}

下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:

func main() {
    x := "hello"  // 1、函数体词法域=>显式的
    for _, x := range x {  // 2、本行代码是初始化词法域=>隐式的
        // 3、此处是循环体词法域=>显式的
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
    }
}

和for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

if x := 111; x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

第二个if语句嵌套在第一个内部,因此第一个if语句条件初始化词法域声明的变量在第二个if中也可以访问。

懂了上例之后,我们再来看一个例子

if f, err := os.Open(fname); err != nil { // compile error: unused: f
    return err
}
f.ReadByte() // compile error: undefined f
f.Close()    // compile error: undefined f

// 变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误

通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:

f, err := os.Open(fname)
if err != nil {
    return err
}
f.ReadByte()
f.Close()

你可能会考虑通过将ReadByte和Close移动到if的else块来解决这个问题:

if f, err := os.Open(fname); err != nil {
    return err
} else {
    // f and err are visible here too
    f.ReadByte()
    f.Close()
}

但这不是Go语言推荐的做法,Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

此外,switch语句的每个分支也有类似的词法域规则:条件部分为一个隐式词法域,然后每个是每个分支的词法域。

(四)包级作用域与声明顺序

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。

func main() {
    sayHi()
}

// 后定义的函数
func sayHi() {
    fmt.Println("hello", name)
}

// 后定义的变量
var name = "egon"

// 递归引用了自身
// var x = x  // 编译错误

(五)注意短变量声明语句的作用范围

要特别注意短变量声明语句的作用域范围,考虑下面的程序了:获取当前的工作目录然后保存到一个包级的变量中。这可以本来可以在包级直接调用os.Getwd完成,但是将这个从主逻辑中分离出来放到一个单独的函数内可能会更好,特别是在需要处理错误的时候,于是我们编写了下述代码

var cwd string

func init() {
    cwd, err := os.Getwd() // 此时是声明语句,compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

虽然cwd在外部已经声明过,但是:=语句还是将cwd和err重新声明为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。

由于当前的编译器会检测到局部声明的cwd并没有本使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。

var cwd string

func init() {
    cwd, err := os.Getwd() // 此时是声明语句,compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

但此时全局的cwd变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个BUG更加隐晦。

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:

var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()  // 此时就完全成了赋值语句,就是更新了包级的cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

(六)练习

练习1:下述代码结果为?

var x int = 111

func main() {
    x=3333
    say()
}

func say() {
    fmt.Println(x)
}

练习2:下述代码结果为?

var x int = 111

func main() {
    x:=222
    {
        x:=333
        {
            x:=444
            fmt.Println(x)
        }
        fmt.Println(x)
    }
    fmt.Println(x)

    say()
}

func say() {
    fmt.Println(x)
}

五 函数类型与变量

5.1 定义函数类型

我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int,int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值,凡是满足该条件的函数都是calculation类型的函数,如下add和sub

func add(x, y int) int{
    return x + y
}
func sub(x, y int) int{
    return x - y
}

func main() {
    fmt.Printf("%T\n",add)  // func(int, int) int
    fmt.Printf("%T\n",sub)  // func(int, int) int
}

5.2 函数类型的变量

我们可以声明函数类型的变量并且为该变量赋值:

func main() {
    var c calculation     // 声明一个calculation类型的变量c
    c = add               // 把add赋值给变量c
    fmt.Printf("%T\n", c) // main.calculation
    fmt.Println(c(1, 2))  // 像调用add一样调用c

    f := add               // 将函数add赋值给变量f
    fmt.Printf("%T\n", f)  // func(int, int) int
    fmt.Println(f(10, 20)) // 像调用add一样调用f
}

六 高阶函数

高阶函数指的是

​ 1、函数可以作为参数

​ 2、函数可以作为返回值

6.1 函数作为参数

type calculation func(int, int) int

func add(x, y int) int {
    return x + y
}
func cal1(x, y int, op calculation) int {
    return op(x, y)
}

func cal2(x, y int, op func(x, y int) int) int { // 同上述cal1
    return op(x, y)
}

func main() {
    res1 := cal1(10, 20, add)
    fmt.Println(res1) // 30

    res2 := cal1(10, 20, add)
    fmt.Println(res2) // 30
}

6.2 函数作为返回值

func do(s string) (func(int, int) int, error) {
    switch s {
    case "+":
        return add, nil
    case "-":
        return sub, nil
    default:
        err := errors.New("无法识别的运算符")
        return nil, err
    }
}

func main() {
    f,err:=do("-")
    fmt.Println(f,err)
    fmt.Println(f(111,222))
}

七 匿名函数和闭包

7.1 匿名函数

匿名函数就是没有名字的函数,用于临时使用一次的场景

func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,所以没办法像普通函数那样调用,匿名函数需要保存到某个变量或者作为立即执行函数:

func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 通过变量调用匿名函数

    //自执行函数:匿名函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

匿名函数多用于实现回调函数和闭包。

7.2 闭包

闭包函数=闭函数+包函数

闭:指的是定义在函数内部的函数,注意,在Go语言中,定义在函数内部的函数必须是匿名函数

包:闭函数包含了对外层函数名字的引用

func counter(start int) func(int) int{
    f:= func(n int) int{
        start += n  // 修改外层函数的变量start
        return start
    }
    return f
}

func main() {
    c:=counter(0)
    fmt.Println(c(1)) // 1
    fmt.Println(c(1)) // 2
    fmt.Println(c(1)) // 3
}

闭包应用示例1

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc("test")) //test.jpg
    fmt.Println(txtFunc("test")) //test.txt
}

闭包应用示例2

func circle(radius float64) (func(), func()) {
    perimeter := func() {
        fmt.Println("周长是:",2 * math.Pi * radius)
    }
    area := func() {
        fmt.Println("面积是:",math.Pi * (radius * radius))
    }
    return perimeter, area
}

func main() {
    f1,f2 := circle(3.3)
    f1()  // 周长是: 20.734511513692635
    f2()  // 面积是: 34.21194399759284
}

八 defer语句

8.1 defer介绍

Go语言中,函数内可以依次包含多个defer语句,在函数结束时,defer语句会逆序执行

func main() {
    fmt.Println("start...")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end...")
}

输出

start...
end...
3
2
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

8.2 defer的执行时机

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:

联系管理员微信tutu19192010,注册账号

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术