函数
讲函数之前,我们依旧通过需求来引出概念
需求:
输入两个数,再输入一个运算符(+,-,*,/),得到结果
先使用传统的方式来解决:
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)
}
写完之后我们可以反思一些问题:
- 这是一个很常见的需求,我们每次都要写这么一堆吗?
- 这么复杂的代码,我再加东西会不会出问题
这也是两个典型的问题,专业点来说一个是代码复用度不高,一个是代码维护难度大,为了解决这个问题,就出现了函数这个概念。
函数分为自定义函数还有系统函数,系统函数就比如我们的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.斐波那契数列
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)
}
注意事项和细节讨论
- 形参列表可以是多个,返回值列表也可以是多个
- 形参列表和返回值列表的数据类型可以是值类型和引用类型
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字符大写表示该函数可以被本包文件或者其他包文件使用,首字母小写只能被本包文件使用
- 函数内变量声明是局部的,函数外不生效
- 基本数据类型和数组默认都是传值,即进行值拷贝在函数内修改,不会影响原来的值
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址
&,函数内以指针的方式操作变量 - GO函数不支持重载
- Go中函数也是一种数据类型可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数进行调用


- 函数既然是一种数据类型,因此在Go中函数可以作为形参,并且调用

- 为了简化数据类型定义,Go支持自定义数据类型
基本语法:type 自定义数据类型名 数据类型
举例:type mySum func(int, int)int ,这个时候mySum就等价于func(int, int)int来使用了,虽然等价但是这两个不是同一个类型,这是两个类型,比如type myInt int,这个时候,我们不能把int类型的变量的值赋给myInt类型的变量,如果需要赋值,这里需要显式转换。
- 使用
_来忽略返回值 - Go支持可变参数,代码为:
func sum(args...int)sum int这就支持0到多个参数,args是slice(切片,可以暂时简单理解为一个长度可变的数组),通过args[index],可以访问到各个值,这里args的值的数量并不固定,可能为0~任意数量 - 支持对函数返回值命名
func getCal(n1 int, n2 int) (sum int , sub int) {
sum = n1 + n2
sub = n1 - n2
return
}
这样就默认返回了sum还有sub两个值
init函数
我们先来bing一下,init是什么意思

initial函数就是初始化函数,每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说,init会在main函数之前被调用。
细节和注意事项
- 如果一个文件同时包含全局变量定义,init函数和main函数,则执行流程是变量定义→init函数→main函数我们来跑一个案例证明一下
- 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这一行的时候,会将后面的语句压入独立的栈中,暂时不执行,当函数执行完毕后,再从这个独立的栈中,按照先入后出的方式,从栈中释放出来,

注意事项:
- 在defer将语句压入栈中时,也会将相应的值拷贝同时入栈,在defer后面对压入变量的值进行变动,并不会影响以及压入栈中变量的值
- defer主要的价值是在函数执行完毕后可以释放函数创建的资源上图也是在golang编程中的常见做法,创建资源后(比如说打开了文件获取了数据库的链接),可以执行defer,在defer后可以继续使用创建的资源,在函数执行完毕后,系统依次从栈中取出语句关闭资源。
包
在实际开发中,我们也会遇到很多问题,比如说A程序员写了一个文件,定义了一个数据筛选函数,B程序员也要用,但是A和B开发时用的不是同一个文件,该怎么办,以及A与B共同开发一个项目时,A和B都想定义一个celsiusToF函数,但是两个函数名重复定义了,又该怎么办。
为了解决这些问题,包应运而生,
包的本质还是一个文件夹,里面存放了程序文件,go每一个文件都属于一个包,也就是说go是以包的形式来管理文件和项目目录结构
包的作用:
- 区分名字相同的函数变量等标识符
- 当程序文件很多的时候,可以很好的管理项目
- 控制函数变量的作用域
基本语法
//打包语法
package util
//引入语法
import "src/../../.."
//调用函数
包名.函数名
我们还是来点例子

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











