Go 语言进阶

这篇博客将深入探讨 Go 语言的一些核心概念,包括并发编程中的线程加锁和调度、内存管理机制,以及依赖管理。这些概念是构建高性能、可扩展的 Go 应用程序的基础。

1. 线程加锁

在并发编程中,多个 goroutine(Go 语言的轻量级线程)可能会同时访问和修改共享资源。为了防止数据竞争和不一致,我们需要使用锁机制来协调对共享资源的访问。

Go 语言的锁机制

Go 语言不仅提供了基于 CSP(Communicating Sequential Processes)的通道(channel)通信模型,还支持基于共享内存的多线程数据访问。sync 包提供了多种锁原语,以满足不同的并发场景需求:

  • sync.Mutex(互斥锁):
    • 最基本的锁类型,用于保护临界区,确保同一时间只有一个 goroutine 可以访问共享资源。
    • 使用 Lock() 方法加锁,Unlock() 方法解锁。
1
2
3
4
5
6
7
8
9
10
11
12
import "sync"

var (
counter int
mutex sync.Mutex
)

func increment() {
mutex.Lock() // 加锁
counter++
mutex.Unlock() // 解锁
}
  • sync.RWMutex(读写锁):
    • 适用于读多写少的场景。
    • 允许多个 goroutine 同时读取共享资源,但在写入时会阻塞所有其他 goroutine(包括读取和写入)。
    • 使用 RLock()RUnlock() 方法进行读锁定,Lock()Unlock() 方法进行写锁定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "sync"

var (
data map[string]string
rwMutex sync.RWMutex
)

func readData(key string) string {
rwMutex.RLock() // 读锁定
defer rwMutex.RUnlock()
return data[key]
}

func writeData(key, value string) {
rwMutex.Lock() // 写锁定
defer rwMutex.Unlock()
data[key] = value
}
  • sync.WaitGroup:
    • 用于等待一组 goroutine 完成。
    • Add(n) 方法增加等待的 goroutine 数量,Done() 方法表示一个 goroutine 完成,Wait() 方法阻塞直到所有 goroutine 完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "sync"

func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 表示当前 goroutine 完成
fmt.Printf("Worker %d starting\n", id)
// 执行任务...
fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加等待的 goroutine 数量
go worker(i, &wg)
}
wg.Wait() // 阻塞,直到所有 goroutine 完成
fmt.Println("All workers done")
}
  • sync.Once:
    • 确保某个函数只执行一次,常用于单例模式的初始化。
    • Do(f) 方法接收一个函数 f 作为参数,并保证 f 只会被调用一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "sync"

var (
once sync.Once
instance *MySingleton
)

type MySingleton struct {
// ...
}

func GetInstance() *MySingleton {
once.Do(func() { // 保证只执行一次
instance = &MySingleton{}
// 初始化 instance...
})
return instance
}
  • sync.Cond(条件变量):
    • 用于协调多个 goroutine 在满足特定条件时等待或被唤醒。
    • Wait() 方法阻塞当前 goroutine,直到其他 goroutine 调用 Signal()Broadcast() 方法唤醒。
    • Signal() 方法唤醒一个等待的 goroutine,Broadcast() 方法唤醒所有等待的 goroutine。
    • 通常与互斥锁一起使用,以保护条件。
    • 下面是 Kubernetes 中使用的 sync.Cond 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Type struct {
cond sync.Cond
// ... 其他字段
}

// Add marks item as needing processing.
func (q *Type) Add(item interface{}) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
// ... 添加 item 到队列的逻辑 ...
q.cond.Signal() // 唤醒一个等待的 goroutine
}

// Get blocks until it can return an item to be processed.
func (q *Type) Get() (item interface{}, shutdown bool) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for len(q.queue) == 0 && !q.shuttingDown {
q.cond.Wait() // 等待条件满足(队列中有元素)
}
// ... 从队列中取出 item 的逻辑 ...
return item, false
}

代码示例:Kubernetes 中的锁应用

  • sharedInformerFactory 中的 sync.Mutex:
1
2
3
4
5
6
7
8
9
10
11
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock() // 加锁
defer f.lock.Unlock() // 解锁
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}

