4、框架代码和业务代码分离
4、框架代码和业务代码分离
在go项目中,项目代码通常分为2类:
- 应用启动框架:例如:命令行框架、配置文件解析和读取、配置项校验等。
- 业务相关的代码:业务相关的代码变更相较于应用框架部分,变更频繁,而且可能会影响业务。
为了提高项目可维护性,将两类代码分类拆分,从目录级别隔离,减少2类代码变更时互相影响的概率
优化ServerOptions结构体定义
分别存放在以下两个目录:
- 应用框架代码: cmd/fg-apiserver/app
- 运行时代码:internal/appiserver
2部分代码有些配置会共享,为了提高代码复用性,需要将cmd/fg-apiserver/app/options/options.go 文件中可复用配置拆分成一个独立的包,共两种类别的代码引用。
将MYSQLOptions结构体定义放在pkg/options/mysql_options.go
文件中。因为我们经常要基于MySQLOptions 结构体的字段创建*gorm.DB
类型的数据库实例。为了提高代码调用效率,给MYSQLOptions结构体类型添加了NewDB方法,用来创建*gorm.DB
类型的实例。
pkg/options/mysql_options.go
内容如下
package options
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net"
"strconv"
"time"
)
type MySQLOption struct {
Addr string `json:"addr,omitempty" mapstructure:"addr"`
Username string `json:"username,omitempty" mapstructure:"username"`
Password string `json:"-" mapstructure:"password"`
Database string `json:"database" mapstructure:"database"`
MaxIdleConnections int `json:"max-idle-connections,omitempty" mapstructure:"max-idle-connections,omitempty"`
MaxOpenConnections int `json:"max-open-connections,omitempty" mapstructure:"max-open-connections"`
MaxConnectionLifeTime time.Duration `json:"max-connection-life-time,omitempty" mapstructure:"max-connection-life-time"`
}
func NewMYSQLOption() *MySQLOption {
return &MySQLOption{
Addr: "127.0.0.1:3306",
Username: "onex",
Password: "onex",
Database: "onex",
MaxIdleConnections: 100,
MaxOpenConnections: 100,
MaxConnectionLifeTime: time.Duration(10) * time.Second,
}
}
// 创建数据库链接地址
func (m *MySQLOption) DSN() string {
return fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s`,
m.Username,
m.Password,
m.Addr,
m.Database,
true,
"Local")
}
// 创建DB方法
func (m *MySQLOption) NewDB() (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(m.DSN()), &gorm.Config{
PrepareStmt: true,
})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(m.MaxOpenConnections)
sqlDB.SetConnMaxLifetime(m.MaxConnectionLifeTime)
sqlDB.SetMaxIdleConns(m.MaxIdleConnections)
return db, nil
}
func (o *MySQLOption) Validate() error {
// 验证MySQL地址格式
if o.Addr == "" {
return fmt.Errorf("MySQL server address cannot be empty")
}
// 检查地址格式是否为host:port
host, portStr, err := net.SplitHostPort(o.Addr)
if err != nil {
return fmt.Errorf("Invalid MySQL address format '%s': %w", o.Addr, err)
}
// 验证端口是否为数字
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("Invalid MySQL port: %s", portStr)
}
// 验证主机名是否为空
if host == "" {
return fmt.Errorf("MySQL hostname cannot be empty")
}
// 验证凭据和数据库名
if o.Username == "" {
return fmt.Errorf("MySQL username cannot be empty")
}
if o.Password == "" {
return fmt.Errorf("MySQL password cannot be empty")
}
if o.Database == "" {
return fmt.Errorf("MySQL database name cannot be empty")
}
// 验证连接池参数
if o.MaxIdleConnections <= 0 {
return fmt.Errorf("MySQL max idle connections must be greater than 0")
}
if o.MaxOpenConnections <= 0 {
return fmt.Errorf("MySQL max open connections must be greater than 0")
}
if o.MaxIdleConnections > o.MaxOpenConnections {
return fmt.Errorf("MySQL max idle connections cannot be greater than max open connections")
}
if o.MaxConnectionLifeTime <= 0 {
return fmt.Errorf("MySQL max connection lifetime must be greater than 0")
}
return nil
}
上面代码会使用gorm.Open
方法创建一个*gorm.DB
实例,并且对该实例进行一些设置:
MaxOpenConnections
:设置最大打开连接数MaxConnectionLifeTime
:设置连接最大存活时间MaxIdleConnections
:设置最大空闲连接数
优化后的cmd/fg-apiserver/app/options/options.go 文件内容:
package options
import (
"gitlab.com/onexstack/fastgo/internal/apiserver"
genericoptions "gitlab.com/onexstack/fastgo/pkg/options"
)
type ServerOptions struct {
MySQLOptions *genericoptions.MySQLOption `json:"mysql" mapstructure:"mysql"`
}
// NewServerOptions 创建带有默认值的 ServerOptions 实例.
func NewServerOptions() *ServerOptions {
return &ServerOptions{
MySQLOptions: genericoptions.NewMYSQLOption(),
}
}
//
// Validate
// @Description: Validate校验ServierOptions参数是否合法
// @receiver s
// @return error
//
func (s *ServerOptions) Validate() error {
return s.MySQLOptions.Validate()
}
//
// Config
// @Description: 基于ServerOptions构建apiserver.Config
// @receiver s
// @return *apiserver.Config
// @return error
//
func (s *ServerOptions) Config() (*apiserver.Config, error) {
return &apiserver.Config{
MYSQLOptions: s.MySQLOptions}, nil
}
ServerOptions
结构体新增了Config
方法,该方法用来基于ServerOptions
创建 *apiserver.Config
实例,供运行时代码调用
新增运行时代码
为了将运行时代码和应用框架代码物理隔开,将运行代码的实现保存在internal/apiserve/server.go 中
package apiserver
import (
"fmt"
genericoptions "gitlab.com/onexstack/fastgo/pkg/options"
)
// Server
// @Description: 定义一个服务器结构体类型
type Server struct {
cfg *Config
}
// Config
// @Description: Config结构体,用于存储应用相关的配置
//
// 不用viper.Get 是因为这种方式能更加清晰知道应用提供了哪些配置项
type Config struct {
MYSQLOptions *genericoptions.MySQLOption
}
// NewServer
//
// @Description: NewServer 根据配置创建服务器
// @receiver cfg
// @return *Server
// @return error
func (cfg *Config) NewServer() (*Server, error) {
return &Server{cfg: cfg}, nil
}
// Run
//
// @Description: Run运行应用
// @receiver s
// @return error
func (s *Server) Run() error {
fmt.Printf("Read MYSQL host from config : %s \n", s.cfg.MYSQLOptions.Addr)
fmt.Println(s.cfg.MYSQLOptions)
select {} // 调用select 语句,阻塞防止进程退出
}
应用启动时调用运行时代码
上面优化了ServerOptions ,并新增了运行时代码实现。下面还要修改cmd/fg-apiserver/app/server.go 文件,在应用启动时调用运行时代码。cmd/fg-apiserver/app/server.go 文件如下
package app
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gitlab.com/onexstack/fastgo/cmd/fg-apiserver/app/options"
)
var configFile string // 配置文件路径
func NewFastGoCommand() *cobra.Command {
// 创建默认的应用命令行选项
opts := options.NewServerOptions()
cmd := &cobra.Command{
// 指定命令的名字,该名字会出现在帮助信息中
Use: "fg-apiserver",
// 命令简短的描述
Short: "A very lightweight full go project",
Long: `A very lightweight full go project, designed to help beginners quickly
learn Go project development.`,
// 命令出错时,不打印帮助信息。设置为true可以确保命令出错时一眼就能看到错误信息
SilenceUsage: true,
// 指定cmd.Execute()时,执行的Run函数
RunE: func(cmd *cobra.Command, args []string) error {
return run(opts)
},
// 设置命令运行时的参数检查,不需要指定命令行参数。例如:./fg-apiserver param1 param2
Args: cobra.NoArgs,
}
// 初始化配置函数,在每个命令运行时调用
cobra.OnInitialize(onInitialize)
fmt.Println(filePath())
// cobra 支持持久性标志(PersistentFlag),该标志可用于它所分配的命令以及该命令下的每个子命令
// 推荐使用配置文件来配置应用,便于管理配置项
cmd.PersistentFlags().StringVarP(&configFile, "config", "c", filePath(), "Path to the fg-apiserver configuration file.")
return cmd
}
//
// run
// @Description: run是主运行逻辑,负责初始化日志、解析配置、校验选项并启动服务器
// @param opts
// @return error
//
func run(opts *options.ServerOptions) error {
// 将viper中的配置解析到opts
if err := viper.Unmarshal(opts); err != nil {
return err
}
// 校验命令选项
if err := opts.Validate(); err != nil {
return err
}
// 获取应用配置
// 将命令行选项和应用配置分开,可以更灵活的处理2种不同类型的配置
cfg, err := opts.Config()
if err != nil {
return err
}
// 创建服务器实例
server, err := cfg.NewServer()
if err != nil {
return err
}
// 启动服务器
return server.Run()
}
上面代码中,将之前的配置解析和校验逻辑迁移到了run方法中。这样做的目的是保持cobra.ommand 应用框架的简洁性。在run方法中通过opts.Config()函数调用,创建了运行时代码需要的配置,并基于此配置创建了运行时服务实例server,之后调用server的run方法,启动整个应用程序。
编译并运行
(base) dujie@MacBook-Pro fastgo % go build -v -o _output/fg-apiserver cmd/fg-apiserver/main.go
gitlab.com/onexstack/fastgo/internal/apiserver
gitlab.com/onexstack/fastgo/cmd/fg-apiserver/app/options
gitlab.com/onexstack/fastgo/cmd/fg-apiserver/app
command-line-arguments
(base) dujie@MacBook-Pro fastgo % ./_output/fg-apiserver -c configs/fg-apiserver.yaml