LOADING

正在加载

Go基础学习(肆)——函数

壹 前言

在学习函数之前,需要明白为什么要学习函数?

首先有这么一段代码:

代码1:

fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")
fmt.Println("Hello!World!")

代码2:

#这段代码的意思是将6句Hello!World!放进fun()自定义函数中,并调用fun()函数
func fun(){
    fmt.Println("Hello!World!")
    fmt.Println("Hello!World!")
    fmt.Println("Hello!World!")
    fmt.Println("Hello!World!")
    fmt.Println("Hello!World!")
    fmt.Println("Hello!World!")
fun()

也许现在会很疑惑,这不就是简单的fmt.Println函数打印6遍而已吗?
是的,他们看上去差不多,甚至有人会觉得代码2比代码1复杂许多,但是你想想如果这6句Hello!World!不是单单在一处地方打印时,而是上百处上千处时,那要把代码1打印多久?而代码2只要把定义写完,想在那打印就调用一下即可。函数在刚学习的时候不容易发现他的长处,甚至觉得比较麻烦,但是到了以后做大项目的时候,会发现编程因为有了函数而变得有趣和简洁。

贰 什么是函数

函数是基本的代码块,用于执行一个任务,Go 语言最少有个 main() 函数,你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务,函数声明告诉了编译器函数的名称,返回类型,和参数。

Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。

叁 创建函数

格式

func 函数名( [参数] ) [返回值] {
   函数体
    [return]
}
  • func:函数由 func 开始声明
  • 函数名:函数名称,参数列表和返回值类型构成了函数签名。
  • 参数:参数列表,参数由参数变量和参数变量的类型组成,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数是可选的,也就是说函数也可以不包含参数。多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。有些功能不需要返回值,这种情况下返回值不是必须的,就不写了。
  • 函数体:实现指定功能的代码块。
  • return:要返回的值,根据返回值确定需不需要,需要就返回值,不需要就不写。

代码

func funtext( x int ) int {
    y := x + 1
    return y
}

肆 函数的调用以及返回值

4.1 多返回值

当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。定义好后的函数,我们直接根据函数加对应参数,即可调用,同时go可以根据函数的[返回值]个数返回对应多个值。如果有返回类型,但是不返回return的话,会报错,所以有几个[返回值],就要有就几个返回值
格式:

func 函数名( [参数] ) [返回值类型] {
   函数体
    return 返回值1,返回值2,...
}
// 调用通过 函数名([参数])的方式调用函数,这里的参数根据定义函数时有无参数决定的,没有不用写
函数名([参数])

例子:

package main
import "fmt"
func main() {
    sum1, sum2 := 10, 20
    fmt.Println(fun(sum1, sum2))
}
//返回多少个值,[返回值类型]内就需要多少个类型,当然如果没有的话就不用写返回类型和return
func fun(a int, b int) (int, int) {
    //这里如果没有下面这行返回对应数量返回值的话会报错
    return a + b, b
}

2e4e8261f8de7534f0b664abe5ac4abb.png

4.2 返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
格式:

func 函数名( [参数] ) (返回值1 返回值类型1, 返回值2 返回值类型2,...) {
    函数体
    return
}

例子:

package main
import "fmt"
func main() {
    ret1, ret2 := calc(100, 200)
    fmt.Println(ret1, ret2)
}
func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    return
}

6ddf853a7737c981a864f0c3029ea9ef.png

4.3 返回值补充

  • 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
    if x == "" {
        return nil // 没必要返回[]int{}
    }
    ...
}
  • 在编程中我们需要报错来判断该方法是否执行成功,但是出现报错会将程序中断,这时候我们可以只写return即可!
    func portScanner(Host, Port string) {
      // 这个方法是go的TCP连接方法
      conn, porterr := net.Dial("tcp", Host+":"+Port)
      if porterr != nil {
          // 可以看到我们需要这个返回值,防止程序终端
          return
      }
      conn.Close()
      fmt.Println(Port, "端口打开")
    }
    

伍 参数传递以及函数参数

函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。调用函数,可以通过两种方式来传递参数:值传递、引用传递。
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

5.1 值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言调用函数使用的是值传递,即在调用过程中不会影响到实际参数。

package main
import "fmt"
func main() {
    /* 定义局部变量 */
    a, b := 100, 200
    fmt.Printf("交换前 a 的值为 : %d\n交换前 b 的值为 : %d\n", a, b)
    /* 通过调用函数来交换值 */
    swap(a, b)
    fmt.Printf("交换后 a 的值 : %d\n交换后 b 的值 : %d\n", a, b)
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
    var temp int
    temp = x /* 保存 x 的值 */
    x = y    /* 将 y 值赋给 x */
    y = temp /* 将 temp 值赋给 y*/
    return temp
}

