升级指南
介绍
DANGER
- 不建议将使用 Goyave v4 的应用程序升级到 v5。Goyave v5 是框架的完全重写版本,与旧版本有很大不同。升级到 v5 将耗时且容易出错。
- 本升级指南可能不完整。如果在升级应用程序时遇到未涵盖的内容,请提交 pull request。
INFO
- 建议在开始升级过程之前阅读新文档和 v5 发布说明。
- 本指南将引导您完成最小升级路径。升级后,您的应用程序不会完全匹配新的推荐目录结构和架构,但会像以前一样继续工作。
- 如果您有任何问题或需要帮助,请随时在 discord 上提问。
准备工作
将现有应用程序升级到 v5 可能是一个漫长的过程。建议首先逐步重构应用程序的某些方面,以使过渡更容易。
首先,您可以在项目中同时导入 v4 和 v5,因为它们的主要版本增加导致导入路径不同。这样您可以部分开始使用新的和更新的工具。
sh
go get -u goyave.dev/goyave/v5
包标识符相同,因此在导入 v5 时需要添加别名:
go
import goyave5 "goyave.dev/goyave/v5"
import validation5 "goyave.dev/goyave/v5/validation"
//...
应用程序中每个等同于 Goyave v5 中组件的元素都应重构为具有构造函数的 struct
。例如,构造函数不再是一组简单的函数。这样做也有助于下一步的依赖解耦。
控制器
go
// http/controller/user/user.go
func Show(response *goyave.Response, request *goyave.Request) {
//...
}
变为:
go
// http/controller/user/user.go
type Controller struct{
goyave5.Component
}
func NewController() *Controller {
return &Controller{}
}
func (ctrl *Controller) Show(response *goyave.Response, request *goyave.Request) {
//...
}
INFO
不要忘记更新路由注册器。
验证规则
go
// http/validation/validation.go
func validateCustom(ctx *validation.Context) bool {
return false
}
变为:
go
// http/validation/custom.go
type CustomValidator struct{ validation5.BaseValidator }
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
return false
}
func (v *CustomValidator) Name() string { return "custom" }
func (v *CustomValidator) IsType() bool { return true } // 仅在需要时
func (v *CustomValidator) IsTypeDependent() bool { return true } // 仅在需要时
func Custom() *CustomValidator {
return &CustomValidator{}
}
注册规则的方式变化:
go
validation.AddRule("password", &validation.RuleDefinition{
Function: validatePassword,
RequiredParameters: 0,
})
变为:
go
validator := Custom()
validation.AddRule(validator.Name(), &validation.RuleDefinition{
Function: validator.Validate(),
RequiredParameters: 0, // v5 切换后不再需要
IsType: validator.IsType(),
IsTypeDependent: validator.IsTypeDependent(),
ComparesFields: false, // v5 切换后不再需要
})
中间件
go
// http/middleware/custom.go
func CustomMiddleware(param, column string, model interface{}) goyave.Middleware {
return func(next goyave.Handler) goyave.Handler {
return func(response *goyave.Response, request *goyave.Request) {
next(response, request)
}
}
}
变为:
go
// http/middleware/custom.go
type Custom struct {
goyave5.Component
}
func (m *Custom) Handle(next goyave.Handler) goyave.Handler {
return func(response *goyave.Response, request *goyave.Request) {
next(response, request)
}
}
路由定义变化如下:
go
router.Middleware(middleware.Custom)
变为:
go
router.Middleware((&middleware.Custom{}).Handle)
状态处理器
go
// http/controller/status/custom.go
func CustomStatusHandler(response *goyave.Response, request *goyave.Request) {
//...
}
变为:
go
// http/controller/status/custom.go
type CustomStatusHandler struct {
goyave5.Component
}
func (*CustomStatusHandler) Handle(response *Response, request *Request) {
//...
}
路由定义变化如下:
go
router.StatusHandler(status.CustomStatusHandler)
变为:
go
router.Middleware((&status.CustomStatusHandler{}).Handle)
依赖解耦
下一步是从组件中解耦全局依赖。
检查所有使用 goyave.dev/goyave/v4/database
、goyave.dev/goyave/v4/lang
、goyave.dev/goyave/v4/config
和 goyave.Logger
/ goyave.ErrLogger
、goyave.AccessLogger
的地方。
对于语言,我们将创建一个简单的适配器,作为全局语言包的代理。由于其结构特性,我们将能够将其作为依赖项用于我们的组件。以后很容易用实际的 v5 语言实现替换:
go
// lang/lang.go
package lang
import glang "goyave.dev/goyave/v4/lang"
type Languages struct{}
func (l *Languages) Get(lang string, line string, placeholders ...string) string {
return glang.Get(lang, line, placeholders...)
}
现在让我们将所有这些全局依赖项移动到结构体字段:
go
type Controller struct{
goyave5.Component
db *gorm.DB
lang *lang.Languages
customConfigEntry string
}
func NewController(db *gorm.DB, lang *lang.Languages, customConfigEntry string) *Controller {
return &Controller{
db: db,
lang: lang,
customConfigEntry: customConfigEntry,
}
}
INFO
- 对于配置,建议仅传递实际值而不是配置对象。
- 不要忘记更新之前使用组件的位置以及它们的初始化方式。
切换到 v5
- 将导入
goyave.dev/goyave/v4
替换为goyave.dev/goyave/v5
并删除之前定义的所有别名。 - 删除之前实现的语言适配器。同时从组件依赖项中删除它。
- 通过直接传递中间件和状态处理器而不是它们的
Handle
方法来更新路由定义。 - 删除所有
validation.AddRule()
的使用。 - 从您的组件中
- 如果您有权访问
goyave.Request
,请使用request.Lang.Get()
而不是语言适配器。否则,只需将lang
的使用替换为Lang()
(可通过goyave.Component
组合访问)。 - 您不一定需要删除
db
依赖项。但如果您想删除,现在可以从component.DB()
访问它。 - 确保您的组件已初始化。如果它们没有通过诸如
router.Controller()
、router.Middleware()
等方法传递给框架,它们可能不会被初始化,这将阻止它们访问服务器的资源。
- 如果您有权访问
- 日志记录现在使用结构化日志。将
Println()
的使用替换为Info()
。
初始化
- 如果您的配置是手动加载的,它现在返回一个
*config.Config
和一个error
,而不仅仅是一个错误。 - 创建一个新的
*goyave.Server
,注册路由以及启动、关闭和信号钩子。 - 使用
server.Start()
启动服务器。 *goyave.Error
已被移除。这意味着服务器不再返回退出代码。您可以使用您选择的退出代码。返回的退出代码并没有真正带来价值。日志中的错误消息更重要。- 服务器返回的错误现在总是
*errors.Error
类型。应该使用 Goyave*slog.Logger
记录这些错误,或者如果您还没有访问记录器,可以这样记录:
go
fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
示例:
go
goyave.RegisterStartupHook(func() {
goyave.Logger.Println("Server is listening")
})
goyave.RegisterShutdownHook(func() {
goyave.Logger.Println("Server is shutting down")
})
if err := goyave.Start(route.Register); err != nil {
os.Exit(err.(*goyave.Error).ExitCode)
}
变为:
go
server, err := goyave.New(opts)
if err != nil {
fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
os.Exit(1)
}
server.Logger.Info("Registering hooks")
server.RegisterSignalHook()
server.RegisterStartupHook(func(s *goyave.Server) {
server.Logger.Info("Server is listening", "host", s.Host())
})
server.RegisterShutdownHook(func(s *goyave.Server) {
s.Logger.Info("Server is shutting down")
})
server.Logger.Info("Registering routes")
server.RegisterRoutes(route.Register)
if err := server.Start(); err != nil {
server.Logger.Error(err)
os.Exit(2)
}
配置
以下配置条目的更改可能会影响您的应用程序:
server.protocol
、server.httpsPort
和server.tls
被移除:协议仅为http
,因为 TLS/HTTPS 支持已被移除,因为 Goyave 应用程序大多数时候部署在代理后面。server.timeout
已被拆分:server.writeTimeout
、server.readTimeout
、server.idleTimeout
、server.readHeaderTimeout
、server.websocketCloseTimeout
。server.maintenance
被移除。database
条目不再有默认值。它们之前使用 MySQL 的默认值。- 新条目
database.defaultReadQueryTimeout
和database.defaultWriteQueryTimeout
为数据库操作添加了超时机制。如果您有长查询,请增加它们的值。设置为0
以禁用超时。 auth.jwt.rsa.password
被移除。
请求
request.ToStruct()
被移除,使用typeutil.Convert()
代替。request.Data
现在是any
而不是map[string]any
。您应该在使用前进行安全的类型断言。- 查询数据不再在
request.Data
中,现在拆分到request.Query
中。 Request.Request().Context()
可以替换为request.Context()
。request.URI()
重命名为request.URL()
。- 请求访问器如
Has()
、String()
、Numeric()
等都被移除。 request.CORSOptions()
被移除。您可以通过路由元数据访问 CORS 选项:request.Route.Meta[goyave.MetaCORS].(*cors.Options)
。
响应
response.HandleDatabaseError(db)
变为!response.WriteDBError(err)
WriteDBError()
如果有错误则返回true
,并且您应该return
。这与HandleDatabaseError
相反。
response.Error()
、response.JSON()
、response.String()
等不再返回错误。response.Redirect()
被移除。您可以使用http.Redirect(response, request.Request(), url, http.StatusPermanentRedirect)
替换。- 模板渲染被移除。
response.Render()
和response.RenderHTML()
不再可用。如果您使用它们,现在应该手动渲染并使用response.Write()
。 response.GetError()
现在返回*errors.Error
而不是any
。response.GetStacktrace()
被移除。您现在可以从错误本身访问堆栈跟踪。response.File()
和response.Download()
现在将文件系统作为第一个参数。使用&osfs.FS{}
以保持之前的行为。
错误处理
- 尝试移除
panic
的使用。相反,让您的方法/函数返回错误,一直传递到 HTTP 处理程序,该处理程序将使用response.Error()
。 - 在返回错误的任何地方使用错误包装。
路由
- 路由注册器现在将服务器作为参数:
func Register(server *goyave.Server, router *goyave.Router)
request.Params
变为request.RouteParams
。request.Route()
变为request.Route
。router.Route()
现在将字符串切片作为第一个参数,而不是管道分隔的方法列表。route.Validate
现在拆分为两个:route.ValidateQuery()
和route.ValidateBody()
。- 路由算法略有变化,以防止两个前缀以相同字符开头的子路由器之间发生冲突。此外,当一个子路由器匹配但它的任何路由都不匹配时,不会检查其他子路由器(无法回头)。
- 解析中间件不再是核心中间件。您需要将其添加为全局中间件:
router.GlobalMiddleware(&parse.Middleware{})
router.Static()
现在将文件系统作为第一个参数,并返回生成的*Route
。
数据库
database.View
被移除,因为在测试更改后它不再有任何用处。- 数据库初始化器被移除,在
server.New()
之后和server.Start()
之前对数据库进行更改。 database.RegisterModel()
被移除,在移除自动迁移和测试更改后,它不再有任何用处。- 自动迁移被移除。如果您愿意,仍然可以使用 Gorm 的自动迁移,但不鼓励这样做。
database.Paginator
现在接受一个表示要分页的模型的泛型参数。- Gorm 实例现在使用 Goyave 记录器而不是默认的记录器。如果您调整了 Gorm 记录器,请确保更新您的实现,最好使用
*database.Logger
。
本地化
- 数组元素的验证消息键已更改。将
.array
替换为.element
。 - 类型相关的验证器现在也支持
object
类型。 lang.Get(request.Lang)
变为request.Lang.Get
或component.Lang().Get()
fields.json
现在是map[string]string
。不再有带有 "name" 或 "rules" 的对象。
验证
- 查询和正文的验证现在分为两部分:
route.ValidateQuery()
和route.ValidateBody()
。其中任何一个的第一次调用都会自动将验证中间件添加到路由中。- 验证错误响应正文略有不同:
- 键现在是
error
而不是validationError
,以与其余错误处理程序保持一致。 - 以下是新格式的示例。高亮行显示了差异:
- 键现在是
json
{
"error": {
"body": {
"fields": {
"user": {
"fields": {
"name": {
"errors": ["名称不能超过 255 个字符。"]
},
"roles": {
"errors": ["角色不能超过 2 项。"],
"elements": {
"2": {
"errors": ["角色元素必须具有以下值之一:viewer、admin、moderator。"]
}
}
}
},
"errors": ["用户必须是一个对象"]
}
}
},
"query": {
"fields": {
"group": {
"errors": ["组 ID 是必需的。"]
}
}
}
}
}
validation.Validate
和validation.ValidateWithExtra
变为validation.Validate(opts)
。validation.Options
包含几个选项,以及外部依赖项,如语言、数据库、配置等。这些将传递给验证器,以便它们可以像任何常规组件一样访问它们。isJSON
变为ConvertSingleValueArrays
,它做同样的事情,但逻辑不同,因此布尔值将相反。- 此函数现在还返回一个错误切片。这些不是验证错误,而是实际的执行错误。
go
data := map[string]any{
"string": "hello world",
"number": 42,
}
ruleSet := validation.RuleSet{
"string": validation.List{"required", "string"},
"number": validation.List{"required", "numeric", "min:10"},
}
errors := validation.Validate(data, ruleSet, true, request.Lang)
变为:
go
// 这里的 "ctrl" 是一个组件
ruleSet := validation.RuleSet{
{Path: validation.CurrentElement, Rules: validation.List{validation.Required(), validation.Object()}},
{Path: "string", Rules: validation.List{validation.Required(), validation.String()}},
{Path: "number", Rules: validation.List{validation.Required(), validation.Float64(), validation.Min(10)}},
}
opt := &validation.Options{
Data: data,
Rules: ruleSet,
Now: request.Now,
ConvertSingleValueArrays: false,
Language: request.Lang,
DB: ctrl.DB().WithContext(request.Context()),
Config: ctrl.Config(),
Logger: ctrl.Logger(),
Extra: map[any]any{},
}
validationErrors, errs := validation.Validate(opt)
- 验证不再像以前那样总是在最后执行。现在它按照与常规中间件相同的排序规则执行:您现在可以选择验证在中间件堆栈中何时发生。确保在权限/认证中间件之后调用
ValidateBody
/ValidateQuery
。 - 字段验证的顺序现在是有保证的,并且可以由开发人员控制。确保您的规则集或自定义规则不依赖于需要转换(
float64
/int
)的其他字段。相应地调整验证顺序或您的自定义规则。 PostValidationHooks
被移除。使用在验证中间件之后执行的中间件来获得相同的效果。- 正在验证的数据的根元素现在可以是任何东西,不一定是对象。确保在所有路径
validation.CurrentElement
的请求上添加Object()
验证器。 - 一些结构被重命名,以考虑到根元素并不总是对象这一事实:
validation.Errors
现在是validation.FieldsErrors
。validation.FieldErrors
现在是validation.Errors
。
- 验证 "rule" 现在命名为 "validator"。
自定义规则
ctx.Data
现在是any
而不是map[string]any
。如果需要,请使用安全的类型断言。ctx.Extra["request"]
变为ctx.Extra[validation.ExtraRequest{}]
ctx.Extra
不再仅限于当前验证器。在Options.Extra
中给出的相同引用在所有验证器之间共享。确保您的自定义验证器不会冲突。- 验证器实例不意味着重用。确保不要在验证器内持久化任何数据。
ctx.Valid()
变为ctx.Invalid
(因此布尔值被反转)。ctx.Rule
被移除。ctx.Rule.Params
变为验证器结构字段。值被传递给验证器构造函数。- 不要在验证器内部
panic
。如果您需要报告错误,请使用Context.AddError()
。
go
func validateCustom(ctx *validation.Context) bool {
if !ctx.Valid() {
return false
}
value, err := strconv.ParseInt(ctx.Rule.Params[0], 10, 64)
if err != nil {
panic(err)
}
ok, err := checkValue(ctx.Value, value)
if err != nil {
panic(err)
}
return ok
}
变为:
go
type CustomValidator struct{
validation.BaseValidator
Value int
}
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
ok, err := checkValue(ctx.Value)
if err != nil {
ctx.AddError(errors.New(err))
return false
}
return false
}
func Custom(value int) *CustomValidator {
return &CustomValidator{
Value: value
}
}
- 验证中占位符的概念已更改:
- 占位符不再是全局函数(替换函数):
validation.SetPlaceholder()
被移除。所有内置占位符因此也被移除。除非您按照下面的说明将它们添加回来,否则您的一些自定义规则的验证错误消息可能会损坏。 :field
占位符保持不变。- 每个验证器返回自己的占位符关联切片,并实现
MessagePlaceholders(*validation.Context) []string
。
- 占位符不再是全局函数(替换函数):
go
validation.SetPlaceholder("value", func(fieldName, language string, ctx *validation.Context) string {
return ctx.Rule.Params[0]
})
变为:
go
func (v *CustomValidator) MessagePlaceholders(_ *validation.Context) []string {
return []string{
":value", strconv.Itoa(v.Value),
}
}
规则集
- 规则集的定义已完全更改。
- 规则集不再意味着重用。应该为每个请求生成一个新的规则集。这就是为什么
route.ValidateBody()
和route.ValidateQuery()
将函数作为参数:func(*Request) validation.RuleSet
。 - 规则(现在称为 "validators")现在是结构实例。它们不再由字符串标识。
- 规则集不再有任何替代语法。
- 规则集是切片。验证的顺序是函数返回的切片的顺序。唯一的例外是数组元素,它们总是在其父元素之前执行,以便可以递归地验证数组并正确转换。
validation.CurrentElement
现在在任何地方都有效,甚至在组合之外。正在验证的根元素可以是任何东西,不一定是对象。每个规则集都应包含一些用于validation.CurrentElement
路径的验证器。
- 规则集不再意味着重用。应该为每个请求生成一个新的规则集。这就是为什么
- 验证规则的文件约定从
request.go
更改为validation.go
。 - 使用组合时,组合规则集中的验证器将相对于元素执行。
ctx.Data
将不等于根数据,而是等于与组合规则集链接的父元素。这会影响比较规则,其比较路径现在将是相对的。
go
// http/controller/request.go
var (
InsertRequest validation.RuleSet = validation.RuleSet{
"email": validation.List{"required", "string", "email", "between:3,100", "unique:users"},
"username": validation.List{"required", "string", "between:3,100", "unique:users"},
"image": validation.List{"nullable", "file", "image", "max:2048", "count:1"},
"password": validation.List{"required", "string", "between:6,100"},
}
)
变为:
go
// http/controller/validation.go
import (
"gorm.io/gorm"
"goyave.dev/goyave/v5"
v "goyave.dev/goyave/v5/validation"
)
func InsertRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "email", Rules: v.List{
v.Required(), v.String(), v.Email(), v.Between(3, 100),
v.Unique(func(db *gorm.DB, val any) *gorm.DB {
return db.Table("users").Where("email", val)
}),
}},
{Path: "username", Rules: v.List{
v.Required(), v.String(), v.Between(3, 100),
v.Unique(func(db *gorm.DB, val any) *gorm.DB {
return db.Table("users").Where("username", val)
}),
}},
{Path: "image", Rules: v.List{
v.Required(), v.File(), v.Image(), v.Max(2048), v.FileCount(1),
}},
{Path: "password", Rules: v.List{
v.Required(), v.String(), v.Between(6,100),
}},
}
}
规则
如前所述,"rules" 现在命名为 "validators"。其中一些发生了显著变化,但大多数保持了相同的行为。
- 数字规则现在让您选择确切的 Go 类型。新的验证器将自动检查输入值是否适合相应的类型。
integer
变为:Int()
、Int8()
、Int16()
、Int32()
、Int64()
、Uint()
、Uint8()
、Uint16()
、Uint32()
、Uint64()
。numeric
变为:Float32()
、Float64()
。
Array()
不再有类型参数。要验证数组元素,请添加一个匹配数组元素的路径条目。Size()
验证器及其衍生品,如Min()
、Max()
等,现在也适用于对象,并将验证其键的数量。
认证
- 认证中间件现在使用路由元数据来识别路由是否需要认证。您现在可以将认证中间件用作全局中间件,并使用
SetMeta(auth.MetaAuth, true)
标记每个路由器或路由。 - 认证器中间件现在接受一个泛型参数,代表认证用户的 DTO。
- 认证器现在使用构造函数并依赖于服务。如果您像这样初始化它们
&auth.JWTAuthenticator{}
,您现在应该使用auth.NewJWTAuthenticator(userService)
。 - 结构标签
auth:"username"
和auth:"password"
不再使用。认证现在使用用户服务。有关详细信息,请参阅认证文档。 auth.FindColumns
被移除。- 自定义认证器现在接受一个代表认证用户 DTO 的泛型参数,以及一个允许它们获取用户的用户服务参数。有关详细信息,请参阅认证文档。
- 对受密码保护的 RSA 密钥的支持已被放弃。
Websockets
- Websockets 现在使用带有控制器接口的
New()
。Websocket 控制器现在应该是实现方法Serve(*websocket.Conn, *goyave.Request) error
的组件。 - 以下 websocket 选项现在是 websocket 控制器可以实现的接口:
UpgradeErrorHandler
、ErrorHandler
、CheckOrigin
、Headers
。 - 有关详细信息,请参阅 websocket 文档。
测试
- 新的
testutil
包包含测试实用程序。 - 您不再被迫使用
testify
。您现在可以使用您选择的测试框架。 goyave.TestSuite
被移除。- 您可以在不使用套件的情况下生成测试请求和响应,使用
testutil.NewTestRequest()
和testutil.NewTestResponse()
。 - 使用
testutil.NewTestServer()
。 - 不再需要运行服务器(并监听端口)来测试您的路由。有关详细信息,请参阅测试文档。
GOYAVE_ENV
环境变量不再自动设置。- 工作目录不再自动更改。
- 测试现在可以安全地并行运行。
- 您可以在不使用套件的情况下生成测试请求和响应,使用
database.Factory
现在接受一个表示要生成的模型的泛型参数。生成器函数现在应该返回实际模型类型的指针,而不是any
。
示例
go
suite.RunServer(route.Register, func() {
resp, err := suite.Get("/hello", nil)
suite.Nil(err)
suite.NotNil(resp)
if resp != nil {
defer resp.Body.Close()
suite.Equal(200, resp.StatusCode)
suite.Equal("Hi!", string(suite.GetBody(resp)))
}
})
变为:
go
server := testutil.NewTestServerWithOptions(t, goyave.Options{Config: config.LoadDefault()})
server.RegisterRoutes(route.Register)
request := httptest.NewRequest(http.MethodGet, "/hello", nil)
response := server.TestRequest(request)
defer response.Body.Close()
// 断言
杂项
goyave.dev/filter
库已更新,部分设计已更改。DefaultSort
现在是一个选项。使用它而不是在过滤之前更改请求的数据/查询。Scope()
现在使用*filter.Request
而不是直接从 HTTP 请求读取。使用filter.NewRequest(request.Query)
创建一个。Scope()
现在返回一个错误而不是数据库实例结果。filter
查询字段现在始终是一个[]string
切片(不是string
)。- 验证错误消息名称添加了 "goyave-filter-" 前缀。如果您覆盖了这些消息,请确保更新条目的名称。
fsutil.File.Data
被移除。您应该打开并读取fsutil.File.Header.Open()
。fsutil
包中的函数现在将文件系统作为参数。util/walk
walk.Path.Walk()
回调现在接受一个指针*walk.Context
。
- 通用和组合访问记录器现在输出到结构化记录器。如果您有解析日志的服务,应相应更新。
goyave.Logger
、goyave.ErrLogger
和goyave.AccessLogger
被移除。如果您使用自定义记录器,请确保它支持结构化日志记录,并替换server.Logger
。- Gzip 中间件被移除,并由
compress.Middleware
替换。有关其新用法的信息,请参阅文档。 util/reflectutil
包被移除。util/sliceutil
包被移除。使用samber/lo
代替。util/typeutil
的函数ToFloat64()
和ToString()
被移除。goyave.BaseURL()
和goyave.ProxyBaseURL()
被移除。使用server.BaseURL()
和server.ProxyBaseURL()
代替。goyave.GetRoute()
被移除。使用router.GetRoute()
代替。可以通过request.Route.GetParent()
从请求中检索路由器。goyave.EnableMaintenance()
、goyave.DisableMaintenance()
和goyave.IsMaintenanceEnabled()
被移除。- CORS 中间件现在是全局的,意味着它在请求的生命周期中更早执行。