项目详解
目录结构
.
├── cmd # 应用入口
│ └── server # 服务器入口
├── config # 配置目录
├── internal # 内部代码
│ ├── controller # 控制器
│ ├── interface # 接口定义
│ ├── module # 模块实现
│ ├── pkg # 工具包
│ └── router # 路由定义
├── scripts # 脚本文件
│ └── mysql # MySQL 初始化脚本
└── tests # 测试文件
└── api # API 测试
配置系统
本项目使用 goner/viper 来管理配置。这个组件支持多种配置文件格式(JSON、YAML、TOML、Properties),并且能够从环境变量和命令行参数读取配置,非常灵活。
配置文件层级与环境覆盖
配置系统遵循以下优先级规则:
- 基础配置:默认配置文件名为
default.*
,其中*
代表文件格式(如 json、yaml、toml、properties 等)。 - 环境特定配置:如果设置了环境变量
ENV
(例如ENV=dev
),系统会读取对应的dev.*
配置文件,并用其中的配置覆盖默认配置。 - 本地开发配置:如果环境变量
ENV
为空,系统会读取local.*
配置文件覆盖默认配置,这种方式特别适合本地开发环境。
例如,在当前项目中存在两个配置文件:default.properties
和 dev.yaml
。在从模板创建项目演示时,本地开发环境没有设置 ENV
环境变量,因此系统读取了 default.properties
中的配置;而在 Docker 容器中运行时,由于添加了 .env
文件并设置了 ENV=dev
,系统读取了 dev.yaml
中的配置来覆盖默认配置中的数据库地址。
常用配置项说明
配置键 | 说明 | 默认值 |
---|---|---|
server.port | 服务器监听端口 | 8080 |
server.host | 服务器监听地址 | 0.0.0.0 |
server.mode | 服务器运行模式(release/debug) | release |
server.html-tpl-pattern | HTML模板文件路径模式 | 无 |
server.health-check | 健康检查路径 | 无 |
server.log.show-access-log | 是否记录访问日志 | true |
server.log.data-max-length | 日志数据最大长度 | 0(无限制) |
server.log.show-request-time | 是否显示请求处理时间 | true |
server.return.wrapped-data | 是否启用响应数据包装(启用后响应会被封装为 {"code": 0, "msg": "err msg", "data": $data} 格式) | true |
database.driver-name | 数据库驱动名称 | 无 |
database.dsn | 数据库连接字符串 | 无 |
当配置了
server.html-tpl-pattern
后,系统会使用gin.LoadHTMLGlob
加载模板文件。关于模板渲染的更多信息,请参考 Gin 框架的 HTML 渲染文档。
路由系统
在本项目中,我们定义了两种不同类型的路由分组,用于处理不同的访问权限需求:
需要鉴权的路由分组
package router
import (
"github.com/gone-io/gone/v2"
"github.com/gone-io/goner/gin"
"github.com/gone-io/goner/gin/injector"
"my-project/internal/interface/entity"
"my-project/internal/interface/service"
"my-project/internal/pkg/utils"
"reflect"
)
const IdAuthRouter = "router-auth"
type authRouter struct {
gone.Flag
gin.RouteGroup
root gin.RouteGroup `gone:"*"` // 注入根路由组
iUser service.IUserLogin `gone:"*"`
}
func (r *authRouter) GonerName() string {
return IdAuthRouter // 定义组件名称,用于在controller中注入使用
}
func (r *authRouter) Init() {
r.RouteGroup = r.root.Group("/api", r.auth)
}
func (r *authRouter) auth(ctx *gin.Context, in struct {
authorization string `gone:"http,header"`
}) error {
token, err := utils.GetBearerToken(in.authorization)
if err != nil {
return gone.ToError(err)
}
userId, err := r.iUser.GetUserIdFromToken(token)
utils.SetUserId(ctx, userId)
return err
}
var _ injector.TypeParser[*gin.Context] = (*tokenParser)(nil)
type tokenParser struct {
gone.Flag
}
func (t tokenParser) Parse(context *gin.Context) (reflect.Value, error) {
userId := utils.GetUserId(context)
return reflect.ValueOf(entity.Token{UserId: userId}), nil
}
func (t tokenParser) Type() reflect.Type {
return reflect.TypeOf(entity.Token{})
}
功能说明:
authRouter
是一个需要用户身份验证的路由分组,它实现了以下核心功能:
- 定义了常量
IdAuthRouter
(值为"router-auth"
),作为组件的唯一标识符,便于在控制器中注入使用。 - 创建了
authRouter
结构体,通过嵌入gone.Flag
使其成为一个 Goner 组件(Gone 框架的核心概念)。 - 在
Init
方法中创建了一个路径为/api
的路由组,并添加了r.auth
中间件用于身份验证。 auth
中间件接收两个参数:ctx *gin.Context
:Gin 的上下文对象- 一个包含
authorization
字段的匿名结构体,该字段通过gone:"http,header"
标签从 HTTP 请求头中自动获取值
- 实现了
tokenParser
结构体,它能将用户 ID 解析为entity.Token
类型,方便在控制器中直接注入使用。
不需要鉴权的路由分组
package router
import (
"github.com/gone-io/gone/v2"
"github.com/gone-io/goner/gin"
)
const IdRouterPub = "router-pub"
type pubRouter struct {
gone.Flag
gin.IRouter
root gin.RouteGroup `gone:"*"` // 注入根路由组
}
func (r *pubRouter) GonerName() string {
return IdRouterPub // 定义组件名称,用于在controller中注入使用
}
func (r *pubRouter) Init() {
r.IRouter = r.root.Group("/api")
}
功能说明:
pubRouter
是一个公开的路由分组,不需要用户身份验证即可访问,它的实现更为简单:
- 定义了常量
IdRouterPub
(值为"router-pub"
),作为组件的唯一标识符。 - 创建了
pubRouter
结构体,同样通过嵌入gone.Flag
成为 Goner 组件。 - 在
Init
方法中创建了一个路径为/api
的路由组,但没有添加任何中间件,表示这个路由组下的所有路由都可以公开访问,无需身份验证。
控制器实现
package controller
import (
"github.com/gone-io/gone/v2"
"github.com/gone-io/goner/gin"
"my-project/internal/interface/entity"
"my-project/internal/interface/service"
"my-project/internal/pkg/utils"
)
type userCtr struct {
gone.Flag
a gin.RouteGroup `gone:"router-auth"`
p gin.RouteGroup `gone:"router-pub"`
iUser service.IUser `gone:"*"`
iUserLogin service.IUserLogin `gone:"*"`
gone.Logger `gone:"*"`
}
func (c *userCtr) Mount() gin.MountError {
c.Infof("mount user controller")
c.p.
POST("/users/login", func(in struct {
req *entity.LoginParam `gone:"http,body"`
}) (*entity.LoginResult, error) {
return c.iUserLogin.Login(in.req)
}).
POST("/users/register", func(in struct {
req *entity.RegisterParam `gone:"http,body"`
}) (*entity.LoginResult, error) {
return c.iUserLogin.Register(in.req)
})
c.a.
GET("/users/me", func(token entity.Token) (any, error) {
return c.iUser.GetUserById(token.UserId)
}).
POST("/users/logout", func(in struct {
authorization string `gone:"http,header"`
}) error {
token, err := utils.GetBearerToken(in.authorization)
if err != nil {
return gone.ToError(err)
}
return c.iUserLogin.Logout(token)
})
return nil
}
功能说明:
userCtr
是一个用户控制器,负责处理用户相关的 HTTP 请求,它的实现包括:
- 定义了
userCtr
结构体,通过嵌入gone.Flag
成为 Goner 组件。 - 注入了两个路由组:
a
:需要鉴权的路由组(对应前面的authRouter
)p
:不需要鉴权的路由组(对应前面的pubRouter
)
- 注入了两个服务接口:
iUser
:用户基本信息服务iUserLogin
:用户登录相关服务- 以及一个日志接口
gone.Logger
- 在
Mount
方法中,分别在两个路由组上注册了不同的路由处理函数:- 在公开路由组
p
上注册了登录和注册接口 - 在需要鉴权的路由组
a
上注册了获取用户信息和登出接口
- 在公开路由组
HTTP 参数注入机制
Gone 框架提供了强大的 HTTP 参数自动注入功能,通过 gone:"http,..."
标签将 HTTP 请求中的各种参数自动绑定到处理函数的参数中,大大简化了参数处理逻辑。
标签格式:
${attributeName} ${attributeType} `gone:"http,${kind}[=${key}]"`
其中:
attributeName
:结构体字段名attributeType
:字段类型kind
:注入类型,支持body
、header
、query
、param
、cookie
等key
:注入键值(可选),如果不指定,默认使用字段名
示例解析:
-
请求体参数注入:
func(in struct {
req *entity.LoginParam `gone:"http,body"`
}) (*entity.LoginResult, error)这个函数的参数是一个匿名结构体,其中
req
字段标记为gone:"http,body"
,表示系统会自动从 HTTP 请求体中解析 JSON 数据并绑定到req
字段。 -
请求头参数注入:
func(in struct {
authorization string `gone:"http,header"`
}) error这里的
authorization
字段标记为gone:"http,header"
,表示系统会自动从 HTTP 请求头中获取Authorization
值并绑定到该字段。 -
自定义类型注入:
func(token entity.Token) (any, error)
这个函数直接使用
entity.Token
类型作为参数,它是通过前面定义的tokenParser
解析得到的,表示直接注入当前请求的用户 Token 信息。
Gone 框架支持的注入类型:
-
类型解析器(TypeParser)支持的类型:
*gin.Context
:注入 Gin 请求上下文指针*http.Request
:注入原始 HTTP 请求指针*url.URL
:注入 URL 指针http.Header
:注入 HTTP 请求头gin.ResponseWriter
:注入响应写入器(用于直接写入响应数据)
-
名称解析器(NameParser)支持的类型:
body
:将请求体解析后注入,支持结构体、结构体指针、[]byte、io.Reader 等类型header
:获取请求头参数,支持简单类型(数字、字符串)param
:获取 URL 路径参数,支持简单类型query
:获取查询字符串参数,支持简单类型、简单类型的数组、结构体和结构体指针cookie
:获取 Cookie 值,支持简单类型
更多HTTP参数注入,请参考:http-inject。
代码生成机制
Gone 框架提供了强大的代码生成工具,可以自动生成项目所需的各种辅助代码,减少重复工作。
module.load.go
module.load.go
是运行 gonectl install
命令时自动生成的文件,用于加载项目依赖的 Goner 组件:
// Code generated by gonectl. DO NOT EDIT.
package template_module
import (
"github.com/gone-io/gone/v2"
"github.com/gone-io/goner/g"
"github.com/gone-io/goner/gin"
"github.com/gone-io/goner/tracer"
"github.com/gone-io/goner/viper"
"github.com/gone-io/goner/xorm"
zap "github.com/gone-io/goner/zap"
)
// load installed gone module LoadFunc
var loaders = []gone.LoadFunc{
gin.Load,
tracer.Load,
viper.Load,
xorm.Load,
zap.Load,
}
func GoneModuleLoad(loader gone.Loader) error {
var ops []*g.LoadOp
for _, f := range loaders {
ops = append(ops, g.F(f))
}
return g.BuildOnceLoadFunc(ops...)(loader)
}
这个文件类似于 Node.js 项目中的 package.json
,它描述了项目依赖的模块,并提供了加载这些模块的入口函数 GoneModuleLoad
。
init.gone.go
init.gone.go
是运行 gonectl generate
(或 gonectl run
)命令时自动生成的文件,用于加载当前包中的 Goner 组件或 LoadFunc
函数。每个包中都会生成一个 init.gone.go
文件。
注意:如果包中存在
LoadFunc
函数,系统将只会加载该函数,而不会加载 Goner 组件。这是因为我们认为定义了LoadFunc
函数意味着开发者选择了手动管理组件的加载。
根目录下的 init.gone.go
文件示例:
// Code generated by gonectl. DO NOT EDIT.
package template_module
import "github.com/gone-io/gone/v2"
func init() {
gone.
Loads(GoneModuleLoad)
}
而 internal/router/init.gone.go
的内容示例:
// Code generated by gonectl. DO NOT EDIT.
package router
import "github.com/gone-io/gone/v2"
func init() {
gone.
Load(&authRouter{}).
Load(&tokenParser{}).
Load(&pubRouter{})
}
可以看到,internal/router/init.gone.go
中加载了三个 Goner 组件:authRouter
、tokenParser
和 pubRouter
。
提示:
LoadFunc
函数的定义为:type LoadFunc func(loader gone.Loader) error
import.gone.go
import.gone.go
是运行 gonectl generate
(或 gonectl run
)命令时自动生成的文件,用于将项目中其他包的路径导入到 main 包中:
// Code generated by gonectl. DO NOT EDIT.
package main
import (
_ "my-project"
_ "my-project/internal/controller"
_ "my-project/internal/module"
_ "my-project/internal/module/dependent"
_ "my-project/internal/module/user"
_ "my-project/internal/router"
)
提示: 一般情况下,运行
gonectl run
(或gonectl generate
)时,脚手架工具会自动寻找当前模块的根路径和 main 目录,并在所有包含 Goner 组件或LoadFunc
函数的包中生成init.gone.go
文件,同时在 main 包中生成import.gone.go
文件。我们也可以在某个文件(通常是
package.go
)中使用注释标记需要扫描的目录和 main 包的目录:package.go//...
//go:generate gonectl generate -s . -m ./cmd/server
//...这样做的好处是:
- 可以更精确地控制需要扫描的目录和 main 包的位置
- 可以使用
go generate ./...
命令来生成代码,便于与其他代码生成需求统一管理更多关于
gonectl generate
命令的用法,请参考:gonectl generate 文档