05c2df0808beee12690e6b86b2e34731.png
函数中使用的是值传递,所以两个值并没有实现交互。

5.2 引用传递(需要后面的指针知识)

这个知识我们了解就行,后面学习了指针再回来学习。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:

package main
import "fmt"
func main() {
    /* 定义局部变量 */
    a, b := 100, 200
    fmt.Printf("交换前 a 的值为 : %d\n交换前 b 的值为 : %d\n", a, b)
    /* 调用 swap() 函数
     * &a 指向 a 指针,a 变量的地址
     * &b 指向 b 指针,b 变量的地址
     */
    swap(&a, &b)
    fmt.Printf("交换后 a 的值 : %d\n交换后 b 的值 : %d\n", a, b)
}

func swap(x *int, y *int) {
    var temp int
    temp = *x /* 保存 x 地址上的值 */
    *x = *y   /* 将 y 值赋给 x */
    *y = temp /* 将 temp 值赋给 y */
}

cc23735115080b1cf89e3be46835935d.png

5.3 普通参数

普通参数定义,例如:

// 通常情况下每个参数都包含参数名和类型
func intSum1(x int, y int) int {
    return x + y
}
// 而如果相邻变量的类型相同,则可以省略类型
func intSum2(x, y int) int {
    return x + y
}
//intSum2函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型

5.4 可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。注意:可变参数通常作为函数的最后一个参数。

例子:

package main
import "fmt"
func main() {
    ret5 := intSum2(100)
    ret6 := intSum2(100, 10)
    ret7 := intSum2(100, 10, 20)
    ret8 := intSum2(100, 10, 20, 30)
    fmt.Println(ret5, ret6, ret7, ret8)
}
func intSum2(x int, y ...int) int {
    fmt.Println(x, y)
    sum := x
    for _, v := range y {
        sum = sum + v
    }
    return sum
}

bf4b94d76c67fc7d4de80ac5814c5328.png
本质上,函数的可变参数是通过切片来实现的。

陆 函数数组传参

如果你想向函数传递数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下方式来声明:

  • 形参设定数组大小:
    void myFunction(param [10]int){
      ...
    }
    
    例子:
    package main
    import "fmt"
    func main() {
     /* 数组长度为 5 */
     var balance = [5]int {1000, 2, 3, 17, 50}
     var avg float32
     /* 数组作为参数传递给函数 */
     avg = getAverage( balance, 5 ) ;
     /* 输出返回的平均值 */
     fmt.Printf( "平均值为: %f ", avg );
    }
    func getAverage(arr [5]int, size int) float32 {
     var i,sum int
     var avg float32  
     for i = 0; i < size;i++ {
        sum += arr[i]
     }
     avg = float32(sum) / float32(size)
     return avg;
    }
    
    00ac9becc6cc56f3aafcee032018d7a9.png

柒 函数类型与变量

7.1 定义函数类型

假设我们要将函数定义成一个可以在不同场景下使用,但是功能一样的函数时,我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int
//用于定义函数类型
//calculation为函数类型名字

上面语句定义了一个叫calculation类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。

简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。

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

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

add和sub都能赋值给calculation类型的变量。

var c calculation
c = add

7.2 函数类型变量

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

func main() {
    var c calculation               // 声明一个calculation类型的变量c
    c = add                         // 把add赋值给c,这里的add函数是上面定义过的,这里只是引用
    fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
    fmt.Println(c(1, 2))            // 像调用add一样调用c
    f := add                        // 将函数add赋值给变量f1
    fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
    fmt.Println(f(10, 20))          // 像调用add一样调用f
}

需要明白一点,在函数赋值给其他变量时,如果带括号返回的是return的值,不是本身,而如果不带括号,则是函数本身赋值给其他变量。

捌 高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

8.1 函数作为参数

函数可以作为参数:

func add(x, y int) int {
    return x + y
}
func calc(x, y int, funtext func(int, int) int) int {
    return funtext(x, y)
}
func main() {
    ret2 := calc(10, 20, add)
    fmt.Println(ret2) //30
}

8.2 函数作为返回值

函数也可以作为返回值:

func add(x, y int) int {
    return x + y
}
func sub(x, y int) int {
    return x - y
}
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
    }
}

玖 闭包以及匿名函数

9.1 匿名函数

在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数,匿名函数就是没有函数名的函数,匿名函数多用于实现回调函数和闭包。

9.1.1 格式

匿名函数的定义格式如下:

