从单体到微服务:用 Go 打造你的第一套分布式系统

架构师App
2026-01-13 10:53
阅读 636

大家好,我是小张,一名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。

流程如下:

  1. 服务启动时,向注册中心“报到”(注册)
  2. 其他服务想调用它,就去注册中心“查电话号码”(发现)
  3. 注册中心还会定期检查服务是否存活(健康检查)

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

学习建议:下一步怎么走?

  1. 简历加分项:把这个项目整理成 GitHub 仓库,README 写清楚架构图(可用文字描述)、技术栈、部署步骤。面试时直接说:“这是我从单体重构到微服务的实战项目。”

  2. 深入方向

    • gRPC 替换 HTTP 调用(性能提升 3-5 倍)
    • 加入 链路追踪(Jaeger / Zipkin)
    • 实现 熔断与限流(用 Go kit 或 Sentinel)
    • 容器化部署(Docker + Kubernetes)
  3. 避坑提醒

    • 不要一上来就追求“完美微服务”,先保证业务跑通
    • 日志一定要集中收集(推荐 ELK 或 Loki)
    • 本地开发可用 Docker Compose 一键启停所有服务

结语

微服务不是银弹,但它确实是现代后端工程师的必备技能。我当初也是从这样一个小 demo 开始,慢慢理解了分布式系统的魅力。技术深度 + 清晰表达 = 简历亮点。希望这篇教程能帮你迈出第一步。

如果你觉得有用,欢迎关注我的博客(虚构哈 😄),我会持续更新 Go、系统设计、求职相关的实战内容。有问题也欢迎留言讨论!

最后送大家一句话:架构是演进而非设计出来的。先跑起来,再优化,别被理论吓住。加油!

评论 0

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