一、变量与常量声明
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()
}()
}