微服务架构必备:Gin + gRPC + Consul + Nacos + GORM 打造用户服务

🎯 概述

本文整合了微服务开发的核心技术栈:使用gRPC实现高性能RPC通信,Consul完成服务注册与发现,Nacos作为配置中心,GORM操作MySQL数据库。从项目初始化、proto定义、服务注册、配置管理到Web API调用,全流程代码讲解,并包含负载均衡、连接池、优雅注销等进阶特性。适合想系统学习Go微服务开发的读者。

Grpc Service 服务

Grpc 的文件目录

shell 复制代码
[root@localhost user_srv]# 
.
├── config
│   └── config.go
├── config-dev.yaml
├── global
│   └── global.go
├── handler
│   └── user.go
├── initialize
│   ├── config.go
│   ├── db.go
│   └── logger.go
├── main.go
├── model
│   └── user.go
├── proto
│   ├── user_grpc.pb.go
│   ├── user.pb.go
│   └── user.proto
├── tests
│   └── user.go
└── utils
    └── addr.go

1.初始化数据库,设置DB全局变量

数据库连接的思路:数据库成功连接后,放置在全局变量上,方便后面的调用的使用,更多关于Gorm的操作可以去官网文档查询

shell 复制代码
go get gorm.io/gorm
go get gorm.io/driver/mysql

#官网 https://gorm.io/zh_CN/

设置基本配置信息,打印日志,配置Mysql连接池,数据库初始化如下:

go 复制代码
func InitDB() error {
    // DSN格式:user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
    // 配置日志(打印SQL)
    config := global.ServerConfig.MysqlInfo
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        config.User,
        config.Password,
        config.Host,
        config.Port,
        config.DB,
    )
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // 输出到控制台
        logger.Config{
            SlowThreshold: time.Second, // 慢SQL阈值(超过1秒打印)
            LogLevel:      logger.Info, // 日志级别:Info(打印所有SQL)、Warn(警告+错误)、Error(仅错误)
            Colorful:      true,        // 彩色打印
        },
    )
    // 连接数据库并配置参数
    var err error
    global.DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: newLogger, // 日志配置
        // 其他常用配置
        SkipDefaultTransaction: true, // 关闭默认事务(提高性能)
        PrepareStmt:            true, // 预编译语句(缓存SQL,提高执行效率)
        // 全局表名规则:给所有表加前缀 t_(优先级低于结构体TableName方法)
        NamingStrategy: schema.NamingStrategy{
            //TablePrefix:   "t_",  // 表名前缀
            SingularTable: true, // 表名是否单数(默认复数,如student→students,开启后为student)
        },
    })
    if err != nil {
        zap.S().Error("连接数据库失败:", zap.Error(err))
        panic(err)
    }

    // 获取底层sql.DB对象,配置连接池
    sqlDB, err := global.DB.DB()
    if err != nil {
        return err
    }
    // 连接池配置
    sqlDB.SetMaxOpenConns(100)                 // 最大打开连接数
    sqlDB.SetMaxIdleConns(20)                  // 最大空闲连接数
    sqlDB.SetConnMaxLifetime(1 * time.Hour)    // 连接最大存活时间
    sqlDB.SetConnMaxIdleTime(30 * time.Minute) // 连接最大空闲时间
    return nil
}

2.安装protoc、protoc-gen-go、protoc-gen-go-grpc

想生成grpc的proto文件,需要安装protoc,protoc-gen-go,protoc-gen-go-grpc 这三个插件,安装protoc 不要忘记需要配置环境变量,我的本地电脑是window,可以使用包管理、也可以使用手动安装方式

shell 复制代码
 # 使用 Chocolatey 安装
 choco install protoc
 
 # 使用 Scoop 安装
 scoop install protobuf
 
 # github 下载地址
 https://github.com/protocolbuffers/protobuf/releases

打开终端,或者是Goland终端,执行go env GOPATH,打开目录,安装protoc-gen-go、protoc-gen-go-grpc,命令如下:

shell 复制代码
# 安装 protoc-gen-go(适配 3.15.5 的版本)
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1

# 安装 protoc-gen-go-grpc(适配 3.15.5 的版本)
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0

# 查看 protoc-gen-go 版本
protoc-gen-go --version
# 查看 protoc-gen-go-grpc 版本
protoc-gen-go-grpc --version

3.定义proto文件,生成需要的通信文件

例举一个简单的代码示例,要注意的是UserInfoResponse结构体里面的定义的顺序,如果有修改需要重新生成,重启服务才会生效。

proto 复制代码
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";

service UserService {
  rpc GetUserList(PageInfo) returns (UserListResponse); //用户列表
  rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse); //通过手机号获取用户信息
  rpc GetUserById(IdRequest) returns (UserInfoResponse); //通过id获取用户信息
  rpc CreateUser(CreateUserInfo) returns (UserInfoResponse);
  rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty);
  rpc CheckPassword(PasswordCheckInfo) returns (CheckResponse);
}

