Golang单体服务开发的最佳实践
Golang单体服务开发的最佳实践
为何撰写此最佳实践
- 对于许多初创公司而言,我们应更专注于尽早交付应用。此时用户基数小,
QPS非常低,我们应采用更简单的技术架构来加速应用的交付,这正是单体架构的优势所在。 - 正如我在演讲中常提到的,当我们使用单体架构快速交付应用时,也需要在开发应用时考虑未来的可能性,我们可以在单体内部清晰地拆分模块。
- 许多开发者询问使用
go-zero开发单体应用的最佳实践是什么。
由于 go-zero 是一个广泛使用的微服务框架,它是我在几个大型项目的完整开发过程中沉淀下来的。我们已充分考虑了单体服务开发的场景。
使用 go-zero 的单体架构,如图所示,同样可以支持大规模的业务量,其中 Service 是一个由多个 Pod 组成的单体服务。

我将详细分享如何使用 go-zero 快速开发一个包含多个模块的单体服务。
单体示例
让我们用一个上传和下载的单体服务来解释 go-zero 单体服务开发的最佳实践,为何使用这样的例子?
go-zero社区经常询问如何为上传文件定义API文件,然后使用goctl自动生成代码。当我最初看到这类问题时,觉得很奇怪,为什么不使用像OSS这样的服务呢?后来我发现很多场景是用户需要上传 excel 文件,然后服务器解析后丢弃该文件。一是文件小,二是服务用户量不大,因此不需要引入OSS,我认为这相当合理。go-zero社区也询问如何通过定义API文件来下载文件,然后由goctl自动生成。之所以通过 Go 提出这样的问题,通常有两个原因:一是业务刚刚起步,部署一个服务来完成所有事情更容易;二是希望利用go-zero内置的JWT认证优势。
这只是一个示例,无需深入探讨上传和下载是否应该用 Go 编写。那么,让我们看看如何使用 go-zero 解决这样一个我们称之为文件服务的单体服务。
单体实现
API 定义
使用过 go-zero 的开发者都知道,我们提供了一种 API 格式文件来描述 RESTful API,然后我们可以通过 goctl 一键生成相应的代码,我们只需要在 logic 文件中填入相应的业务逻辑。让我们看看 download 和 upload 服务是如何定义 API 的。
Download API 定义
示例需求如下。
- 通过路径
/static/<filename>下载名为<filename>的文件 - 直接返回文件内容即可
我们在 api 目录下创建一个名为 download.api 的文件,内容如下。
syntax = "v1"
type DownloadRequest {
File string `path: "file"`
}
service file-api {
@handler DownloadHandler
get /static/:file(DownloadRequest)
}
zero-api 的语法相对不言自明,含义如下。
syntax = "v1"表示这是zero-api的v1语法type DownloadRequest定义了Download的请求格式service file-api定义了Download的请求路由
Upload API 定义
示例需求如下。
- 通过路径
/upload上传文件 - 通过
json返回上传状态,其中code可用于表达比HTTP code更丰富的场景
我们在 api 目录下创建一个名为 upload.api 的文件,内容如下。
syntax = "v1"
type UploadResponse {
Code int `json: "code"`
}
service file-api {
@handler UploadHandler
post /upload returns (UploadResponse)
}
解释如下。
syntax = "v1"表示这是zero-api的v1语法type UploadResponse定义了Upload的返回格式service file-api定义了Upload的请求路由
问题来了
我们已经定义了 Download 和 Upload 服务,但如何将它们放入一个服务中呢?
我不知道你是否注意到了一些细节。
- 无论是
Download还是Upload,我们都为request和response数据定义添加了前缀,没有直接使用诸如Request或Response这样的名称 - 我们在
download.api和upload.api中定义service时,都使用了file-api作为service name,而不是分别使用download-api和upload-api
这样做的目的是,当我们将两个服务放入单体服务时,能够自动生成相应的 Go 代码。让我们看看如何将 Download 和 Upload 合并在一起~
定义单体服务 API
为简单起见,goctl 仅支持接受单个 API 文件作为参数,接受多个 API 文件的问题此处不讨论,如果我们找到简单高效的解决方案,后续可能会支持。
我们在 api 目录中创建一个新的 file.api 文件,内容如下。
syntax = "v1"
import "download.api"
import "upload.api"
这样我们就导入了 Download 和 Upload 服务,类似于 C/C++ 中的 #include。但有几件事需要注意。
- 定义的结构体不能重名
- 所有文件中的
service name必须相同
最外层的
API文件也可以包含部分相同的service定义,但我们建议保持对称,除非这些API确实属于父级别,例如与Download和Upload处于同一逻辑级别,那么它们就不应该在file.api中定义。
至此,我们有了以下文件结构。
.
└── api
├── download.api
├── file.api
└── upload.api
生成单体服务
现在我们已经定义了 API 接口,下一步对于 go-zero 来说就相当直接了(当然,定义 API 本身也很直接,不是吗?)。让我们使用 goctl 来生成单体服务代码。
$ goctl api go -api api/file.api -dir .
让我们看一下生成的文件结构。
.
├── api
│ ├── download.api
│ ├── file.api
│ └── upload.api
├── etc
│ └── file-api.yaml
├── file.go
├─ go.mod
├── go.sum
└── internal
├─ config
│ └─ config.go
├─ handler
│ ├── downloadhandler.go
│ ├── routes.go
│ └── uploadhandler.go
├─ logic
│ ├── downloadlogic.go
│ └── uploadlogic.go
├── svc
│ └── servicecontext.go
└─ types
└─ types.go
让我们按目录解释项目的布局。
api目录:我们之前定义的API接口描述文件,无需多言etc目录:用于存放yaml配置文件,所有配置项都可以写在file-api.yaml文件中file.go:包含main函数的文件,与service同名,去掉了-api后缀internal/config目录:服务的配置定义internal/handler目录:API文件中定义的路由的handler实现internal/logic目录:用于放置每个路由对应的业务处理逻辑,区分handler和logic的原因是为了最小化业务处理部分的依赖,隔离HTTP 请求与逻辑处理代码,并便于后续根据需要拆分为RPC 服务internal/svc目录:用于定义业务逻辑处理的依赖项,我们可以在main中创建依赖资源,并通过ServiceContext传递给handler和logicinternal/types目录:定义API请求和响应的数据结构
我们暂时不做任何更改,直接运行看看效果。
$ go run file.go -f etc/file-api.yaml
Starting server at 0.0.0.0:8888...
实现业务逻辑
接下来我们需要实现相关的业务逻辑,但这里的逻辑真的只是为了演示,所以不要过多关注实现细节,只需理解我们应该在 logic 层编写业务逻辑。
这里做了以下几件事。
- 在配置项中添加
Path设置,用于放置上传的文件,默认我写了当前目录,因为是示例,如下。
type Config struct {
RestConf
// 新增
Path string `json:",default=." `
}
- 调整了请求体大小限制,如下。
Name: file-api
Host: localhost
Port: 8888
# 新增
MaxBytes: 1073741824
- 由于
Download需要将文件写入客户端,我们将ResponseWriter作为io.Writer传递到logic层,修改后的代码如下
func (l *DownloadLogic) Download(req *types.DownloadRequest) error {
logx.Infof("download %s", req.File)
body, err := ioutil.ReadFile(req.File)
if err ! = nil {
return err
}
n, err := l.writer.Write(body)
if err ! = nil {
return err
}
if n < len(body) {
return io.ErrClosedPipe
}
return nil
}
- 由于
Upload需要读取用户上传的文件,我们将http.Request传递到logic层,修改后的代码如下。
func (l *UploadLogic) Upload() (resp *types.UploadResponse, err error) {
l.r.ParseMultipartForm(maxFileSize)
file, handler, err := l.r.FormFile("myFile")
if err ! = nil {
return nil, err
}
defer file.Close()
logx.Infof("upload file: %+v, file size: %d, MIME header: %+v",
handler.Filename, handler.Size, handler.Header)
tempFile, err := os.Create(path.Join(l.svcCtx.Config.Path, handler.Filename))
if err ! = nil {
return nil, err
}
defer tempFile.Close()
io.Copy(tempFile, file)
return &types.UploadResponse{
Code: 0,
}, nil
}
完整代码:zero-examples/monolithic at main · zeromicro/zero-examples · GitHub
我们可以通过运行以下命令启动 file 单体服务。
$ go run file.go -f etc/file-api.yaml
Download 服务可以使用 curl 验证:
$ curl -i "http://localhost:8888/static/file.go"
HTTP/1.1 200 OK
Traceparent: 00-831431c47d162b4decfb6b30fb232556-dd3b383feb1f13a9-00
Date: Mon, 25 Apr 2022 01:50:58 GMT
Content-Length: 584
Content-Type: text/plain; charset=utf-8
...
示例仓库包含 upload.html,浏览器可以打开此文件来尝试 Upload 服务。
单体开发总结
让我总结一下使用 go-zero 开发单体服务的完整流程,如下所示。
- 为每个子模块定义
API文件,例如download.api和upload.api - 定义通用的
API文件,例如file.api。用它来import第 1 步中定义的各个子模块的API文件 - 通过
goctl api go命令生成单体服务框架代码 - 添加和调整配置,实现相应子模块的业务逻辑
此外,goctl 可以根据 SQL 一键生成 CRUD 和 cache 代码,这可以帮助你更快速地开发单体服务。
项目地址
欢迎使用 go-zero 并 star 支持我们!
更多关于Golang单体服务开发的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang单体服务开发的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
使用 go-zero 开发单体服务确实能有效平衡开发效率与架构清晰度。以下针对你提供的示例,补充一些关键实践和代码细节。
1. 配置管理优化
在 etc/file-api.yaml 中,除了基础配置,建议明确区分环境配置(如开发、测试、生产)。可通过 go-zero 的 RestConf 结构体扩展支持:
// internal/config/config.go
type Config struct {
rest.RestConf
Path string `json:",default=./uploads"`
MaxBytes int64 `json:",default=1073741824"` // 1GB
Env string `json:",optional,default=dev"` // 环境标识
}
对应 yaml 文件:
Name: file-api
Host: 0.0.0.0
Port: 8888
MaxBytes: 1073741824
Path: ./uploads
Env: prod # 根据环境切换
2. 统一错误处理
在 logic 层实现自定义错误类型,确保错误信息结构化返回:
// internal/logic/baseerror.go
type BizError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *BizError) Error() string {
return e.Message
}
// internal/logic/downloadlogic.go
func (l *DownloadLogic) Download(req *types.DownloadRequest) error {
if !strings.HasSuffix(req.File, ".go") {
return &BizError{Code: 1001, Message: "仅支持下载.go文件"}
}
data, err := os.ReadFile(req.File)
if err != nil {
return &BizError{Code: 1002, Message: "文件读取失败"}
}
_, err = l.writer.Write(data)
return err
}
3. 文件上传安全增强
在 uploadlogic.go 中添加文件类型和大小校验:
func (l *UploadLogic) Upload() (*types.UploadResponse, error) {
// 限制表单大小
if err := l.r.ParseMultipartForm(l.svcCtx.Config.MaxBytes); err != nil {
return nil, &BizError{Code: 2001, Message: "文件大小超限"}
}
file, header, err := l.r.FormFile("myFile")
if err != nil {
return nil, err
}
defer file.Close()
// 校验文件类型
buf := make([]byte, 512)
if _, err = file.Read(buf); err != nil {
return nil, err
}
if !strings.Contains(http.DetectContentType(buf), "image/") {
return nil, &BizError{Code: 2002, Message: "仅支持图片格式"}
}
file.Seek(0, 0) // 重置文件指针
// 保存文件
dstPath := filepath.Join(l.svcCtx.Config.Path, header.Filename)
dst, err := os.Create(dstPath)
if err != nil {
return nil, err
}
defer dst.Close()
if _, err = io.Copy(dst, file); err != nil {
return nil, err
}
return &types.UploadResponse{Code: 0}, nil
}
4. 中间件集成
在 file.go 中全局添加日志、限流等中间件:
// file.go
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
// 全局中间件
server.Use(middleware.NewLoggerMiddleware().Handle)
server.Use(middleware.NewRateLimitMiddleware(time.Minute, 100).Handle)
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("启动服务 %s:%d\n", c.Host, c.Port)
server.Start()
}
5. 数据库集成示例
若需添加数据库操作,可通过 goctl model 生成带缓存的 CRUD 代码:
# 生成 user 表模型
goctl model mysql datasource -url="user:pass@tcp(127.0.0.1:3306)/dbname" -table="user" -dir ./internal/model
在 internal/svc/servicecontext.go 中注入:
type ServiceContext struct {
Config config.Config
UserModel model.UserModel // 自动生成的模型
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(sqlx.NewMysql(c.DataSource), c.Cache),
}
}
6. 性能监控集成
在 config.go 中添加 Metrics 配置,并启用 Prometheus 指标收集:
type Config struct {
rest.RestConf
Path string `json:",default=./uploads"`
Metrics rest.MetricsConf `json:",optional"` // 监控配置
}
// file.go 中启用
server := rest.MustNewServer(c.RestConf, rest.WithMetrics(c.Metrics))
7. 完整目录结构
最终项目结构如下:
.
├── api
│ ├── download.api
│ ├── file.api
│ └── upload.api
├── etc
│ ├── file-api-dev.yaml
│ └── file-api-prod.yaml
├── file.go
├── go.mod
└── internal
├── config
│ └── config.go
├── handler
│ ├── downloadhandler.go
│ ├── routes.go
│ └── uploadhandler.go
├── logic
│ ├── baseerror.go
│ ├── downloadlogic.go
│ └── uploadlogic.go
├── model
│ └── usermodel.go # 生成的模型
└── svc
└── servicecontext.go
这些实践确保了单体服务在快速开发的同时,保持代码可维护性和扩展性。当业务增长时,可通过 go-zero 的 RPC 工具将模块平滑拆分为微服务。

