函数及包

函数

讲函数之前,我们依旧通过需求来引出概念

需求:

输入两个数,再输入一个运算符(+,-,*,/),得到结果

先使用传统的方式来解决:

 package main
 ​
 import "fmt"
 ​
 func main() {
  var n1, n2, res float64
  var operator string
  fmt.Print("请输入两个数字: ")
  fmt.Scanln(&n1, &n2)
  fmt.Print("请输入操作符 (+, -, *, /): ")
  fmt.Scanln(&operator) // %s 可以读取单个字符
  switch operator {
  case "+":
  res = n1 + n2
  case "-":
  res = n1 - n2
  case "*":
  res = n1 * n2
  case "/":
  if n2 == 0 {
  fmt.Println("错误:除数不能为零")
  }
  res = n1 / n2
  default:
  fmt.Println("你的输入有误,不支持的操作符")
  }
  fmt.Printf("%.2f %s %.2f = %.2f\n", n1, operator, n2, res)
 }

写完之后我们可以反思一些问题:

  1. 这是一个很常见的需求,我们每次都要写这么一堆吗?
  2. 这么复杂的代码,我再加东西会不会出问题

这也是两个典型的问题,专业点来说一个是代码复用度不高,一个是代码维护难度大,为了解决这个问题,就出现了函数这个概念。

函数分为自定义函数还有系统函数,系统函数就比如我们的NAN等等,自定义函数就是我们自己定义的函数,那就要了解基本语法了

 func functionName(parameterList) (returnType) {
     ······
     return returnValue
 }

? 看不懂?自己去百度翻译

我们了解了函数,我们再来改造一下上面的代码。

 package main
 ​
 import "fmt"
 ​
 func cal(num1 float64, num2 float64, operator string) float64 {
  var res float64
  switch operator {
  case "+":
  res = num1 + num2
  case "-":
  res = num1 - num2
  case "*":
  res = num1 * num2
  case "/":
  if num2 == 0 {
  fmt.Println("错误:除数不能为零")
  }
  res = num1 / num2
  default:
  fmt.Println("你的输入有误,不支持的操作符")
  }
  return res
 }
 func main() {
  var n1, n2, res float64
  var operator string
 ​
  fmt.Print("请输入两个数字(用空格分隔): ")
  fmt.Scanln(&n1, &n2)
  fmt.Print("请输入操作符 (+, -, *, /): ")
  fmt.Scanln(&operator) // %s 可以读取单个字符
  res = cal(n1, n2, operator)
  fmt.Printf("%.2f %s %.2f = %.2f\n", n1, operator, n2, res)
 }

函数调用机制的底层分析

  1. 在调用一个函数的时候,会给该函数分配一个新的空间,编译器会通过自身的处理让新的空间和其他的栈空间分开,
  2. 每个函数对应的栈中,数据空间是独立的不会混淆
  3. 当一个函数调用完毕后,程序会销毁这个函数对应的栈空间

递归调用

一个函数在函数体内又调用了本身,我们称为递归调用

我们直接上案例

1.斐波那契数列

 package main
 ​
 import "fmt"
 ​
 func fib(n int) int {
  if n == 1 || n == 2 {
  return 1
  } else {
  return fib(n-1) + fib(n-2)
  }
 }
 ​
 func main() {
  fmt.Println("请输入你要查看的斐波那契数列的位数")
  var num int
  fmt.Scanln(&num)
  for i := 1; i < num+1; i++ {
  var str = fib(i)
  fmt.Println(str)
  }
 }

but 这么写,会占用较多内存, 采用纯递归,时间复杂度为 O(2^n),当 n > 40 时会明显卡顿甚至卡死,我们可以优化一下(当然不采用递归而采取迭代),避免指数级递归,使用循环计算,时间复杂度降为 O(n),空间 O(1)

 func fib(n int) int {
  if n == 1 || n == 2 {
  return 1
  }
  a, b := 1, 1
  for i := 3; i <= n; i++ {
  a, b = b, a+b
  }
  return b
 }

什么?不知道时间复杂度是啥?自己搜去!!!!

2.猴子吃桃子

有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有1个桃子了。问题:最初共多少个桃子?

不多说直接上代码

 func peach(n int) int {
     result := 1
     for i := 1; i < n; i++ {
         result = 2 * (result + 1)
    }
     return result
 }

