Go 语言学习笔记

一、基础语法

1.1 结构体与接口

Go 中结构体和接口通过**隐式实现(Duck Typing)**关联,不需要 implements 关键字。

go 复制代码
type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return d.Name + ": Woof!"
}

// Dog 自动满足 Speaker 接口,无需声明
func MakeItSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

方法集规则

  • T 值只能调用值接收者方法
  • *T 指针可调用值+指针接收者方法

编译期验证

go 复制代码
var _ Speaker = Dog{}        // 验证值接收者
var _ Speaker = (*Dog)(nil)  // 验证指针接收者

1.2 不同接口同名方法

场景 结果
同名同签名 一次实现,同时满足两个接口
同名同签名,语义不同 编译通过但逻辑错误,需拆分类型
同名不同签名 无法同时满足,编译错误

Go 只看方法签名,不看接口名字或语义。

1.3 切片(Slice)

底层结构
go 复制代码
type slice struct {
    ptr *array  // 指向底层数组的指针
    len int     // 当前长度
    cap int     // 容量
}
len vs cap
  • len :当前可读写的元素个数(s[i]0 <= i < len 合法)

  • cap:当前不重新分配底层数组,最多能装多少个元素

    底层数组: [1] [2] [3] [4] [5] [6] [7] [8]
    ↑ ↑ ↑
    ptr len cap

  • [0, len) 范围:可读可写

  • [len, cap) 范围:空间已分配,但无法直接访问,只能通过 append 逐个扩展

  • 超过 cap:自动扩容,不是硬限制

扩容规则(Go 1.18+)
条件 最终容量
期望容量 > 旧容量 * 2 直接用期望容量
旧容量 < 256 新容量 = 旧容量 * 2
旧容量 >= 256 新容量 = 旧容量 + 旧容量/4 + 192(平滑增长,趋近 ~1.25x)

计算后还会进行内存对齐,实际 cap 可能更大。

扩容本质:分配新底层数组 + memmove 批量拷贝旧数据。

常见陷阱
go 复制代码
// 1. 共享底层数组
a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b = append(b, 99) // cap够,不扩容,覆盖 a[3]!

// 2. 三索引切割限制 cap
b := a[1:3:3]     // len=2, cap=2,限制 cap
b = append(b, 99) // cap不够,扩容,不影响 a
排序

sort.Slice(推荐)

go 复制代码
sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age // 升序
})

slices.SortFunc(Go 1.21+)

go 复制代码
slices.SortFunc(people, func(a, b Person) int {
    return a.Age - b.Age // 升序:a-b,降序:b-a
})

cmp(a, b) 返回负数 -> a 排前面,正数 -> b 排前面,0 -> 相等。

1.4 数组 vs 切片

数组 切片
长度 固定,编译期确定 动态,运行时可变
类型 [3]int[5]int 是不同类型 []int 统一类型
传参 值拷贝 传切片头(廉价)

[...]int{1,2,3} 是数组初始化的语法糖,编译器自动推断长度,结果等同于 [3]int{1,2,3},仍是固定长度数组。不能用于函数参数或类型声明。

1.5 可变参数

go 复制代码
func sum(nums ...int) int { ... }  // nums 内部是 []int
sum(1, 2, 3)                        // 直接传
sum(nums...)                        // 展开切片传入

1.6 基本数据类型

分类 类型
布尔 bool
有符号整数 int8 int16 int32 int64 int
无符号整数 uint8 uint16 uint32 uint64 uint uintptr
浮点 float32 float64
复数 complex64 complex128
字符串 string
别名 byte(uint8) rune(int32)

1.7 rune 与 string

  • rune :Unicode 码点,int32,一个整数编号
  • string:UTF-8 编码后的字节序列
go 复制代码
var r rune = '中'       // 码点: 20013 (0x4E2D)
var s string = "中"     // UTF-8: E4 B8 AD(3字节)

s := string(r)          // 码点 → UTF-8 编码
r, _ := utf8.DecodeRuneInString(s) // UTF-8 → 码点

字符串索引 s[i] 返回 byte(uint8),range 遍历返回 rune(int32)。


二、make vs new

new(T) make(T)
返回值 *T 指针 T
初始化 零值(不初始化内部结构) 完整初始化(可直接使用)
适用范围 任何类型 仅 slice、map、channel
类型 make 字面量 var 声明 var 后能否直接用
切片 可 nil切片 append 可,读写可
Map 可 nil map 读取可,写入 panic
Channel 不可 可 nil channel 发送/接收永久阻塞,close panic

三、并发编程

3.1 Channel

Channel 是 goroutine 之间传递数据和同步的管道。

Go 哲学:不要通过共享内存来通信,而要通过通信来共享内存。

