一、变量与常量声明
var - 变量声明
go
复制代码
// var 声明变量(可在函数内外使用)
var name string = "Go" // 显式指定类型为string
var age = 25 // 编译器自动推断类型为int
var x, y int = 1, 2 // 同时声明多个同类型变量,初始化为指定值
// 零值初始化(未赋值的变量自动初始化为零值)
var count int // 0(int的零值)
var price float64 // 0.0(float64的零值)
var isReady bool // false(bool的零值)
var text string // ""(string的零值)
var numbers []int // nil(切片的零值)
var data map[string]int // nil(映射的零值)
var ptr *int // nil(指针的零值)
简短声明 :=
go
复制代码
// := 简短声明(只能在函数内部使用,自动推断类型)
func main() {
name := "Alice" // 编译器自动推断为string类型
count := 10 // 编译器自动推断为int类型
a, b := 1, "hello" // 同时声明不同类型的变量
// 注意::= 左侧至少有一个新变量,否则应使用 =
}
const - 常量声明
go
复制代码
// const 声明常量(值不可修改,编译时确定)
const Pi = 3.14159 // 单常量声明,类型推断
const MaxUsers = 1000 // 常量值在编译时确定
// 常量组(批量声明)
const (
StatusOK = 200 // HTTP状态码:成功
StatusNotFound = 404 // HTTP状态码:未找到
StatusServerError = 500 // HTTP状态码:服务器错误
)
// iota 枚举生成器(从0开始,每行自动递增1)
// iota 是什么?
// iota 是 Go 语言中的预定义标识符,只能在常量声明中使用
// 它在每个 const 块中从 0 开始,每行自动递增 1
// 常用于定义枚举值或生成序列常量
const (
Monday = iota + 1 // iota=0, Monday=1(从1开始)
Tuesday // iota=1, Tuesday=2(自动继承表达式)
Wednesday // iota=2, Wednesday=3
Thursday // iota=3, Thursday=4
Friday // iota=4, Friday=5
)
// iota 位运算示例(常用于定义文件大小单位)
const (
_ = iota // 使用下划线跳过0值
KB = 1 << (10 * iota) // 1左移10位 = 1024(1KB)
MB // 1左移20位 = 1,048,576(1MB)
GB // 1左移30位 = 1,073,741,824(1GB)
)
// iota 详细示例
const (
// 第一个const块,iota从0开始
Zero = iota // 0
One // 1
Two // 2
)
const (
// 新的const块,iota重新从0开始
Sunday = iota // 0
Monday2 // 1
Tuesday2 // 2
)
// iota 在同一行中会重复使用相同的值
const (
A, B = iota, iota + 1 // A=0, B=1(同一行iota相同)
C, D // C=1, D=2(iota递增)
E, F // E=2, F=3
)
二、make 与 new
make - 创建并初始化引用类型
go
复制代码
// make 用于创建切片、映射、管道等引用类型,返回已初始化的值
// 切片:make([]T, length, capacity)
s1 := make([]int, 5) // 创建长度为5,容量为5的int切片,所有元素初始化为0
s2 := make([]int, 3, 10) // 长度3,容量10,实际分配10个元素的空间(可预扩容)
// 映射:make(map[K]V, initialCapacity)
m1 := make(map[string]int) // 创建空映射,使用默认容量
m2 := make(map[string]int, 10) // 创建初始容量为10的映射(优化性能,减少扩容)
// 管道:make(chan T, bufferSize)
ch1 := make(chan int) // 无缓冲管道,发送会阻塞直到有接收者
ch2 := make(chan int, 3) // 缓冲大小为3,可暂存3个元素而不阻塞
new - 分配内存并返回指针
go
复制代码
// new(T) 分配T类型的内存,返回指针 *T,值初始化为零值
p := new(int) // p是*int类型,*p = 0(int的零值)
*p = 42 // 通过指针修改值
// 结构体
type Point struct { X, Y int }
pt := new(Point) // pt是*Point类型,X:0, Y:0(结构体零值)
pt.X = 10 // 通过指针访问结构体字段(自动解引用)
pt.Y = 20
// 注意:new只分配内存并返回指针,make会初始化引用类型
make vs new 对比
go
复制代码
// make 用于引用类型(切片、映射、管道),返回已初始化的值
slice := make([]int, 3) // slice是[]int类型,值为[0 0 0]
// new 用于任何类型,返回指针,引用类型需要单独初始化
ptr := new([]int) // ptr是*[]int类型,*ptr是nil切片
*ptr = make([]int, 3) // 需要显式初始化才能使用
// 推荐:切片、映射、管道使用make,结构体指针使用new或&
三、基本数据结构
数组(固定长度)
go
复制代码
// 数组是固定长度的值类型,长度是类型的一部分
var arr1 [3]int // 声明长度为3的int数组,初始化为[0 0 0]
arr2 := [3]int{1, 2, 3} // 显式初始化,长度为3
arr3 := [...]int{1, 2, 3, 4} // 编译器自动计算长度,长度为4
arr4 := [2][3]int{{1,2,3}, {4,5,6}} // 二维数组
// 数组操作
arr2[0] = 10 // 修改元素
fmt.Println(len(arr2)) // 获取长度:3
fmt.Println(arr2[1]) // 访问元素:2
// 重要:固定长度数组不能新增或删除元素
// 数组长度在声明时就确定了,无法动态改变
arr := [3]int{1, 2, 3}
// arr = append(arr, 4) // ❌ 错误:数组不支持append
// 如果需要动态长度,应该使用切片(slice)
// 检查数组是否包含某个值(需要手动遍历)
func contains(arr [3]int, target int) bool {
for _, v := range arr {
if v == target {
return true
}
}
return false
}
arr := [3]int{1, 2, 3}
if contains(arr, 2) {
fmt.Println("数组包含2")
}
// 或者使用切片(更灵活)
slice := []int{1, 2, 3}
// 切片可以使用标准库的 slices.Contains(Go 1.21+)
// import "slices"
// if slices.Contains(slice, 2) {
// fmt.Println("切片包含2")
// }
切片(动态数组)
go
复制代码
// 切片是动态数组,是对底层数组的引用
slice1 := []int{1, 2, 3} // 字面量创建切片
slice2 := make([]int, 5) // make创建,长度为5,容量5
var slice3 []int // nil切片(零值)
// 切片操作
slice1 = append(slice1, 4, 5) // 追加元素,返回新切片
copy(slice2, slice1) // 复制切片元素
fmt.Println(len(slice1)) // 长度:5
fmt.Println(cap(slice1)) // 容量(可能大于长度)
// 切片表达式(从数组或切片创建新切片)
arr := [5]int{1,2,3,4,5}
s1 := arr[1:4] // [2,3,4],左闭右开区间
s2 := arr[:3] // [1,2,3],从开始到索引3
s3 := arr[2:] // [3,4,5],从索引2到结束
映射(字典)
go
复制代码
// 映射是键值对集合,引用类型
m1 := map[string]int{"a": 1, "b": 2} // 字面量创建
m2 := make(map[string]int) // make创建空映射
var m3 map[string]int // nil映射(不能直接使用)
// 映射操作
m1["c"] = 3 // 添加/修改键值对
val := m1["a"] // 读取值:1
delete(m1, "b") // 删除键值对
// 映射取值的两种方式:
// 方式1:只接收一个值(默认返回value,如果键不存在返回零值)
val := m1["a"] // val = 1(键存在)
val2 := m1["x"] // val2 = 0(键不存在,返回int的零值)
// 问题:无法区分是键不存在还是值本身就是0
// 方式2:接收两个值(value 和 exists)
value, exists := m1["x"] // exists为false,value为0(int零值)
// exists 是 bool 类型,表示键是否存在
// value 是值类型,如果键存在返回实际值,不存在返回零值
// 检查键是否存在(推荐使用方式2)
if val, ok := m1["a"]; ok {
// ok 为 true 表示键存在
fmt.Println("键存在,值为:", val) // 安全访问
} else {
// ok 为 false 表示键不存在
fmt.Println("键不存在")
}
// 判断字典是否包含某个key的完整示例
m := map[string]int{"apple": 5, "banana": 3}
// 方法1:使用两个返回值
if value, exists := m["apple"]; exists {
fmt.Printf("键'apple'存在,值为: %d\n", value)
} else {
fmt.Println("键'apple'不存在")
}
// 方法2:只检查是否存在(忽略值)
if _, exists := m["orange"]; exists {
fmt.Println("键'orange'存在")
} else {
fmt.Println("键'orange'不存在")
}
// 方法3:通过零值判断(不推荐,因为无法区分零值和不存在)
if m["grape"] != 0 {
fmt.Println("键'grape'存在且值不为0")
} else {
// 无法确定是键不存在还是值为0
fmt.Println("键'grape'不存在或值为0")
}
结构体
go
复制代码
// 结构体是自定义复合类型,值类型
type Person struct {
Name string // 字段名首字母大写表示公开
Age int
Contact struct { // 嵌套结构体
Phone string
Email string
}
}
// 创建结构体实例
p1 := Person{Name: "Alice", Age: 25} // 字段名初始化(推荐)
p2 := Person{"Bob", 30} // 顺序初始化(需按字段顺序)
p3 := &Person{Name: "Charlie"} // 创建指针,未指定字段为零值
// 匿名结构体(临时使用)
temp := struct {
X, Y int
}{10, 20} // 创建并初始化
四、指针与方法(重点:值是否会变化)
指针基础
go
复制代码
// 指针存储变量的内存地址
var a int = 42
var p *int = &a // p指向a的地址(&取地址操作符)
fmt.Println(*p) // 通过指针访问值:42(*解引用操作符)
*p = 100 // 通过指针修改a的值
fmt.Println(a) // a变为100
// 结构体指针
type Circle struct { Radius float64 }
c := &Circle{Radius: 5} // 创建结构体指针
c.Radius = 10 // 自动解引用(Go语法糖)
(*c).Radius = 15 // 显式解引用(效果相同)
值接收者 vs 指针接收者(关键区别)
go
复制代码
type Counter struct {
count int
}
// 值接收者方法 - 接收结构体的副本,不修改原对象
func (c Counter) IncrementByValue() {
c.count++ // 修改的是副本,不影响原对象
fmt.Printf("IncrementByValue内部: count = %d\n", c.count)
// 注意:方法内的修改不会影响外部
}
// 指针接收者方法 - 接收结构体的指针,可修改原对象
func (c *Counter) IncrementByPointer() {
c.count++ // 通过指针修改原对象
fmt.Printf("IncrementByPointer内部: count = %d\n", c.count)
// 注意:方法内的修改会影响外部
}
func main() {
counter := Counter{count: 0}
fmt.Printf("初始值: count = %d\n", counter.count) // 输出:0
// 测试值接收者方法
counter.IncrementByValue() // 内部输出:count = 1
fmt.Printf("值接收者调用后: count = %d\n", counter.count)
// 输出:0(未改变!原对象未被修改)
// 测试指针接收者方法
counter.IncrementByPointer() // 内部输出:count = 1
fmt.Printf("指针接收者调用后: count = %d\n", counter.count)
// 输出:1(已改变!原对象被修改)
// 结论:
// 1. 值接收者方法:内部修改不影响外部(操作的是副本)
// 2. 指针接收者方法:内部修改影响外部(操作的是原对象)
}
自动转换机制
go
复制代码
type Circle struct {
Radius float64
}
// 值接收者方法(只读,不修改)
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
// 指针接收者方法(可修改)
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
func main() {
// 情况1:值类型变量调用指针接收者方法
c1 := Circle{Radius: 5}
c1.Scale(2) // Go自动转换为: (&c1).Scale(2)
fmt.Printf("c1半径: %.2f\n", c1.Radius) // 输出:10.00(原对象被修改)
// 情况2:指针类型变量调用值接收者方法
c2 := &Circle{Radius: 5}
area := c2.Area() // Go自动转换为: (*c2).Area()
fmt.Printf("c2面积: %.2f\n", area) // 输出:78.50(计算结果,原对象未修改)
// Go的便利性:无论值类型还是指针类型,都可以调用两种方法
// 编译器会自动处理 & 和 * 的转换
}
方法接收者选择原则
go
复制代码
// 值接收者适用场景:
// 1. 方法不需要修改接收者
// 2. 接收者是小型结构体(避免复制开销)
// 3. 需要不可变性
// 指针接收者适用场景:
// 1. 方法需要修改接收者
// 2. 接收者是大型结构体(避免复制开销)
// 3. 保证一致性(如缓存、连接等)
// 4. 实现某些接口时
// 一致性原则:同一类型的多个方法应该使用相同类型的接收者
五、控制流
if - 条件判断
go
复制代码
// 基础if语句
if age >= 18 {
fmt.Println("成年人")
}
// if-else链
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
// if带初始化语句(变量作用域仅在if块内)
// 语法:if 初始化语句; 条件表达式 { ... }
if err := process(); err != nil {
fmt.Printf("处理出错: %v\n", err)
}
// err在这里不可访问(作用域仅在if块内)
// 判断变量是否为nil
if data != nil {
fmt.Println("数据有效")
}
// if 语句的详细语法说明
// 1. 基础语法:if 条件表达式 { ... }
if x > 0 {
fmt.Println("x是正数")
}
// 2. if-else 语法
if x > 0 {
fmt.Println("正数")
} else {
fmt.Println("非正数")
}
// 3. if-else if-else 链
if x > 0 {
fmt.Println("正数")
} else if x < 0 {
fmt.Println("负数")
} else {
fmt.Println("零")
}
// 4. if 带初始化语句(重要语法)
// 格式:if 初始化语句; 条件表达式 { ... }
// 初始化语句可以是变量声明、赋值、函数调用等
// 变量作用域仅在 if-else 块内
// 示例1:声明并判断
if val, ok := m1["a"]; ok {
// 这里 val 和 ok 都可以使用
fmt.Println("键存在,值为:", val)
} else {
// 这里 val 和 ok 也可以使用(else块内)
fmt.Println("键不存在")
}
// val 和 ok 在这里不可访问(超出作用域)
// 示例2:函数调用并判断错误
if err := process(); err != nil {
fmt.Printf("处理出错: %v\n", err)
return // 早期返回
}
// err 在这里不可访问
// 示例3:多个初始化语句(用逗号分隔)
if x, y := 1, 2; x < y {
fmt.Println("x小于y")
}
// 5. 嵌套if语句
if x > 0 {
if y > 0 {
fmt.Println("x和y都是正数")
}
}
// 6. 逻辑运算符组合条件
if x > 0 && y > 0 {
fmt.Println("x和y都是正数")
}
if x > 0 || y > 0 {
fmt.Println("x或y至少一个是正数")
}
// 7. 类型断言(结合if使用)
var val interface{} = "hello"
if str, ok := val.(string); ok {
fmt.Println("是字符串:", str)
}
switch - 多分支选择
go
复制代码
// 基础switch(匹配值)
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("工作日")
case "Saturday", "Sunday":
fmt.Println("周末")
default:
fmt.Println("无效日期")
}
// switch无表达式(相当于if-else链,更清晰)
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("D")
}
// fallthrough关键字(继续执行下一个case,不推荐使用)
switch num {
case 1:
fmt.Println("1")
fallthrough // 继续执行case 2(不检查条件)
case 2:
fmt.Println("2") // 即使num=1也会执行这里
case 3:
fmt.Println("3")
}
for - 循环
go
复制代码
// 传统for循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while风格(Go没有while关键字,用for代替)
count := 0
for count < 5 {
fmt.Println(count)
count++
}
// 无限循环(必须有break或return退出)
for {
if condition {
break // 跳出循环
}
}
// range遍历(数组、切片、字符串、映射、管道)
for index, value := range []int{1, 2, 3} {
fmt.Printf("索引: %d, 值: %d\n", index, value)
}
// 忽略索引或值(使用下划线)
for _, value := range slice { // 忽略索引
fmt.Println(value)
}
for key := range map { // 忽略值
fmt.Println(key)
}
break 和 continue
go
复制代码
// break - 跳出循环
for i := 0; i < 10; i++ {
if i == 5 {
break // 当i=5时跳出循环
}
fmt.Println(i) // 只打印0-4
}
// 带标签的break(跳出多层循环)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 直接跳出外层循环
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// continue - 跳过当前迭代
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Println(i) // 只打印奇数:1,3,5,7,9
}
六、函数
函数定义
go
复制代码
// 基本函数定义
func add(a int, b int) int {
return a + b
}
// 参数类型简写(相同类型可合并)
func multiply(x, y float64) float64 {
return x * y
}
// 多返回值(Go的特色)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除零错误") // 返回错误
}
return a / b, nil // 返回结果和nil(无错误)
}
// 命名返回值(可直接为返回值赋值)
func getCoordinates() (x int, y int) {
x = 10 // 直接为返回值赋值
y = 20
return // 自动返回 x, y(裸返回)
}
// 可变参数函数(参数数量可变)
func sum(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// 调用:sum(1, 2, 3) 或 sum(1, 2, 3, 4, 5)
defer - 延迟执行
go
复制代码
// defer 将函数调用推迟到外层函数返回时执行
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // 确保函数返回前关闭文件(无论是否出错)
// 处理文件内容...
return nil
}
// 多个defer按后进先出(LIFO)顺序执行
func test() {
defer fmt.Println("第一个defer")
defer fmt.Println("第二个defer")
defer fmt.Println("第三个defer")
fmt.Println("函数体")
}
// 输出顺序:函数体 → 第三个defer → 第二个defer → 第一个defer
panic 和 recover
go
复制代码
// panic 使程序崩溃,打印错误信息并终止
func divide(a, b int) int {
if b == 0 {
panic("除数不能为零") // 引发panic,程序终止
}
return a / b
}
// recover 捕获panic,只能在defer函数中调用
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil { // 捕获panic
err = fmt.Errorf("发生错误: %v", r)
result = 0
}
}()
if b == 0 {
panic("除零错误") // 触发panic
}
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("错误:", err) // 输出:错误: 发生错误: 除零错误
} else {
fmt.Println("结果:", result)
}
}
七、接口
接口定义与实现
go
复制代码
// 接口定义一组方法签名
type Shape interface {
Area() float64
Perimeter() float64
}
// 实现接口(只要实现了所有方法,就自动实现了接口)
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 使用接口
var s Shape = Rectangle{Width: 3, Height: 4}
area := s.Area() // 12
interface{} vs any(Go 1.18+)
go
复制代码
// any 是 interface{} 的类型别名(Go 1.18引入)
// 两者完全等价,可以互换使用
// 使用 interface{} (传统写法,兼容旧代码)
var v1 interface{} = 42
var v2 interface{} = "hello"
// 使用 any (推荐,更简洁)
var v3 any = 42
var v4 any = "hello"
// 两者可以互相赋值
v1 = v3 // ✅ 完全兼容
v3 = v1 // ✅ 完全兼容
// 函数参数中使用
func PrintValue(v interface{}) {
fmt.Println(v)
}
// 等价于(推荐使用any)
func PrintValueAny(v any) {
fmt.Println(v)
}
// 何时使用 interface{} vs any:
// 1. 新代码中优先使用 any,更简洁易读
// 2. 维护旧代码时保持 interface{} 以保持一致性
// 3. 在泛型代码中必须使用 any(与类型约束配合)
// 类型断言(检查并转换类型)
var data any = []int{1, 2, 3}
if slice, ok := data.([]int); ok {
fmt.Println("是int切片:", slice)
}
// 类型switch(根据类型分支处理)
switch v := data.(type) {
case []int:
fmt.Println("int切片,长度:", len(v))
case string:
fmt.Println("字符串:", v)
default:
fmt.Printf("其他类型: %T\n", v)
}
八、并发编程
goroutine(轻量级线程)
go
复制代码
// go 关键字启动一个新的goroutine(异步执行)
func sayHello() {
fmt.Println("Hello from goroutine")
}
func main() {
// 启动goroutine(异步执行,不阻塞主程序)
go sayHello()
// 主goroutine继续执行
fmt.Println("Hello from main")
// 等待一下让goroutine有机会执行(实际应用应该用WaitGroup或channel)
time.Sleep(100 * time.Millisecond)
}
// 启动匿名函数goroutine
go func(msg string) {
fmt.Println(msg)
}("Hello from goroutine")
// 注意:如果主goroutine退出,所有子goroutine都会终止
channel(管道)
go
复制代码
// channel 是goroutine间通信的管道
ch := make(chan int) // 创建无缓冲管道
ch := make(chan int, 3) // 创建缓冲大小为3的管道
// 发送和接收数据
ch <- 42 // 发送数据到管道(阻塞直到有接收者)
value := <-ch // 从管道接收数据(阻塞直到有数据)
// 关闭管道(只能由发送者关闭)
close(ch)
value, ok := <-ch // ok为false表示管道已关闭
// 遍历管道(直到管道关闭)
for value := range ch {
fmt.Println(value)
}
// 管道方向限定(提高类型安全)
func producer(ch chan<- int) { // 只写管道参数
ch <- 1
}
func consumer(ch <-chan int) { // 只读管道参数
value := <-ch
}
channel 阻塞机制详解
go
复制代码
// 阻塞是什么?
// 阻塞是指程序执行到某个操作时,如果条件不满足,会暂停等待,直到条件满足才继续执行
// 在非异步(单线程)情况下,阻塞会导致整个程序暂停
// 1. 无缓冲管道(阻塞机制详解)
// 示例:阻塞直到有接收者
func main() {
ch := make(chan int) // 无缓冲管道
// 情况1:在主goroutine中发送(会死锁!)
// ch <- 42 // ❌ 错误:这里会永远阻塞,因为没有接收者
// fmt.Println("这行不会执行")
// 正确做法:在另一个goroutine中发送
go func() {
fmt.Println("准备发送数据...")
ch <- 42 // 发送数据,如果此时没有接收者,会阻塞等待
fmt.Println("数据已发送")
}()
fmt.Println("准备接收数据...")
value := <-ch // 接收数据,如果此时没有数据,会阻塞等待
fmt.Println("接收到数据:", value)
// 输出顺序:
// 准备接收数据...
// 准备发送数据...
// 接收到数据: 42
// 数据已发送
}
// 2. 有缓冲管道(部分阻塞)
func main() {
ch := make(chan int, 2) // 缓冲大小为2
// 缓冲未满时,发送不会阻塞
ch <- 1 // ✅ 立即发送,不阻塞
ch <- 2 // ✅ 立即发送,不阻塞
// 缓冲已满时,发送会阻塞
// ch <- 3 // ❌ 会阻塞,因为缓冲已满(2个),且没有接收者
// 有数据时,接收不会阻塞
val1 := <-ch // ✅ 立即接收,不阻塞
val2 := <-ch // ✅ 立即接收,不阻塞
// 无数据时,接收会阻塞
// val3 := <-ch // ❌ 会阻塞,因为没有数据,且没有发送者
}
// 3. 阻塞机制完整示例
func demonstrateBlocking() {
ch := make(chan string) // 无缓冲管道
// 启动goroutine发送数据(异步)
go func() {
time.Sleep(2 * time.Second) // 模拟耗时操作
fmt.Println("goroutine: 准备发送数据")
ch <- "Hello" // 发送数据(此时主goroutine在等待接收,所以不阻塞)
fmt.Println("goroutine: 数据已发送")
}()
fmt.Println("main: 准备接收数据(会阻塞2秒)")
// 这里会阻塞,直到上面的goroutine发送数据
// 阻塞期间,程序不会继续执行下面的代码
value := <-ch // 阻塞直到有数据
fmt.Println("main: 接收到数据:", value)
// 输出顺序:
// main: 准备接收数据(会阻塞2秒)
// (等待2秒)
// goroutine: 准备发送数据
// main: 接收到数据: Hello
// goroutine: 数据已发送
}
// 4. 阻塞 vs 非阻塞(使用select的default)
func nonBlockingExample() {
ch := make(chan int)
// 非阻塞发送(使用select + default)
select {
case ch <- 42:
fmt.Println("发送成功")
default:
fmt.Println("发送失败(管道满或无接收者)") // 立即返回,不阻塞
}
// 非阻塞接收(使用select + default)
select {
case value := <-ch:
fmt.Println("接收到:", value)
default:
fmt.Println("没有数据(立即返回,不阻塞)") // 立即返回,不阻塞
}
}
// 5. 实际应用:生产者-消费者模式
func producerConsumer() {
ch := make(chan int, 3) // 缓冲大小为3
// 生产者(发送数据)
go func() {
for i := 1; i <= 5; i++ {
ch <- i // 发送数据
// 如果缓冲满,这里会阻塞直到消费者取走数据
fmt.Printf("生产: %d\n", i)
}
close(ch) // 关闭管道,通知消费者结束
}()
// 消费者(接收数据)
for value := range ch {
// 如果管道为空,这里会阻塞直到生产者发送数据
fmt.Printf("消费: %d\n", value)
time.Sleep(500 * time.Millisecond) // 模拟处理时间
}
}
// 总结:
// 1. "阻塞直到有接收者":发送操作会等待,直到有goroutine接收数据
// 2. "阻塞直到有数据":接收操作会等待,直到有goroutine发送数据
// 3. 在单线程(非goroutine)情况下,阻塞会导致程序暂停
// 4. 使用goroutine可以避免阻塞主程序
// 5. 使用缓冲管道可以减少阻塞(缓冲未满时发送不阻塞,缓冲有数据时接收不阻塞)
select - 多路复用
go
复制代码
// select 监控多个channel操作,哪个先就绪就执行哪个
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自ch2"
}()
// select等待第一个就绪的channel
select {
case msg1 := <-ch1:
fmt.Println("收到:", msg1) // 1秒后执行(更快)
case msg2 := <-ch2:
fmt.Println("收到:", msg2) // 2秒后执行
case <-time.After(3 * time.Second):
fmt.Println("超时") // 3秒后执行(如果前两个都没就绪)
default:
fmt.Println("非阻塞") // 如果所有case都不就绪,立即执行
}
// 注意:如果多个case同时就绪,select会随机选择一个(公平性)
sync.WaitGroup(等待组)
go
复制代码
// WaitGroup 用于等待多个goroutine完成
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数(每个goroutine +1)
go func(id int) {
defer wg.Done() // 完成时减1(defer确保执行)
fmt.Printf("任务%d完成\n", id)
}(i)
}
wg.Wait() // 阻塞直到计数为0(所有goroutine完成)
fmt.Println("所有任务完成")
九、时间处理
Timer(单次定时)
go
复制代码
// Timer 在指定时间后触发一次
timer := time.NewTimer(2 * time.Second)
<-timer.C // 阻塞等待2秒后触发
fmt.Println("时间到")
// 停止计时器(如果还未触发)
if timer.Stop() {
fmt.Println("计时器已停止")
}
Ticker(重复定时)- 正确使用方式
go
复制代码
// Ticker 每隔指定时间触发一次(必须使用defer停止)
func main() {
// 创建定时器:每500毫秒触发一次
ticker := time.NewTicker(500 * time.Millisecond)
// 关键:必须使用 defer 停止 ticker,否则会内存泄漏
// defer 确保函数退出时(即使是 panic)也会执行 Stop()
defer ticker.Stop() // 正确:在main函数退出时停止
// 启动一个goroutine处理定时任务
go func() {
for t := range ticker.C { // 从ticker的管道接收时间
fmt.Printf("定时触发: %v\n", t.Format("15:04:05.000"))
}
fmt.Println("Ticker停止,goroutine退出")
}()
// 主程序等待3秒
time.Sleep(3 * time.Second)
fmt.Println("主程序结束,defer ticker.Stop() 将被调用")
// 函数结束时,defer会执行,ticker被停止
// ticker.C 管道被关闭,上面的goroutine会退出
}
// 错误用法:忘记停止 Ticker(会导致内存泄漏)
func wrongUsage() {
ticker := time.NewTicker(1 * time.Second)
go func() {
for t := range ticker.C {
fmt.Println(t)
}
}()
// ❌ 错误:没有调用 ticker.Stop()
// 即使函数返回,ticker仍在后台运行,goroutine不会退出
// 导致内存泄漏!
}
// 正确用法:使用defer确保停止
func correctUsage() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // ✅ 正确:确保停止
go func() {
for t := range ticker.C {
fmt.Println(t)
}
}()
// 函数返回时,defer执行,ticker停止
// goroutine会退出,没有内存泄漏
}
时间操作
go
复制代码
now := time.Now() // 当前时间
fmt.Println(now.Format("2006-01-02 15:04:05")) // 格式化时间
duration := 2 * time.Hour // 2小时的时间间隔
future := now.Add(duration) // 加时间
past := now.Add(-duration) // 减时间
diff := future.Sub(now) // 时间差
fmt.Println(duration.Seconds()) // 转换为秒数
十、其他重要关键字
type - 类型定义
go
复制代码
// 创建新类型(底层类型相同但不能直接运算)
type Celsius float64 // 摄氏度类型
type Fahrenheit float64 // 华氏度类型
type ID string // 用户ID类型
// 类型别名(与原类型完全兼容)
type IntAlias = int // 只是int的另一个名字,可以互相赋值
// 结构体类型
type Person struct {
Name string
Age int
}
// 接口类型
type Reader interface {
Read(p []byte) (n int, err error)
}
// 函数类型
type Handler func(string) int
package 和 import
go
复制代码
// package 声明包名
package main // 可执行程序的主包
package mylib // 库包
// import 导入包
import (
"fmt" // 标准库包
"math"
"github.com/user/package" // 第三方包
. "mypackage" // 点导入:可直接访问包内标识符(不推荐)
m "math" // 别名导入:使用 m 代替 math
_ "database/sql" // 匿名导入:只执行包的init函数
)
range - 遍历
go
复制代码
// range遍历各种集合类型
// 数组/切片
for i, v := range []int{1, 2, 3} {
fmt.Printf("索引%d: 值%d\n", i, v)
}
// 映射
for k, v := range map[string]int{"a": 1, "b": 2} {
fmt.Printf("键%s: 值%d\n", k, v)
}
// 字符串(遍历rune,不是字节)
for i, ch := range "你好" {
fmt.Printf("位置%d: 字符%c\n", i, ch)
}
// 管道(直到管道关闭)
for value := range ch {
fmt.Println(value)
}
获取对象类型
go
复制代码
// 1. 使用 fmt.Printf 的 %T 格式化符(最常用)
var x int = 42
var y string = "hello"
var z []int = []int{1, 2, 3}
fmt.Printf("x的类型: %T\n", x) // 输出: x的类型: int
fmt.Printf("y的类型: %T\n", y) // 输出: y的类型: string
fmt.Printf("z的类型: %T\n", z) // 输出: z的类型: []int
// 2. 使用 reflect.TypeOf(需要导入 reflect 包)
import "reflect"
var num int = 100
var name string = "Go"
fmt.Println("num的类型:", reflect.TypeOf(num)) // int
fmt.Println("name的类型:", reflect.TypeOf(name)) // string
// 3. 获取结构体类型
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 25}
fmt.Printf("p的类型: %T\n", p) // 输出: p的类型: main.Person
// 4. 获取指针类型
ptr := &Person{Name: "Bob", Age: 30}
fmt.Printf("ptr的类型: %T\n", ptr) // 输出: ptr的类型: *main.Person
// 5. 获取接口类型
var val interface{} = 42
fmt.Printf("val的类型: %T\n", val) // 输出: val的类型: int
// 6. 类型断言获取具体类型
var data any = "hello"
switch v := data.(type) {
case int:
fmt.Printf("是int类型: %d\n", v)
case string:
fmt.Printf("是string类型: %s\n", v)
default:
fmt.Printf("其他类型: %T\n", v)
}
// 7. 获取类型的字符串表示
import "reflect"
t := reflect.TypeOf(42)
fmt.Println("类型名称:", t.Name()) // int
fmt.Println("类型字符串:", t.String()) // int
fmt.Println("类型种类:", t.Kind()) // int
// 8. 实际应用:类型检查函数
func printType(v interface{}) {
switch v := v.(type) {
case int:
fmt.Printf("整数: %d (类型: %T)\n", v, v)
case string:
fmt.Printf("字符串: %s (类型: %T)\n", v, v)
case []int:
fmt.Printf("int切片: %v (类型: %T)\n", v, v)
default:
fmt.Printf("未知类型: %T, 值: %v\n", v, v)
}
}
printType(42) // 整数: 42 (类型: int)
printType("hello") // 字符串: hello (类型: string)
printType([]int{1, 2, 3}) // int切片: [1 2 3] (类型: []int)
十一、重要特性总结
1. 值类型 vs 引用类型
类型
值类型
引用类型
示例
int, float, bool, string, 数组, 结构体
切片, 映射, 管道, 函数, 接口
赋值
复制整个值
复制引用(共享底层数据)
nil值
不能为nil
可以为nil
修改
修改不影响原值
修改影响所有引用
2. 方法接收者选择原则
值接收者 :方法不需要修改接收者,或接收者是小型结构体
指针接收者 :方法需要修改接收者,或接收者是大型结构体(避免复制)
一致性 :同一类型的多个方法应该使用相同类型的接收者
3. any 使用指南
Go 1.18+ :优先使用 any,更简洁
泛型代码 :必须使用 any
旧代码维护 :保持 interface{} 以兼容
两者完全等价 :编译后没有区别
4. Ticker 最佳实践
go
复制代码
func useTicker() {
ticker := time.NewTicker(interval)
defer ticker.Stop() // 必须!防止内存泄漏
// 使用ticker...
}
// 注意:ticker.Stop() 不会关闭ticker.C管道
// 已发送到管道的数据仍然可以接收
5. 并发安全模式
go
复制代码
// 使用WaitGroup等待所有goroutine
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 工作...
}()
}
wg.Wait()
// 使用channel传递结果
results := make(chan Result, n)
for i := 0; i < n; i++ {
go func() {
results <- doWork()
}()
}