go项目实战
结合之前的基础,我们这里给出自己的实践环节。先给出完整的步骤
- 创建项目目录结构。
- 初始化 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-servicego 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 # 我是本地启的dockerport: 5432user: postgrespassword: yourpassworddbname: scaling_group_dbsslmode: disabletimezone: Asia/Shanghaiserver:http_port: 8080grpc_port: 50051
其中config\config.go
package configimport ("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 Configviper.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 stringMinInstanceCount int32MaxInstanceCount int32Region stringZone stringStatus stringCreatedAt int64
}
实现Repository层
在 internal/repository/scaling_group_repository.go
中实现数据访问层:
package repositoryimport ("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.ScalingGroupif 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 serviceimport ("context""fmt""scaling-group-service/api/proto/open/v1""scaling-group-service/internal/model""scaling-group-service/internal/repository""time"
)type ScalingGroupService struct {v1.UnimplementedScalingGroupServiceServerrepo *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- 开头的 IDid := 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 serverimport ("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 serverimport ("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 shutdownstop := make(chan os.Signal, 1)signal.Notify(stop, os.Interrupt)<-stopctxShutDown, 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 utilsimport ("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 handlerimport ("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 mainimport ("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.GRPCPorthttpPort := cfg.Server.HTTPPortgrpcAddr := fmt.Sprintf("localhost:%d", grpcPort)var wg sync.WaitGroupwg.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 stringMinInstanceCount int32MaxInstanceCount int32Region stringZone stringStatus stringCreatedAt 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
标签。