无缓冲 vs 有缓冲
无缓冲 有缓冲
创建 make(chan int) make(chan int, N)
发送阻塞 没人接收就阻塞 缓冲区满才阻塞
接收阻塞 没人发送就阻塞 缓冲区空才阻塞
本质 同步,一对一握手 异步,生产者-消费者
关闭 Channel
规则 说明
谁发送谁关闭 只有发送方关闭
不能关闭已关闭的通道 panic
不能向已关闭的通道发送 panic

不是必须关闭 ,唯一必须关闭的场景是接收方用 range 遍历时。不关闭 range 会死锁。

方向限定
go 复制代码
func send(ch chan<- int)  {} // 只写
func recv(ch <-chan int)  {} // 只读

3.2 获取协程执行结果

goroutine 的返回值会被丢弃,只能通过 channel 获取结果。

go 复制代码
type Result struct {
    ID    int
    Value int
    Err   error
}

ch := make(chan Result, 3)
for i := 0; i < 3; i++ {
    go func(id int) {
        val, err := doWork(id)
        ch <- Result{ID: id, Value: val, Err: err}
    }(i)
}
for i := 0; i < 3; i++ {
    r := <-ch
    fmt.Printf("任务%d: 结果=%d\n", r.ID, r.Value)
}

3.3 GMP 调度模型

复制代码
G - Goroutine  协程(用户态轻量线程)
M - Machine    操作系统线程(内核态)
P - Processor   逻辑处理器(调度上下文)

P 是 GMP 的灵魂:M 必须持有 P 才能执行 G。

M 从哪拿 G(优先级)
复制代码
1. P 的本地队列(LRQ)   ← 优先,无锁
2. 全局队列(GRQ)       ← 每 61 次取 1 个,有锁但极少
3. 其他 P 偷(work stealing) ← 本地空了才偷
4. 网络轮询器(netpoll) ← 等 IO 的 G
5. 阻塞等待              ← 实在没有就休眠
两种阻塞的区别
系统调用阻塞 Channel/IO 阻塞
谁阻塞了 M 阻塞(线程卡在内核) G 阻塞(协程挂起)
P 要不要解绑 必须,M 卡住了 不需要,M 还活着
M 的去向 等系统调用返回 继续执行其他 G
P 的去向 找新 M 绑定(hand off) 不动,继续用当前 M
全局队列的锁竞争

全局队列确实有锁,但访问频率极低:

  • P 优先从本地无锁队列取 G(99%)
  • 每 61 次调度才从全局队列取 1 个
  • 取时批量获取,减少访问次数
  • 本地队列满溢出时才放全局队列
抢占式调度
  • G 执行超过 10ms 被标记为抢占
  • Go 1.14+ 基于信号的抢占,即使死循环也能被踢走

四、垃圾回收(GC)

4.1 核心特点

特点 说明
算法 三色标记清除
触发方式 并发,不 Stop The World(大部分时间)
不支持 无分代、无压缩

4.2 三色标记法

颜色 含义
白色 未被访问,GC 结束后回收
灰色 已访问,但引用的对象还没全访问
黑色 已访问,且引用的对象都已访问

过程:从根对象出发,标灰 → 标黑 + 引用标灰 → 直到没有灰色 → 剩余白色 = 垃圾。

4.3 写屏障

并发标记时,用户代码可能修改引用关系导致漏标。写屏障在每次修改引用时,强制将被引用的对象标灰,防止漏标。

写屏障只对堆生效,栈上没有写屏障(栈操作太频繁,加屏障性能损失大)。

4.4 GC 完整流程

复制代码
1. 标记准备(STW,极短)  → 开启写屏障,根对象标灰
2. 并发标记(和用户代码并行)→ 扫描灰色对象,写屏障保护
3. 标记终止(STW,极短)  → 重扫描栈,处理剩余灰色,关闭写屏障
4. 并发清除(和用户代码并行)→ 回收白色对象

STW(Stop The World):暂停所有用户代码,只让 GC 干活。Go 只在开头和结尾各 STW 极短时间(微秒级)。

4.5 GC 触发条件

条件 说明
堆内存增长到阈值 默认 GOGC=100,堆翻倍时触发
2分钟未触发 强制触发一次
手动调用 runtime.GC()

4.6 减少GC压力

  • 减少堆分配,用值类型
  • sync.Pool 复用对象
  • 预分配切片 make([]int, 0, N)
  • strings.Builder 代替字符串拼接

五、init 函数

  • 在 main 函数之前,包被导入时自动执行
  • 不能被调用,不能有参数和返回值
  • 一个文件可有多个 init,按定义顺序执行
  • 跨包:依赖的包先初始化
  • 只执行一次,即使包被多次导入

执行顺序:全局变量初始化 → init → main


六、defer

  • 延迟执行,函数返回前调用,先进后出(栈)
  • 参数在 defer 时就确定
  • 最常用场景:资源释放(Close/Unlock)
  • defer 可以修改命名返回值
  • 循环中 defer 会堆积,改用匿名函数
