📚 原创系列: “Go语言学习系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第44篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包
- 并发编程(五):并发模式
- 并发编程(六):原子操作与内存模型
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控
- 第三阶段项目实战:微服务聊天应用 👈 当前位置
📖 文章导读
在本文中,您将了解:
- 如何设计和实现一个基于微服务架构的实时聊天应用
- 微服务之间如何通过gRPC、RESTful API和消息队列进行通信
- 使用WebSocket实现实时消息推送
- 如何处理分布式系统中的服务注册与发现
- 基于Docker的服务部署和扩展策略
- 现代微服务系统的日志收集与监控实践

第三阶段项目实战:微服务聊天应用
在完成了前面的理论学习后,本文将带领读者实战构建一个完整的微服务聊天应用。这个项目整合了我们之前学习的多种技术,包括Go语言基础、Web开发、微服务、数据库、并发编程、日志监控等,旨在通过一个真实的项目来巩固知识并获取实践经验。
1. 项目概述
1.1 业务需求
我们将构建一个名为"GopherChat"的聊天应用,它具有以下功能:
- 用户管理:注册、登录、个人资料管理
- 聊天功能:
- 一对一私聊
- 群聊
- 消息历史记录查询
- 消息类型:
- 文本消息
- 图片消息(URL链接)
- 在线状态:显示用户在线/离线状态
- 消息通知:离线消息推送
1.2 技术栈选择
项目使用以下技术栈:
-
后端:
- Go语言:核心开发语言
- gRPC:服务间通信
- WebSocket:实时消息推送
- NATS:消息队列
- PostgreSQL:主数据存储
- Redis:缓存和会话管理
- JWT:认证
-
前端:
- Vue.js:前端框架(本文重点关注后端实现)
-
部署:
- Docker:容器化
- Docker Compose:本地环境编排
-
监控:
- Prometheus:指标收集
- Grafana:可视化
- Jaeger:分布式追踪
- ELK Stack:日志分析
2. 系统架构设计
2.1 微服务拆分
我们将系统拆分为以下微服务:
-
用户服务(User Service):
- 处理用户注册、登录
- 管理用户资料
- 提供用户信息查询
-
认证服务(Auth Service):
- 处理认证和授权
- 生成和验证JWT令牌
- 维护会话状态
-
聊天服务(Chat Service):
- 管理聊天会话(一对一和群聊)
- 处理消息发送
- 存储聊天历史
-
推送服务(Push Service):
- 维护WebSocket连接
- 向客户端推送实时消息
- 处理用户在线状态
-
媒体服务(Media Service):
- 处理图片上传和处理
- 生成图片URL
-
API网关(API Gateway):
- 统一接入点
- 请求路由
- 协议转换(HTTP/WebSocket -> gRPC)
- 基础认证
2.2 整体架构图

