目录
[1. 函数](#1. 函数)
[1.1 函数定义](#1.1 函数定义)
[1.2 函数传参](#1.2 函数传参)
[1.3 函数返回值](#1.3 函数返回值)
[1.4 可变参数](#1.4 可变参数)
[1.5 函数类型](#1.5 函数类型)
[1.6 匿名函数](#1.6 匿名函数)
[1.7 defer 延迟调用](#1.7 defer 延迟调用)
[1.8 闭包(Closure)](#1.8 闭包(Closure))
[2. 包](#2. 包)
[2.1. 基本概念](#2.1. 基本概念)
[2.2 使用方式](#2.2 使用方式)
[2.3 init 函数](#2.3 init 函数)
1. 函数
1.1 函数定义
func 函数名(参数列表) 返回值类型 {
// 函数体
return 返回值
}
注意: Go中的函数不支持函数重载
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
fmt.Println(add(1, 2))
}
1.2 函数传参
参数传递的方式有两种:
-
值传递:传参时传递的是值的拷贝,函数内部对参数的修改不会影响到原始数据,值类型参数默认采用的就是值传递,包括基本数据类型、数组和结构体。
-
引用传递:传参时传递的是地址的拷贝,在函数内部对参数的修改会影响到原始数据,引用类型参数默认采用引用传递,包括指针、切片、管道、接口等。
package main
import "fmt"
// 值传递,无法改变内部的数据
func swap(a int, b int) {
temp := a
a = b
b = temp
}
func main() {
var a int = 100
var b int = 200
fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200
swap(a, b)
fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200}
引用传参:
package main
import "fmt"
func swap(a *int, b *int) {
temp := *a
*a = *b
*b = temp
}
func main() {
var a int = 100
var b int = 200
fmt.Println("a: ", a, "b: ", b) // a: 100 b: 200
swap(&a, &b)
fmt.Println("a: ", a, "b: ", b) // a: 200 b: 100
}
1.3 函数返回值
1. 返回多个值
Go中函数支持返回多个值,通过返回值列表指明各个返回值的类型即可。如下:
package main
import "fmt"
func GetSumAndSub(a int, b int) (int, int) { // 返回多个值
add := a + b
sub := a - b
return add, sub
}
func main() {
add, sub := GetSumAndSub(10, 20)
fmt.Println(add) // 30
fmt.Println(sub) // -10
}
2. 忽略返回值
如果函数返回多个值,在接收时,可以通过_
(占位符)忽略不需要的返回值。如下:
package main
import "fmt"
func GetSumAndSub(a int, b int) (int, int) { // 返回多个值
add := a + b
sub := a - b
return add, sub
}
func main() {
_, sub := GetSumAndSub(10, 20)
fmt.Println(sub) // -10
}
3. 返回值命名
Go中函数支持在返回值列表给返回值命名,这时函数在返回时无需在return后指明需要返回的值,可以避免返回顺序出错。如下:
package main
import "fmt"
func GetSumAndSub(a int, b int) (add int, sub int) { // 返回多个值
add = a + b
sub = a - b
return
}
func main() {
add, sub := GetSumAndSub(10, 20)
fmt.Println(add) // 30
fmt.Println(sub) // -10
}
1.4 可变参数
参数数量不确定时使用:
-
如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表的最后。
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}func main() {
fmt.Println(sum(1)) // 1
fmt.Println(sum(1, 2)) // 3
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4)) // 10
}
1.5 函数类型
在Go中函数也是一种数据类型,可以将其赋值给一个变量,然后通过该变量即可对函数进行调用。如下:
package main
import "fmt"
func add(num1 int, num2 int) int {
return num1 + num2
}
func main() {
sumFunc := add
fmt.Printf("类型:%T, 值:%d", sumFunc, sumFunc(1, 2)) // 类型:func(int, int) int, 值:3
}
类型定义和类型别名
1. 类型定义
-
这里
MyInt
是一个新类型 ,它的底层类型是int
,但是编译器认为int
和MyInt
是两个不同的类型。 -
所以直接赋值会报错,必须 显式类型转换
package main
import "fmt"
func main() {
type myInt intvar a myInt = 100 var b int = 200 fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:100 fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200 // a = b // 错误, a = myInt(b) fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:main.myInt, 值:200 fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
}
2. 类型别名
-
这里
MyInt
只是int
的别名,两者完全等价,编译器认为它们就是同一个类型。 -
因此可以直接赋值,不需要转换
package main
import "fmt"
func main() {
type myInt = intvar a myInt = 100 var b int = 200 fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:100 fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200 a = b // 可以直接赋值 fmt.Printf("类型:%T, 值:%d\n", a, a) // 类型:int, 值:200 fmt.Printf("类型:%T, 值:%d\n", b, b) // 类型:int, 值:200
}
在Go中将函数作为形参也是常见的用法,这时结合自定义数据类型给函数类型取别名,能有效提高代码的可读性。如下:
package main
import "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 main
import "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 main
import "fmt"
func demo() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
}func test() {
x := 10
defer fmt.Println("defer x =", x) // 保存的是 x=10
x = 20
fmt.Println("x =", x)
}func main() {
demo() // C B A
test() // 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 main
import "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 main
import "fmt"
func add(a int) func(int) int {
var sum = a
return func(x int) int {
sum += x
return sum
}
}func main() {
var posSum = add(10)
fmt.Printf("类型:%T\n", posSum) // 类型:func(int) int
fmt.Printf("值:%d\n", posSum(10)) // 值:20
fmt.Printf("值:%d\n", posSum(20)) // 值:40
fmt.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 main
import (
"fmt"
h "test_go/hello" // 可以避免包名冲突,或缩短使用
)
func main() {
fmt.Println(h.Add(1, 2))
}
4. 使用包
// 通过 包名.标识符 调用
// 注意:只有 首字母大写 的函数/变量/类型才能被外部包访问。
package main
import (
"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 main
import (
"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 main
import (
"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
*/