学习微服务通信,本文详细介绍了如何构建两个 Golang 微服务(用户服务与订单服务),并通过 Docker 与 Docker Compose 进行容器化部署。
创建两个微服务
user-service
新建项目
1
2
3
4
5
6
7
8
9# 创建文件夹
mkdir user-service
cd user-service
# 生成 go.mod 文件,管理依赖版本。
go mod init user-service
# 安装依赖 gin, gorm, mysql
go get -u github.com/gin-gonic/gin gorm.io/gorm gorm.io/driver/mysql配置 mysql 及账号密码
1
2
3
4# 拉取 MySQL 镜像
docker pull mysql:latest
# 启动容器(设置 root 密码为 your_password)
docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=your_password -d mysql:latest编写 main.go 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67package main
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 连接数据库
dsn := "root:yourpassword@tcp(mysql:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("数据库连接失败")
}
// 自动创建表
db.AutoMigrate(&User{})
// 初始化 Gin
r := gin.Default()
// 路由
r.POST("/users", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
result := db.Create(&user)
if result.Error != nil {
c.JSON(500, gin.H{"error": "创建用户失败"})
return
}
c.JSON(200, user)
})
// 获取所有用户
r.GET("/users", func(c *gin.Context) {
var users []User
db.Find(&users)
c.JSON(200, users)
})
// 查询单个用户
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
var user User
result := db.First(&user, id)
if result.Error != nil {
c.JSON(404, gin.H{"error": "用户不存在"})
return
}
c.JSON(200, user)
})
// 启动服务
r.Run(":8080")
}运行
go run main.go进行验证
order-service
新建项目
1
2
3
4
5
6
7
8
9# 创建文件夹
mkdir order-service
cd order-service
# 生成 go.mod 文件,管理依赖版本。
go mod init order-service
# 安装依赖 gin, gorm, mysql
go get -u github.com/gin-gonic/gin gorm.io/gorm gorm.io/driver/mysql编写 main.go 文件
注意 ⚠️:getUser 函数中,本地测试使用”http://localhost:8080/users/%d",docker 容器中通信使用”http://user-service:8080/users/%d“
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
)
// 订单模型
type Order struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Product string `json:"product"`
}
// 调用用户服务的 API
func getUser(userID uint) (map[string]interface{}, error) {
resp, err := http.Get(fmt.Sprintf("http://user-service:8080/users/%d", userID))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var user map[string]interface{}
json.Unmarshal(body, &user)
return user, nil
}
func main() {
r := gin.Default()
// 创建订单(需关联用户)
r.POST("/orders", func(c *gin.Context) {
var order Order
if err := c.ShouldBindJSON(&order); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
// 调用用户服务验证用户是否存在
user, err := getUser(order.UserID)
if err != nil || user["id"] == nil {
c.JSON(400, gin.H{"error": "用户不存在"})
return
}
// 此处应保存订单到数据库(简化示例,直接返回)
c.JSON(200, gin.H{
"order_id": order.ID,
"user": user,
"product": order.Product,
})
})
r.Run(":8081")
}
测试通信
使用 Postman
POST
http://localhost:8080/users1
2// 请求
{ "name": "Alice", "age": 25 }GET
http://localhost:8080/users1
2
3
4
5
6
7
8// 预期返回
[
{
"id": 1,
"name": "Alice",
"age": 25
}
]POST
http://localhost:8081/orders1
2// 请求
{ "user_id": 1, "product": "Laptop" }1
2
3
4
5
6
7
8
9
10// 预期返回
{
"order_id": 0,
"product": "Laptop",
"user": {
"id": 1,
"name": "Alice",
"age": 25
}
}
Docker 部署
- 新建 Dockerfile 文件
user-service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 第一阶段:构建可执行文件
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -o user-service .
# 第二阶段:运行
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/user-service .
EXPOSE 8080
CMD ["./user-service"]Order-service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 第一阶段:构建可执行文件
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN go build -o order-service .
# 第二阶段:运行
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/order-service .
EXPOSE 8081
CMD ["./order-service"]
build 容器
1
2
3
4
5cd user-service
docker build -t user-service .
cd order-service
docker build -t order-service .创建 Docker 网络
使容器间能通过服务名(如
user-service)通信,而非 IP 地址。1
docker network create my-network
创建
docker-compose.yml文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33version: "3"
services:
mysql:
image: mysql:latest
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: user_db
ports:
- "3306:3306"
networks:
- my-network
user-service:
build: ./user-service
ports:
- "8080:8080"
networks:
- my-network
depends_on:
- mysql
order-service:
build: ./order-service
ports:
- "8081:8081"
networks:
- my-network
depends_on:
- user-service
networks:
my-network:启动 docker compose
1
docker-compose up --build
测试通信
1
2
3
4
5
6
7
8
9
10
11
12# 创建一个用户
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 25}'
# 查询用户
curl http://localhost:8080/users
# 测试通信
curl -X POST http://localhost:8081/orders \
-H "Content-Type: application/json" \
-d '{"id": 1, "user_id": 1, "product": "ProductA"}'
常见报错及问题
问题:build docker 时报错:
invalid go version '1.23.5': must match format 1.23解决:修改
go.mod文件,将go 1.23.5改为go 1.23问题:build docker 时报错:
package slices is not in GOROOT (/usr/local/go/src/slices)原因:在编译过程中,Go 版本过低,不包含标准库中的
slices包。slices包是在 Go 1.21 中引入的,如果使用的 Go 版本低于 1.21,就会出现找不到该包的错误。解决:修改 Dockerfile 中的
FROM指令:FROM golang:1.23-alpine AS builder问题:启动
user-service容器报错1
22025-02-01 01:43:55 [error] failed to initialize database, got error dial tcp 127.0.0.1:3306: connect: connection refused
2025-02-01 01:40:02 panic: 数据库连接失败
解决:user-service 尝试连接到 127.0.0.1:3306,即本地地址的 MySQL 数据库。但在 Docker 容器中,127.0.0.1 指向的是容器自身,而不是宿主机或其他容器。因此,我们需要修改127.0.0.1:3306为 mysql:3306
- 两个微服务在 docker 不能通信原因:
ip 地址
在
user-service的代码中,DSN 使用了127.0.0.1:3306。在 Docker Compose 中,每个服务在各自容器内运行,127.0.0.1指的是容器自身,而非 MySQL 服务。正确做法是使用 Compose 中定义的服务名(这里是mysql),修改 DSN 为:1
dsn := "root:yourpassword@tcp(mysql:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
这样,
user-service就能通过 Docker 网络正确访问 MySQL 容器。order-service 请求 user-service 应该使用
user-service:8080而不是localhost:8080Docker compose up 运行时,user-service 需要等待 mysql 准备好才能连接,现在是手动连接,接下来需要增加 health check 自动连接
全部准备就绪后,需要向 user-service POST 添加数据,才可以用 order-service 进行测试