文章

Go语言函数

什么是函数

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

函数的基本形式

//函数定义。a,b是形参
func argf(a int, b int) { 
	a = a + b 
}
var x, y int = 3, 6
argf(x, y) //函数调用。x,y是实参
  • 形参是函数内部的局部变量,实参的值会拷贝给形参。
  • 函数定义时的第一个的大括号不能另起一行。
  • 形参可以有0个或多个。
  • 参数类型相同时可以只写一次,比如argf(a,b int)。
  • 在函数内部修改形参的值,实参的值不受影响。
  • 如果想通过函数修改实参,就需要指针类型。
func argf(a, b *int) { 
    *a = *a + *b
    *b = 888
}
var x, y int = 3, 6
argf(&x, &y)

 slice、map、channel都是引用类型,它们作为函数参数时其实跟普通struct没什么区别,都是对struct内部的各个字段做一次拷贝传到函数内部。

package main

import "fmt"

func slice_arg_1(arr []int) { //slice作为参数,实际上是把slice的arrayPointer、len、cap拷贝了一份传进来
	arr[0] = 1           //修改底层数据里的首元素
	arr = append(arr, 1) //arr的len和cap发生了变化,不会影响实参
}

func main() {
	arr := []int{8}
	slice_arg_1(arr)
	fmt.Println(arr[0])   //1
	fmt.Println(len(arr)) //1
}

函数的声明

Go语言函数定义格式如下:

func function_name([parameter list])[return_types]{
  函数体
}
package main

import "fmt"

// main函数
func main() {
   fmt.Println("hello world")
   fmt.Println(add(1, 2))
}

// func 函数名(参数,参数...) 函数调用后的返回值{
// 函数体 :执行一段代码
// return 返回结果
// }
func add(a, b int) int {
   c := a + b
   return c
}

image-20221216232000367

  • 无参无返回值函数
package main

import "fmt"

func main() {
   printinfo()
}
func printinfo() {
   fmt.Println("无参无返回值")
}
  • 有一个参数零个返回值的函数
package main

import "fmt"

func main() {
   printinfo2(2)
}
func printinfo2(num1 int) {
   fmt.Println("有一个参数的函数")
   fmt.Println(num1)
}
  • 有两个参数零个返回值的函数
package main

import "fmt"

func main() {
   printinfo3(2, 4)
}
func printinfo3(num1 int, num2 int) {
   fmt.Println(num2, num1)

}
  • 有多个参数(可变参数)零个返回值的函数
package main

import "fmt"

func main() {
   printinfo4(1, 2, 3, 4, 5, 6)
}
func printinfo4(num1, num2 int, nums ...int) {
   var sum int
   sum = num1 + num2
   fmt.Println(len(nums))
   for i := 0; i < len(nums); i++ {
      fmt.Println("可变参数", i, ":", nums[i])
      sum += nums[i]
   }
   fmt.Println(sum)
}
  • 有一个返回值的函数
package main

import "fmt"

func main() {
   sum := printInfo(100, 200)
   fmt.Println(sum)

}
func printInfo(num1, num2 int) int {
   return num1 + num2
}
  • 有两个返回值的函数
package main

import "fmt"

func main() {
   fmt.Println(printInfo2(100, 200))

}
func printInfo2(num1, num2 int) (int, int) {
   return num2, num1
}
  • 有多个返回值的函数
package main

import "fmt"

func main() {
   _, _, _, r4 := func2(2, 4)
   fmt.Println(r4)
}

func func2(len, wid float64) (float64, float64, float64, float64) {
   area := len * wid
   zc := (len + wid) * 2
   fmt.Println("zc:", zc)
   fmt.Println("area:", area)
   return area, zc, 1, 3
}

形式参数和实际参数

package main

import "fmt"

func main() {
   //传入实际参数
   res := max(4, 5)
   fmt.Println(res)
}

// 形式参数:定义函数时,用来接收外部传入数据的参数
// 实际参数:调用函数时,传给形参的实际数据叫做实际参数
func max(num1, num2 int) int {

   var result int
   if num1 > num2 {
      result = num1
   } else {
      result = num2
   }
   //一个函数定义上有返回值,那么函数中必须使用return语句
   return result
}

image-20221217000502089

可变参数

一个函数的参数类型确定,但个数不确定,就可以使用可变参数

func mufunc(arg...int){}
//arg...int告诉go这个函数接收不定数量的参数,类型全部是int
package main

import "fmt"

func main() {
   getSum(1, 2, 4, 5, 6, 7, 8)
}

// ...可变参数
func getSum(nums ...int) {
   sum := 0
   for i := 0; i < len(nums); i++ {
      fmt.Println(nums[i])
      sum += nums[i]
   }
   fmt.Println("sum:", sum)
}

image-20221217005453164

⚠️⚠️

  • 如果一个函数的参数时可变参数,同时还有其他的参数,可变参数要放在列表的最后
  • 一个函数的参数列表中最多只能有一个可变参数