go 复制代码
func work() {
    mu.Lock()
    defer mu.Unlock()
    // 不怕中间 return 忘记解锁
}

七、recover

只有在 defer 中调用 recover 才能捕获 panic,其他地方调用返回 nil。

go 复制代码
defer func() {
    if r := recover(); r != nil {
        fmt.Println("捕获:", r)
    }
}()

八、装饰器

Go 没有装饰器语法,用函数包装实现:

go 复制代码
func timer(fn func()) func() {
    return func() {
        start := time.Now()
        fn()
        fmt.Printf("耗时: %v\n", time.Since(start))
    }
}

work := timer(work) // 包装
work()

HTTP 中间件是最典型的装饰器模式。


九、JSON 处理

go 复制代码
type User struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email,omitempty"`
    Pass  string `json:"-"`
}

// 解析
var user User
json.Unmarshal([]byte(jsonStr), &user) // 必须传指针

// 序列化
data, _ := json.Marshal(user)
tag 作用
json:"name" JSON 字段名为 name
json:"-" 忽略该字段
json:"name,omitempty" 为零值时省略

注意:数字默认解析为 float64


十、反射(reflect)

10.1 TypeOf vs ValueOf

go 复制代码
reflect.TypeOf(x)  → reflect.Type  → 类型信息(名字、字段、tag、方法签名)
reflect.ValueOf(x) → reflect.Value → 值信息(读值、改值、调方法)

10.2 操作结构体

go 复制代码
t := reflect.TypeOf(u)   // Type 上
v := reflect.ValueOf(u)  // Value 上

// 字段
t.NumField()              // 字段数量
t.Field(i).Name           // 字段名
t.Field(i).Tag.Get("json") // tag
v.Field(i)                // 字段值
v.FieldByName("Name")     // 按名字取值

// 方法
t.NumMethod()             // 方法数量
t.Method(i).Name          // 方法名
v.MethodByName("X").Call(args) // 调用方法

10.3 Map 相关

go 复制代码
// Type 上
t.Key()   // map 的 key 类型
t.Elem()  // map 的 value 类型 / slice 元素类型 / chan 元素类型

// Value 上
v.MapKeys()           // 所有 key
v.MapIndex(key)       // 根据 key 取 value

10.4 修改值(必须传指针)

go 复制代码
v := reflect.ValueOf(&x)
v = v.Elem()    // 解引用
v.SetInt(100)   // 修改

10.5 Kind 种类

go 复制代码
reflect.Int / reflect.String / reflect.Bool / reflect.Struct /
reflect.Map / reflect.Slice / reflect.Ptr / reflect.Chan / reflect.Func

十一、版本管理工具

工具 特点
goenv Go 版本切换,最常用
gvm 功能全但不推荐,维护不活跃
mise 多语言版本管理,asdf 的 Rust 重写,新项目推荐
asdf 多语言版本管理元老,慢,已被 mise 取代

mise 安装:

powershell 复制代码
# Windows
winget install jdx.mise --source winget --location "D:\software"

# macOS
brew install mise

mise 替代的是版本管理(nvm/pyenv/goenv),不是包管理。数据科学仍需 conda。


十二、Python match-case(对比参考)

Python 3.10+ 的 match-case 不是简单 switch,而是模式匹配,支持解构、类型判断、条件守卫:

python 复制代码
match point:
    case (0, 0):       print("Origin")
    case (x, 0):       print(f"X axis: {x}")
    case (x, y):       print(f"Point: ({x}, {y})")
特性 Go switch Python match-case
基本值匹配 支持 支持
解构匹配 不支持 支持
类型匹配 不支持 支持
条件守卫 不支持 case x if cond:
相关推荐
聚搜云——JuSouClouD2 小时前
深圳阿里云代理商:K8s Pod OOM智能排查修复指南
阿里云·kubernetes·云计算
明王明王2 小时前
从零搭建一个单节点 K8S 可观测实验室(六):安装 Tempo + OpenTelemetry,构建链路追踪链路
云原生·容器·kubernetes
wsad05323 小时前
Docker 网络故障排查记:当 bridge 模式失效时,host 模式如何救场
运维·docker·容器
张毅2004-10-103 小时前
docker详解:安装docker,docker架构,docker镜像、容器、网络、存储,容器监控,容器日志,综合实验
linux·运维·docker·云原生·云计算
小狼嚎月3 小时前
K8s PV / PVC / StorageClass 有状态应用持久化
云原生·容器·kubernetes
AAA@峥4 小时前
K8s 资源混乱怎么办?Namespace 隔离 + 上下文切换实战
容器·贪心算法·kubernetes
重庆小透明4 小时前
深入探寻微服务【第一篇微服务的坏】
微服务·云原生·架构
Zhu7589 小时前
在k8s集群部署MySQL单实例,支持多个主流发行版
android·mysql·kubernetes
张文君13 小时前
docker使用代理拉取镜像
运维·docker·容器