这个例子展示了 Kubernetes 的 sharedInformerFactory 如何使用互斥锁来保护 informersstartedInformers 字段,确保在启动 informer 时不会发生并发冲突。

  • PodClient 中的 sync.WaitGroup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CreateBatch create a batch of pods. All pods are created before
// waiting.
func (c *PodClient) CreateBatch(pods []*v1.Pod) []*v1.Pod {
ps := make([]*v1.Pod, len(pods))
var wg sync.WaitGroup
for i, pod := range pods {
wg.Add(1) // 增加等待的 goroutine 数量
go func(i int, pod *v1.Pod) {
defer wg.Done() // 表示当前 goroutine 完成
defer GinkgoRecover()
ps[i] = c.CreateSync(pod)
}(i, pod)
}
wg.Wait() // 阻塞,直到所有 goroutine 完成
return ps
}

这个例子展示了 Kubernetes 的 PodClient 如何使用 sync.WaitGroup 来并发创建多个 Pod,并在所有 Pod 创建完成后返回结果。

2. 线程调度

在操作系统层面,线程是调度的基本单位。理解线程调度对于理解 Go 语言的 goroutine 调度至关重要。

进程与线程

  • 进程: 资源分配的基本单位。每个进程都有独立的内存空间、文件句柄、信号处理等。
  • 线程: 调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间),但有各自的栈和寄存器。

在 Linux 中,无论是进程还是线程,都由 task_struct 结构体表示。从内核的角度来看,进程和线程没有本质区别。Glibc 提供的 pthread 库实现了 NPTL(Native POSIX Threading Library),为用户空间提供了线程支持。

Linux 进程的内存模型

graph LR
    subgraph 虚拟地址空间
        A[内核空间] --> B(参数环境变量)
        B --> C[栈 Stack]
        C --> D[未分配内存]
        D --> E[堆 Heap]
        E --> F[未初始化数据 BSS]
        F --> G[已初始化数据 Data]
        G --> H[程序代码 Text]
    end
  
    subgraph 物理内存
        I[物理内存]
    end

    subgraph 磁盘
        J[磁盘_虚拟内存]
    end
  
    H --> PGD
    PGD --> PUD
    PUD --> PMD
    PMD --> PT
    PT --> I

    I -- 换出/换入 --> J
  
    A -.->|用户空间不可见| I

    style D fill:#f9f,stroke:#333,stroke-width:2px
  • 内核空间: 存放内核代码和数据,用户程序不能直接访问。
  • 栈 (Stack): 存储局部变量、函数参数、返回值等。栈的大小是有限的,由操作系统或编译器决定。
  • 堆 (Heap): 动态分配的内存区域,用于存储程序运行时创建的对象。
  • BSS 段: 存储未初始化的全局变量和静态变量。
  • 数据段 (Data): 存储已初始化的全局变量和静态变量。
  • 代码段 (Text): 存储程序的机器指令。

CPU 对内存的访问

graph LR
    A[CPU] --> B(MMU)
    B --> C{TLB}
    C -- 命中 --> F[总线]
    C -- 未命中 --> D[页表 PGD/PUD/PMD/PT]
    D --> F
    F --> E[物理内存]
  1. CPU 发出虚拟地址。
  2. MMU(Memory Management Unit)首先在 TLB(Translation Lookaside Buffer)中查找虚拟地址对应的物理地址。
  3. 如果 TLB 命中,则直接从 TLB 中获取物理地址。
  4. 如果 TLB 未命中,则 MMU 需要查询页表(PGD、PUD、PMD、PT)来获取物理地址,并将映射关系缓存到 TLB 中。
  5. CPU 使用物理地址访问内存。

进程切换开销

进程切换的开销主要包括:

  • 直接开销:
    • 切换页表全局目录(PGD)。
    • 切换内核态堆栈。
    • 切换硬件上下文(寄存器等)。
    • 刷新 TLB。
    • 执行系统调度器的代码。
  • 间接开销:
    • CPU 缓存失效,导致访问内存的次数增加。

线程切换开销

线程切换比进程切换开销小,因为同一进程内的线程共享虚拟地址空间,不需要切换页表。但线程切换仍然需要内核参与,进行上下文切换。

