结合之前的基础,我们这里给出自己的实践环节。先给出完整的步骤
- 创建项目目录结构。
- 初始化 Go 模块。
- 添加必要的依赖。
- 编写 config.yaml 配置文件。
- 定义 Proto 文件并编译。
- 定义 Model。
- 实现 Repository 层。
- 实现 Service 层。
- 实现 Handler 层。
- 实现 Server 层。
- 实现数据库初始化逻辑。
- 启动服务
创建项目目录结构
mkdir -p scaling-group-service/{api/proto/open/v1,cmd,config,internal/{model,repository,service,handler,server,utils},go.mod}
项目目录如下
scaling-group-service % tree
.
├── api # 存放 gRPC 接口定义及相关生成代码
│ └── proto # Protocol Buffers 定义文件
│ └── open # 开放 API 命名空间(可理解为模块或分类)
│ └── v1 # API 版本 v1
│ ├── scaling_group.pb.go # 由 proto 编译生成的 Go 数据结构(pb)
│ ├── scaling_group.pb.gw.go # gRPC-Gateway 生成的 HTTP 路由代理代码
│ ├── scaling_group.proto # 原始的 gRPC 接口定义文件(proto3 格式)
│ └── scaling_group_grpc.pb.go# gRPC 服务接口和客户端的 Go 实现代码
├── cmd # 命令行入口目录
│ └── main.go # 程序主入口,用于启动 gRPC 和 HTTP 服务
├── config # 配置相关目录
│ ├── config.go # 配置加载逻辑(如读取 YAML 文件)
│ └── config.yml # YAML 格式的配置文件,包含数据库、端口等配置
├── go.mod # Go 模块描述文件,定义模块路径和依赖
├── go.sum # Go 模块依赖校验文件,记录依赖的哈希值
└── internal # 项目核心代码目录(Go 推荐使用 internal)
├── handler # HTTP 请求处理器(适配 gRPC-Gateway)
│ └── scaling_group_handler.go # ScalingGroup 的 HTTP 请求处理逻辑
├── model # 数据模型目录
│ └── scaling_group.go # ScalingGroup 的结构体定义,对应数据库表
├── repository # 数据访问层(DAO),负责与数据库交互
│ └── scaling_group_repository.go # ScalingGroup 的数据库操作逻辑(如 CRUD)
├── server # 服务启动逻辑目录
│ ├── grpc_server.go # 启动 gRPC 服务,注册服务实现
│ └── http_server.go # 启动 HTTP 服务(gRPC-Gateway),注册路由
├── service # 业务逻辑层目录
│ └── scaling_group_service.go # ScalingGroup 的业务逻辑实现(gRPC 接口实现)
└── utils # 工具类函数目录
└── db.go # 数据库连接工具函数(如 PostgreSQL 初始化)
初始化go模块
cd scaling-group-service
go mod init scaling-group-service
生成一个go.mod
文件,如下所示:
module scaling-group-service
go 1.23.4
添加依赖
我们需要添加一些必要的依赖,包括Viper, GORM和gRPC等,分别用于读取配置(数据库信息)文件、GORM数据库操作,gRPC等
go get github.com/spf13/viper
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get google.golang.org/grpc
go get github.com/grpc-ecosystem/grpc-gateway/v2
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
此时go.mod
中新增如下内容
编写配置文件
在config/config.yml
文件
database:
host: localhost # 我是本地启的docker
port: 5432
user: postgres
password: yourpassword
dbname: scaling_group_db
sslmode: disable
timezone: Asia/Shanghai
server:
http_port: 8080
grpc_port: 50051
其中config\config.go
package config
import (
"fmt"
"github.com/spf13/viper"
)
//type Config struct {
// Database struct {
// Host string `yaml:"host"`
// Port int `yaml:"port"`
// User string `yaml:"user"`
// Password string `yaml:"password"`
// DBName string `yaml:"dbname"`
// SSLMode string `yaml:"sslmode"`
// Timezone string `yaml:"timezone"`
// } `yaml:"database"`
//
// Server struct {
// HTTPPort int `yaml:"http_port"`
// GRPCPort int `yaml:"grpc_port"`
// } `yaml:"server"`
//}
type Config struct {
Database struct {
Host string `yaml:"host" mapstructure:"host"`
Port int `yaml:"port" mapstructure:"port"`
User string `yaml:"user" mapstructure:"user"`
Password string `yaml:"password" mapstructure:"password"`
DBName string `yaml:"dbname" mapstructure:"dbname"`
SSLMode string `yaml:"sslmode" mapstructure:"sslmode"`
Timezone string `yaml:"timezone" mapstructure:"timezone"`
} `yaml:"database" mapstructure:"database"`
Server struct {
HTTPPort int `yaml:"http_port" mapstructure:"http_port"`
GRPCPort int `yaml:"grpc_port" mapstructure:"grpc_port"`
} `yaml:"server" mapstructure:"server"`
}
func LoadConfig(path string) (Config, error) {
var cfg Config
viper.AddConfigPath(path)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return cfg, fmt.Errorf("failed to read config file: %w", err)
}
fmt.Printf("Raw config: %+v\n", viper.AllSettings())
if err := viper.Unmarshal(&cfg); err != nil {
return cfg, fmt.Errorf("unable to decode into struct: %w", err)
}
fmt.Printf("Loaded config: %+v\n", cfg)
return cfg, nil
}
定义Proto文件
在 api/proto/open/v1/scaling_group.proto
中定义服务接口:
syntax = "proto3";
package open.v1;
option go_package = "scaling-group-service/api/proto/open/v1;v1";
import "google/api/annotations.proto";
message ScalingGroup {
string id = 1;
string name = 2;
int32 min_instance_count = 3;
int32 max_instance_count = 4;
string region = 5;
string zone = 6;
string status = 7;
int64 created_at = 8;
}
message CreateScalingGroupRequest {
string name = 1;
int32 min_instance_count = 2;
int32 max_instance_count = 3;
string region = 4;
string zone = 5;
}
message CreateScalingGroupResponse {
string id = 1;
}
message DeleteScalingGroupRequest {
string id = 1;
}
message DeleteScalingGroupResponse {}
message DescribeScalingGroupRequest {
string id = 1;
}
message DescribeScalingGroupResponse {
ScalingGroup group = 1;
}
message ModifyScalingGroupRequest {
string id = 1;
optional int32 min_instance_count = 2;
optional int32 max_instance_count = 3;
optional string name = 4;
}
message ModifyScalingGroupResponse {}
service ScalingGroupService {
rpc CreateScalingGroup(CreateScalingGroupRequest) returns (CreateScalingGroupResponse) {
option (google.api.http) = {
post: "/v1/scaling-groups"
body: "*"
};
}
rpc DeleteScalingGroup(DeleteScalingGroupRequest) returns (DeleteScalingGroupResponse) {
option (google.api.http) = {
delete: "/v1/scaling-groups/{id}"
};
}
rpc DescribeScalingGroup(DescribeScalingGroupRequest) returns (DescribeScalingGroupResponse) {
option (google.api.http) = {
get: "/v1/scaling-groups/{id}"
};
}
rpc ModifyScalingGroup(ModifyScalingGroupRequest) returns (ModifyScalingGroupResponse) {
option (google.api.http) = {
put: "/v1/scaling-groups/{id}"
body: "*"
};
}
}
为了生成 Go 代码,您需要使用 protoc
编译器以及相关的插件。首先,确保您已经安装了protoc
和必要的插件。如果尚未安装,请参考以下命令进行安装:
ggo install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
然后,在项目根目录下运行以下命令来编译 .proto 文件:
scaling-group-service % protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. \
--grpc-gateway_opt=paths=source_relative \
--openapiv2_out=./swagger \
--openapiv2_opt=logtostderr=true \
api/proto/open/v1/scaling_group.proto
此时会报错如下:
google/api/annotations.proto: File not found.
launch_template.proto:5:1: Import "google/api/annotations.proto" was not found or had errors.
注意:我们使用了 google/api/annotations.proto,需要下载这个文件到本地或通过 proto import path解析
如果你遇到找不到 google/api/annotations.proto 的问题,可以克隆官方仓库:
mkdir -p third_party/google/api
curl -o third_party/google/api/annotations.proto https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto
然后编译时加上 -Ithird_party
参数。
或者使用
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@latest
之所以要下载这个,因为github.com\grpc-ecosystem\grpc-gateway@latest\third_party\googleapis\google\api
目录下就有我们需要的annotations.proto
文件。
执行上述下载命令之后,就会将protoc-gen-grpc-gateway
下载到电脑的GOPATH
下,自己电脑的GOPATH
可以通过命令go env
查看.
echo $GOPATH
/Users/zhiyu/go
dance@MacBook-Pro v2@v2.27.1 % cd /Users/zhiyu/go/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.14.5/third_party/googleapis
dance@MacBook-Pro googleapis % ls
LICENSE README.grpc-gateway google
我们引用本地的路劲,现在再次执行protoc
protoc \
-I . \
-I $(go env GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. \
--grpc-gateway_opt=paths=source_relative \
api/proto/open/v1/scaling_group.proto
此时会在v1
目录下生成两个文件
api/proto/open/v1/scaling_group.pb.go
: 包含消息类型和序列化逻辑。api/proto/open/v1/scaling_group_grpc.pb.go
: 包含gRPC
服务的客户端和服务器端接口。api/proto/open/v1/scaling_group.pb.gw.go
: HTTP 路由绑定代码。
定义Model
在 internal/model/scaling_group.go
中定义模型:
package model
// import "gorm.io/gorm"
type ScalingGroup struct {
ID string `gorm:"primaryKey"`
Name string
MinInstanceCount int32
MaxInstanceCount int32
Region string
Zone string
Status string
CreatedAt int64
}
实现Repository层
在 internal/repository/scaling_group_repository.go
中实现数据访问层:
package repository
import (
"context"
"errors"
"gorm.io/gorm"
"scaling-group-service/internal/model"
)
type ScalingGroupRepository struct {
db *gorm.DB
}
func NewScalingGroupRepository(db *gorm.DB) *ScalingGroupRepository {
return &ScalingGroupRepository{db: db}
}
func (r *ScalingGroupRepository) Create(ctx context.Context, group *model.ScalingGroup) error {
return r.db.WithContext(ctx).Create(group).Error
}
func (r *ScalingGroupRepository) GetByID(ctx context.Context, id string) (*model.ScalingGroup, error) {
var group model.ScalingGroup
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&group).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &group, nil
}
func (r *ScalingGroupRepository) Update(ctx context.Context, group *model.ScalingGroup) error {
return r.db.WithContext(ctx).
Model(group).
Where("id = ?", group.ID).
Save(group).Error
}
func (r *ScalingGroupRepository) Delete(ctx context.Context, id string) error {
return r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.ScalingGroup{}).Error
}
实现Service层
在internal/service/scaling_group_service.go
中实现业务逻辑层:
package service
import (
"context"
"fmt"
"scaling-group-service/api/proto/open/v1"
"scaling-group-service/internal/model"
"scaling-group-service/internal/repository"
"time"
)
type ScalingGroupService struct {
v1.UnimplementedScalingGroupServiceServer
repo *repository.ScalingGroupRepository
}
func NewScalingGroupService(repo *repository.ScalingGroupRepository) *ScalingGroupService {
return &ScalingGroupService{
repo: repo,
}
}
func (s *ScalingGroupService) CreateScalingGroup(ctx context.Context, req *v1.CreateScalingGroupRequest) (*v1.CreateScalingGroupResponse, error) {
// 修改这里:生成 asg- 开头的 ID
id := fmt.Sprintf("asg-%d", time.Now().UnixNano())
group := &model.ScalingGroup{
ID: id,
Name: req.Name,
MinInstanceCount: req.MinInstanceCount,
MaxInstanceCount: req.MaxInstanceCount,
Region: req.Region,
Zone: req.Zone,
Status: "active",
CreatedAt: time.Now().Unix(),
}
if err := s.repo.Create(ctx, group); err != nil {
return nil, err
}
return &v1.CreateScalingGroupResponse{Id: group.ID}, nil
}
func (s *ScalingGroupService) DeleteScalingGroup(ctx context.Context, req *v1.DeleteScalingGroupRequest) (*v1.DeleteScalingGroupResponse, error) {
if err := s.repo.Delete(ctx, req.Id); err != nil {
return nil, err
}
return &v1.DeleteScalingGroupResponse{}, nil
}
func (s *ScalingGroupService) DescribeScalingGroup(ctx context.Context, req *v1.DescribeScalingGroupRequest) (*v1.DescribeScalingGroupResponse, error) {
group, err := s.repo.GetByID(ctx, req.Id)
if err != nil {
return nil, err
}
if group == nil {
return &v1.DescribeScalingGroupResponse{}, nil
}
return &v1.DescribeScalingGroupResponse{
Group: &v1.ScalingGroup{
Id: group.ID,
Name: group.Name,
MinInstanceCount: group.MinInstanceCount,
MaxInstanceCount: group.MaxInstanceCount,
Region: group.Region,
Zone: group.Zone,
Status: group.Status,
CreatedAt: group.CreatedAt,
},
}, nil
}
func (s *ScalingGroupService) ModifyScalingGroup(ctx context.Context, req *v1.ModifyScalingGroupRequest) (*v1.ModifyScalingGroupResponse, error) {
existing, err := s.repo.GetByID(ctx, req.Id)
if err != nil || existing == nil {
return nil, fmt.Errorf("group not found")
}
if req.Name != nil {
existing.Name = *req.Name
}
if req.MinInstanceCount != nil {
existing.MinInstanceCount = *req.MinInstanceCount
}
if req.MaxInstanceCount != nil {
existing.MaxInstanceCount = *req.MaxInstanceCount
}
if err := s.repo.Update(ctx, existing); err != nil {
return nil, err
}
return &v1.ModifyScalingGroupResponse{}, nil
}
实现Server层
server/grpc_server.go
package server
import (
"fmt"
"google.golang.org/grpc"
"net"
"scaling-group-service/api/proto/open/v1"
"scaling-group-service/internal/service"
)
func StartGRPCServer(grpcPort int, scalingGroupService *service.ScalingGroupService) error {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
if err != nil {
return err
}
grpcServer := grpc.NewServer()
v1.RegisterScalingGroupServiceServer(grpcServer, scalingGroupService)
fmt.Printf("gRPC server listening on port %d\n", grpcPort)
return grpcServer.Serve(lis)
}
另一个是http_server.go
package server
import (
"context"
"fmt"
"google.golang.org/grpc"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"scaling-group-service/internal/handler"
)
func StartHTTPServer(ctx context.Context, httpPort int, grpcAddr string) error {
mux := runtime.NewServeMux()
if err := handler.RegisterHandlers(ctx, mux, grpcAddr, []grpc.DialOption{grpc.WithInsecure()}); err != nil {
return err
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", httpPort),
Handler: mux,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server ListenAndServe: %v", err)
}
}()
fmt.Printf("HTTP server listening on port %d\n", httpPort)
// Graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
ctxShutDown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer func() {
cancel()
}()
if err := srv.Shutdown(ctxShutDown); err != nil {
log.Fatalf("HTTP Server Shutdown failed: %v", err)
}
return nil
}
实现utils
db.go
文件
package utils
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"scaling-group-service/config"
"scaling-group-service/internal/model"
)
func ConnectDB(cfg config.Config) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.User,
cfg.Database.Password,
cfg.Database.DBName,
cfg.Database.SSLMode,
cfg.Database.Timezone,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
err = db.AutoMigrate(&model.ScalingGroup{})
if err != nil {
return nil, err
}
return db, nil
}
handler
实现handler/scaling_group_handler.go
package handler
import (
"context"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"scaling-group-service/api/proto/open/v1"
)
func RegisterHandlers(ctx context.Context, mux *runtime.ServeMux, grpcAddr string, opts []grpc.DialOption) error {
conn, err := grpc.DialContext(ctx, grpcAddr, opts...)
if err != nil {
return err
}
//defer conn.Close()
client := v1.NewScalingGroupServiceClient(conn)
return v1.RegisterScalingGroupServiceHandlerClient(ctx, mux, client)
}
入口函数cmd/main
package main
import (
"context"
"fmt"
"log"
"sync"
"scaling-group-service/config"
"scaling-group-service/internal/repository"
"scaling-group-service/internal/server"
"scaling-group-service/internal/service"
"scaling-group-service/internal/utils"
)
func main() {
cfg, err := config.LoadConfig("./config")
if err != nil {
log.Fatalf("Load config error: %v", err)
}
fmt.Printf("Http port: %v\n", cfg.Server.HTTPPort)
fmt.Printf("Http port: %v\n", cfg.Server.GRPCPort)
db, err := utils.ConnectDB(cfg)
if err != nil {
log.Fatalf("Connect DB error: %v", err)
}
repo := repository.NewScalingGroupRepository(db)
svc := service.NewScalingGroupService(repo)
grpcPort := cfg.Server.GRPCPort
httpPort := cfg.Server.HTTPPort
grpcAddr := fmt.Sprintf("localhost:%d", grpcPort)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if err := server.StartGRPCServer(grpcPort, svc); err != nil {
log.Fatalf("Start GRPC server error: %v", err)
}
}()
go func() {
defer wg.Done()
if err := server.StartHTTPServer(context.Background(), httpPort, grpcAddr); err != nil {
log.Fatalf("Start HTTP server error: %v", err)
}
}()
wg.Wait()
}
准备好之后,创建db
使用docker run 创建PostgreSQL
- 启动postgreslq
运行 PostgreSQL 容器
docker run --name scaling-db \
-e POSTGRES_PASSWORD=yourpassword \
-p 5432:5432 \
-d postgres:14
- 创建库和表
创建数据库
docker exec scaling-db psql -U postgres -c "CREATE DATABASE scaling_group_db;"
注意这里的model结构如下
package model
// import "gorm.io/gorm"
type ScalingGroup struct {
ID string `gorm:"primaryKey"`
Name string
MinInstanceCount int32
MaxInstanceCount int32
Region string
Zone string
Status string
CreatedAt int64
}
所以创建表结构
# 创建表结构
docker exec scaling-db psql -U postgres -d scaling_group_db -c "
docker exec scaling-db psql -U postgres -d scaling_group_db -c "
CREATE TABLE IF NOT EXISTS scaling_groups (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
min_instance_count INT,
max_instance_count INT,
region VARCHAR(255),
zone VARCHAR(255),
status VARCHAR(255),
created_at BIGINT
);
"
- 查看数据库表
# 列出所有数据库
docker exec scaling-db psql -U postgres -c "\l"
# 查看表结构
docker exec scaling-db psql -U postgres -d scaling_group_db -c "\d scaling_groups"
请求与测试
go run cmd/main.go
创建资源
curl -X POST http://localhost:8080/v1/scaling-groups \
-H "Content-Type: application/json" \
-d '{
"name": "test-group",
"min_instance_count": 1,
"max_instance_count": 3,
"region": "cn-beijing",
"zone": "cn-beijing-1"
}'
登录数据库查询
docker exec -it scaling-db psql -U postgres -d scaling_group_db
查询资源
curl http://localhost:8080/v1/scaling-groups/asg-1752661325196631000
修改资源
curl -X PUT http://localhost:8080/v1/scaling-groups/asg-1752661325196631000 \
-H "Content-Type: application/json" \
-d '{
"min_instance_count": 2,
"max_instance_count": 5,
"name": "updated-name"
}'
删除资源
curl -X DELETE http://localhost:8080/v1/scaling-groups/asg-1752661325196631000
Q&A
在config/config.go
中我们注释了如下的部分
type Config struct {
Database struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
DBName string `yaml:"dbname"`
SSLMode string `yaml:"sslmode"`
Timezone string `yaml:"timezone"`
} `yaml:"database"`
Server struct {
HTTPPort int `yaml:"http_port"`
GRPCPort int `yaml:"grpc_port"`
} `yaml:"server"`
}
执行go run cmd/main.go
的时候,发现服务的port是0
✅ viper 成功读取了 config.yaml 中
的 server.http_port
和 server.grpc_port
❌ 但 viper.Unmarshal(&cfg)
没有把值映射到结构体中的 Server.HTTPPort
和 Server.GRPCPort
你正在使用 viper 的默认解码器(mapstructure
),它不支持 yaml tag
,只支持 mapstructure tag
。也就是说:
HTTPPort int `yaml:"http_port"`
会被 YAML 正确解析,但 不会被 viper.Unmarshal
识别,因为 viper 默认使用 mapstructure
标签。