高级并发模式与微服务入门 — 从单兵作战到集团军
掌握了goroutine和channel,你已经学会了单兵格斗。但在真实的生产环境中,你需要的是军团级的协同作战能力——Worker Pool批量处理任务、Pipeline流水线加工数据、Rate Limiter控制火力密度。当单体应用无法满足需求时,微服务架构让你把一支大军拆成多个精锐小队,各自独立又协同配合。
📋 开篇自测:你已经知道多少?
- Worker Pool模式和直接启动N个goroutine有什么区别?为什么需要限制goroutine数量?
- 你能说出Fan-out/Fan-in模式的典型应用场景吗?
context.WithTimeout和time.After在超时控制上有什么区别?
一、Worker Pool模式:固定编制的”工作班组”
1.1 为什么不能无限开goroutine
在第五章中,我们知道goroutine很轻量,但”轻量”不等于”免费”。在实际生产环境中,如果为每个请求都启动一个goroutine,当瞬间涌入十万个请求时,即使每个goroutine只占几KB内存,总量也会达到数百MB甚至数GB。更严重的是,大量goroutine同时访问下游资源(数据库、第三方API),可能直接把下游服务打垮。
Worker Pool的核心思路是:预先创建固定数量的goroutine(工人),让它们从共享的任务队列中领取任务并执行。 就像工厂的生产线——车间里固定30个工位,订单再多也不会临时搭100个工位,而是排队等空闲工位来处理。
1.2 实现一个通用Worker Pool
package main
import (
"fmt"
"sync"
"time"
)
// Task 代表一个待处理的任务
type Task struct {
ID int
Payload string
}
// Result 代表任务的处理结果
type Result struct {
TaskID int
Output string
}
// StartWorkerPool 启动固定数量的worker来处理任务
func StartWorkerPool(numWorkers int, tasks <-chan Task, results chan<- Result) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for task := range tasks {
// 模拟耗时处理
time.Sleep(200 * time.Millisecond)
results <- Result{
TaskID: task.ID,
Output: fmt.Sprintf("worker-%d processed: %s", workerID, task.Payload),
}
}
}(i)
}
// 所有worker完成后关闭results通道
go func() {
wg.Wait()
close(results)
}()
}
func main() {
const totalTasks = 20
const numWorkers = 5
tasks := make(chan Task, totalTasks)
results := make(chan Result, totalTasks)
// 启动Worker Pool
StartWorkerPool(numWorkers, tasks, results)
// 投递任务
for i := 1; i <= totalTasks; i++ {
tasks <- Task{ID: i, Payload: fmt.Sprintf("data-%d", i)}
}
close(tasks) // 所有任务投递完毕
// 收集结果
for r := range results {
fmt.Printf("任务%d完成: %s\n", r.TaskID, r.Output)
}
}
这里有几个设计要点:
taskschannel是任务队列,所有worker从中竞争领取任务close(tasks)通知所有worker”没有更多任务了”,worker的range循环会自动退出sync.WaitGroup确保所有worker退出后才关闭resultschannel- 不管投递多少任务,同时运行的goroutine始终只有
numWorkers个
🤔 想一想 如果某个任务处理特别慢(比如需要10秒),会不会阻塞整个Worker Pool?如何设计任务超时机制?
二、Fan-out/Fan-in模式:并行分发与结果汇聚
2.1 模式概述
Fan-out/Fan-in是并发编程中经典的数据处理模式:
- Fan-out(扇出):将一个数据源分发给多个goroutine并行处理
- Fan-in(扇入):将多个goroutine的处理结果汇聚到一个channel中
┌──► worker-1 ──┐
│ │
数据源 ──► Fan-out──► worker-2 ──► Fan-in ──► 汇总结果
│ │
└──► worker-3 ──┘
2.2 实战:并行抓取多个网页
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type FetchResult struct {
URL string
Latency time.Duration
Status string
}
// fetch 模拟抓取单个URL
func fetch(url string) FetchResult {
delay := time.Duration(100+rand.Intn(400)) * time.Millisecond
time.Sleep(delay) // 模拟网络延迟
return FetchResult{
URL: url,
Latency: delay,
Status: "200 OK",
}
}
// fanOut 将URL列表分发给多个worker
func fanOut(urls []string, numWorkers int) []<-chan FetchResult {
channels := make([]<-chan FetchResult, numWorkers)
urlCh := make(chan string, len(urls))
// 将所有URL放入任务队列
for _, u := range urls {
urlCh <- u
}
close(urlCh)
// 启动多个worker,每个worker产出一个result channel
for i := 0; i < numWorkers; i++ {
ch := make(chan FetchResult)
channels[i] = ch
go func() {
defer close(ch)
for url := range urlCh {
ch <- fetch(url)
}
}()
}
return channels
}
// fanIn 将多个channel的结果合并为一个
func fanIn(channels []<-chan FetchResult) <-chan FetchResult {
merged := make(chan FetchResult)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan FetchResult) {
defer wg.Done()
for result := range c {
merged <- result
}
}(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
func main() {
urls := []string{
"https://api.service-a.com/data",
"https://api.service-b.com/data",
"https://api.service-c.com/data",
"https://api.service-d.com/data",
"https://api.service-e.com/data",
"https://api.service-f.com/data",
}
start := time.Now()
// Fan-out: 分发给3个worker
resultChannels := fanOut(urls, 3)
// Fan-in: 汇聚结果
for result := range fanIn(resultChannels) {
fmt.Printf("[%s] %s (耗时 %v)\n", result.Status, result.URL, result.Latency)
}
fmt.Printf("\n全部完成,总耗时: %v\n", time.Since(start))
}
如果串行抓取6个URL,每个平均300ms,总耗时约1.8秒。用3个worker并行处理,总耗时可降至约600ms——加速比接近3倍。
三、Pipeline模式:多阶段流水线
3.1 模式概述
Pipeline将一个复杂的处理流程拆分为多个阶段,每个阶段由独立的goroutine负责,阶段之间通过channel连接。数据像工厂流水线上的半成品一样,依次经过每个工位的加工。
原始数据 ──► 阶段1(过滤) ──► 阶段2(转换) ──► 阶段3(聚合) ──► 最终结果
chan chan chan
3.2 实战:日志处理流水线
package main
import (
"fmt"
"strings"
"time"
)
type LogEntry struct {
Timestamp time.Time
Level string
Message string
}
// stage1: 生成原始日志
func generate(logs []LogEntry) <-chan LogEntry {
out := make(chan LogEntry)
go func() {
defer close(out)
for _, log := range logs {
out <- log
}
}()
return out
}
// stage2: 只保留ERROR级别的日志
func filterErrors(in <-chan LogEntry) <-chan LogEntry {
out := make(chan LogEntry)
go func() {
defer close(out)
for entry := range in {
if entry.Level == "ERROR" {
out <- entry
}
}
}()
return out
}
// stage3: 将消息转为大写并添加标记
func enrich(in <-chan LogEntry) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for entry := range in {
enriched := fmt.Sprintf("[ALERT] %s | %s",
entry.Timestamp.Format("15:04:05"),
strings.ToUpper(entry.Message))
out <- enriched
}
}()
return out
}
func main() {
now := time.Now()
logs := []LogEntry{
{Timestamp: now, Level: "INFO", Message: "server started"},
{Timestamp: now, Level: "ERROR", Message: "database connection timeout"},
{Timestamp: now, Level: "DEBUG", Message: "processing request"},
{Timestamp: now, Level: "ERROR", Message: "disk space critically low"},
{Timestamp: now, Level: "INFO", Message: "request completed"},
{Timestamp: now, Level: "ERROR", Message: "authentication service down"},
}
// 构建流水线:生成 → 过滤 → 增强
stage1 := generate(logs)
stage2 := filterErrors(stage1)
stage3 := enrich(stage2)
// 消费最终结果
for alert := range stage3 {
fmt.Println(alert)
}
}
输出:
[ALERT] 14:30:05 | DATABASE CONNECTION TIMEOUT
[ALERT] 14:30:05 | DISK SPACE CRITICALLY LOW
[ALERT] 14:30:05 | AUTHENTICATION SERVICE DOWN
Pipeline的优势在于每个阶段可以独立开发、测试和扩展。如果某个阶段成为瓶颈,可以对该阶段单独做Fan-out并行处理,而不影响其他阶段。
四、Rate Limiter:控制请求频率
4.1 为什么需要限流
在调用第三方API或者保护自身服务时,限流是必备的防护措施。没有限流,突发流量会像洪水一样冲垮下游服务。
4.2 基于令牌桶的限流器
令牌桶算法的工作原理:一个桶以固定速率生成令牌,每次请求需要消耗一个令牌。桶满了新令牌会被丢弃,桶空了请求就需要等待。Go标准库的 time.Ticker 天然适合实现令牌桶。
package main
import (
"fmt"
"time"
)
type TokenBucketLimiter struct {
tokens chan struct{}
stop chan struct{}
}
// NewTokenBucketLimiter 创建一个令牌桶限流器
// rate: 每秒生成的令牌数, capacity: 桶的最大容量
func NewTokenBucketLimiter(rate int, capacity int) *TokenBucketLimiter {
limiter := &TokenBucketLimiter{
tokens: make(chan struct{}, capacity),
stop: make(chan struct{}),
}
// 预填充令牌
for i := 0; i < capacity; i++ {
limiter.tokens <- struct{}{}
}
// 以固定速率补充令牌
go func() {
interval := time.Second / time.Duration(rate)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
select {
case limiter.tokens <- struct{}{}:
// 成功补充一个令牌
default:
// 桶满了,丢弃令牌
}
case <-limiter.stop:
return
}
}
}()
return limiter
}
// Wait 阻塞等待直到获取一个令牌
func (l *TokenBucketLimiter) Wait() {
<-l.tokens
}
// Close 停止令牌生成
func (l *TokenBucketLimiter) Close() {
close(l.stop)
}
func main() {
// 每秒最多5个请求,允许突发3个
limiter := NewTokenBucketLimiter(5, 3)
defer limiter.Close()
start := time.Now()
for i := 1; i <= 10; i++ {
limiter.Wait()
fmt.Printf("请求 %2d 发出,时刻: %v\n", i, time.Since(start).Round(time.Millisecond))
}
}
输出大致如下(前3个立即发出,后续按每200ms一个的节奏):
请求 1 发出,时刻: 0s
请求 2 发出,时刻: 0s
请求 3 发出,时刻: 0s
请求 4 发出,时刻: 200ms
请求 5 发出,时刻: 400ms
请求 6 发出,时刻: 600ms
...
提示:在生产环境中,推荐使用
golang.org/x/time/rate包,它提供了经过充分测试的令牌桶实现,支持Allow()(非阻塞检查)、Wait(ctx)(可取消的等待)、Reserve()(预约令牌)等丰富的API。
五、Context深入:并发控制的指挥棒
5.1 Context的本质
context.Context 是Go并发编程的指挥棒——它在goroutine树中自上而下传递取消信号、超时截止和请求级别的数据。
Background (根)
|
WithTimeout(3s)
/ \
WithCancel WithValue("userID", 42)
| | |
worker1 worker2 handler
当父context被取消时,所有子context也会被取消——信号沿着树结构自动传播,无需手动通知每个goroutine。
5.2 WithTimeout:限时任务
package main
import (
"context"
"fmt"
"time"
)
// queryDatabase 模拟一个可能很慢的数据库查询
func queryDatabase(ctx context.Context, query string) (string, error) {
// 模拟查询耗时
resultCh := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second) // 模拟慢查询
resultCh <- "查询结果: 42条记录"
}()
select {
case result := <-resultCh:
return result, nil
case <-ctx.Done():
return "", fmt.Errorf("查询被取消: %w", ctx.Err())
}
}
func main() {
// 场景1:超时时间充足
ctx1, cancel1 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel1()
result, err := queryDatabase(ctx1, "SELECT * FROM orders")
if err != nil {
fmt.Println("场景1失败:", err)
} else {
fmt.Println("场景1成功:", result)
}
// 场景2:超时时间不足
ctx2, cancel2 := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel2()
result, err = queryDatabase(ctx2, "SELECT * FROM orders")
if err != nil {
fmt.Println("场景2失败:", err)
} else {
fmt.Println("场景2成功:", result)
}
}
输出:
场景1成功: 查询结果: 42条记录
场景2失败: 查询被取消: context deadline exceeded
5.3 WithCancel:手动取消
func longRunningSearch(ctx context.Context, keyword string, resultCh chan<- string) {
databases := []string{"用户库", "订单库", "日志库", "商品库"}
for _, db := range databases {
select {
case <-ctx.Done():
fmt.Printf(" 搜索 %s 被取消\n", db)
return
default:
time.Sleep(300 * time.Millisecond) // 模拟搜索耗时
if db == "订单库" {
resultCh <- fmt.Sprintf("在%s中找到: %s", db, keyword)
return // 找到结果,直接返回
}
fmt.Printf(" %s 中未找到\n", db)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
resultCh := make(chan string, 1)
// 启动3个并行搜索,谁先找到就取消其他的
for i := 0; i < 3; i++ {
go longRunningSearch(ctx, "订单#1001", resultCh)
}
// 等待第一个结果
result := <-resultCh
cancel() // 通知其他搜索停止
time.Sleep(100 * time.Millisecond) // 等待取消信号传播
fmt.Println("最终结果:", result)
}
5.4 Context使用的最佳实践
| 规则 | 说明 |
|---|---|
| context作为函数第一个参数传入 | func DoWork(ctx context.Context, ...) 是Go社区的通用约定 |
| 不要把context存到结构体中 | context的生命周期应该和请求绑定,而不是和对象绑定 |
| 永远调用cancel函数 | 即使context会自然超时,也要 defer cancel() 来释放资源 |
| 不要传nil context | 如果不确定用哪个,用 context.TODO() 作为占位符 |
| 用WithValue传递请求级数据要克制 | 只用于跨API边界的请求级元数据(如traceID),不要当参数传递用 |
⚠️ 常见误区
- 误区一:忘记调用
cancel()。即使WithTimeout会自动到期,不调用cancel会导致context关联的资源(timer等)无法及时释放。- 误区二:用
context.WithValue传递业务参数。这会让函数签名失去信息量,调用者不知道函数需要什么数据。正确做法是把业务参数放在函数签名里。- 误区三:在goroutine内部创建了context却不传递给子goroutine。取消信号无法传播,子goroutine会泄漏。
六、gRPC入门:高性能的服务间通信
6.1 为什么需要gRPC
当你的系统从单体拆分成多个微服务后,服务之间需要一种高效的通信方式。HTTP+JSON是最常见的选择,但它有性能上的短板——JSON文本格式解析慢、体积大。gRPC使用Protocol Buffers(protobuf)作为序列化格式,二进制编码比JSON更紧凑、解析更快。
| 对比维度 | HTTP+JSON | gRPC+Protobuf |
|---|---|---|
| 数据格式 | 文本(JSON) | 二进制(Protobuf) |
| 传输协议 | HTTP/1.1 | HTTP/2 |
| 序列化速度 | 较慢 | 快3-10倍 |
| 数据体积 | 较大 | 小30%-50% |
| 接口定义 | 文档约定(如Swagger) | .proto文件强类型约束 |
| 流式传输 | 需要WebSocket | 原生支持双向流 |
6.2 用Protobuf定义服务
首先安装protoc编译器和Go插件:
# 安装protoc(macOS)
brew install protobuf
# 安装Go的protoc插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
定义一个商品查询服务的 .proto 文件:
// file: proto/catalog.proto
syntax = "proto3";
package catalog;
option go_package = "myapp/proto/catalog";
// 商品查询请求
message GetProductRequest {
int64 product_id = 1;
}
// 商品信息响应
message GetProductResponse {
int64 id = 1;
string name = 2;
double price = 3;
int32 stock = 4;
}
// 商品目录服务
service CatalogService {
rpc GetProduct(GetProductRequest) returns (GetProductResponse);
}
生成Go代码:
protoc --go_out=. --go-grpc_out=. proto/catalog.proto
6.3 实现gRPC服务端
package main
import (
"context"
"fmt"
"log"
"net"
pb "myapp/proto/catalog"
"google.golang.org/grpc"
)
// catalogServer 实现CatalogService接口
type catalogServer struct {
pb.UnimplementedCatalogServiceServer
products map[int64]*pb.GetProductResponse
}
func newCatalogServer() *catalogServer {
return &catalogServer{
products: map[int64]*pb.GetProductResponse{
1: {Id: 1, Name: "机械键盘", Price: 599.0, Stock: 120},
2: {Id: 2, Name: "无线鼠标", Price: 199.0, Stock: 300},
3: {Id: 3, Name: "显示器支架", Price: 89.0, Stock: 85},
},
}
}
func (s *catalogServer) GetProduct(
ctx context.Context, req *pb.GetProductRequest,
) (*pb.GetProductResponse, error) {
product, ok := s.products[req.ProductId]
if !ok {
return nil, fmt.Errorf("商品不存在: ID=%d", req.ProductId)
}
return product, nil
}
func main() {
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听端口失败: %v", err)
}
server := grpc.NewServer()
pb.RegisterCatalogServiceServer(server, newCatalogServer())
log.Println("gRPC服务启动,监听端口 :50051")
if err := server.Serve(listener); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
6.4 实现gRPC客户端
package main
import (
"context"
"fmt"
"log"
"time"
pb "myapp/proto/catalog"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 建立连接
conn, err := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
client := pb.NewCatalogServiceClient(conn)
// 设置超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 调用远程方法
resp, err := client.GetProduct(ctx, &pb.GetProductRequest{ProductId: 1})
if err != nil {
log.Fatalf("查询失败: %v", err)
}
fmt.Printf("商品信息: ID=%d, 名称=%s, 价格=%.2f, 库存=%d\n",
resp.Id, resp.Name, resp.Price, resp.Stock)
}
注意客户端调用 GetProduct 就像调用本地函数一样——gRPC自动处理了网络传输、序列化和反序列化的细节。这种体验正是RPC(Remote Procedure Call,远程过程调用)的核心理念:让远程调用看起来和本地调用一样简单。
🤔 想一想 gRPC默认使用HTTP/2协议,支持多路复用、头部压缩和双向流。这些特性对微服务通信有什么实际好处?
七、服务注册与发现
7.1 为什么需要服务发现
当你只有2个服务时,把对方的地址写死在配置文件里完全可行。但当服务增长到20个、200个时,每个服务可能有多个实例、动态扩缩容、实例随时可能挂掉重启——这时候手动维护地址列表就是噩梦。
服务发现解决的核心问题是:让服务消费方自动找到服务提供方的地址,无需硬编码。
传统方式(硬编码):
服务A ──── "192.168.1.10:8080" ────► 服务B
服务发现方式:
服务B启动 ──► 注册到注册中心: "我是服务B, 地址是 192.168.1.10:8080"
服务A请求 ──► 询问注册中心: "服务B在哪?" ──► 得到地址列表 ──► 调用服务B
7.2 主流注册中心
| 注册中心 | 语言 | 一致性模型 | 典型用户 |
|---|---|---|---|
| etcd | Go | 强一致性(Raft) | Kubernetes |
| Consul | Go | 强一致性(Raft) | HashiCorp生态 |
| ZooKeeper | Java | 强一致性(ZAB) | Hadoop/Kafka |
| Nacos | Java | AP/CP可切换 | 阿里巴巴/Spring Cloud |
在Go生态中,etcd和Consul是最常见的选择——它们本身就是用Go编写的,与Go服务集成非常自然。
7.3 服务发现的基本流程
// 伪代码:展示服务注册与发现的基本流程
// --- 服务提供方 ---
func registerService(registry RegistryClient, info ServiceInfo) {
// 1. 启动时注册自己
registry.Register(info)
// 2. 定期续租(心跳),证明自己还活着
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
registry.Renew(info.ID)
}
}()
// 3. 关闭时注销
// defer registry.Deregister(info.ID)
}
// --- 服务消费方 ---
func discoverService(registry RegistryClient, serviceName string) []string {
// 从注册中心查询可用实例
instances := registry.Discover(serviceName)
// 返回地址列表,调用方可以做负载均衡
var addrs []string
for _, inst := range instances {
addrs = append(addrs, inst.Address)
}
return addrs
}
八、微服务拆分原则与Go生态工具链
8.1 什么时候该拆微服务
微服务不是银弹。在拆分之前,先问自己三个问题:
- 单体应用是否已经成为瓶颈? 如果团队只有3个人,单体应用完全够用。过早拆分只会增加运维复杂度。
- 能否按业务领域清晰划分? 好的微服务边界应该对应独立的业务能力——用户服务、订单服务、支付服务各自内聚。如果两个服务之间需要频繁互调几十个接口,说明拆分边界不合理。
- 团队是否有微服务运维能力? 微服务意味着分布式系统——你需要处理服务发现、负载均衡、链路追踪、分布式事务等一系列问题。
8.2 拆分的基本原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个服务只负责一个业务领域 |
| 高内聚低耦合 | 服务内部紧密相关,服务之间松散关联 |
| 独立部署 | 修改一个服务不需要重新部署其他服务 |
| 数据自治 | 每个服务拥有自己的数据库,不直接访问其他服务的数据 |
| 面向失败设计 | 任何服务都可能宕机,调用方必须有降级和重试策略 |
8.3 Go微服务生态工具链
Go语言在微服务领域有丰富的工具链,以下是最常用的框架和组件:
go-kit:面向大型组织的微服务工具集。它不是一个框架,而是一组可组合的库——你可以按需选择日志、限流、服务发现、链路追踪等组件。
// go-kit 的分层架构概念
//
// Transport层(HTTP/gRPC)
// |
// Endpoint层(请求/响应的抽象)
// |
// Service层(纯业务逻辑)
go-kit的设计哲学是”关注点分离”——业务逻辑不应该关心传输协议是HTTP还是gRPC,也不应该关心日志怎么记、限流怎么做。这些横切关注点通过中间件(middleware)注入。
go-micro:更偏向”开箱即用”的微服务框架,内置服务发现、负载均衡、消息编码、RPC等功能。适合快速搭建微服务原型。注意:go-micro项目近年维护活跃度有所下降,新项目建议优先考虑go-zero或Kratos。
go-zero:国内使用广泛的微服务框架(由好未来团队开源),内置API网关、服务发现、熔断限流、链路追踪等,并提供 goctl 代码生成工具,可以从API定义文件一键生成服务骨架代码。
Kratos:B站开源的微服务框架,设计思路受Google内部框架影响,强调工程化和规范化。
8.4 一个典型的Go微服务技术栈
┌─────────────────────────────────────────────┐
│ API 网关 │
│ (Nginx / Kong / Traefik) │
└───────────────┬─────────────────────────────┘
│
┌───────────┼───────────┐
│ │ │
┌───▼──┐ ┌───▼──┐ ┌───▼──┐
│用户服务│ │订单服务│ │商品服务│ ← Go微服务
│(Gin) │ │(gRPC)│ │(gRPC)│
└──┬───┘ └──┬───┘ └──┬───┘
│ │ │
┌──▼──┐ ┌──▼──┐ ┌──▼──┐
│MySQL│ │MySQL│ │MySQL│ ← 每个服务独立数据库
└─────┘ └─────┘ └─────┘
横切关注点(通过中间件/Sidecar实现):
- 服务注册与发现:etcd / Consul
- 链路追踪:Jaeger / Zipkin
- 配置中心:etcd / Apollo
- 消息队列:Kafka / NATS
- 容器编排:Kubernetes
- 监控告警:Prometheus + Grafana
🤔 想一想 你当前的项目是否真的需要微服务?如果项目初期选择了单体架构,如何设计代码结构使得未来拆分微服务时尽可能平滑?提示:即使是单体应用,也可以在代码层面按业务领域划分package,为每个领域定义清晰的接口——这种”模块化单体”为日后拆分打下基础。
📝 掌握度自测
-
Worker Pool模式的核心思想是:
- A) 为每个任务创建一个goroutine
- B) 预先创建固定数量的goroutine,从共享队列中领取任务
- C) 使用互斥锁保护任务列表
- D) 用单个goroutine顺序处理所有任务
-
Fan-out/Fan-in模式中,Fan-in的作用是:
- A) 将任务分发给多个worker
- B) 将多个channel的结果汇聚到一个channel
- C) 限制goroutine的创建数量
- D) 控制请求频率
-
关于
context.WithTimeout,以下说法正确的是:- A) 超时后context会自动释放所有资源,不需要调用cancel
- B) 即使context会自动超时,也应该
defer cancel()来及时释放资源 - C) WithTimeout只能用于HTTP请求
- D) WithTimeout不能和WithCancel嵌套使用
-
gRPC相比HTTP+JSON的主要优势包括:
- A) 使用文本格式,更容易调试
- B) 不需要定义接口
- C) 二进制编码更紧凑、解析更快,且基于HTTP/2支持多路复用
- D) 不需要网络连接
-
关于微服务拆分,以下哪个做法是正确的?
- A) 项目一开始就应该使用微服务架构
- B) 每个服务应该共享同一个数据库以保证数据一致性
- C) 每个服务应该拥有自己的数据库,服务之间通过API通信
- D) 微服务之间应该直接调用对方的数据库
💡 自我评估
- 答对5题:高级并发模式和微服务的核心概念已经掌握,可以开始在项目中实践了!
- 答对3-4题:基本概念理解不错,建议动手实现一个Worker Pool和Pipeline来加深理解。
- 答对0-2题:这些是进阶内容,建议先回顾第五章的goroutine和channel基础,然后结合本章的代码示例逐步实践。
参考答案: 1-B, 2-B, 3-B, 4-C, 5-C
购买课程解锁全部内容
高并发不踩坑:Go 语言从语法到微服务
¥29.90