微服务架构必备: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。

相关推荐
雮尘2 天前
手把手带你玩转Android gRPC:一篇搞定原理、配置与客户端开发
android·前端·grpc
刀法如飞3 天前
一款Go语言Gin框架MVC脚手架,满足大部分场景
go·mvc·gin
阿里云云原生3 天前
MSE Nacos Prompt 管理:让 AI Agent 的核心配置真正可治理
微服务·云原生
阿里云云原生4 天前
阿里云微服务引擎 MSE 及 API 网关 2026 年 1 月产品动态
微服务
花酒锄作田4 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
麦聪聊数据4 天前
统一 Web SQL 平台如何收编企业内部的“野生数据看板”?
数据库·sql·低代码·微服务·架构
云司科技codebuddy4 天前
技术支持过硬Trae核心代理
大数据·运维·python·微服务
递归尽头是星辰4 天前
微服务事务分级治理:从 Seata 全模式到 TDSQL 实战
微服务·云原生·架构·分布式事务·事务分级治理
没有bug.的程序员4 天前
订单系统重构史诗:从单体巨兽到微服务矩阵的演进、数据一致性内核与分布式事务
java·微服务·矩阵·重构·分布式事务·数据一致性·订单系统