🎯 概述
本文整合了微服务开发的核心技术栈:使用gRPC实现高性能RPC通信,Consul完成服务注册与发现,Nacos作为配置中心,GORM操作MySQL数据库。从项目初始化、proto定义、服务注册、配置管理到Web API调用,全流程代码讲解,并包含负载均衡、连接池、优雅注销等进阶特性。适合想系统学习Go微服务开发的读者。
Grpc Service 服务
Grpc 的文件目录
[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的操作可以去官网文档查询
go get gorm.io/gorm
go get gorm.io/driver/mysql
#官网 https://gorm.io/zh_CN/
设置基本配置信息,打印日志,配置Mysql连接池,数据库初始化如下:
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,可以使用包管理、也可以使用手动安装方式
# 使用 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,命令如下:
# 安装 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结构体里面的定义的顺序,如果有修改需要重新生成,重启服务才会生效。
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文件,命令如下:
# 第一步:生成基础的 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 操作数据服务(以列表为例)
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文件生成好的协议,会有类似的代码示例,比如我的用户服务是这样的,只需要补充具体的逻辑即可。
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服务
启动服务的加载顺序: 加载全局日志 -> 加载配置文件->初始化数据库连接->优雅退出
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 服务
[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)
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上添加特殊时使用中间件进行操作
# 初始化
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数据库
"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服务的负载均衡。
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服务中进行服务发现,也就是拉取注册中心的服务,以便于进行网络通信
//注册服务健康检查
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的服务中使用轮询的策略进行负载均衡的服务分发
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格式的文件
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服务信息的一致,不然服务会受到影响
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、创建动态客户端
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。