message PageInfo {
  uint32 pn = 1;
  uint32 pSize = 2;
}

message UserListResponse {
  int32 total = 1;
  repeated UserInfoResponse data = 2;
}

message UserInfoResponse {
  int32 id = 1;
  string password = 2;
  string mobile = 3;
  string nickName = 4;
  uint64 birthDay = 5;
  string gender = 6;
  int32 role = 7;
}

生成proto文件,命令如下:

bash 复制代码
# 第一步:生成基础的 Protobuf Go 代码
protoc -I . user.proto --go_out=. --go_opt=paths=source_relative

# 第二步:生成 gRPC 相关的 Go 代码
protoc -I . user.proto --go-grpc_out=. --go-grpc_opt=paths=source_relative

4.提供Grpc 操作数据服务(以列表为例)

go 复制代码
type UserServiceServer interface {
    GetUserList(context.Context, *PageInfo) (*UserListResponse, error)
    GetUserByMobile(context.Context, *MobileRequest) (*UserInfoResponse, error)
    GetUserById(context.Context, *IdRequest) (*UserInfoResponse, error)
    CreateUser(context.Context, *CreateUserInfo) (*UserInfoResponse, error)
    UpdateUser(context.Context, *UpdateUserInfo) (*emptypb.Empty, error)
    CheckPassword(context.Context, *PasswordCheckInfo) (*CheckResponse, error)
    mustEmbedUnimplementedUserServiceServer()
}

其实Grpc的服务很简单,它的操作前提是由proto文件生成好的协议,会有类似的代码示例,比如我的用户服务是这样的,只需要补充具体的逻辑即可。

go 复制代码
type UserServer struct {
    proto.UnimplementedUserServiceServer // 嵌入未实现的服务结构体
}

func (s *UserServer) mustEmbedUnimplementedUserServiceServer() {
    //TODO implement me
    panic("implement me")
}

func ModelToResponse(user model.User) proto.UserInfoResponse {
    userInfo := proto.UserInfoResponse{
        Id:       user.ID,
        Password: user.Password,
        Mobile:   user.Mobile,
        NickName: user.NickName,
        Gender:   user.Gender,
        Role:     int32(user.Role),
    }
    if user.Birthday != nil {
        userInfo.BirthDay = uint64(user.Birthday.Unix())
    }
    return userInfo
}

func (s *UserServer) GetUserList(ctx context.Context, request *proto.PageInfo) (*proto.UserListResponse, error) {
    //获取用户列表
    var users []model.User
    result := global.DB.Find(&users)
    if result.Error != nil {
        return nil, result.Error
    }

    rsp := proto.UserListResponse{}
    rsp.Total = int32(result.RowsAffected)
    global.DB.Scopes(Paginate(int(request.Pn), int(request.PSize))).Find(&users)

    for _, user := range users {
        fmt.Println(user)
        userInfoRsp := ModelToResponse(user)
        rsp.Data = append(rsp.Data, &userInfoRsp)
    }
    return &rsp, nil
}

5.启动Grpc服务

启动服务的加载顺序: 加载全局日志 -> 加载配置文件->初始化数据库连接->优雅退出

go 复制代码
func main() {
    IP := flag.String("ip", "0.0.0.0", "ip address")
    Port := flag.Int("port", 0, "port number")

    initialize.InitLogger()
    initialize.InitConfig()
    initialize.InitDB()

    flag.Parse()
    fmt.Println("IP:", *IP)
    if *Port == 0 {
       *Port, _ = utils.GetFreePort()
    }

    server := grpc.NewServer()
    proto.RegisterUserServiceServer(server, &handler.UserServer{})

    lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
    if err != nil {
       panic(err.Error())
    }
    
    err = server.Serve(lis)
    if err != nil {
      panic(err.Error())
    }
    
    quit := make(chan os.Signal)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    if err = client.Agent().ServiceDeregister(serviceID); err != nil {
       zap.S().Info("服务注销失败:", err.Error())
    }
    zap.S().Info("注销成功")
}

Web Api 服务

bash 复制代码
[root@localhost user-web]# tree
.
├── api
│   ├── chaptcha.go
│   ├── sms.go
│   └── user.go
├── config
│   └── config.go
├── config-debug.yaml
├── config-dev.yaml
├── config-pro.yaml
├── forms
│   ├── sms.go
│   └── user.go
├── global
│   ├── global.go
│   └── response
│       └── user.go
├── index.html
├── initiate
│   ├── config.go
│   ├── logger.go
│   ├── router.go
│   ├── srv_conn.go
│   └── validator.go
├── main.go
├── middlewares
│   ├── admin.go
│   ├── cors.go
│   └── jwt.go
├── models
│   └── request.go
├── proto
│   ├── user_grpc.pb.go
│   ├── user.pb.go
│   └── user.proto
├── router
│   ├── base.go
│   └── user.go
├── utils
│   └── addr.go
└── validator
    └── validator.go

