Golang学习笔记 入门篇

Go语言学习笔记

Q1: 简短声明 := 的类型推断

  • := 用于函数内声明并初始化变量
  • 编译器根据值自动推断类型:100int50.5float64
  • 可一行声明多个不同类型的变量
  • 规则:只能在函数内使用、至少一个新变量、不能指定类型

Q2: Go的强类型特性

  • 无自动类型提升/隐式转换,不同类型不能直接运算或赋值
  • intfloat64不能直接相加,需显式转换:float64(i) + j
  • 设计目的:代码清晰、避免隐藏bug、编译时捕获错误

示例1:不同类型变量赋值错误

go 复制代码
age := 29      // age被推断为int类型
age = "naveen" // 编译错误:cannot use "naveen" (type string) as type int in assignment

示例2:不同类型变量运算错误

go 复制代码
i := 55      // int
j := 67.8    // float64
sum := i + j // 编译错误:invalid operation: i + j (mismatched types int and float64)

正确做法------显式类型转换

go 复制代码
// 将int转换为float64
sum1 := float64(i) + j

// 将float64转换为int(注意:会丢失精度)
sum2 := i + int(j)

Q3: 使用 %T 打印变量类型

  • fmt.Printf("%T", 变量) 输出变量的类型
  • 调试时确认变量类型的实用工具
go 复制代码
age := 29
name := "naveen"
fmt.Printf("age的类型是: %T\n", age)   // 输出: age的类型是: int
fmt.Printf("name的类型是: %T\n", name) // 输出: name的类型是: string

Q4: const常量特性

  • 常量值必须在编译时确定
  • 不能使用运行时才能确定的函数调用结果:const b = math.Sqrt(4)
  • 部分内置函数(如len()对字符串常量)可在编译时计算
go 复制代码
// 正确
const a = 42
const b = len("Hello")  // 编译时可计算

// 错误
const c = math.Sqrt(4)  // 编译错误:函数调用在运行时

Q5: 无类型常量

  • 常量可以是"无类型"的(只有值,无固定类型)
  • 默认类型,在需要时(如赋值给变量)提供
  • 无类型常量可赋值给兼容的不同类型变量

重要细节对比

go 复制代码
// ✅ 可行:两个都是无类型常量
var a = 5.9 / 8

// ❌ 不可行:b是float64变量,c是int变量,类型不匹配
var b = 2.9
var c = 8
var d = b / c  // 编译错误

灵活性示例

go 复制代码
const untyped = 42
var i int = untyped      // ✅ 可以作为int
var f float64 = untyped  // ✅ 也可以作为float64

有类型常量的限制

go 复制代码
const typed int = 42
var i int = typed        // ✅ 可以
var f float64 = typed    // ❌ 不能

Q6: Go函数基础

函数声明语法

go 复制代码
func 函数名(参数名 类型) 返回值类型 {
    // 函数体
    return 返回值
}

参数简写

  • 连续参数类型相同时,只需在最后一个参数后指定类型
go 复制代码
func rectProps(length, width float64) float64 {
    return length * width
}

多返回值

  • 多个返回值必须用括号括起
go 复制代码
func rectProps(length, width float64) (float64, float64) {
    area := length * width
    perimeter := (length + width) * 2
    return area, perimeter
}

命名返回值

  • 可在函数声明时给返回值命名,视为函数第一行声明的变量
  • return语句可省略返回值列表,自动返回命名变量
go 复制代码
func rectProps(length, width float64) (area, perimeter float64) {
    area = length * width
    perimeter = (length + width) * 2
    return  // 自动返回area和perimeter
}

空白符 _

  • 用作占位符,忽略不需要的返回值
go 复制代码
area, _ := rectProps(10.8, 5.6)  // 只取面积,忽略周长

函数传递

Go语言中函数传递只有值传递,即永远传递的都是数据的副本,但是副本的内容取决于类型。

类型 变量存储的内容 传递时复制的副本 函数内修改影响外部?
int, float, bool 值本身 值的副本 ❌ 不影响
struct 字段值 整个结构体副本 ❌ 不影响
array (如 [3]int) 数组元素 整个数组副本 ❌ 不影响
string 字符串头(ptr, len) 头部副本,共享底层字节 ⚠️ 不能修改,但共享数据
slice 切片头(ptr, len, cap) 头部副本,共享底层数组 ⚠️ 修改元素影响,append可能不影响
map 指针 to hmap结构 指针副本,共享同一个map ✅ 修改影响外部
channel 指针 to hchan结构 指针副本,共享同一个channel ✅ 操作影响外部
pointer 内存地址 地址副本,指向同一处 ✅ 解引用后修改影响

Q7: if-else 条件语句

标准语法

go 复制代码
if (statement); condition {
    // condition为true时执行
} else if condition {
    // 前一condition为false且当前condition为true时执行
} else {
    // 所有condition都为false时执行
}

示例

go 复制代码
if num := 10; num % 2 == 0 {
    fmt.Println("偶数")
} else {
    fmt.Println("奇数")
}
// 注意:num 只在 if 语句作用域内有效

重要特性

  1. 可选语句 :可在条件判断前执行初始化,与条件判断用分号;分隔
  2. 变量作用域:在可选语句中声明的变量仅在 if-else 块内有效

大括号位置规则

  • else 必须与上一层的右大括号 } 在同一行
  • 这是由 Go 的分号自动插入机制决定的

Go语言分号自动插入规则

当一行以以下内容结尾时,自动在行尾插入分号:

  • 标识符、字面量、break/continue/return等关键字
  • 自增运算符 ++、自减运算符 --
  • 右括号 )、右大括号 }、右方括号 ]
go 复制代码
// ✅ 正确写法
if x > 0 {
    fmt.Println("positive")
} else {
    fmt.Println("non-positive")
}

// ❌ 错误写法
if x > 0
{
    fmt.Println("positive")
}
else
{
    fmt.Println("non-positive")
}

Q8: for 循环(Go唯一循环语句)

标准语法

go 复制代码
for 初始化语句; 条件语句; 后置语句 {
    // 循环体
}

三种形式

  1. 完整形式
go 复制代码
for i := 0; i < 5; i++ {
    fmt.Println(i)  // 输出 0 1 2 3 4
}
  1. 类似 while 形式
go 复制代码
i := 0
for i < 5 {
    fmt.Println(i)
    i++
}
  1. 无限循环
go 复制代码
for {
    // 无限循环,通常配合 break 退出
}

break 语句

  • 终止整个 for 循环
go 复制代码
for i := 0; i < 10; i++ {
    if i == 5 {
        break  // i 等于 5 时终止循环
    }
    fmt.Println(i)  // 输出 0 1 2 3 4
}

continue 语句

  • 跳过当前循环的剩余代码,直接进入下一次循环
go 复制代码
for i := 0; i < 5; i++ {
    if i == 2 {
        continue  // 跳过本次循环
    }
    fmt.Println(i)  // 输出 0 1 3 4
}

Q9: switch 语句

基本语法

go 复制代码
switch 可选语句; 可选表达式 {
case 值1:
    // 执行代码
case 值2:
    // 执行代码
default:
    // 默认代码
}

带可选语句的 switch

go 复制代码
switch finger := 8; finger {
case 1:
    fmt.Println("Thumb")
case 2:
    fmt.Println("Index")
default:
    fmt.Println("incorrect finger number")
}
// finger 作用域仅限于 switch

多表达式 case

go 复制代码
switch letter {
case "a", "e", "i", "o", "u":  // 匹配任意一个
    fmt.Println("vowel")
default:
    fmt.Println("not a vowel")
}

无表达式的 switch

  • 省略表达式相当于 switch true
  • 可用于替代多个 if-else
go 复制代码
num := 75
switch {  // 等同于 switch true
case num >= 0 && num <= 50:
    fmt.Println("0-50")
case num >= 51 && num <= 100:
    fmt.Println("51-100")  // 75 匹配此项
}

fallthrough 语句

  • fallthrough 强制执行下一个 case 的代码(不判断条件)
go 复制代码
switch num := 75; {
case num < 50:
    fmt.Println("<50")
    fallthrough
case num < 100:
    fmt.Println("<100")  // 执行
    fallthrough
case num < 200:
    fmt.Println("<200")  // fallthrough 后也执行
}
// 输出:
// <100
// <200

Q10: 数组

数组定义

  • 同一类型元素的集合,不能混合不同类型
  • 表示形式:[n]T,n 为元素数量,T 为元素类型
  • 元素数量 n 是数组类型的一部分

声明方式

go 复制代码
var a [3]int                    // 声明长度为3的int数组,元素默认为零值
b := [3]int{12, 78, 50}         // 声明并初始化
c := [...]int{12, 78, 50}       // 让编译器自动计算长度

重要特性

  • 长度是类型的一部分[5]int[25]int 是不同类型
  • 值类型:数组赋值或传参时复制整个数组,修改副本不影响原数组
  • 不可调整大小:数组长度固定

遍历数组

go 复制代码
a := [...]float64{67.7, 89.8, 21, 78}
for i, v := range a {  // range 返回索引和值
    fmt.Printf("索引:%d 值:%.2f\n", i, v)
}

多维数组

