Skip to content
Go
计算机基础知识点

计算机基础知识点

本文讲解 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()  // 必须调用:写入填充并 flush

3. 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)  // ← 自动使用 sendfile

4. 内存对齐 (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 == truedata 还是旧值。

Go 中的内存模型保证

  • sync.MutexLock/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()