1.加载日志和路由(zap)

go 复制代码
import "go.uber.org/zap"

func InitLogger() {
    devLogger, err := zap.NewDevelopment()
    if err != nil {
        panic("创建开发环境Logger失败: " + err.Error())
    }
    defer devLogger.Sync() // 确保日志刷入输出(如文件/控制台)
    zap.ReplaceGlobals(devLogger)
}

在路由上,在启动服务初始化的时候,需要加载路由,在某一个Api上添加特殊时使用中间件进行操作

go 复制代码
# 初始化
func Routers() *gin.Engine {
    Router := gin.Default()
    zap.S().Info("配置用户路由相关Url")
    Router.Use(middlewares.Cors())
    ApiGroup := Router.Group("/u/v1")
    router.InitRouter(ApiGroup)
    router.InitBaseRouter(ApiGroup)
    return Router
}

#用户校验、超管校验
func InitRouter(Router *gin.RouterGroup) {
    UserRouter := Router.Group("user")
    {
        UserRouter.GET("list", middlewares.JWTAuth(), middlewares.IsAdminAuth(), api.GetUserList)
        UserRouter.POST("pwd_login", api.PasswordLogin)
        UserRouter.POST("register", api.Register)
    }
}

2.连接和使用Redis数据库

go 复制代码
"github.com/redis/go-redis/v9"

RedisInfo := global.ServerConfig.RedisInfo
rdb := redis.NewClient(&redis.Options{
        Addr:     fmt.Sprintf("%s:%d", RedisInfo.Host, RedisInfo.Port),
        Password: RedisInfo.Password,
    })
rdb.Set(
    context.Background(), 
    sendSmsForm.Mobile, 
    smsCode, 
    time.Duration(global.ServerConfig.RedisInfo.Expire)*time.Second
)

Consul 服务注册与发现

无论是分布式架构场景、还是微服务架构场景,在大型的架构场景中都会同时运行很多服务,当我们添加一个服务的Service和Port时,程序帮我们自动注册到服务中心来完成自动化服务和注册,我们这里使用Consul来实践。

在下面的GetFreePort方法中自动获取端口 + Goroutine协程方式来实现Grpc服务的负载均衡。

go 复制代码
import (
    "github.com/hashicorp/consul/api"
    "google.golang.org/grpc/health"
    "google.golang.org/grpc/health/grpc_health_v1"
)

flag.Parse()
    *Port, _ = utils.GetFreePort()

    server := grpc.NewServer()
    proto.RegisterUserServiceServer(server, &handler.UserServer{})

    lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
    if err != nil {
        panic(err.Error())
    }
    
    go func() {
        err = server.Serve(lis)
        if err != nil {
            panic(err.Error())
        }
    }()

Consul的服务截图如下:

1.Grpc注册 注册服务的实现逻辑,在Grpc的服务中进行注册,在Web服务中进行服务发现,也就是拉取注册中心的服务,以便于进行网络通信

go 复制代码
//注册服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
//服务注册
cfg := api.DefaultConfig()
consul := global.ServerConfig.ConsulInfo
cfg.Address = fmt.Sprintf("%s:%d", consul.Host, consul.Port)
client, err := api.NewClient(cfg)
if err != nil {
    zap.S().Panic("服务注册失败:", err.Error())
}

serviceID := fmt.Sprintf("%s", uuid.NewV4())
registration := new(api.AgentServiceRegistration)
registration.Name = global.ServerConfig.Name
registration.ID = serviceID
registration.Port = *Port
registration.Tags = []string{"user_srv", "grpc"}
registration.Address = global.ServerConfig.Host
registration.Check = &api.AgentServiceCheck{
    GRPC:                           fmt.Sprintf("%s:%d", global.ServerConfig.Host, *Port),
    Interval:                       "5s",
    Timeout:                        "5s",
    DeregisterCriticalServiceAfter: "10s",
}
err = client.Agent().ServiceRegister(registration)
if err != nil {
    zap.S().Infof("服务注册失败:%s", err.Error())
    panic(err.Error())
}

2.Web Api 连接

在web服务调用Grpc的服务中使用轮询的策略进行负载均衡的服务分发

go 复制代码
func InitSrvConn() {
    consul := global.ServerConfig.ConsulInfo
    userConn, err := grpc.Dial(
        fmt.Sprintf("consul://%s:%d/%s?wait=14s", consul.Host, consul.Port, global.ServerConfig.UserSrvInfo.Name),
        grpc.WithInsecure(),
        grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
    )
    if err != nil {
        zap.S().Fatalf("[InitSrvConn] 连接 【用户服务失败】")
    }

    userSrvClient := proto.NewUserServiceClient(userConn)
    global.UserSrvClient = userSrvClient
}

