文章目录
函数基本语法参数传递方式返回值特点可变参数函数类型匿名函数defer机制闭包 包基本概念包的使用init函数
函数
基本语法
基本语法
Go中函数的基本语法如下:
使用案例如下:
package mainimport "fmt"// 函数定义func Factorial(num int) int {result := 1for i := 2; i <= num; i++ {result *= i}return result}func main() {result := Factorial(10) // 函数调用fmt.Printf("result = %d\n", result) // result = 3628800}
注意: Go中的函数不支持函数重载。
参数传递方式
参数传递方式
参数传递的方式有两种:
值传递:传参时传递的是值的拷贝,函数内部对参数的修改不会影响到原始数据,值类型参数默认采用的就是值传递,包括基本数据类型、数组和结构体。引用传递:传参时传递的是地址的拷贝,在函数内部对参数的修改会影响到原始数据,引用类型参数默认采用引用传递,包括指针、切片、管道、接口等。值传递案例如下:
package mainimport "fmt"func ModifyNum(num int) { // 值传递num = 20fmt.Printf("num = %d\n", num) // num = 20}func main() {var num int = 10ModifyNum(num)fmt.Printf("num = %d\n", num) // num = 10(原始数据未被影响)}
对于值类型参数,如果希望函数内部对参数的修改能影响到原始数据,可以传入值类型变量的地址,然后在函数内部通过指针的方式操作变量。如下:
package mainimport "fmt"func ModifyNum(num *int) { // 引用传递*num = 20fmt.Printf("num = %d\n", *num) // num = 20}func main() {var num int = 10ModifyNum(&num)fmt.Printf("num = %d\n", num) // num = 20(原始数据被影响)}
返回值特点
返回多个值
Go中函数支持返回多个值,通过返回值列表指明各个返回值的类型即可。如下:
package mainimport "fmt"func GetSumAndSub(num1 int, num2 int) (int, int) { // 返回多个值sum := num1 + num2sub := num1 - num2return sum, sub}func main() {sum, sub := GetSumAndSub(10, 20)fmt.Printf("sum = %d\n", sum) // sum = 30fmt.Printf("sub = %d\n", sub) // sub = -10}
说明一下:
如果只有一个返回值,那么返回值列表可以不用()
包裹。 忽略返回值
如果函数返回多个值,在接收时,可以通过_
(占位符)忽略不需要的返回值。如下:
package mainimport "fmt"func GetSumAndSub(num1 int, num2 int) (int, int) { // 返回多个值sum := num1 + num2sub := num1 - num2return sum, sub}func main() {_, sub := GetSumAndSub(10, 20)fmt.Printf("sub = %d\n", sub) // sub = -10}
返回值命名
Go中函数支持在返回值列表给返回值命名,这时函数在返回时无需在return后指明需要返回的值,可以避免返回顺序出错。如下:
func GetSumAndSub(num1 int, num2 int) (sum int, sub int) { // 返回多个值sum = num1 + num2sub = num1 - num2return}
可变参数
可变参数
Go中函数支持可变参数,只需在对应形参类型的前面加上...
即可将其设置为可变参数,可变参数支持传入0个或多个参数。如下:
package mainimport "fmt"func Sum(nums ...int) (sum int) { // 可变参数fmt.Printf("nums type = %T\n", nums) // nums type = []intfor i := 0; i < len(nums); i++ {sum += nums[i]}return}func main() {fmt.Printf("result1 = %d\n", Sum()) // result1 = 0fmt.Printf("result2 = %d\n", Sum(10, 20)) // result2 = 30fmt.Printf("result3 = %d\n", Sum(10, 20, 30)) // result3 = 60}
说明一下:
可变参数的类型本质是对应的切片类型,可以通过形参名[下标]
的方式访问到切片中的各个值。len是Go中的内建函数,可以用于获取切片的长度,即切片中的元素个数。如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表的最后。 函数类型
函数类型
在Go中函数也是一种数据类型,可以将其赋值给一个变量,然后通过该变量即可对函数进行调用。如下:
package mainimport "fmt"func Sum(num1 int, num2 int) int {return num1 + num2}func main() {sumFunc := Sumfmt.Printf("sumFunc type = %T\n", sumFunc) // sumFunc type = func(int, int) intfmt.Printf("sum = %d\n", sumFunc(10, 20)) // sum = 30}
自定义数据类型
Go中自定义数据类型的基本语法如下:
使用案例如下:
package mainimport "fmt"func main() {type MyInt int // 自定义数据类型var num1 MyInt = 10fmt.Printf("num1 type = %T\n", num1) // num1 type = main.MyIntvar num2 int = int(num1)fmt.Printf("num2 type = %T\n", num2) // num2 type = int}
说明一下:
在上述代码中,虽然int和MyInt本质都是int类型,但是编译器认为这是两个不同的类型,在相互赋值时需要进行类型转换,无法直接进行赋值。在Go中将函数作为形参也是常见的用法,这时结合自定义数据类型给函数类型取别名,能有效提高代码的可读性。如下:
package mainimport "fmt"type SumType func(int, int) int // 自定义数据类型func MyFunc(f SumType, num1 int, num2 int) int {return f(num1, num2)}func main() {result := MyFunc(Sum, 10, 20)fmt.Printf("result = %d\n", result) // result = 30}
匿名函数
匿名函数
Go中支持匿名函数,如果希望某个函数只使用一次,可以考虑使用匿名函数。如下:
package mainimport "fmt"func main() {// 匿名函数result := func(num1 int, num2 int) int { // 定义匿名函数并调用return num1 + num2}(10, 20)fmt.Printf("result = %d\n", result) // result = 30}
在定义匿名函数时如果将其赋值给一个变量,后续再通过该变量来调用匿名函数,就能实现匿名函数的多次调用。如下:
package mainimport "fmt"func main() {// 匿名函数f := func(num1 int, num2 int) int { // 定义匿名函数return num1 + num2}fmt.Printf("result = %d\n", f(100, 200)) // result = 300}
defer机制
defer机制
defer相关介绍:
在函数中,经常需要创建资源(网络连接、文件句柄、锁、数据库连接等),这些资源在使用完后需要及时释放,否则会造成资源泄露,因此Go设计者提供了defer(延时机制)。当Go执行到defer时,不会立即执行defer后的语句,而是将defer后的语句压入到defer栈中,并继续执行后续的语句。当函数执行完毕后,会从defer栈的栈顶依次取出语句执行,这意味着defer语句的执行顺序与压入defer栈的顺序相反。下面是一个经典的defer题目,请问程序的输出结果是什么。如下:
package mainimport "fmt"func DeferFunc(num1 int, num2 int) {defer fmt.Printf("defer num1 = %d\n", num1)defer fmt.Printf("defer num2 = %d\n", num2)num1++num2++fmt.Printf("num1 = %d\n", num1)fmt.Printf("num2 = %d\n", num2)}func main() {DeferFunc(10, 20)fmt.Println("main code...")}
程序运行结果如下:
说明一下:
由于defer语句会在当前函数执行完毕后再执行,因此最先看到是第10和11行代码的输出结果。由于defer语句的执行顺序与压入defer栈的顺序相反,因此先看到的是输出num2的defer语句,然后再是输出num1的defer语句,在defer语句输出完后才看到主函数后续的输出结果。需要特别注意的是,defer语句压栈时会将语句中用到的相关变量的值也拷贝入栈,因此虽然defer语句是在修改num1和num2之后执行的,但输出的却是它们修改之前的值。defer使用案例
在Go中通常会在创建资源后,通过defer语句将资源关闭,由于defer语句会在当前函数执行完毕后再执行,因此在defer语句之后仍然可以使用创建的资源。在其他语言中,资源释放时机是一个常见的问题,而Go中的defer机制就使得资源的创建和释放可以成对存在,程序员再也不用担心资源释放时机的问题了。下面是一个对文件操作的函数,其中利用defer机制对文件资源进行了延时释放,在函数执行完毕后文件会自动关闭。如下:
func FileOperation(filename string) (err error) {file, err := os.Open(filename) // 打开文件if err != nil {fmt.Printf("open file error, err = %v\n", err)return}defer file.Close() // 关闭文件// 进行文件操作...return}
闭包
闭包
闭包相关介绍:
闭包是一种函数值,它可以访问并持有定义在其外部作用域的变量,闭包是由函数与其相关的引用环境组合而成的一个整体。闭包可以获取并存储其所在作用域的变量,并在函数内部引用这些变量,即使在其定义的作用域已经不存在或不可访问时仍然有效。使用案例如下:
package mainimport "fmt"// 创建累加器func CreateCounter(base int) func(int) int {var count = basereturn func(n int) int { // 返回闭包count += nreturn count}}func main() {// 闭包f := CreateCounter(10)fmt.Printf("count = %d\n", f(1)) // count = 11fmt.Printf("count = %d\n", f(3)) // count = 14fmt.Printf("count = %d\n", f(5)) // count = 19}
说明一下:
代码中的CreateCounter函数用于创建一个累加器,在创建时可以指明累加器的起始累加值。函数返回了一个匿名函数,在匿名函数中引用了定义在其外部作用域的base变量(引用环境),因此这个匿名函数和base变量构成了闭包。累加器创建后,每次调用闭包都会在原来的基础上进行累加,因为count变量是在调用CreateCounter时进行的声明和初始化,后续调用闭包时使用的都是同一个count变量。闭包的优势
闭包的优势如下:
封装状态: 闭包允许将变量封装在函数内部,并使其在函数执行时保持状态,这样可以因此内部状态,并提供一个清晰的接口来操作和访问这些状态,使得状态的管理更加灵活和可控。数据持久化: 闭包可以使函数在其定义的作用域之外继续存在,让函数可以捕获和持有外部作用域中的变量,这为在函数调用之间保持数据的持久性和连续性提供了一种简单的方法,并使得函数可以记住先前的状态,并在后续调用中使用。实现函数工厂: 闭包可以用于创建函数工厂,即根据不同的配置或参数创建并返回不同的函数。通过捕获外部变量,闭包可以在每次创建函数时自定义函数的行为和逻辑,这种模式可以用于生成具有一致接口但具有不同行为的函数实例。代码简洁性: 闭包可以减少代码的冗余,特别是在需要多个函数中共享状态或变量时,相比于将状态传递给每个函数或使用全局变量,闭包提供了一种更简洁和自包含的方式来处理状态。包
基本概念
基本概念
在Go语言中,包是一种组织和封装代码的方式,每一个go文件都归属于一个包,而一个包可以包含多个文件。
包的主要优势如下:
封装: 包可以将相关的代码组织在一起,并通过标识符首字母大小写的方式控制对外部的可见性。通过将不同的功能和实现细节封装在包中,可以提高代码的可读性、可维护性和安全性。代码复用: 包与包之间可以相互引用,将常用的功能封装在包中,可以避免重复编写相同的代码,提高开发效率。命名空间: 不同的包可以拥有相同的函数、变量或类型名称,通过包名和导入路径的组合,可以唯一地标识一个包。可管理性: 包可以按照逻辑和功能进行组织,形成层次结构,使项目的代码更加清晰和可管理,便于团队协作和项目的扩展。在Go项目中,通常一个包对应一个目录,包名与目录名相同,每个目录下的go文件都归属于当前包。如下:
包的使用
包的使用
包的使用方式如下:
打包: 在go文件的第一行通过package 包名
的方式,指明当前go文件所属的包的名称,并将文件放在对应的目录中。导入包: 在go文件中通过import 包路径
的方式,指明需要导入的包所在的路径。给包取别名: 通过import 别名 包路径
的方式,可以在导入包时给包取别名。使用包: 正确导入包后,通过包名来访问对应包在的函数、变量和类型等,比如包名.函数
。 例如,下面在Go项目中创建了db、utils、main三个目录,分别用于存放db包、utils包和main包的go文件代码,并在main包中调用了db包和utils包中的函数。如下:
说明一下:
当需要导入多个包时,一般通过import()
的方式进行导入。在导入包时,包路径从GOPATH下的src目录开始(不包含src),在import 包路径
时对应的目录路径是$GOPATH/src/包路径
。在导入包时如果给包取了别名,那么后续就只能通过别名来访问包,而不能再使用原来的包名。main函数必须位于main包中,当使用go build或go run命令构建或运行可执行程序时,编译器会在main包中查找main函数。在同一个包下,不能有相同的函数名、全局变量名、类型名等,否则会产生重定义报错。 init函数
init函数
在每一个go文件中都可以包含一个init函数,该函数会在main函数执行前被Go运行框架调用。如下:
package mainimport ("fmt")func init() {fmt.Println("main init...")}func main() {fmt.Println("main run...")}
代码的运行结果如下:
说明一下:
init函数主要用于执行一些初始化操作,比如注册变量、设置配置、初始化数据等。init函数没有参数和返回值,并且无法显式地调用它们,它们在程序运行过程中由系统自动调用。每个包可以对应多个go文件,每个文件中都可以包含init函数,因此一个包中可能会存在多个init函数,但这并不会产生重定义报错,因为init函数在编译阶段会被编译器处理为特殊的符号,确保所有init函数被正确执行而不会发生冲突。init函数的执行顺序
init函数的执行顺序如下:
对main包中所有导入的包,按照导入顺序依次进行初始化。对于每个包来说,如果这个包导入了其他包,则会先对其导入的包进行初始化。在初始化每个包时,会先对包中的全局变量进行初始化,然后再调用包中的init函数。下面分别在之前的main包、utils包和db包中添加了一些输出代码,方便看到程序的执行顺序。
main.go文件中的代码如下:
package mainimport ("fmt""go_code/FunctionAndPackage/PackageDemo/db""go_code/FunctionAndPackage/PackageDemo/utils")var globalNum = test()func init() {fmt.Println("main init...")}func test() int {fmt.Println("main global variable init...")return 0}func main() {fmt.Println("main run...")db.DbPrint()utils.UtilsPrint()}
utils.go文件中的代码如下:
package utilsimport "fmt"var globalNum = test()func init() {fmt.Println("utils pakcage init...")}func test() int {fmt.Println("utils global variable init...")return 0}func UtilsPrint() {fmt.Println("utils package print...")}
db.go文件中的代码如下:
package dbimport "fmt"var globalNum = test()func init() {fmt.Println("db pakcage init...")}func test() int {fmt.Println("db global variable init...")return 0}func DbPrint() {fmt.Println("db package print...")}
运行程序后可以看到,按照main包中的导入顺序先后对db包和utils包进行了初始化,然后再对main包进行了初始化,每个包在初始化过程中先对全局变量进行了初始化,然后再调用了init函数,在所有包初始化完毕后开始执行main函数的代码逻辑。如下:
说明一下:
如果db包和utils包中导入了其他包,那么在对db包和utils包初始化之前,会先对其导入的包进行初始化。