用户线程 vs. 内核线程

  • 用户线程: 在用户空间创建和管理,无需内核支持。创建和销毁速度快,但不能利用多核 CPU。
  • 内核线程: 由内核创建和管理,可以利用多核 CPU,但创建和销毁开销较大。
graph LR
    subgraph 用户态
        A[Process] --> B[User Thread]
        A --> C[User Thread]
        D[Process] --> E[User Thread]
        D --> F[User Thread]
        G[Process] --> H[User Thread]
        G --> I[User Thread]
    end

    subgraph 内核态
        B --> J[Kernel Thread]
        C --> J
        E --> K[Kernel Thread]
        F --> K
        H --> L[Kernel Thread]
        I --> L
    end

    J --> M[CPU]
    K --> M
    L --> M

Goroutine

Go 语言通过 GMP 模型实现了用户态线程(goroutine):

  • G (Goroutine): 代表一个 goroutine,拥有自己的栈空间、定时器等。初始栈大小为 2KB,可按需增长。
  • M (Machine): 代表内核线程,负责执行 goroutine。M 会保存 goroutine 的栈信息,以便在调度时恢复执行。
  • P (Processor): 代表逻辑处理器,负责调度 goroutine。P 维护一个本地 goroutine 队列,M 从 P 获取 goroutine 并执行。P 还负责部分内存管理。
graph LR
    subgraph 全局资源
        A(GRQ - 全局可运行G队列)
        B(sudog - 阻塞队列)
        C(gFree - 全局自由G列表)
        D(pidle - 全局空闲P列表)
    end

    subgraph P的本地资源
        E(LRQ - 本地可运行G队列)
        F(gFree - 本地自由G列表)
    end

    A -- G --> E
    B -- G --> M
    C -- G --> C  
    D -- P --> D  

    E -- G --> M
    F -- G --> F

    M --> P
    P --> CPU

    G1(G - Grunnable) -- 状态 --> G2(G - Grunning)
    G2 -- 状态 --> G3(G - Gwaiting)
    G2 -- 状态 --> G4(G - Gsyscall)
    G3 -- 状态 --> G1
    G4 -- 状态 --> G1
    G2 -- 状态 --> G5(G - Gdead)
    G5 -- 状态 --> F

    P1(P - Pidle) -- 状态 --> P2(P - Prunning)
    P2 -- 状态 --> P3(P - Psyscall)
    P3 -- 状态 --> P1

Goroutine 的创建过程

  1. 获取或创建新的 Goroutine 结构体:
    • 尝试从 P 的 gFree 列表中获取空闲的 Goroutine。
    • 如果 gFree 列表为空,则通过 runtime.malg 创建一个新的 Goroutine 结构体。
  2. 将函数参数复制到 Goroutine 的栈上。
  3. 更新 Goroutine 的调度信息,将其状态设置为 _Grunnable
  4. 将 Goroutine 存储到全局变量 allgs 中。
  5. 将 Goroutine 放入运行队列:
    • 优先放入 P 的 runnext 字段,作为下一个要执行的 Goroutine。
    • 如果 P 的本地运行队列已满,则将一部分 Goroutine 和待加入的 Goroutine 放入全局运行队列。

调度器行为

  1. 公平性: 如果全局运行队列中有待执行的 Goroutine,调度器会以一定概率从全局队列中选择 Goroutine 执行。
  2. 本地队列: 调度器优先从 P 的本地运行队列中选择 Goroutine 执行。
  3. 阻塞查找: 如果本地队列和全局队列都为空,调度器会通过 runtime.findrunnable 函数阻塞地查找可运行的 Goroutine:
    • 从本地队列、全局队列、网络轮询器中查找。
    • 尝试从其他 P 的本地队列中窃取 Goroutine。

3. 内存管理

内存管理是程序设计中的关键问题。手动管理内存容易出错,而自动管理内存(垃圾回收)可能会影响性能。Go 语言采用了自动垃圾回收机制,并对内存管理进行了优化。

堆内存管理