?????我们不是在讲递归吗,搞错了再来

 package main
 ​
 import "fmt"
 ​
 func peach(day int) int {
     if day == 10 {
         return 1
    }
     return 2 * (peach(day+1) + 1)
 }
 ​
 func main() {
     result := peach(1)
     fmt.Printf("第1天最开始共有 %d 个桃子。\n", result)
 }

注意事项和细节讨论

  1. 形参列表可以是多个,返回值列表也可以是多个
  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型
  3. 函数的命名遵循标识符命名规范,首字母不能是数字,首字符大写表示该函数可以被本包文件或者其他包文件使用,首字母小写只能被本包文件使用
  4. 函数内变量声明是局部的,函数外不生效
  5. 基本数据类型和数组默认都是传值,即进行值拷贝在函数内修改,不会影响原来的值
  6. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量
  7. GO函数不支持重载
  8. Go中函数也是一种数据类型可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数进行调用
image-20251018194826135
  1. 函数既然是一种数据类型,因此在Go中函数可以作为形参,并且调用
image-20251018195649260
  1. 为了简化数据类型定义,Go支持自定义数据类型

基本语法:type 自定义数据类型名 数据类型

举例:type mySum func(int, int)int ,这个时候mySum就等价于func(int, int)int来使用了,虽然等价但是这两个不是同一个类型,这是两个类型,比如type myInt int,这个时候,我们不能把int类型的变量的值赋给myInt类型的变量,如果需要赋值,这里需要显式转换。

  1. 使用_来忽略返回值
  2. Go支持可变参数,代码为:func sum(args...int)sum int 这就支持0到多个参数,args是slice(切片,可以暂时简单理解为一个长度可变的数组),通过args[index],可以访问到各个值,这里args的值的数量并不固定,可能为0~任意数量
  3. 支持对函数返回值命名
 func getCal(n1 int, n2 int) (sum int , sub int) {
  sum = n1 + n2
  sub = n1 - n2
  return
 }

这样就默认返回了sum还有sub两个值

init函数

我们先来bing一下,init是什么意思

image-20251020200628262

initial函数就是初始化函数,每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说,init会在main函数之前被调用。

细节和注意事项

  1. 如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程是变量定义→init函数→main函数我们来跑一个案例证明一下
    image-20251020201902944
  2. init函数主要作用就是完成一些初始化的工作,比如初始化变量的值

匿名函数

在程序开发中,有些函数我们就希望使用一次,那么我们就可以考虑使用匿名函数(没有名字的函数),匿名函数也可以实现多次调用,

使用方式

1.在定义匿名函数的时候直接使用,这种方式匿名函数只能调用一次

 func main() {
     func(a, b int) {
         result := a + b
         fmt.Printf("两数之和是:%d\n", result)
    }(5, 3)
 }

2.讲匿名函数赋给一个变量,再通过该变量来调用匿名函数

 func main() {
     add := func(x, y int) int {
         return x + y
    }
 ​
     fmt.Println(add(2, 3))  
     fmt.Println(add(10, 5))
     fmt.Println(add(7, 8))  
 }

注意:这里的add不要理解成函数名

如果我们将匿名函数赋给一个全局变量,那么这个匿名函数就会成为一个全局匿名函数。

闭包

闭包是一个函数与其相关的引用环境组合的一个整体。

我们来看个例子

 package main
 ​
 import "fmt"
 ​
 func newCounter() func() int {
     count := 0                    // 外部变量,被闭包捕获
     return func() int {
         count++                   // 闭包内部修改外部变量
         return count
    }
 }
 ​
 func main() {
     counter := newCounter()
 ​
     fmt.Println(counter()) // 输出: 1
     fmt.Println(counter()) // 输出: 2
     fmt.Println(counter()) // 输出: 3
 ​
     // 再创建一个独立的计数器
     counter2 := newCounter()
     fmt.Println(counter2()) // 输出: 1 (独立环境)
 }

newCounter函数返回的是一个匿名函数,这个匿名函数引用到了函数外的count,匿名函数和count就形成了一个整体,构成闭包。在反复调用的时候,n的初始化只有一次,就能实现一个累加。

没理解?没关系,我们来个案例沉浸式体验闭包:

我们来编写一个程序makeSuffix(suffix string)可以接受一个文件后缀名,并返回一个闭包,调用闭包可以传入一个文件名,如果没有特定后缀就返回文件名.jpg,如果有后缀就返回原文件名。

