【Golang】:函数和包
目录
1. 函数
1.1 函数定义
1.2 函数传参
1.3 函数返回值
1.4 可变参数
1.5 函数类型
1.6 匿名函数
1.7 defer 延迟调用
1.8 闭包(Closure)
2. 包
2.1. 基本概念
2.2 使用方式
2.3 init 函数
1. 函数
1.1 函数定义
func 函数名(参数列表) 返回值类型 {// 函数体return 返回值
}
注意: Go中的函数不支持函数重载
package mainimport "fmt"func add(a int, b int) int {return a + b
}
func main() {fmt.Println(add(1, 2))}
1.2 函数传参
参数传递的方式有两种:
- 值传递:传参时传递的是值的拷贝,函数内部对参数的修改不会影响到原始数据,值类型参数默认采用的就是值传递,包括基本数据类型、数组和结构体。
- 引用传递:传参时传递的是地址的拷贝,在函数内部对参数的修改会影响到原始数据,引用类型参数默认采用引用传递,包括指针、切片、管道、接口等。
package mainimport "fmt"// 值传递,无法改变内部的数据
func swap(a int, b int) {temp := aa = bb = temp
}
func main() {var a int = 100var b int = 200fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200swap(a, b)fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200}
引用传参:
package mainimport "fmt"func swap(a *int, b *int) {temp := *a*a = *b*b = temp
}
func main() {var a int = 100var b int = 200fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200swap(&a, &b)fmt.Println("a: ", a, "b: ", b) // a: 200 b: 100}
1.3 函数返回值
1. 返回多个值
Go中函数支持返回多个值,通过返回值列表指明各个返回值的类型即可。如下:
package mainimport "fmt"func GetSumAndSub(a int, b int) (int, int) { // 返回多个值add := a + bsub := a - breturn add, sub
}func main() {add, sub := GetSumAndSub(10, 20)fmt.Println(add) // 30fmt.Println(sub) // -10
}
2. 忽略返回值
如果函数返回多个值,在接收时,可以通过_
(占位符)忽略不需要的返回值。如下:
package mainimport "fmt"func GetSumAndSub(a int, b int) (int, int) { // 返回多个值add := a + bsub := a - breturn add, sub
}func main() {_, sub := GetSumAndSub(10, 20)fmt.Println(sub) // -10
}
3. 返回值命名
Go中函数支持在返回值列表给返回值命名,这时函数在返回时无需在return后指明需要返回的值,可以避免返回顺序出错。如下:
package mainimport "fmt"func GetSumAndSub(a int, b int) (add int, sub int) { // 返回多个值add = a + bsub = a - breturn
}func main() {add, sub := GetSumAndSub(10, 20)fmt.Println(add) // 30fmt.Println(sub) // -10
}
1.4 可变参数
参数数量不确定时使用:
- 如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表的最后。
package mainimport "fmt"func sum(nums ...int) int {total := 0for _, v := range nums {total += v}return total
}func main() {fmt.Println(sum(1)) // 1fmt.Println(sum(1, 2)) // 3fmt.Println(sum(1, 2, 3)) // 6fmt.Println(sum(1, 2, 3, 4)) // 10
}
1.5 函数类型
在Go中函数也是一种数据类型,可以将其赋值给一个变量,然后通过该变量即可对函数进行调用。如下:
package mainimport "fmt"func add(num1 int, num2 int) int {return num1 + num2
}func main() {sumFunc := addfmt.Printf("类型:%T, 值:%d", sumFunc, sumFunc(1, 2)) // 类型:func(int, int) int, 值:3
}
类型定义和类型别名
1. 类型定义
-
这里
MyInt
是一个新类型,它的底层类型是int
,但是编译器认为int
和MyInt
是两个不同的类型。 -
所以直接赋值会报错,必须 显式类型转换
package mainimport "fmt"func main() {type myInt intvar a myInt = 100var b int = 200fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:100fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200// a = b // 错误,a = myInt(b)fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:200fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
}
2. 类型别名
-
这里
MyInt
只是int
的别名,两者完全等价,编译器认为它们就是同一个类型。 -
因此可以直接赋值,不需要转换
package mainimport "fmt"func main() {type myInt = intvar a myInt = 100var b int = 200fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:100fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200a = b // 可以直接赋值fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:200fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
}
在Go中将函数作为形参也是常见的用法,这时结合自定义数据类型给函数类型取别名,能有效提高代码的可读性。如下:
package mainimport "fmt"func Sum(a int, b int) int {return a + b
}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("值:%d\n", result) // 值:30
}
1.6 匿名函数
如果希望某个函数只使用一次,可以考虑使用匿名函数,定义时直接使用,不必命名:
func(a int, b int) int{return a + b}()
可以赋值给变量,进行使用:
package mainimport "fmt"func main() {// 定义匿名函数并调用sum1 := func(a int, b int) int {return a + b}(1, 2)fmt.Printf("类型:%T, 值:%d\n", sum1, sum1) // 类型:int, 值:3// 定义匿名函数sum2 := func(a int, b int) int {return a + b}fmt.Printf("类型:%T, 值:%d\n", sum2, sum2(100, 200)) // 类型:func(int, int) int, 值:300
}
1.7 defer 延迟调用
1. defer 的作用:用来延迟执行某些操作,通常是释放资源
- 文件关闭(
file.Close()
) - 解锁(
mu.Unlock()
) - 数据库连接释放
- 网络连接关闭
这样写可以避免忘记释放资源,即使函数发生 panic
,defer
语句依然会执行。
2. 执行顺序
-
当程序执行到
defer
时,不会立刻执行,而是把该语句 压入 defer 栈。 -
当函数返回时,Go 会按照 后进先出(LIFO) 的顺序执行这些语句。
-
defer 语句的参数会在声明时就被求值,而不是在函数结束时。
package mainimport "fmt"func demo() {defer fmt.Println("A")defer fmt.Println("B")fmt.Println("C")
}func test() {x := 10defer fmt.Println("defer x =", x) // 保存的是 x=10x = 20fmt.Println("x =", x)
}func main() {demo() // C B Atest() // x = 20 defer x = 10
}
常见应用场景
- 在Go中通常会在创建资源后,通过defer语句将资源关闭,由于defer语句会在当前函数执行完毕后再执行,因此在defer语句之后仍然可以使用创建的资源。
- 在其他语言中,资源释放时机是一个常见的问题,而Go中的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
}
注意事项(常见坑)
-
多个 defer 的执行顺序是反向的(栈结构)。
-
defer 的参数在声明时就确定了,不会随着变量变化而改变。
-
defer 与 return 结合时,如果
defer
修改了返回值(需要命名返回值),可能导致结果不同。
package mainimport "fmt"func f() (x int) {defer func() { x++ }()return 3
}func main() {x := f()fmt.Println("值:%d\n", x) // 4
}
1.8 闭包(Closure)
1. 什么是闭包?
-
闭包 = 函数 + 外部变量引用环境
-
它允许函数“记住”并操作其外部作用域中的变量,即使该作用域已经结束。
package mainimport "fmt"func add(a int) func(int) int {var sum = areturn func(x int) int {sum += xreturn sum}
}func main() {var posSum = add(10)fmt.Printf("类型:%T\n", posSum) // 类型:func(int) intfmt.Printf("值:%d\n", posSum(10)) // 值:20fmt.Printf("值:%d\n", posSum(20)) // 值:40fmt.Printf("值:%d\n", posSum(30)) // 值:70}
-
sum
是外部变量,func(x int) int
是内部函数。 -
即使
add()
执行完毕,sum
依然被闭包函数捕获并存储在内存中。
注意:闭包记住环境,变量延长寿命;引用而非拷贝,循环要格外小心。
2. 包
2.1. 基本概念
-
包 = 代码的最小组织单元
-
每个 Go 源文件都必须声明一个
package
。 -
同一个目录下的
.go
文件必须属于同一个包(除非是main
包)。 -
首字母大写:对外可见(公有)。
-
首字母小写:仅包内可见(私有)。
-
可管理性:大项目通常会按功能划分多个包,例如:
models
、controllers
、services
。
2.2 使用方式
包的使用方式可以分为四部:打包、导入包、给包取别名、使用包
1. 打包
// 在 .go 文件的第一行写上
package 包名// 注意:同一目录下的 .go 文件必须属于同一个包。
2. 导入包
// 在需要使用的地方通过 import 引入
import "test_go/hello"
3. 给包取别名
注意:取了别名就只能用别名
package mainimport ("fmt"h "test_go/hello" // 可以避免包名冲突,或缩短使用
)func main() {fmt.Println(h.Add(1, 2))
}
4. 使用包
// 通过 包名.标识符 调用
// 注意:只有 首字母大写 的函数/变量/类型才能被外部包访问。
package mainimport ("fmt"h "test_go/hello"
)func main() {fmt.Println(h.Add(1, 2))
}
2.3 init 函数
1. 基本概念
-
每个 Go 源文件都可以包含 一个或多个
init
函数。 -
init
函数在 程序运行前自动调用,且 在main()
之前执行。 -
不能被显式调用,即你不能在代码里写
init()
。
2. 执行顺序
-
包级变量初始化:先初始化包里的全局变量。
-
执行
init
函数:包中可能有多个init
,会按照它们在源码中出现的顺序执行,但这并不会产生重定义报错,因为init函数在编译阶段会被编译器处理为特殊的符号,确保所有init函数被正确执行而不会发生冲突。 -
导入包的
init
-
如果
main
包依赖其它包,会先初始化依赖包(递归执行)。 -
即:先初始化被导入的包,再初始化当前包。
-
-
最后才执行
main()
。
依赖包变量初始化 → 依赖包 init() → 当前包变量初始化 → 当前包 init() → main()
package mainimport ("fmt"
)var num = initNum()func initNum() int {fmt.Println("初始化全局变量 num")return 100
}
func init() {fmt.Println("init1() 被调用")
}
func init() {fmt.Println("init2() 被调用")
}func main() {fmt.Println("main() 被调用")fmt.Println("num =", num)
}// 初始化全局变量 num
// init1() 被调用
// init2() 被调用
// main() 被调用
// num = 100
如果 main 包导入了其他包,main包初始化之前,会先对其导入的包进行初始化。
package mainimport ("fmt"h "test_go/hello" )var num = initNum()func initNum() int {fmt.Println("初始化全局变量 num")return 100 } func init() {fmt.Println("init() 被调用") }func main() {fmt.Println("main() 被调用")fmt.Println("num =", num)fmt.Println(h.Add(1, 2)) } /* hello.go中init() 被调用 初始化全局变量 num init() 被调用 main() 被调用 num = 100 3 */