青州网站建设 258,教学网站系统流程图,优化落实防控措施,拼多多海外跨境电商入驻流程Go学习第五章——函数与包 1 函数1.1 基本语法1.2 函数多返回值1.3 函数的可见性和包级函数1.4 函数调用机制底层原理1.5 值类型和引用类型1.6 注意事项和细节1.7 逃逸机制#xff08;补#xff0c;可不看#xff09; 2 包2.1 快速入门2.2 包的使用细节 3 函数详细讲解3.1 递… 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 mainimport fmt// 定义一个名为greeting的函数它接收一个字符串参数name并没有返回值
func greeting(name string) {fmt.Printf(Hello, %s!\n, name)
}func main() {// 调用greeting函数传入一个名字作为参数greeting(Alice)
}运行上面的代码输出结果为
Hello, Alice!1.2 函数多返回值
在 Go 语言中函数可以返回多个值。这在某些情况下很有用例如一个函数需要返回多个计算结果或者需要返回一个值和一个错误状态。
下面是一个示例展示了如何定义和使用返回多个值的函数
package mainimport fmt// 定义一个名为divide的函数它接收两个整数参数并返回一个商和余数
func divide(a, b int) (int, int) {quotient : a / bremainder : a % breturn quotient, remainder
}func main() {// 调用divide函数并接收两个返回值q, r : divide(10, 3)fmt.Printf(商%d余数%d\n, q, r)
}运行上面的代码输出结果为
商3余数1通过返回多个值函数的调用方可以方便地获得函数计算的多个结果。
1.3 函数的可见性和包级函数
在 Go 语言中函数和变量的可见性是由它们的命名规则决定的。一个函数或变量是否对其他代码可见取决于它们的名称是否以大写字母开头。
如果一个函数或变量的名称以大写字母开头则它对其他代码可见如果名称以小写字母开头则它只对同一个包内的代码可见。
下面是一个示例展示了可见性的规则
package mainimport 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 fmtfunc add1(num int) {num num 1fmt.Println(在add1函数里num, num)
}func add2(num *int) {*num *num 1fmt.Println(在add2函数里num, *num)
}func main() {var num int 2add1(num)fmt.Println(在main函数里,第一次num, num)add2(num)fmt.Println(在main函数里,第二次num, num)
}
输出结果
在add1函数里num 3
在main函数里,第一次num 2
在add2函数里num 3
在main函数里,第二次num 31.6 注意事项和细节
golang不支持重载通过其他方式实现在Go中函数也是一种数据类型可以赋值给一个变量则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。函数既然是一种数据类型因此在Go中函数可以作为形参并且调用。为了简化数据类型定义Go支持自定义数据类型
基本语法type 自定义数据类型名 数据类型 // 理解相当于一个别名案例type myInt int // 这是myInt就等价int来使用了案例type mySum func(int, int) int // 这是mySum就等价一个函数类型func (int, int) int
import fmttype myFunType func(int, int) intfunc myFun(funvar myFunType, num1 int, num2 int) int {return funvar(num1, num2)
}func main() {type myInt intvar 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 (fmtstrconv
)func cal1(n1 int, n2 int) (int, string) {sum : n1 n2sub : 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 n2return
}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 fmtfunc cal(n1 int, n2 int) (sum int, sub int) {sum n1 n2sub n1 - n2return
}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 mainimport (util GoStudy_Day1/Day03/model/utilsfmt
)func main() {var num int 2nums : 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 mainimport fmt// 定义一个名为sum的函数它接收任意数量的整数参数并返回它们的总和
func sum(nums ...int) int {total : 0for _, 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 fmtfunc init() {fmt.Println(main init...)
}func main() {fmt.Println(main...)
}输出结果
main init...
main... 细节讨论
如果一个文件同时包含全局变量定义init函数和main函数则执行的流程是变量定义-init函数-main函数init函数最主要的作用就是完成一些初始化的工作比如下面的案例
utils包
package utilsvar Name string
var Age intfunc init() {Name TomAge 100
}main包
import (GoStudy_Day1/Day03/model/utilsfmt
)func main() {fmt.Printf(Name %v, Age %v, utils.Name, utils.Age)
}输出结果Name Tom, Age 100
从过程可以看出有点像Java定义成员变量并且给一个默认值的感觉
3.4 匿名函数
匿名函数是一种特殊的函数它没有函数名可以直接在其他函数中定义和使用。匿名函数在需要临时定义一段代码并且这段代码不需要复用时很有用。
下面是示例展示了如何定义和调用匿名函数
实例一
package mainimport fmtfunc 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 mainimport fmt// 定义一个名为counter的函数返回一个匿名函数
func counter() func() int {count : 0return func() int {countreturn count}
}func main() {// 创建一个计数器实例c : counter()// 使用闭包进行计数fmt.Println(c()) // 输出1fmt.Println(c()) // 输出2fmt.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 mainimport 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 fmtfunc sum(n1 int, n2 int) int {defer fmt.Println(ok1 n1, n1)defer fmt.Println(ok2 n2, n2)res : n1 n2fmt.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 mainimport (fmtmath
)// 定义一个名为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 : 10Name : Tom~
}func main() {
}函数外部声明/定义的变量叫全局变量作用域在整个包都有效如果其首字母为大写则作用域在整个程序有效。 //函数外部声明/定义的变量叫全局变量
//作用域在整个包都有效如果其首字母为大写则作用域在整个程序有效
var age int 50
var Name string jack~//函数
func test() {//age 和 Name的作用域就只在test函数内部age : 10Name : tom~fmt.Println(age, age) // 10fmt.Println(Name, Name) // tom~
}func main() {fmt.Println(age, age) // 50fmt.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~~~~结束啦冲冲冲