架构说明:
- 前端通过API网关与后端微服务交互
- API网关负责请求路由和协议转换
- 微服务之间通过gRPC进行同步通信
- 消息队列(NATS)用于异步事件处理
- 数据库采用服务专属模式,每个微服务有自己的数据库/表
- 缓存(Redis)用于提高读取性能和会话存储
2.3 通信模式
系统中使用三种主要的通信模式:
-
同步通信(gRPC):
- 服务间直接调用
- 适用于需要立即响应的场景
- 例如:用户信息查询、身份验证
-
异步通信(消息队列):
- 基于NATS的发布/订阅模式
- 适用于事件驱动场景
- 例如:消息通知、状态更新
-
实时通信(WebSocket):
- 客户端与推送服务之间建立WebSocket连接
- 适用于实时更新
- 例如:新消息推送、状态变更通知
2.4 数据模型设计
以下是主要数据模型的简化设计:
用户服务数据模型:
// 用户模型
type User struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
PasswordHash string `json:"-"`
DisplayName string `json:"display_name"`
AvatarURL string `json:"avatar_url"`
Status string `json:"status"` // online, offline, away
LastSeen time.Time `json:"last_seen"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
聊天服务数据模型:
// 会话模型
type Conversation struct {
ID string `json:"id"`
Type string `json:"type"` // private, group
Title string `json:"title"` // For group chats
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 会话参与者
type Participant struct {
ConversationID string `json:"conversation_id"`
UserID string `json:"user_id"`
JoinedAt time.Time `json:"joined_at"`
Role string `json:"role"` // admin, member
}
// 消息模型
type Message struct {
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
SenderID string `json:"sender_id"`
Type string `json:"type"` // text, image
Content string `json:"content"`
SentAt time.Time `json:"sent_at"`
DeliveredAt time.Time `json:"delivered_at,omitempty"`
ReadAt time.Time `json:"read_at,omitempty"`
}
3. 各服务核心实现
下面我们将介绍每个微服务的核心实现思路,专注于关键代码和设计决策。
3.1 用户服务实现
用户服务负责管理用户信息,包括注册、用户资料管理和查询。
3.1.1 服务接口定义(Protocol Buffers)
syntax = "proto3";
package user;
option go_package = "github.com/gopherchat/user-service/proto";
service UserService {
// 创建用户
rpc CreateUser(CreateUserRequest) returns (UserResponse);
// 获取用户信息
rpc GetUser(GetUserRequest) returns (UserResponse);
// 更新用户信息
rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
// 搜索用户
rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse);
}
// 各消息定义
message CreateUserRequest {
string username = 1;
string email = 2;
string password = 3;
string display_name = 4;
}
message GetUserRequest {
string user_id = 1;
}
message UpdateUserRequest {
string user_id = 1;
optional string display_name = 2;
optional string avatar_url = 3;
optional string status = 4;
}
message UserResponse {
string id = 1;
string username = 2;
string email = 3;
string display_name = 4;
string avatar_url = 5;
string status = 6;
string last_seen = 7;
string created_at = 8;
string updated_at = 9;
}
message SearchUsersRequest {
string query = 1;
int32 limit = 2;
int32 offset = 3;
}
message SearchUsersResponse {
repeated UserResponse users = 1;
int32 total = 2;
}
3.1.2 用户服务核心实现
用户服务的核心结构包括:
type UserService struct {
repo repository.UserRepository
hasher utils.PasswordHasher
eventPub events.Publisher
proto.UnimplementedUserServiceServer
}
创建用户方法:
func (s *UserService) CreateUser(ctx context.Context, req *proto.CreateUserRequest) (*proto.UserResponse, error) {
// 验证请求数据
if err := validateCreateUserRequest(req); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Invalid request: %v", err)
}
// 检查用户名是否已存在
exists, err := s.repo.ExistsByUsername(ctx, req.Username)
if err != nil {
return nil, status.Errorf(codes.Internal, "Database error: %v", err)
}
if exists {
return nil, status.Error(codes.AlreadyExists, "Username already exists")
}
// 检查邮箱是否已存在
exists, err = s.repo.ExistsByEmail(ctx, req.Email)
if err != nil {
return nil, status.Errorf(codes.Internal, "Database error: %v", err)
}
if exists {
return nil, status.Error(codes.AlreadyExists, "Email already exists")
}
// 哈希密码
passwordHash, err := s.hasher.HashPassword(req.Password)
if err != nil {
return nil, status.Errorf(codes.Internal, "Password hashing error: %v", err)
}
// 创建用户
now := time.Now()
user := &model.User{
ID: uuid.New().String(),
Username: req.Username,
Email: req.Email,
PasswordHash: passwordHash,
DisplayName: req.DisplayName,
Status: "offline",
LastSeen: now,
CreatedAt: now,
UpdatedAt: now,
}
// 保存用户
if err := s.repo.Create(ctx, user); err != nil {
return nil, status.Errorf(codes.Internal, "Failed to create user: %v", err)
}
// 发布用户创建事件
event := &events.UserCreatedEvent{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt,
}
if err := s.eventPub.Publish(ctx, "user.created", event); err != nil {
log.Printf("Failed to publish user.created event: %v", err)
}
// 返回用户信息
return userToProto(user), nil
}
3.2 认证服务实现
认证服务负责处理用户认证和授权,生成JWT令牌,并维护用户会话。
3.2.1 服务接口定义
syntax = "proto3";
package auth;
option go_package = "github.com/gopherchat/auth-service/proto";
service AuthService {
// 用户登录
rpc Login(LoginRequest) returns (LoginResponse);
// 验证令牌
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
// 刷新令牌
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
// 用户登出
rpc Logout(LogoutRequest) returns (LogoutResponse);
}
message LoginRequest {
string username = 1;
string password = 2;
}
message LoginResponse {
string access_token = 1;
string refresh_token = 2;
string token_type = 3;
int64 expires_in = 4;
string user_id = 5;
}
message ValidateTokenRequest {
string token = 1;
}
message ValidateTokenResponse {
bool valid = 1;
string user_id = 2;
repeated string permissions = 3;
}
message RefreshTokenRequest {
string refresh_token = 1;
}
message RefreshTokenResponse {
string access_token = 1;
string refresh_token = 2;
string token_type = 3;
int64 expires_in = 4;
}
message LogoutRequest {
string user_id = 1;
string refresh_token = 2;
}
message LogoutResponse {
bool success = 1;
}
3.2.2 认证服务核心实现
认证服务结构体:
type AuthService struct {
userClient proto.UserServiceClient
tokenMaker token.Maker
passwordComp utils.PasswordComparer
sessionStore session.Store
proto.UnimplementedAuthServiceServer
}
登录方法:
func (s *AuthService) Login(ctx context.Context, req *proto.LoginRequest) (*proto.LoginResponse, error) {
// 验证请求
if req.Username == "" || req.Password == "" {
return nil, status.Error(codes.InvalidArgument, "username and password required")
}
// 通过用户名获取用户
user, err := s.userClient.GetUserByUsername(ctx, &userpb.GetUserByUsernameRequest{
Username: req.Username,
})
if err != nil {
st, ok := status.FromError(err)
if ok && st.Code() == codes.NotFound {
return nil, status.Error(codes.NotFound, "user not found")
}
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
// 验证密码
valid, err := s.passwordComp.ComparePassword(req.Password, user.PasswordHash)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to compare password: %v", err)
}
if !valid {
return nil, status.Error(codes.Unauthenticated, "incorrect password")
}
// 生成访问令牌
accessTokenDuration := 15 * time.Minute
accessToken, accessPayload, err := s.tokenMaker.CreateToken(
user.Id,
accessTokenDuration,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create access token: %v", err)
}
// 生成刷新令牌
refreshTokenDuration := 7 * 24 * time.Hour
refreshToken, refreshPayload, err := s.tokenMaker.CreateToken(
user.Id,
refreshTokenDuration,
)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create refresh token: %v", err)
}
// 保存会话到Redis
session := &session.Session{
ID: refreshPayload.ID.String(),
UserID: user.Id,
RefreshToken: refreshToken,
UserAgent: metadata.GetUserAgent(ctx),
ClientIP: metadata.GetClientIP(ctx),
ExpiresAt: refreshPayload.ExpiredAt,
CreatedAt: time.Now(),
}
if err := s.sessionStore.Create(ctx, session); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create session: %v", err)
}
// 返回登录响应
res := &proto.LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: int64(accessTokenDuration.Seconds()),
UserId: user.Id,
}
return res, nil
}
验证令牌方法:
func (s *AuthService) ValidateToken(ctx context.Context, req *proto.ValidateTokenRequest) (*proto.ValidateTokenResponse, error) {
// 验证令牌
payload, err := s.tokenMaker.VerifyToken(req.Token)
if err != nil {
return &proto.ValidateTokenResponse{
Valid: false,
UserId: "",
}, nil
}
// 返回验证结果
return &proto.ValidateTokenResponse{
Valid: true,
UserId: payload.UserID,
Permissions: []string{}, // 这里可以从用户服务获取权限
}, nil
}
3.3 聊天服务实现
聊天服务是应用的核心,负责管理会话和消息。
3.3.1 服务接口定义
syntax = "proto3";
package chat;
option go_package = "github.com/gopherchat/chat-service/proto";
service ChatService {
// 创建会话
rpc CreateConversation(CreateConversationRequest) returns (ConversationResponse);
// 获取会话
rpc GetConversation(GetConversationRequest) returns (ConversationResponse);
// 获取用户的会话列表
rpc ListConversations(ListConversationsRequest) returns (ListConversationsResponse);
// 发送消息
rpc SendMessage(SendMessageRequest) returns (MessageResponse);
// 获取消息
rpc GetMessage(GetMessageRequest) returns (MessageResponse);
// 获取会话的消息列表
rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse);
// 标记消息为已读
rpc MarkMessageAsRead(MarkMessageAsReadRequest) returns (MarkMessageAsReadResponse);
}
// 消息定义省略,以减少篇幅
3.3.2 聊天服务核心实现
发送消息方法:
func (s *ChatService) SendMessage(ctx context.Context, req *proto.SendMessageRequest) (*proto.MessageResponse, error) {
// 验证请求
if req.ConversationId == "" || req.SenderId == "" || req.Content == "" {
return nil, status.Error(codes.InvalidArgument, "missing required fields")
}
// 检查会话是否存在
conversation, err := s.repo.GetConversation(ctx, req.ConversationId)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, status.Error(codes.NotFound, "conversation not found")
}
return nil, status.Errorf(codes.Internal, "failed to get conversation: %v", err)
}
// 检查发送者是否是会话参与者
isParticipant, err := s.repo.IsParticipant(ctx, req.ConversationId, req.SenderId)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to check participant: %v", err)
}
if !isParticipant {
return nil, status.Error(codes.PermissionDenied, "sender is not a participant of the conversation")
}
// 创建消息
now := time.Now()
message := &model.Message{
ID: uuid.New().String(),
ConversationID: req.ConversationId,
SenderID: req.SenderId,
Type: req.Type,
Content: req.Content,
SentAt: now,
}
// 保存消息
if err := s.repo.CreateMessage(ctx, message); err != nil {
return nil, status.Errorf(codes.Internal, "failed to create message: %v", err)
}
// 更新会话的最后活动时间
if err := s.repo.UpdateConversationLastActivity(ctx, req.ConversationId, now); err != nil {
log.Printf("Failed to update conversation last activity: %v", err)
}
// 发布消息事件
event := &events.MessageCreatedEvent{
ID: message.ID,
ConversationID: message.ConversationID,
SenderID: message.SenderID,
Type: message.Type,
Content: message.Content,
SentAt: message.SentAt,
}
if err := s.eventPub.Publish(ctx, "message.created", event); err != nil {
log.Printf("Failed to publish message.created event: %v", err)
}
// 获取对话参与者
participants, err := s.repo.GetConversationParticipants(ctx, req.ConversationId)
if err != nil {
log.Printf("Failed to get conversation participants: %v", err)
} else {
// 发送消息通知给所有参与者(除了发送者)
for _, participant := range participants {
if participant.UserID != req.SenderId {
notification := &events.MessageNotificationEvent{
UserID: participant.UserID,
MessageID: message.ID,
ConversationID: message.ConversationID,
SenderID: message.SenderID,
}
if err := s.eventPub.Publish(ctx, "message.notification", notification); err != nil {
log.Printf("Failed to publish message.notification event: %v", err)
}
}
}
}
// 返回消息响应
return messageToProto(message), nil
}
3.4 推送服务实现
推送服务负责维护与客户端的WebSocket连接,实时推送消息和状态更新。
3.4.1 WebSocket处理器
type WebSocketHandler struct {
upgrader websocket.Upgrader
hub *Hub
authClient proto.AuthServiceClient
eventSub events.Subscriber
connections map[string]*Connection
mu sync.RWMutex
}
func NewWebSocketHandler(authClient proto.AuthServiceClient, eventSub events.Subscriber) *WebSocketHandler {
h := &WebSocketHandler{
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// TODO: 实际环境中应该检查Origin
return true
},
},
hub: NewHub(),
authClient: authClient,
eventSub: eventSub,
connections: make(map[string]*Connection),
}
// 订阅消息通知事件
h.subscribeToEvents()
return h
}
func (h *WebSocketHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
// 从请求中获取令牌
token := extractToken(r)
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 验证令牌
resp, err := h.authClient.ValidateToken(r.Context(), &proto.ValidateTokenRequest{
Token: token,
})
if err != nil || !resp.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
userID := resp.UserId
// 升级HTTP连接到WebSocket
conn, err := h.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Failed to upgrade connection: %v", err)
return
}
// 创建新的连接对象
connection := NewConnection(conn, userID, h.hub)
// 存储连接
h.mu.Lock()
h.connections[userID] = connection
h.mu.Unlock()
// 发布用户在线事件
event := &events.UserStatusChangedEvent{
UserID: userID,
Status: "online",
Time: time.Now(),
}
if err := h.eventSub.Publish(r.Context(), "user.status.changed", event); err != nil {
log.Printf("Failed to publish user online event: %v", err)
}
// 启动goroutine处理连接
go connection.readPump()
go connection.writePump()
}
func (h *WebSocketHandler) subscribeToEvents() {
// 订阅消息通知事件
h.eventSub.Subscribe("message.notification", func(e events.Event) {
notification, ok := e.(*events.MessageNotificationEvent)
if !ok {
return
}
// 查找用户连接
h.mu.RLock()
connection, exists := h.connections[notification.UserID]
h.mu.RUnlock()
if exists {
// 发送消息到WebSocket
connection.Send(&WebSocketMessage{
Type: "new_message",
Data: map[string]interface{}{
"message_id": notification.MessageID,
"conversation_id": notification.ConversationID,
"sender_id": notification.SenderID,
},
})
}
})
// 订阅用户状态变更事件
h.eventSub.Subscribe("user.status.changed", func(e events.Event) {
statusEvent, ok := e.(*events.UserStatusChangedEvent)
if !ok {
return
}
// 广播用户状态变更消息
h.hub.Broadcast(&WebSocketMessage{
Type: "user_status_changed",
Data: map[string]interface{}{
"user_id": statusEvent.UserID,
"status": statusEvent.Status,
"time": statusEvent.Time,
},
})
})
}
3.5 API网关实现
API网关是整个系统的统一接入点,负责请求路由、协议转换和基础认证。
3.5.1 网关核心功能
type APIGateway struct {
router *mux.Router
userClient proto.UserServiceClient
authClient proto.AuthServiceClient
chatClient proto.ChatServiceClient
wsHandler *WebSocketHandler
jwtAuthenticator middleware.JWTAuthenticator
}
func NewAPIGateway(
userClient proto.UserServiceClient,
authClient proto.AuthServiceClient,
chatClient proto.ChatServiceClient,
wsHandler *WebSocketHandler,
) *APIGateway {
gateway := &APIGateway{
router: mux.NewRouter(),
userClient: userClient,
authClient: authClient,
chatClient: chatClient,
wsHandler: wsHandler,
jwtAuthenticator: middleware.NewJWTAuthenticator(authClient),
}
gateway.setupRoutes()
return gateway
}
func (g *APIGateway) setupRoutes() {
// 公共路由
g.router.HandleFunc("/api/health", g.healthCheckHandler).Methods("GET")
// 用户认证相关
g.router.HandleFunc("/api/auth/register", g.registerHandler).Methods("POST")
g.router.HandleFunc("/api/auth/login", g.loginHandler).Methods("POST")
g.router.HandleFunc("/api/auth/refresh", g.refreshTokenHandler).Methods("POST")
// 需要认证的API
secured := g.router.PathPrefix("/api").Subrouter()
secured.Use(g.jwtAuthenticator.Middleware)
// 用户相关API
secured.HandleFunc("/users/me", g.getCurrentUserHandler).Methods("GET")
secured.HandleFunc("/users/{id}", g.getUserHandler).Methods("GET")
secured.HandleFunc("/users", g.searchUsersHandler).Methods("GET")
secured.HandleFunc("/users/me", g.updateUserHandler).Methods("PATCH")
// 聊天相关API
secured.HandleFunc("/conversations", g.createConversationHandler).Methods("POST")
secured.HandleFunc("/conversations", g.listConversationsHandler).Methods("GET")
secured.HandleFunc("/conversations/{id}", g.getConversationHandler).Methods("GET")
secured.HandleFunc("/conversations/{id}/messages", g.sendMessageHandler).Methods("POST")
secured.HandleFunc("/conversations/{id}/messages", g.listMessagesHandler).Methods("GET")
secured.HandleFunc("/messages/{id}/read", g.markMessageAsReadHandler).Methods("POST")
// WebSocket连接
secured.HandleFunc("/ws", g.wsHandler.HandleWebSocket)
}
// 示例处理器:用户登录
func (g *APIGateway) loginHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 调用认证服务
resp, err := g.authClient.Login(r.Context(), &authpb.LoginRequest{
Username: req.Username,
Password: req.Password,
})
if err != nil {
st, ok := status.FromError(err)
if !ok {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
switch st.Code() {
case codes.NotFound, codes.Unauthenticated:
http.Error(w, st.Message(), http.StatusUnauthorized)
case codes.InvalidArgument:
http.Error(w, st.Message(), http.StatusBadRequest)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// 返回登录结果
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"access_token": resp.AccessToken,
"refresh_token": resp.RefreshToken,
"token_type": resp.TokenType,
"expires_in": resp.ExpiresIn,
"user_id": resp.UserId,
})
}
// 示例处理器:发送消息
func (g *APIGateway) sendMessageHandler(w http.ResponseWriter, r *http.Request) {
// 从路径参数获取会话ID
vars := mux.Vars(r)
conversationID := vars["id"]
// 从上下文中获取用户ID
userID := r.Context().Value(middleware.UserIDKey).(string)
var req struct {
Content string `json:"content"`
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 调用聊天服务
resp, err := g.chatClient.SendMessage(r.Context(), &chatpb.SendMessageRequest{
ConversationId: conversationID,
SenderId: userID,
Content: req.Content,
Type: req.Type,
})
if err != nil {
st, ok := status.FromError(err)
if !ok {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
switch st.Code() {
case codes.NotFound:
http.Error(w, st.Message(), http.StatusNotFound)
case codes.PermissionDenied:
http.Error(w, st.Message(), http.StatusForbidden)
case codes.InvalidArgument:
http.Error(w, st.Message(), http.StatusBadRequest)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// 返回消息
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
### 3.6 消息队列集成
我们使用NATS作为消息队列,处理微服务间的事件和通知。
#### 3.6.1 事件发布者实现
```go
type NATSPublisher struct {
conn *nats.Conn
}
func NewNATSPublisher(url string) (*NATSPublisher, error) {
conn, err := nats.Connect(url)
if err != nil {
return nil, err
}
return &NATSPublisher{conn: conn}, nil
}
func (p *NATSPublisher) Publish(ctx context.Context, topic string, event interface{}) error {
data, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("failed to marshal event: %w", err)
}
return p.conn.Publish(topic, data)
}
func (p *NATSPublisher) Close() error {
p.conn.Close()
return nil
}
3.6.2 事件订阅者实现
type NATSSubscriber struct {
conn *nats.Conn
subscriptions []*nats.Subscription
mu sync.Mutex
}
func NewNATSSubscriber(url string) (*NATSSubscriber, error) {
conn, err := nats.Connect(url)
if err != nil {
return nil, err
}
return &NATSSubscriber{
conn: conn,
subscriptions: make([]*nats.Subscription, 0),
}, nil
}
func (s *NATSSubscriber) Subscribe(topic string, handler func(events.Event)) error {
subscription, err := s.conn.Subscribe(topic, func(msg *nats.Msg) {
// 解析事件
var eventData map[string]interface{}
if err := json.Unmarshal(msg.Data, &eventData); err != nil {
log.Printf("Failed to unmarshal event: %v", err)
return
}
// 根据主题类型创建不同的事件对象
var event events.Event
switch topic {
case "user.created":
event = &events.UserCreatedEvent{}
case "message.created":
event = &events.MessageCreatedEvent{}
case "message.notification":
event = &events.MessageNotificationEvent{}
case "user.status.changed":
event = &events.UserStatusChangedEvent{}
default:
log.Printf("Unknown event topic: %s", topic)
return
}
// 解析具体事件
data, err := json.Marshal(eventData)
if err != nil {
log.Printf("Failed to re-marshal event data: %v", err)
return
}
if err := json.Unmarshal(data, event); err != nil {
log.Printf("Failed to unmarshal into specific event: %v", err)
return
}
// 调用处理器
handler(event)
})
if err != nil {
return err
}
s.mu.Lock()
s.subscriptions = append(s.subscriptions, subscription)
s.mu.Unlock()
return nil
}
func (s *NATSSubscriber) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
for _, sub := range s.subscriptions {
if err := sub.Unsubscribe(); err != nil {
log.Printf("Failed to unsubscribe: %v", err)
}
}
s.conn.Close()
return nil
}
4. 部署与运维
4.1 Docker容器化
为每个微服务创建Dockerfile:
# 构建阶段
FROM golang:1.19-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main ./cmd/service
# 运行阶段
FROM alpine:3.15
WORKDIR /app
COPY --from=builder /app/main .
COPY config.yaml .
EXPOSE 8080
CMD ["./main"]
4.2 Docker Compose配置
使用Docker Compose简化本地开发和测试:
version: '3'
services:
# 数据库
postgres:
image: postgres:14-alpine
environment:
POSTGRES_USER: gopherchat
POSTGRES_PASSWORD: password
POSTGRES_DB: gopherchat
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
# Redis
redis:
image: redis:6-alpine
ports:
- "6379:6379"
# NATS
nats:
image: nats:2.7-alpine
ports:
- "4222:4222"
- "8222:8222"
# 用户服务
user-service:
build:
context: ./user-service
dockerfile: Dockerfile
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: gopherchat
DB_PASSWORD: password
DB_NAME: gopherchat
NATS_URL: nats://nats:4222
depends_on:
- postgres
- nats
# 认证服务
auth-service:
build:
context: ./auth-service
dockerfile: Dockerfile
environment:
USER_SERVICE_URL: user-service:50051
REDIS_URL: redis:6379
JWT_SECRET: your-secret-key
JWT_ISSUER: gopherchat
depends_on:
- redis
- user-service
# 聊天服务
chat-service:
build:
context: ./chat-service
dockerfile: Dockerfile
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: gopherchat
DB_PASSWORD: password
DB_NAME: gopherchat
NATS_URL: nats://nats:4222
USER_SERVICE_URL: user-service:50051
depends_on:
- postgres
- nats
- user-service
# 推送服务
push-service:
build:
context: ./push-service
dockerfile: Dockerfile
environment:
AUTH_SERVICE_URL: auth-service:50051
NATS_URL: nats://nats:4222
depends_on:
- nats
- auth-service
# API网关
api-gateway:
build:
context: ./api-gateway
dockerfile: Dockerfile
environment:
USER_SERVICE_URL: user-service:50051
AUTH_SERVICE_URL: auth-service:50051
CHAT_SERVICE_URL: chat-service:50051
PUSH_SERVICE_URL: push-service:50051
ports:
- "8080:8080"
depends_on:
- user-service
- auth-service
- chat-service
- push-service
volumes:
postgres_data:
4.3 监控与可观测性
集成Prometheus、Grafana和Jaeger进行监控和追踪:
# 添加到docker-compose.yml
services:
# 省略其他服务...
# Prometheus
prometheus:
image: prom/prometheus:v2.37.0
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
# Grafana
grafana:
image: grafana/grafana:9.1.0
depends_on:
- prometheus
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
# Jaeger
jaeger:
image: jaegertracing/all-in-one:1.36
ports:
- "16686:16686" # UI
- "14268:14268" # 收集器
- "6831:6831/udp" # 代理
volumes:
grafana_data:
5. 系统测试与性能优化
5.1 压力测试
使用K6等工具对聊天应用进行压力测试,确保系统在高负载下的性能和稳定性:
// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '30s', target: 100 }, // 逐步增加到100用户
{ duration: '1m', target: 100 }, // 保持100用户1分钟
{ duration: '30s', target: 0 }, // 逐步减少到0用户
],
};
export default function() {
// 登录
let loginRes = http.post('http://localhost:8080/api/auth/login', JSON.stringify({
username: 'testuser',
password: 'password',
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login successful': (r) => r.status === 200,
});
let token = JSON.parse(loginRes.body).access_token;
// 发送消息
let sendMessageRes = http.post(
'http://localhost:8080/api/conversations/123/messages',
JSON.stringify({
content: 'Hello, World!',
type: 'text',
}),
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
}
);
check(sendMessageRes, {
'message sent': (r) => r.status === 200,
});
sleep(1);
}
5.2 性能优化策略
-
连接池优化:
- 数据库连接池参数调整
- gRPC客户端连接复用
-
缓存策略:
- 使用Redis缓存频繁访问的数据
- 缓存用户信息和会话元数据
-
数据库优化:
- 索引优化
- 查询优化
- 分页加载消息历史
-
消息推送优化:
- WebSocket连接心跳机制
- 连接断开后的重连策略
- 消息队列批处理
总结
在这个完整的项目实战中,我们成功构建了一个功能丰富的微服务聊天应用,将我们在整个Go语言学习系列中学到的知识和技能整合在一起。通过这个项目,我们实践了微服务架构的设计与实现,应用了gRPC进行服务间通信,并结合了消息队列、缓存、数据库和前端技术,构建了一个完整的实时聊天系统。
在开发过程中,我们不仅关注功能实现,还注重代码质量、测试覆盖、性能优化和系统安全性。我们使用了Docker和Kubernetes进行容器化和编排,实现了系统的可伸缩部署。通过整合日志、监控和追踪系统,我们确保了应用的可观测性,便于问题排查和性能分析。
这个项目展示了Go语言在构建现代分布式系统中的强大能力。Go的简洁语法、强大的并发模型、丰富的标准库和活跃的开源生态,使其成为微服务开发的理想选择。
随着项目的完成,我们也结束了Go语言学习系列的第三阶段。在接下来的文章中,我们将进入性能优化专题,深入探讨如何使Go应用达到极致性能。
希望这个项目能够为你提供实践参考,帮助你将所学知识应用到实际开发中。记住,最好的学习方式就是动手实践,不断探索和改进。期待你能基于这个项目,发挥创意,构建更加丰富和强大的应用!
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,包括Go语言开发实战、Go语言底层原理、Go语言源码分析等。
为什么要关注我们:
- 系统化学习路径,从入门到进阶
- 实战为主的教学方式
- 持续更新的技术文章
- 活跃的技术交流社区
如何关注我们:
- 点击优快云专栏右上角的"关注"按钮
- 关注微信公众号:Gopher部落
读者福利:
关注公众号,回复"Go",即可获取:
- 完整的Go语言学习路线图
- Go语言面试题PDF
- 项目实战源码
- 专属学习规划指导
期待与您在Go语言的学习旅程中共同成长!
169

被折叠的 条评论
为什么被折叠?