go 复制代码
// 二维数组示例
matrix := [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

// 访问元素
value := matrix[1][2]  // 第2行第3列,值为6

Q11: 切片

切片定义

  • 切片是对数组的包装(引用),本身不拥有数据
  • 动态长度,更灵活

区分数组与切片

  • [n]T → 数组(有数字,固定长度,值类型)
  • []T → 切片(无数字,动态长度,引用类型)

创建切片

go 复制代码
// 从数组创建
a := [5]int{76, 77, 78, 79, 80}
b := a[1:4]  // [77 78 79] 从索引1到3

// 直接创建切片
// 先创建了一个匿名的底层数组,再创建切片并指向这个数组
c := []int{76, 77, 78, 79, 80}


// 使用 make 创建
d := make([]int, 5)     // 长度5,容量5
e := make([]int, 5, 10) // 长度5,容量10

make(类型,长度,容量):会创建底层数组,并初始化切片的lencap
make是 Go 语言专门用于创建并初始化引用类型的内置函数,它的核心功能有两个:

  • 分配内存:为引用类型(切片 slice、映射 map、通道 chan)分配底层存储空间;
  • 初始化结构:设置该类型的默认属性(比如切片的长度 / 容量、map 的哈希表、通道的缓冲区)。

长度与容量

  • len(s):切片中元素个数
  • cap(s):从切片起始位置到底层数组末尾的元素个数

追加元素 append

go 复制代码
cars := []string{"Ferrari", "Honda", "Ford"}
cars = append(cars, "Toyota")  // 返回新切片

// 容量不足时扩容规则:
// - 原切片长度 < 1024:新[容量] ≈ 原[容量] × 2
// - 原切片长度 ≥ 1024:新[容量] ≈ 原[容量] × 1.25

nil 切片

go 复制代码
var names []string  // nil切片,len=0, cap=0
names = append(names, "John")  // 可直接追加

合并切片

go 复制代码
veggies := []string{"potatoes", "tomatoes"}
fruits := []string{"oranges", "apples"}
food := append(veggies, fruits...)  // 使用...展开切片

切片是引用类型

  • 切片作为函数参数传递时,函数内修改会影响原切片
go 复制代码
func subtractOne(numbers []int) {
    for i := range numbers {
        numbers[i] -= 2  // 修改原底层数组
    }
}

多维切片

go 复制代码
pls := [][]string{
    {"C", "C++"},
    {"JavaScript"},
    {"Go", "Rust"},
}

内存优化

  • copy 创建副本,释放原数组
go 复制代码
countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
needed := countries[:len(countries)-2]
cpy := make([]string, len(needed))
copy(cpy, needed)  // 复制到新切片,原匿名数组可被回收

copy(dst []T, src []T) int:安全复制切片的 "元素值",复制后两个切片底层数组完全独立

  • dst:目标切片(接收复制的元素);

  • src:源切片(提供复制的元素);

  • T:切片的元素类型(两个切片的元素类型必须完全一致,比如都是[]string[]int);

  • 返回值:int 类型,代表实际复制的元素个数(= min (len (dst), len (src)))。

思考:为了避免多个切片共享同一个底层数组时,出现一个切片通过append修改后影响其他切片,可以怎么做:

  1. 设置原始切片大小,一旦需要append必然会需要扩容,此时新切片和原切片不再共享底层数组
  2. 类似于方法1,我们可以通过三索引截断法,例如要获取某个数组的前三个元素:newSlice:=array[:3:3],通过设置截断长度等于容量,使得append时必扩容
  3. 使用copy深拷贝

Q12: 可变参数函数

定义

  • 参数个数可变的函数
  • 最后一个参数用 ...T 表示,可接受任意个 T 类型参数

语法规则

  • 只有函数的最后一个参数才允许是可变参数
  • 可变参数在函数内部被当作切片处理

基本示例

go 复制代码
func find(num int, nums ...int) {
    // nums 在函数内部是 []int 类型切片
    for i, v := range nums {
        fmt.Println(i, v)
    }
}

// 调用方式
find(89, 89, 90, 95)     // 传递多个参数
find(87)                  // 不传可变参数(nums 为空切片)

工作原理

  • 编译器将传入的可变参数转换为对应类型的切片
  • 例如 find(89, 89, 90, 95)nums 变为 []int{89, 90, 95}

传入切片

go 复制代码
nums := []int{89, 90, 95}

// ❌ 错误:不能直接将切片传给可变参数
find(89, nums)  // 编译错误

// ✅ 正确:使用 ... 后缀
find(89, nums...)  // 直接将切片作为可变参数传入

... 语法糖

  • 在切片后加上 ... 可将切片直接传入可变参数函数
  • 此时不会创建新切片,原切片直接被使用
go 复制代码
func change(s ...string) {
    s[0] = "Go"  // 修改会影响原切片
}

welcome := []string{"hello", "world"}
change(welcome...)  // 原切片被修改
fmt.Println(welcome)  // 输出 [Go world]

注意事项

  • 可变参数函数中修改切片元素会影响原切片(因为引用同一底层数组)
  • 但执行 append 可能触发扩容,扩容后对新切 片的修改不影响原切片
go 复制代码
func change(s ...string) {
    s[0] = "Go"           // 影响原切片
    s = append(s, "playground")  // 可能触发扩容,后续修改不影响原切片
}

Q13: Map(映射/字典)

Map 定义

  • Map 是 Go 的内置类型,用于存储键值对(key-value pairs)
  • 无序集合,键不可重复
  • 零值是 nilnil map 不能添加元素

声明与创建

  1. 使用 make 创建
go 复制代码
// 创建空 map
m := make(map[string]int)
// 创建并指定初始容量
m := make(map[string]int, 100)

Map底层是一个哈希表结构,如果直接声明且未进行初始化的话,此时Map的值为nil,即未创建底层哈希表结构,因此需要使用make进行声明与初始化,分配哈希表的内存空间再进行添加key
make是 Go 语言专门用于创建并初始化引用类型的内置函数,它的核心功能有两个:

  • 分配内存:为引用类型(切片 slice、映射 map、通道 chan)分配底层存储空间;
  • 初始化结构:设置该类型的默认属性(比如切片的长度 / 容量、map 的哈希表、通道的缓冲区)。
  1. 声明
go 复制代码
var m map[string]int  // nil map
  1. 字面量创建
go 复制代码
m := map[string]int{
    "a": 1,
    "b": 2,
    "c": 3,
}

// 空 map
m := map[string]int{}

操作方法

  1. 添加/修改元素
go 复制代码
m["key"] = value  // 如果键存在则修改,不存在则添加
  1. 访问元素
go 复制代码
value := m["key"]  // 如果键不存在,返回值类型的零值

// 二值语法:获取值的同时判断键是否存在
value, ok := m["key"]
if ok {
    fmt.Println("键存在,值为:", value)
} else {
    fmt.Println("键不存在")
}
  1. 删除元素
go 复制代码
delete(m, "key")  // 删除键对应的元素
// 如果键不存在,不会报错,只是什么都不做
  1. 遍历 map
go 复制代码
// 遍历键值对
for key, value := range m {
    fmt.Println(key, value)
}

// 只遍历键
for key := range m {
    fmt.Println(key)
}

常用内置函数

  • len(m):返回 map 中键值对的数量
  • delete(m, key):删除指定键的元素

注意事项

  1. nil map 问题
go 复制代码
var m map[string]int
// m["a"] = 1  // 运行时错误:assignment to entry in nil map

// 必须先初始化
m = make(map[string]int)
m["a"] = 1  // 正确
  1. 遍历顺序随机
go 复制代码
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)
}
// 每次运行输出顺序可能不同
  1. 不支持切片、函数、map 作为键
go 复制代码
m := make(map[[]string]int)  // 编译错误:slice cannot be a map key
m := make(map[map[string]int]int)  // 编译错误:map cannot be a map key

原因是:map 需要能够判断两个键是否相等以确保每个键的唯一性,map 键的数据类型必须满足以下条件:

  • 可比较性(Comparable):用于定义 map 键的类型必须是可比较的,也就是说,Go 语言能够确定两个相同类型的键是否相等。这要求该类型支持 == 操作符来进行比较。
  • 不可变性(Immutable):虽然 Go 语言规范并未明确指出键必须不可变,但由于 map的内部实现机制,键在创建后不能改变,因此通常选择不可变类型作为键。
  1. 支持结构体作为键(如果结构体所有字段可比较)
go 复制代码
type Point struct {
    X int
    Y int
}
m := make(map[Point]string)
  1. map 是引用类型
go 复制代码
func modify(m map[string]int) {
    m["new"] = 100  // 修改会影响原 map
}

m := make(map[string]int)
modify(m)
fmt.Println(m)  // 输出: map[new:100]
  1. 并发不安全
go 复制代码
// 多个 goroutine 同时读写 map 会导致运行时错误
// 需要使用 sync.Mutex 或 sync.Map 保证线程安全
var mu sync.Mutex
mu.Lock()
m["key"] = value
mu.Unlock()

多返回值应用示例

go 复制代码
scores := map[string]int{"Alice": 90, "Bob": 85}

// 检查并获取值
if score, ok := scores["Alice"]; ok {
    fmt.Println("Alice 的分数:", score)
} else {
    fmt.Println("Alice 不存在")
}

// 检查键不存在的情况
if score, ok := scores["Charlie"]; !ok {
    fmt.Println("Charlie 不存在,使用默认值 0")
    score = 0
}

Q14: 包 (Package)

什么是包

  • 包用于组织 Go 源代码,提供更好的可重用性与可读性
  • 使得 Go 应用程序易于维护
  • 将相关功能的代码组织在同一个包中,便于复用

main 函数和 main 包

  • 所有可执行程序必须有一个 main 函数,作为程序入口
  • main 函数必须放在 main 包中
  • package packagename 必须放在源文件第一行

创建自定义包

  • 包的源文件必须放在同名文件夹中(Go 惯例)
  • 例如:rectangle 包 → rectangle/ 文件夹

导入包

go 复制代码
import "geometry/rectangle"  // 导入自定义包

导出名字 (Exported Names)

  • 以大写字母开头的变量/函数才是导出的
  • 其他包只能访问导出的函数和变量
  • 小写开头的名称是私有的
go 复制代码
func Area(len, wid float64) float64  // 导出,可被其他包访问
func area(len, wid float64) float64   // 未导出,私有

init 函数

  • 每个包可以有 init 函数
  • 不能有返回值和参数,不能显式调用
  • 用于初始化验证或准备资源
  • 初始化顺序
    1. 先初始化被导入的包
    2. 然后初始化包级别变量
    3. 调用 init 函数
    4. 最后调用 main 函数
  • 一个包只会被初始化一次(即使被多次导入)
go 复制代码
func init() {
    fmt.Println("包初始化完成")
}

空白标识符 _

  • 导入但不使用包会编译错误:imported and not used: "package"
  • 使用 _ 可以导入并初始化包,但不使用其中的任何内容
go 复制代码
// 场景1:屏蔽未使用包的错误(临时开发用)
import "geometry/rectangle"
var _ = rectangle.Area  // 错误屏蔽器,使用后应移除

// 场景2:仅需初始化包(使用 init 函数的功能)
import _ "geometry/rectangle"
// 程序启动时会执行 rectangle 包的 init 函数
// 但无法使用其中的任何函数或变量
func main() {
    // rectangle 包已被初始化,但不可见
}

Q15: 字符串 (String)

字符串本质

  • Go 中字符串是字节切片(但不是严格意义上的切片)
  • 内部结构:包含指向底层数组的指针和长度
  • 使用双引号 "" 创建
  • 兼容 Unicode,使用 UTF-8 编码
go 复制代码
// 字符串的内部结构(简化理解)
type string struct {
    data *byte  // 指向底层数组的指针
    len  int    // 长度
}

获取字符串的字节

go 复制代码
name := "Hello"
for i := 0; i < len(name); i++ {
    fmt.Printf("%x ", name[i])  // 16进制字节
}
// 输出:48 65 6c 6c 6f

rune 类型

  • runeint32 的别名
  • 表示 Unicode 代码点(一个字符)
  • 一个 rune 可以表示任意编码的字符(无论占用多少字节)
go 复制代码
// 问题:直接遍历字符串会出错
name := "Señor"
// 错误:ñ 的 UTF-8 编码占2个字节 (c3 b1),会被拆成两个"字符"