PS:函数strings.HasSuffix

 package main
 ​
 import (
  "fmt"
  "strings"
 )
 ​
 func makeSuffix(suffix string) func(string) string {
  return func(name string) string {
  if strings.HasSuffix(name, suffix) == false {
  return name + suffix
  }
  return name
  }
 }
 func main() {
  f := makeSuffix(".jpg")
  fmt.Println("文件名处理后=", f("winter"))
 }
 ​

defer

在函数中,程序员需要创建资源,为了在函数执行完毕后,及时释放资源,Go的设计者提供了defer(延时机制)

我们先来个案例尝尝咸淡

 package main
 ​
 import "fmt"
 ​
 func sum(n1 int, n2 int) int {
  defer fmt.Println("ok1 n1=", n1)
  defer fmt.Println("ok2 n2=", n2)
  res := n1 + n2
  fmt.Println("ok3 res=", res)
  return res
 }
 ​
 func main() {
  res := sum(10, 20)
  fmt.Println(res)
 }

当程序运行到defer这一行的时候,会将后面的语句压入独立的栈中,暂时不执行,当函数执行完毕后,再从这个独立的栈中,按照先入后出的方式,从栈中释放出来,

image-20251021160829238

注意事项:

  1. 在defer将语句压入栈中时,也会将相应的值拷贝同时入栈,在defer后面对压入变量的值进行变动,并不会影响以及压入栈中变量的值
  2. defer主要的价值是在函数执行完毕后可以释放函数创建的资源
    image-20251021161259404
    上图也是在golang编程中的常见做法,创建资源后(比如说打开了文件获取了数据库的链接),可以执行defer,在defer后可以继续使用创建的资源,在函数执行完毕后,系统依次从栈中取出语句关闭资源。

在实际开发中,我们也会遇到很多问题,比如说A程序员写了一个文件,定义了一个数据筛选函数,B程序员也要用,但是A和B开发时用的不是同一个文件,该怎么办,以及A与B共同开发一个项目时,A和B都想定义一个celsiusToF函数,但是两个函数名重复定义了,又该怎么办。

为了解决这些问题,包应运而生,

包的本质还是一个文件夹,里面存放了程序文件,go每一个文件都属于一个包,也就是说go是以包的形式来管理文件和项目目录结构

包的作用:

  1. 区分名字相同的函数变量等标识符
  2. 当程序文件很多的时候,可以很好的管理项目
  3. 控制函数变量的作用域

基本语法

 //打包语法
 package util
 //引入语法
 import "src/../../.."
 //调用函数
 包名.函数名

我们还是来点例子

image-20251014202321780
 package main
 ​
 import (
  "fmt"
  "go_code/project01/pointerdemo/Calculator"
 )
 ​
 func main() {
  var n1, n2, res float64
  var operator string
  fmt.Println("请输入两个数字(用空格分隔): ")
  fmt.Scanln(&n1, &n2)
  fmt.Println("请输入操作符 (+, -, *, /): ")
  fmt.Scanln(&operator) // %s 可以读取单个字符
  res = Calculator.Cal(n1, n2, operator)
  fmt.Printf("%.2f %s %.2f = %.2f\n", n1, operator, n2, res)
 }
 ​
 package Calculator
 ​
 import "fmt"
 ​
 // 注意这里函数名首字母要大写,才能表示该函数可以导出
 func Cal(num1 float64, num2 float64, operator string) float64 {
  var res float64
  switch operator {
  case "+":
  res = num1 + num2
  case "-":
  res = num1 - num2
  case "*":
  res = num1 * num2
  case "/":
  if num2 == 0 {
  fmt.Println("错误:除数不能为零")
  }
  res = num1 / num2
  default:
  fmt.Println("你的输入有误,不支持的操作符")
  }
  return res
 }
 ​

使用细节

  1. 给一个文件打包时,该包对饮一个文件夹,文件的包名通常和文件夹名一致,一般为小写字母
  2. package指令在文件第一行,然后是import指令
  3. 所有包导入必须基于模块根(module root)或标准库,不支持相对路径导入。
  4. 为了让其他包的我呢见,可以访问到本包的函数,该函数名首字母要大写,才能表示该函数可以导出,也就类似于其他语言的public
  5. 如果包名较长,Go支持给包去别名,注意细节,取别名后,原来的包就不能使用了
 import 别名 "src/../../.."
 ···
  别名.函数名
 ···
  1. 在同一个包下,不能有相同的函数名,否则报重复命名 ,注意是包不是文件哦。
  2. 如果要编译成可执行程序文件,就需要将这个包声明为main
暂无评论

发送评论 编辑评论


				
上一篇
下一篇