Go学习第五章——函数与包
- 1 函数
- 1.1 基本语法
- 1.2 函数多返回值
- 1.3 函数的可见性和包级函数
- 1.4 函数调用机制底层原理
- 1.5 值类型和引用类型
- 1.6 注意事项和细节
- 1.7 逃逸机制(补,可不看)
- 2 包
- 2.1 快速入门
- 2.2 包的使用细节
- 3 函数详细讲解
- 3.1 递归调用
- 3.2 可变函数参数
- 3.3 init 函数
- 3.4 匿名函数
- 3.5 闭包
- 3.6 函数作为参数和返回值
- 3.7 defer函数
- 3.8 作为结构体的方法
- 4 变量的作用域
1 函数
函数是一段可以重复执行的代码块,通过函数可以将代码模块化,提高代码的可读性和可维护性。
要定义函数,需要指定函数的名称、参数和返回值(如果有的话)。
1.1 基本语法
基本语法
func 函数名(形参列表)(返回值类型列表){
执行语句..
return + 返回值列表
}
下面是一个简单的示例,展示了如何定义和调用一个简单的函数:
package main
import "fmt"
// 定义一个名为greeting的函数,它接收一个字符串参数name并没有返回值
func greeting(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
// 调用greeting函数,传入一个名字作为参数
greeting("Alice")
}
运行上面的代码,输出结果为:
Hello, Alice!
1.2 函数多返回值
在 Go 语言中,函数可以返回多个值。这在某些情况下很有用,例如一个函数需要返回多个计算结果,或者需要返回一个值和一个错误状态。
下面是一个示例,展示了如何定义和使用返回多个值的函数:
package main
import "fmt"
// 定义一个名为divide的函数,它接收两个整数参数,并返回一个商和余数
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
func main() {
// 调用divide函数,并接收两个返回值
q, r := divide(10, 3)
fmt.Printf("商:%d,余数:%d\n", q, r)
}
运行上面的代码,输出结果为:
商:3,余数:1
通过返回多个值,函数的调用方可以方便地获得函数计算的多个结果。
1.3 函数的可见性和包级函数
在 Go 语言中,函数和变量的可见性是由它们的命名规则决定的。一个函数或变量是否对其他代码可见,取决于它们的名称是否以大写字母开头。
如果一个函数或变量的名称以大写字母开头,则它对其他代码可见;如果名称以小写字母开头,则它只对同一个包内的代码可见。
下面是一个示例,展示了可见性的规则:
package main
import "fmt"
// 可以被其他代码访问
func PublicFunc() {
fmt.Println("公有函数")
}
// 只能在当前包内访问
func privateFunc() {
fmt.Println("私有函数")
}
func main() {
PublicFunc()
privateFunc() // 错误:无法访问私有函数
}
在这个示例中,我们定义了一个名为 PublicFunc
的公有函数,以及一个名为 privateFunc
的私有函数。在 main
函数中,我们可以正常调用 PublicFunc
,但无法调用 privateFunc
。
按照这个规则,我们可以将一些公共的、被其他代码调用的函数定义为包级函数,并将一些内部函数定义为私有函数。这有助于将代码逻辑与实现细节隔离,并提高代码的封装性。
1.4 函数调用机制底层原理
- 执行
n1 := 10
,会生成一个存储这个值的区域,这里只是抽象为有这么一个main栈区,实际上不是这样命名,是使用寄存器和栈帧来实现,具体不讲解 - 因为是栈的方式,后进先出,所以这里调用函数后占用的数据,也会被优先回收掉。
函数调用是计算机程序中的一个重要概念,它用于在程序执行过程中跳转到函数代码的起始位置,并在函数执行完毕后返回到原来的位置。函数调用的底层实现涉及到栈的分配、参数传递和返回值处理等过程。
-
栈空间的分配:每个线程都会有自己的栈空间,用于存储函数的局部变量、函数参数和返回值。函数调用时,会给调用栈分配一块空间来存储函数执行过程中所需的数据。栈的分配是一个
后进先出
(LIFO)的过程,即新的函数调用会在栈的顶部分配空间。 -
参数传递:函数调用需要将参数传递给被调用的函数。参数的传递方式一般分为两种:
值传递和引用传递
。
-
在值传递中,参数的值会被复制到被调用函数的栈帧中,由于是复制操作,被调函数的修改不会影响到调用函数。
-
在引用传递中,函数参数是一个指针,传递的是变量在内存中的地址,被调用函数可以通过指针来访问和修改原始数据。
-
函数调用过程:当一个函数需要调用另一个函数时,会先将当前函数的执行状态压入栈中,包括返回地址、参数、局部变量等信息。然后跳转到被调用函数的起始位置,执行被调用函数的代码。在被调用函数执行结束后,会将返回值返回给调用函数,在栈中恢复调用函数的执行状态,包括返回地址和栈帧等信息,然后继续执行。
-
返回值处理:函数执行完毕后,需要将返回值返回给调用函数。返回值的处理方式和参数传递类似,可以使用值传递或者引用传递。在实际的底层实现中,一般通过寄存器或者栈帧来传递和存储返回值。
总结起来,函数调用的底层实现主要涉及栈空间的分配与释放、参数传递和返回值处理等过程。这些过程是通过寄存器和栈帧来实现的,不同的编程语言和编译器可能会有一些细节上的差异,但基本思想是相通的。
1.5 值类型和引用类型
前面使用的值传输,所以会发现函数并没有修改值,只是修改函数本身的栈空间里的变量值,当然还有相对应,可以直接通过引用类型传递,修改对应的值。
使用传输地址,也就是引用传递的方式,让函数直接修改地址值。
import "fmt"
func add1(num int) {
num = num + 1
fmt.Println("在add1函数里,num=", num)
}
func add2(num *int) {
*num = *num + 1
fmt.Println("在add2函数里,num=", *num)
}
func main() {
var num int = 2
add1(num)
fmt.Println("在main函数里,第一次,num=", num)
add2(&num)
fmt.Println("在main函数里,第二次,num=", num)
}
输出结果:
在add1函数里,num= 3
在main函数里,第一次,num= 2
在add2函数里,num= 3
在main函数里,第二次,num= 3
1.6 注意事项和细节
- golang不支持重载(通过其他方式实现)
- 在Go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
- 函数既然是一种数据类型,因此在Go中,函数可以作为
形参
,并且调用。 - 为了简化数据类型定义,Go支持自定义数据类型
- 基本语法:
type 自定义数据类型名 数据类型
// 理解:相当于一个别名 - 案例:type myInt int // 这是myInt就等价int来使用了
- 案例:type mySum func(int, int) int // 这是mySum就等价一个函数类型func (int, int) int
import "fmt"
type myFunType func(int, int) int
func myFun(funvar myFunType, num1 int, num2 int) int {
return funvar(num1, num2)
}
func main() {
type myInt int
var num1 myInt = 40
// num2 := int(num1) // 报错,因为myInt本质是,不是一个数据类型
fmt.Println("num1=", num1)
res := myFun(func(i1 int, i2 int) int { // 这里用到了匿名函数,可以去看看后面所说的定义
return i1 + i2
}, 500, 600)
fmt.Println("res=", res)
}
输出:
num1= 40
res= 1100
- 支持对函数返回值命名
cal1和cal2的函数是一样的,只是cal2里对返回值命名,这样就可以不用考虑顺序,因为如果是cal1的方式,return的值需要考虑跟返回值类型的顺序一样。
import (
"fmt"
"strconv"
)
func cal1(n1 int, n2 int) (int, string) {
sum := n1 + n2
sub := strconv.FormatInt(int64(n1-n2), 10)
return sum, sub
}
func cal2(n1 int, n2 int) (sum int, sub string) {
sub = strconv.FormatInt(int64(n1-n2), 10)
sum = n1 + n2
return
}
func main() {
sum1, sub1 := cal1(2, 2)
sum2, sub2 := cal2(2, 2)
fmt.Printf("cal1的返回值:%v, %v \n", sum1, sub1)
fmt.Printf("cal2的返回值:%v, %v \n", sum2, sub2)
}
输出结果:
cal1的返回值:4, 0
cal2的返回值:4, 0
- 使用_标识符,忽略返回值
import "fmt"
func cal(n1 int, n2 int) (sum int, sub int) {
sum = n1 + n2
sub = n1 - n2
return
}
func main() {
res1, _ := cal(10, 20)
fmt.Printf("res1 = %d", res1)
}
输出:res1 = 30
1.7 逃逸机制(补,可不看)
逃逸分析是编译器的一种静态分析技术,用于分析程序中的变量是否会逃逸到堆上分配内存。逃逸指的是当一个变量在函数内部分配内存,并且在函数外部被引用时,该变量就会逃逸到堆上。
逃逸分析的作用是优化内存分配和回收,将一部分变量从堆上分配转移到栈上分配,减少堆的压力和垃圾回收的负担。逃逸分析可以减少内存分配的次数,避免频繁的系统调用和锁竞争,提高程序的性能和并发能力。
逃逸分析的实现原理可以分为以下几个步骤:
-
内联优化:逃逸分析通常是在函数级别上进行的,首先编译器会尝试内联函数。内联优化是将函数的代码插入到调用它的函数中,减少函数调用的开销。内联优化会扩展函数的作用域,使函数内部的变量和参数可以直接访问。这样,一些局部变量就可以在栈上分配,而不是在堆上分配。
-
变量分析:逃逸分析会对函数的变量进行分析,判断变量是否逃逸。如果一个变量逃逸,编译器会将其分配在堆上;如果一个变量不逃逸,则可以将其分配在栈上。变量的逃逸分析包括以下情况的判断:
- 变量是否在函数返回后继续存在;
- 变量是否被存储到全局变量中,以供其他函数使用;
- 变量是否被闭包函数引用。
-
逃逸分析结果的使用:逃逸分析的结果会被编译器用于指导内存分配器进行内存分配。根据逃逸分析的结果,编译器可以决定将变量分配在栈上还是堆上。对于不逃逸的变量,编译器可以直接在栈上分配内存,避免了堆分配和垃圾回收的开销。
总结起来,逃逸分析是一种优化技术,它使用静态分析的方法判断变量是否会逃逸到堆上分配内存。逃逸分析的作用是减少堆的压力和垃圾回收的负担,提高程序的性能和并发能力。逃逸分析的实现原理包括内联优化、变量分析和逃逸分析结果的使用。
2 包
go的每一个文件都是属于一个包,也就是说go是以包的形式来管理文件和项目目录结构。
2.1 快速入门
包的三大作用:
- 区分相同名字的很熟、变量等标识符
- 当程序文件很多时,可以很好的管理项目
- 控制函数、变量等访问范围,即作用域
首先创建不同文件夹下面的包,再编写好对应的函数,然后import导入,最后调用!
2.2 包的使用细节
-
在给一个文件夹打包时,该包对应一个文件夹,比如这里的是utils文件夹对应的包名就是utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。
-
当包名过长时,可以给包名取别名,取完之后,之前的就不能用了
在import时,在前面写的代码,就是别名。例如:这里把utils改成util。
package main
import (
util "GoStudy_Day1/Day03/model/utils"
"fmt"
)
func main() {
var num int = 2
nums := util.Cal(num)
fmt.Printf("%v 的平方等于:%v \n", num, nums)
}
- 编译一个可执行程序文件,就需要将这个包声明为
main
,然后实际开发的时候,都是编译成exe文件再运行。
编译可以指定名字和目录,比如:放在bin目录下:go build -o bin/my.exe go_code/project/main
3 函数详细讲解
3.1 递归调用
在函数里,又调用了本身,也就是自己
下面是一个案例和底层栈空间的调用情况
注意细节:
- 执行一个函数时,就会串接一个新的受保护的独立空间(新函数栈,如图所示)
- 函数的局部变量是独立的,不会相互影响
- 递归必须向退出递归的条件逼近,否则就是无限递归,很容易栈溢出崩溃,实际开发不怎么用
- 当一个函数执行完毕或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时函数执行完毕就销毁。
3.2 可变函数参数
可变参数函数是指可以接收不定数量参数的函数,这些参数被看作是一个切片。
Go 语言中的可变参数函数使用 ...
表示。
下面是一个示例,展示了如何定义和调用可变参数函数:
package main
import "fmt"
// 定义一个名为sum的函数,它接收任意数量的整数参数,并返回它们的总和
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func main() {
// 调用sum函数,传入多个整数参数
fmt.Println("总和:", sum(1, 2, 3, 4, 5))
fmt.Println("总和:", sum(10, 20, 30))
}
运行上面的代码,输出结果为:
总和: 15
总和: 60
在这个示例中,我们定义了一个名为 sum
的函数,它可以接收任意数量的整数参数,并返回它们的总和。在 main
函数中,我们调用了 sum
函数两次,并传递了不同数量的参数。
通过使用可变参数,我们可以简化函数的调用,使之更加灵活。
3.3 init 函数
基本介绍:
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用。
案例说明:
import "fmt"
func init() {
fmt.Println("main init...")
}
func main() {
fmt.Println("main...")
}
输出结果:
main init...
main...
细节讨论:
- 如果一个文件同时包含
全局变量定义
,init函数和main函数,则执行的流程是变量定义->init函数->main函数 - init函数最主要的作用,就是完成一些初始化的工作,比如下面的案例:
utils包:
package utils
var Name string
var Age int
func init() {
Name = "Tom"
Age = 100
}
main包
import (
"GoStudy_Day1/Day03/model/utils"
"fmt"
)
func main() {
fmt.Printf("Name = %v, Age = %v", utils.Name, utils.Age)
}
输出结果:Name = Tom, Age = 100
从过程可以看出,有点像Java定义成员变量,并且给一个默认值的感觉
3.4 匿名函数
匿名函数是一种特殊的函数,它没有函数名,可以直接在其他函数中定义和使用。匿名函数在需要临时定义一段代码,并且这段代码不需要复用时很有用。
下面是示例,展示了如何定义和调用匿名函数:
实例一:
package main
import "fmt"
func main() {
// 在main函数内定义一个匿名函数,并立即调用它
func() {
fmt.Println("这是一个匿名函数!")
}()
// 将匿名函数赋值给变量,然后进行调用
greeting := func(name string) {
fmt.Printf("Hello, %s!\n", name)
}
greeting("Alice")
}
运行上面的代码,输出结果为:
这是一个匿名函数!
Hello, Alice!
实例二:将匿名函数赋给a变量
func main() {
a := func(n1 int, n2 int) int {
return n1 - n2
}
res2 := a(10, 10)
fmt.Println("res2=", res2)
}
输出结果:res2= 0
实例三:匿名函数赋值给全局变量,那么就成为一个全局匿名函数
这里不写了,跟上面雷同~~~
匿名函数可以用于需要临时定义一段代码的场景,例如在并发编程中,可以将匿名函数传递给协程进行并发执行。
3.5 闭包
闭包是指一个函数捕获并保存了其自身外部作用域的变量的引用。
简单来说,闭包就是一个函数以及它所引用的变量的组合体。
下面是一个示例,展示了如何使用闭包:
package main
import "fmt"
// 定义一个名为counter的函数,返回一个匿名函数
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
// 创建一个计数器实例
c := counter()
// 使用闭包进行计数
fmt.Println(c()) // 输出:1
fmt.Println(c()) // 输出:2
fmt.Println(c()) // 输出:3
}
运行上面的代码,输出结果为:
1
2
3
在这个示例中,我们定义了一个 counter
函数,它返回一个匿名函数。在匿名函数内部,我们定义了一个变量 count
,然后在每次调用匿名函数时更新这个变量,并返回它的值。
最佳实践
假设传入一个文件名
,设置一个变量作为文件名的后缀,如果这个文件名没有指定的后缀,则自动给这个文件名添加后缀,如果有的话,直接输出。
这里需要使用HasSuffix函数,表示查找该该string有没有指定的后缀
func makeSuffix(suffix string) func(string) string {
return func(name string) string {
// 如果 name 没有指定后缀,则加上,否则就返回原来的名字
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
f := makeSuffix(".jpg")
fmt.Println("文件名处理后=", f("winter"))
fmt.Println("文件名处理后=", f("winter.jpg"))
}
输出结果:
文件名处理后= winter.jpg
文件名处理后= winter.jpg
虽然也可以用普通函数实现,但是过程太过复杂,所以不用。
通过闭包,我们可以创建一个状态被隐藏的函数,这个函数可以持续地访问和修改它所引用的变量。
ps:闭包还能通过这方式实现协程之间的数据传递,不过需要避免变量共享问题。(以后再讲~~)
3.6 函数作为参数和返回值
在 Go 语言中,函数可以作为参数传递给其他函数,也可以作为函数的返回值。这种能力使得代码更加灵活,可以根据需要将函数与其他函数进行组合。
下面是一个示例,展示了如何将函数作为参数和返回值:
package main
import "fmt"
// 定义一个名为apply的函数,它接收一个函数作为参数,并将参数函数应用到数字5上
func apply(f func(int, int) int) {
result := f(5, 10)
fmt.Println("应用结果:", result)
}
// 定义一个名为add的函数,它接收两个整数并返回它们的和
func add(a, b int) int {
return a + b
}
func main() {
// 将add函数作为参数传递给apply函数
apply(add)
// 将匿名函数作为参数传递给apply函数
apply(func(a, b int) int {
return a * b
})
}
运行上面的代码,输出结果为:
应用结果: 15
应用结果: 50
在这个示例中,我们定义了一个 apply
函数,它接收一个函数作为参数,并将这个函数应用到数字5和10上。在 main
函数中,我们分别将 add
函数和一个匿名函数作为参数传递给 apply
函数,用于实现加法和乘法操作。
通过将函数作为参数和返回值,我们可以更灵活地组合和使用函数,实现更多复杂的功能。
3.7 defer函数
为什么需要defer
在函数中,程序员进程需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)。
在 Go 语言中,我们可以使用 defer
关键字延迟执行一些代码,无论外部函数执行的怎样,这些代码都会在函数返回之前被执行。
下面是一个示例,展示了 defer
的使用场景:
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=", res)
}
运行上面的代码,输出结果为:
ok3 res= 30
ok2 n2= 20
ok1 n1= 10
res= 30
当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,当函数执行完毕之后,再从defer栈中,一次从栈顶取出语句执行,所以从上面的案例就可以看出,先执行的是ok2语句。
3.8 作为结构体的方法
在 Go 语言中,方法是一种与特定类型关联的函数。它们可以通过定义在类型上的方法来实现某些特定操作。
下面是一个示例,展示了如何定义和使用方法:
package main
import (
"fmt"
"math"
)
// 定义一个名为Circle的结构体类型
type Circle struct {
radius float64
}
// 在Circle类型上定义一个名为area的方法,它返回这个圆的面积
func (c Circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
// 创建一个Circle实例
c := Circle{radius: 5}
// 调用Circle类型的方法
fmt.Println("圆的面积:", c.area())
}
运行上面的代码,输出结果为:
圆的面积: 78.53981633974483
在这个示例中,我们定义了一个名为 Circle
的结构体类型,它包含一个半径属性。然后,在 Circle
类型上定义了一个名为 area
的方法,它用于计算圆的面积。
在 main
函数中,我们创建了一个 Circle
实例,并调用了 Circle
类型的 area
方法来计算面积。
通过方法,我们可以将某些操作与特定类型绑定,使得代码更加清晰和面向对象。
4 变量的作用域
-
函数内部声明/定义的遍历叫局部变量,作用域仅限于函数内部。
func test() { // age 和 name 的作用域就只在test函数内部 age := 10 Name := "Tom~" } func main() { }
-
函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。
//函数外部声明/定义的变量叫全局变量, //作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效 var age int = 50 var Name string = "jack~" //函数 func test() { //age 和 Name的作用域就只在test函数内部 age := 10 Name := "tom~" fmt.Println("age=", age) // 10 fmt.Println("Name=", Name) // tom~ } func main() { fmt.Println("age=", age) // 50 fmt.Println("Name=", Name) // jack~ test() }
-
如果变量是在一个代码块,比如 for / if中,那么这个变量的作用域就在该代码块。
package main
import (
"fmt"
)
func main() {
//如果变量是在一个代码块,比如 for / if中,那么这个变量的的作用域就在该代码块
for i := 0; i <= 10; i++ {
fmt.Println("i=", i)
}
var i int //局部变量
for i = 0; i <= 10; i++ {
fmt.Println("i=", i)
}
fmt.Println("i=", i)
}
Over~~~~结束啦!!!!冲冲冲!!!