跳到主要内容

项目详解

目录结构

.
├── cmd # 应用入口
│ └── server # 服务器入口
├── config # 配置目录
├── internal # 内部代码
│ ├── controller # 控制器
│ ├── interface # 接口定义
│ ├── module # 模块实现
│ ├── pkg # 工具包
│ └── router # 路由定义
├── scripts # 脚本文件
│ └── mysql # MySQL 初始化脚本
└── tests # 测试文件
└── api # API 测试

配置系统

本项目使用 goner/viper 来管理配置。这个组件支持多种配置文件格式(JSON、YAML、TOML、Properties),并且能够从环境变量和命令行参数读取配置,非常灵活。

配置文件层级与环境覆盖

配置系统遵循以下优先级规则:

  1. 基础配置:默认配置文件名为 default.*,其中 * 代表文件格式(如 json、yaml、toml、properties 等)。
  2. 环境特定配置:如果设置了环境变量 ENV(例如 ENV=dev),系统会读取对应的 dev.* 配置文件,并用其中的配置覆盖默认配置。
  3. 本地开发配置:如果环境变量 ENV 为空,系统会读取 local.* 配置文件覆盖默认配置,这种方式特别适合本地开发环境。

例如,在当前项目中存在两个配置文件:default.propertiesdev.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-patternHTML模板文件路径模式
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 渲染文档

路由系统

在本项目中,我们定义了两种不同类型的路由分组,用于处理不同的访问权限需求:

需要鉴权的路由分组

internal/router/auth_router.go
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 是一个需要用户身份验证的路由分组,它实现了以下核心功能:

  1. 定义了常量 IdAuthRouter(值为 "router-auth"),作为组件的唯一标识符,便于在控制器中注入使用。
  2. 创建了 authRouter 结构体,通过嵌入 gone.Flag 使其成为一个 Goner 组件(Gone 框架的核心概念)。
  3. Init 方法中创建了一个路径为 /api 的路由组,并添加了 r.auth 中间件用于身份验证。
  4. auth 中间件接收两个参数:
    • ctx *gin.Context:Gin 的上下文对象
    • 一个包含 authorization 字段的匿名结构体,该字段通过 gone:"http,header" 标签从 HTTP 请求头中自动获取值
  5. 实现了 tokenParser 结构体,它能将用户 ID 解析为 entity.Token 类型,方便在控制器中直接注入使用。

不需要鉴权的路由分组

internal/router/pub_router.go
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 是一个公开的路由分组,不需要用户身份验证即可访问,它的实现更为简单:

  1. 定义了常量 IdRouterPub(值为 "router-pub"),作为组件的唯一标识符。
  2. 创建了 pubRouter 结构体,同样通过嵌入 gone.Flag 成为 Goner 组件。
  3. Init 方法中创建了一个路径为 /api 的路由组,但没有添加任何中间件,表示这个路由组下的所有路由都可以公开访问,无需身份验证。

控制器实现

internal/controller/user.go
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 请求,它的实现包括:

  1. 定义了 userCtr 结构体,通过嵌入 gone.Flag 成为 Goner 组件。
  2. 注入了两个路由组:
    • a:需要鉴权的路由组(对应前面的 authRouter
    • p:不需要鉴权的路由组(对应前面的 pubRouter
  3. 注入了两个服务接口:
    • iUser:用户基本信息服务
    • iUserLogin:用户登录相关服务
    • 以及一个日志接口 gone.Logger
  4. Mount 方法中,分别在两个路由组上注册了不同的路由处理函数:
    • 在公开路由组 p 上注册了登录和注册接口
    • 在需要鉴权的路由组 a 上注册了获取用户信息和登出接口

HTTP 参数注入机制

Gone 框架提供了强大的 HTTP 参数自动注入功能,通过 gone:"http,..." 标签将 HTTP 请求中的各种参数自动绑定到处理函数的参数中,大大简化了参数处理逻辑。

标签格式

${attributeName} ${attributeType} `gone:"http,${kind}[=${key}]"`

其中:

  • attributeName:结构体字段名
  • attributeType:字段类型
  • kind:注入类型,支持 bodyheaderqueryparamcookie
  • key:注入键值(可选),如果不指定,默认使用字段名

示例解析

  1. 请求体参数注入

    func(in struct {
    req *entity.LoginParam `gone:"http,body"`
    }) (*entity.LoginResult, error)

    这个函数的参数是一个匿名结构体,其中 req 字段标记为 gone:"http,body",表示系统会自动从 HTTP 请求体中解析 JSON 数据并绑定到 req 字段。

  2. 请求头参数注入

    func(in struct {
    authorization string `gone:"http,header"`
    }) error

    这里的 authorization 字段标记为 gone:"http,header",表示系统会自动从 HTTP 请求头中获取 Authorization 值并绑定到该字段。

  3. 自定义类型注入

    func(token entity.Token) (any, error)

    这个函数直接使用 entity.Token 类型作为参数,它是通过前面定义的 tokenParser 解析得到的,表示直接注入当前请求的用户 Token 信息。

Gone 框架支持的注入类型

  1. 类型解析器(TypeParser)支持的类型

    • *gin.Context:注入 Gin 请求上下文指针
    • *http.Request:注入原始 HTTP 请求指针
    • *url.URL:注入 URL 指针
    • http.Header:注入 HTTP 请求头
    • gin.ResponseWriter:注入响应写入器(用于直接写入响应数据)
  2. 名称解析器(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 文件示例:

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 的内容示例:

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 组件:authRoutertokenParserpubRouter

提示LoadFunc 函数的定义为:

type LoadFunc func(loader gone.Loader) error

import.gone.go

import.gone.go 是运行 gonectl generate(或 gonectl run)命令时自动生成的文件,用于将项目中其他包的路径导入到 main 包中:

cmd/server/import.gone.go
// 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

//...

这样做的好处是:

  1. 可以更精确地控制需要扫描的目录和 main 包的位置
  2. 可以使用 go generate ./... 命令来生成代码,便于与其他代码生成需求统一管理

更多关于 gonectl generate 命令的用法,请参考:gonectl generate 文档