项目结构化
本章将介绍如何对项目进行结构化设计。
前言
到目前为止,我们已经利用 Go-Sail 构建了一个简单的登录服务,具备了读取配置、热重载与分布式链路追踪等基础能力。但你可能已经注意到一个潜在问题:我们当前的大部分代码都集中在 main.go 文件中。这并不是错误或不专业,而是为了让教程内容更直接、简明。在实际项目开发中,为了提升代码的可读性、维护性及保障项目的健康可持续演进,我们必须对项目进行正确的结构划分。
文件拆分
首先,我们将庞大的 main.go 文件拆分为若干小单元。遵循业界流行的 MVC 模式,将代码分为控制器(在 Go 生态中通常称为“处理函数”)、数据模型。由于我们是纯 API 服务,自然不需要 “View” 模块。此外,还增加了“路由”、“服务”、“配置”等模块。
处理函数(Handlers)
handlers.go
package main
import (
"github.com/gin-gonic/gin"
)
func Login(c *gin.Context) {
LoginSvc(c)
}
func ThirdPartyNotify(c *gin.Context) {
ThirdPartyNotifySvc(c)
}
中间件(Middlewares)
middlewares.go
package main
import (
"github.com/gin-gonic/gin"
sailConstants "github.com/keepchen/go-sail/v3/constants"
"github.com/keepchen/go-sail/v3/sail"
)
...
// ValidateToken 验证用户 Token
func ValidateToken() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("Authorization")
if token != "this-is-a-valid-token" {
sail.Response(c).Wrap(sailConstants.ErrAuthorizationTokenInvalid, nil).Send()
return
}
c.Next()
}
}
...
模型(Models)
models.go
package main
type User struct {
Username string `gorm:"column:username;type:varchar(100);NOT NULL;comment:username"`
Password string `gorm:"column:password;type:varchar(1024);NOT NULL;comment:password"`
}
func (User) TableName() string {
return "users"
}
路由(Routes)
routes.go
package main
import (
"github.com/gin-gonic/gin"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
func RegisterRoutes(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", Login)
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", ThirdPartyNotify)
}
服务(Services)
services.go
package main
import (
"fmt"
"net/http"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
func LoginSvc(c *gin.Context) {
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("绑定请求参数失败", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// 用户不存在
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// 密码不匹配
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
}
func ThirdPartyNotifySvc(c *gin.Context) {
c.JSON(http.StatusOK, "SUCCESS")
}
错误(Errors)
errors.go
package main
import (
"fmt"
"sync"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
类型(Types)
types.go
package main
import (
"fmt"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("用户名不能为空")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("密码不能为空")
}
return sailConstants.ErrorNone, nil
}
配置(Config)
config.go
package main
import (
"gopkg.in/yaml.v2"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
var conf config.Config
func ParseConfig() {
parseFn := func(content []byte, viaWatch bool) {
if viaWatch {
// 配置已被热更新,可以做一些事情...
}
fmt.Println("配置内容: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.Config(parseFn).ViaFile("./go-sail.config.local.yaml").Parse(parseFn)
}
主入口(Main)
main.go
package main
import (
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/http/api"
)
var (
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User{})
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("记录一些日志...")
}
)
func main() {
ParseConfig()
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", &conf).
SetupApiOption(options).
Hook(RegisterRoutes, nil, afterFunc).Launch()
}
如上所述,我们将原有的 main.go 文件拆分成了多个文件。但到目前为止,所有代码依然都属于 main 包,并且文件全部位于同一个目录。实际工程开发远不止于此,结构颗粒度通常更细。
目录结构
真实项目中,通常会按模块将目录划分清楚,所归属的 Go 包也会随之变化。在上文结构化的基础上,我们进一步升级项目结构。
初始化模块
首先使用 go mod 进行包管理。本教程下,我们以 tutorials 命名初始化模块,命令如下:
go mod init tutorials
处理函数(Handlers)
http/handlers/user.go
package handlers
import (
"tutorials/http/services"
"github.com/gin-gonic/gin"
)
func Login(c *gin.Context) {
services.LoginSvc(c)
}
http/handlers/third-party.go
package handlers
import (
"tutorials/http/services"
"github.com/gin-gonic/gin"
)
func ThirdPartyNotify(c *gin.Context) {
services.ThirdPartyNotifySvc(c)
}
中间件(Middlewares)
http/middlewares/authorization.go
package middlewares
import (
"github.com/gin-gonic/gin"
sailConstants "github.com/keepchen/go-sail/v3/constants"
"github.com/keepchen/go-sail/v3/sail"
)
// ValidateToken 验证用户 Token
func ValidateToken() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.Request.Header.Get("Authorization")
if token != "this-is-a-valid-token" {
sail.Response(c).Wrap(sailConstants.ErrAuthorizationTokenInvalid, nil).Send()
return
}
c.Next()
}
}
模型(Models)
pkg/models/user.go
package models
type User struct {
Username string `gorm:"column:username;type:varchar(100);NOT NULL;comment:username"`
Password string `gorm:"column:password;type:varchar(1024);NOT NULL;comment:password"`
}
func (User) TableName() string {
return "users"
}
路由(Routes)
http/routes/routes.go
package routes
import (
"tutorials/http/middlewares"
"github.com/gin-gonic/gin"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
func RegisterRoutes(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", Login)
userGroup := ginEngine.Group("/user").Use(middlewares.ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", ThirdPartyNotify)
}
服务(Services)
http/services/user.go
package services
import (
"net/http"
"tutorials/http/types"
"tutorials/pkg/common"
"tutorials/pkg/models"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
func LoginSvc(c *gin.Context) {
var loginRequest types.LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("绑定请求参数失败", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&models.User{Username: loginRequest.Username}).First(&user)
// 用户不存在
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(common.ErrUserNotExist, nil).Send()
return
}
// 密码不匹配
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
}
http/services/third-party.go
package services
import (
"net/http"
"github.com/gin-gonic/gin"
)
func ThirdPartyNotifySvc(c *gin.Context) {
c.JSON(http.StatusOK, "SUCCESS")
}
错误(Errors)
pkg/common/errors.go
package common
import (
"fmt"
"sync"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
类型(Types)
http/types/user.go
package types
import (
"fmt"
sailConstants "github.com/keepchen/go-sail/v3/constants"
)
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("用户名不能为空")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("密码不能为空")
}
return sailConstants.ErrorNone, nil
}
配置(Config)
config/config.go
package config
import (
"gopkg.in/yaml.v2"
"github.com/keepchen/go-sail/v3/sail"
sailConfig "github.com/keepchen/go-sail/v3/sail/config"
var conf sailConfig.Config
func ParseConfig() {
parseFn := func(content []byte, viaWatch bool) {
if viaWatch {
// 配置已被热更新,可以做一些事情...
}
fmt.Println("配置内容: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.Config(parseFn).ViaFile("./go-sail.config.local.yaml").Parse(parseFn)
}
func Get() *sailConfig.Config {
return &conf
}
主入口(Main)
main.go
package main
import (
"tutorials/config"
"tutorials/http/routes"
"tutorials/pkg/models"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/http/api"
)
var (
afterFunc = func() {
sail.GetDBW().AutoMigrate(&models.User{})
var user User
sail.GetDBW().Where(&models.User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("记录一些日志...")
}
)
func main() {
config.ParseConfig()
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", config.Get()).
SetupApiOption(options).
Hook(routes.RegisterRoutes, nil, afterFunc).Launch()
}
当结构整理完成后,我们可通过如下命令清理和管理依赖包:
go mod tidy
通过这样的结构优化后,我们可以看到:每个文件都各司其职,项目变得非常清晰。基于这样的结构扩展功能会变得异常容易。
最终,整个项目目录结构大致如下:
tutorials/
├── config
│ └── config.go
├── main.go
├── http
│ ├── handlers
│ │ ├── user.go
│ │ └── third-party.go
│ ├── middlewares
│ │ └── authorization.go
│ ├── routes
│ │ └── routes.go
│ ├── services
│ │ ├── user.go
│ │ └── third-party.go
│ ├── types
│ │ └── user.go
└── pkg
├── common
│ └── errors.go
└── models
└── user.go
提示
Go-Sail 对你的项目结构并不做强制规定,结构组织完全取决于你自己。毕竟,世界上没有“最优解”,适合自己的才是最好的。