Go语言:函数

105 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情


函数:将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。

就比如说:一个程序里,频繁地出现某些相同的功能,这些功能可能就是改个变量,其他一模一样,那么我就可以把这些功能封装起来成为一个整体,再给这个整体起个名字,我以后用我就通过名字在主程序里调用就行了,没必要再重复去写相同功能,这个整体就叫函数。

1.函数的基本定义与使用

基本语法:

func 函数名()  {
   函数体
}

调用:在主函数main里直接函数名()即可

比如我现在随便定义两个函数,并在主函数里调用

func play() {
   fmt.Println("玩游戏")
}

func eat() {
   fmt.Println("吃饭")
}

func main() {
   eat()
   play()
}

上面的代码就会输出 吃饭 玩游戏 ;

因为主函数main()是程序的入口,往下执行看到eat()时,程序就会去找eat()函数在哪,找到了就执行eat()内部函数,执行完之后再回主函数接着往下执行,发现是play()函数,再去找play()去执行play()里的函数体。

2.函数指定参数个数

  1. 基本语法:
func 函数名(参数)  {
   函数体
}
  1. 调用:在主函数main里直接函数名(参数)即可

其实就是在函数调用时在()里加参数

  1. 形参和实参:
    • 在主函数调用其他自定义函数时传的参数叫实参
    • 自定义函数()里的叫形参
  2. 可以在自定义函数()里声明多个形参,比如func Add(num1 int , num2 int){},就声明了两个整型参数,调用时也要传俩实参
  3. 实参传给形参时一定要注意类型,类型一定要对得上

练习:用函数求1-100数字和

首先思路就是定义一个函数,在函数内实现并打印,在主函数调用即可,代码如下:

func SumAdd() {
   var sum int
   for i := 1; i <= 100; i++ {
      sum += i
   }
   fmt.Println(sum)
}

func main() {
   SumAdd()
}

但我的需求可能会改变,比如我要求1-200,那么可以把SumAdd()函数里的100改成200,那我要求1-300,那么又会更改一次;那如果一直在函数内部更改,函数的灵活性就变差了,我们写函数就是希望写完内部功能之后,在主函数调用它,并不想改变它,那么此时我们就可以采用传参数行为。

把定义的函数更改如下:

func SumAdd(num int) {
   var sum int
   for i := 1; i <= num; i++ {
      sum += i
   }
   fmt.Println(sum)
}

此时在括号内定义了一个int类型的num变量,把 i <= 100 改成 i <= num,这样循环的次数由num决定

那num是什么呢?我们在主函数调用这个SumAdd函数时,就可以指明num是多少。

func main() {
   SumAdd(100)
}

这里就表示num的值为100,我要是写SumAdd(100),那么num值为200

3.函数不定参数列表

在上面我们是指明了函数括号()里的参数有多少个,主函数调用时就要传多少个实参。

但可能会出现,有时候我们暂时不清楚要传多少个参数,我们就可以使用不定参数列表。

  1. 基本语法
func 函数名(参数名...类型)  {
   函数体
}

使用不定参数时一定要加那三个点!

  1. 基本使用

比如我现在定义一个函数,采用不定参数列表的形式

func TestSum(args ...int) {
   
}

在主函数调用此函数时,就可以传任意个数的参数给args,类型必须是int。

func main() {
   TestSum(1, 3, 5)
}

此时我就传了1,3,5给形参,那么args就可以看做一个集合,里面装了1,3,5这几个数

那如何在args这个集合内区分这三个数呢?

Go语言给这些参数搞了编号,比如第一个参数它的编号就是0,第二个的编号就是1

比如我想取第一个数字1,那就可以通过args[0]得到它。

现在演示把args里的数通过循环打印出来

func TestSum(args ...int) {
   for i := 0; i < len(args); i++ {
      fmt.Println(args[i])
   }
}

func main() {
   TestSum(1, 3, 5)
}

len()函数可以获取一些数组集合的长度,有多少个数据,长度就是几。

  1. 可以在参数列表中,声明一些具体参数,和一个不定参数,但不定参数一定要放在最后!因为不定的含义就是不确定你会传多少个。比如func TestA(num int , args...int){}

for range循环遍历

  • for range结构是一个增强for循环。常用来遍历数组,切片,集合。

  • for range会返回两个值,可以自定义两个变量接收,第一个是下标(编号),第二个是下标对应的数据。有时候我们不打算使用下标,或不用数据,就用下划线_代替变量

同样是上面的代码,用for range进行遍历args

func TestSum(args ...int) {
   for a, v := range args {
      fmt.Println("a = ", a) //下标
      fmt.Println("v = ", v) //集合内的值
   }
}

func main() {
   TestSum(1, 3, 5)
}

如果不想打印下标,就用下划线代替,代码如下

func TestSum(args ...int) {
   for _, v := range args {
      fmt.Println("v = ", v)
   }
}

func main() {
   TestSum(1, 3, 5)
}

为什么非要用下划线代替?有人可能会觉得我不写那个接收下标的变量,或者我写了不用不就行了吗?

  • 首先,如果不写接收下标的变量,比如只写for v := range args,那么v里面存的是下标,而不是数据。
  • 第二,你写了接收下标的变量但不用,也会报错,你以前随便声明个变量不用,编译器运行还会报a declared but not used错误,既然声明就一定要用,这是Go语言的特点。

4.函数返回值

返回值就是可以在函数声明时指定返回值类型。

