go项目实战二
还是参考阿里云创建伸缩组的接口。挑选几个接口用来继续练习如何开发go项目,作为学习笔记。
项目目录如下所示:
autoscaling-app % tree
.
├── cmd
│ └── main.go
├── go.mod
├── go.sum
├── openapi
│ └── pkg
│ └── proto
│ └── autoscaling.swagger.json
└── pkg
├── config
│ └── config.go
├── model
│ └── scaling_group.go
├── proto
│ ├── autoscaling.pb.go
│ ├── autoscaling.pb.gw.go
│ ├── autoscaling.proto
│ └── autoscaling_grpc.pb.go
├── repository
│ └── scaling_group_repo.go
├── server
│ └── grpc_server
│ └── grpc_server.go
└── service
└── scaling_group_service.go
初始化项目
mkdir -p autoscaling-app/{cmd,pkg/{config,model,repository,service,proto,openapi}}
cd autoscaling-app
go mod init autoscaling-app
标题准备 .proto 文件
创建proto/autoscaling.proto
文件
syntax = "proto3";package autoscaling;import "google/api/annotations.proto";option go_package = "pkg/proto;proto";service AutoScalingService {rpc CreateScalingGroup (CreateScalingGroupRequest) returns (CreateScalingGroupResponse) {option (google.api.http) = {post: "/v1/scalingGroups"body: "*"};}rpc GetScalingGroup (GetScalingGroupRequest) returns (GetScalingGroupResponse) {option (google.api.http) = {get: "/v1/scalingGroups/{id}"};}rpc ListScalingGroups (ListScalingGroupsRequest) returns (ListScalingGroupsResponse) {option (google.api.http) = {get: "/v1/scalingGroups"};}rpc UpdateScalingGroup (UpdateScalingGroupRequest) returns (UpdateScalingGroupResponse) {option (google.api.http) = {put: "/v1/scalingGroups/{id}"body: "*"};}rpc DeleteScalingGroup (DeleteScalingGroupRequest) returns (DeleteScalingGroupResponse) {option (google.api.http) = {delete: "/v1/scalingGroups/{id}"};}
}message CreateScalingGroupRequest {string name = 1;int32 min_instance_num = 2;int32 max_instance_num = 3;repeated string subnet_ids = 4;string health_check_type = 5;
}message CreateScalingGroupResponse {string id = 1;int64 created_at = 2;
}message GetScalingGroupRequest {string id = 1;
}message GetScalingGroupResponse {string id = 1;string name = 2;int32 min_instance_num = 3;int32 max_instance_num = 4;repeated string subnet_ids = 5;string health_check_type = 6;int64 created_at = 7;
}message ListScalingGroupsRequest {}message ListScalingGroupsResponse {repeated GetScalingGroupResponse scaling_groups = 1;
}message UpdateScalingGroupRequest {string id = 1;string name = 2;int32 min_instance_num = 3;int32 max_instance_num = 4;repeated string subnet_ids = 5;string health_check_type = 6;
}message UpdateScalingGroupResponse {}message DeleteScalingGroupRequest {string id = 1;
}message DeleteScalingGroupResponse {}
这里为什么不使用post: "/v1/CreateScalingGroups"而是使用post: “/v1/scalingGroups”
因为这违背了 RESTful API 的核心设计原则:
RESTful API 应该“通过标准 HTTP 方法 + 资源路径”来表达操作,而不是在路径中用动词描述操作。
正确做法:使用统一资源路径 + HTTP 方法区分操作
发现最后是一个:action的方式,这个是GoogleAPI的风格,表示对一个资源的动作,而不是/v1/scalingGroups/{id}/start
,如果是/start
是RESTful,表示访问资源下的资资源。
安装必要工具和依赖
安装 Protobuf 编译器插件
go 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# PostgreSQL 驱动
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres# 加载 .env 文件
go get -u github.com/joho/godotenv
获取 google/api/annotations.proto
git clone https://github.com/googleapis/googleapis.git $GOPATH/src/github.com/googleapis/googleapis
确保你的 protoc
命令能找到它(通过 -I
参数指定路径)。
# 项目所在的根目录下
autoscaling-app % protoc \--go_out=. \--go-grpc_out=. \--grpc-gateway_out=. \--openapiv2_out=./openapi \-I . \-I $GOPATH/src/github.com/googleapis/googleapis \pkg/proto/autoscaling.proto
-
- –go_out=. 会生成 .pb.go 文件,放在 pkg/proto/ 目录下
-
- –go-grpc_out=. 会生成 _grpc.pb.go 文件。
-
- –grpc-gateway_out=. 会生成 .pb.gw.go 文件。
-
- –openapiv2_out=./openapi 会在 ./openapi 下生成 OpenAPI 文档。
配置文件.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=yourpassword
DB_NAME=autoscaling
HTTP_PORT=:8080
GRPC_PORT=:50051
配置管理(pkg/config/config.go)
package configimport (//"github.com/spf13/viper""github.com/joho/godotenv""os"
)type Config struct {DBHost stringDBPort stringDBUser stringDBPassword stringDBName stringHTTPPort stringGRPCPort string
}// 加载配置文件
func LoadConfig() (*Config, error) {err := godotenv.Load()if err != nil {return nil, err}return &Config{DBHost: os.Getenv("DB_HOST"),DBPort: os.Getenv("DB_PORT"),DBUser: os.Getenv("DB_USER"),DBPassword: os.Getenv("DB_PASSWORD"),DBName: os.Getenv("DB_NAME"),HTTPPort: os.Getenv("HTTP_PORT"),GRPCPort: os.Getenv("GRPC_PORT"),}, nil
}
数据模型(pkg/model/scaling_group.go)
package modelimport "github.com/lib/pq"// 定义数据库模型
// omitempty 表示在该字段的值其类型的零值时,在序列化的时候JSON会忽略type ScalingGroup struct {ID string `json:"id" gorm:"type:uuid;primary_key;default:gen_random_uuid()"`Name string `json:"name" gorm:"not null"`MinInstanceNum int `json:"min_instance_num" gorm:"not null;check:min_instance_num >= 0"`MaxInstanceNum int `json:"max_instance_num" gorm:"not null;check:max_instance_num >= min_instance_num"`//SubnetIDs []string `json:"subnet_ids" gorm:"type:text[]"`SubnetIDs pq.StringArray `json:"subnet_ids" gorm:"type:text[]"` // 修改这里HealthCheckType string `json:"health_check_type" gorm:"not null"`CreatedAt int64 `json:"created_at" gorm:"autoCreateTime:milli"`
}
数据库 Repository(pkg/repository/scaling_group_repo.go)
package repositoryimport ("autoscaling-app/pkg/model""gorm.io/driver/postgres""gorm.io/gorm""log"
)type ScalingGroupRepository struct {db *gorm.DB
}func NewScalingGroupRepository(dsn string) (*ScalingGroupRepository, error) {db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})if err != nil {return nil, err}log.Println("Connected to PostgreSQL")if err := db.AutoMigrate(&model.ScalingGroup{}); err != nil {return nil, err}return &ScalingGroupRepository{db: db}, nil
}func (r *ScalingGroupRepository) Create(group *model.ScalingGroup) error {return r.db.Create(group).Error
}func (r *ScalingGroupRepository) GetAll() ([]model.ScalingGroup, error) {var groups []model.ScalingGrouperr := r.db.Find(&groups).Errorreturn groups, err
}func (r *ScalingGroupRepository) GetByID(id string) (*model.ScalingGroup, error) {var group model.ScalingGrouperr := r.db.Where("id = ?", id).First(&group).Errorreturn &group, err
}func (r *ScalingGroupRepository) Update(id string, group *model.ScalingGroup) error {return r.db.Where("id = ?", id).Updates(group).Error
}func (r *ScalingGroupRepository) Delete(id string) error {return r.db.Where("id = ?", id).Delete(&model.ScalingGroup{}).Error
}
业务逻辑(pkg/service/scaling_group_service.go)
package service//负责业务逻辑的实现,它不关心请求是如何到达的(HTTP、gRPC 等),只关注如何处理数据和完成业务需求。import ("autoscaling-app/pkg/model""autoscaling-app/pkg/repository"
)type ScalingGroupService struct {repo *repository.ScalingGroupRepository
}func NewScalingGroupService(repo *repository.ScalingGroupRepository) *ScalingGroupService {return &ScalingGroupService{repo: repo}
}func (s *ScalingGroupService) Create(req *model.ScalingGroup) (*model.ScalingGroup, error) {err := s.repo.Create(req)return req, err
}func (s *ScalingGroupService) GetAll() ([]model.ScalingGroup, error) {return s.repo.GetAll()
}func (s *ScalingGroupService) GetByID(id string) (*model.ScalingGroup, error) {return s.repo.GetByID(id)
}func (s *ScalingGroupService) Update(id string, req *model.ScalingGroup) error {return s.repo.Update(id, req)
}func (s *ScalingGroupService) Delete(id string) error {return s.repo.Delete(id)
}
配置 PostgreSQL 服务
启动PostgreSQL
如果你使用的是 Ubuntu 或 Debian:
# 启动 PostgreSQL 服务
sudo service postgresql start# 检查状态
sudo service postgresql status
如果你使用的是 macOS(brew 安装):
brew services start postgresql
创建数据库
sudo -u postgres psql
在 psql 中执行:
-- 创建数据库
CREATE DATABASE autoscaling;-- 创建用户(可选)
CREATE USER autoscaling_user WITH PASSWORD 'yourpassword';-- 授权(可选)
GRANT ALL PRIVILEGES ON DATABASE autoscaling TO autoscaling_user;-- 退出
\q
创建数据表
使用 GORM 自动迁移(推荐)
在我们之前写的 pkg/repository/scaling_group_repository.go
中已经包含了自动迁移:
if err := db.AutoMigrate(&model.ScalingGroup{}); err != nil {return nil, err
}
只要你启动服务,GORM
会自动帮你创建表结构。
✅ 你不需要手动建表,除非你想自定义索引、约束等.
✅ 或者手动建表(可选)
docker run数据库
当然上面的方法也可以使用docker run启动postgresql并创建数据库
# 运行 PostgreSQL 容器
docker run --name autoscaling-postgres \-e POSTGRES_DB=autoscaling \-e POSTGRES_USER=postgres \-e POSTGRES_PASSWORD=yourpassword \-p 5432:5432 \-d \postgres:14
启动之后
ec112f3c6378d028db28ea1d89236785030d2b00118cbf398e0a58ca5bb0b911
#测试连接.输入密码 yourpassword,如果进入 psql 命令行,说明数据库已成功创建。
Book-Pro ~ % psql -h localhost -U postgres -d autoscaling -W
口令:
psql (16.9 (Homebrew), 服务器 14.18 (Debian 14.18-1.pgdg120+1))
输入 “help” 来获取帮助信息.
autoscaling=#
server/grpc_server.go
- 实现对外暴露的接口(如 gRPC 接口、HTTP 接口)
- 接收请求、解析参数、调用 service 层处理业务逻辑
- 返回响应(gRPC 响应、JSON 响应等)
- 不包含核心业务逻辑
package serverimport ("context""autoscaling-app/pkg/model""autoscaling-app/pkg/proto""autoscaling-app/pkg/service"
)// AutoScalingServer 实现 gRPC 接口
type AutoScalingServer struct {proto.UnimplementedAutoScalingServiceServerService *service.ScalingGroupService // 导出字段,供 main.go 初始化
}func (s *AutoScalingServer) CreateScalingGroup(ctx context.Context, req *proto.CreateScalingGroupRequest) (*proto.CreateScalingGroupResponse, error) {scalingGroup := &model.ScalingGroup{Name: req.Name,MinInstanceNum: int(req.MinInstanceNum),MaxInstanceNum: int(req.MaxInstanceNum),SubnetIDs: req.SubnetIds,HealthCheckType: req.HealthCheckType,}created, err := s.Service.Create(scalingGroup)if err != nil {return nil, err}return &proto.CreateScalingGroupResponse{Id: created.ID,CreatedAt: created.CreatedAt,}, nil
}func (s *AutoScalingServer) GetScalingGroup(ctx context.Context, req *proto.GetScalingGroupRequest) (*proto.GetScalingGroupResponse, error) {group, err := s.Service.GetByID(req.Id)if err != nil {return nil, err}return &proto.GetScalingGroupResponse{Id: group.ID,Name: group.Name,MinInstanceNum: int32(group.MinInstanceNum),MaxInstanceNum: int32(group.MaxInstanceNum),SubnetIds: group.SubnetIDs,HealthCheckType: group.HealthCheckType,CreatedAt: group.CreatedAt,}, nil
}func (s *AutoScalingServer) ListScalingGroups(ctx context.Context, req *proto.ListScalingGroupsRequest) (*proto.ListScalingGroupsResponse, error) {groups, err := s.Service.GetAll()if err != nil {return nil, err}var responses []*proto.GetScalingGroupResponsefor _, g := range groups {responses = append(responses, &proto.GetScalingGroupResponse{Id: g.ID,Name: g.Name,MinInstanceNum: int32(g.MinInstanceNum),MaxInstanceNum: int32(g.MaxInstanceNum),SubnetIds: g.SubnetIDs,HealthCheckType: g.HealthCheckType,CreatedAt: g.CreatedAt,})}return &proto.ListScalingGroupsResponse{ScalingGroups: responses}, nil
}func (s *AutoScalingServer) UpdateScalingGroup(ctx context.Context, req *proto.UpdateScalingGroupRequest) (*proto.UpdateScalingGroupResponse, error) {scalingGroup := &model.ScalingGroup{Name: req.Name,MinInstanceNum: int(req.MinInstanceNum),MaxInstanceNum: int(req.MaxInstanceNum),SubnetIDs: req.SubnetIds,HealthCheckType: req.HealthCheckType,}err := s.Service.Update(req.Id, scalingGroup)return &proto.UpdateScalingGroupResponse{}, err
}func (s *AutoScalingServer) DeleteScalingGroup(ctx context.Context, req *proto.DeleteScalingGroupRequest) (*proto.DeleteScalingGroupResponse, error) {err := s.Service.Delete(req.Id)return &proto.DeleteScalingGroupResponse{}, err
}
创建mian.go并启动
package mainimport ("context""fmt""log""net""net/http""github.com/grpc-ecosystem/grpc-gateway/v2/runtime""google.golang.org/grpc""google.golang.org/grpc/reflection""autoscaling-app/pkg/config"pb "autoscaling-app/pkg/proto""autoscaling-app/pkg/repository""autoscaling-app/pkg/server/grpc_server""autoscaling-app/pkg/service"
)func main() {// 1. 加载配置cfg, err := config.LoadConfig()if err != nil {log.Fatalf("Load config error: %v", err)}// 2. 构建数据库连接dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",cfg.DBHost, cfg.DBPort, cfg.DBUser, cfg.DBPassword, cfg.DBName)// 3. 初始化数据库仓库repo, err := repository.NewScalingGroupRepository(dsn)if err != nil {log.Fatalf("Failed to initialize repository: %v", err)}// 4. 初始化业务服务svc := service.NewScalingGroupService(repo)// 5. 初始化 gRPC 服务grpcServer := grpc.NewServer()reflection.Register(grpcServer)// 6. 初始化 gRPC 接口实现(从 server 包中引用)autoScalingServer := &server.AutoScalingServer{Service: svc}pb.RegisterAutoScalingServiceServer(grpcServer, autoScalingServer)// 7. 启动 gRPC 服务go func() {lis, err := net.Listen("tcp", cfg.GRPCPort)if err != nil {log.Fatalf("Failed to listen on gRPC port: %v", err)}log.Printf("gRPC server is running at %s", cfg.GRPCPort)if err := grpcServer.Serve(lis); err != nil {log.Fatalf("Failed to serve gRPC: %v", err)}}()// 8. 启动 HTTP 网关ctx := context.Background()ctx, cancel := context.WithCancel(ctx)defer cancel()mux := runtime.NewServeMux()opts := []grpc.DialOption{grpc.WithInsecure()}err = pb.RegisterAutoScalingServiceHandlerFromEndpoint(ctx, mux, cfg.GRPCPort, opts)if err != nil {log.Fatalf("Failed to register HTTP gateway: %v", err)}log.Printf("HTTP server is running at %s", cfg.HTTPPort)if err := http.ListenAndServe(cfg.HTTPPort, mux); err != nil {log.Fatalf("Failed to serve HTTP: %v", err)}
}
main.go 只负责启动和初始化,业务逻辑应按模块拆分到 handler、service、repository 等目录中,这样代码结构清晰、易于维护、便于测试和扩展。
运行与测试
启动
go run cmd/main.go
测试RPC服务
- 使用evans工具
# 安装evans go install github.com/ktr0731/evans@latest
启动evans cli,确保服务已经在监听50051,-r参数表示交互模式。键入evans后,输入
package autoscaling
service AutoScalingService
输入命令如下:
当然也可以使用grpcurl
测试RESTful接口
curl -X POST http://localhost:8080/v1/scalingGroups \-H "Content-Type: application/json" \-d '{"name": "group1","min_instance_num": 1,"max_instance_num": 5,"subnet_ids": ["subnet-a", "subnet-b"],"health_check_type": "ECS"}'
{"id":"8a02bfd4-c1d3-4f9d-931d-3b24755996f2", "createdAt":"1753345069134"}%
curl http://localhost:8080/v1/scalingGroups
{"scalingGroups":[{"id":"8a02bfd4-c1d3-4f9d-931d-3b24755996f2", "name":"group1", "minInstanceNum":1, "maxInstanceNum":5, "subnetIds":["subnet-a", "subnet-b"], "healthCheckType":"ECS", "createdAt":"1753345069134"}]}%
curl --location 'http://localhost:8080/v1/scalingGroups/8a02bfd4-c1d3-4f9d-931d-3b24755996f2'
curl --location --request PUT 'http://localhost:8080/v1/scalingGroups/8a02bfd4-c1d3-4f9d-931d-3b24755996f2' \
--header 'Content-Type: application/json' \
--data '{"name": "updated-group-name","minInstanceNum": 2,"maxInstanceNum": 10,"subnetIds": ["subnet-c", "subnet-d"],"healthCheckType": "TCP"
}'
curl --location --request DELETE 'http://localhost:8080/v1/scalingGroups/8a02bfd4-c1d3-4f9d-931d-3b24755996f2' \
--data ''
总结
✅使用 .proto 文件定义接口 + google.api.http 注解
✅ 使用 grpc-gateway 自动生成 RESTful 网关
✅ 模型、仓储、服务、接口模块清晰分离
✅ CreatedAt 动态生成(基于 GORM 的 autoCreateTime)
✅ 同时支持 gRPC 和 HTTP/JSON