Go-Zero项目开发8: 构建社交API服务与微服务解耦实践

纲要

  • 回顾:社交 RPC 服务已就绪,需暴露 HTTP 接口
  • 构建社交 API 服务
    • 编写 social.api 定义路由与数据结构
    • 使用 goctl api go 生成代码
    • ServiceContext 注入 social-rpcuser-rpc 客户端
    • 配置 etc/socialapi.yamljwt 认证
  • 业务接口实现
    • 好友申请:验证登录状态,调用社交 RPC
    • 好友申请处理:透传处理逻辑
    • 好友列表:聚合用户信息与好友关系
      • 从社交 RPC 获取好友 ID 列表
      • 批量调用用户 RPC 查询用户详情
      • 数据映射与组装
  • 好友列表聚合流程(Mermaid 图)
  • 微服务设计反思
    • 为什么不共享数据库:解耦、灵活性、安全性
    • 为什么不在 RPC 服务间相互调用:避免循环依赖、复杂度、故障隔离
    • BFF 层的价值:聚合、隔离、可观测性
  • 测试验证与总结

背景

前面我们已经完成了社交 RPC 服务,实现了好友申请、处理与列表查询的 gRPC 接口。为了让前端能够调用这些功能,还需要构建一层 HTTP API 服务(social-api)。同时,好友列表不仅需要关系数据,还需要用户详细信息,这涉及多服务数据聚合,是微服务中典型的 BFF(Backend For Frontend)场景。

本文将基于 go-zero@latest 完成社交 API 服务的搭建,并深入探讨服务间数据聚合的设计哲学。

项目结构

apps/social/
├── api/
│   ├── internal/
│   │   ├── config/
│   │   ├── handler/
│   │   ├── logic/
│   │   ├── svc/
│   │   └── types/
│   ├── social.api 
│   └── socialapi.go 
└── rpc/
    ├── internal/
    ├── model/
    └── social.proto 

构建社交 API 服务

定义 API 描述文件

创建 apps/social/api/social.api,定义好友相关接口:

type (
    FriendApplyReq {
        FriendId string `json:"friend_id"`
        Reason   string `json:"reason,optional"`
    }
    FriendApplyResp {}
 
    FriendApplyHandleReq {
        ApplyId    int64 `json:"apply_id"`
        HandleType int32 `json:"handle_type"` // 1:通过 2:拒绝 
    }
    FriendApplyHandleResp {}
 
    FriendListResp {
        List []FriendInfo `json:"list"`
    }
    FriendInfo {
        UserId   string `json:"user_id"`
        Nickname string `json:"nickname"`
        Avatar   string `json:"avatar"`
        Desc     string `json:"desc"`
    }
)
 
@server(
    prefix: /v1/social 
    jwt: Auth 
)
service social-api {
    @handler FriendApplyHandler 
    post /friend/apply (FriendApplyReq) returns (FriendApplyResp)
 
    @handler FriendApplyHandleHandler 
    put /friend/apply/handle (FriendApplyHandleReq) returns (FriendApplyHandleResp)
 
    @handler FriendListHandler 
    get /friend/list returns (FriendListResp)
}

注意:jwt: Auth 指定该服务需要 JWT 验证,相关配置会在生成代码后自动添加中间件。

执行生成命令:

$ goctl api go -api apps/social/api/social.api -dir apps/social/api -style goZero 

配置多 RPC 客户端

编辑 apps/social/api/internal/config/config.go,增加两个 RPC 配置:

package config 
 
import (
    "github.com/zeromicro/go-zero/rest"
    "github.com/zeromicro/go-zero/zrpc"
)
 
type Config struct {
    rest.RestConf 
    SocialRpc zrpc.RpcClientConf 
    UserRpc   zrpc.RpcClientConf 
    JWT       struct {
        AccessSecret string 
        AccessExpire int64 
    }
}

etc/socialapi.yaml 示例(开发环境):

Name: social-api 
Host: 0.0.0.0 
Port: 8881 
SocialRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379 
    Key: social.rpc 
UserRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379 
    Key: user.rpc 
JWT:
  AccessSecret: "your-secret-key"
  AccessExpire: 86400 

svc/servicecontext.go 创建双客户端代理:

package svc 
 
import (
    "im-system/apps/social/api/internal/config"
    "im-system/apps/social/rpc/social"
    "im-system/apps/user/rpc/user"
    "github.com/zeromicro/go-zero/zrpc"
)
 
type ServiceContext struct {
    Config     config.Config 
    SocialRpc  social.SocialClient 
    UserRpc    user.UserClient 
}
 
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        SocialRpc: social.NewSocialClient(zrpc.MustNewClient(c.SocialRpc).Conn()),
        UserRpc:   user.NewUserClient(zrpc.MustNewClient(c.UserRpc).Conn()),
    }
}

业务逻辑实现

好友申请

JWT 解析出的 context 中提取当前用户 uid,然后调用社交 RPC。

// internal/logic/friendapplylogic.go 
func (l *FriendApplyLogic) FriendApply(req *types.FriendApplyReq) (*types.FriendApplyResp, error) {
    uid := svc.GetUidFromCtx(l.ctx) // 工具函数从 context 提取 uid 
    _, err := l.svcCtx.SocialRpc.FriendApply(l.ctx, &social.FriendApplyRequest{
        UserId:   uid,
        FriendId: req.FriendId,
        Reason:   req.Reason,
    })
    if err != nil {
        return nil, err 
    }
    return &types.FriendApplyResp{}, nil 
}

好友申请处理

同样透传,仅需传递 apply_idhandle_type

