从单体到微服务:用Go手把手拆解你的第一个分布式产品

Postman使者
2026-01-13 07:46
阅读 595

大家好,我是老张,一个写了五年后端的老兵。今天想和你聊聊微服务——不是讲一堆高大上的理论,而是用最实在的方式,带你亲手把一个“玩具级”单体应用,一步步改造成真正能跑在生产环境的微服务架构。

我当初学微服务时踩过无数坑:以为拆得越细越好,结果调用链乱成蜘蛛网;以为只要用了gRPC就是微服务,结果连日志都对不上时间戳……所以这篇教程,我会把那些血泪教训变成最佳实践,让你少走弯路。

为什么你需要微服务?

先说清楚:微服务不是银弹。如果你的产品只有几个用户、功能简单,单体架构反而更高效。但当你遇到以下情况,就该考虑微服务了:

  • 团队膨胀,10+人同时开发一个代码库,Git冲突天天见
  • 某个非核心功能(比如发短信)拖垮整个系统
  • 需要独立升级某个模块(比如支付系统),但不敢动其他部分

关键认知:微服务的核心目标是 解耦独立演进,不是为了技术炫技。

环境准备:5分钟搭好Go微服务开发环境

我们用Go语言实战,因为它编译快、部署简单、标准库强大,特别适合微服务。以下是最低配置:

工具 版本要求 安装命令
Go ≥1.20 brew install go (Mac) / 官网下载
Docker ≥20.10 官方安装指南
IDE VS Code + Go插件 插件市场搜 "Go"

验证安装:

go version  # 应输出 go1.20.x
docker --version # 应输出 Docker version xx.xx.x

避坑指南:不要用Windows WSL1!WSL2可以,但初学者建议直接用Mac或Linux。Go的模块代理记得设置:go env -w GOPROXY=https://goproxy.cn,direct(国内用户)

核心概念三句话讲透

  1. 单体架构(Monolith)
    所有功能打包成一个进程,像一整块蛋糕。优点:开发部署简单;缺点:牵一发而动全身。

  2. 微服务架构(Microservices)
    把蛋糕切成小块,每块独立烘焙(开发)、独立包装(部署)。通过API互相通信。

  3. 服务治理(Service Governance)
    微服务多了就像管理一支军队:需要知道谁在哪(服务发现)、谁响应慢(监控)、谁挂了(熔断)。

新手误区:微服务 ≠ 多个HTTP服务。真正的微服务必须包含 可观测性(日志/指标/追踪)和 弹性设计(重试/超时/熔断)。

实战:把单体电商拆成微服务

我们模拟一个简化版电商系统,包含:

  • 用户服务(User Service)
  • 订单服务(Order Service)
  • 产品服务(Product Service)

第一步:从单体开始(baseline)

先写一个单体版,所有逻辑在一个main.go里:

// main.go (单体版本)
package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type Product struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Price int    `json:"price"`
}

var products = []Product{
	{1, "手机", 5000},
	{2, "耳机", 300},
}

func getProduct(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(products)
}

func main() {
	http.HandleFunc("/products", getProduct)
	log.Println("单体服务启动在 :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

运行:go run main.go,访问 http://localhost:8080/products 能看到商品列表。

第二步:拆出第一个微服务——产品服务

创建新目录 product-service,迁移产品相关逻辑:

// product-service/main.go
package main

import (
	"context"
	"log"
	"net"

	pb "product-service/proto" // 稍后定义
	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedProductServiceServer
}

func (s *server) GetProducts(ctx context.Context, req *pb.Empty) (*pb.ProductList, error) {
	// 简化:直接返回硬编码数据
	return &pb.ProductList{
		Products: []*pb.Product{
			{Id: 1, Name: "手机", Price: 5000},
			{Id: 2, Name: "耳机", Price: 300},
		},
	}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("监听失败: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterProductServiceServer(s, &server{})
	log.Println("产品服务启动在 :50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("服务启动失败: %v", err)
	}
}

为什么用gRPC不用HTTP?
gRPC基于Protocol Buffers,性能比JSON高5-10倍,且强类型定义避免接口不一致。微服务间内部通信首选gRPC,对外API才用REST。

第三步:定义Protobuf接口

product-service/proto/product.proto 中定义接口:

syntax = "proto3";

option go_package = "product-service/proto";

service ProductService {
  rpc GetProducts(Empty) returns (ProductList);
}

message Empty {}

message Product {
  int32 id = 1;
  string name = 2;
  int32 price = 3;
}

message ProductList {
  repeated Product products = 1;
}

生成Go代码:

# 在product-service目录下
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc --go_out=. --go-grpc_out=. proto/product.proto

第四步:订单服务调用产品服务

创建 order-service,通过gRPC调用产品服务:

// order-service/main.go
package main

import (
	"context"
	"log"
	"net/http"

	pb "product-service/proto" // 引用product-service的proto
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func createOrder(w http.ResponseWriter, r *http.Request) {
	// 连接产品服务
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("无法连接产品服务: %v", err)
	}
	defer conn.Close()

	client := pb.NewProductServiceClient(conn)
	resp, err := client.GetProducts(context.Background(), &pb.Empty{})
	if err != nil {
		http.Error(w, "获取商品失败", 500)
		return
	}

	// 简化:直接返回商品列表作为订单内容
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte("订单已创建,包含商品: "))
	for _, p := range resp.Products {
		w.Write([]byte(p.Name + ", "))
	}
}

func main() {
	http.HandleFunc("/create-order", createOrder)
	log.Println("订单服务启动在 :8081")
	log.Fatal(http.ListenAndServe(":8081", nil))
}

第五步:解决服务发现痛点

现在订单服务写死连接 localhost:50051,这在分布式环境会崩溃!我们需要服务发现

用Consul做服务注册中心(轻量级,适合入门):

  1. 启动Consul(Docker):
docker run -d --name consul -p 8500:8500 consul
  1. 产品服务启动时注册自己:
// 在product-service/main.go中添加
import "github.com/hashicorp/consul/api"

func registerToConsul() {
	config := api.DefaultConfig()
	client, _ := api.NewClient(config)
	agent := client.Agent()
	_ = agent.ServiceRegister(&api.AgentServiceRegistration{
		ID:      "product-service-1",
		Name:    "product-service",
		Port:    50051,
		Address: "127.0.0.1",
	})
}
  1. 订单服务通过Consul查找产品服务:
// 在order-service中替换grpc.Dial
client, _ := api.NewClient(api.DefaultConfig())
services, _, _ := client.Catalog().Service("product-service", "", nil)
addr := services[0].Address + ":" + strconv.Itoa(services[0].Port)
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))

