Go 语言入门核心概念总结

107 阅读24分钟

一、变量与常量声明

var - 变量声明

// 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(指针的零值)

简短声明 :=

// := 简短声明(只能在函数内部使用,自动推断类型)
func main() {
    name := "Alice"             // 编译器自动推断为string类型
    count := 10                 // 编译器自动推断为int类型
    a, b := 1, "hello"          // 同时声明不同类型的变量
    // 注意::= 左侧至少有一个新变量,否则应使用 =
}

const - 常量声明

// 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 - 创建并初始化引用类型

// 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 - 分配内存并返回指针

// 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 对比

// make 用于引用类型(切片、映射、管道),返回已初始化的值
slice := make([]int, 3)  // slice是[]int类型,值为[0 0 0]

// new 用于任何类型,返回指针,引用类型需要单独初始化
ptr := new([]int)        // ptr是*[]int类型,*ptr是nil切片
*ptr = make([]int, 3)    // 需要显式初始化才能使用

// 推荐:切片、映射、管道使用make,结构体指针使用new或&

三、基本数据结构

数组(固定长度)

// 数组是固定长度的值类型,长度是类型的一部分
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")
// }

切片(动态数组)

// 切片是动态数组,是对底层数组的引用
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到结束

映射(字典)

// 映射是键值对集合,引用类型
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")
}

结构体

// 结构体是自定义复合类型,值类型
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}  // 创建并初始化

四、指针与方法(重点:值是否会变化)

指针基础

// 指针存储变量的内存地址
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 指针接收者(关键区别)

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. 指针接收者方法:内部修改影响外部(操作的是原对象)
}

自动转换机制

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的便利性:无论值类型还是指针类型,都可以调用两种方法
    // 编译器会自动处理 & 和 * 的转换
}

方法接收者选择原则

// 值接收者适用场景:
// 1. 方法不需要修改接收者
// 2. 接收者是小型结构体(避免复制开销)
// 3. 需要不可变性

// 指针接收者适用场景:
// 1. 方法需要修改接收者
// 2. 接收者是大型结构体(避免复制开销)
// 3. 保证一致性(如缓存、连接等)
// 4. 实现某些接口时

// 一致性原则:同一类型的多个方法应该使用相同类型的接收者

五、控制流

if - 条件判断

// 基础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 - 多分支选择

// 基础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 - 循环

// 传统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

// 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
}

六、函数

函数定义

// 基本函数定义
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 - 延迟执行

// 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

// 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)
    }
}

七、接口

接口定义与实现

// 接口定义一组方法签名
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+)

// 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 关键字启动一个新的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(管道)

// 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 阻塞机制详解

// 阻塞是什么?
// 阻塞是指程序执行到某个操作时,如果条件不满足,会暂停等待,直到条件满足才继续执行
// 在非异步(单线程)情况下,阻塞会导致整个程序暂停

// 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 - 多路复用

// 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(等待组)

// 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(单次定时)

// Timer 在指定时间后触发一次
timer := time.NewTimer(2 * time.Second)
<-timer.C                       // 阻塞等待2秒后触发
fmt.Println("时间到")

// 停止计时器(如果还未触发)
if timer.Stop() {
    fmt.Println("计时器已停止")
}

Ticker(重复定时)- 正确使用方式

// 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会退出,没有内存泄漏
}

时间操作

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 - 类型定义

// 创建新类型(底层类型相同但不能直接运算)
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

// package 声明包名
package main      // 可执行程序的主包
package mylib     // 库包

// import 导入包
import (
    "fmt"                       // 标准库包
    "math"
    
    "github.com/user/package"   // 第三方包
    
    . "mypackage"               // 点导入:可直接访问包内标识符(不推荐)
    m "math"                    // 别名导入:使用 m 代替 math
    _ "database/sql"            // 匿名导入:只执行包的init函数
)

range - 遍历

// 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)
}

获取对象类型

// 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 最佳实践

func useTicker() {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()  // 必须!防止内存泄漏
    
    // 使用ticker...
}

// 注意:ticker.Stop() 不会关闭ticker.C管道
// 已发送到管道的数据仍然可以接收

5. 并发安全模式

// 使用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()
    }()
}