函数:
函数是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。它
将复杂的算法过程分解为若干较小任务,隐藏相关细节,使得程序结构更加清晰,易于维护。函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。因此,函数还是代码复用和测试的基本单元。
关键字func用于定义函数
- 函数必须先定义,后调用,定义的过程成为
函数定义,- 函数创建后并不是直接可以运行的,需要手动使用后,才执行,该过程成为
函数调用
Go函数特点:
- 不支持嵌套函数,但可以嵌套匿名函数
- 不支持函数重载
- 不支持默认参数
- 支持可变参数
- 支持多返回值和命名返回值,同时返回值可以是一个函数
- 支持匿名函数和闭包
函数定义:
func 函数名(参数1 数据类型,参数2 数据类型...)(返回值1 返回值类型...){
语句体
return 返回值
}
函数调用:
普通格式调用:
函数名(形参列表)
函数值格式调用:
变量 := 函数名(形参列表)
函数表达式格式调用:
变量 := 类名.函数名(形参列表)
函数调用时,参数的数量与数据类型必须与函数定义中的相匹配,否则程序将报错。函数的返回值通常会使用变量接收,否则该返回值将无意义
package function
import "fmt"
func Function(a int, b int) int { // 定义函数function,两个形参分别是a、b,返回值类型是int
return a + b
}
func main() {
// 普通格式调用
Function(1, 2) // 调用function,传入实参为1,2赋值给a、b
// 函数值格式调用
f := Function(1, 2)
fmt.Println(f)
}
————————————————————————————分界线————————————————————————————
package main
import (
function "go_basics/func"
)
func main() {
// 函数表达式格式调用
function.Function(1, 2)
}
函数只能判断是否为nil,不支持其他比较操作
func main() {
fmt.Println(function01 == nil)
fmt.Println(function01 == function02) // 无效运算: function01 == function02 (在 func() 中未定义运算符 ==)
}
func function01() {}
func function02() {}
函数中的变量是局部的,函数外不生效
func function03() int {
num := 1
return num
}
num // 报错,找不到a
参数:
基本类型和数组默认都是值传递,实参将自己的地址值拷贝一份给形参。
如果参数是引用类型,那么就会改变实参的值
调用时必须按参数顺序传递指定类型的实参,就算以“_”命名也不能忽略。参数列表中,相邻的同数据类型参数可以合并数据类型
func main() {
function04(1, 2, "abc") // 报错;'function04' 调用中的实参不足,给bool变量赋值就可以了,或者删除bool这个变量
}
func function04(x, y int, s string, _ bool) *int {
return nil
}
参数可视作函数的局部变量,因此不能在函数内部再定义同名变量
func function05(x, y int) int {
x := 100 // 错误:':=' 的左侧没有新变量
var y int // 错误:此函数中重新声明了 'y'
return x + y
}
不管是指针、引用类型,还是其他类型参数,都是值拷贝传递,无非是拷贝目标对象,还是拷贝指针自身而已。在函数调用时,会为形参和返回值分配内存空间, 并将实参数据拷贝到形参内存。
func main() {
a := 0x100
p := &a
fmt.Println("p的内存地址:", &p, "\tp的值:", p)
function06(p)
}
func function06(x *int) {
fmt.Println("x的内存地址:", &x, "\tx的值:", x)
}
输出:从输出结果可以看出,尽管实参和形参都指向同一目标,但指针自身依然被复制。
p的内存地址: 0x1400011c018 p的值: 0x14000122008
x的内存地址: 0x1400011c028 x的值: 0x14000122008
如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。
func main() {
num := 20
function07(&num)
fmt.Println("main() n1:", num)
}
func function07(n1 *int) {
*n1 = *n1 + 10
fmt.Println("function07() n1:", *n1)
}
输出:
function07() n1: 30
main() n1: 30
Go不支持函数重载
func function08(n1 int) {}
func function08(n1 int, n2 int) {} // 此包中重新声明的 'function08'
函数也是一种数据类型,可以赋值给一个变量,那么这个变量就是一个函数类型的变量,通过该变量可以对函数调用
func main() {
z := function09 // 直接把函数赋值给一个变量
fmt.Printf("变量z的数据类型为:%T\n,函数function09的数据类型为:%T\n,", z, function09)
num := z(1, 2) // 因为是赋值给变量了,所以可以直接使用变量调用相当于原函数名本身,把返回值赋值给num再输出
fmt.Println("num=", num)
}
func function09(x, y int) int {
return x + y
}
输出:
变量z的数据类型为:func(int, int) int
函数function09的数据类型为:func(int, int) int
num= 3
既然函数是一种数据类型,那么函数也可以作为形参,并且调用
func main() {
num := function10(getSum, 20, 30)
fmt.Println(num) //50
}
func getSum(x, y int) int {
return x + y
}
func function10(funcTest func(int, int) int, num1, num2 int) int {
return funcTest(num1, num2)
}
图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WGjGXlc-1661692826206)(/Users/itzhuzhu/Library/Application Support/typora-user-images/image-20220828171523185.png)]
要实现传出参数,通常建议使用返回值。当然,二级指针也是常用的手法,二级指针和二维数组一样理解就可以了
func main() {
var p *int
function05(&p)
println(*p)
}
func function05(p **int) {
x := 100 * p = &x
}
形参和实参:
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。形参相当于函数局部变量,而实参则是函数外部对象,可以是常量、变量、表达式或函数等。
形参:
形参就是函数定义中的参数,顾名思义就是只有一个形式,没有具体的数值,等同于变量定义格式
例如:var num int 这里的num只是一个形式,num并没有赋值
实参:
函数调用中的参数,顾名思义就是有实际的参数数值,等同于使用变量或常量
例如:var num int =10 这里的num就已经赋值了
传递的形参即使在函数中操作,也不会影响原来的数据值
func main() {
var a = 1
var b = 2
fmt.Printf("main函数中a=%d,b=%d\n", a, b)
function11(a, b)
}
func function11(a, b int) {
a, b = b, a
fmt.Printf("function11函数中a=%d,b=%d", a, b)
}
输出:
main函数中a=1,b=2
function11函数中a=2,b=1
可变参数:
顾名思义函数中参个数是可以变化的,写函数时如果不确定形参长度,可以使用可变参数传递,可变变参本质上就是一个切片,它只能接收相同类型的参数值,
且必须放在参数列表的最后
查看可变参数类型
func main() {
function12("abc", 1, 2, 3, 4)
}
func function12(s string, a ...int) {
fmt.Printf("%T, %v\n", a, a)
}
输出:
[]int, [1 2 3 4]
可变参数的使用:
func main() {
function13(1, 2, 3, 4, 5)
function14(111, 2, 3, 4, 5)
}
func function13(num ...int) {
for i := 0; i < len(num); i++ {
fmt.Println(num[i])
}
}
// 如果可变参数和其它参数一起作为形参时,可变参数需要放在后面
func function14(a int, num ...int) {
for i := 0; i < len(num); i++ {
fmt.Println(num[i])
}
fmt.Println(a)
}
变参是切片,那么参数复制的仅是切片自身,不包括底层数组,因此我们可以修改原数据。
func main() {
a := []int{10, 20, 30}
function15(a...)
fmt.Println(a)
}
func function15(a ...int) {
for i := range a {
a[i] += 100
}
}
输出:
[10 20 30]
函数参数传递的方式:
返回值:
- Go支持多个返回值,如果没有定义返回值,但是写了return,那么就相当于终止了函数,跳出循环的意思
- 返回值不想接收时候可以使用下划线忽略
_- 返回值只有一个时可以不写括号,有多个似必须写括号
没定义返回值但写了return就会终止,return后面的代码是不会执行的
func main() {
function16(1, 2) // 结果为空
}
func function16(x, y int) {
return
z := x + y
fmt.Println(z)
}
函数后面只有返回值类型没有给返回值命名
func main() {
f := function17(1, 2)
fmt.Println(f)
}
func function17(x, y int) int {
sum := x + y
return sum
}
命名返回值
func main() {
f := function18(1, 2)
fmt.Println(f) // 3
}
// 要在参数后面加上返回值和返回值的数据类型,不需要在函数里再定义返回值
func function18(x, y int) (sum int) {
sum = x + y
return // 函数返回值那里已经命名了,在函数中可以省略返回值名,直接return,相当于return sum
}
函数返回多个返回值:
func main() {
x, y := function19()
fmt.Println("x = ", x, "\ty = ", y)
}
func function19() (int, int) {
x := 1
y := 2
// 返回两个变量的值
return x, y
}
有返回值的函数,必须有明确的return终止语句
func function20(x int) int {
if x > 0 {
return -1
} else if x < 0 {
return -1
}
} // 错误:函数结束时缺少返回值
func function21(x int) int {
for {
break
}
} // 错误:函数结束时缺少返回值
特殊的局部变量会被不同层级的同名变量遮蔽,造成潜在错误。好在编译器会自动检查此类状况,只需改为显式return返回即可
func function22(x, y int) (z int) {
{
z := x + y // 新定义的同名局部变量,同名遮蔽。
return // 错误:未使用的变量 'z' (改成 “return z” 即可)
}
return
}
相同类型的多返回值可用作调用实参,或直接返回
func main() {
log(test()) //多返回值直接用作实参。
}
func log(x int, err error) {
fmt.Println(x, err)
}
func test() (int, error) {
return div(5, 0) //多返回值直接用作return结果。
}
func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("error...")
}
return x / y, nil
}
匿名函数:
匿名函数就是没有名字的函数,如果某个函数只使用一次,就可以使用匿名函数,匿名函数也可以实现多次调用。
匿名函数除没有名字外,其他和普通函数完全相同。最大的区别是,我们可在函数内部定义匿名函数,形成类似嵌套函数的效果。匿名函数可直接调用,保存到变量,作为参数或返回值
作用:
- 匿名函数只有在被调用的时候才会开辟空间,执行完毕就会被销毁,可以节省内存
- 减少重名的风险
- 可以实现闭包
格式:
func(形参)(返回值) {
函数体
}(实参) // 在定义的时候就已经传入了参数
无返回值匿名函数
func main() {
func(s string) {
fmt.Println(s)
}("我是实参,上面的s,是形参,我会被打印不")
}
输出:
我是实参,上面的s,是形参,我会被打印不
有返回值匿名函数:把匿名函数赋值给一个变量,再通过变量调用函数
func main() {
num := func(x, y int) int {
return x + y
}
fmt.Println(num(1, 2))
}
全局匿名函数:把匿名函数赋值一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
var num = func(x, y int) int { // 匿名函数把结果赋值给num,然后调用num传入参数就可以了
return x + y
}
func main() {
fmt.Println(num(1, 2))
}
如果定义了匿名函数但是没有使用会报错,在结尾传入参数就可以
func main() {
func(x int) { // 报错:func 已评估但未使用
fmt.Println(x)
}
}
闭包:
- 闭包(closure)是在函数和其引用环境的组合体(匿名函数引用了匿名函数外部的数据,如变量、常量、函数等。)
- 闭包让我们不用传递参数就可读取或修改环境状态,传入一次就可以反复使用
ClosePackage返回的匿名函数会引用匿名函数外部的变量x,这种现象就称作闭包,不管是变量,还是其它数据,只要是匿名函数引用了外部的数据,那么就会称为闭包,因为变量x只初始化一次,所以连续调用时候结果就会累计
func main() {
num := ClosePackage()
fmt.Println(num(1)) // 传入一个值为1,这个1会赋给匿名函数中的y
fmt.Println(num(2))
fmt.Println(num(3))
}
func ClosePackage() func(int) int { // 定义一个函数,无形参,返回值是一个函数
var x int = 1 // 定义一个变量x,初始化为1,准备在匿名函数中使用
return func(y int) int { // ClosePackage函数的返回值是一个匿名函数
x = y + 1 // 在这里使用匿名函数外的变量x
return x
}
}
输出:
2
3
4
通过输出指针,闭包直接引用了原环境变量。所以说闭包是函数和引用环境组合体更加确切
func main() {
f := ClosePackage02(0x100)
f()
}
func ClosePackage02(x int) func() {
println(&x)
return func() {
println(&x, x)
}
}
输出:
0xc82000a100
0xc82000a100 256
闭包应用:
func main() {
f := FileTest(".pdf")
fmt.Println(f("Go语言学习笔记"))
fmt.Println(f("Go语言学习笔记.韩顺平"))
fmt.Println(f(".pdf"))
}
func FileTest(FileName string) func(string) string {
return func(name string) string {
// 判断传入的name开头是否有指定的后缀(FileName),不等于就加上后缀,如果等于就返回name
if !strings.HasPrefix(name, FileName) {
return name + FileName
}
return name
}
}
输出:
Go语言学习笔记.pdf
Go语言学习笔记.韩顺平.pdf
.pdf
内置函数:
len:用来计算长度的,string、arr、slice、map、channel都可以
new:用来分配值内存的,int、float32、struct返回值是指针
make:用来分配引用类型内存的,slice、map、channel
func main() {
num := 100
fmt.Printf("num的类型:%T,num的值:%v,num的内存地址:%v\n", num, num, &num)
num2 := new(int)
*num2 = 100
fmt.Printf("num2的类型:%T,num2的值:%v,num2的内存地址:%v,num2指向地址存储的数据:%v", num2, num2, &num2, *num2)
}
输出:
num的类型:int,num的值:100,num的内存地址:0x1400012c008
num2的类型:*int,num2的值:0x1400012c020,num2的内存地址:0x14000126020,num2指向地址存储的数据:100
直接定义变量的流程是:开辟内存空间 -> 将数据存储到内存空间
适用new定义变量的流程是:开启指针内存空间 -> 指向数据的内存地址
defer:
defer用于向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,它们直到当前函数执行结束前才被执行,常用于资源释放、错误处理等操作
func main() {
defer fmt.Println("第1个defer")
defer fmt.Println("第2个defer")
defer fmt.Println("第3个defer")
fmt.Println("第1个输出")
fmt.Println("第2个输出")
fmt.Println("第3个输出")
}
输出:defer的结果是倒叙的,原因是:进入main函数发现了defer,就把defer抓走放在了一个独立的栈中等待执行(压栈),然后继续执行下面的,直到所有的程序执行完,才执行defer(弹栈),而栈内存是先进后出(就像弹夹一样,先放的子弹是最后才打出去的),所以是先输出了第3个defer
第1个输出
第2个输出
第3个输出
第3个defer
第2个defer
第1个defer
循环处理多个日志文件,defer导致文件关闭时机延长
func main() {
for i := 0; i < 10000; i++ {
path := fmt.Sprintf("./log/%d.txt", i)
f, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
defer f.Close()
// 这个关闭操作在main函数结束时才会执行,而不是当前循环。
// 延长了逻辑结束时间和f生命周期,平白多消耗了内存等资源。
// 编译器也会提醒:可能发生资源泄漏,在 'for' 循环中调用 'defer'
}
}
应该直接调用,或重构为函数,让循环和处理算法分离
func main() {
//日志处理算法。
do := func(n int) {
path := fmt.Sprintf("./log/%d.txt", n)
f, err := os.Open(path)
if err != nil {
log.Println(err)
return
}
defer f.Close() // 该延迟调用在此匿名函数结束时执行,而非main
}
for i := 0; i < 10000; i++ {
do(i)
}
}
init:
init 函数最主要的作用,就是完成一些初始化的工作,每一个源文件都可以包含一个init函数,该函数会在main函数执行前被调用
var name = "itzhuzhu"
var age = 24
func main() {
fmt.Println("main方法执行")
}
func init() {
fmt.Println("init方法执行")
fmt.Println("name=", name, "age=", age)
}
输出:
init方法执行
name= itzhuzhu age= 24
main方法执行
如果一个文件同时包含全局变量定义,init函数和main 函数,则执行的流程全局变量定义 > init > main
var num = test()
func test() int {
fmt.Println("test方法执行")
return 2022
}
func init() {
fmt.Println("init方法执行")
}
func main() {
fmt.Println("main方法执行")
}
输出:
test方法执行
init方法执行
main方法执行
如果 main.go引用了utils.go,但是两个文件都含有定义变量、init、main,执行i的流程是怎么样的?
- 先执行utils.go
- 再执行utils.go下的变量 > init > main
- 再回去执行main.go下的变量 > init > main
如果是mian.go文件中的一个函数引用了utils.go下的函数,则流程是
- 先执行mian.go,然后走到引用utils.go的代码才会进入utils.go文件中执行
递归:
- 递归指的是一个函数在函数体内调用了自己
- 当一个函数执行完毕,或者遇到 return,就会返回给调用者,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁
递归注意事项:
- 递归一定要有出口。否则内存溢出(出口:什么时候不再调用自己)
- 递归虽然有出口,但是递归的次数也不宜过多。否则内存溢出
演示:
func main() {
test(4)
}
func test(n int) {
if n > 2 {
n--
test(n)
}
fmt.Println(n)
}
输出:
2
2
3
递归案例过程分析:
// main调用test,现在N=4
func test(4 int) {
if 4 > 2 {
4--
test(3)
}
fmt.Println(3)
}
func test(3 int) {
if 3 > 2 {
3--
test(2)
}
fmt.Println(2)
}
func test(2 int) {
if 2 > 2 {
不成立,if执行完以后,就会把n的值返回给调用者,会往上面传
}
fmt.Println(2)
}
// 这段代码是在栈中完成的,栈的特点是先进后出,所以打印的结果是2、2、3
斐波那契数
给你一个整数n,请使用递归的方式,求出它的斐波那契数是多少?
斐波那契数:1,1,2,3,5,8,13…,从第三个数开始是前两个的和
func main() {
res := test(6)
fmt.Println(res)
}
func test(n int) (result int) {
if n == 1 || n == 2 {
return 1
} else {
return test(n-1) + test(n-2)
}
}
递归求阶乘:
var s = 1
func main() {
recursion(5)
fmt.Println(s)
}
func recursion(num int) {
if num == 1 {
return // 终止函数的意思
}
s *= num
recursion(num - 1)
}