最佳实践:生产环境用Kubernetes内置服务发现,但Consul更适合学习原理。

第六步:加上可观测性(日志+追踪)

微服务没有日志就像黑夜开车。我们用OpenTelemetry:

  1. 产品服务添加追踪:
// product-service/main.go
import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

s := grpc.NewServer(
	grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
)
  1. 订单服务传递追踪上下文:
// order-service/main.go
ctx, span := tracer.Start(context.Background(), "create-order")
defer span.End()

// gRPC调用自动携带trace上下文
resp, err := client.GetProducts(ctx, &pb.Empty{})

启动Jaeger收集追踪数据:

docker run -d --name jaeger -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one

访问 http://localhost:16686 就能看到调用链!

新手高频问题解答

Q1:微服务一定要用Docker吗?

:开发阶段可以不用,但生产环境必须容器化。Docker解决的是“在我机器上能跑”的千古难题。建议从第一天就用Docker Compose管理服务:

# docker-compose.yml
version: '3'
services:
  product-service:
    build: ./product-service
    ports: ["50051:50051"]
  order-service:
    build: ./order-service
    ports: ["8081:8081"]
  consul:
    image: consul
    ports: ["8500:8500"]
  jaeger:
    image: jaegertracing/all-in-one
    ports: ["16686:16686", "6831:6831/udp"]

Q2:数据库怎么拆?

:每个微服务独占数据库!产品服务用product_db,订单服务用order_db。这是微服务的黄金法则。跨服务查询用:

  • 最终一致性(发事件)
  • 冗余字段(如订单里存商品快照)
  • 绝不共享同一张表!

Q3:Go微服务框架选哪个?

:新手别用框架!直接用标准库+gRPC。等你理解原理后,再考虑:

  • kit:轻量,只提供基础工具
  • go-zero:国产,自动生成代码
  • Kratos:B站开源,生态完整

我见过太多人沉迷框架,却连HTTP状态码都说不清。先学会走路,再想跑。

下一步学习路径

  1. 深入gRPC:学习流式RPC、拦截器、TLS加密
  2. 消息队列:用Kafka/RabbitMQ解耦服务(比如下单后发邮件)
  3. API网关:用Kong/Traefik统一入口、限流、鉴权
  4. 配置中心:Nacos/Apollo管理多环境配置
  5. 混沌工程:故意制造故障,测试系统韧性

最后忠告:微服务不是目的,快速交付可靠的产品才是。我见过团队花3个月搭微服务架子,结果业务需求早变了。先跑通单体MVP,等痛点真实出现再拆分!


附:项目结构速查表

ecommerce/
├── product-service/
│   ├── main.go          # gRPC服务实现
│   ├── proto/           # Protobuf定义
│   └── Dockerfile       # 容器化配置
├── order-service/
│   ├── main.go          # 调用product-service
│   └── Dockerfile
├── docker-compose.yml   # 一键启动所有服务
└── README.md            # 启动步骤说明

动手试试吧!遇到问题随时回来翻这篇指南。记住:所有复杂的架构,都是从一行go run开始的。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