graph LR
    A{Mutator} --> B{Allocator}
    B --> C{Heap}
    C --> D{Object Header}
    C --> E{Collector}
    B --> E

    style C fill:#ccf,stroke:#333,stroke-width:2px
  • Mutator: 用户程序,通过 Allocator 申请内存。
  • Allocator: 内存分配器,负责从堆中分配内存块。
  • Heap: 堆,一块连续的内存区域,用于动态分配。
  • Object Header: 对象头,存储对象的元数据(大小、是否被使用、下一个对象的地址等)。
  • Collector: 垃圾回收器,负责回收不再使用的内存。

TCMalloc

Go 语言的内存分配器借鉴了 TCMalloc(Thread-Caching Malloc)的思想。

graph LR
    subgraph        A["Virtual Memory"]
    end

    subgraph "PageHeap"
        B["PageHeap"] --> C["Span list 1 (1 page)"]
        B --> D["Span list 2 (2 pages)"]
        B --> E["... (3-127 pages)"]
        B --> F["Span list 128 (128 pages = 1MB)"]
        B --> G["Large span set (Large and medium object)"]
    end
  
    subgraph "CentralCache"
      H["CentralCache"] --> I["Size class 0"]
      H --> J["Size class 1"]
      H --> K["..."]
      H --> L["Size class n"]
      I --> C
      J --> D
      L --> F

    end

    subgraph "ThreadCache"
        M["ThreadCache 1"] --> N["Size class 0"]
        M --> O["Size class 1"]
        M --> P["..."]
        M --> Q["Size class n"]
        N --> FreeObject["(Free Object)"]
    end
  
    subgraph "ThreadCache"
      R["ThreadCache 2"]
    end

    subgraph "ThreadCache"
        S["ThreadCache n"]
    end
  
    M --> H
    R --> H
    S --> H

    T["Application"] --> M
    T --> R
    T --> S
  • page: 内存页,8KB 大小的内存块。Go 语言与操作系统之间的内存申请和释放都以 page 为单位。
  • span: 内存块,由一个或多个连续的 page 组成。
  • sizeclass: 空间规格,每个 span 都有一个 sizeclass,表示 span 中的 page 应该如何使用。
  • object: 对象,用于存储变量数据的内存空间。一个 span 在初始化时会被分割成多个等大小的 object。

对象大小

  • 小对象: 0 ~ 256KB
  • 中对象: 256KB ~ 1MB
  • 大对象: > 1MB

分配流程

  • 小对象: ThreadCache -> CentralCache -> HeapPage。通常情况下,ThreadCache 足够满足小对象的分配需求,无需访问 CentralCache 和 HeapPage。
  • 中对象: 直接从 PageHeap 中选择合适大小的 span。
  • 大对象: 从 large span set 中选择合适数量的 page 组成 span。

Go 语言内存分配

graph LR
    subgraph 虚拟内存
        A[Virtual Memory]
    end
  
    subgraph arenas
        B[arenas] --> C[heapArena]
        B --> D[heapArena]
        B --> E[...]
        C --> F[span class 133]
        C --> G[span class 134]
        D --> H[span class 134]
    end

    subgraph mheap
        I[mheap] --> J[free]
        I --> K[scav]
        I --> L[mcentral]
        I --> B
        L --> M[span class 0]
        L --> N[span class 1]
        L --> O[...]
        M --> P[Span]
        P --> Q[Pages]
    end
  
    subgraph mcache
        R[mcache of P1] --> S[span class 0]
        R --> T[span class 1]
        R --> U[...]
        S --> V[Free Object]
    end

    subgraph mcache
        W[mcache of P2]
    end

    subgraph mcache
        X[mcache of P3]
    end
  
    R --> L
    W --> L
    X --> L
  
    Y[Application] --> R
    Y --> W
    Y --> X
    Y --> LargeObject[(Large and medium object)]
    Y --> TinyObject[(Tiny object)]
    LargeObject --> B
  • mcache: 小对象的内存分配直接从 mcache 获取。mcache 包含多个 size class(1 到 66),每个 class 有两个 span。span 大小为 8KB,根据 size class 的大小进行切分。
  • mcentral: 当 mcache 中的 span 没有剩余空间时,会向 mcentral 申请一个 span。mcentral 如果没有符合条件的 span,则会向 mheap 申请。
  • mheap: 当 mheap 没有足够的内存时,会向操作系统申请内存。mheap 将 span 组织成树结构,并分配到 heapArena 进行管理。heapArena 包含地址映射和 span 是否包含指针等位图信息。