// 正确:先转换为 rune 切片
runes := []rune(name)
for i := 0; i < len(runes); i++ {
    fmt.Printf("%c", runes[i])  // 正确输出:Señor
}

for range 遍历(推荐)

  • for range 会自动按 rune(字符)遍历,而不是字节
go 复制代码
for index, rune := range name {
    fmt.Printf("%c starts at byte %d\n", rune, index)
}
// 输出:
// S starts at byte 0
// e starts at byte 1
// ñ starts at byte 2  # 注意:ñ占2个字节
// o starts at byte 4
// r starts at byte 5

计算字符串长度

go 复制代码
import "unicode/utf8"

len("Señor")                   // 6(字节数)
utf8.RuneCountInString("Señor") // 5(字符数)

字符串不可变

go 复制代码
s := "hello"
s[0] = 'a'  // 编译错误:cannot assign to s[0]

// 修改方法:先转为 rune 切片
s := []rune("hello")
s[0] = 'a'
result := string(s)  // "aello"

字节/rune 切片与字符串转换

go 复制代码
// 字符串 → 字节切片(复制数据)
byteSlice := []byte("Hello")

// 字符串 → rune 切片(复制数据)
runeSlice := []rune("Señor")

// 字节切片 → 字符串
str := string(byteSlice)

// rune 切片 → 字符串
str := string(runeSlice)

理解切片

  • 切片是一个结构体,包含三个部分:
go 复制代码
type slice struct {
    array unsafe.Pointer  // 指向底层数组的指针
    len   int            // 长度(元素个数)
    cap   int            // 容量(底层数组能容纳的元素个数)
}
  • 切片是对底层数组的引用/视图,不是数据的复制
  • 类型转换(如 []rune(s))会创建新的底层数组,复制数据

Q16: 指针 (Pointer)

什么是指针

  • 指针是存储变量内存地址的变量
  • 指针变量本身也有地址
go 复制代码
b := 255
a := &b  // & 获取变量的地址
// a 是指针,存储了 b 的内存地址

指针的声明

go 复制代码
var a *int = &b  // *int 表示指向 int 类型变量的指针
fmt.Println(a)    // 打印地址,如 0x1040a124
fmt.Printf("%T", a)  // 输出:*int

指针的零值

  • 指针的零值是 nil
go 复制代码
var p *int
fmt.Println(p)  // <nil>

指针的解引用

  • 使用 *指针 获取或修改指向变量的值
go 复制代码
b := 255
a := &b
fmt.Println(*a)  // 255(获取值)
*a++              // 通过指针修改原变量
fmt.Println(b)   // 256

向函数传递指针参数

go 复制代码
func change(val *int) {
    *val = 55  // 修改指针指向的值
}
func main() {
    a := 58
    change(&a)  // 传递地址
    fmt.Println(a)  // 55
}

不要传递数组指针,用切片代替

go 复制代码
// ❌ 不推荐:传数组指针
func modify(arr *[3]int) {
    arr[0] = 90
}

// ✅ 推荐:使用切片
func modify(sls []int) {
    sls[0] = 90
}
func main() {
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)  // [90, 90, 91]
}

Go 不支持指针运算

go 复制代码
b := [...]int{109, 110, 111}
p := &b
p++  // 编译错误:invalid operation: p++ (non-numeric type *[3]int)

Q17: 结构体 (Struct)

什么是结构体

  • 结构体是用户定义的类型,表示若干个字段的集合
  • 将相关数据组合在一起
go 复制代码
type Employee struct {
    firstName string
    lastName  string
    age       int
    salary    int
}

声明结构体

go 复制代码
// 命名结构体
type Employee struct {
    firstName, lastName string  // 同类型可写在一行
    age, salary         int
}

// 匿名结构体
var employee struct {
    firstName string
    age       int
}
特性 命名结构体 匿名结构体
定义方式 先定义类型,后声明变量。type T struct { ... } var t T 定义类型与声明变量合二为一。var t struct { ... }
复用性 。定义一次,可在整个包甚至外部包(若导出)中无限次使用。 。一次性使用。若需另一个相同结构的变量,必须重写整个定义。
类型名 有名字(如 Employee),可用于类型断言、类型转换、方法绑定。 无名字。只有变量名(如 employee),无法进行类型断言或绑定方法。
主要场景 业务实体、数据模型、复杂对象。 配置选项、临时数据聚合、测试数据、JSON 解析。

创建结构体变量

go 复制代码
// 使用字段名
emp1 := Employee{
    firstName: "Sam",
    age:       25,
    salary:    500,
    lastName:  "Anderson",
}

// 不使用字段名(需按声明顺序)
emp2 := Employee{"Thomas", "Paul", 29, 800}

结构体的零值

  • 未初始化的结构体,字段默认为零值
go 复制代码
var emp Employee  // {"" "" 0 0}
emp.firstName = "Jack"  // 后续赋值

访问结构体字段

go 复制代码
emp.firstName = "Sam"
fmt.Println(emp.firstName)

结构体指针

go 复制代码
emp8 := &Employee{"Sam", "Anderson", 55, 6000}

// 两种访问方式都可以
fmt.Println((*emp8).firstName)
fmt.Println(emp8.firstName)  // Go 允许直接访问

匿名字段

  • 字段没有名字,只有类型
  • 字段名默认为类型名
go 复制代码
type Person struct {
    string
    int
}

p := Person{"Naveen", 50}
p.string  // 访问方式:类型名作为字段名
p.int

嵌套结构体

go 复制代码
type Address struct {
    city  string
    state string
}

type Person struct {
    name    string
    age     int
    address Address
}

p := Person{
    name:    "Naveen",
    address: Address{city: "Chicago"},
}
fmt.Println(p.address.city)  // 访问嵌套字段

提升字段

如果是结构体中有匿名的结构体类型字段,则该匿名结构体里的字段就称为提升字段。这是因为提升字段就像是属于外部结构体一样,可以用外部结构体直接访问。

go 复制代码
type Person struct {
    name string
    Address  // 匿名字段,Address 是结构体
}

p := Person{name: "Naveen", Address: Address{city: "Chicago"}}
p.city  // 直接访问,就像字段在 Person 中一样

导出结构体和字段

  • 结构体名/字段名首字母大写 → 可导出(其他包可见)
  • 首字母小写 → 私有
go 复制代码
type Spec struct {
    Maker string  // 导出
    model string  // 未导出
}

结构体相等性

  • 结构体是值类型
  • 如果所有字段可比较,则结构体可比较
go 复制代码
name1 := name{"Steve", "Jobs"}
name2 := name{"Steve", "Jobs"}
name1 == name2  // true
  • 包含 map/slice 等不可比较字段的结构体不可比较
go 复制代码
type image struct {
    data map[int]int  // map 不可比较
}
// image1 == image2  // 编译错误

Q18: 方法 (Methods)

什么是方法?

方法是带有接收器函数 。接收器是特殊参数,在 func 关键字和方法名中间声明。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的

要求:方法接收器类型和方法的定义必须在同一个包中。

go 复制代码
func (t Type) methodName(parameter list) {
}

创建了一个接收器类型为 Type 的方法 methodName

方法示例

go 复制代码
type Employee struct {
    name string
    salary int
    currency string
}

func (e Employee) displaySalary() {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

emp1 := Employee{name: "Sam", salary: 5000, currency: "$"}
emp1.displaySalary()  // 调用方法

为什么需要方法?

  • Go 不是纯粹的面向对象语言,不支持类,方法是实现类行为的途径
  • 相同方法名可以定义在不同类型上
go 复制代码
type Rectangle struct { length, width int }
type Circle struct { radius float64 }

func (r Rectangle) Area() int { return r.length * r.width }
func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius }

值接收器 vs 指针接收器

值接收器:方法内的修改对调用者不可见

go 复制代码
func (e Employee) changeName(newName string) {
    e.name = newName  // 不影响原值
}

指针接收器:方法内的修改对调用者可见

go 复制代码
func (e *Employee) changeAge(newAge int) {
    e.age = newAge  // 影响原值
}

使用建议:

  • 需要修改原值时用指针接收器
  • 结构体较大时用指针接收器避免拷贝
  • 其他情况可用值接收器

匿名字段的方法

属于结构体的匿名字段的方法可以直接调用:

go 复制代码
type address struct {
    city string
    state string
}

func (a address) fullAddress() {
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

type person struct {
    firstName string
    lastName string
    address  // 匿名字段
}

p := person{firstName: "Elon", lastName: "Musk"}
p.fullAddress()  // 直接调用匿名字段的方法

方法接收器的特殊规则

接收器方法:无论是值接收器还是指针接收器,其都可以接受值和指针

go 复制代码
// 值接收器
func (r rectangle) area() {}
r.area()      // OK
(&r).area()   // OK,Go自动解引用
p := &r
p.area()      // OK
go 复制代码
// 指针接收器方法
func (r *rectangle) perimeter() {}
r.perimeter()     // OK,Go自动取地址
(&r).perimeter() // OK
p := &r
p.perimeter()    // OK

函数参数:必须严格匹配

go 复制代码
func area(r rectangle) {}     // 只接受值
func perimeter(r *rectangle) {}  // 只接受指针
area(p)     // 错误,指针不能传给值参数
perimeter(r)  // 错误,值不能传给指针参数

在非结构体类型上定义方法

无论是否为在结构体类型上定义方法,都需要满足基本的要求 (方法接收器类型和方法的定义必须在同一个包中。)

go 复制代码
// 不能直接给内置类型 int 添加方法,因为内置类型int的定义不在该包中,为此我们可以创建新的以int为基础的新类型

// func (a int) add(b int) {}  // 编译错误

// 解决方法:创建新类型
type myInt int
// 不能通过别名
// type myInt = int

func (a myInt) add(b myInt) myInt {
    return a + b
}

num1 := myInt(5)//通过类型转换为myInt 而非构造函数,Go语言无构造函数
num2 := myInt(10)
sum := num1.add(num2)  // 15

在上面的代码中,我们提到了对于内置的类型,如果想要为其绑定方法,必须使用新类型,而不能使用别名,这是因为通过定义新类型会使得该类型具有独立的类型描述符(即与原类型不等价),而如果使用别名(共享类型描述符),仍然会出现类型定义与类型绑定方法不在同一个包的问题

Q19: 接口 (Interface)

什么是接口?

  • 接口是方法签名的集合
  • 当一个类型定义了接口中的所有方法,称它实现了该接口
  • Go 采用隐式实现 ,无需 implement 关键字
go 复制代码
type VowelsFinder interface {
    FindVowels() []rune
}

type MyString string

func (ms MyString) FindVowels() []rune {
    // 实现方法
}
// MyString 隐式实现了 VowelsFinder 接口

接口的声明与实现

go 复制代码
type VowelsFinder interface {
    FindVowels() []rune
}

type MyString string

func (ms MyString) FindVowels() []rune {
    var vowels []rune
    for _, rune := range ms {
        if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
            vowels = append(vowels, rune)
        }
    }
    return vowels
}

