Go语言实战指南 程序员如何从零搭建高并发Web服务 包含数据库连接池配置API接口设计部署上线全流程案例解析
为什么选择Go来写高并发服务
说实话,如果你还在用PHP或者Python硬扛高并发,那可能真的需要换个思路了。Go语言之所以在云原生和微服务领域这么火,核心就两点:goroutine轻量和原生支持并发。
想象一下,你在餐厅点了一百道菜,如果用传统方式,可能得一个个做。但Go的方式是:每个厨师一个线程(goroutine),每个线程只处理一道菜,而且线程之间切换的开销极小,几乎可以忽略不计。这就是为什么Go处理并发请求时这么爽。
项目结构搭建
先别急着写代码,好的项目结构是成功的一半。我们来看看这个项目的目录结构:
go-web-service/
├── cmd/
│ └── server/
│ └── main.go # 入口文件
├── internal/
│ ├── config/
│ │ └── config.go # 配置加载
│ ├── database/
│ │ ├── pool.go # 数据库连接池
│ │ └── migration.go # 数据库迁移
│ ├── handler/
│ │ ├── user_handler.go # 用户接口处理器
│ │ └── order_handler.go # 订单接口处理器
│ ├── model/
│ │ ├── user.go # 用户模型
│ │ └── order.go # 订单模型
│ ├── middleware/
│ │ ├── logger.go # 请求日志中间件
│ │ └── auth.go # 认证中间件
│ ├── repository/
│ │ ├── user_repo.go # 用户数据访问层
│ │ └── order_repo.go # 订单数据访问层
│ └── service/
│ ├── user_service.go # 用户业务逻辑
│ └── order_service.go # 订单业务逻辑
├── pkg/
│ ├── response/
│ │ └── response.go # 统一响应格式
│ └── validator/
│ └── validator.go # 请求参数验证
├── migrations/
│ ├── 001_create_users.sql
│ └── 002_create_orders.sql
├── config/
│ └── config.yaml # 配置文件
├── scripts/
│ ├── deploy.sh # 部署脚本
│ └── health_check.sh # 健康检查脚本
├── Dockerfile
├── docker-compose.yml
└── Makefile
这个结构遵循了Go官方的推荐规范,把所有和业务相关的代码都放在 internal 下面,确保外部无法直接访问,这样代码的组织性更好,也更安全。
配置管理
好的服务离不开灵活可配置的设计。我们使用 viper 来管理配置,这样可以支持从配置文件、环境变量、命令行参数等多个来源读取配置。
// internal/config/config.go
package config
import (
"fmt"
"os"
"time"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
JWT JWTConfig
Log LogConfig
}
type ServerConfig struct {
Port string `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
MaxHeaderBytes int `mapstructure:"max_header_bytes"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
}
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireTime time.Duration `mapstructure:"expire_time"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
File string `mapstructure:"file"`
}
func LoadConfig(configPath string) (*Config, error) {
v := viper.New()
// 配置文件路径
v.SetConfigFile(configPath)
// 环境变量前缀
v.SetEnvPrefix("APP")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 读取配置文件
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &cfg, nil
}
配置文件 config.yaml 的内容:
server:
port: "8080"
read_timeout: 15s
write_timeout: 15s
shutdown_timeout: 5s
max_header_bytes: 1 << 20
database:
host: "localhost"
port: "5432"
user: "postgres"
password: "your_password"
dbname: "web_service"
max_open_conns: 100
max_idle_conns: 10
conn_max_lifetime: 5m
conn_max_idle_time: 30s
redis:
addr: "localhost:6379"
password: ""
db: 0
jwt:
secret: "your-super-secret-key-change-in-production"
expire_time: 24h
log:
level: "info"
format: "json"
file: "logs/app.log"
数据库连接池配置
数据库连接池是高并发服务的核心组件之一。配置不当会导致连接泄漏、性能下降甚至服务崩溃。我们用 pgx 作为 PostgreSQL 的驱动,它比 lib/pq 性能更好,而且支持更现代的 Go 特性。
// internal/database/pool.go
package database
import (
"context"
"fmt"
"log"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"go-web-service/internal/config"
)
type Pool struct {
conn *pgxpool.Pool
}
func NewPool(cfg config.DatabaseConfig) (*Pool, error) {
dsn := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName,
)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
connConfig, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("failed to parse database config: %w", err)
}
// 配置连接池参数
connConfig.MaxConns = int32(cfg.MaxOpenConns)
connConfig.MinConns = int32(cfg.MaxIdleConns)
connConfig.MaxConnLifetime = cfg.ConnMaxLifetime
connConfig.MaxConnIdleTime = cfg.ConnMaxIdleTime
// 连接池重试机制
connConfig.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
// 连接建立后的初始化
return nil
}
connConfig.BeforeRelease = func(conn *pgx.Conn) bool {
// 连接释放前的检查
return true
}
pool, err := pgxpool.NewWithConfig(ctx, connConfig)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// 健康检查
go startHealthCheck(pool, 30*time.Second)
log.Printf("Database connection pool created successfully. MaxConns: %d, MinConns: %d",
cfg.MaxOpenConns, cfg.MaxIdleConns)
return &Pool{conn: pool}, nil
}
func (p *Pool) Get() *pgxpool.Pool {
return p.conn
}
func (p *Pool) Close() {
if p.conn != nil {
p.conn.Close()
}
}
func startHealthCheck(pool *pgxpool.Pool, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := pool.Ping(ctx); err != nil {
log.Printf("Database health check failed: %v", err)
}
cancel()
// 打印连接池状态
stats := pool.Stat()
log.Printf("DB Pool Stats - Open: %d, Idle: %d, Waiting: %d",
stats.OpenConnections(), stats.Idle(), stats.WaitCount())
}
}
为什么要配置连接池的这些参数呢?MaxOpenConns 控制最大连接数,防止数据库被过多连接打垮;MaxIdleConns 保持最小空闲连接,避免频繁创建连接的开销;ConnMaxLifetime 设置连接最大生命周期,防止数据库服务端超时断开连接;ConnMaxIdleTime 让空闲连接及时释放,节省资源。
路由设计与API接口
好的API设计不仅仅是写出能跑的接口,更要考虑RESTful规范、版本控制、错误处理和性能优化。
// internal/handler/user_handler.go
package handler
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"go-web-service/internal/middleware"
"go-web-service/internal/service"
"go-web-service/pkg/response"
"go-web-service/pkg/validator"
)
type UserHandler struct {
userService service.UserService
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Register 用户注册
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username" validate:"required,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8,max=64"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
return
}
if err := validator.Validate(req); err != nil {
response.Error(w, http.StatusBadRequest, err.Error(), "VALIDATION_ERROR")
return
}
user, err := h.userService.Register(r.Context(), req.Username, req.Email, req.Password)
if err != nil {
if strings.Contains(err.Error(), "duplicate") {
response.Error(w, http.StatusConflict, err.Error(), "DUPLICATE_USER")
return
}
response.InternalError(w, err)
return
}
response.Success(w, http.StatusCreated, user)
}
// Login 用户登录
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
return
}
token, user, err := h.userService.Login(r.Context(), req.Email, req.Password)
if err != nil {
response.Error(w, http.StatusUnauthorized, "invalid credentials", "INVALID_CREDENTIALS")
return
}
resp := map[string]interface{}{
"token": token,
"user": user,
"expires_at": time.Now().Add(24 * time.Hour).Format(time.RFC3339),
}
response.Success(w, http.StatusOK, resp)
}
// GetUser 获取用户信息
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid user id", "INVALID_ID")
return
}
user, err := h.userService.GetUserByID(r.Context(), id)
if err != nil {
response.NotFound(w, "user not found")
return
}
response.Success(w, http.StatusOK, user)
}
// ListUsers 分页获取用户列表
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit < 1 || limit > 100 {
limit = 20
}
search := r.URL.Query().Get("search")
sortBy := r.URL.Query().Get("sort_by")
sortOrder := r.URL.Query().Get("sort_order")
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
users, total, err := h.userService.ListUsers(r.Context(), page, limit, search, sortBy, sortOrder)
if err != nil {
response.InternalError(w, err)
return
}
response.PaginatedSuccess(w, http.StatusOK, users, total, page, limit)
}
// UpdateUser 更新用户信息
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid user id", "INVALID_ID")
return
}
var req struct {
Username string `json:"username" validate:"omitempty,min=3,max=32"`
Email string `json:"email" validate:"omitempty,email"`
Avatar string `json:"avatar" validate:"omitempty,url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
return
}
if err := validator.Validate(req); err != nil {
response.Error(w, http.StatusBadRequest, err.Error(), "VALIDATION_ERROR")
return
}
user, err := h.userService.UpdateUser(r.Context(), id, req.Username, req.Email, req.Avatar)
if err != nil {
response.NotFound(w, "user not found")
return
}
response.Success(w, http.StatusOK, user)
}
// DeleteUser 删除用户
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid user id", "INVALID_ID")
return
}
if err := h.userService.DeleteUser(r.Context(), id); err != nil {
response.InternalError(w, err)
return
}
response.Success(w, http.StatusOK, map[string]string{"message": "user deleted successfully"})
}
// 注册路由
func (h *UserHandler) RegisterRoutes(r chi.Router) {
r.Post("/users/register", h.Register)
r.Post("/users/login", h.Login)
// 需要认证的路由
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware)
r.Get("/users/{id}", h.GetUser)
r.Get("/users", h.ListUsers)
r.Patch("/users/{id}", h.UpdateUser)
r.Delete("/users/{id}", h.DeleteUser)
})
}
这里有个小细节值得注意,我们在登录接口返回了 expires_at 时间,这样前端可以知道 token 何时过期,主动提示用户重新登录,而不是等到 token 失效后才发现问题。
再看订单处理器的设计:
// internal/handler/order_handler.go
package handler
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"go-web-service/internal/middleware"
"go-web-service/internal/service"
"go-web-service/pkg/response"
"go-web-service/pkg/validator"
)
type OrderHandler struct {
orderService service.OrderService
}
func NewOrderHandler(orderService service.OrderService) *OrderHandler {
return &OrderHandler{orderService: orderService}
}
// CreateOrder 创建订单
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req struct {
ProductID uint64 `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1,max=100"`
Note string `json:"note" validate:"omitempty,max=500"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
return
}
if err := validator.Validate(req); err != nil {
response.Error(w, http.StatusBadRequest, err.Error(), "VALIDATION_ERROR")
return
}
// 从 context 中获取用户ID
userId := middleware.GetUserID(r.Context())
order, err := h.orderService.CreateOrder(r.Context(), userId, req.ProductID, req.Quantity, req.Note)
if err != nil {
response.InternalError(w, err)
return
}
// 返回时隐藏敏感信息
response.Success(w, http.StatusCreated, order)
}
// GetOrder 获取订单详情
func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid order id", "INVALID_ID")
return
}
userId := middleware.GetUserID(r.Context())
order, err := h.orderService.GetOrder(r.Context(), id, userId)
if err != nil {
response.NotFound(w, "order not found")
return
}
response.Success(w, http.StatusOK, order)
}
// ListOrders 分页获取订单列表
func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
userId := middleware.GetUserID(r.Context())
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit < 1 || limit > 100 {
limit = 20
}
status := r.URL.Query().Get("status")
startDate := r.URL.Query().Get("start_date")
endDate := r.URL.Query().Get("end_date")
var start, end time.Time
if startDate != "" {
start, _ = time.Parse("2006-01-02", startDate)
}
if endDate != "" {
end, _ = time.Parse("2006-01-02", endDate)
}
orders, total, err := h.orderService.ListOrders(r.Context(), userId, page, limit, status, start, end)
if err != nil {
response.InternalError(w, err)
return
}
response.PaginatedSuccess(w, http.StatusOK, orders, total, page, limit)
}
// CancelOrder 取消订单
func (h *OrderHandler) CancelOrder(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.Error(w, http.StatusBadRequest, "invalid order id", "INVALID_ID")
return
}
userId := middleware.GetUserID(r.Context())
order, err := h.orderService.CancelOrder(r.Context(), id, userId)
if err != nil {
response.NotFound(w, err.Error())
return
}
response.Success(w, http.StatusOK, order)
}
func (h *OrderHandler) RegisterRoutes(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware)
r.Post("/orders", h.CreateOrder)
r.Get("/orders/{id}", h.GetOrder)
r.Get("/orders", h.ListOrders)
r.Post("/orders/{id}/cancel", h.CancelOrder)
})
}
业务逻辑层设计
服务层是业务逻辑的核心,这里需要处理好事务、缓存、错误处理等关键问题。
// internal/service/user_service.go
package service
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"go-web-service/internal/model"
"go-web-service/internal/repository"
)
type UserService interface {
Register(ctx context.Context, username, email, password string) (*model.User, error)
Login(ctx context.Context, email, password string) (string, *model.User, error)
GetUserByID(ctx context.Context, id uint64) (*model.User, error)
ListUsers(ctx context.Context, page, limit int, search, sortBy, sortOrder string) ([]*model.User, int64, error)
UpdateUser(ctx context.Context, id uint64, username, email, avatar string) (*model.User, error)
DeleteUser(ctx context.Context, id uint64) error
}
type userService struct {
userRepo repository.UserRepository
jwtService JWTService
}
func NewUserService(userRepo repository.UserRepository, jwtService JWTService) UserService {
return &userService{userRepo: userRepo, jwtService: jwtService}
}
func (s *userService) Register(ctx context.Context, username, email, password string) (*model.User, error) {
// 检查用户名是否已存在
existing, err := s.userRepo.GetByUsername(ctx, username)
if err != nil && !errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("failed to check username: %w", err)
}
if existing != nil {
return nil, errors.New("username already exists")
}
// 检查邮箱是否已存在
existing, err = s.userRepo.GetByEmail(ctx, email)
if err != nil && !errors.Is(err, repository.ErrNotFound) {
return nil, fmt.Errorf("failed to check email: %w", err)
}
if existing != nil {
return nil, errors.New("email already exists")
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
user := &model.User{
Username: username,
Email: email,
Password: string(hashedPassword),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
func (s *userService) Login(ctx context.Context, email, password string) (string, *model.User, error) {
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
return "", nil, errors.New("invalid credentials")
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return "", nil, errors.New("invalid credentials")
}
// 生成JWT
token, err := s.jwtService.GenerateToken(user.ID, user.Username)
if err != nil {
return "", nil, fmt.Errorf("failed to generate token: %w", err)
}
return token, user, nil
}
func (s *userService) GetUserByID(ctx context.Context, id uint64) (*model.User, error) {
return s.userRepo.GetByID(ctx, id)
}
func (s *userService) ListUsers(ctx context.Context, page, limit int, search, sortBy, sortOrder string) ([]*model.User, int64, error) {
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 20
}
return s.userRepo.List(ctx, page, limit, search, sortBy, sortOrder)
}
func (s *userService) UpdateUser(ctx context.Context, id uint64, username, email, avatar string) (*model.User, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if username != "" {
user.Username = username
}
if email != "" {
user.Email = email
}
if avatar != "" {
user.Avatar = avatar
}
user.UpdatedAt = time.Now()
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return user, nil
}
func (s *userService) DeleteUser(ctx context.Context, id uint64) error {
return s.userRepo.Delete(ctx, id)
}
数据访问层设计
仓储层负责与数据库交互,这里要处理好事务和并发安全。
// internal/repository/user_repo.go
package repository
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"go-web-service/internal/model"
)
var (
ErrNotFound = errors.New("record not found")
)
type UserRepository interface {
Create(ctx context.Context, user *model.User) error
GetByID(ctx context.Context, id uint64) (*model.User, error)
GetByUsername(ctx context.Context, username string) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error)
List(ctx context.Context, page, limit int, search, sortBy, sortOrder string) ([]*model.User, int64, error)
Update(ctx context.Context, user *model.User) error
Delete(ctx context.Context, id uint64) error
BatchCreate(ctx context.Context, users []*model.User) error
}
type userRepository struct {
pool *pgxpool.Pool
}
func NewUserRepository(pool *pgxpool.Pool) UserRepository {
return &userRepository{pool: pool}
}
func (r *userRepository) Create(ctx context.Context, user *model.User) error {
query := `
INSERT INTO users (username, email, password, avatar, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, username, email, avatar, created_at, updated_at
`
err := r.pool.QueryRow(ctx, query,
user.Username, user.Email, user.Password, user.Avatar,
user.CreatedAt, user.UpdatedAt,
).Scan(&user.ID, &user.Username, &user.Email, &user.Avatar, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
func (r *userRepository) GetByID(ctx context.Context, id uint64) (*model.User, error) {
query := `
SELECT id, username, email, avatar, created_at, updated_at
FROM users
WHERE id = $1
`
user := &model.User{}
err := r.pool.QueryRow(ctx, query, id).Scan(
&user.ID, &user.Username, &user.Email, &user.Avatar,
&user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*model.User, error) {
query := `
SELECT id, username, email, avatar, created_at, updated_at
FROM users
WHERE username = $1
`
user := &model.User{}
err := r.pool.QueryRow(ctx, query, username).Scan(
&user.ID, &user.Username, &user.Email, &user.Avatar,
&user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get user by username: %w", err)
}
return user, nil
}
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) {
query := `
SELECT id, username, email, avatar, created_at, updated_at
FROM users
WHERE email = $1
`
user := &model.User{}
err := r.pool.QueryRow(ctx, query, email).Scan(
&user.ID, &user.Username, &user.Email, &user.Avatar,
&user.CreatedAt, &user.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("failed to get user by email: %w", err)
}
return user, nil
}
func (r *userRepository) List(ctx context.Context, page, limit int, search, sortBy, sortOrder string) ([]*model.User, int64, error) {
// 构建查询条件
var whereClauses []string
var args []interface{}
argIndex := 1
if search != "" {
whereClauses = append(whereClauses, fmt.Sprintf("(username ILIKE $%d OR email ILIKE $%d)", argIndex, argIndex+1))
args = append(args, "%"+search+"%", "%"+search+"%")
argIndex += 2
}
whereSQL := ""
if len(whereClauses) > 0 {
whereSQL = "WHERE " + strings.Join(whereClauses, " AND ")
}
// 排序
allowedSortFields := map[string]bool{"username": true, "email": true, "created_at": true}
if !allowedSortFields[sortBy] {
sortBy = "created_at"
}
if sortOrder != "asc" && sortOrder != "desc" {
sortOrder = "desc"
}
offset := (page - 1) * limit
// 查询总数
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM users %s", whereSQL)
var total int64
if err := r.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("failed to count users: %w", err)
}
// 查询数据
query := fmt.Sprintf(`
SELECT id, username, email, avatar, created_at, updated_at
FROM users %s
ORDER BY %s %s
LIMIT $%d OFFSET $%d
`, whereSQL, sortBy, sortOrder, argIndex, argIndex+1)
args = append(args, limit, offset)
rows, err := r.pool.Query(ctx, query, args...)
if err != nil {
return nil, 0, fmt.Errorf("failed to list users: %w", err)
}
defer rows.Close()
var users []*model.User
for rows.Next() {
user := &model.User{}
if err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.Avatar, &user.CreatedAt, &user.UpdatedAt); err != nil {
return nil, 0, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, 0, fmt.Errorf("failed to iterate users: %w", err)
}
return users, total, nil
}
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
query := `
UPDATE users
SET username = $1, email = $2, avatar = $3, updated_at = $4
WHERE id = $5
RETURNING id, username, email, avatar, created_at, updated_at
`
err := r.pool.QueryRow(ctx, query,
user.Username, user.Email, user.Avatar, time.Now(), user.ID,
).Scan(&user.ID, &user.Username, &user.Email, &user.Avatar, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
return nil
}
func (r *userRepository) Delete(ctx context.Context, id uint64) error {
result, err := r.pool.Exec(ctx, "DELETE FROM users WHERE id = $1", id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
if result.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
// BatchCreate 批量创建用户,使用事务保证原子性
func (r *userRepository) BatchCreate(ctx context.Context, users []*model.User) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
query := `
INSERT INTO users (username, email, password, avatar, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
for _, user := range users {
_, err := tx.Exec(ctx, query,
user.Username, user.Email, user.Password, user.Avatar,
user.CreatedAt, user.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
这里有个重要的设计点,批量操作使用了事务,确保要么全部成功,要么全部回滚。这在数据一致性要求高的场景下非常重要。
中间件设计
中间件是 Go Web 服务的灵魂,好的中间件设计可以让代码更简洁,逻辑更清晰。
// internal/middleware/logger.go
package middleware
import (
"context"
"log"
"net/http"
"time"
)
type loggerEntry struct {
requestID string
startTime time.Time
path string
method string
}
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
entry := &loggerEntry{
requestID: generateRequestID(),
startTime: time.Now(),
path: r.URL.Path,
method: r.Method,
}
// 将 loggerEntry 存入 context
ctx := context.WithValue(r.Context(), "logger", entry)
// 包装 ResponseWriter 以捕获状态码
wrappedWriter := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrappedWriter, r.WithContext(ctx))
// 记录日志
duration := time.Since(entry.startTime)
log.Printf("[REQUEST] %s %s %d %s - %s - %v",
entry.requestID,
entry.method,
entry.path,
wrappedWriter.statusCode,
r.RemoteAddr,
duration,
)
})
}
type responseWriterWrapper struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
// internal/middleware/auth.go
package middleware
import (
"context"
"net/http"
"strings"
)
type contextKey string
const userIDKey contextKey = "userID"
const usernameKey contextKey = "username"
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, `{"error":"missing authorization header"}`, http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, `{"error":"invalid authorization header format"}`, http.StatusUnauthorized)
return
}
token := parts[1]
claims, err := validateJWT(token)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusUnauthorized)
return
}
// 将用户信息存入 context
ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
ctx = context.WithValue(ctx, usernameKey, claims.Username)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetUserID(ctx context.Context) uint64 {
if id, ok := ctx.Value(userIDKey).(uint64); ok {
return id
}
return 0
}
func GetUsername(ctx context.Context) string {
if username, ok := ctx.Value(usernameKey).(string); ok {
return username
}
return ""
}
统一响应格式
好的 API 应该有一致的响应格式,这样前端处理起来更方便,后端调试也更直观。
// pkg/response/response.go
package response
import (
"encoding/json"
"net/http"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Meta *Meta `json:"meta,omitempty"`
}
type Meta struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
PageCount int `json:"page_count"`
}
func Success(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Code: 0,
Message: "success",
Data: data,
})
}
func PaginatedSuccess(w http.ResponseWriter, status int, data interface{}, total int64, page, limit int) {
pageCount := int(total) / limit
if int(total)%limit > 0 {
pageCount++
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Code: 0,
Message: "success",
Data: data,
Meta: &Meta{
Page: page,
Limit: limit,
Total: total,
PageCount: pageCount,
},
})
}
func Error(w http.ResponseWriter, status int, message, code string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Code: status,
Message: message,
})
}
func NotFound(w http.ResponseWriter, message string) {
Error(w, http.StatusNotFound, message, "NOT_FOUND")
}
func InternalError(w http.ResponseWriter, err error) {
Error(w, http.StatusInternalServerError, "internal server error", "INTERNAL_ERROR")
}
入口文件与服务启动
最后,我们需要把所有组件组装在一起,启动服务。
// cmd/server/main.go
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"go-web-service/internal/config"
"go-web-service/internal/database"
"go-web-service/internal/handler"
"go-web-service/internal/middleware"
"go-web-service/internal/repository"
"go-web-service/internal/service"
"go-web-service/pkg/jwt"
)
func main() {
configPath := flag.String("config", "config/config.yaml", "config file path")
flag.Parse()
// 加载配置
cfg, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 初始化数据库
dbPool, err := database.NewPool(cfg.Database)
if err != nil {
log.Fatalf("Failed to create database pool: %v", err)
}
defer dbPool.Close()
// 初始化仓储层
userRepo := repository.NewUserRepository(dbPool.Get())
orderRepo := repository.NewOrderRepository(dbPool.Get())
// 初始化 JWT 服务
jwtService := jwt.NewJWTService(cfg.JWT.Secret, cfg.JWT.ExpireTime)
// 初始化服务层
userService := service.NewUserService(userRepo, jwtService)
orderService := service.NewOrderService(orderRepo, userRepo)
// 初始化处理器
userHandler := handler.NewUserHandler(userService)
orderHandler := handler.NewOrderHandler(orderService)
// 初始化路由
r := chi.NewRouter()
// 全局中间件
r.Use(middleware.LoggerMiddleware)
r.Use(middleware.RequestIDMiddleware)
r.Use(middleware.RateLimitMiddleware(100)) // 每秒100个请求
r.Use(middleware.CORSMiddleware())
// 健康检查
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
})
// 版本化 API
r.Mount("/api/v1", func(r chi.Router) {
userHandler.RegisterRoutes(r)
orderHandler.RegisterRoutes(r)
})
// 创建 HTTP 服务器
server := &http.Server{
Addr: ":" + cfg.Server.Port,
Handler: r,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: 60 * time.Second,
}
// 优雅关闭
go func() {
log.Printf("Server starting on port %s", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited properly")
}
Docker 部署配置
生产环境部署离不开容器化,我们使用多阶段构建来减小镜像体积。
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 安装依赖
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/server
# 生产镜像
FROM alpine:3.18
WORKDIR /app
# 安装必要的运行时依赖
RUN apk add --no-cache ca-certificates tzdata
ENV TZ=Asia/Shanghai
# 复制二进制文件
COPY --from=builder /app/server .
COPY --from=builder /app/config ./config
# 创建日志目录
RUN mkdir -p /app/logs
EXPOSE 8080
# 使用非 root 用户运行
RUN adduser -D -g '' appuser
USER appuser
CMD ["./server", "--config", "config/config.yaml"]
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- APP_DATABASE_HOST=db
- APP_DATABASE_PORT=5432
- APP_DATABASE_USER=postgres
- APP_DATABASE_PASSWORD=your_password
- APP_DATABASE_DBNAME=web_service
- APP_REDIS_ADDR=redis:6379
- APP_JWT_SECRET=your-super-secret-key
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
volumes:
- app-logs:/app/logs
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=your_password
- POSTGRES_DB=web_service
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:
redisdata:
app-logs:
性能优化要点
高并发服务的性能优化是个系统工程,有几个关键点需要注意。
连接池的配置需要根据实际负载调优。如果发现自己的服务经常出现连接等待超时,可以适当增加 MaxOpenConns;如果内存占用过高,可以减少 MaxIdleConns 或缩短 ConnMaxLifetime。有个经验公式可以参考:MaxOpenConns = CPU核数 * 2 + 磁盘数,这只是一个起点,实际需要根据压测结果调整。
数据库查询的性能直接影响 API 响应速度。对于高频查询,可以考虑加缓存层。比如用户信息查询,可以用 Redis 做缓存,设置合理的过期时间。分页查询一定要用 LIMIT/OFFSET 而不是直接取全部数据再切片,大数据量时这个差别非常大。
HTTP 服务器的性能也可以通过一些配置优化。启用 Gzip 压缩可以减少响应大小,特别是在传输 JSON 数据时效果明显。设置合适的 IdleTimeout 可以让连接及时释放,避免资源浪费。对于高并发场景,可以考虑使用 http2 协议,虽然 Go 的 net/http 包已经内置支持。
日志和监控也是高并发服务的重要组成部分。结构化日志(JSON 格式)便于后续用 ELK 或 Loki 等工具分析。Prometheus 指标暴露可以让运维团队实时监控服务状态。健康检查端点 /health 是容器编排平台判断服务是否正常的基础。
部署上线时,建议先在小流量环境验证,逐步放量。使用蓝绿部署或金丝雀发布可以减少上线风险。记得配置日志轮转,避免日志文件撑爆磁盘。定期备份数据库,这是最基本的底线。
好了,以上就是从零搭建高并发 Web 服务的全流程解析。Go 语言的优势在于简单高效,但要做到真正的高并发服务,需要在架构设计、代码规范和运维监控等多个维度下功夫。希望这篇文章能帮你建立起完整的认知框架,在实际项目中少走弯路。