参数传递

按照数据的存储特点来分:

  • 值类型的数据:操作的事数据本身:int、string、bool、float64、arrar…
  • 引用类型的数据:操作的事数据的地址:slice、map、chan…

值传递

值传递的是数据的副本,修改数据,对于原始的数据没有影响

值类型的数据,默认都是值传递、基础类型、array、struct

package main

import "fmt"

func main() {
   //值传递
   arr := [4]int{1, 2, 3, 4}
   fmt.Println(arr)
   //传递,拷贝arr
   update(arr)
   fmt.Println("arr调用后的数据:", arr)
   //引用传递
}
func update(arr2 [4]int) {
   fmt.Println("arr接收的数据:", arr2)
   arr2[2] = 100
   fmt.Println("arr修改后的数据:", arr2)
}

image-20221217235026935

引用传递

变量在内存中是存放在一定的地址上的,修改变量实际是修改变量地址出的内存

package main

import "fmt"

// 引用传递
func main() {
   //切片,可以扩容的数组
   s1 := []int{1, 2, 3, 4}
   fmt.Println("默认的数据", s1)
   //传入的是引用类型的数据,地址
   update2(s1)
   fmt.Println("调用后的数据", s1)
}
func update2(s2 []int) {
   fmt.Println("传递的数据", s2)
   s2[0] = 100
   fmt.Println("修改后的数据", s2)
}

image-20221218001627505

函数中变量的作用域

作用域:变量可以使用的范围

局部变量:函数内部定义的变量,叫做局部变量

全局变量:函数外部定义的变量,叫做全局变量

package main

import "fmt"

// 全局变量
var num string = "我是全局变量"

func main() {
   //函数体的局部变量
   temp := 100
   if b := 1; b <= 10 {
      //语句内的局部变量
      temp := 50
      fmt.Println(temp) //局部变量遵循就近原则
      fmt.Println(b)

   }
   fmt.Println(temp)
   fmt.Println(num)
   f1()

}
func f1() {
   fmt.Println(num)
}

image-20221218002451051

⚠️⚠️无论是全局变量还是局部变量,均遵循就近原则

递归函数

定义:一个函数自己调用自己,就叫递归函数

⚠️递归函数需要有一个出口,逐渐向出口靠近,没有出口就会形成死循环

package main

import "fmt"

func main() {
   sum := getSum(5)
   fmt.Println(sum)
}
func getSum(n int) int {
   if n == 1 {
      return 1
   }
   return getSum(n-1) + n
}

image-20221218003100226

defer

语义:推迟延迟,在go语言中使用defer关键字来延迟一个函数或者方法的执行

package main

import "fmt"

func main() {
   f("1")
   fmt.Println("2")
   defer f("3")
   fmt.Println("4")

}
func f(s string) {
   fmt.Println(s)
}

image-20221218003651534

defer函数或者方法:一个函数或方法的执行被延迟了

  • 可以在函数中添加多个defer语句,当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回,特别是当你进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然容易造成资源泄漏等问题
  • 如果有很多调用defer,那么defer是采用先进后出模式
  • defer用于注册一个延迟调用(在函数返回之前调用)。
  • defer典型的应用场景是释放资源,比如关闭文件句柄,释放数据库连接等。
  • 如果同一个函数里有多个defer,则后注册的先执行。
  • defer后可以跟一个func,func内部如果发生panic,会把panic暂时搁置,当把其他defer执行完之后再来执行这个。
  • defer后不是跟func,而直接跟一条执行语句,则相关变量在注册defer时被拷贝或计算。
package main

import "fmt"

func main() {
   f("1")
   fmt.Println("2")
   defer f("3")
   fmt.Println("4")
   defer f("5")
   fmt.Println("6")
   defer f("7")
   fmt.Println("8")
}
func f(s string) {
   fmt.Println(s)
}

image-20221218004059344

defer函数传递参数的时候

package main

import "fmt"

func main() {
   a := 10
   fmt.Println("a=", a)
   defer print(a) //参数以及传递进去了,在最后执行
   a++
   fmt.Println("end a=", a)
}
func print(s int) {
   fmt.Println("函数里面的a=", s)
}

image-20221218004540133

defer的用法:

  • 对象.close()临时文件的删除

    文件.open()
    defer.close()
    读或写操作
    
  • go语言中关于异常的处理,使用panic()和recover()

    • panic()函数用于引发恐慌,导致程序中断执行
    • recover函数用于恢复程序的执行,recover()语法上要求必须在defer中执行

函数的数据类型

package main

import "fmt"

func main() {
	fmt.Printf("%T\n", func() {})                                              //func()
	fmt.Printf("%T\n", func(a, b int, c ...string) (int, int) { return 0, 0 }) //func(int, int, ...string) (int, int)

	var f4 func(int, int)
	f4 = func13         // 引用类型
	f4(3, 5)            // 3 5
	fmt.Println(func13) //0x102337840
	fmt.Println(f4)     //0x102337840
}