func main() {
    name := MyString("Sam Anderson")
    var v VowelsFinder = name  // MyString 实现了接口,可以赋值,此时v的类型为MyString
    fmt.Printf("Vowels are %c", v.FindVowels())  // 输出: Vowels are [a e o]
}

接口的实际用途

  • 接口允许不同类型统 一调用
  • 增加新类型时无需修改原有函数
go 复制代码
type SalaryCalculator interface {
    CalculateSalary() int
}

type Permanent struct {
    empId    int
    basicpay int
    pf       int
}

type Contract struct {
    empId    int
    basicpay int
}

func (p Permanent) CalculateSalary() int {
    return p.basicpay + p.pf
}

func (c Contract) CalculateSalary() int {
    return c.basicpay
}

func totalExpense(s []SalaryCalculator) int {
    expense := 0
    for _, v := range s {
        expense += v.CalculateSalary()
    }
    return expense
}

func main() {
    pemp1 := Permanent{1, 5000, 20}
    pemp2 := Permanent{2, 6000, 30}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    fmt.Printf("Total Expense Per Month $%d", totalExpense(employees))
}

接口的内部表示

  • 接口内部是 (type, value) 元组
  • type 是具体类型,value 是具体值
go 复制代码
type Test interface {
    Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {
    fmt.Println(m)
}

func describe(t Test) {
    fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {
    var t Test
    f := MyFloat(89.7)
    t = f
    describe(t)  // Interface type main.MyFloat value 89.7
}

空接口

  • 空接口 interface{} 没有方法
  • 所有类型都实现了空接口
  • 用于接收任意类型的值
go 复制代码
func describe(i interface{}) {
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {
    describe("Hello World")  // Type = string, value = Hello World
    describe(55)             // Type = int, value = 55
    describe(struct{ name string }{name: "Naveen"})
}

类型断言

  • 用于提取接口的底层值
  • 语法:i.(T)
go 复制代码
// 基本语法
v := i.(int)  // 如果类型不匹配会 panic

// 安全语法
v, ok := i.(int)
// ok=true 表示类型匹配,v 是提取的值
// ok=false 表示类型不匹配,v 是零值,不会 panic
go 复制代码
func assert(i interface{}) {
    v, ok := i.(int)
    fmt.Println(v, ok)
}

func main() {
    var s interface{} = 56
    assert(s)  // 56 true

    var i interface{} = "Steven Paul"
    assert(i)  // 0 false
}

类型选择(Type Switch)

  • 用于比较接口的具体类型,类型选择的语法类似于类型断言。类型断言的语法是 i.(T),而对于类型选择,类型 T 由关键字 type 代替
  • 语法:switch i.(type)
go 复制代码
func findType(i interface{}) {
    switch i.(type) {
    case string:
        fmt.Printf("I am a string and my value is %s\n", i.(string))
    case int:
        fmt.Printf("I am an int and my value is %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}

func main() {
    findType("Naveen")   // I am a string and my value is Naveen
    findType(77)        // I am an int and my value is 77
    findType(89.98)    // Unknown type
}
  • 还可以与接口类型比较

    如果一个类型实现了接口,那么该类型与其实现的接口就可以互相比较。

go 复制代码
type Describer interface {
    Describe()
}

type Person struct {
    name string
    age  int
}

func (p Person) Describe() {
    fmt.Printf("%s is %d years old", p.name, p.age)
}

func findType(i interface{}) {
    switch v := i.(type) {
    case Describer:
        v.Describe()
    default:
        fmt.Printf("unknown type\n")
    }
}

func main() {
    findType("Naveen")              // unknown type
    findType(Person{"Naveen R", 25}) // Naveen R is 25 years old
}

在上面程序中,结构体 Person 实现了 Describer 接口。在第 19 行的 case 语句中,v 与接口类型 Describer 进行了比较。p 实现了 Describer,因此满足了该 case 语句,于是当程序运行到第 32 行的 findType(p) 时,程序调用了 Describe() 方法。

指针接受者与值接受者

  • 使用值接受者实现接口:值和指针都可以赋值给接口

  • 使用指针接受者实现接口:只能用指针赋值给接口

  • 本质上是Go中方法集的规则

    • 值类型 T 的方法集 :仅包含所有以 T 为接收器(值接收器)的方法。
    • 指针类型 *T 的方法集 :包含所有以 T*T 为接收器的方法(值接收器 + 指针接收器)。
    接收器类型 T 的方法集包含 *T 的方法集包含
    (t T)
    (t *T)
go 复制代码
type Describer interface {
    Describe()
}

type Person struct {
    name string
    age  int
}

func (p Person) Describe() {}  // 值接受者实现

type Address struct {
    state   string
    country string
}

func (a *Address) Describe() {}  // 指针接受者实现

func main() {
    var d1 Describer
    p1 := Person{"Sam", 25}
    d1 = p1  // OK:值接受者实现,值可以赋值
    d1 = &p1 // OK:指针也可以赋值

    var d2 Describer
    a := Address{"Washington", "USA"}
    // d2 = a   // 错误:Address 值没有实现接口
    d2 = &a  // OK:必须用指针赋值
}

实现多个接口

  • 一个类型可以实现多个接口
go 复制代码
type SalaryCalculator interface {
    DisplaySalary()
}

type LeaveCalculator interface {
    CalculateLeavesLeft() int
}

type Employee struct {
    firstName   string
    lastName    string
    basicPay    int
    pf          int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {
    fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}

func (e Employee) CalculateLeavesLeft() int {
    return e.totalLeaves - e.leavesTaken
}

func main() {
    e := Employee{
        firstName:   "Naveen",
        lastName:    "Ramanathan",
        basicPay:    5000,
        pf:          200,
        totalLeaves: 30,
        leavesTaken: 5,
    }
    var s SalaryCalculator = e
    s.DisplaySalary()

    var l LeaveCalculator = e
    fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}

接口的嵌套

  • 可以嵌套其他接口创建新接口
go 复制代码
type SalaryCalculator interface {
    DisplaySalary()
}

type LeaveCalculator interface {
    CalculateLeavesLeft() int
}

// EmployeeOperations 嵌套了两个接口
type EmployeeOperations interface {
    SalaryCalculator
    LeaveCalculator
}

// Employee 实现了 EmployeeOperations
type Employee struct {
    firstName   string
    lastName    string
    basicPay    int
    pf          int
    totalLeaves int
    leavesTaken int
}

func (e Employee) DisplaySalary() {}
func (e Employee) CalculateLeavesLeft() int { return 0 }

func main() {
    var empOp EmployeeOperations = Employee{}
    empOp.DisplaySalary()
    empOp.CalculateLeavesLeft()
}

接口的零值

  • 接口的零值是 nil
  • nil 接口的底层类型和值都为 nil
  • 调用 nil 接口的方法会 panic
go 复制代码
type Describer interface {
    Describe()
}

func main() {
    var d1 Describer
    fmt.Printf("d1 is nil and has type %T value %v\n", d1, d1)
    // d1 is nil and has type <nil> value <nil>

    // d1.Describe()  // panic: nil pointer dereference
}

Q20: 并发入门 (Concurrency)

什么是并发?

  • 并发是指立即处理多个任务的能力
  • 在同一时间间隔内交替执行多个任务
  • 一个人跑步时鞋带松了,停下来系鞋带,然后继续跑 --- 这就是并发

什么是并行?

  • 并行是指同时处理多个任务
  • 在同一时刻真正同时执行多个任务
  • 一个人跑步时同时听音乐 --- 这就是并行

并发与并行的区别

  • 并发:同一时间间隔内交替执行多个任务(单核处理器也能实现)
  • 并行:同一时刻真正同时执行多个任务(需要多核处理器)

技术角度理解

  • 并发:进程在不同时间点开始,交替运行

    • 单核处理器通过上下文切换实现
    • 组件间通信开销小
  • 并行:进程在同一时刻同时运行

    • 多核处理器上不同组件在不同核上运行
    • 组件间通信开销可能很高
    • 注意:并行不一定更快,因为有通信开销

Go 对并发的支持

  • Go 是并发式语言,而不是并行式语言
  • Go 使用 Go 协程 (Goroutine)信道 (Channel) 来处理并发

Q21: Go 协程 (Goroutine)

什么是 Go 协程?

  • Go 协程是与其他函数或方法一起并发运行的函数或方法
  • 可以看作是轻量级线程
  • 创建成本很小,Go 应用中常有数以千计的协程并发运行

Go 协程 vs 线程

  • 成本极低:堆栈大小只有若干 KB,可根据需求增减
  • 复用线程:数以千计的协程可能只用一个 OS 线程
  • 自动调度:如果某协程阻塞,系统会创建新线程并移动其他协程

如何启动 Go 协程?

  • 在函数调用前加上 go 关键字
go 复制代码
func hello() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go hello()  // 启动协程
    fmt.Println("main function")
}

协程的重要特性

  1. 启动立即返回:调用协程后,程序控制立即返回,不会等待协程执行完毕
  2. 主协程终止则程序终止:如果主协程结束,其他协程不会继续运行
go 复制代码
// 问题示例
func main() {
    go hello()
    fmt.Println("main function")
    // 输出: main function
    // hello 协程可能没机会运行
}

// 解决:让主协程等待
func main() {
    go hello()
    time.Sleep(1 * time.Second)  // 等待协程执行
    fmt.Println("main function")
}

启动多个协程

go 复制代码
func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}

func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}

func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

// 输出: 1 a 2 3 b 4 c 5 d e main terminated

注意事项

  • 协程间的通信使用信道 (Channel),避免共享内存导致的竞态条件
  • 实际开发中常用信道来阻塞主协程,而不是 time.Sleep

Q22: 信道 (Channel)

什么是信道?

  • 信道是 Go 协程之间通信的管道
  • 如同管道中的水从一端流到另一端,数据也可以从一端发送,在另一端接收
  • 信道都关联一个类型,信道只能运输这种类型的数据

信道的声明

  • chan T 表示 T 类型的信道
  • 信道的零值是 nil,无实际意义,应该使用make来定义信道
go 复制代码
var a chan int  // 声明信道
a = make(chan int)  // 初始化

// 简短声明
a := make(chan int)

通过信道发送和接收

go 复制代码
data := <- a  // 从信道读取数据
a <- data     // 向信道写入数据
  • 箭头方向指向信道:发送/写入数据
  • 箭头方向离开信道:接收/读取数据

发送与接收默认阻塞

  • 发送数据时,程序阻塞直到有协程接收数据;接收数据时,程序阻塞直到有协程发送数据
  • 这是 Go 协程高效通信的基础,不需要用到其他编程语言常见的显式锁或条件变量。
  • 这是默认行为,信道默认为无缓冲信道,无缓冲信道的发送和接收过程都是阻塞的。

使用信道示例

go 复制代码
func hello(done chan bool) {
    fmt.Println("Hello world goroutine")
    done <- true
}

func main() {
    done := make(chan bool)
    go hello(done)
    <-done  // 阻塞等待协程完成
    fmt.Println("main function")
}
// 输出:
// Hello world goroutine
// main function

死锁

  • 当有协程给信道发送数据时,需要其他协程接收,如果没有的话,程序就会在运行时触发 panic,形成死锁。
  • 当协程等待从信道接收数据时,需要其他协程发送,否则 panic
go 复制代码
func main() {
    ch := make(chan int)
    ch <- 5  // fatal error: all goroutines are asleep - deadlock!
}

单向信道

  • 双向信道:可以发送和接收
  • 唯送信道 chan<- int:只能发送
  • 唯收信道 <-chan int:只能接收
go 复制代码
// 唯送信道
sendch := make(chan<- int)
sendch <- 10  // OK
<-sendch      // 编译错误

// 唯收信道
recvch := make(<-chan int)
<-recvch     // OK
recvch <- 10 // 编译错误

信道转换

  • 可以将双向信道转换为单向信道
  • 常用于函数参数,限制信道用途
go 复制代码
func sendData(sendch chan<- int) {
    sendch <- 10  // 只能发送
}

func main() {
    cha1 := make(chan int)
    go sendData(cha1)  // 传递双向信道,会自动转换为唯送信道
    fmt.Println(<-cha1) // 主协程接收
}

关闭信道

  • 语法:close(chanel)

  • 发送方可以关闭信道,通知接收方不再有数据

  • 接收方可以多使用一个变量来接收信道状态,例如使用 ok 检查信道是否关闭:如果成功接收信道所发送的数据,那么 ok 等于 true。而如果 ok 等于 false,说明我们试图读取一个关闭的通道。从关闭的信道读取到的值会是该信道类型的零值。

go 复制代码
v, ok := <- ch
go 复制代码
func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)  // 关闭信道
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received", v, ok)
    }
}

for range 遍历信道

  • 使用for range能够自动遍历信道直到关闭
go 复制代码
func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received", v)
    }
}