内存回收

常见的垃圾回收算法包括:

  • 引用计数: 为每个对象维护一个引用计数,当引用计数为 0 时回收对象。
    • 优点:对象可以很快被回收。
    • 缺点:无法处理循环引用,维护引用计数有开销。
  • 标记-清除: 从根对象开始遍历所有可达对象,标记为“被引用”,未被标记的对象被回收。
    • 优点:可以处理循环引用。
    • 缺点:需要 STW(Stop The World),暂停程序运行。
  • 分代收集: 根据对象的生命周期将内存划分为不同的代,对不同代采用不同的回收频率。
    • 优点:提高回收效率。
    • 缺点:实现复杂。

Go 语言采用标记-清除算法,并进行了优化。

mspan

  • allocBits: 记录了每块内存的分配情况。
  • gcmarkBits: 记录了每块内存的引用情况。在标记阶段,有对象引用的内存块被标记为 1,没有的标记为 0。

标记结束后,allocBits 指向 gcmarkBits,被标记的内存块保留,未标记的被回收。

GC 工作流程

Go 语言的 GC 大部分过程与用户代码并发执行。

graph LR
    A[关闭 GC] --> B{栈扫描}
    B -- 开启写屏障 --> C["STW (开启写屏障等准备工作)"]
    C --> D["标记 (从全局空间和 goroutine 栈扫描变量)"]
    D -- "三色标记,直到没有灰色对象" --> E["标记结束 (STW, 重新扫描 root 区域新变量)"]
    E --> F["清除 (关闭 STW 和写屏障,清除白色对象)"]
    F --> A
  1. Mark:
    • Mark Prepare: 初始化 GC 任务,开启写屏障(write barrier)和辅助 GC(mutator assist),统计 root 对象的数量。这个阶段需要 STW。
    • GC Drains: 扫描所有 root 对象(全局指针和 goroutine 栈上的指针),将它们加入标记队列(灰色队列),并循环处理灰色队列中的对象,直到队列为空。这个阶段并发执行。
    • Mark Termination: 完成标记工作,重新扫描全局指针和栈。由于 Mark 阶段与用户程序并发执行,可能会有新的对象分配和指针赋值,写屏障会记录这些变化,re-scan 阶段会再次检查。这个阶段需要 STW。
  2. Sweep: 根据标记结果回收所有白色对象。这个阶段并发执行。
  3. Sweep Termination: 清扫未被清扫的 span。只有上一轮 GC 的清扫工作完成后,才能开始新一轮 GC。

三色标记

graph LR
    A["a"] --> B["b"]
    A["a"] --> C["c"]
    B["b"] --> D["d"]

    subgraph "初始状态"
        A1["a (白色)"] --> B1["b (白色)"]
        A1["a (白色)"] --> C1["c (白色)"]
        B1["b (白色)"] --> D1["d (白色)"]
    end

    subgraph "第一次遍历"
        A2["a (灰色)"] --> B2["b (白色)"]
        A2["a (灰色)"] --> C2["c (白色)"]
        B2["b (白色)"] --> D2["d (白色)"]
    end

    subgraph "第二次遍历"
        A3["a (黑色)"] --> B3["b (灰色)"]
        A3["a (黑色)"] --> C3["c (灰色)"]
        B3["b (灰色)"] --> D3["d (白色)"]
    end

    subgraph "第三次遍历"
        A4["a (黑色)"] --> B4["b (黑色)"]
        A4["a (黑色)"] --> C4["c (灰色)"]
        B4["b (黑色)"] --> D4["d (白色)"]
    end
  
    subgraph "第四次遍历"
        A5["a (黑色)"] --> B5["b (黑色)"]
        A5["a (黑色)"] --> C5["c (黑色)"]
        B5["b (黑色)"] --> D5["d (灰色)"]
    end
    
    subgraph "第五次遍历"
        A6["a (黑色)"] --> B6["b (黑色)"]
        A6["a (黑色)"] --> C6["c (黑色)"]
        B6["b (黑色)"] --> D6["d (黑色)"]
    end
  1. GC 开始时,所有对象都被认为是白色(垃圾)。
  2. 从 root 对象开始遍历,可达对象被标记为灰色。
  3. 遍历所有灰色对象,将它们引用的对象标记为灰色,自身标记为黑色。
  4. 重复第 3 步,直到没有灰色对象,只剩下黑色和白色对象。白色对象即为垃圾。
  5. 对于黑色对象,如果在标记期间发生了写操作,写屏障会在赋值前将新对象标记为灰色。
  6. 标记过程中新分配的对象会被直接标记为黑色。