func func13(a, b int) {
	fmt.Println(a, b)
}

匿名函数

package main

import "fmt"

func main() {
   f1()
   f2 := f1
   f2()
   //匿名函数
   f3 := func() {
      fmt.Println("我是f3函数")
   }
   f3()
   //匿名函数自己调用自己
   func() {
      fmt.Println("我是f4函数")
   }()
   //匿名函数自己调用自己,可以有参数
   func(a, b int) {
      fmt.Println(a, b)
      fmt.Println("我是f5函数")
   }(1, 2)
   //匿名函数自己调用自己,可以有参数,有返回值
   r1 := func(a, b int) int {
      fmt.Println("我是f6函数")
      return a + b
   }(1, 2)
   fmt.Println(r1)
}
func f1() {
   fmt.Println("我是f1函数")
}

image-20221218120633289

Go语言是支持函数式bian chen编程:

  • 将匿名函数作为另外一个函数的参数,回调函数
  • 将匿名函数作为另外一个函数的返回值,可以形成闭包结构

回调函数

高阶函数:根据go语言的数据类型的特点,可以将一个函数作为另外一个函数的参数

fun1()

fun2(fun1)

fun1函数作为fun2这个函数的参数

fun1函数:就叫做回调函数,作为另外一个函数的参数

fun2函数:就叫做高阶函数,接收了一个函数作为参数的函数

package main

import "fmt"

func main() {
   r1 := add(1, 2)
   fmt.Println(r1) //3

   r2 := oper(3, 4, add)
   fmt.Println(r2) //7
   r3 := oper(5, 4, sub)
   fmt.Println(r3) //1
   r4 := oper(8, 2, func(a int, b int) int {
      if b == 0 {
         fmt.Println("除数不能为0")
         return 0
      }
      return a / b
   })
   fmt.Println(r4) //4
}

// 高阶函数
func oper(a, b int, fun func(int, int) int) int {
   r := fun(a, b)
   return r
}
func add(a, b int) int {
   return a + b
}
func sub(a, b int) int {
   return a - b
}

闭包结构

一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量,并且该外层函数的返回值就是这个内层函数

这个内层函数和外层函数的局部变量,统称为闭包结构

局部变量的声明周期就会发生变化,正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁,但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还在继续使用

package main

import "fmt"

// 是一种特殊的结构:闭包结构,违反了程序正常的生命周期。合法的使用。程序允许的一种特殊结构,变量作用域升级了。

// 什么时候用闭包: js (xxxxxxx.html   引用大量的第三方库:10个js库,js库中很多变量名是冲突的)
// js 很多框架都是闭包结构的,防止变量冲突,全局变量污染

// 我的代码里面的变量就不会和你代码里面的变量冲突了。解决一些变量作用域冲突的问题。

/*
闭包结构:
一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量并且该外层函数的返回值就是这个内层函数。

在闭包结构中:局部变量的生命周期就会发生改变,
正常的局部变量会随着函数的调用而创建,随着函数的结束而销毁
但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还在继续使用.

// 由于垃圾回收期不会将闭包中的变量销毁,可能会造成内存泄漏。
*/

// 你的代码变量和你同事的变量冲突了,解决。 i 新建一个变量。 第三方库中的代码都是闭包结构实现的导出。
var i int = 10

func main() {
	r1 := increment()
	fmt.Println(r1) // 返回的是一个 increment() 内存函数,还没有执行
	// -- 执行这个内层函数
	//
	v1 := r1()
	fmt.Println(v1)
	v2 := r1()
	fmt.Println(v2)
	fmt.Println(r1())
	fmt.Println(r1())
	fmt.Println(r1())
	// 你写的代码是对的,但是结果不对,你的变量被污染了
	fmt.Println("--------------------------")

	// r2和r1指向同一个地址
	r2 := increment() // 再次调用的时候 ,i = 0
	v3 := r2()
	fmt.Println(v3) // 1

	//因为我们内层还是用i,还存在引用,系统不会销货这个i,保护,单独作用r1
	fmt.Println(r1()) // 6 	// 这里的i 并没有随着 第二次创建就被销毁归0,而是在内层函数继续调用着。
	fmt.Println(r2()) // 2
	// r1 名字 ----> 内存地址 &r1
	fmt.Printf("%p\n", &r1)
	fmt.Printf("%p\n", &r2)

}

// 自增函数
// increment() 函数返回值为  func() int 类型
func increment() func() int { // 外层函数,项目(很多的全局变量)
	// 定义一个局部变量
	i := 0
	// 在外层函数内部定义一个匿名函数,给变量自增并返回。
	fun := func() int {
		i++
		return i
	}
	return fun
}

image-20230302095959626

License:  CC BY 4.0 test