//可以看到没有函数名,而是将其赋值给一个变量,不赋值到变量里,该匿名函数就没有意义,或者直接在后面加参数执行,具体实现下面会讲
valfun := func(参数)(返回值){
    函数体
}

9.1.2 例子

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

package main
import "fmt"
func main() {
    // 将匿名函数保存到变量
    add := func(x, y int) {
        fmt.Println(x + y)
    }
    add(10, 20) // 通过变量调用匿名函数
    //自执行函数:匿名函数定义完加()直接执行
    func(x, y int) {
        fmt.Println(x + y)
    }(10, 20)
}

d7342241e34645c6619cf2825ce48c66.png

9.2 闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。 首先我们来看一个例子:

// func(int)是adder()函数返回值,而int是func(int)返回值
func adder() func(int) int {
    var x int
    // fmt.Println(x)
    // x不进行赋值默认是0
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    // 这就就是将func(y int) int {
    // 			x += y
    // 			return x
    // 		}
    // 这个匿名函数传给f,这时候x已经进行了实例化,即为0,这样就直接保留了x这个变量的值
    var f = adder()
    // 接着我们将10赋值到f里,就是将10赋值给y,然后进行x=x+y运算
    fmt.Println(f(10)) //10
    // 接着我们将20赋值到f里,就是将20赋值给y,然后进行x=x+y运算,由于前面x的已经被f(10)改变为10,所以这里x=x+y=10+20=30
    fmt.Println(f(20)) //30
    fmt.Println(f(30)) //60
    // 由于这里是新的变量赋值,所以重新实例化一个adder函数,x也被重新实例化,但是不影响之前的x
    f1 := adder()
    fmt.Println(f1(40)) //40
    fmt.Println(f1(50)) //90
}

变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。可以看到闭包就是将adder这个函数的返回值(一个匿名函数)和adder函数内的作用域环境给f

闭包进阶示例1:

func adder2(x int) func(int) int {
    return func(y int) int {
        x += y
        return x
    }
}
func main() {
    var f = adder2(10)
    fmt.Println(f(10)) //20
    fmt.Println(f(20)) //40
    fmt.Println(f(30)) //70

    f1 := adder2(20)
    fmt.Println(f1(40)) //60
    fmt.Println(f1(50)) //110
}

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return "1"
    }
}
func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc(".jpg")) //1
    fmt.Println(txtFunc("test")) //test.txt
}

闭包进阶示例3:

func calc(base int) (func(int) int, func(int) int) {
    add := func(i int) int {
        base += i
        return base
    }

    sub := func(i int) int {
        base -= i
        return base
    }
    return add, sub
}

func main() {
    f1, f2 := calc(10)
    fmt.Println(f1(1), f2(2)) //11 9
    fmt.Println(f1(3), f2(4)) //12 8
    fmt.Println(f1(5), f2(6)) //13 7
}

闭包其实并不复杂,只要牢记闭包=函数+引用环境

拾 递归函数

10.1 递归介绍

递归,就是在运行的过程中调用自己。语法格式如下:

func recursion() {
   recursion() /* 函数调用自身 */
}
func main() {
    recursion()
}

Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。

10.2 阶乘

以下实例通过 Go 语言的递归函数实例阶乘:

package main
import "fmt"
func Factorial(n uint64)(result uint64) {
    if (n > 0) {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}
func main() {  
    var i int = 15
    fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

268db5642aa83cbbf2d1086dae55c6c0.png

10.3 斐波那契数列

以下实例通过 Go 语言的递归函数实现斐波那契数列:

package main
import "fmt"
func fibonacci(n int) int {
  if n < 2 {
   return n
  }
  return fibonacci(n-2) + fibonacci(n-1)
}
func main() {
    var i int
    for i = 0; i < 10; i++ {
       fmt.Printf("%d\t", fibonacci(i))
    }
}

520cb49ad09f0e675cf97a5944ab30d2.png

拾壹 defer语句——延时处理语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常该语句用来处理一些最后要执行的语句。

举个例子:

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

0ce07c6bfa643d88651940e35c7069dd.png

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

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

在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

如下示例

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()

dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()

return io.Copy(dst, src)
}

以下是defer三条使用规则:

  • 当defer被声明时,其参数就会被实时解析
    即defer的参数的变量值,在代码中defer使用后的位置改变并不会对改变defer用到的值

  • defer执行顺序为先进后出
    在函数中,先定义的defer将会在最后执行

  • defer可以读取函数内变量
    defer代码块的作用域仍然在函数之内,因此defer仍然可以读取函数内的变量

  • defer执行在panic语句之前
    当出现panic语句的时候,会先按照defer的后进先出的顺序执行,最后才会执行panic

avatar
小C&天天

修学储能 先博后渊


今日诗句