Skip to content

服务器

服务器表示层的核心组件。其引用用于让所有组件访问应用程序的基本资源(配置、数据库、语言、记录器等)。

服务器使用 goyave.New(options) 创建。

INFO

您可以在 go.pkg.dev 参考中了解所有可用选项的更多信息。

创建服务器时:

  • 除非在选项中指定了配置,否则将加载应用程序的配置文件,覆盖默认值。
  • 加载语言文件。
  • HTTP 服务器已初始化,但尚未在网络上监听。路由器已创建但尚未包含任何路由。
  • 如果配置未将 none 指定为数据库连接类型,则创建数据库连接池。

状态

服务器状态是一个原子数。此状态的原子性使其并发安全,因此任何 goroutine 都可以轻松读取它。它允许从任何 goroutine 调用启动和停止控制。

每个阶段状态递增:

  • 0 已创建:服务器刚刚使用 goyave.New() 创建
  • 1 准备中:已调用 Start() 方法,但服务器尚未准备好服务请求
  • 2 就绪:服务器已准备好服务请求
  • 3 已停止:服务器已停止(无论是手动停止、从信号钩子停止,还是因为发生错误)

此状态不可直接访问,并由 IsReady() 方法抽象。

状态永远不会递减,因为服务器实例不打算重用。停止后,服务器无法重新启动

钩子

启动钩子

启动钩子在网络监听器开始接受连接后,在单个 goroutine 中按注册顺序执行,它们共享该 goroutine。

go
server, err := goyave.New(goyave.Options{})
if err != nil {
	fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
	os.Exit(1)
}

server.RegisterStartupHook(func(s *goyave.Server) {
	s.Logger.Info("服务器正在监听", "host", s.Host())
})

WARNING

如果发生错误,server.Start() 方法立即返回。在这种情况下,您的启动钩子可能会也可能不会执行。框架通过在执行前检查状态是否就绪来限制这种情况,但由于 Go 并发模型的性质,goroutine 可能在返回服务错误之前启动。

对于强烈依赖服务器资源的启动钩子,最好再次检查服务器状态是否就绪。

go
server.RegisterStartupHook(func(s *goyave.Server) {
	if !s.IsReady() {
		return
	}
	//...
})

关闭钩子

关闭钩子在服务器停止时执行,在 server.Start() 返回之前,并按注册顺序在与 server.Start() 调用者相同的 goroutine 中执行。

如果在创建网络监听器时发生错误,则不执行关闭钩子。

这些钩子在需要停止后台进程或 goroutine(例如 websocket 集线器处理器)时非常有用。

WARNING

因此,关闭钩子是阻塞操作,不使用超时机制。如果您期望钩子需要不确定的时间,您有责任自己实现超时机制。

go
server, err := goyave.New(goyave.Options{})
if err != nil {
	fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
	os.Exit(1)
}

server.RegisterShutdownHook(func(s *goyave.Server) {
	s.Logger.Info("服务器正在关闭")
})

OS 信号钩子

您可以注册一个 OS 信号钩子,该钩子将监听 SIGINTSIGTERM 信号。如果收到这些信号之一,将启动服务器的关闭过程。

go
server, err := goyave.New(goyave.Options{})
if err != nil {
	fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
	os.Exit(1)
}

server.RegisterSignalHook()

信号监听器在服务器停止时自动移除。此信号通道是缓冲的,缓冲区大小为 64,为同时发送许多信号留出足够空间。

TIP

建议始终注册信号钩子,测试除外。正确处理信号将改善应用程序在生产环境中的行为。

注册路由

在启动服务器之前,您需要注册路由:

go
server, err := goyave.New(goyave.Options{})
if err != nil {
	fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
	os.Exit(1)
}

server.RegisterRoutes(route.Register) // route.Register 是一个 func(*Server, *Router)

信息

您可以在路由文档中找到有关路由的更多信息。

启动

一旦服务器准备就绪,并且您已注册所有钩子、服务、仓库和路由,您可以启动服务器。

go
if err := server.Start(); err != nil {
	server.Logger.Error(err)
	os.Exit(3)
}

TIP

server.Start() 返回的错误将始终是 *goyave.dev/goyave/v5/util/errors.Error 类型。

停止

可以从任何 goroutine 手动停止服务器。这是一个并发安全的操作。

go
server.Stop()

当服务器停止时,活动连接不会中断,但不会接受新连接。

server.Start() 在执行所有关闭钩子(按注册顺序)后返回。如果有任何数据库连接,它也会干净地关闭。

TIP

您应始终确保程序在 server.Start() 返回后退出,以实现优雅关闭。

上下文

服务器利用上下文 API。默认情况下,服务器使用 context.Background(),但可以在 Options 中使用 options.BaseContext 指定自定义基础上下文。

它将用作所有传入请求的基础上下文。提供的 net.Listener 是即将开始接受请求的特定 Listener。返回的上下文中添加了服务器实例作为值。因此可以使用 goyave.ServerFromContext(ctx) 检索服务器。如果上下文被取消,服务器不会自动关闭,如果您希望发生这种情况,您有责任调用 server.Stop()。否则服务器将继续服务请求,但可能会生成“上下文已取消”错误。

go
type customKey struct{}

func main() {
	opts := goyave.Options{
		BaseContext: func(l net.Listener) context.Context {
			return context.WithValue(context.Background(), customKey{}, "自定义值")
		},
	}

	//...
}

数据库

可以从服务器使用 server.DB() 检索数据库连接。

go
server.DB()

事务模式

Goyave 支持测试的事务模式。您可以设置它,以便所有数据库请求都在可以回滚的事务中运行。

go
import (
	//...
	"goyave.dev/goyave/v5/util/testutil"
)

func TestDB(t *testing.T) {
	server, err := testutil.NewTestServer(t, "config.test.json")
	//...
	rollback := server.Transaction()
	//...
	t.Cleanup(rollback)
}

手动替换数据库

如果需要,您可以手动替换数据库,例如用于模拟。这不是并发安全的,应仅用于测试。

如果连接已存在,则在丢弃之前关闭它。

go
import (
	//...
	"goyave.dev/goyave/v5/util/testutil"
	"gorm.io/gorm/utils/tests"
)

func TestDB(t *testing.T) {
	server, err := testutil.NewTestServer(t, "config.test.json")
	//...
	err := server.ReplaceDB(tests.DummyDialector{})
	//...
}