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 服务的搭建,并深入探讨服务间数据聚合的设计哲学。

项目结构

dir 复制代码
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,定义好友相关接口:

go 复制代码
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 验证,相关配置会在生成代码后自动添加中间件。

执行生成命令:

bash 复制代码
$ goctl api go -api apps/social/api/social.api -dir apps/social/api -style goZero 

配置多 RPC 客户端

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

go 复制代码
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 示例(开发环境):

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 创建双客户端代理:

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。

go 复制代码
// 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

go 复制代码
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. 组装结果。
go 复制代码
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-rpc social-rpc social-api 客户端 数据库 user-rpc social-rpc social-api 客户端 #mermaid-svg-CYKaaRqvFtDJ34hS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CYKaaRqvFtDJ34hS .error-icon{fill:#552222;}#mermaid-svg-CYKaaRqvFtDJ34hS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CYKaaRqvFtDJ34hS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CYKaaRqvFtDJ34hS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CYKaaRqvFtDJ34hS .marker.cross{stroke:#333333;}#mermaid-svg-CYKaaRqvFtDJ34hS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CYKaaRqvFtDJ34hS p{margin:0;}#mermaid-svg-CYKaaRqvFtDJ34hS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CYKaaRqvFtDJ34hS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CYKaaRqvFtDJ34hS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-CYKaaRqvFtDJ34hS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-CYKaaRqvFtDJ34hS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-CYKaaRqvFtDJ34hS .sequenceNumber{fill:white;}#mermaid-svg-CYKaaRqvFtDJ34hS #sequencenumber{fill:#333;}#mermaid-svg-CYKaaRqvFtDJ34hS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-CYKaaRqvFtDJ34hS .messageText{fill:#333;stroke:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CYKaaRqvFtDJ34hS .labelText,#mermaid-svg-CYKaaRqvFtDJ34hS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .loopText,#mermaid-svg-CYKaaRqvFtDJ34hS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-CYKaaRqvFtDJ34hS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-CYKaaRqvFtDJ34hS .noteText,#mermaid-svg-CYKaaRqvFtDJ34hS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-CYKaaRqvFtDJ34hS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CYKaaRqvFtDJ34hS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CYKaaRqvFtDJ34hS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-CYKaaRqvFtDJ34hS .actorPopupMenu{position:absolute;}#mermaid-svg-CYKaaRqvFtDJ34hS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-CYKaaRqvFtDJ34hS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-CYKaaRqvFtDJ34hS .actor-man circle,#mermaid-svg-CYKaaRqvFtDJ34hS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-CYKaaRqvFtDJ34hS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 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 通信部分,构建完整的聊天核心。

相关推荐
Echo缘6 小时前
嵌入式系统C语言资源分类与内存分布分析
c语言·开发语言
JaneConan7 小时前
鸿蒙 ArkUI 深水区:@Watch 和 @Observed,状态变了「自动跑」+ 嵌套对象「深层重绘」
开发语言·后端·ui·harmonyos
赤水无泪7 小时前
qt中图标、名称的设置方式
开发语言·qt
王莎莎-MinerU7 小时前
MCP 解决的是工具接入,科研 Agent 还缺的是科学证据接口标准化
开发语言·网络·人工智能·深度学习·pdf·c#·php
Wang's Blog7 小时前
Go-Zero项目开发9: 微服务治理之服务注册中心
微服务·golang
橘子海全栈攻城狮7 小时前
【最新源码】基于SpringBoot + Vue的超市管理系统的设计与实现D002
java·开发语言·vue.js·spring boot·后端·spring
在水一缸7 小时前
深入浅出 Catch2:现代 C++ 测试框架的优雅实践
开发语言·c++·单元测试·log4j·测试框架·catch2
-银雾鸢尾-8 小时前
C#中Object类内的方法
开发语言·c#
浮江雾8 小时前
Flutter第十节-----Flutter布局与组件全解析
android·开发语言·前端·学习·flutter·入门
爱喝水的鱼丶8 小时前
SAP-ABAP:ALV跨版本兼容指南——适配ECC与S4 HANA系统的ALV开发注意事项
开发语言·性能优化·sap·abap·经验交流