Q23: 缓冲信道和工作池

什么是缓冲信道?

  • 无缓冲信道:发送和接收都是阻塞的
  • 缓冲信道:带有缓冲区,只有在缓冲满时才阻塞发送,只有在缓冲空时才阻塞接收
  • 创建语法:ch := make(chan type, capacity)
go 复制代码
ch := make(chan string, 2)  // 容量为2的缓冲信道
ch <- "naveen"
ch <- "paul"  // 不会阻塞

缓冲信道的阻塞时机

  • 发送阻塞:缓冲已满时
  • 接收阻塞:缓冲为空时
go 复制代码
func write(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i  // 当缓冲满时阻塞
        fmt.Println("successfully wrote", i)
    }
    close(ch)
}

func main() {
    ch := make(chan int, 2)  // 容量为2
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v)
        time.Sleep(2 * time.Second)
    }
}

死锁

向容量已满的缓冲信道发送数据会引发死锁:

go 复制代码
ch := make(chan string, 2)
ch <- "naveen"
ch <- "paul"
ch <- "steve"  // 超过容量,阻塞 → 死锁

长度 vs 容量

  • 容量 (cap):信道可以存储的元素数量(创建时指定)
  • 长度 (len):当前排队的元素个数
go 复制代码
ch := make(chan string, 3)
ch <- "naveen"
ch <- "paul"
fmt.Println("capacity is", cap(ch))  // 3
fmt.Println("length is", len(ch))    // 2
fmt.Println("read value", <-ch)
fmt.Println("new length is", len(ch))  // 1

WaitGroup

  • WaitGroup是一个结构体,其内部存储了计数器、等待者数、信号量等信息

  • 用于等待一批 Go 协程执行结束,程序控制会一直阻塞,直到这些协程全部执行完毕

  • 计数器工作模式:Add(n) 增加计数,Done() 递减,Wait() 阻塞直到计数为0

go 复制代码
func process(i int, wg *sync.WaitGroup) {
    fmt.Println("started Goroutine", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done()  // 计数器减1
}

func main() {
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1)  // 计数器加1
        go process(i, &wg)  // 传递指针
    }
    wg.Wait()  // 阻塞直到计数为0
    fmt.Println("All go routines finished executing")
}

重要:传递 wg 的地址,否则每个协程得到的是拷贝,主协程无法感知完成

工作池实现

工作池是一组等待任务分配的协程,完成任务后继续等待新任务。

核心组件

  1. 任务信道 (jobs):分发作业
  2. 结果信道 (results):收集结果
  3. WaitGroup:等待所有 worker 完成
go 复制代码
type Job struct {
    id       int
    randomno int
}

type Result struct {
    job        Job
    sumofdigits int
}

var jobs = make(chan Job, 10)
var results = make(chan Result, 10)

计算函数(模拟耗时任务):

go 复制代码
func digits(number int) int {
    sum := 0
    no := number
    for no != 0 {
        digit := no % 10
        sum += digit
        no /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}

Worker 协程

go 复制代码
func worker(wg *sync.WaitGroup) {
    for job := range jobs {  // 监听任务信道
        output := Result{job, digits(job.randomno)}
        results <- output  // 写入结果信道
    }
    wg.Done()
}

创建工作池

go 复制代码
func createWorkerPool(noOfWorkers int) {
    var wg sync.WaitGroup
    for i := 0; i < noOfWorkers; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    close(results)  // 所有worker完成后关闭结果信道
}

分配任务

go 复制代码
func allocate(noOfJobs int) {
    for i := 0; i < noOfJobs; i++ {
        randomno := rand.Intn(999)
        job := Job{i, randomno}
        jobs <- job
    }
    close(jobs)  // 关闭任务信道
}

读取结果

go 复制代码
func result(done chan bool) {
    for result := range results {
        fmt.Printf("Job id %d, input %d, sum %d\n",
            result.job.id, result.job.randomno, result.sumofdigits)
    }
    done <- true
}

主函数

go 复制代码
func main() {
    startTime := time.Now()
    noOfJobs := 100
    
    go allocate(noOfJobs)
    done := make(chan bool)
    go result(done)
    
    noOfWorkers := 10
    createWorkerPool(noOfWorkers)
    
    <-done
    endTime := time.Now()
    fmt.Println("total time:", diff.Seconds(), "seconds")
}

工作池通过缓冲信道实现任务分发,WaitGroup 确保所有 worker 完成后才结束程序

Q24: Select

什么是 Select?

  • select 用于在多个发送/接收信道操作中进行选择
  • 会一直阻塞,直到某个 case 准备就绪
  • 如果多个 case 同时就绪,随机选取其中一个执行
go 复制代码
func server1(ch chan string) {
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}

func server2(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "from server2"
}

func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}
// 输出: from server2 (server2先响应)

Select 的实际应用

向多个服务器发送请求,选取最快响应的结果:

go 复制代码
// 假设 server1 和 server2 是与不同区域服务器通信的函数
// select 会选择首先响应的服务器,忽略其他响应
select {
case s1 := <-output1:
    fmt.Println(s1)
case s2 := <-output2:
    fmt.Println(s2)
}

默认情况 (Default Case)

  • 当没有 case 准备就绪时,执行 default
  • 防止 select 一直阻塞
go 复制代码
func process(ch chan string) {
    time.Sleep(10500 * time.Millisecond)
    ch <- "process successful"
}

func main() {
    ch := make(chan string)
    go process(ch)
    
    for {
        time.Sleep(1000 * time.Millisecond)
        select {
        case v := <-ch:
            fmt.Println("received value:", v)
            return
        default:
            fmt.Println("no value received")
        }
    }
}

死锁与默认情况

没有 default 时

go 复制代码
ch := make(chan string)
select {
case <-ch:  // 阻塞,没有goroutine写入 → 死锁
}
// fatal error: all goroutines are asleep - deadlock!

有 default 时

go 复制代码
ch := make(chan string)
select {
case <-ch:
default:
    fmt.Println("default case executed")
}
// 输出: default case executed

nil 信道的情况

go 复制代码
var ch chan string  // nil 信道
select {
case v := <-ch:
    fmt.Println("received value", v)
default:
    fmt.Println("default case executed")
}
// 输出: default case executed

随机选取

当多个 case 同时就绪时,select 会随机选择一个执行:

go 复制代码
func server1(ch chan string) { ch <- "from server1" }
func server2(ch chan string) { ch <- "from server2" }

func main() {
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    
    time.Sleep(1 * time.Second)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}
// 多次运行可能输出 from server1 或 from server2

空 Select

go 复制代码
select {}
// 没有任何 case,会一直阻塞 → 死锁
// fatal error: all goroutines are asleep - deadlock!

Q25: Mutex

临界区 (Critical Section)

  • 并发运行时,多个 Go 协程不应该同时访问修改共享资源的代码
  • 这些代码称为临界区

竞态条件示例x = x + 1 实际包含三个步骤:

  1. 读取 x 当前值
  2. 计算 x + 1
  3. 将结果赋值给 x

两个协程并发时可能导致最终值不正确(应该是 2,实际可能是 1)

Mutex

定义

Mutex是一个结构体,其内部包含了:

  • 锁当前是 开还是关
  • 谁正在持有这把锁
  • 等待这把锁的 goroutine 队列
  • 一些用于公平、性能的状态标记

Mutex提供加锁机制,确保某时刻只有一个协程在临界区运行,其位于 sync 包,有两个方法:Lock()Unlock()。锁已持有时,其他协程会被阻塞

go 复制代码
mutex.Lock()
x = x + 1  // 临界区
mutex.Unlock()

竞态条件示例

go 复制代码
var x = 0

func increment(wg *sync.WaitGroup) {
    x = x + 1
    wg.Done()
}

func main() {
    var w sync.WaitGroup
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}
// 输出不稳定:941、928、922...(不是期望的 1000)

使用 Mutex 修复

go 复制代码
var x = 0
var m sync.Mutex

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()
}

func main() {
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)  // 输出: 1000
}

重要:传递 Mutex 的地址,否则每个协程得到的是拷贝(即复制了该结构体),对于复制的结构体的修改无法影响到原来的结构体

使用信道处理竞态条件

go 复制代码
var x = 0

func increment(wg *sync.WaitGroup, ch chan bool) {
    ch <- true    // 获取锁
    x = x + 1    // 临界区
    <-ch         // 释放锁
    wg.Done()
}