垃圾回收触发机制

  • 内存分配量达到阈值:
    • 每次内存分配时都会检查当前内存分配量是否达到阈值。
    • 阈值 = 上次 GC 内存分配量 * 内存增长率。
    • 内存增长率由环境变量 GOGC 控制,默认为 100(即内存扩大一倍时触发 GC)。
  • 定期触发: 默认情况下,每 2 分钟触发一次 GC。
  • 手动触发: 使用 runtime.GC() 函数手动触发 GC。

4. 包引用与依赖管理

Go 语言的依赖管理经历了从 GOPATH 到 vendor,再到 Go Modules 的演变过程。

GOPATH

  • 通过环境变量 GOPATH 设置 Go 语言类库的目录。
  • 问题:
    • 不同项目可能依赖同一库的不同版本。
    • 代码被克隆后需要设置 GOPATH 才能编译。

vendor

  • Go 1.6 版本引入了 vendor 目录。
  • 每个项目创建一个 vendor 目录,并将依赖复制到该目录。
  • Go 语言项目会自动将 vendor 目录作为依赖路径。
  • 优点:
    • 每个项目的 vendor 目录独立,可以灵活选择版本。
    • vendor 目录与源代码一起提交,其他人克隆后可以直接编译。
    • 编译期间无需下载依赖。

vendor 管理工具

  • Godeps, Glide
  • Go 官方的依赖管理工具 Gopkg
  • Go Modules (gomod)

Go Modules (gomod)

  • 通过 go mod 命令开启:export GO111MODULE=on/off/auto
  • 更灵活易用,基本统一了 Go 语言的依赖管理。

Go Modules 的目的

  • 版本管理
  • 防止篡改

Go Modules 使用

  1. 创建项目。
  2. 初始化 Go 模块:go mod init
  3. 下载依赖包:go mod download(依赖包下载到 $GOPATH/pkg,如果没有设置 GOPATH,则下载到项目根目录下的 pkg 目录)。
  4. 在代码中使用依赖包,例如 github.com/emicklei/go-restful
  5. 添加缺少的依赖并清理:go mod tidy
  6. 将依赖复制到 vendor 目录:go mod vendor

go.mod 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module k8s.io/apiserver

go 1.13

require (
github.com/evanphx/json-patch v4.9.0+incompatible
github.com/go-openapi/jsonreference v0.19.3 // indirect
github.com/go-openapi/spec v0.19.3
github.com/gogo/protobuf v1.3.2
github.com/google/go-cmp v0.3.0
github.com/google/gofuzz v1.1.0
k8s.io/apimachinery v0.0.0-20210518100737-44f1264f7b6b
)

replace (
golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
golang.org/x/text => golang.org/x/text v0.3.2
k8s.io/api => k8s.io/api v0.0.0-20210518101910-53468e23a787
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20210518100737-44f1264f7b6b
k8s.io/client-go => k8s.io/client-go v0.0.0-20210518104342-fa3acefe68f3
k8s.io/component-base => k8s.io/component-base v0.0.0-20210518111421-67c12a31a26a
)
  • module: 定义模块的导入路径。
  • go: 指定 Go 语言版本。
  • require: 指定依赖包及其版本。
  • replace: 替换依赖包。

