系列文章:Go 聊天室实战系列(6/6)
项目地址:https://gitee.com/gaogaohang/go-land_-learning
技术栈:Go 1.23 + Gin + PostgreSQL + Redis + Vue3
一、Docker 容器化
1.1 Dockerfile
# 构建阶段
FROM golang:1.23-alpine AS builder
WORKDIR /build
# 安装依赖
RUN apk add --no-cache git
# 配置 Go 代理(加速下载)
ENV GOPROXY=https://goproxy.cn,direct
# 复制依赖文件
COPY go.mod go.sum ./
RUN go mod download
# 复制源代码
COPY . .
# 编译(静态链接,支持 CGO)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s" \
-o /build/go-chat \
main.go
# 运行阶段
FROM alpine:latest
WORKDIR /app
# 安装 CA 证书(HTTPS 需要)
RUN apk --no-cache add ca-certificates tzdata
# 从构建阶段复制二进制文件
COPY --from=builder /build/go-chat /app/go-chat
COPY --from=builder /build/config.prod.yaml /app/config.yaml
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
# 启动服务
CMD ["/app/go-chat", "-config", "/app/config.yaml"]1.2 .dockerignore
# Git
.git
.gitignore
# 开发文件
*.md
.air.toml
Makefile
# 测试文件
*_test.go
coverage.out
# 前端(单独构建)
web/node_modules
web/dist
# 日志
*.log
logs/
# 临时文件
tmp/
bin/1.3 docker-compose.yml
version: '3.8'
services:
# 应用服务
app:
build:
context: .
dockerfile: Dockerfile
container_name: go-chat-app
ports:
- "8080:8080"
environment:
- CHAT_ENV=prod
- CHAT_DATABASE_HOST=postgres
- CHAT_REDIS_HOST=redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- ./logs:/app/logs
- ./uploads:/app/uploads
restart: unless-stopped
networks:
- chat-network
# PostgreSQL
postgres:
image: postgres:14-alpine
container_name: go-chat-db
environment:
- POSTGRES_USER=chatuser
- POSTGRES_PASSWORD=chatpass123
- POSTGRES_DB=chatdb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U chatuser -d chatdb"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- chat-network
# Redis
redis:
image: redis:7-alpine
container_name: go-chat-redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- chat-network
# Nginx(反向代理 + 前端)
nginx:
image: nginx:alpine
container_name: go-chat-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./web/dist:/usr/share/nginx/html
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
networks:
- chat-network
volumes:
postgres_data:
redis_data:
networks:
chat-network:
driver: bridge二、生产环境配置
2.1 config.prod.yaml
server:
port: 8080
mode: release # release 模式(无 debug 日志)
read_timeout: 30s
write_timeout: 30s
database:
host: ${CHAT_DATABASE_HOST:localhost}
port: 5432
user: chatuser
password: ${CHAT_DATABASE_PASSWORD:chatpass123}
db_name: chatdb
max_open_conns: 100
max_idle_conns: 20
conn_max_lifetime: 1h
redis:
host: ${CHAT_REDIS_HOST:localhost}
port: 6379
password: ${CHAT_REDIS_PASSWORD:}
db: 0
pool_size: 50
jwt:
secret: ${CHAT_JWT_SECRET:change-this-in-production}
expire_hour: 720 # 30 天
log:
level: warn # 生产环境只记录警告及以上
output: file
file_path: /app/logs/app.log
max_size: 100 # MB
max_backups: 5
max_age: 30 # 天
upload:
path: /app/uploads
max_size: 10485760 # 10MB
allowed_types:
- image/jpeg
- image/png
- image/gif2.2 环境变量管理
# .env.production
CHAT_ENV=prod
CHAT_DATABASE_HOST=192.168.1.100
CHAT_DATABASE_PASSWORD=strong_password_here
CHAT_REDIS_HOST=192.168.1.101
CHAT_JWT_SECRET=your-super-secret-key-min-32-chars
CHAT_SERVER_PORT=9090三、性能优化
3.1 数据库连接池
// internal/database/postgres.go
func InitPostgres() {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
config.DB.Host, config.DB.Port, config.DB.User, config.DB.Password, config.DB.DBName)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
logger.Fatalf("数据库连接失败:%v", err)
}
sqlDB, err := db.DB()
if err != nil {
logger.Fatalf("获取 SQL DB 失败:%v", err)
}
// 连接池配置
sqlDB.SetMaxOpenConns(config.DB.MaxOpenConns) // 最大连接数
sqlDB.SetMaxIdleConns(config.DB.MaxIdleConns) // 空闲连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
// 验证连接
if err := sqlDB.Ping(); err != nil {
logger.Fatalf("数据库 Ping 失败:%v", err)
}
logger.Info("数据库连接成功")
}3.2 Redis 缓存策略
// internal/service/cache.go
package service
import (
"context"
"encoding/json"
"time"
)
// CacheService 缓存服务
type CacheService struct {
redis *redis.Client
}
// GetUserProfile 获取用户资料(带缓存)
func (s *CacheService) GetUserProfile(ctx context.Context, userID int64) (*model.PublicUser, error) {
key := fmt.Sprintf("user:profile:%d", userID)
// 1. 尝试从缓存获取
data, err := s.redis.Get(ctx, key).Bytes()
if err == nil {
var user model.PublicUser
json.Unmarshal(data, &user)
return &user, nil
}
// 2. 从数据库获取
user, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, err
}
// 3. 写入缓存(5 分钟过期)
data, _ = json.Marshal(user.ToPublic())
s.redis.Set(ctx, key, data, 5*time.Minute)
return user.ToPublic(), nil
}
// InvalidateUserCache 失效用户缓存
func (s *CacheService) InvalidateUserCache(ctx context.Context, userID int64) {
key := fmt.Sprintf("user:profile:%d", userID)
s.redis.Del(ctx, key)
}3.3 消息队列(可选)
// 使用 Redis Stream 作为消息队列
// internal/queue/message_queue.go
package queue
type MessageQueue struct {
redis *redis.Client
streamKey string
}
// Enqueue 入队
func (q *MessageQueue) Enqueue(ctx context.Context, msg *websocket.Message) error {
data, _ := json.Marshal(msg)
return q.redis.XAdd(ctx, &redis.XAddArgs{
Stream: q.streamKey,
Values: map[string]interface{}{
"data": data,
"ts": time.Now().UnixMilli(),
},
}).Err()
}
// Dequeue 出队(阻塞)
func (q *MessageQueue) Dequeue(ctx context.Context) (*websocket.Message, error) {
result, err := q.redis.XRead(ctx, &redis.XReadArgs{
Streams: []string{q.streamKey, "0"},
Count: 1,
Block: 5 * time.Second,
}).Result()
if err != nil {
return nil, err
}
var msg websocket.Message
json.Unmarshal([]byte(result[0].Messages[0].Values["data"].(string)), &msg)
// 确认消费
q.redis.XDel(ctx, q.streamKey, result[0].Messages[0].ID)
return &msg, nil
}四、监控与日志
4.1 Prometheus 监控
// internal/middleware/prometheus.go
package middleware
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/gin-gonic/gin"
"time"
)
var (
httpRequests = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "path", "status"},
)
httpDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
wsConnections = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "websocket_connections",
Help: "Current WebSocket connections",
},
)
)
// Prometheus Prometheus 监控中间件
func Prometheus() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
// 记录指标
duration := time.Since(start).Seconds()
httpRequests.WithLabelValues(c.Request.Method, c.FullPath(), string(c.Writer.Status())).Inc()
httpDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration)
}
}4.2 Grafana 仪表盘
{
"dashboard": {
"title": "Go Chat 监控",
"panels": [
{
"title": "QPS",
"targets": [
{
"expr": "rate(http_requests_total[1m])"
}
]
},
{
"title": "响应时间 P99",
"targets": [
{
"expr": "histogram_quantile(0.99, http_request_duration_seconds_bucket)"
}
]
},
{
"title": "WebSocket 连接数",
"targets": [
{
"expr": "websocket_connections"
}
]
},
{
"title": "数据库连接池",
"targets": [
{
"expr": "go_goroutines"
}
]
}
]
}
}4.3 日志收集(ELK)
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /app/logs/*.log
json.keys_under_root: true
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "go-chat-%{+yyyy.MM.dd}"五、CI/CD
5.1 GitHub Actions
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/postgres?sslmode=disable
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t go-chat:${{ github.sha }} .
- name: Push to registry
run: |
docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
docker push go-chat:${{ github.sha }}
- name: Deploy
run: |
ssh ${{ secrets.DEPLOY_HOST }} << 'EOF'
cd /opt/go-chat
docker-compose pull
docker-compose up -d
EOF六、安全加固
6.1 SQL 注入防护
// 正确:使用参数化查询
db.Where("username = ?", username).First(&user)
// 错误:字符串拼接
db.Where(fmt.Sprintf("username = '%s'", username)).First(&user)6.2 XSS 防护
// 前端转义
import { escape } from 'lodash'
const safeContent = escape(userInput)
// 后端设置 CSP 头
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Content-Security-Policy", "default-src 'self'")
c.Next()
}
}6.3 限流熔断
// internal/middleware/ratelimit.go
package middleware
import (
"golang.org/x/time/rate"
"sync"
)
var (
visitors = make(map[string]*rate.Limiter)
mu sync.Mutex
)
// RateLimit 限流中间件
func RateLimit(maxRequests int, burst int) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
mu.Lock()
limiter, exists := visitors[ip]
if !exists {
limiter = rate.NewLimiter(rate.Limit(maxRequests), burst)
visitors[ip] = limiter
}
mu.Unlock()
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "请求过于频繁,请稍后再试",
})
c.Abort()
return
}
c.Next()
}
}七、部署脚本
7.1 deploy.sh
#!/bin/bash
set -e
echo "🚀 开始部署..."
# 1. 拉取最新代码
git pull origin main
# 2. 构建前端
cd web
npm install
npm run build
cd ..
# 3. 构建 Docker 镜像
docker-compose build
# 4. 运行数据库迁移
docker-compose run --rm app ./go-chat migrate
# 5. 重启服务
docker-compose up -d
# 6. 检查健康状态
sleep 5
curl -f http://localhost:8080/health || exit 1
echo "部署完成!"7.2 备份脚本
#!/bin/bash
# backup.sh - 数据库备份
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
# PostgreSQL 备份
docker exec go-chat-db pg_dump -U chatuser chatdb > ${BACKUP_DIR}/db_${DATE}.sql
# Redis 备份
docker exec go-chat-redis redis-cli BGSAVE
cp /var/lib/docker/volumes/chat_redis_data/_data/dump.rdb ${BACKUP_DIR}/redis_${DATE}.rdb
# 压缩
tar -czf ${BACKUP_DIR}/backup_${DATE}.tar.gz ${BACKUP_DIR}/db_${DATE}.sql ${BACKUP_DIR}/redis_${DATE}.rdb
# 清理 7 天前的备份
find ${BACKUP_DIR} -name "backup_*.tar.gz" -mtime +7 -delete
echo "备份完成:backup_${DATE}.tar.gz"八、总结
本系列完成了:
项目架构设计
JWT 认证与中间件
用户与好友系统
WebSocket 实时通信
私聊与群聊功能
部署与优化