从单体到微服务:用 Go 打造你的第一套分布式系统
大家好,我是小张,一名211高校的计算机研究生。平时除了做科研,我特别喜欢在技术博客上分享学习心得。最近不少学弟学妹私信问我:“微服务到底难不难?零基础能不能上手?”我当初学的时候也是一头雾水——什么服务拆分、注册中心、RPC 调用,听起来像天书。但其实,只要动手写一遍,你会发现它并没有那么神秘。
更重要的是,掌握微服务架构对写简历非常有帮助。无论是实习还是校招,只要你在项目经历里写上“基于 Go 的微服务系统”,HR 和面试官都会多看一眼。今天这篇教程,我就带你从零开始,用最简单的代码,把一个单体应用一步步改造成真正的微服务架构。
为什么需要微服务?
想象你正在开发一个电商网站。一开始功能简单:用户登录、商品浏览、下单。所有代码都塞在一个项目里,部署也方便——直接扔到一台服务器就行。这就是单体架构(Monolithic Architecture)。
但随着业务增长,问题来了:
- 团队变大,多人改同一份代码容易冲突
- 某个模块崩溃(比如支付失败),整个网站都挂了
- 想给商品搜索加高性能缓存?得动整个系统
于是,微服务架构(Microservices Architecture) 应运而生:把大系统拆成多个小服务,每个服务独立开发、部署、扩展。比如:
- 用户服务(User Service)
- 商品服务(Product Service)
- 订单服务(Order Service)
它们通过网络互相调用,各自专注自己的职责。
💡 新手误区:微服务不是越多越好!拆得太细反而增加运维复杂度。一般建议先从核心业务边界入手拆分。
环境准备:5分钟搭好 Go 微服务开发环境
我们全程使用 Go 语言,因为它并发性能强、编译快、部署简单,非常适合做后端微服务。
步骤 1:安装 Go
访问 https://go.dev/dl/ 下载最新版(建议 1.22+),安装后验证:
go version
# 输出类似:go version go1.22.0 linux/amd64
步骤 2:初始化项目
创建一个工作目录:
mkdir micro-demo && cd micro-demo
go mod init micro-demo
步骤 3:安装必要工具
我们会用到两个轻量级库:
gin:Web 框架,写 HTTP 接口超快consul:服务注册与发现(后面会讲)
安装命令:
go get -u github.com/gin-gonic/gin
go get -u github.com/hashicorp/consul/api
✅ 避坑指南:国内用户建议配置 GOPROXY:
go env -w GOPROXY=https://goproxy.cn,direct
核心概念:微服务三大基石
要理解微服务,只需掌握三个关键点:
1. 服务拆分(Service Decomposition)
按业务功能划分独立服务。例如:
- 用户服务:处理注册、登录、个人信息
- 商品服务:管理商品列表、库存
- 每个服务有自己的数据库(避免共享 DB 导致耦合)
2. 服务注册与发现(Service Registry & Discovery)
当服务 A 要调用服务 B,它怎么知道 B 的 IP 和端口?
答案:服务注册中心。常用工具有 Consul、Etcd、Nacos。
流程如下:
- 服务启动时,向注册中心“报到”(注册)
- 其他服务想调用它,就去注册中心“查电话号码”(发现)
- 注册中心还会定期检查服务是否存活(健康检查)
3. 远程调用(Remote Procedure Call, RPC)
服务之间通信不能直接调函数,必须通过网络。方式有两种:
- HTTP/REST:简单通用,适合跨语言
- gRPC:高性能二进制协议,适合内部高频调用
本教程先用 HTTP,后续可升级到 gRPC。
实战:从单体到微服务的三步改造
我们以一个极简“用户-商品”系统为例。
第一步:写一个单体应用(对照组)
main.go:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 用户接口
r.GET("/user/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三"})
})
// 商品接口
r.GET("/product/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "笔记本电脑", "price": 5999})
})
r.Run(":8080")
}
运行后访问 http://localhost:8080/user/1 和 /product/1 都能拿到数据——但这是单体!
第二步:拆分成两个独立服务
用户服务(user-service/main.go)
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/user/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三", "service": "user"})
})
r.Run(":8081") // 注意端口不同
}
商品服务(product-service/main.go)
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Gin()
r.GET("/product/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "笔记本电脑", "price": 5999, "service": "product"})
})
r.Run(":8082")
}
现在你有两个独立进程,分别监听 8081 和 8082。
第三步:加入服务注册与发现(用 Consul)
启动 Consul(本地测试)
下载 Consul:https://developer.hashicorp.com/consul/downloads
解压后运行:
./consul agent -dev
访问 http://localhost:8500 可看到 Web UI。
改造用户服务:自动注册
// user-service/main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/hashicorp/consul/api"
)
func registerService() {
config := api.DefaultConfig()
client, _ := api.NewClient(config)
registration := &api.AgentServiceRegistration{
ID: "user-service-1",
Name: "user-service",
Port: 8081,
Address: "127.0.0.1",
}
client.Agent().ServiceRegister(registration)
}
func main() {
registerService() // 启动时注册
r := gin.Default()
r.GET("/user/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id"), "name": "张三", "service": "user"})
})
r.Run(":8081")
}
商品服务同理,只需改 Name 为 product-service,Port 为 8082。
🔄 验证:启动两个服务后,刷新 Consul UI,你会看到两个服务已注册!
第四步:API 网关(统一入口)
现在客户端要记两个地址?不行!我们需要一个网关来路由请求。
新建 gateway/main.go:
package main
import (
"io/ioutil"
"net/http"
"github.com/gin-gonic/gin"
"github.com/hashicorp/consul/api"
)
func getServiceAddress(serviceName string) (string, error) {
config := api.DefaultConfig()
client, _ := api.NewClient(config)
services, _, err := client.Catalog().Service(serviceName, "", nil)
if err != nil || len(services) == 0 {
return "", err
}
return services[0].Address + ":" + string(rune(services[0].Port)), nil
}
func proxyRequest(c *gin.Context, serviceName, path string) {
addr, err := getServiceAddress(serviceName)
if err != nil {
c.JSON(500, gin.H{"error": "service not found"})
return
}
resp, err := http.Get("http://" + addr + path)
if err != nil {
c.JSON(500, gin.H{"error": "request failed"})
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
c.Data(resp.StatusCode, "application/json", body)
}
func main() {
r := gin.Default()
r.GET("/user/:id", func(c *gin.Context) {
proxyRequest(c, "user-service", c.Request.URL.Path)
})
r.GET("/product/:id", func(c *gin.Context) {
proxyRequest(c, "product-service", c.Request.URL.Path)
})
r.Run(":8080") // 网关统一入口
}
现在,客户端只需访问 http://localhost:8080/user/1,网关会自动转发到用户服务!
新手常见问题解答
Q1:Consul 必须用吗?能不用注册中心吗?
可以,但不推荐。初期你可以硬编码服务地址(比如在配置文件里写死 user_service_url = "http://localhost:8081")。但一旦服务实例变多(比如部署到多台机器),你就必须用注册中心动态管理。
Q2:微服务之间怎么传递用户身份?
常见做法:在网关验证 JWT Token,然后把用户 ID 放到请求头(如 X-User-ID),下游服务直接读取,不要重复鉴权。
Q3:数据库怎么拆?
每个服务独享自己的数据库。比如用户服务用 user_db,商品服务用 product_db。绝对不要共享同一张表!
Q4:性能会不会变差?毕竟多了网络调用。
确实有损耗,但可通过以下方式优化:
- 使用连接池(Go 的
http.Client默认复用连接) - 合理设计接口,减少调用次数
- 关键路径用 gRPC 替代 HTTP
学习建议:下一步怎么走?
简历加分项:把这个项目整理成 GitHub 仓库,README 写清楚架构图(可用文字描述)、技术栈、部署步骤。面试时直接说:“这是我从单体重构到微服务的实战项目。”
深入方向:
- 用 gRPC 替换 HTTP 调用(性能提升 3-5 倍)
- 加入 链路追踪(Jaeger / Zipkin)
- 实现 熔断与限流(用 Go kit 或 Sentinel)
- 容器化部署(Docker + Kubernetes)
避坑提醒:
- 不要一上来就追求“完美微服务”,先保证业务跑通
- 日志一定要集中收集(推荐 ELK 或 Loki)
- 本地开发可用 Docker Compose 一键启停所有服务
结语
微服务不是银弹,但它确实是现代后端工程师的必备技能。我当初也是从这样一个小 demo 开始,慢慢理解了分布式系统的魅力。技术深度 + 清晰表达 = 简历亮点。希望这篇教程能帮你迈出第一步。
如果你觉得有用,欢迎关注我的博客(虚构哈 😄),我会持续更新 Go、系统设计、求职相关的实战内容。有问题也欢迎留言讨论!
最后送大家一句话:架构是演进而非设计出来的。先跑起来,再优化,别被理论吓住。加油!

评论 0