func main() {
    var w sync.WaitGroup
    ch := make(chan bool, 1)  // 容量为1的缓冲信道作为锁
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, ch)
    }
    w.Wait()
    fmt.Println("final value of x", x)  // 输出: 1000
}

Mutex vs 信道

  • 信道:适用于协程间通信
  • Mutex:适用于只允许一个协程访问临界区

选择原则:用适合问题的工具,而非让问题迁就工具。

Q26: 结构体取代类

Go 是面向对象语言吗?

Go 不是完全面向对象的语言,但支持面向对象编程风格:

  • 有类型和方法,支持 OOP 风格
  • 没有类型的层次结构(继承)
  • 接口提供不同方法,更为通用
  • 支持结构体嵌套(类似子类化)

OOP(Object-Oriented Programming)面向对象编程

使用结构体替代类

Go 不支持类,提供结构体 + 方法实现类似类的效果:

go 复制代码
// employee.go (包名: employee)
package employee

import "fmt"

type Employee struct {
    FirstName   string
    LastName    string
    TotalLeaves int
    LeavesTaken int
}

func (e Employee) LeavesRemaining() {
    fmt.Printf("%s %s has %d leaves remaining",
        e.FirstName, e.LastName, (e.TotalLeaves - e.LeavesTaken))
}
go 复制代码
// main.go
package main

import "oop/employee"

func main() {
    e := employee.Employee{
        FirstName:   "Sam",
        LastName:    "Adolf",
        TotalLeaves: 30,
        LeavesTaken: 20,
    }
    e.LeavesRemaining()  // 输出: Sam Adolf has 10 leaves remaining
}

使用 New() 函数替代构造器

由于零结构体(内部字段均为默认的零值)可能没有意义而Go语言又不支持Java那样的构造器。

go 复制代码
// 无意义的零结构体
var e employee.Employee
e.LeavesRemaining()  // 输出: has 0 leaves remaining

为了避免使用到不可用的零值,我们可以通过两种方法解决解决:

  1. 先将结构体设为私有(小写开头),避免该结构体被其他包直接引用
  2. 提供 New 函数创建有效实例,让其他包通过New函数来创建所需要的结构体
go 复制代码
// employee.go
package employee

import "fmt"

type employee struct {  // 私有,无法从其他包访问
    firstName   string
    lastName    string
    totalLeaves int
    leavesTaken int
}

// New 函数替代构造器
func New(firstName string, lastName string, totalLeave int, leavesTaken int) employee {
    e := employee{firstName, lastName, totalLeave, leavesTaken}
    return e
}

func (e employee) LeavesRemaining() {
    fmt.Printf("%s %s has %d leaves remaining",
        e.firstName, e.lastName, (e.totalLeaves - e.leavesTaken))
}
go 复制代码
// main.go
package main

import "oop/employee"

func main() {
    e := employee.New("Sam", "Adolf", 30, 20)
    e.LeavesRemaining()  // 输出: Sam Adolf has 10 leaves remaining
}

// 在上面的代码中,"e := employee.New("Sam", "Adolf", 30, 20)"中的employee是导入的那个包,而不是结构体

命名惯例:

  • 包只含一种类型 → 使用 New(parameters)
  • 包含多种类型 → 使用 NewTypeName(parameters)

Q27: 组合取代继承

组合 vs 继承

  • Go 不支持继承,但支持组合
  • 组合:将多个结构体嵌套在一起,类似于汽车由车轮、引擎等部件组合

嵌套结构体实现组合

go 复制代码
type author struct {
    firstName string
    lastName  string
    bio       string
}

func (a author) fullName() string {
    return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}

type post struct {
    title   string
    content string
    author  // 嵌套匿名字段,post 组合了 author
}

func (p post) details() {
    fmt.Println("Title: ", p.title)
    fmt.Println("Content: ", p.content)
    fmt.Println("Author: ", p.fullName())  // 可直接访问 author 的方法
    fmt.Println("Bio: ", p.bio)            // 可直接访问 author 的字段
}

func main() {
    author1 := author{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        bio:       "Golang Enthusiast",
    }
    post1 := post{
        title:   "Inheritance in Go",
        content: "Go supports composition instead of inheritance",
        author:  author1,
    }
    post1.details()
}

提升字段

嵌套结构体的字段和方法可以直接访问,称为"提升字段":

go 复制代码
p.fullName()   // 相当于 p.author.fullName()
p.bio          // 相当于 p.author.bio

嵌套结构体切片

注意 :Go语法要求未命名的切片类型(如 [] int)不能作为结构体的匿名字段,需要为其给定字段名;

go 复制代码
type website struct {
    posts []post  // 切片不能匿名,需要字段名
}

func (w website) contents() {
    fmt.Println("Contents of Website\n")
    for _, v := range w.posts {
        v.details()
        fmt.Println()
    }
}

func main() {
    author1 := author{"Naveen", "Ramanathan", "Golang Enthusiast"}
    
    post1 := post{"Inheritance in Go", "Go supports composition", author1}
    post2 := post{"Struct instead of Classes", "Go does not support classes", author1}
    post3 := post{"Concurrency", "Go is a concurrent language", author1}
    
    w := website{posts: []post{post1, post2, post3}}
    w.contents()
}

Q28: 多态

Go 中的多态

  • Go 通过接口实现多态
  • 隐式实现接口:类型定义了接口的所有方法,就实现了该接口
  • 所有实现接口的类型都可以保存在接口变量中

使用接口实现多态

go 复制代码
/*
以下项目假设了一个组织的收入来源于两个项目:fixed billing 和 time and material,Income接口包含了两个方法:calculate() 计算并返回项目的收入,而 source() 返回项目名称。

FixedBillin结构体 有两个字段:projectName 表示项目名称,而 biddedAmount 表示组织向该项目投标的金额。

TimeAndMaterial 结构体。拥有三个字段名:projectName、noOfHours 和 hourlyRate,在该项目中收入等于 noOfHours 和 hourlyRate 的乘积。
*/
package main

import (  
    "fmt"
)

type Income interface {  
    calculate() int
    source() string
}

type FixedBilling struct {  
    projectName string
    biddedAmount int
}

type TimeAndMaterial struct {  
    projectName string
    noOfHours  int
    hourlyRate int
}

func (fb FixedBilling) calculate() int {  
    return fb.biddedAmount
}

func (fb FixedBilling) source() string {  
    return fb.projectName
}

func (tm TimeAndMaterial) calculate() int {  
    return tm.noOfHours * tm.hourlyRate
}

func (tm TimeAndMaterial) source() string {  
    return tm.projectName
}

func calculateNetIncome(ic []Income) {  
    var netincome int = 0
    for _, income := range ic {
        fmt.Printf("Income From %s = $%d\n", income.source(), income.calculate())
        netincome += income.calculate()
    }
    fmt.Printf("Net income of organisation = $%d", netincome)
}

func main() {  
    project1 := FixedBilling{projectName: "Project 1", biddedAmount: 5000}
    project2 := FixedBilling{projectName: "Project 2", biddedAmount: 10000}
    project3 := TimeAndMaterial{projectName: "Project 3", noOfHours: 160, hourlyRate: 25}
    incomeStreams := []Income{project1, project2, project3}
    calculateNetIncome(incomeStreams)
}

/*输出
Income From Project 1 = $5000  
Income From Project 2 = $10000  
Income From Project 3 = $4000  
Net income of organisation = $19000
*/

多态的好处:新增类型时无需修改原有函数,接口自动适配

Q29: Defer

什么是 defer?

  • defer 语句延迟函数的执行,在所属函数即将返回之前调用
  • 常用于资源清理、释放锁等场景
go 复制代码
func finished() {
    fmt.Println("Finished finding largest")
}

func largest(nums []int) {
    defer finished()  // 在 largest 返回前调用
    fmt.Println("Started finding largest")
    max := nums[0]
    for _, v := range nums {
        if v > max {
            max = v
        }
    }
    fmt.Println("Largest number is", max)
}

func main() {
    nums := []int{78, 109, 2, 563, 300}
    largest(nums)
}
// 输出:
// Started finding largest
// Largest number is 563
// Finished finding largest

延迟方法

go 复制代码
type person struct {
    firstName string
    lastName  string
}

func (p person) fullName() {
    fmt.Printf("%s %s", p.firstName, p.lastName)
}

func main() {
    p := person{firstName: "John", lastName: "Smith"}
    defer p.fullName()  // 延迟方法调用
    fmt.Printf("Welcome ")
}
// 输出: Welcome John Smith

实参取值时机

  • 延迟函数的实参在执行 defer 语句时确定,而非调用时
  • 延迟函数捕获的是实参的值拷贝
go 复制代码
func printA(a int) {
    fmt.Println("value of a in deferred function", a)
}

func main() {
    a := 5
    defer printA(a)  // 此时 a=5,所以实参确定为 5
    a = 10
    fmt.Println("value of a before deferred function call", a)
}
// 输出:
// value of a before deferred function call 10
// value of a in deferred function 5

defer 栈

  • 当一个函数内多次调用 defer 时,Go 会把 defer 调用放入到一个 中,多个 defer 按后进先出(LIFO)顺序执行
go 复制代码
func main() {
    name := "Naveen"
    fmt.Printf("Original String: %s\n", name)
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
}
// 输出:
// Original String: Naveen
// Reversed String: neevaN

defer 的实际应用

替代多个 wg.Done() 调用

go 复制代码
func (r rect) area(wg *sync.WaitGroup) {
    defer wg.Done()  // 一次 defer 替代多个 Done() 调用
    
    if r.length < 0 {
        fmt.Printf("rect %v's length should be greater than zero\n", r)
        return
    }
    if r.width < 0 {
        fmt.Printf("rect %v's width should be greater than zero\n", r)
        return
    }
    area := r.length * r.width
    fmt.Printf("rect %v's area %d\n", r, area)
}
  • 使用 defer wg.Done() 确保无论哪个分支返回,都会执行
  • 代码更简洁,避免遗漏 wg.Done() 调用

Q30: 错误处理与自定义错误

什么是错误?

  • 错误表示程序中的异常情况
  • Go 中错误用内置的 error 类型表示
  • 错误作为函数的最后一个返回值
go 复制代码
f, err := os.Open("/test.txt")
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(f.Name(), "opened successfully")
// 输出: open /test.txt: No such file or directory

error

error类型是一个接口类型,其定义如下:

go 复制代码
type error interface {
    Error() string
}

所有实现该接口的类型都可以当作错误类型。

获取错误详细信息的方法

我们现在已经知道了error是一个接口类型,并且在前面的示例中,我们打印了错误的描述信息,但是如果我们想要知道这个错误的具体错误信息,例如上面的打开文件的路径是什么需要怎么做呢?可以通过直接解析错误描述信息中的字符串来提取路径,以下为示例的错误描述信息输出

go 复制代码
open /test.txt: No such file or directory