GOPROXYGOPRIVATE

  • GOPROXY: 设置 Go 依赖的代理。
    • export GOPROXY=https://goproxy.cn
    • 设置 GOPROXY 后,默认所有依赖都会通过代理拉取,并进行 checksum 校验。
  • GOPRIVATE: 声明私有代码仓库,避免通过 GOPROXY 拉取。
    • GOPRIVATE=*.corp.example.com
    • GONOPROXY=myrepo.corp.example.com
    • GOPROXY=proxy.example.com

5. Makefile

Go 语言项目通常使用 Makefile 来组织编译过程。

1
2
3
4
5
6
7
8
9
10
ROOT=github.com/cncamp/golang

.PHONY: root
root:

.PHONY: release
release:
@echo "building httpserver binary"
@mkdir -p bin/amd64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/amd64 .
  • .PHONY: 声明伪目标。
  • release: 定义构建目标,设置环境变量 CGO_ENABLEDGOOSGOARCH,然后使用 go build 编译程序。

6. 编写 HTTP Server

理解 net/http

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"log"
"net/http"
)

func healthz(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok\n")
}

func main() {
http.HandleFunc("/healthz", healthz) // 注册处理函数
err := http.ListenAndServe(":80", nil) // 监听端口,使用默认的 DefaultServeMux
if err != nil {
log.Fatal(err)
}
}
  • http.HandleFunc: 注册处理函数,将 URL 路径与处理函数关联起来。
  • http.ListenAndServe: 启动 HTTP 服务器,监听指定端口。第二个参数为 nil 时,使用默认的 DefaultServeMux
  • healthz: 处理函数,接收 http.ResponseWriterhttp.Request 作为参数。

阻塞 IO 模型

sequenceDiagram
    participant 应用进程
    participant 系统内核
    应用进程->>系统内核: recvfrom (系统调用)
    Note over 系统内核: 数据报文尚未就绪
    Note over 应用进程,系统内核: 进程阻塞于 recvfrom 调用
    Note over 系统内核: 数据报文就绪
    系统内核->>应用进程: 拷贝数据
    Note over 应用进程,系统内核: 数据复制到进程缓冲区期间,进程阻塞
    应用进程->>应用进程: 处理数据报文
    系统内核-->>应用进程: 返回 OK (拷贝完成)

非阻塞 IO 模型

sequenceDiagram
    participant 应用进程
    participant 系统内核
    应用进程->>系统内核: recvfrom (系统调用)
    Note over 系统内核: 数据报文尚未就绪
    系统内核-->>应用进程: 返回错误
    Note over 应用进程: 进程重复调用 recvfrom
    应用进程->>系统内核: recvfrom (系统调用)
    Note over 系统内核: 数据报文尚未就绪
    系统内核-->>应用进程: 返回错误
    Note over 应用进程: ...
    应用进程->>系统内核: recvfrom (系统调用)
    Note over 系统内核: 数据报文就绪
    系统内核->>应用进程: 拷贝数据
     Note over 应用进程,系统内核: 数据复制到进程缓冲区期间,进程阻塞
    应用进程->>应用进程: 处理数据报文
    系统内核-->>应用进程: 返回 OK (拷贝完成)

IO 多路复用

sequenceDiagram
    participant 应用进程
    participant 系统内核
    应用进程->>系统内核: select/poll (系统调用)
    Note over 系统内核: 数据报文尚未就绪
    Note over 应用进程,系统内核: 进程阻塞于 select/poll 调用,等待有可读的 socket
    系统内核-->>应用进程: 返回可读
    应用进程->>系统内核: recvfrom (系统调用)
    Note over 系统内核: 数据报文就绪
    系统内核->>应用进程: 拷贝数据
    Note over 应用进程,系统内核: 数据复制到进程缓冲区期间,进程阻塞
    应用进程->>应用进程: 处理数据报文
    系统内核-->>应用进程: 返回 OK (拷贝完成)

异步 IO

sequenceDiagram
    participant 应用进程
    participant 系统内核
    应用进程->>系统内核: 异步 IO 读 (系统调用)
    Note over 系统内核: 数据报文尚未就绪
  

Go 语言进阶
https://mfzzf.github.io/2025/03/16/golang进阶/
作者
Mzzf
发布于
2025年3月16日
许可协议