那为什么要有返回值?比如我通过一个函数完成两个数相加,如果此时我想在主函数里通过一个变量拿到两数相加的结果,此时就要用到返回值。

跟着下面的代码来, 先写一个两数相加并打印的函数,在主函数传参。

func Add(num1 int, num2 int) {
   fmt.Println(num1 + num2)
}

func main() {
   Add(3, 4)
}

此时我打算在主函数接收两数相加的结果,并除以2,代码如下

func Add(num1 int, num2 int) int {
   var sum int
   sum = num1 + num2
   return sum
}

func main() {
   s := Add(3, 4)
   fmt.Println(s/2)
}

在函数Add()里,用sum变量接收num1和num2相加的值,然后返回sum给主函数就行,因为sum是整型,所以在自定义函数的参数列表后面声明返回值类型为int,并在主函数要有变量接收这个函数的返回值,这里我用的是变量s

注意的点:

  • 返回值关键字是return
  • 返回值类型要写在参数列表后面
  • 要在主函数通过变量接收这个函数的返回值
  • Add()函数内部也可以直接省去第一行sum变量的定义,直接写sum := num1 + num2

🎈第二种写法:就是在返回值类型时直接指明要返回哪个变量的值,顺便也帮你把变量创建好了。

func Add(num1 int, num2 int) (sum int) {
   sum = num1 + num2
   return sum
}

func main() {
   s := Add(3, 4)
   fmt.Println(s / 2)
}

注意:

  • 要用小括号把要返回值的那个变量及它的类型括起来
  • 如果采用这种写法,就表示最终会返回整型变量sum中的值
  • 在函数体中也没有必要再重新创建sum变量

🎈第三种写法:在第二种写法的基础上,省略return后面需要返回的变量名

func Add(num1 int, num2 int) (sum int) {
   sum = num1 + num2
   return
}

func main() {
   s := Add(3, 4)
   fmt.Println(s / 2)
}

也就是说,如果已经指定了返回的变量的名称,那么return后面可以不用再加上变量的名称。

返回多个值

比如现在我写一个Get()函数,返回两个值,并在主函数接收

func Get() (int, int) {
   num1 := 10
   num2 := 20
   return num1, num2 //表明返回两个变量的值
}

func main() {
   s1, s2 := Get()
   fmt.Println("s1 =", s1, "s2 =", s2)
}

5.函数作用域

局部变量

定义在函数内部的变量。

比如我在main()函数里定义了一个变量a,那么这个a就是一个局部变量,因为它是声明在函数内部,并且只在函数内部有效

看下面一段代码

func Test() {
   a := 1
   a++
}

func main() {
   a := 10
   Test()
   fmt.Println(a)
}

在main()函数和Test()函数里都声明了一个a,但两个a都在函数内部,所以是两个不同的a,互不影响。

比如你看我在main()函数调用了Test()函数,但打印出来的a还是10,而不是2(Test函数内部进行了a++操作)。所以我们可以得出局部变量只在函数内部有效

全局变量

在函数外部声明,任何一个函数都可以获取和更改此变量。

var a int

func main() {
   a = 20
   Test()
}

func Test() {
   fmt.Println(a)
}

在函数外部声明了一个a变量,在main()函数内赋值,在Test()函数内打印,都是对同一个变量进行操作。

由此可见,全局变量可以在整个程序内进行调用。

🎈注意:如果局部变量的名字和全局变量的名字相同时,优先使用的是局部变量

6.延迟调用defer

我们发现当我们调用一个函数时,程序执行到它时会立即去此函数内部执行完,但有时候我们并不想让程序马上执行它。我们就可以用defer。defer调用的函数一定会执行!

  1. 基本使用:关键字defer用于延迟一个函数的执行。

    比如我打印两句话:

    func main(){
       fmt.Println("China")
       fmt.Println("good")
    }
    

    那么此时程序一定会先打印China再打印good,但如果我用延迟defer

    func main(){
       defer fmt.Println("China")
       fmt.Println("good")
    }
    

    那它就会先打印good,再打印China

  2. 执行顺序:如果一个函数中有多个defer语句,他们会以后进先出的顺序执行。即谁在后谁先执行

    func main() {
       defer fmt.Println("China")
       defer fmt.Println("good")
       defer fmt.Println("up")
    }
    

其实实际开发中,用到defer最多的地方就是文件操作。

7.递归函数

简介

最常用来解决“树形结构”的问题,比如查看电脑中某一个文件夹下面有什么其他文件夹,其他文件夹又会包含其他文件或文件夹,就是一种典型的树形结构。

语法

给个情景:比如现在自己在看电影,想知道自己是第几排,那么可以问问前一排的人是第几排,如果前一排的人不知道就再问他们前一排的观众,一直问到第一排的观众为止。

那么可以这么写

func main() {
   c := Test(3) //假如自己在第三排
   fmt.Println(c)
}

func Test(n int) int {
   // 只有第一排的人才知道自己的排数
   if n == 1 {
      return 1
   }
   // 如果不是第一排,问一下前一排的人
   r := Test(n - 1)
   fmt.Println("前一排的排数:", r)
   // 把前一排人的排数+1,计算出自己的排数
   return r + 1
}

执行流程:

image.png

案例

计算一个数的阶乘

var s = 1

func main() {
   Test(5)
   fmt.Println(s)
}

func Test(n int) {
   if n == 1 {
      return //终止函数
   }
   s *= n
   Test(n - 1)
}