但这种方式不优雅,且随着Go版本更新,描述信息可能发生变化,因此我们需要采用更加可靠的方式。Go 标准库给出了各种提取错误相关信息的方法:

方法1:断言底层结构体类型,使用字段获取信息

我们查阅Open函数的文档,发现它返回的错误类型是*PathErrorPathError结构体类型,它在标准库中的实现如下:

go 复制代码
type PathError struct {  
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

通过上面的代码,我们可以知道*PathError 通过声明 Error() string 方法,实现了 error 接口。Error() string 将文件操作、路径和实际错误拼接,并返回该字符串。于是我们得到该错误信息。因此为了得到路径Path字段,我们可以修改程序代码,采用类型断言语法如下:

go 复制代码
f, err := os.Open("/test.txt")
if err, ok := err.(*os.PathError); ok {
    fmt.Println("File at path", err.Path, "failed to open")
    return
}
方法2:断言底层结构体类型,调用方法获取信息

第二种获取更多错误信息的方法,也是对底层类型进行断言,然后通过调用该结构体类型的方法,来获取更多的信息。

标准库中的 DNSError 结构体类型定义如下:

go 复制代码
type DNSError struct {  
    ...
}

func (e *DNSError) Error() string {  
    ...
}
func (e *DNSError) Timeout() bool {  
    ... 
}
func (e *DNSError) Temporary() bool {  
    ... 
}

从上述代码可以看到,DNSError 结构体还有 Timeout() boolTemporary() bool 两个方法,它们返回一个布尔值,指出该错误是由超时引起的,还是临时性错误。

接下来我们编写一个程序,断言 *DNSError 类型,并调用这些方法来确定该错误是临时性错误,还是由超时导致的。

go 复制代码
addr, err := net.LookupHost("golangbot123.com")
if err, ok := err.(*net.DNSError); ok {
    if err.Timeout() {
        fmt.Println("operation timed out")
    } else if err.Temporary() {
        fmt.Println("temporary error")
    } else {
        fmt.Println("generic error:", err)
    }
    return
}
方法3:直接比较

第三种获取错误的更多信息的方式,是与 error 类型的变量直接比较。我们通过一个示例来理解。

filepath 包中的 Glob 用于返回满足 glob 模式的所有文件名。如果模式写的不对,该函数会返回一个错误 ErrBadPattern

filepath 包中的 ErrBadPattern 定义如下:

go 复制代码
var ErrBadPattern = errors.New("syntax error in pattern")

errors.New() 用于创建一个新的错误。我们会在下一教程中详细讨论它。

当模式不正确时,Glob 函数会返回 ErrBadPattern

我们来写一个小程序来看看这个错误。

go 复制代码
files, error := filepath.Glob("[")
if error != nil && error == filepath.ErrBadPattern {
    fmt.Println(error)
    return
}

绝不要忽略错误

go 复制代码
// 错误示例
files, _ := filepath.Glob("[")
fmt.Println("matched files", files)  // 输出: [] (错误被忽略)

// 正确做法
files, error := filepath.Glob("[")
if error != nil {
    fmt.Println(error)
    return
}

自定义错误

使用 errors.New() 创建

创建自定义错误最简单的方法是使用 errors 包中的 New 函数。在使用 New 函数 创建自定义错误之前,我们先来看看 New 是如何实现的。如下所示,是 errors 中的 New 函数的实现。

go 复制代码
package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

现在我们已经知道了 New 函数是如何工作的,我们开始在程序里使用 New 来创建自定义错误吧。

我们将创建一个计算圆半径的简单程序,如果半径为负,它会返回一个错误。

go 复制代码
import "errors"

func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")
    }
    return math.Pi * radius * radius, nil
}
使用 fmt.Errorf() 添加更多信息

上面的程序效果不错,但是如果我们能够打印出当前圆的半径,那就更好了。这就要用到 fmt 包中的 Errorf 函数了。Errorf 函数会根据格式说明符,规定错误的格式,并返回一个符合该错误的字符串

go 复制代码
func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}
使用结构体类型和字段提供更多信息
go 复制代码
//创建一个表示错误的结构体类型。错误类型的命名约定是名称以 Error 结尾。因此我们不妨把结构体类型命名为 areaError。
type areaError struct {
    err    string
    radius float64
}

// 实现error接口
func (e *areaError) Error() string {
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

func circleArea(radius float64) (float64, error) {
    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }
    return math.Pi * radius * radius, nil
}

func main() {
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            fmt.Printf("Radius %0.2f is less than zero", err.radius)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}
使用结构体类型的方法提供更多信息

在本节里,我们会编写一个计算矩形面积的程序。如果长或宽小于零,程序就会打印出错误。

go 复制代码
// 创建一个表示错误的结构体。
type areaError struct {
    err    string
    length float64
    width  float64
}

// 实现 error 接口,并给该错误类型添加两个方法,使它提供了更多的错误信息。
func (e *areaError) Error() string {
    return e.err
}

func (e *areaError) lengthNegative() bool {
    return e.length < 0
}

func (e *areaError) widthNegative() bool {
    return e.width < 0
}

func rectArea(length, width float64) (float64, error) {
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

func main() {
    length, width := -5.0, -9.0
    area, err := rectArea(length, width)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            if err.lengthNegative() {
                fmt.Printf("error: length %0.2f is less than zero\n", err.length)
            }
            if err.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", err.width)
            }
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Println("area of rect", area)
}

Q31: panic 和 recover

什么是 panic?

  • panic 用于终止程序,当程序发生异常无法继续运行时使用
  • panic 会终止当前函数执行,执行完所有 defer 后返回调用方
  • 这个过程会沿着调用栈一直持续,直到所有函数返回,然后打印 panic 信息和堆栈跟踪

尽可能使用错误处理,只有在程序不能继续运行时才使用 panic

什么时候使用 panic?

  1. 无法恢复的错误:如 web 服务器无法绑定端口
  2. 编程错误:如用 nil 参数调用只能接收合法指针的方法

panic 示例

go 复制代码
func fullName(firstName *string, lastName *string) {
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
}

func main() {
    firstName := "Elon"
    fullName(&firstName, nil)  // 触发 panic
}
// 输出:
// panic: runtime error: last name cannot be nil
// goroutine 1 [running]:
// main.fullName()
// main.main()

panic 时的 defer 执行顺序

go 复制代码
func fullName(firstName *string, lastName *string) {
    defer fmt.Println("deferred call in fullName")
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
}
// 输出:
// deferred call in fullName      // 先执行被调用函数的 defer
// deferred call in main          // 再执行调用方函数的 defer
// panic: runtime error: ...

recover

  • recover 用于重新获得 panic 的控制
  • 只能在 defer 函数内部调用才有效
  • 调用 recover 可以获取 panic 的参数,停止 panic 续发事件
go 复制代码
func recoverName() {
    if r := recover(); r != nil {
        fmt.Println("recovered from", r)
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverName()
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
}

func main() {
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")  // 继续执行
}
// 输出:
// recovered from runtime error: last name cannot be nil
// returned normally from main
// deferred call in main

panic 和 recover 与协程

  • recover 只能恢复相同协程的 panic
  • 不能恢复其他协程的 panic
go 复制代码
func recovery() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}

func a() {
    defer recovery()
    fmt.Println("Inside A")
    go b()  // b 在不同协程,panic 无法恢复
    time.Sleep(1 * time.Second)
}

func b() {
    fmt.Println("Inside B")
    panic("oh! B panicked")
}
// 输出:
// Inside A
// Inside B
// panic: oh! B panicked
// (panic 无法恢复)

运行时 panic

  • 运行时错误(如数组越界)也会导致 panic
go 复制代码
func a() {
    n := []int{5, 7, 4}
    fmt.Println(n[3])  // 数组越界,触发 panic
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered", r)
        }
    }()
    a()
    fmt.Println("normally returned from main")
}
// 输出:
// Recovered runtime error: index out of range
// normally returned from main

恢复后获取堆栈跟踪

即通过recover恢复后,我们就不知道panic的报错日志了,例如:

log 复制代码
goroutine 1 [running]:  
main.a()  
    /tmp/sandbox780439659/main.go:9 +0x40
main.main()  
    /tmp/sandbox780439659/main.go:13 +0x20

下面通过Debug包中的PrintStack函数可以打印堆栈跟踪:

go 复制代码
import "runtime/debug"

func r() {
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()  // 打印堆栈跟踪
    }
}

Q32: 头等函数(First Class Functions)

什么是头等函数?

  • 支持头等函数的编程语言,可以把函数赋值给变量,也可以把函数作为参数或返回值
  • Go 语言支持头等函数

匿名函数

go 复制代码
// 赋值给变量
a := func() {
    fmt.Println("hello world first class function")
}
a()  // 调用

// 直接定义并调用
func() {
    fmt.Println("hello world first class function")
}()

// 带参数的匿名函数
func(n string) {
    fmt.Println("Welcome", n)
}("Gophers")

用户自定义函数类型

go 复制代码
type add func(a int, b int) int

func main() {
    var a add = func(a int, b int) int {
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)  // 输出: Sum 11
}

高阶函数

满足以下条件之一的函数:

  • 接收一个或多个函数作为参数
  • 返回值是一个函数

作为参数

go 复制代码
func simple(a func(a, b int) int) {
    fmt.Println(a(60, 7))
}

func main() {
    f := func(a, b int) int {
        return a + b
    }
    simple(f)  // 输出: 67
}

作为返回值

go 复制代码
func simple() func(a, b int) int {
    f := func(a, b int) int {
        return a + b
    }
    return f
}

func main() {
    s := simple()
    fmt.Println(s(60, 7))  // 输出: 67
}

闭包

  • 闭包是匿名函数的一种,访问函数外部的变量
  • 每个闭包绑定自己的外围变量,且相互独立,不会受到影响
go 复制代码
func appendStr() func(string) string {
    // 被闭包函数操作的变量
    t := "Hello"
    // 赋值给c的这个匿名函数就是闭包函数
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}

func main() {
    // 第一次调用appendStr():
    // 1. 创建独立的t变量(值为"Hello")
    // 2. 创建闭包函数c,绑定这个t
    // 3. 返回c,赋值给a
    a := appendStr()
    
    // 第二次调用appendStr():
    // 1. 重新创建一个全新的t变量(值为"Hello")
    // 2. 创建新的闭包函数c,绑定这个新t
    // 3. 返回c,赋值给b
    b := appendStr()
    fmt.Println(a("World"))   // Hello World
    fmt.Println(b("Everyone")) // Hello Everyone
    fmt.Println(a("Gopher"))  // Hello World Gopher
    fmt.Println(b("!"))       // Hello Everyone !
}

头等函数的实际用途

filter 函数示例

在这个示例中我们会创建一个程序,基于一些条件,来过滤一个 students 切片。

