date
related_level
slug
type
relate_date
summary
status
tags
category
last_updated
Jan 18, 2026 10:11 PM
是否已更新
orginal_page
是否推荐
设置国内镜像源
go env -w GO111MODULE=on- 打开模块支持
go env -w GOPRIVATE=git.mycompany.com,github.com/my/private- 设置不走代理的私有仓库
- 取消镜像代理或校验
go env -w GOPROXY=directgo env -w GOSUMDB=off- 取消校验
- 镜像源(阿里云)
go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct- 不支持
GOSUMDB
- 镜像源(官方代理)
go env -w GOPROXY=https://proxy.golang.com.cn,directgo env -w GOSUMDB=sum.golang.google.cn
包管理
- 包是位于同一目录下的源文件的集合,同一目录下的所有源文件必须共享同一个包名
package 关键字定义当前文件所在的包
"math/rand"包中的文件以语句package rand开头
- 必须是每个
.go文件的第一行非注释代码 - 除了
//go:build等编译指令
- 可执行程序的入口包必须是
package main,并且必须包含func main()函数
- 包内首字母大写的标识符(如函数、变量、类型、常量)会被导出(对外可见),小写的则为私有
math.pi未导出,math.Pi正常导出
import 关键字导入其他包
- 语法
import "math/rand"- 包名与导入路径的最后一个元素一致,这里的
"math/rand"为rand - 调用
rand.Intn(10) import ( "fmt" "math/rand" )- 分组导入
import m "math"m作为导入包的别名import _ "pkg"- 匿名导入,仅触发
init()
- 导入后只能访问首字母的实体(函数、类型、变量、常量)
- 如果只导入包但不使用,Go 编译器会报错
数据模型
参考资料
类型转换 T(v) 将值 v 转为类型 T
var f float64 = float64(i)
f := float64(i)
底层标准类型
nil
布尔类型
bool
字符串类型
string
数值类型
int int8 int16 int32 int64rune // int32 的别名,表示一个 Unicode 码位
uint uint8 uint16 uint32 uint64 uintptrbyte // uint8 的别名
float32 float64
complex64 complex128
Go 指针 *T 保存了值的内存地址,不支持指针运算,不支持直接内存地址操作
- 类型
*T为指向T类型值的指针,其零值为nil var p *int
& 操作符生成一个指向其操作数的指针
* 操作符解引用指针以获取其指向的底层值
- 指针间接引用的使用场景
- 传递结构体指针比复制整个结构体更高效
在函数中修改原始变量
避免大对象拷贝(性能优化)
映射 map[KeyType]ValueType 将键映射到值
- 未初始化时的零值是
nil nil映射既没有键,也不能添加键
键类型必须是可以比较的
- 基本类型:
int,string,bool,float64
- 指针、通道、接口(只要动态值可比较)
- 结构体(所有字段都可比较)
- 数组(元素类型可比较)
- 映射在遍历时不保证顺序(每次运行可能不同)
始终用 make 函数创建映射 make(map[KeyType]ValueType[, hint])
- 可以指定容量提示 hint
- 非上限,仅用于性能优化:减少后续 rehash 次数
m := make(map) 与 var m map 比对
特性 | nil map | 空 map(make) |
声明方式 | var m map[K]V | m := make(map[K]V) |
len(m) | 0 | 0 |
读取 | 返回 zero value | 返回 zero value |
写入 | panic! | ✅ 成功 |
删除 | 无效果(安全) | 无效果(安全) |
映射的核心操作
- 检测键是否存在
elem, ok = m[key]elem, ok := m[key]
操作 | 语法 | 说明 |
取值 | v := m[key] | 若 key 不存在,返回 valueType 的零值 |
赋值/新增 | m[key] = value | 自动扩容 |
删除 | delete(m, key) | 安全:key 不存在也不报错 |
清空 | clear(m) | 删除所有键值对 |
长度 | len(m) | O(1) 操作 |
数组 [n]T 包含 n 个类型为 T 的元素
- 数组的长度
n不能为负数
- 数组的长度是其类型的一部分,因此不能改变
数组不能自包含(递归),即数组类型 T 不能包含类型为 T 的元素
- 自包含时,编译器无法确定数组的长度(无限大)
- 通过结构体间接包含也不行
- 允许通过指令等方式间接引用,此时长度是可确定的
- 指针的大小是固定的(64 位系统上为 8 字节)
- 函数类型为指向函数描述符的指针
- 切片本身不存储元素,只存储指向底层数组的指针
[…]Type{} 初始化可以让编译器自动推断数组的长度
- 相当于
[4]string
切片 []T 包含可变个类型为 T 的元素
- 未初始化时的零值是
nil nil切片的长度和容量为 0 且没有底层数组
切片包含三个用户不可见的关键信息 ptr, len, cap
指向底层数组的指针
赋值时仅浅拷贝,共享底层数组,存在副作用
长度(len)为切片包含的元素个数
- 通过两个下标来界定
a[low : high] - 下界默认为 0,上界默认为长度
a[0:]等于a[0:10]a[:10]等于a[0:10]a[:]等于a[0:10]
- 区间开闭同 python,左闭右开
[2, 5]包括下标从 2 到 4,但不包括 5
容量(cap)为从切片的起始位置到底层数组末尾的元素总数
切片 append 与容量 cap 相关
- 容量足够时 append 与容量相关会在原数组基础上操作
- 容量不足时 append 会分配新的底层数据,不再共享
make 函数创建切片 make([]T, length[, capacity])
- 可以指定容量
- 分配一个隐藏的底层数组,并返回指向其前
length个元素的切片 - 这个数组对外不可见,只能通过切片访问
数组与切片比较
- 比较
特性 | 数组(Array) | 切片(Slice) |
本质 | 一块连续的值内存 | 包含指针的值类型(ptr, len, cap) |
长度 | 固定(编译时确定) | 动态(运行时可变) |
存储 | 独立内存块 | 对底层数组的引用,可以共享 |
赋值/传参 | 深拷贝整个数组 | 浅拷贝(只复制 slice header) |
数组自动初始化,切片必须手动初始化
- 切片零值为 nil 切片
Go 没有 enum 枚举关键字和类型,通常可以通过特殊常量 iota 实现
- iota 是可以被编译器修改的编译期常量
- 在
const块中,iota是从 0 开始、每行递增 1 的“行号计数器”
样例:prometheus 的 ValueType 枚举类型
样例:非连续枚举
样例:非连续枚举(位掩码)
样例:枚举类型 + String 方法
Go 结构体 struct 包含零组或多组 field 字段
- 语法
type <name> struct { [field ...] } - 字段名必须唯一
- 此时字段名为类型名
- 嵌入式字段必须指定为类型名
T或指向非接口类型名*T的指针,并且T本身不能是指针类型或类型参数
每个字段至少要提供类型
声明了类型但没有显式指定字段名的字段称为嵌入式字段
结构体可以嵌套
- 通过
.来访问结构体内的字段
- 通过结构体指针来访问结构体内的字段
(*p).X - 写
p.X即可,Go 编译器会自动解引用指针来访问字段
通过 fieldname: <value> 实现关键字参数赋值
接口类型 interface 是一组接口元素的集合
- 接口类型的零值为
nil
接口元素(interface elements)可以是一个方法或者一个类型元素
- 类型元素(type element)是由一个或多个类型项(type terms) 组成的联合(union)
- 类型项可以是单一类型,也可以是单一的底层类型(underlying type)
- 对于内置类型(如
int,string,[]byte等),底层类型就是自身
基本接口为一组方法签名(参数、返回值、名称)的集合
- 方法签名(参数、返回值、名称)必须完全匹配
- 每个显式指定的方法名称必须唯一且不能是空白标识符
嵌入接口包含了实现 T 显式声明的方法和实现嵌入接口 E 的所有方法
嵌入接口时,同名方法必须具有完全相同的签名
通用接口通过类型元素限制接口的类型集合
类型元素可以是
- 任意类型项
T
形如 ~T 的底层类型项(表示底层类型为 T 的所有类型)
T的底层类型必须是它自身,且T不能是接口类型
多个类型项的联合(union),形式为 t1 | t2 | … | tn
结合方法声明,类型元素可以精确地定义接口的类型集合,即类型约束(type constraints)
- 空接口的类型集合是所有非接口类型的集合
- 非空接口的类型集合是其所有接口元素类型集合的交集
- 方法声明的类型集合是所有非接口类型中方法集包含该方法的类型的集合
- “所有非接口类型”的量化范围不仅限于当前程序中声明的类型,而是指所有可能程序中的所有非接口类型,因此是无限的
- 非接口类型项
T的类型集合仅包含该类型T本身
- 形如
~T的项的类型集合是所有底层类型为T的类型的集合
- 联合项
t1 | t2 | … | tn的类型集合是各个项类型集合的并集
基本接口和通用接口对比
- 基本接口只能约束「行为」,不能约束「类型形态」
- 通用接口限定「可用的类型集合」
- 再加上方法则可以同时约束「操作能力 + 行为」
特性 | 基本接口 | 通用接口 |
组成 | 仅方法 | 方法 + 类型项( T, ~T, T1|T2) |
用途 | 普通变量、参数、返回值 | 仅限泛型约束 |
类型集合 | 所有实现方法的类型 | 通过交集/并集精确指定 |
示例 | io.Reader | constraints.Integer |
能否声明变量 | ✅ var r io.Reader | ❌ var x Float |
接口实现
接口的核心设计哲学:隐式实现(Implicit Implementation)
- 隐式接口解耦(Decoupling) 了 接口定义 和 类型实现
- 接口定义者 和 类型实现者 互不知晓对方存在
- 接口可以在类型之后定义,反之亦然
只要一个类型(如 MyFile)拥有某个接口(如 io.Reader)所要求的全部方法签名,那么它就自动实现了该接口
- 对比 Java/C# 通过 implements 显式定义
class MyFile implements Reader { ... }- 每新增一个接口,所有实现类(如
MyFile)都要加implements X
一个类型要实现 ReadCloser 只需要实现两个对应方法即可
- 方法签名(参数、返回值、名称)必须完全匹配
Read(p []byte) (n int, err error)和Close() error
- 类型
T实现接口I需要满足以下条件之一
T 不是接口类型,并且 T 属于 I 的类型集合
T 是接口类型,并且 T 的类型集合是 I 的类型集合的子集
- 如果类型
T实现了接口I,则类型为T的值也实现了该接口
多个类型可以实现同一个接口
- 所有类型都实现了空接口
interface {},它代表了所有(非接口)类型的集合 - 预声明类型
any是空接口的别名
- 如果两个类型
S1和S2都具有如下方法集,无论S1和S2是否还拥有其他方法,它们都实现了File接口
接口类型 T 不得直接或间接地嵌入一个包含 T 本身的类型元素
- 无论是作为类型项、联合项还是嵌套接口
非基本接口(即包含类型项或联合的接口)只能用作类型约束
- 不能作为值或变量的类型,也不能作为其他非接口类型的组成部分
接口变量存储的是具体类型的值,而非接口本身
- 在内部,接口值可以看做包含值和具体类型的元组
(value, type)
- 接口值调用方法时会执行其底层类型的同名方法
对于底层值为 nil 的接口值,方法仍然会被 nil 接收者调用
- 在一些语言中,这会触发一个空指针异常
- 在 Go 中通常会写一些方法来优雅地处理它
对于 (<nil>, <nil>) 接口值调用方法会产生运行时错误
- 没有具体类型来接收方法
- 接口是否为 nil,取决于类型和值是否都为 nil
类型断言(Type Assertion)从接口值中提取其具体类型的值
t := i.(T)断言接口值i保存了具体类型T,并将其底层类型为T的值赋予变量t- 若
i并未保存T类型的值,断言语句会触发一个 panic
t, ok := i.(T)安全断言- 断言失败时,
ok将为false而t将为T类型的零值,不会产生 panic
类型选择(Type Switch)对一个接口值,根据其动态类型进行多路分支判断
- 类似于
switch,但针对类型
- 类型选择中的声明与类型断言
i.(T)的语法相同,只是具体类型T被替换成了关键字type
样例:Stringer 接口
- fmt 包中定义的 Stringer 是一个可以用字符串描述自己的类型
样例:error 接口
error 类型是一个内建接口
通常函数会返回一个 error 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理
error为 nil 时表示成功;非 nil 的error表示失败
样例:io.Reader 接口
io.Reader 接口 Read 方法
- 在遇到数据流的结尾时,
Read方法会返回一个io.EOF错误
样例:Sqrt 函数异常处理
在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环
- 由于没有实现
String()方法,fmt在格式化值时会优先调用 ErrNegativeSqrt 的Error()方法
float64(e)显式把 e 转成了一个不实现 error 的类型
样例:Reader 类型实现 Read 方法
在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环
- 由于没有实现
String()方法,fmt在格式化值时会优先调用 ErrNegativeSqrt 的Error()方法
float64(e)显式把 e 转成了一个不实现 error 的类型
样例:rot13Reader 类型实现 Read 方法从另一个 io.Reader 读取数据,通过应用 rot13 代换密码解密数据
rr.r.Read(b)从另一个io.Reader读取数据
泛型类型是一种带有类型参数的类型定义
类型约束(Type Constraints)限制类型参数(Type Parameters)可以接受哪些类型
- 类型参数是占位符(如
T,K,V),代表将来会传入的具体类型
- 类型约束必须是接口类型(包括
any、comparable等预声明接口) any是空接口interface{}的别名comparable是一个特殊的空接口,其类型集合包含所有支持==和!=操作的非接口类型- 接口类型本身不在
comparable的类型集合中 - 接口是否可比较取决于其动态类型,而泛型约束必须在编译时静态确定
- 是否满足由编译器根据类型是否可比较自动决定
- 常见可比较类型
comparable 在泛型中限制类型参数必须可比较
类型 | 示例 |
基本类型 | int, float64, string, bool |
指针 | *T(两个指针可比较是否指向同一地址) |
通道 | chan int(可比较是否为同一通道) |
结构体 | 所有字段都可比较 |
数组 | 元素类型可比较的数组(如 [3]int) |
类型参数 T 和类型约束 constraint 出现在参数之前的方括号之间 [T constraint]
type Box[T any] struct { Value T }any是类型约束,表示类型参数T可以是任意类型
func Index[T comparable](s []T, x T) ints是满足类型约束comparable的任何类型T的切片x同理
样例:定义一个泛型结构体(链表)
样例:定义一个泛型接口(容器)
样例:定义一个泛型函数(Print 任意类型)
样例:Index 函数将值与所有切片元素进行比较,直到找到匹配项
comparable限制类型参数必须可比较
- 对任意满足该类型的值使用
==和!=运算符
控制流:循环执行,条件执行,延迟执行,跳转
循环语句只有 for 关键字,没有 while 关键字
- 语法
for [ Condition | ForClause | RangeClause ] {} - 没有
()小括号包括
ForClause 分为三部分 [ InitStmt ] ; [ Condition ] ; [ PostStmt ]
- 可选的初始化语句
i := 0:在第一次迭代前执行 - 一般使用短声明
- 作用域局限于
for语句内
- 条件表达式
i < 10:在每次迭代前求值判断 - 为
false时终止循环 - 省略时,无限循环,通过
break中断循环
- 可选的后置语句
i++:在每次迭代的结尾执行
不提供初始化语句和后置语句时 for 相当于 while 关键字
未声明初始化和后置语句时 for ; Condition ; {}
- 需要在 for 之前声明变量 sum 并在 for 内实现后置语句
相当于 for Condition {}
range 子句会遍历元素 for RangeClause {}
RangeClause = [ ExpressionList = | IdentifierList := ] range Expression
左侧的操作数必须是可寻址或映射索引的表达式,表示迭代变量
- 可以赋值
_表示忽略,或者只取第一个 for i, _ := range powfor _, value := range powfor i := range pow
- 最多两个操作数(通道/整数只能一个)
右侧的表达式为范围表达式,表示遍历目标
- 遍历 pow 切片,打印下标和对应元素值的一份副本
- range 使用快照,修改原数据不影响遍历值
不同范围表达式的迭代行为
Range 表达式类型 | 变量数 | 第一个值(key/索引) | 第二个值(value/元素) | 迭代顺序 | 特殊说明 |
数组 / 切片 / 数组指针
[N]T, []T, *[N]T | 2 | 索引 i(int) | 元素 a[i](类型 T) | 固定: 0 → len-1 | • nil 切片:0 次迭代
• 修改原数据不影响遍历值 |
字符串 string | 2 | 字节索引 i(int) | Unicode 码点 rune | 按 UTF-8 编码顺序 | • 非法 UTF-8 → 返回 0xFFFD
• 索引是字节位置,非字符序号 |
映射 map[K]V | 2 | 键 k(类型 K) | 值 m[k](类型 V) | 随机、无序 | • nil map:0 次迭代
• 遍历时增删键:行为不确定 |
通道 chan T | 1 | 接收到的元素 v(类型 T) | ㅤ | 按发送顺序 | • 阻塞直到通道关闭
• nil 通道:永久阻塞(死锁) |
整数 n(整型或无类型常量) | 1 | 值 i | ㅤ | 固定: 0 → n-1 | • n ≤ 0:0 次迭代
• 类型:若 n 无类型 → i 为 int |
迭代器函数 func(yield func(...)) | 0–2 | 由 yield 参数决定 | 由 yield 参数决定 | 由函数逻辑决定 | • yield(k, v) → 产生一次 (k, v)
• break 会使 yield 返回 false |
条件语句 if 关键字和 switch 关键字
if语句语法if condition_clause {} [ else [if condition_clause] {} ]else语句可选,else语句的if条件语句可选- 没有
elif关键字
if 条件语句分为两部分 if v := math.Pow(x, n); v < lim { }
- 可选的初始化语句
v := math.Pow(x, n):在条件判断前执行 - 一般使用短声明
- 作用域局限于
if语句内
- 条件表达式
v < lim:在条件判断前求值 - 为
false时跳过
switch 语句语法
switch [ condition_clause ] {
[ case Expression { "," Expression }: StatementList ... ]
[ default: StatementList ]
}
default可以放在任意位置,但只有在没有任何case匹配时才会执行
switch v := x.(type) { }- 类型开关,专门用于在运行时判断接口(
interface{})变量的实际类型,并根据不同类型执行不同逻辑
switch的case的表达式无需为常量,且取值不限于整数- 必须是可比较的,无类型值
nil不能用作 switch 表达式
- 顺序匹配
case子句,只会运行匹配的case子句然后跳出switch语句 - 相当于自动添加
break,除非以fallthrough语句结束,否则分支会自动终止 fallthrough语句不会重新判断条件,不能用于类型开关
defer 关键字延迟表达式的执行
defer Expression- 表达式必须是函数或方法调用;不能用括号括起来
- 其参数会立即求值,但直到包含
defer子句的外层函数返回前,表达式都不会被调用
对表达式的调用会被压入一个栈中,返回时,按照后进先出的顺序调用
- 一些使用场景
释放互斥锁
打印页脚
goto 关键字跳转到同一函数内具有标签的语句
语法 goto <Label>
代码块外部的 goto 语句不能跳转到代码块内部的标签
- if 语句内的 goto 子句无法跳转到 for 语句内的标签 L1
函数和方法
变量和常量的声明和赋值
使用 var 关键字定义变量和类型 var a int
- 有初始赋值时可以省略类型定义,Go 会根据上下文推断
- 未明确初始值的类型会赋予对应默认值
- 数值类型为
0 - 布尔值类型为
false - 字符串类型为
""
操作符 := 可以简洁地声明变量,但只能用在函数内
- 函数外的每个语句都 必须 以关键字开始(
var、func等)
- 使用
const关键字定义常量和类型,不能用操作符:=来简化
func 关键字定义函数
- Go 函数默认是值传递,参数是副本
- 通过指针传递变量为参数
可以接受任意数量的参数 func add(x int, y int) int {}
- 参数变量后接其数值类型
x int
- 多个相同类型的参数可以仅保留最后一个类型关键字
(x int, y int)可以省略为(x, y int)
- 实际返回的值类型需要和函数定义的返回值类型一致,不允许隐式类型转换
可以返回任意数量的值
return y, x- 返回两个值
y, x
返回值可以被命名,被视作定义在函数顶部的变量
func split(sum int) (x, y int) { return }- 没有参数的
return语句会直接返回已命名的返回值x,y - 在长的函数中可能会影响代码的可读性
可以接受函数作为参数和返回值
- 样例里接受类型为
func(float64, float64) float64的函数参数fn,返回值为float64
函数值(function value)引用了外部变量,绑定在一起从而形成函数闭包(Closure)
- 闭包 = 内层函数 + 外层作用域中被引用的变量
- 内层函数可以读写外层函数的局部变量
- 即使外层函数已返回,这些变量仍“存活”在闭包中,因此成为闭包
样例:计数器闭包(无参数)
- 两个闭包相互独立
样例:累加器闭包(接受参数)
- 两个闭包相互独立
Go 没有类,但可以为类型定义一种特殊的函数作为方法
- 方法传入一个特定的类型作为接收者(Receiver),使得该类型的值可以“拥有”行为
常规的函数与方法对比
特性 | 函数(Function) | 方法(Method) |
定义 | func name(...) | func (recv Type) name(...) |
调用 | name(x) | x.name() |
关联 | 无 | 绑定到接收者类型 |
func (r ReceiverType) MethodName(parameters) ReturnType { }
r:接收者变量名(类似其他语言的self或this)
ReceiverType:接收者类型
接收者类型必须在当前的包中定义
- 不能为内置类型(如
int,string)直接定义方法
- 也不能为未命名类型(如
struct{}、[]int)定义方法
接收者类型分为值和指针
- 值接收者接收的是值的副本,不能修改原始值
func (v Vertex) Abs() float64
- 指针接收者接收的是值的地址,可以修改原始值
func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f }
- 一般选择指针作为接受者
- 方法能够修改其接收者指向的值
- 可以避免在每次调用方法时复制该值
- 值的类型为大型结构体时,可以避免拷贝开销
- 如果类型要实现接口,且接口方法是指针接收者,则必须用指针接收者
Go 语法糖:可寻址的值 v(如变量、结构体字段等)调用一个只有指针接收者才有的方法 Scale 时,Go 编译器会自动将语句 v.Scale(5) 解释为 (&v).Scale(5)
接收者为指针的的方法被调用时,接收者既能是指针又能是值
- 带指针参数的函数必须接受一个指针
反之亦然:接收者为值的的方法被调用时,接收者既能是值又能是指针
反例:不可寻址的值不能自动取地址
类型能调用的所有方法的集合称为方法集(Method Sets)
方法调用的合法性检查
x的类型的方法集中包含方法m
- 实参能赋值给形参(类型兼容)
命名类型 T 仅包含以 T 为接收者的值方法,不包含以 *T 为接收者的指针方法
- x 可以调用方法集为
{ ValueMethod }
- y 可以调用方法集为
{ ValueMethod, PointerMethod }
样例:Abs 方法拥有一个名字为 v,类型为 Vertex 的值接收者
样例:Abs 方法拥有一个名字为 f,类型为 MyFloat 的值接收者
样例:Scale 方法拥有一个名字为 v,类型为 *Vertex 的指针接收者
Go 协程,信道和协程协调
Go 程(goroutine)是由 Go 运行时管理的轻量级线程
- 比操作系统线程更轻量:初始栈仅约 2KB,而 OS 线程通常为 1–8MB
启动一个 goroutine 只需在函数调用前加关键字 go
所有 goroutine 共享同一地址空间(需注意并发安全)
- 通过
sync包提供了共享内存的访问
- 一般倾向于用 channel 类型共享数据,而不是用共享内存通信
样例:输出 Hello World
信道类型(Channel Types)提供了 goroutine 之间通信的机制
- 并发执行的函数能够通过发送和接收指定元素类型的值来进行通信
- 通道的行为类似于先进先出(FIFO)队列
- 未初始化的信道值为
nil nil信道永远无法进行通信
make 函数创建信道 make(chan type[, cap])
- 可选的容量
cap决定了信道内部缓冲区的大小,以元素个数计 - 如果容量为零或未指定,则该信道是无缓冲的(unbuffered),通信只有在发送方和接收方都已准备好时才能成功
- 指定了容量时,只要缓冲区未满(发送)或非空(接收),通信即可成功且不会阻塞
可选的 <- 操作符用于指定信道的方向:发送或接收
<-操作符指向即为数据流向- 在
<-操作符左侧为发送数据到信道 ch ch <- v // 将 v 发送至信道 ch- 在
<-操作符右侧为从信道 ch 接收数据 v := <-ch // 从 ch 接收值并赋予 v
如果类型指定了 <- 修饰符,则该信道是单向的;否则,信道是双向的
类型中的 <- 永远修饰最接近的 channel
- 常见场景:pipeline / fan-out / fan-in
func workerPool(in <-chan int) <-chan intfunc dispatcher(out chan<- <-chan int)
内置函数 close(ch) 关闭一个信道 ch
- 接收操作的多值赋值形式可以报告所接收到的值是否是在通道关闭前发送的
v, ok := <-ch- 如果 ch 还有数据:v 是接收到的值,ok = true
- 如果 ch 已关闭且无剩余数据:v 是类型的零值,ok = false
- 只应由发送者关闭信道,而不应由接收者关闭
- 向已关闭的信道发送数据会触发 panic
- 信道通常不需要关闭
- 信道是 Go runtime 管理的内存对象,GC 会自动回收,即使不
close
- 循环
for i := range c会不断从信道接收值,自动在信道关闭后退出
样例:切片通过协程求和
- 切片分为两个切片,创建协程分别执行 sum 函数,通过信道
c汇总 sum 函数执行结果并传递给 x, y
样例:通过协程计算斐波那契数列
样例:使用 close + range
阻塞(blocking) 是指一个 goroutine 在执行过程中暂停执行,等待某个条件满足
阻塞的常见场景
阻塞操作 | 说明 |
无缓冲 channel 的发送/接收 | ch <- x 或 <-ch 在没有配对操作时会阻塞 |
互斥锁(Mutex)竞争 | mu.Lock() 在锁被占用时阻塞 |
系统调用(I/O) | file.Read(), net.Conn.Read(), http.Get() 等 |
time.Sleep() | 主动休眠(也是一种阻塞) |
WaitGroup.Wait() | 等待计数归零 |
context 等待取消 | <-ctx.Done() |
阻塞可能导致的问题
- 死锁(Deadlock):多个 goroutine 互相等待对方释放资源,导致所有相关 goroutine 永久阻塞
- goroutine 泄漏:goroutine 因永远无法满足阻塞条件而无法退出,持续占用内存
- 忘记关闭 channel,导致 receiver 永远阻塞
- 未处理 context 取消信号
- 等待永远不会发生的事件
- 性能下降 / 响应变慢:过多 goroutine 阻塞在 I/O 或锁上
- 级联故障(Cascading Failure):微服务架构中常见的“雪崩效应”
- 一个服务因阻塞变慢 → 调用方等待超时 → 调用方线程/goroutine 耗尽 → 调用方也变慢 → 整个系统雪崩
其他协调 goroutine 的工具
select 语句从一组可能的发送或接收操作中选择一个来执行
- 语法类似于
switch语句,但所有case分支都必须是通信操作(即发送或接收)
select 语句执行分为以下三个步骤:求值,选择,执行
求值阶段(Evaluation):对 case 求值以得到待操作的 channel 及发送值
- 按源代码顺序对所有 case 中的以下内容精确求值一次,得到一组待操作的 channel 及对应的发送值
- 接收操作中的 channel 操作数(如
<-c1中的c1) - 发送语句中的 channel 和右侧要发送的值(如
c2 <- x中的c2和x)
- 注意:此阶段产生的副作用(side effects)总会发生,无论最终哪个分支被选中
- 接收语句左侧的赋值目标(如
i1 = <-c1中的i1)在此阶段不会被求值
选择阶段(Selection):选择分支,无可用分支则阻塞
- 如果至少有一个通信操作可以立即进行(即不会阻塞),则随机均匀地选择其中一个执行
- 否则,如果存在
default分支,则执行default分支 default分支可以出现在任意位置
- 如果既无可执行的通信操作,又没有
default分支,则select语句阻塞,直到至少有一个通信操作可以进行
执行阶段(Execution):通信(收/发)操作只会发生在选中的分支
- 如果选中的不是
default分支,则执行对应的通信操作(发送或接收)
- 如果选中的是带有短变量声明或赋值的接收语句(如
x := <-ch或x, ok := <-ch),则此时才对左侧变量进行求值并完成赋值
- 最后,执行所选
case中的语句列表
其他 select 语句典型用例
随机发送(无 default,无阻塞)
- 每次循环随机选择发送
0或1
永久阻塞
select 语句样例
sync.Mutex 互斥锁类型可以保护共享可变状态,确保同一时间只有一个 goroutine 访问临界区
- 提供
.Lock()和.Unlock()方法来保证一段代码的互斥执行
样例:并发安全计数器
- 用
defer语句来保证互斥锁一定会被解锁
sync.WaitGroup 类型可以让主 goroutine 等待 N 个子 goroutine 全部执行完毕
WaitGroup对象内部有一个从 0 开始的计数器,
- 提供
.Add(),Done()和.Wait()方法 Add(n)让计数器加 nDone()让计数器减一Wait()阻塞直到计数器清零
- 只用于同步“完成”事件
样例:并行下载多个文件后汇总
context.Context 类型在 goroutine 树中传播取消信号、超时或截止时间
- 常用于 HTTP 请求、数据库操作、微服务调用等有生命周期的操作
- 通常作为函数第一个参数传递:
func(ctx context.Context, ...)
样例
- Author:白鸟3
- URL:https://blog.kun2peng.top/develop/notebook_golang
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
