Lazy loaded image
编程开发
Go 语言学习笔记
Words 13052Read Time 33 min
2026-1-18
2026-1-18
date
related_level
slug
type
relate_date
summary
status
tags
category
last_updated
Jan 18, 2026 10:11 PM
是否已更新
orginal_page
是否推荐
官方教程
Go 练习
设置国内镜像源
参考资料
  • go env -w GO111MODULE=on
    • 打开模块支持
  • go env -w GOPRIVATE=git.mycompany.com,github.com/my/private
    • 设置不走代理的私有仓库
  • 取消镜像代理或校验
    • go env -w GOPROXY=direct
    • go env -w GOSUMDB=off
      • 取消校验
  • 镜像源(阿里云)
    • go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
      • 不支持 GOSUMDB
  • 镜像源(官方代理)
    • go env -w GOPROXY=https://proxy.golang.com.cn,direct
    • go 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 int64
    • rune // int32 的别名,表示一个 Unicode 码位
    • uint uint8 uint16 uint32 uint64 uintptr
      • byte // uint8 的别名
    • float32 float64
    • complex64 complex128
    Go 指针 *T 保存了值的内存地址,不支持指针运算,不支持直接内存地址操作
    • 类型 *T 为指向 T 类型值的指针,其零值为 nil
      • var p *int
    & 操作符生成一个指向其操作数的指针
    * 操作符解引用指针以获取其指向的底层值
    • 指针间接引用的使用场景
      • 在函数中修改原始变量
        避免大对象拷贝(性能优化)
        • 传递结构体指针比复制整个结构体更高效
    映射 map[KeyType]ValueType 将键映射到值
    • 未初始化时的零值是 nil
      • nil 映射既没有键,也不能添加键
    键类型必须是可以比较的
    • 基本类型:intstringboolfloat64
    • 指针、通道、接口(只要动态值可比较)
      • 结构体(所有字段都可比较)
      • 数组(元素类型可比较)
      • 映射在遍历时不保证顺序(每次运行可能不同)
      始终用 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)
        • 对于内置类型(如 intstring[]byte 等),底层类型就是自身
      基本接口为一组方法签名(参数、返回值、名称)的集合
      • 方法签名(参数、返回值、名称)必须完全匹配
      • 每个显式指定的方法名称必须唯一且不能是空白标识符
      嵌入接口包含了实现 T 显式声明的方法和实现嵌入接口 E 的所有方法
      嵌入接口时,同名方法必须具有完全相同的签名
      通用接口通过类型元素限制接口的类型集合
      类型元素可以是
      • 任意类型项 T
      形如 ~T 的底层类型项(表示底层类型为 T 的所有类型)
      • T 的底层类型必须是它自身,且 T 不能是接口类型
      多个类型项的联合(union),形式为 t1 | t2 | … | tn
      结合方法声明,类型元素可以精确地定义接口的类型集合,即类型约束(type constraints)
      • 空接口的类型集合是所有非接口类型的集合
      • 非空接口的类型集合是其所有接口元素类型集合的交集
      • 方法声明的类型集合是所有非接口类型方法集包含该方法的类型的集合
        • “所有非接口类型”的量化范围不仅限于当前程序中声明的类型,而是指所有可能程序中的所有非接口类型,因此是无限的
      • 非接口类型项 T 的类型集合仅包含该类型 T 本身
      • 形如 ~T 的项的类型集合是所有底层类型为 T 的类型的集合
      • 联合项 t1 | t2 | … | tn 的类型集合是各个项类型集合的并集
      基本接口和通用接口对比
      • 基本接口只能约束「行为」,不能约束「类型形态」
      • 通用接口限定「可用的类型集合」
        • 再加上方法则可以同时约束「操作能力 + 行为」
      特性
      基本接口
      通用接口
      组成
      仅方法
      方法 + 类型项(T~TT1|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)可以接受哪些类型
        • 类型参数是占位符(如 TKV),代表将来会传入的具体类型
        • 类型约束必须是接口类型(包括 anycomparable 等预声明接口)
          • any 是空接口 interface{} 的别名
          • comparable 在泛型中限制类型参数必须可比较
            • comparable 是一个特殊的空接口,其类型集合包含所有支持 == 和 != 操作的非接口类型
              • 接口类型本身不在 comparable 的类型集合中
                • 接口是否可比较取决于其动态类型,而泛型约束必须在编译时静态确定
              • 是否满足由编译器根据类型是否可比较自动决定
            • 常见可比较类型
              • 类型
                示例
                基本类型
                intfloat64stringbool
                指针
                *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) int
          • s 是满足类型约束 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 pow
          • for _, value := range pow
          • for i := range pow
        • 最多两个操作数(通道/整数只能一个)
        右侧的表达式为范围表达式,表示遍历目标
        • 遍历 pow 切片,打印下标和对应元素值的一份副本
          • range 使用快照,修改原数据不影响遍历值
        不同范围表达式的迭代行为
        Range 表达式类型
        变量数
        第一个值(key/索引)
        第二个值(value/元素)
        迭代顺序
        特殊说明
        数组 / 切片 / 数组指针 [N]T[]T*[N]T
        2
        索引 iint
        元素 a[i](类型 T
        固定:0 → len-1
        • nil 切片:0 次迭代 • 修改原数据不影响遍历值
        字符串 string
        2
        字节索引 iint
        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
          • 字符串类型为 ""
        操作符 := 可以简洁地声明变量,但只能用在函数内
        • 函数外的每个语句都 必须 以关键字开始(varfunc 等)
          • 使用 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:接收者类型
          接收者类型必须在当前的包中定义
          • 不能为内置类型(如 intstring)直接定义方法
          • 也不能为未命名类型(如 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 共享同一地址空间(需注意并发安全)
          • 一般倾向于用 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 int
            • func 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) 让计数器加 n
            • Done() 让计数器减一
            • Wait() 阻塞直到计数器清零
          • 只用于同步“完成”事件
          样例:并行下载多个文件后汇总
          context.Context 类型在 goroutine 树中传播取消信号、超时或截止时间
          • 常用于 HTTP 请求、数据库操作、微服务调用等有生命周期的操作
          • 通常作为函数第一个参数传递:func(ctx context.Context, ...)
          样例
          上一篇
          GnuPG 加密工具
          下一篇
          Prometheus 客户端 Python 库 prometheus_client

          Comments
          Loading...