计算机基础知识点
本文讲解 Go 开发中经常遇到的计算机基础概念。这些不是 Go 特有的,但理解它们能帮你写出更好的 Go 代码。
1. 大小端 (Endianness)
什么是大小端?
大小端描述的是多字节数据在内存中的排列顺序。
- 大端 (Big-Endian):高位字节存在低地址(符合人类阅读习惯,从左到右)。
- 小端 (Little-Endian):低位字节存在低地址(x86/x64 CPU 的本地顺序)。
以 32 位整数 0x12345678 为例:
地址: 低地址 → 高地址
大端: [12] [34] [56] [78] ← 高位在前(网络字节序)
小端: [78] [56] [34] [12] ← 低位在前(x86 本地序)为什么需要关心?
- 网络协议统一使用大端(称为"网络字节序")。
- x86/x64 架构的 CPU 使用小端。
- 读写二进制文件格式(如 PNG、ELF 头部)时,要知道格式规范指定了什么端序。
- 在不同架构的机器之间传输二进制数据时。
Go 中如何处理
Go 的 encoding/binary 包提供了两种端序:
import "encoding/binary"
data := make([]byte, 4)
// 大端写入 (网络字节序)
binary.BigEndian.PutUint32(data, 0x12345678)
// data = [0x12, 0x34, 0x56, 0x78]
// 小端写入 (x86 本地序)
binary.LittleEndian.PutUint32(data, 0x12345678)
// data = [0x78, 0x56, 0x34, 0x12]
// 读回
val := binary.BigEndian.Uint32(data) // → 0x12345678
// 更简单的写法(Go 1.22+):
// binary.Encode / binary.Decode 支持 io.Writer/Reader + 端序🔬
binary.BigEndian.PutUint32详解:
- 方法签名:
func (bigEndian) PutUint32(b []byte, v uint32)b是目标字节切片,长度至少为 4。v是要写入的 uint32 值。- 写入后
b[0]存最高 8 位,b[3]存最低 8 位(大端序)。- 对应读取方法:
Uint32(b []byte) uint32。
如何判断本机端序
func IsLittleEndian() bool {
var i int32 = 0x01020304
u := unsafe.Pointer(&i)
pb := (*byte)(u)
return *pb == 0x04 // 低地址存的是最低字节 → 小端
}实际开发中不需要判断:网络传输永远用 BigEndian;本地顺序不重要。
2. Base64 编码
为什么需要 Base64?
二进制数据(如图片、加密密钥)中包含不可打印字符。如果直接放到 JSON/XML/URL 中会出问题。Base64 将任意二进制数据转换为64 个可打印 ASCII 字符。
原始: 任意字节 (0x00 ~ 0xFF)
Base64: 可打印字符 (A-Z, a-z, 0-9, +, /)
编码膨胀率: 约 33% (3 字节 → 4 字符)编码原理
每 3 个字节(24 bit)分成 4 组 6 bit,每组映射到一个字符。
原始数据: Man (3 字节)
ASCII: 77 97 110
二进制: 01001101 01100001 01101110
分组 (6bit): 010011 010110 000101 101110
十进制: 19 22 5 46
Base64 字符: T W F u
结果: "TWFu"填充:如果输入字节数不是 3 的倍数,用 = 填充到 4 的倍数。
| 余数字节 | 填充 |
|---|---|
| 剩 1 字节 | 加 2 个 = |
| 剩 2 字节 | 加 1 个 = |
| 刚好整除 | 不加 = (Raw) |
Base64 的三种变体
| 变体 | 第 62 字符 | 第 63 字符 | 填充 | 适用场景 |
|---|---|---|---|---|
| Standard | + |
/ |
有 = |
通用、PEM 证书 |
| URL | - |
_ |
有 = |
URL/文件名中的 token |
| RawURL | - |
_ |
无 = |
JWT(最常用) |
🚨 为什么 URL 需要
-和_? 因为+在 URL 中表示空格,/是路径分隔符,=是查询参数分隔符。JWT token 出现在 URL 中时,必须用 URL-safe 编码。
Go 中的 Base64
import "encoding/base64"
data := []byte("Hello, 世界")
// 标准编码
enc := base64.StdEncoding.EncodeToString(data)
dec, _ := base64.StdEncoding.DecodeString(enc)
// URL-safe 编码(最常用于 JWT / API token)
token := base64.RawURLEncoding.EncodeToString(data)
// 流式编码(处理大文件)
encoder := base64.NewEncoder(base64.StdEncoding, writer)
encoder.Write(largeData)
encoder.Close() // 必须调用:写入填充并 flush3. sendfile 系统调用与零拷贝
传统文件发送流程
从磁盘读取文件并通过网络发送的典型流程:
传统方式 (4 次拷贝):
磁盘 → 内核缓冲区 → 用户空间缓冲区 → 内核 Socket 缓冲区 → 网卡
(DMA) (CPU copy) (CPU copy) (DMA)
涉及: 2 次 DMA 拷贝 + 2 次 CPU 拷贝 + 4 次上下文切换sendfile 做了什么?
sendfile() 系统调用在内核态直接将数据从文件描述符传输到 socket 描述符,绕过用户空间。
sendfile 方式 (2 次拷贝):
磁盘 → 内核缓冲区 → 内核 Socket 缓冲区 → 网卡
(DMA) (CPU copy) (DMA)
涉及: 2 次 DMA 拷贝 + 1 次 CPU 拷贝 + 2 次上下文切换⚡ sendfile 的优势:省掉了到用户空间的来回拷贝和一半的上下文切换。处理大文件时性能提升显著。
Go 中的 sendfile
Go 标准库中,以下场景会自动使用 sendfile(Go runtime 透明优化):
// io.Copy — 当 src 是 *os.File 且 dst 是 *net.TCPConn 时自动使用 sendfile
http.ServeFile(w, r, "./large-file.zip") // 内部使用 sendfile
// 条件:
// 1. src 底层是 *os.File(能获取 fd)
// 2. dst 底层是 *net.TCPConn(能获取 fd)
// 3. 平台支持(Linux 2.4+, macOS, FreeBSD)🔬 深入原理:
io.Copy在内部会检测src是否实现了WriterTo(如*os.File),以及dst是否实现了ReaderFrom(如*net.TCPConn)。如果满足条件,Go runtime 通过poll.SendFile包装系统调用sendfile(2)。
// 手动触发 sendfile 的方式:
// 1. 使用 *os.File 作为 src
// 2. 使用 *net.TCPConn 作为 dst
// 3. 调用 io.Copy
file, _ := os.Open("video.mp4")
conn, _ := net.Dial("tcp", "example.com:8080")
tcpConn := conn.(*net.TCPConn)
io.Copy(tcpConn, file) // ← 自动使用 sendfile4. 内存对齐 (Memory Alignment)
为什么需要对齐?
CPU 访问内存时不是逐字节读取,而是按字长(32 位 CPU 一次读 4 字节,64 位一次读 8 字节)。如果数据跨越了两个字的边界,CPU 需要两次内存访问。
未对齐: [字1] [字2]
[data] ← 跨边界,需要 2 次读取
对齐: [字1] [字2]
[data] ← 在边界内,1 次读取Go 中的对齐规则
- 类型的对齐值 = 类型大小 和 平台字长 的较小值。
- 结构体字段按对齐值填充。
// 浪费内存的布局:48 字节
type BadLayout struct {
a bool // 1 字节 + 7 字节填充(对齐到 8)
b int64 // 8 字节
c bool // 1 字节 + 7 字节填充(对齐到 8)
}
// 优化后的布局:16 字节
type GoodLayout struct {
b int64 // 8 字节
a bool // 1 字节
c bool // 1 字节
// + 6 字节尾部填充
}💡 最佳实践:结构体字段按类型大小从大到小排列,可以减少内存占用。
// 查看类型的大小和对齐
fmt.Println(unsafe.Sizeof(BadLayout{})) // 大小
fmt.Println(unsafe.Alignof(BadLayout{})) // 对齐5. 内存屏障 (Memory Barrier/Fence)
问题:CPU 和编译器的重排
为了提高性能,CPU 和编译器可能重排指令执行顺序:
// 你写的代码:
data = 42 // A
ready = true // B
// CPU 可能执行的顺序:
ready = true // B 先执行
data = 42 // A 后执行在单线程中没有区别,但在并发场景中,另一个 goroutine 可能看到 ready == true 但 data 还是旧值。
Go 中的内存模型保证
sync.Mutex的Lock/Unlock提供了 acquire/release 语义。sync/atomic的原子操作提供了顺序保证。- Channel 的发送/接收提供了 happens-before 关系。
var data int
var done = make(chan bool)
// goroutine 1
data = 42
done <- true // ← 发送前所有写操作对接收方可见
// goroutine 2
<-done // ← 接收后能看到 goroutine 1 的所有写操作
fmt.Println(data) // 保证是 42🚨 陷阱:普通的
bool标志位不提供内存屏障。多个 goroutine 共享标志位时必须用atomic.Bool或 channel。
6. 文件描述符 (File Descriptor)
每个打开的文件/网络连接在内核中对应一个文件描述符(非负整数)。
// Go 中文件描述符的生命周期
file, _ := os.Open("data.txt") // 分配 fd (如 fd=5)
// 使用 file...
file.Close() // 释放 fd(如果忘记,fd 泄漏)
// 每个进程的 fd 是有限的:
// Linux 默认 1024(ulimit -n 查看)
// 高并发服务器需要调大🚨 常见问题:HTTP 客户端忘记
resp.Body.Close()→ fd 泄漏 → 最终too many open files。
7. 🚨 常见陷阱速查
| 概念 | 陷阱 | 正确做法 |
|---|---|---|
| 大小端 | 网络传输用本地端序 | 网络传输统一 BigEndian |
| Base64 | 不同变体混用编解码 | 编码和解码用同一个 Encoding |
| 内存对齐 | 不关心字段排列顺序 | 按类型大小从大到小排列 |
| 内存屏障 | 用普通变量做并发标志 | 用 atomic 或 channel |
| 文件描述符 | 忘记 Close 导致泄漏 | 始终 defer Close() |