Golang 微服务通信练习

学习微服务通信,本文详细介绍了如何构建两个 Golang 微服务(用户服务与订单服务),并通过 Docker 与 Docker Compose 进行容器化部署。

创建两个微服务

user-service

  1. 新建项目

    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
  2. 配置 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
  3. 编写 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
    67
    package 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")
    }
  4. 运行go run main.go进行验证

order-service

  1. 新建项目

    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
  2. 编写 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
    59
    package 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/users

    1
    2
    // 请求
    { "name": "Alice", "age": 25 }
  • GET http://localhost:8080/users

    1
    2
    3
    4
    5
    6
    7
    8
    // 预期返回
    [
    {
    "id": 1,
    "name": "Alice",
    "age": 25
    }
    ]
  • POST http://localhost:8081/orders

    1
    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 部署

  1. 新建 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"]
  1. build 容器

    1
    2
    3
    4
    5
    cd user-service
    docker build -t user-service .

    cd order-service
    docker build -t order-service .
  2. 创建 Docker 网络

    使容器间能通过服务名(如 user-service)通信,而非 IP 地址。

    1
    docker network create my-network
  3. 创建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
    33
    version: "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:
  4. 启动 docker compose

    1
    docker-compose up --build
  5. 测试通信

    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"}'

常见报错及问题

  1. 问题:build docker 时报错:invalid go version '1.23.5': must match format 1.23

    解决:修改go.mod文件,将 go 1.23.5 改为 go 1.23

  2. 问题: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

  3. 问题:启动user-service容器报错

    1
    2
    2025-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:3306mysql:3306

  1. 两个微服务在 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:8080

  • Docker compose up 运行时,user-service 需要等待 mysql 准备好才能连接,现在是手动连接,接下来需要增加 health check 自动连接

  • 全部准备就绪后,需要向 user-service POST 添加数据,才可以用 order-service 进行测试