func (l *FriendApplyHandleLogic) FriendApplyHandle(req *types.FriendApplyHandleReq) (*types.FriendApplyHandleResp, error) {
    _, err := l.svcCtx.SocialRpc.FriendApplyHandle(l.ctx, &social.FriendApplyHandleRequest{
        ApplyId:    req.ApplyId,
        HandleType: req.HandleType,
    })
    if err != nil {
        return nil, err 
    }
    return &types.FriendApplyHandleResp{}, nil 
}

好友列表(核心聚合)

好友列表需要返回好友的昵称、头像等信息,这些数据存在于用户服务。因此需要:

  1. 调用社交 RPC 获取好友 ID 列表。
  2. 调用用户 RPC 批量查询用户信息。
  3. 组装结果。
func (l *FriendListLogic) FriendList() (*types.FriendListResp, error) {
    uid := svc.GetUidFromCtx(l.ctx)
 
    // 1. 获取好友 ID 列表 
    friendsResp, err := l.svcCtx.SocialRpc.FriendList(l.ctx, &social.FriendListRequest{
        UserId: uid,
    })
    if err != nil {
        return nil, err 
    }
    if len(friendsResp.FriendIds) == 0 {
        return &types.FriendListResp{List: []types.FriendInfo{}}, nil 
    }
 
    // 2. 批量获取用户信息 
    usersResp, err := l.svcCtx.UserRpc.FindUser(l.ctx, &user.FindUserRequest{
        Ids: friendsResp.FriendIds,
    })
    if err != nil {
        return nil, err 
    }
 
    // 3. 构建映射:uid → 用户信息 
    userMap := make(map[string]*user.GetUserInfoResponse, len(usersResp.Users))
    for _, u := range usersResp.Users {
        userMap[u.Uid] = u 
    }
 
    // 4. 组装好友列表 
    list := make([]types.FriendInfo, 0, len(friendsResp.FriendIds))
    for _, fid := range friendsResp.FriendIds {
        friendInfo := types.FriendInfo{
            UserId: fid,
        }
        if u, ok := userMap[fid]; ok {
            friendInfo.Nickname = u.Username 
            friendInfo.Avatar   = u.Avatar 
            friendInfo.Desc     = "" // 可扩展 
        }
        list = append(list, friendInfo)
    }
 
    return &types.FriendListResp{List: list}, nil 
}

注意:用户服务中的 FindUser 支持按 ID 集合查询,正好用于批量获取。这要求我们在用户 RPC 中实现了 FindUser 接口(之前已实现)。

聚合流程图示

数据库user-rpcsocial-rpcsocial-api客户端数据库user-rpcsocial-rpcsocial-api客户端GET /v1/social/friend/list (JWT)解析 JWT 获取 uidFriendList(uid)查询好友关系表friend_idsfriend_idsFindUser(ids)批量查询用户表用户列表用户详情列表映射组装 FriendInfo{code:0, data:{list:[...]}}

微服务设计反思

在开发过程中,有两个设计决策值得深入讨论:

为什么不直接在 social-rpc 中查询用户表?

从微服务原则分析

  • 解耦:每个微服务应维护自己独立的数据存储,避免数据库成为耦合点。共享数据库会让服务间产生隐式依赖,一旦表结构变更,影响面广。
  • 灵活性:如果用户表被多个服务直接访问,用户服务的扩展、分库分表都会受限于其他服务的操作模式。
  • 安全性:数据库权限集中,任何服务的漏洞都可能泄露整体用户数据;独立数据库可以实施更精细的权限隔离。
  • 团队自治:不同团队可以独立管理自己的数据库,不必协调表结构变更。

因此,社交服务不直接操作用户表,只能通过 RPC 接口获取用户信息。

为什么不在 social-rpc 中直接调用 user-rpc,而要放在 API 层?

如果 social-rpc 调用 user-rpc,会形成服务间的网状依赖,可能引发循环依赖。例如用户服务也可能需要好友关系数据,相互调用会造成死锁或级联故障。

将聚合逻辑上提到 API 层(BFF) 有以下好处:

  • 职责清晰:RPC 服务专注于自身领域的原子操作,API 层负责面向前端的业务编排。
  • 避免循环依赖:RPC 之间保持无依赖或单向依赖(如社交 RPC 不依赖用户 RPC),系统依赖关系收敛。
  • 故障隔离:用户服务不可用时,聚合层可以降级返回好友 ID 列表而非整体失败,RPC 层相互隔离不影响核心逻辑。
  • 便于扩展:当需要新增前端时,可以在 API 层灵活组合已有 RPC,无需改动底层服务。

当然,少量、单向的 RPC 间调用并非绝对禁止,但本项目通过 BFF 提前聚合,遵循了更加清晰的设计范式。

测试验证

启动所需服务后,使用 API 工具完成测试:

  • 好友申请:登录用户 A,向用户 B 发起申请,检查数据库新增记录。
  • 好友申请处理:用户 B 登录,批准申请,确认好友关系表增加两条记录。
  • 好友列表:任意已登录用户请求,应返回好友的昵称、头像等信息,而非仅 ID。

通过调试,确认多服务协作正常,响应格式统一。

总结

本文完成了社交 API 服务的构建,重点展示了以下技术实践:

  • 使用 goctl api 快速生成 API 代码,并配置双 RPC 客户端。
  • 利用 BFF 层聚合社交关系与用户信息,避免 RPC 服务间直接调用。
  • 结合 Mermaid 时序图直观展示跨服务查询流程。
  • 阐述了微服务数据库隔离与服务间调用的设计原则,强调了 BFF 在解耦和稳定性方面的价值。

至此,即时通讯系统的用户与社交两大基础服务已初步形成,后续将继续扩展 IM 服务与 WebSocket 通信部分,构建完整的聊天核心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值