go 复制代码
type student struct {
    firstName string
    lastName  string
    grade     string
    country   string
}

func filter(s []student, f func(student) bool) []student {
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

func main() {
    s1 := student{"Naveen", "Ramanathan", "A", "India"}
    s2 := student{"Samuel", "Johnson", "B", "USA"}
    s := []student{s1, s2}
    
    f := filter(s, func(s student) bool {
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)  // [{Samuel Johnson B USA}]
}

map 函数示例

下面这个程序会对切片的每个元素执行相同的操作,并返回结果。例如,如果我们希望将切片中的所有整数乘以 5,并返回出结果。我们把这种对集合中的每个元素进行操作的函数称为 map 函数。

go 复制代码
func iMap(s []int, f func(int) int) []int {
    var r []int
    for _, v := range s {
        r = append(r, f(v))
    }
    return r
}

func main() {
    a := []int{5, 6, 7, 8, 9}
    r := iMap(a, func(n int) int {
        return n * 5
    })
    fmt.Println(r)  // [25 30 35 40 45]
}

Q33: 反射(Reflect)

什么是反射?

  • 反射允许程序在运行时检查变量和值,求出它们的类型
  • Go 的 reflect 包提供了运行时反射的能力

为何需要反射?

  • 编写通用函数时,在编译时无法确定类型
  • 例如:编写一个函数,接收任意结构体参数并生成 SQL 查询
  • 这种情况需要在运行时检查参数的类型和字段

reflect 包

  • reflect.TypeOf():返回接口的具体类型
  • reflect.ValueOf():返回接口的具体值
go 复制代码
type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    v := reflect.ValueOf(q)
    fmt.Println("Type", t)
    fmt.Println("Value", v)
}

func main() {
    o := order{ordId: 456, customerId: 56}
    createQuery(o)
}
// 输出:
// Type main.order
// Value {456 56}

reflect.Kind vs reflect.Type

  • Type:表示接口的实际类型(如 main.order
  • Kind:表示类型的特定类别(如 struct
go 复制代码
func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type", t)
    fmt.Println("Kind", k)
}
// 输出:
// Type main.order
// Kind struct

NumField() 和 Field()

  • NumField():返回结构体字段数量
  • Field(i):返回第 i 个字段的 reflect.Value
go 复制代码
func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        v := reflect.ValueOf(q)
        fmt.Println("Number of fields", v.NumField())
        for i := 0; i < v.NumField(); i++ {
            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
        }
    }
}
// 输出:
// Number of fields 2
// Field:0 type:reflect.Value value:456
// Field:1 type:reflect.Value value:56

Int() 和 String()

  • Int():取出 reflect.Value 作为 int64
  • String():取出 reflect.Value 作为 string
go 复制代码
a := 56
x := reflect.ValueOf(a).Int()
fmt.Printf("type:%T value:%v\n", x, x)  // type:int64 value:56

b := "Naveen"
y := reflect.ValueOf(b).String()
fmt.Printf("type:%T value:%v\n", y, y)  // type:string value:Naveen

完整示例:通用 SQL 查询生成器

go 复制代码
type order struct {
    ordId      int
    customerId int
}

type employee struct {
    name    string
    id      int
    address string
    salary  int
    country string
}

func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        t := reflect.TypeOf(q).Name()
        query := fmt.Sprintf("insert into %s values(", t)
        v := reflect.ValueOf(q)
        for i := 0; i < v.NumField(); i++ {
            switch v.Field(i).Kind() {
            case reflect.Int:
                if i == 0 {
                    query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
                } else {
                    query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
                }
            case reflect.String:
                if i == 0 {
                    query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
                } else {
                    query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
                }
            }
        }
        query = fmt.Sprintf("%s)", query)
        fmt.Println(query)
        return
    }
    fmt.Println("unsupported type")
}

func main() {
    o := order{ordId: 456, customerId: 56}
    createQuery(o)
    // insert into order values(456, 56)

    e := employee{name: "Naveen", id: 565, address: "Coimbatore", salary: 90000, country: "India"}
    createQuery(e)
    // insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")

    i := 90
    createQuery(i)
    // unsupported type
}

应该使用反射吗?

"清晰优于聪明。而反射并不是一目了然的。" --- Rob Pike

  • 反射是 Go 语言非常强大和高级的概念
  • 使用反射编写清晰和可维护的代码十分困难
  • 应该尽可能避免使用反射,只在必须使用时才使用

Q34: 文件操作

读取文件

将整个文件读取到内存

依赖包:os

使用 os.ReadFile 读取整个文件,并且支持自动处理文件打开和关闭 ,该函数将返回文件的原始二进制数据字节切片),因此其也能够处理图像等数据:

项目结构:

md 复制代码
src
    filehandling
        filehandling.go
        test.txt
go 复制代码
package main
import (
    "fmt"
    "os"
)
func main() {
    data, err := os.ReadFile("test.txt") 
    if err != nil {
        panic(err)
    }
    fmt.Println(string(data))
}

我们知道Go语言通过install指令后会进行编译,编译后的二进制程序独立于源代码,可以在任何位置运行,但是在源代码中给出的文件路径是相对路径,一旦在其他路径运行二进制程序会报错,示例如下:

cmd 复制代码
File reading error open test.txt: The system cannot find the file specified.

为此我们可以尝试三种方法解决该问题:

使用绝对路径比较简单,在此不做介绍,我们主要看后面两种。

使用命令行标记传递文件路径

所谓的使用命令行标记传递文件路径,其实就是在调用该Go脚本文件时传递相关参数,就像我们使用git以一行形式查看git历史一样

bash 复制代码
git log --oneline

我们通过flag包可以从输入的命令行获取到用户在命令行传递的参数,具体实现是该包下有一个名为String的函数,其接收三个参数:第一个参数是标记名,第二个是默认值,第三个是标记的简短描述。最后该函数返回的是存储 flag 值的字符串变量的地址。

go 复制代码
// 从命令行读取文件名
import "flag"

fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
data, err := ioutil.ReadFile(*fptr)

以上面的代码为例,flag.String() 创建了一个**"命令行参数解析器"**,它做三件事:

  1. 注册一个标记名 :告诉程序"我要接收一个叫做 -fpath 的参数"
  2. 设定默认值 :如果用户没提供,就用 "test.txt"(String的第二个参数)
  3. 生成帮助文档 :用户输入 -h 时显示说明(即String的第三个参数)

因此此时在命令行运行方式:./program -fpath=/path/to/file.txt

注意:为了使得该功能生效,需要满足:

要求 说明
✅ 所有 flag.Xxx() 定义必须在 flag.Parse() 之前 先注册,后解析
flag.Parse() 必须在访问 flag 值之前 否则拿到的是默认值
❌ 不一定要在 main() 的最开始 可以有其他前置代码
go 复制代码
func main() {
    fptr := flag.String("fpath", "test.txt", "file path to read from")
    flag.Parse()
    // 调用了flag的值
    fmt.Println("value of fpath is", *fptr)
}
将文件绑定在二进制文件中
  • embed包(推荐)

  • packr方案(旧)

此处不做介绍,后续自行查阅

分块读取文件

在前面的章节,我们学习了如何把整个文件读取到内存。当文件非常大时,尤其在 RAM 存储量不足的情况下,把整个文件都读入内存是没有意义的,此时就不能使用之前的方法了。

更好的方法是分块读取文件。这可以使用 bufio 包搭配os.Open函数来完成。使用 bufio.NewReader进行分块读取文件:

go 复制代码
f, err := os.Open("test.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

r := bufio.NewReader(f)
b := make([]byte, 3)  // 每次读取3字节
for {
    _, err := r.Read(b)
    if err != nil {
        break
    }
    fmt.Println(string(b))
}
逐行读取文件

使用 bufio.NewScanner

go 复制代码
f, err := os.Open("test.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

s := bufio.NewScanner(f)
for s.Scan() {
    fmt.Println(s.Text())
}
err = s.Err()
if err != nil {
    log.Fatal(err)
}

写入文件

将字符串写入文件
go 复制代码
f, err := os.Create("test.txt")
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()

l, err := f.WriteString("Hello World")
if err != nil {
    fmt.Println(err)
    return
}
fmt.Println(l, "bytes written successfully")

os.Create 会创建文件,如果文件已存在则截断

将字节写入文件
go 复制代码
f, err := os.Create("bytes")
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()

d := []byte{104, 101, 108, 108, 111}  // "hello"
n, err := f.Write(d)
逐行写入文件

使用 fmt.Fprintln

go 复制代码
f, err := os.Create("lines")
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()

lines := []string{"Welcome to Go.", "Go is compiled.", "Easy to learn."}
for _, v := range lines {
    fmt.Fprintln(f, v)
}
追加到文件

使用 os.OpenFile 以追加模式打开:

go 复制代码
f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
    fmt.Println(err)
    return
}
defer f.Close()

fmt.Fprintln(f, "File handling is easy.")
并发写文件

多个 goroutine 并发写文件时需要避免竞态条件。使用 channel 确保只有一个 goroutine 写入:

go 复制代码
func produce(data chan int, wg *sync.WaitGroup) {
    n := rand.Intn(999)
    data <- n
    wg.Done()
}

func consume(data chan int, done chan bool) {
    f, err := os.Create("concurrent")
    if err != nil {
        fmt.Println(err)
        done <- false
        return
    }
    defer f.Close()
    
    for d := range data {
        fmt.Fprintln(f, d)
    }
    done <- true
}

func main() {
    data := make(chan int)
    done := make(chan bool)
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go produce(data, &wg)
    }
    
    go consume(data, done)
    go func() {
        wg.Wait()
        close(data)
    }()
    
    if <-done {
        fmt.Println("File written successfully")
    }
}

并发写文件时,使用 channel 确保只有一个 goroutine 操作文件,避免竞态条件

相关推荐
啊阿狸不会拉杆1 小时前
《计算机视觉:模型、学习和推理》第 18 章-身份与方式模型
人工智能·python·学习·计算机视觉·分类·子空间身份模型·plda
adore.9681 小时前
3.11 复试学习
学习
自传丶1 小时前
【学习笔记】大模型应用开发系列(二)Embedding 模型
笔记·学习·embedding
左左右右左右摇晃2 小时前
Java 对象:创建方式与内存回收机制
java·笔记
JMchen1232 小时前
企业级图表组件库完整实现
android·java·经验分享·笔记·canvas·android-studio
爱写代码的小朋友5 小时前
人工智能驱动下个性化学习路径的构建与实践研究——以K12数学学科为例
人工智能·学习
不灭锦鲤11 小时前
网络安全学习第48天
学习
ALKAOUA11 小时前
力扣面试150题刷题分享
javascript·笔记
無限進步D11 小时前
Java 循环 高级(笔记)
java·笔记·入门