Nacos 配置中心

单体服务的配置一般的处理步骤,设置在系统变量来读取本地中的yaml配置文件,多个分布式服务或者是微服务中采用配置中心进行统一拉取,这里我采用的是Nacos,Nacos支持的多种格式的配置,操作界面如下:

在NacosConfig结构体中,mapstructure是解析yaml格式,json是解析Json格式的文件

go 复制代码
type NacosConfig struct {
    Host      string `mapstructure:"host" json:"host"`
    Port      int    `mapstructure:"port" json:"port"`
    Namespace string `mapstructure:"namespace" json:"namespace"`
    DataId    string `mapstructure:"data_id" json:"dataId"`
    Group     string `mapstructure:"group" json:"group"`
}

yaml配置,一定要检查好配置文件中的信息和Nacos服务信息的一致,不然服务会受到影响

yaml 复制代码
host: "192.168.31.156"
port: 8848
namespace: "70ed4982-deed-4c33-b410-9cdce96cbe2c"
dataId: "user-web.json"
group: "dev"

读取本地Nacos yaml文件,操作步骤:

1、读取本地yaml配置 2、拉取配置中心的信息 3、创建动态客户端

go 复制代码
func InitConfig() {
    projectRoot := "E:/GoProject/mxshop"
    configFileName := filepath.Join(projectRoot, "user_srv", "config-dev.yaml")

    v := viper.New()
    v.SetConfigFile(configFileName)
    if err := v.ReadInConfig(); err != nil {
        panic(err)
    }
    if err := v.Unmarshal(&global.NacosConfig); err != nil {
        panic(err)
    }

    // 至少一个ServerConfig
    serverConfigs := []constant.ServerConfig{
        {
            IpAddr: global.NacosConfig.Host,
            Port:   uint64(global.NacosConfig.Port),
        },
    }

    clientConfig := constant.ClientConfig{
        NamespaceId:         global.NacosConfig.Namespace, // 命名空间
        TimeoutMs:           5000,
        NotLoadCacheAtStart: true, // 首次不加载缓存
        LogDir:              logDir,
        CacheDir:            cacheDir,
        LogLevel:            "debug", // 使用debug级别便于排查问题
        UpdateThreadNum:     20,
    }

    // 创建动态配置客户端
    configClient, err := clients.CreateConfigClient(map[string]interface{}{
        "serverConfigs": serverConfigs,
        "clientConfig":  clientConfig,
    })
    if err != nil {
        panic(err)
    }

    // 先测试连接是否正常
    fmt.Println("正在连接Nacos服务器...")

    content, err := configClient.GetConfig(vo.ConfigParam{
        DataId: "user-srv.json",
        Group:  "dev",
    })
    if err != nil {
        fmt.Printf("获取配置失败: %v\n", err)
        fmt.Println("请检查:")
        fmt.Println("1. Nacos服务器是否正常运行")
        fmt.Println("2. 配置文件 user-web.yaml 是否存在于 dev 分组中")
        fmt.Println("3. NamespaceId 是否正确")
        panic(err)
    }

    fmt.Println("成功获取配置内容:", content)
    err = json.Unmarshal([]byte(content), &global.ServerConfig)
    if err != nil {
        zap.S().Fatalf("读取nacos配置失败: %s", err.Error())
    }
}

Yapi 测试

使用Yapi进行接口调试,网络通畅,用户微服务完美Ending。

相关推荐
蝎子莱莱爱打怪6 天前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
SamDeepThinking7 天前
Java微服务练习方式
java·后端·微服务
米丘10 天前
微前端之 Web Components 完全指南
微服务·html
霸道流氓气质13 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
霸道流氓气质13 天前
Spring Boot 微服务性能优化完全指南
spring boot·微服务·性能优化
地瓜伯伯13 天前
从MESI缓存一致性协议讲透synchronized的底层
java·spring boot·spring·spring cloud·微服务·springcloud
Devin~Y13 天前
大厂 Java 面试实录:从音视频内容社区到 AI RAG 的全链路技术设计
java·spring boot·redis·spring cloud·微服务·kafka·音视频
递归尽头是星辰13 天前
AI 访问数据仓库:从直连到微服务化
数据仓库·人工智能·微服务·dataagent·ai数据治理
就改了14 天前
Windows 环境 SkyWalking 完整实操教程
windows·微服务·skywalking
至乐活着14 天前
Docker Compose多服务编排实战:从零搭建Node.js+MySQL+Redis全栈应用
docker·微服务·devops·容器编排·compose