Go语言学习笔记
Q1: 简短声明 := 的类型推断
:=用于函数内声明并初始化变量- 编译器根据值自动推断类型:
100→int,50.5→float64 - 可一行声明多个不同类型的变量
- 规则:只能在函数内使用、至少一个新变量、不能指定类型
Q2: Go的强类型特性
- 无自动类型提升/隐式转换,不同类型不能直接运算或赋值
int和float64不能直接相加,需显式转换: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 语句作用域内有效
重要特性
- 可选语句 :可在条件判断前执行初始化,与条件判断用分号
;分隔 - 变量作用域:在可选语句中声明的变量仅在 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 初始化语句; 条件语句; 后置语句 {
// 循环体
}
三种形式
- 完整形式
go
for i := 0; i < 5; i++ {
fmt.Println(i) // 输出 0 1 2 3 4
}
- 类似 while 形式
go
i := 0
for i < 5 {
fmt.Println(i)
i++
}
- 无限循环
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(类型,长度,容量):会创建底层数组,并初始化切片的
len和cap。
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修改后影响其他切片,可以怎么做:
- 设置原始切片大小,一旦需要append必然会需要扩容,此时新切片和原切片不再共享底层数组
- 类似于方法1,我们可以通过三索引截断法,例如要获取某个数组的前三个元素:
newSlice:=array[:3:3],通过设置截断长度等于容量,使得append时必扩容- 使用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)
- 无序集合,键不可重复
- 零值是
nil,nilmap 不能添加元素
声明与创建
- 使用
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 的哈希表、通道的缓冲区)。
- 声明
go
var m map[string]int // nil map
- 字面量创建
go
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
// 空 map
m := map[string]int{}
操作方法
- 添加/修改元素
go
m["key"] = value // 如果键存在则修改,不存在则添加
- 访问元素
go
value := m["key"] // 如果键不存在,返回值类型的零值
// 二值语法:获取值的同时判断键是否存在
value, ok := m["key"]
if ok {
fmt.Println("键存在,值为:", value)
} else {
fmt.Println("键不存在")
}
- 删除元素
go
delete(m, "key") // 删除键对应的元素
// 如果键不存在,不会报错,只是什么都不做
- 遍历 map
go
// 遍历键值对
for key, value := range m {
fmt.Println(key, value)
}
// 只遍历键
for key := range m {
fmt.Println(key)
}
常用内置函数
len(m):返回 map 中键值对的数量delete(m, key):删除指定键的元素
注意事项
nilmap 问题
go
var m map[string]int
// m["a"] = 1 // 运行时错误:assignment to entry in nil map
// 必须先初始化
m = make(map[string]int)
m["a"] = 1 // 正确
- 遍历顺序随机
go
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v)
}
// 每次运行输出顺序可能不同
- 不支持切片、函数、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的内部实现机制,键在创建后不能改变,因此通常选择不可变类型作为键。
- 支持结构体作为键(如果结构体所有字段可比较)
go
type Point struct {
X int
Y int
}
m := make(map[Point]string)
- 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]
- 并发不安全
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函数 - 不能有返回值和参数,不能显式调用
- 用于初始化验证或准备资源
- 初始化顺序 :
- 先初始化被导入的包
- 然后初始化包级别变量
- 调用 init 函数
- 最后调用 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 类型
rune是int32的别名- 表示 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) ❌ ✅ - 值类型 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")
}
协程的重要特性
- 启动立即返回:调用协程后,程序控制立即返回,不会等待协程执行完毕
- 主协程终止则程序终止:如果主协程结束,其他协程不会继续运行
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的地址,否则每个协程得到的是拷贝,主协程无法感知完成
工作池实现
工作池是一组等待任务分配的协程,完成任务后继续等待新任务。
核心组件:
- 任务信道 (
jobs):分发作业 - 结果信道 (
results):收集结果 - 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 实际包含三个步骤:
- 读取 x 当前值
- 计算 x + 1
- 将结果赋值给 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
为了避免使用到不可用的零值,我们可以通过两种方法解决解决:
- 先将结构体设为私有(小写开头),避免该结构体被其他包直接引用
- 提供
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函数的文档,发现它返回的错误类型是*PathError。PathError 是结构体类型,它在标准库中的实现如下:
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() bool 和 Temporary() 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?
- 无法恢复的错误:如 web 服务器无法绑定端口
- 编程错误:如用 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作为int64String():取出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() 创建了一个**"命令行参数解析器"**,它做三件事:
- 注册一个标记名 :告诉程序"我要接收一个叫做
-fpath的参数" - 设定默认值 :如果用户没提供,就用
"test.txt"(String的第二个参数) - 生成帮助文档 :用户输入
-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 操作文件,避免竞态条件