Ciclo de vida da aplicação
Entenda o ciclo de vida da aplicação Wails, da inicialização ao encerramento
Entendendo o ciclo de vida da aplicação
As aplicações desktop têm um ciclo de vida que vai da inicialização ao encerramento. O Wails v3 fornece serviços, eventos e hooks para gerenciar esse ciclo de vida com eficiência.
Etapas do ciclo de vida
1. Criação da aplicação
Crie sua aplicação com application.New():
app := application.New(application.Options{
Name: "My App",
Description: "An application built with Wails",
Services: []application.Service{
application.NewService(&MyService{}),
},
Assets: application.AssetOptions{
Handler: application.BundledAssetFileServer(assets),
},
})O que acontece:
- As opções são analisadas e validadas
- Os serviços são registrados (mas ainda não são iniciados)
- O servidor de assets é configurado
- O runtime é configurado
2. Execução da aplicação
Chame app.Run() para iniciar a aplicação:
err := app.Run() // Blocks until quit
if err != nil {
log.Fatal(err)
}O que acontece:
- Os serviços são iniciados na ordem de registro
- Os listeners de eventos são ativados
- As janelas podem ser criadas
- O loop de eventos é iniciado
3. Loop de eventos
A aplicação entra no loop de eventos, no qual passa a maior parte do tempo:
- Os eventos do sistema operacional são processados (eventos de mouse, teclado e janela)
- As mensagens de Go para JS são tratadas
- As chamadas de JS para Go são executadas
- As atualizações da interface são renderizadas
4. Encerramento
Quando a aplicação é encerrada:
- O callback
ShouldQuité verificado (se estiver definido) - Os callbacks
OnShutdownsão executados - Os serviços são encerrados na ordem inversa
- As janelas são fechadas
- Os recursos são liberados
Ciclo de vida dos serviços
Os serviços são a principal forma de gerenciar o ciclo de vida no Wails v3. Eles fornecem hooks de inicialização e encerramento por meio de interfaces. Para consultar a documentação completa sobre serviços, consulte o guia de serviços.
Criando um serviço
type MyService struct {
db *sql.DB
}
// ServiceStartup is called when the application starts
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
var err error
s.db, err = sql.Open("sqlite3", "app.db")
if err != nil {
return err // Startup aborts if error returned
}
// Run migrations
if err := s.runMigrations(); err != nil {
return err
}
return nil
}
// ServiceShutdown is called when the application shuts down
func (s *MyService) ServiceShutdown() error {
if s.db != nil {
return s.db.Close()
}
return nil
}Registrando serviços
app := application.New(application.Options{
Services: []application.Service{
application.NewService(&MyService{}),
application.NewService(&AnotherService{}),
},
})Pontos principais:
- Os serviços são iniciados na ordem de registro
- Os serviços são encerrados na ordem inversa à de registro
- Se
ServiceStartupde um serviço retornar um erro, a aplicação será abortada - O
ctxpassado paraServiceStartupé cancelado quando o encerramento começa
Usando o contexto da aplicação
O contexto passado para ServiceStartup é válido durante todo o ciclo de vida do aplicativo:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
// Start a background task that respects shutdown
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.performBackgroundSync()
case <-ctx.Done():
// Application is shutting down
return
}
}
}()
return nil
}Você também pode acessar o contexto pela instância do aplicativo:
app := application.Get()
ctx := app.Context()Hooks no nível do aplicativo
Esses são callbacks de conveniência em application.Options que permitem interagir com o ciclo de vida do aplicativo sem criar um serviço completo. Eles são úteis para tarefas simples de limpeza, confirmação de encerramento ou quando você precisa executar código em pontos específicos da sequência de desligamento.
Para um gerenciamento mais complexo do ciclo de vida, com lógica de inicialização, injeção de dependências ou recursos com estado, use Serviços.
ShouldQuit
O callback ShouldQuit é chamado sempre que o encerramento é solicitado, seja quando o usuário fecha a última janela, pressiona Cmd+Q (macOS) ou Alt+F4 (Windows), seja quando app.Quit() é chamado programaticamente.
Valor de retorno:
- Retorne
truepara permitir que o encerramento prossiga (o aplicativo será desligado) - Retorne
falsepara cancelar o encerramento (o aplicativo continuará em execução)
Essa é a oportunidade de interceptar solicitações de encerramento e, opcionalmente, impedi-las, por exemplo, para avisar o usuário sobre alterações não salvas:
app := application.New(application.Options{
ShouldQuit: func() bool {
if !hasUnsavedChanges() {
return true // No unsaved changes, allow quit
}
// Prompt the user — MessageDialog.Show() blocks and returns nothing.
// The button's OnClick callback fires for whichever button the user picks.
shouldQuit := false
dlg := application.Get().Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("You have unsaved changes. Quit anyway?")
quit := dlg.AddButton("Quit")
cancel := dlg.AddButton("Cancel")
dlg.SetDefaultButton(cancel)
dlg.SetCancelButton(cancel)
quit.OnClick(func() { shouldQuit = true })
dlg.Show()
return shouldQuit
},
})Se ShouldQuit não estiver definido, o aplicativo será encerrado imediatamente quando solicitado.
Quando ShouldQuit é chamado:
- O usuário fecha a última janela (a menos que
DisableQuitOnLastWindowClosedesteja definido) - O usuário pressiona Cmd+Q no macOS
- O usuário pressiona Alt+F4 no Windows (quando o foco está na última janela)
- O código chama
app.Quit()
Quando ShouldQuit NÃO é chamado:
- O processo é encerrado à força (SIGKILL ou encerramento forçado pelo Gerenciador de Tarefas)
os.Exit()é chamado diretamente
OnShutdown
O callback OnShutdown é chamado quando se confirma que o aplicativo será encerrado (depois que ShouldQuit retorna true, caso esteja definido). Use-o para tarefas de limpeza, como salvar o estado, fechar conexões com o banco de dados ou liberar recursos.
app := application.New(application.Options{
OnShutdown: func() {
// Save application state
saveState()
// Close connections
cleanup()
},
})Você também pode registrar callbacks adicionais de desligamento programaticamente a qualquer momento durante o ciclo de vida do aplicativo:
app.OnShutdown(func() {
log.Println("Application shutting down...")
})Vários callbacks são executados na ordem em que foram registrados. O processo de desligamento fica bloqueado até que todos os callbacks sejam concluídos.
Importante: Mantenha os callbacks de desligamento rápidos (menos de 1 segundo). O sistema operacional pode encerrar à força aplicativos que demoram demais para fechar, o que pode interromper a limpeza e causar perda de dados.
PostShutdown
O callback PostShutdown é chamado depois que todas as tarefas de desligamento são concluídas, pouco antes do término do processo. Nesse ponto, a instância do aplicativo não pode mais ser usada: as janelas estão fechadas, os serviços foram desligados e os recursos foram liberados.
Isso é útil principalmente para:
- Registro final de logs que precisa ocorrer depois de todas as outras tarefas de limpeza
- Testes e depuração do comportamento de desligamento
- Plataformas nas quais
app.Run()não retorna (o callback garante que seu código seja executado)
app := application.New(application.Options{
PostShutdown: func() {
// Final logging
log.Println("Application terminated cleanly")
// Flush any buffered logs
logger.Sync()
},
})Observação: Não tente usar recursos do aplicativo (janelas, caixas de diálogo etc.) em PostShutdown, pois eles não estão mais disponíveis.
Ciclo de vida baseado em eventos
O Wails fornece um sistema de eventos que notifica você quando algo acontece no aplicativo, como a abertura de janelas, a inicialização do aplicativo, alterações de tema e muito mais. Você pode escutar esses eventos para reagir a mudanças no ciclo de vida sem bloqueá-las nem interceptá-las.
Para eventos de janela, você também pode usar RegisterHook em vez de OnWindowEvent para interceptar e cancelar ações, por exemplo, para impedir que uma janela seja fechada. Consulte Hooks de janela abaixo.
Para consultar a documentação completa do sistema de eventos, consulte o guia de eventos.
Eventos do aplicativo
Escute os eventos do ciclo de vida do aplicativo:
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
app.Logger.Info("Application has started!")
})Também há eventos específicos de cada plataforma:
// macOS
app.Event.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(event *application.ApplicationEvent) {
// Handle macOS launch
})
app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(event *application.ApplicationEvent) {
// Handle macOS termination
})
// Windows
app.Event.OnApplicationEvent(events.Windows.ApplicationStarted, func(event *application.ApplicationEvent) {
// Handle Windows start
})Eventos de janela
Escute os eventos do ciclo de vida das janelas:
window := app.Window.New()
window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) {
app.Logger.Info("Window gained focus")
})
window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) {
app.Logger.Info("Window is closing")
})Hooks de janela (eventos canceláveis)
Use RegisterHook em vez de OnWindowEvent quando precisar cancelar um evento:
window := app.Window.New()
var countdown = 3
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
countdown--
if countdown > 0 {
app.Logger.Info("Not closing yet!", "remaining", countdown)
e.Cancel() // Prevent the window from closing
return
}
app.Logger.Info("Window closing now")
})Diferença entre OnWindowEvent e RegisterHook:
OnWindowEvent: notifica você quando ocorre um evento (não permite cancelá-lo)RegisterHook: permite interceptar e, possivelmente, cancelar o evento
Ciclo de vida das janelas
As janelas têm seu próprio ciclo de vida, desde a criação até a destruição. Cada janela carrega o conteúdo do frontend de forma independente e pode ser exibida, ocultada ou fechada a qualquer momento. Quando um usuário tenta fechar uma janela, você pode interceptar essa ação com um RegisterHook para solicitar confirmação ou ocultar a janela em vez de destruí-la.
Para consultar a documentação completa sobre janelas, veja o guia de janelas.
Criação de janelas
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "My Window",
Width: 800,
Height: 600,
})Como impedir o fechamento da janela
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
if !hasUnsavedChanges() {
return
}
// MessageDialog.Show() returns nothing; per-button OnClick handlers fire.
dlg := application.Get().Dialog.Question().
SetTitle("Unsaved Changes").
SetMessage("Save before closing?")
save := dlg.AddButton("Save")
discard := dlg.AddButton("Discard")
cancel := dlg.AddButton("Cancel")
dlg.SetDefaultButton(save)
dlg.SetCancelButton(cancel)
save.OnClick(func() { saveChanges() })
cancel.OnClick(func() { e.Cancel() }) // Prevent close
_ = discard // "Discard" falls through and allows close
dlg.Show()
})Ocultar em vez de fechar
Um padrão comum para aplicativos da bandeja do sistema:
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
window.Hide() // Hide instead of destroy
e.Cancel() // Prevent actual close
})Ciclo de vida com várias janelas
Com várias janelas:
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Main Window",
})
settingsWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Settings",
Width: 400,
Height: 600,
Hidden: true, // Start hidden
})O comportamento padrão varia conforme a plataforma:
| Plataforma | Comportamento padrão ao fechar a última janela |
|---|---|
| macOS | O aplicativo continua em execução (a barra de menus permanece) |
| Windows | O aplicativo é encerrado |
| Linux | O aplicativo é encerrado |
O macOS segue as convenções nativas da plataforma, nas quais os aplicativos normalmente permanecem ativos na barra de menus mesmo sem nenhuma janela. No Windows e no Linux, eles são encerrados por padrão.
Fazer o aplicativo ser encerrado em todas as plataformas quando a última janela for fechada:
app := application.New(application.Options{
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})Fazer o aplicativo continuar em execução em todas as plataformas quando a última janela for fechada:
Isso é útil para aplicativos da bandeja do sistema ou aplicativos que devem continuar em execução em segundo plano.
app := application.New(application.Options{
Windows: application.WindowsOptions{
DisableQuitOnLastWindowClosed: true,
},
Linux: application.LinuxOptions{
DisableQuitOnLastWindowClosed: true,
},
})Padrões comuns
Padrão 1: serviço de banco de dados
type DatabaseService struct {
db *sql.DB
}
func (s *DatabaseService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
var err error
s.db, err = sql.Open("sqlite3", "app.db")
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
if err := s.db.PingContext(ctx); err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
return nil
}
func (s *DatabaseService) ServiceShutdown() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
// Exported methods are available to the frontend
func (s *DatabaseService) GetUsers() ([]User, error) {
// Query implementation
}Padrão 2: serviço de configuração
type ConfigService struct {
config *Config
path string
}
func (s *ConfigService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
s.path = "config.json"
data, err := os.ReadFile(s.path)
if err != nil {
if os.IsNotExist(err) {
s.config = &Config{} // Default config
return nil
}
return err
}
return json.Unmarshal(data, &s.config)
}
func (s *ConfigService) ServiceShutdown() error {
data, err := json.MarshalIndent(s.config, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.path, data, 0644)
}Padrão 3: worker em segundo plano
type WorkerService struct {
cancel context.CancelFunc
}
func (s *WorkerService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
workerCtx, cancel := context.WithCancel(ctx)
s.cancel = cancel
go s.runWorker(workerCtx)
return nil
}
func (s *WorkerService) ServiceShutdown() error {
if s.cancel != nil {
s.cancel()
}
return nil
}
func (s *WorkerService) runWorker(ctx context.Context) {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.doWork()
case <-ctx.Done():
return
}
}
}Referência do ciclo de vida
| Hook/interface | Quando é chamado | Pode cancelar? | Finalidade |
|---|---|---|---|
ServiceStartup |
Durante app.Run(), antes do loop de eventos |
Não (retorne um erro para interromper) | Inicialização |
ServiceShutdown |
Durante o encerramento, após OnShutdown |
Não | Limpeza |
OnShutdown |
Quando o encerramento é confirmado | Não | Limpeza do aplicativo |
ShouldQuit |
Quando o encerramento é solicitado | Sim (retorne false) | Confirmar encerramento |
RegisterHook(WindowClosing) |
Quando o fechamento da janela é solicitado | Sim (e.Cancel()) |
Impedir o fechamento da janela |
OnWindowEvent |
Quando o evento ocorre | Não | Reagir a eventos |
OnApplicationEvent |
Quando o evento ocorre | Não | Reagir a eventos |
Diferenças entre plataformas
macOS
- O menu do aplicativo permanece disponível mesmo sem nenhuma janela
- Cmd+Q inicia o encerramento do aplicativo (passando por
ShouldQuit) - O ícone no Dock permanece visível, a menos que seja ocultado
- Use
ApplicationShouldTerminateAfterLastWindowClosedpara controlar o comportamento de encerramento
Windows
- Não há menu do aplicativo sem uma janela
- Alt+F4 fecha a janela (isso pode ser impedido com
RegisterHook) - A bandeja do sistema pode manter o aplicativo em execução
Linux
- O comportamento varia de acordo com o ambiente de desktop
- Geralmente semelhante ao Windows
Depuração de problemas do ciclo de vida
Problema: o aplicativo não encerra
Causas:
ShouldQuitretornandofalseOnShutdowndemorando demais- Goroutines em segundo plano não são interrompidas
Solução:
// 1. Check ShouldQuit logic
ShouldQuit: func() bool {
log.Println("ShouldQuit called")
return true
}
// 2. Keep OnShutdown fast
OnShutdown: func() {
log.Println("OnShutdown started")
// Fast cleanup only
log.Println("OnShutdown finished")
}
// 3. Use context for background tasks
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
go func() {
<-ctx.Done()
log.Println("Context cancelled, stopping background work")
}()
return nil
}Problema: falha na inicialização do serviço
Solução: Retorne erros descritivos:
func (s *MyService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if err := s.init(); err != nil {
return fmt.Errorf("failed to initialise: %w", err)
}
return nil
}O erro será registrado, e o aplicativo não será iniciado.
Práticas recomendadas
Faça
- Use serviços para gerenciar o ciclo de vida — Eles fornecem hooks adequados de inicialização e encerramento
- Mantenha o encerramento rápido — Procure concluir toda a limpeza em menos de 1 segundo
- Use o contexto para cancelamento — Interrompa corretamente as tarefas em segundo plano
- Trate erros durante a inicialização — Retorne os erros para interromper o processo de forma segura
- Registre os eventos do ciclo de vida — Isso ajuda na depuração
Não faça
- Não bloqueie durante a inicialização do serviço — Mantenha a inicialização rápida (menos de 2 segundos)
- Não exiba caixas de diálogo durante o encerramento — O aplicativo está sendo encerrado, e a interface pode não funcionar
- Não ignore o contexto — Sempre verifique
ctx.Done()nas goroutines - Não cause vazamento de recursos — Sempre implemente
ServiceShutdown
Próximas etapas
Serviços — Saiba mais sobre o sistema de serviços Saiba mais →
Sistema de eventos — Use eventos para comunicação Saiba mais →
Gerenciamento de janelas — Crie e gerencie janelas Saiba mais →
Tem dúvidas sobre o ciclo de vida? Pergunte no Discord ou consulte os exemplos.