前言
在现代软件开发中,微服务架构已成为构建可扩展、可维护应用的重要模式。Go语言凭借其高性能、并发性强和部署简单的特点,成为微服务开发的热门选择。本文将通过一个完整的实战项目,详细介绍如何使用Go语言结合Gin Web框架、gRPC远程调用和MySQL数据库来构建一个微服务系统。
项目概述
我们将构建一个简单的用户管理系统,包含用户注册、登录、查询等功能。整个系统采用微服务架构,将不同的业务功能拆分为独立的服务模块,通过gRPC进行服务间通信,使用MySQL作为数据存储。
技术栈说明
- Go语言:核心编程语言
- Gin框架:Web API开发框架
- gRPC:服务间通信协议
- MySQL:关系型数据库
- Docker:容器化部署
- Protobuf:接口定义语言
项目初始化与结构设计
项目目录结构
user-service/
├── cmd/
│ └── user-service/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ └── user_handler.go
│ ├── model/
│ │ ├── user.go
│ │ └── user.pb.go
│ ├── repository/
│ │ └── user_repository.go
│ ├── service/
│ │ └── user_service.go
│ └── utils/
│ └── database.go
├── proto/
│ └── user.proto
├── docker-compose.yml
├── Dockerfile
└── go.mod
初始化Go模块
mkdir user-service
cd user-service
go mod init user-service
MySQL数据库设计与配置
数据库表结构
首先创建用户表:
CREATE DATABASE IF NOT EXISTS user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE user_db;
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
数据库连接配置
// internal/config/config.go
package config
import (
"fmt"
"os"
"time"
)
type DatabaseConfig struct {
Host string
Port string
User string
Password string
Name string
Charset string
}
type Config struct {
Database DatabaseConfig
Server ServerConfig
}
type ServerConfig struct {
Port string
}
func LoadConfig() (*Config, error) {
config := &Config{
Database: DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "3306"),
User: getEnv("DB_USER", "root"),
Password: getEnv("DB_PASSWORD", ""),
Name: getEnv("DB_NAME", "user_db"),
Charset: getEnv("DB_CHARSET", "utf8mb4"),
},
Server: ServerConfig{
Port: getEnv("SERVER_PORT", "8080"),
},
}
return config, nil
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
数据库连接工具
// internal/utils/database.go
package utils
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
"user-service/internal/config"
)
func InitDB(cfg *config.DatabaseConfig) (*sql.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
cfg.User,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.Name,
cfg.Charset,
)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %v", err)
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
// 测试连接
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %v", err)
}
log.Println("Database connected successfully")
return db, nil
}
gRPC服务定义与实现
Protobuf接口定义
// proto/user.proto
syntax = "proto3";
package user;
option go_package = "./;user";
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
}
message User {
int64 id = 1;
string username = 2;
string email = 3;
string password = 4;
string created_at = 5;
string updated_at = 6;
}
message CreateUserRequest {
string username = 1;
string email = 2;
string password = 3;
}
message CreateUserResponse {
int64 id = 1;
bool success = 2;
string message = 3;
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
User user = 1;
bool success = 2;
string message = 3;
}
message UpdateUserRequest {
int64 id = 1;
string username = 2;
string email = 3;
string password = 4;
}
message UpdateUserResponse {
bool success = 1;
string message = 2;
}
message DeleteUserRequest {
int64 id = 1;
}
message DeleteUserResponse {
bool success = 1;
string message = 2;
}
gRPC服务实现
// internal/service/user_service.go
package service
import (
"context"
"database/sql"
"fmt"
"log"
"time"
"user-service/internal/model"
"user-service/internal/repository"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type UserService struct {
userRepo *repository.UserRepository
model.UnimplementedUserServiceServer
}
func NewUserService(db *sql.DB) *UserService {
return &UserService{
userRepo: repository.NewUserRepository(db),
}
}
func (s *UserService) CreateUser(ctx context.Context, req *model.CreateUserRequest) (*model.CreateUserResponse, error) {
log.Printf("Creating user: %s", req.Username)
// 验证输入
if req.Username == "" || req.Email == "" || req.Password == "" {
return &model.CreateUserResponse{
Success: false,
Message: "Username, email and password are required",
}, status.Error(codes.InvalidArgument, "Username, email and password are required")
}
// 检查用户是否已存在
existingUser, err := s.userRepo.FindByUsername(ctx, req.Username)
if err != nil && err != sql.ErrNoRows {
return &model.CreateUserResponse{
Success: false,
Message: "Database error",
}, status.Error(codes.Internal, "Database error")
}
if existingUser != nil {
return &model.CreateUserResponse{
Success: false,
Message: "Username already exists",
}, status.Error(codes.AlreadyExists, "Username already exists")
}
// 创建用户
user, err := s.userRepo.Create(ctx, req.Username, req.Email, req.Password)
if err != nil {
return &model.CreateUserResponse{
Success: false,
Message: "Failed to create user",
}, status.Error(codes.Internal, "Failed to create user")
}
log.Printf("User created successfully with ID: %d", user.ID)
return &model.CreateUserResponse{
Id: user.ID,
Success: true,
Message: "User created successfully",
}, nil
}
func (s *UserService) GetUser(ctx context.Context, req *model.GetUserRequest) (*model.GetUserResponse, error) {
log.Printf("Getting user by ID: %d", req.Id)
if req.Id <= 0 {
return &model.GetUserResponse{
Success: false,
Message: "Invalid user ID",
}, status.Error(codes.InvalidArgument, "Invalid user ID")
}
user, err := s.userRepo.FindByID(ctx, req.Id)
if err != nil {
if err == sql.ErrNoRows {
return &model.GetUserResponse{
Success: false,
Message: "User not found",
}, status.Error(codes.NotFound, "User not found")
}
return &model.GetUserResponse{
Success: false,
Message: "Database error",
}, status.Error(codes.Internal, "Database error")
}
return &model.GetUserResponse{
User: &model.User{
Id: user.ID,
Username: user.Username,
Email: user.Email,
Password: user.Password,
CreatedAt: user.CreatedAt.Format(time.RFC3339),
UpdatedAt: user.UpdatedAt.Format(time.RFC3339),
},
Success: true,
Message: "User retrieved successfully",
}, nil
}
func (s *UserService) UpdateUser(ctx context.Context, req *model.UpdateUserRequest) (*model.UpdateUserResponse, error) {
log.Printf("Updating user ID: %d", req.Id)
if req.Id <= 0 {
return &model.UpdateUserResponse{
Success: false,
Message: "Invalid user ID",
}, status.Error(codes.InvalidArgument, "Invalid user ID")
}
// 检查用户是否存在
existingUser, err := s.userRepo.FindByID(ctx, req.Id)
if err != nil {
if err == sql.ErrNoRows {
return &model.UpdateUserResponse{
Success: false,
Message: "User not found",
}, status.Error(codes.NotFound, "User not found")
}
return &model.UpdateUserResponse{
Success: false,
Message: "Database error",
}, status.Error(codes.Internal, "Database error")
}
// 更新用户信息
updatedUser, err := s.userRepo.Update(ctx, req.Id, req.Username, req.Email, req.Password)
if err != nil {
return &model.UpdateUserResponse{
Success: false,
Message: "Failed to update user",
}, status.Error(codes.Internal, "Failed to update user")
}
log.Printf("User updated successfully: %s", updatedUser.Username)
return &model.UpdateUserResponse{
Success: true,
Message: "User updated successfully",
}, nil
}
func (s *UserService) DeleteUser(ctx context.Context, req *model.DeleteUserRequest) (*model.DeleteUserResponse, error) {
log.Printf("Deleting user ID: %d", req.Id)
if req.Id <= 0 {
return &model.DeleteUserResponse{
Success: false,
Message: "Invalid user ID",
}, status.Error(codes.InvalidArgument, "Invalid user ID")
}
err := s.userRepo.Delete(ctx, req.Id)
if err != nil {
if err == sql.ErrNoRows {
return &model.DeleteUserResponse{
Success: false,
Message: "User not found",
}, status.Error(codes.NotFound, "User not found")
}
return &model.DeleteUserResponse{
Success: false,
Message: "Database error",
}, status.Error(codes.Internal, "Database error")
}
log.Printf("User deleted successfully: %d", req.Id)
return &model.DeleteUserResponse{
Success: true,
Message: "User deleted successfully",
}, nil
}
MySQL数据访问层实现
用户数据模型
// internal/model/user.go
package model
import (
"time"
)
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
数据访问层实现
// internal/repository/user_repository.go
package repository
import (
"context"
"database/sql"
"fmt"
"time"
"user-service/internal/model"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{
db: db,
}
}
func (r *UserRepository) Create(ctx context.Context, username, email, password string) (*model.User, error) {
query := `INSERT INTO users (username, email, password, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
now := time.Now()
_, err := r.db.ExecContext(ctx, query, username, email, password, now, now)
if err != nil {
return nil, fmt.Errorf("failed to create user: %v", err)
}
// 获取新创建的用户
user, err := r.FindByUsername(ctx, username)
if err != nil {
return nil, fmt.Errorf("failed to get created user: %v", err)
}
return user, nil
}
func (r *UserRepository) FindByID(ctx context.Context, id int64) (*model.User, error) {
query := `SELECT id, username, email, password, created_at, updated_at
FROM users WHERE id = ?`
var user model.User
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Username,
&user.Email,
&user.Password,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*model.User, error) {
query := `SELECT id, username, email, password, created_at, updated_at
FROM users WHERE username = ?`
var user model.User
err := r.db.QueryRowContext(ctx, query, username).Scan(
&user.ID,
&user.Username,
&user.Email,
&user.Password,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *UserRepository) Update(ctx context.Context, id int64, username, email, password string) (*model.User, error) {
query := `UPDATE users SET username = ?, email = ?, password = ?, updated_at = ?
WHERE id = ?`
now := time.Now()
_, err := r.db.ExecContext(ctx, query, username, email, password, now, id)
if err != nil {
return nil, fmt.Errorf("failed to update user: %v", err)
}
// 获取更新后的用户
user, err := r.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get updated user: %v", err)
}
return user, nil
}
func (r *UserRepository) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM users WHERE id = ?`
_, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete user: %v", err)
}
return nil
}
Gin Web API实现
HTTP路由和处理器
// internal/handler/user_handler.go
package handler
import (
"context"
"encoding/json"
"net/http"
"time"
"user-service/internal/model"
"user-service/internal/service"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
)
type UserHandler struct {
userService *service.UserService
grpcConn *grpc.ClientConn
}
func NewUserHandler(userService *service.UserService, grpcConn *grpc.ClientConn) *UserHandler {
return &UserHandler{
userService: userService,
grpcConn: grpcConn,
}
}
// CreateUser 创建用户
func (h *UserHandler) CreateUser(c *gin.Context) {
var req model.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
"error": err.Error(),
})
return
}
// 调用gRPC服务
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client := model.NewUserServiceClient(h.grpcConn)
response, err := client.CreateUser(ctx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to create user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": response.Success,
"message": response.Message,
"data": gin.H{
"id": response.Id,
},
})
}
// GetUser 获取用户信息
func (h *UserHandler) GetUser(c *gin.Context) {
id := c.Param("id")
// 验证ID格式
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "User ID is required",
})
return
}
// 调用gRPC服务
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client := model.NewUserServiceClient(h.grpcConn)
response, err := client.GetUser(ctx, &model.GetUserRequest{
Id: parseInt64(id),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to get user",
"error": err.Error(),
})
return
}
if !response.Success {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": response.Message,
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": response.Success,
"message": response.Message,
"data": response.User,
})
}
// UpdateUser 更新用户信息
func (h *UserHandler) UpdateUser(c *gin.Context) {
id := c.Param("id")
var req model.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "Invalid request body",
"error": err.Error(),
})
return
}
// 设置ID
req.Id = parseInt64(id)
// 调用gRPC服务
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client := model.NewUserServiceClient(h.grpcConn)
response, err := client.UpdateUser(ctx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to update user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": response.Success,
"message": response.Message,
})
}
// DeleteUser 删除用户
func (h *UserHandler) DeleteUser(c *gin.Context) {
id := c.Param("id")
// 调用gRPC服务
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client := model.NewUserServiceClient(h.grpcConn)
response, err := client.DeleteUser(ctx, &model.DeleteUserRequest{
Id: parseInt64(id),
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "Failed to delete user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": response.Success,
"message": response.Message,
})
}
// ListUsers 获取用户列表
func (h *UserHandler) ListUsers(c *gin.Context) {
// 这里可以实现分页查询逻辑
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "List users endpoint",
"data": []interface{}{},
})
}
// 辅助函数:字符串转int64
func parseInt64(s string) int64 {
var result int64
json.Unmarshal([]byte(s), &result)
return result
}
Gin应用启动
// cmd/user-service/main.go
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"time"
"user-service/internal/config"
"user-service/internal/handler"
"user-service/internal/service"
"user-service/internal/utils"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal("Failed to load config:", err)
}
// 初始化数据库
db, err := utils.InitDB(&cfg.Database)
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer db.Close()
// 创建gRPC服务
grpcServer := grpc.NewServer()
// 创建用户服务并注册到gRPC服务器
userService := service.NewUserService(db)
// 这里应该使用实际的gRPC服务注册代码
// 启动gRPC服务器
go func() {
log.Println("Starting gRPC server on :50051")
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// 注册服务
// model.RegisterUserServiceServer(grpcServer, userService)
reflection.Register(grpcServer)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}()
// 创建Gin路由
r := gin.Default()
// 初始化用户处理器
// userHandler := handler.NewUserHandler(userService, grpcConn)
// 定义API路由
api := r.Group("/api/v1")
{
// 用户相关路由
users := api.Group("/users")
{
users.POST("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Create user endpoint",
})
})
users.GET("/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Get user endpoint",
})
})
users.PUT("/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Update user endpoint",
})
})
users.DELETE("/:id", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Delete user endpoint",
})
})
}
}
// 启动HTTP服务器
go func() {
log.Printf("Starting HTTP server on :%s", cfg.Server.Port)
if err := r.Run(":" + cfg.Server.Port); err != nil {
log.Fatalf("Failed to start HTTP server: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 停止gRPC服务器
grpcServer.GracefulStop()
log.Println("Server exited")
}
项目构建与部署
Dockerfile配置
# 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 -a -installsuffix cgo -o user-service cmd/user-service/main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/user-service .
EXPOSE 8080 50051
CMD ["./user-service"]
Docker Compose配置
# docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: user-mysql
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: user_db
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- user-network
user-service:
build: .
container_name: user-service
ports:
- "8080:8080"
- "50051:50051"
depends_on:
- mysql
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_USER: user
DB_PASSWORD: password
DB_NAME: user_db
SERVER_PORT: 8080
networks:
- user-network
volumes:
mysql_data:
networks:
user-network:
driver: bridge
初始化SQL脚本
-- init.sql
CREATE DATABASE IF NOT EXISTS user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE user_db;
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 插入测试数据
INSERT INTO users (username, email, password) VALUES
('admin
评论 (0)