Server
The server is the central component of the presentation layer. Its reference is used to let all components access the essential resources of an application (configuration, database, language, logger, etc).
A server is created with goyave.New(options)
.
INFO
You can learn more about all the available options on the go.pkg.dev reference.
When the server is created:
- Unless a configuration is specified in the options, the application's configuration file is loaded, overriding the default values.
- The language files are loaded.
- The HTTP server is initialized but it doesn't listen on the network yet. The router is created but doesn't contain any route yet.
- The database connection pool is created if the configuration doesn't specify
none
as the database connection type.
State
The server state is an atomic number. The atomic nature of this state makes it concurrently safe, so any goroutine can read it easily. It allows the start and stop controls to be called from any goroutine.
The state is incremented for each stage:
0
Created: the server was just created withgoyave.New()
1
Preparing: theStart()
method has ben called but the server is not yet ready to serve requests2
Ready: the server is ready to serve requests3
Stopped: the server was stopped (either manually, from a signal hook, or because an error occurred)
This state is not directly accessible and is abstracted by the IsReady()
method.
The state is never decremented because a server instance is not meant for re-use. After being stopped, a server cannot be restarted.
Hooks
Startup hooks
Startup hooks are executed in a single goroutine that they all share, in the order of registration, after the network listener starts accepting connections.
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("Server is listening", "host", s.Host())
})
WARNING
In case of error, the server.Start()
method immediately returns. In this case, your startup hooks may or may not be executed. The framework limits this by checking if the state is ready before executing them, but due to the nature of the Go concurrency model, the goroutine might start before the serve error is returned.
For startup hooks that strongly depend on the server's resources, it is a good practice to check that the server's state is ready a second time.
server.RegisterStartupHook(func(s *goyave.Server) {
if !s.IsReady() {
return
}
//...
})
Shutdown hooks
Shutdown hooks are executed when the server stops before server.Start()
returns, and are executed in the order of registration in the same goroutine as the caller of server.Start()
.
Shutdown hooks are not executed if there is an error while creating the network listener.
These hooks are useful when you need to stop background processes or goroutines (such as a websocket hub handler).
WARNING
Shutdown hooks are therefore blocking operations and do not use a timeout mechanism. If you expect your hooks to take an indeterminate amount of time, you are responsible of implementing a timeout mechanism yourself.
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("Server is shutting down")
})
OS Signal hook
You can register a OS signal hook that will listen on SIGINT
and SIGTERM
signals. If one of these signals is received, the server's shutdown process is initiated.
server, err := goyave.New(goyave.Options{})
if err != nil {
fmt.Fprintln(os.Stderr, err.(*errors.Error).String())
os.Exit(1)
}
server.RegisterSignalHook()
The signal listener is removed automatically when the server stops. This signal channel is buffered with a buffer size of 64, leaving enough room in case many signals are sent at once.
TIP
It is recommended to always register the signal hooks, except in tests. Properly handling signals will improve how your application behaves when deployed in production.
Registering routes
Before starting your server, you need to regsiter your routes:
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 is a func(*Server, *Router)
Info
You can find more information about routing in the routing documentation.
Start
Once the server is ready and you have registered all your hooks, services, repositories and routes, you can start your server.
if err := server.Start(); err != nil {
server.Logger.Error(err)
os.Exit(3)
}
TIP
The error returned by server.Start()
will always be of type *goyave.dev/goyave/v5/util/errors.Error
.
Stop
The server can be stopped manually from any goroutine. It is a concurrently safe operation.
server.Stop()
When the server stops, active connections are not interrupted but new connections won't be accepted.
server.Start()
returns after the execution of all shutdown hooks in the order of registration. If there is any, the database connection is cleanly closed as well.
TIP
You should always make sure that your program exits after server.Start()
returns for a graceful shutdown.
Context
The server makes use of the context API. By default, the server uses context.Background()
, but a custom base context can be specified in the Options
with options.BaseContext
.
It will be used as base context for all incoming requests. The provided net.Listener
is the specific Listener
that's about to start accepting requests. The context returned has a the server instance added to it as a value. The server can thus be retrieved using goyave.ServerFromContext(ctx)
. If the context is canceled, the server won't shut down automatically, you are responsible of calling server.Stop()
if you want this to happen. Otherwise the server will continue serving requests, at the risk of generating "context canceled" errors.
type customKey struct{}
func main() {
opts := goyave.Options{
BaseContext: func(l net.Listener) context.Context {
return context.WithValue(context.Background(), customKey{}, "custom value")
},
}
//...
}
Database
The database connection can be retrieved from the server with server.DB()
.
server.DB()
Transaction mode
Goyave supports transaction mode for tests. You can set it so all DB requests are run inside a transaction that can then be rolled back.
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)
}
Manually replacing the database
You can manually replace the database if need to, for example for mocking. This is not concurrently safe and should only be used in tests.
If a connection already exists, it is closed before being discarded.
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{})
//...
}