Maison  >  Article  >  développement back-end  >  Création de services système multiplateformes dans Go : un guide étape par étape

Création de services système multiplateformes dans Go : un guide étape par étape

Linda Hamilton
Linda Hamiltonoriginal
2024-11-04 08:18:01843parcourir

Building Cross-Platform System Services in Go: A Step-by-Step Guide

Que sont les services système ?

Les services système sont des programmes légers qui fonctionnent en arrière-plan sans interface utilisateur graphique . Ils démarrent automatiquement lors du démarrage du système et s'exécutent indépendamment. Leur cycle de vie, qui comprend des opérations telles que le démarrage, l'arrêt et le redémarrage, est géré par Service Control Manager sous Windows, systemd sous Linux (dans la plupart des destructions) et launchd sur macOS.

Contrairement aux applications standard, les services sont conçus pour un fonctionnement continu et sont essentiels pour des tâches telles que la surveillance, la journalisation et d'autres processus en arrière-plan. Sous Linux, ces services sont généralement appelés démons, tandis que sur macOS, ils sont appelés agents de lancement ou démons.

Pourquoi opter pour les services de systèmes de construction* ?*

La création de services système multiplateformes nécessite un langage qui équilibre efficacité, convivialité et fiabilité. Go excelle à cet égard pour plusieurs raisons :

  • Concurrence et performances : les goroutines de Go facilitent l'exécution de plusieurs tâches à la fois, améliorant ainsi l'efficacité et la vitesse sur différentes plates-formes. Associé à une bibliothèque standard robuste, cela minimise les dépendances externes et améliore la compatibilité multiplateforme.

  • Gestion et stabilité de la mémoire : le garbage collection de Go empêche les fuites de mémoire et maintient les systèmes stables. Sa gestion claire des erreurs facilite également le débogage de services complexes.

  • Simplicité et maintenabilité : la syntaxe claire de Go simplifie l'écriture et la maintenance des services . Sa capacité à produire des fichiers binaires liés statiquement donne lieu à des fichiers exécutables uniques qui incluent toutes les dépendances nécessaires, éliminant ainsi le besoin d'environnements d'exécution séparés.

  • Compilation croisée et flexibilité : la prise en charge de Go pour la compilation croisée permet de créer des exécutables pour différents systèmes d'exploitation à partir d'une seule base de code. Avec CGO, Go peut interagir avec des API système de bas niveau, telles que Win32 et Objective-C, offrant ainsi aux développeurs la flexibilité nécessaire pour exploiter les fonctionnalités natives.

Services de rédaction dans Go

Cette procédure pas à pas de code suppose que GO est installé sur votre ordinateur et que vous avez une connaissance de base de la syntaxe de GO, sinon je vous recommande fortement de faire un tour .

Aperçu du projet

go-service/
├── Makefile                 # Build and installation automation
├── cmd/
│   └── service/
│       └── main.go          # Main entry point with CLI flags and command handling
├── internal/
│   ├── service/
│   │   └── service.go       # Core service implementation
│   └── platform/            # Platform-specific implementations
│       ├── config.go        # Configuration constants
│       ├── service.go       # Cross-platform service interface
│       ├── windows.go       # Windows-specific service management
│       ├── linux.go         # Linux-specific systemd service management
│       └── darwin.go        # macOS-specific launchd service management
└── go.mod                   # Go module definition

Étape 1 : Définir les configurations

Initialisez le module Go :

go mod init go-service

Définissez les constantes de configuration dans config.go dans le répertoire internal/platform. Ce fichier centralise toutes les valeurs configurables, facilitant ainsi l'ajustement des paramètres.

Fichier : internal/platform/config.go

package main

const (
 ServiceName    = "go-service"                                                      //Update your service name
 ServiceDisplay = "Go Service"                                                      // Update your display name
 ServiceDesc    = "A service that appends 'Hello World' to a file every 5 minutes." // Update your Service Description
 LogFileName    = "go-service.log"                                              // Update your Log file name
)

func GetInstallDir() string {
 switch runtime.GOOS {
 case "darwin":
  return "/usr/local/opt/go-service"
 case "linux":
  return "/opt/go-service"
 case "windows":
  return filepath.Join(os.Getenv("ProgramData"), ServiceName)
 default:
  return ""
 }
}

func copyFile(src, dst string) error {
 source, err := os.Open(src)
 if err != nil {
  return fmt.Errorf("failed to open source file: %w", err)
 }
 defer source.Close()

 destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
 if err != nil {
  return fmt.Errorf("failed to create destination file: %w", err)
 }
 defer destination.Close()

 _, err = io.Copy(destination, source)
 return err
}

Principales fonctionnalités :

  • Constantes de service qui seront utilisées lors des configurations de service spécifiques à la plate-forme.

  • GetInstallDir() fournit des chemins d'installation de service et de fichiers journaux appropriés pour chaque système d'exploitation.

  • copyFile() utilisé lors de l'installation du service pour copier l'exécutable vers notre chemin spécifique fourni par GetInstallDir().

Étape 2 : Définir la logique du service principal

Dans l'interne/service, implémentez la fonctionnalité de base de votre service. L'implémentation du service de base gère la fonctionnalité principale de notre service.

Dans cet exemple, le service ajoute « Hello World » à un fichier du répertoire personnel de l'utilisateur toutes les 5 minutes.

Fichier :interne/service/service.go

Structure des services

go-service/
├── Makefile                 # Build and installation automation
├── cmd/
│   └── service/
│       └── main.go          # Main entry point with CLI flags and command handling
├── internal/
│   ├── service/
│   │   └── service.go       # Core service implementation
│   └── platform/            # Platform-specific implementations
│       ├── config.go        # Configuration constants
│       ├── service.go       # Cross-platform service interface
│       ├── windows.go       # Windows-specific service management
│       ├── linux.go         # Linux-specific systemd service management
│       └── darwin.go        # macOS-specific launchd service management
└── go.mod                   # Go module definition

Création de services

go mod init go-service

Cycle de vie des services

Le service implémente les méthodes Start et Stop pour la gestion du cycle de vie :

package main

const (
 ServiceName    = "go-service"                                                      //Update your service name
 ServiceDisplay = "Go Service"                                                      // Update your display name
 ServiceDesc    = "A service that appends 'Hello World' to a file every 5 minutes." // Update your Service Description
 LogFileName    = "go-service.log"                                              // Update your Log file name
)

func GetInstallDir() string {
 switch runtime.GOOS {
 case "darwin":
  return "/usr/local/opt/go-service"
 case "linux":
  return "/opt/go-service"
 case "windows":
  return filepath.Join(os.Getenv("ProgramData"), ServiceName)
 default:
  return ""
 }
}

func copyFile(src, dst string) error {
 source, err := os.Open(src)
 if err != nil {
  return fmt.Errorf("failed to open source file: %w", err)
 }
 defer source.Close()

 destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
 if err != nil {
  return fmt.Errorf("failed to create destination file: %w", err)
 }
 defer destination.Close()

 _, err = io.Copy(destination, source)
 return err
}

Boucle de service principale

type Service struct {
 logFile string
 stop    chan struct{}
 wg      sync.WaitGroup
 started bool
 mu      sync.Mutex
}

La méthode run gère la logique de base du service :

Principales fonctionnalités :

  • Exécution basée sur des intervalles à l'aide du ticker

  • Prise en charge de l'annulation contextuelle

  • Gestion des arrêts en douceur

  • Journalisation des erreurs

Écriture de journaux

Le service ajoute « Hello World » avec un horodatage toutes les 5 minutes

func New() (*Service, error) {
 installDir := platform.GetInstallDir()
 if installDir == "" {
  return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
 }

 logFile := filepath.Join(installDir, "logs", platform.LogFileName)

 return &Service{
  logFile: logFile,
  stop:    make(chan struct{}),
 }, nil
}

Étape 3 : Création de configurations de service spécifiques à la plate-forme

Le répertoire internal/platform contient des configurations spécifiques à la plate-forme pour installer, désinstaller et gérer le service.

macOS (darwin.go)

Dans darwin.go, définissez la logique spécifique à macOS pour créer un fichier .plist, qui gère l'installation et la désinstallation du service à l'aide de launchctl.

Fichier :internal/platform/darwin.go

// Start the service
func (s *Service) Start(ctx context.Context) error {
 s.mu.Lock()
 if s.started {
  s.mu.Unlock()
  return fmt.Errorf("service already started")
 }
 s.started = true
 s.mu.Unlock()

 if err := os.MkdirAll(filepath.Dir(s.logFile), 0755); err != nil {
  return fmt.Errorf("failed to create log directory: %w", err)
 }

 s.wg.Add(1)
 go s.run(ctx)

 return nil
}

// Stop the service gracefully
func (s *Service) Stop() error {
 s.mu.Lock()
 if !s.started {
  s.mu.Unlock()
  return fmt.Errorf("service not started")
 }
 s.mu.Unlock()

 close(s.stop)
 s.wg.Wait()

 s.mu.Lock()
 s.started = false
 s.mu.Unlock()

 return nil
}

Linux (linux.go)

Sous Linux, nous utilisons systemd pour gérer le service. Définissez un fichier .service et les méthodes associées.

Fichier :internal/platform/linux.go

func (s *Service) run(ctx context.Context) {
 defer s.wg.Done()
 log.Printf("Service started, logging to: %s\n", s.logFile)

 ticker := time.NewTicker(5 * time.Minute)
 defer ticker.Stop()

 if err := s.writeLog(); err != nil {
  log.Printf("Error writing initial log: %v\n", err)
 }

 for {
  select {
  case <-ctx.Done():
   log.Println("Service stopping due to context cancellation")
   return
  case <-s.stop:
   log.Println("Service stopping due to stop signal")
   return
  case <-ticker.C:
   if err := s.writeLog(); err != nil {
    log.Printf("Error writing log: %v\n", err)
   }
  }
 }
}

Windows (windows.go)

Pour Windows, utilisez la commande sc pour installer et désinstaller le service.

Fichier :internal/platform/windows.go

func (s *Service) writeLog() error {
 f, err := os.OpenFile(s.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 if err != nil {
  return fmt.Errorf("failed to open log file: %w", err)
 }
 defer f.Close()

 _, err = f.WriteString(fmt.Sprintf("[%s] Hello World\n", time.Now().Format(time.RFC3339)))
 if err != nil {
  return fmt.Errorf("failed to write to log file: %w", err)
 }
 return nil
}

Étape 4 : configuration du fichier principal (main.go)

Enfin, configurez main.go dans cmd/service/main.go pour gérer l'installation, la désinstallation et le démarrage du service.

Fichier :cmd/service/main.go

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
)

type darwinService struct{}

const plistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>%s</string>
    <key>ProgramArguments</key>
    <array>
        <string>%s</string>
        <string>-run</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>WorkingDirectory</key>
    <string>%s</string>
</dict>
</plist>`

func (s *darwinService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 // Copy binary to installation directory
 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 plistPath := filepath.Join("/Library/LaunchDaemons", ServiceName+".plist")
 content := fmt.Sprintf(plistTemplate, ServiceName, installedBinary, installDir)

 if err := os.WriteFile(plistPath, []byte(content), 0644); err != nil {
  return fmt.Errorf("failed to write plist file: %w", err)
 }

 if err := exec.Command("launchctl", "load", plistPath).Run(); err != nil {
  return fmt.Errorf("failed to load service: %w", err)
 }
 return nil
}

func (s *darwinService) Uninstall() error {
 plistPath := filepath.Join("/Library/LaunchDaemons", ServiceName+".plist")

 if err := exec.Command("launchctl", "unload", plistPath).Run(); err != nil {
  return fmt.Errorf("failed to unload service: %w", err)
 }

 if err := os.Remove(plistPath); err != nil {
  return fmt.Errorf("failed to remove plist file: %w", err)
 }
 return nil
}

func (s *darwinService) Status() (bool, error) {
 err := exec.Command("launchctl", "list", ServiceName).Run()
 return err == nil, nil
}

func (s *darwinService) Start() error {
 if err := exec.Command("launchctl", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *darwinService) Stop() error {
 if err := exec.Command("launchctl", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Créer et gérer votre service

Pour créer votre service pour différents systèmes d'exploitation, utilisez les variables d'environnement GOOS et GOARCH. Par exemple, pour créer pour Windows :

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
)

type linuxService struct{}

const systemdServiceTemplate = `[Unit]
Description=%s

[Service]
ExecStart=%s -run
Restart=always
User=root
WorkingDirectory=%s

[Install]
WantedBy=multi-user.target
`

func (s *linuxService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 servicePath := filepath.Join("/etc/systemd/system", ServiceName+".service")
 content := fmt.Sprintf(systemdServiceTemplate, ServiceDesc, installedBinary, installDir)

 if err := os.WriteFile(servicePath, []byte(content), 0644); err != nil {
  return fmt.Errorf("failed to write service file: %w", err)
 }

 commands := [][]string{
  {"systemctl", "daemon-reload"},
  {"systemctl", "enable", ServiceName},
  {"systemctl", "start", ServiceName},
 }

 for _, args := range commands {
  if err := exec.Command(args[0], args[1:]...).Run(); err != nil {
   return fmt.Errorf("failed to execute %s: %w", args[0], err)
  }
 }
 return nil
}

func (s *linuxService) Uninstall() error {
 _ = exec.Command("systemctl", "stop", ServiceName).Run()
 _ = exec.Command("systemctl", "disable", ServiceName).Run()

 servicePath := filepath.Join("/etc/systemd/system", ServiceName+".service")
 if err := os.Remove(servicePath); err != nil {
  return fmt.Errorf("failed to remove service file: %w", err)
 }
 return nil
}

func (s *linuxService) Status() (bool, error) {
 output, err := exec.Command("systemctl", "is-active", ServiceName).Output()
 if err != nil {
  return false, nil
 }
 return string(output) == "active\n", nil
}

func (s *linuxService) Start() error {
 if err := exec.Command("systemctl", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *linuxService) Stop() error {
 if err := exec.Command("systemctl", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Pour Linux :

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
 "strings"
)

type windowsService struct{}

func (s *windowsService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 cmd := exec.Command("sc", "create", ServiceName,
  "binPath=", fmt.Sprintf("\"%s\" -run", installedBinary),
  "DisplayName=", ServiceDisplay,
  "start=", "auto",
  "obj=", "LocalSystem")

 if err := cmd.Run(); err != nil {
  return fmt.Errorf("failed to create service: %w", err)
 }

 descCmd := exec.Command("sc", "description", ServiceName, ServiceDesc)
 if err := descCmd.Run(); err != nil {
  return fmt.Errorf("failed to set service description: %w", err)
 }

 if err := exec.Command("sc", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *windowsService) Uninstall() error {
 _ = exec.Command("sc", "stop", ServiceName).Run()
 if err := exec.Command("sc", "delete", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to delete service: %w", err)
 }

 // Clean up installation directory
 installDir := GetInstallDir()
 if err := os.RemoveAll(installDir); err != nil {
  return fmt.Errorf("failed to remove installation directory: %w", err)
 }
 return nil
}
func (s *windowsService) Status() (bool, error) {
 output, err := exec.Command("sc", "query", ServiceName).Output()
 if err != nil {
  return false, nil
 }
 return strings.Contains(string(output), "RUNNING"), nil
}

func (s *windowsService) Start() error {
 if err := exec.Command("sc", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *windowsService) Stop() error {
 if err := exec.Command("sc", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Pour macOS :

go-service/
├── Makefile                 # Build and installation automation
├── cmd/
│   └── service/
│       └── main.go          # Main entry point with CLI flags and command handling
├── internal/
│   ├── service/
│   │   └── service.go       # Core service implementation
│   └── platform/            # Platform-specific implementations
│       ├── config.go        # Configuration constants
│       ├── service.go       # Cross-platform service interface
│       ├── windows.go       # Windows-specific service management
│       ├── linux.go         # Linux-specific systemd service management
│       └── darwin.go        # macOS-specific launchd service management
└── go.mod                   # Go module definition

Gérer votre service

Une fois que vous avez créé votre service pour le système d'exploitation respectif, vous pouvez le gérer à l'aide des commandes suivantes.

Remarque : Assurez-vous d'exécuter les commandes avec les privilèges root, car ces actions nécessitent des autorisations élevées sur toutes les plates-formes.

  • Installer le service : utilisez l'indicateur --install pour installer le service.
go mod init go-service
  • Vérifier l'état : Pour vérifier si le service est en cours d'exécution, utilisez :
package main

const (
 ServiceName    = "go-service"                                                      //Update your service name
 ServiceDisplay = "Go Service"                                                      // Update your display name
 ServiceDesc    = "A service that appends 'Hello World' to a file every 5 minutes." // Update your Service Description
 LogFileName    = "go-service.log"                                              // Update your Log file name
)

func GetInstallDir() string {
 switch runtime.GOOS {
 case "darwin":
  return "/usr/local/opt/go-service"
 case "linux":
  return "/opt/go-service"
 case "windows":
  return filepath.Join(os.Getenv("ProgramData"), ServiceName)
 default:
  return ""
 }
}

func copyFile(src, dst string) error {
 source, err := os.Open(src)
 if err != nil {
  return fmt.Errorf("failed to open source file: %w", err)
 }
 defer source.Close()

 destination, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
 if err != nil {
  return fmt.Errorf("failed to create destination file: %w", err)
 }
 defer destination.Close()

 _, err = io.Copy(destination, source)
 return err
}
  • Désinstaller le service : Si vous devez supprimer le service, utilisez l'indicateur --uninstall :
type Service struct {
 logFile string
 stop    chan struct{}
 wg      sync.WaitGroup
 started bool
 mu      sync.Mutex
}

Création et gestion de votre service à l'aide de TaskFile (facultatif)

Bien que vous puissiez utiliser les commandes et les indicateurs Go pour créer et gérer le service, je vous recommande fortement d'utiliser TaskFile. Il automatise ces processus et fournit :

  • Commandes cohérentes sur toutes les plateformes

  • Configuration simple basée sur YAML

  • Gestion intégrée des dépendances

Configuration de la tâche

Tout d'abord, vérifiez si Task est installé :

func New() (*Service, error) {
 installDir := platform.GetInstallDir()
 if installDir == "" {
  return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
 }

 logFile := filepath.Join(installDir, "logs", platform.LogFileName)

 return &Service{
  logFile: logFile,
  stop:    make(chan struct{}),
 }, nil
}

S'il n'est pas présent, installez en utilisant :

macOS

// Start the service
func (s *Service) Start(ctx context.Context) error {
 s.mu.Lock()
 if s.started {
  s.mu.Unlock()
  return fmt.Errorf("service already started")
 }
 s.started = true
 s.mu.Unlock()

 if err := os.MkdirAll(filepath.Dir(s.logFile), 0755); err != nil {
  return fmt.Errorf("failed to create log directory: %w", err)
 }

 s.wg.Add(1)
 go s.run(ctx)

 return nil
}

// Stop the service gracefully
func (s *Service) Stop() error {
 s.mu.Lock()
 if !s.started {
  s.mu.Unlock()
  return fmt.Errorf("service not started")
 }
 s.mu.Unlock()

 close(s.stop)
 s.wg.Wait()

 s.mu.Lock()
 s.started = false
 s.mu.Unlock()

 return nil
}

Linux

func (s *Service) run(ctx context.Context) {
 defer s.wg.Done()
 log.Printf("Service started, logging to: %s\n", s.logFile)

 ticker := time.NewTicker(5 * time.Minute)
 defer ticker.Stop()

 if err := s.writeLog(); err != nil {
  log.Printf("Error writing initial log: %v\n", err)
 }

 for {
  select {
  case <-ctx.Done():
   log.Println("Service stopping due to context cancellation")
   return
  case <-s.stop:
   log.Println("Service stopping due to stop signal")
   return
  case <-ticker.C:
   if err := s.writeLog(); err != nil {
    log.Printf("Error writing log: %v\n", err)
   }
  }
 }
}

Windows

func (s *Service) writeLog() error {
 f, err := os.OpenFile(s.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
 if err != nil {
  return fmt.Errorf("failed to open log file: %w", err)
 }
 defer f.Close()

 _, err = f.WriteString(fmt.Sprintf("[%s] Hello World\n", time.Now().Format(time.RFC3339)))
 if err != nil {
  return fmt.Errorf("failed to write to log file: %w", err)
 }
 return nil
}

Configuration des tâches

Créez un Taskfile.yml à la racine de votre projet :

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
)

type darwinService struct{}

const plistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>%s</string>
    <key>ProgramArguments</key>
    <array>
        <string>%s</string>
        <string>-run</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>WorkingDirectory</key>
    <string>%s</string>
</dict>
</plist>`

func (s *darwinService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 // Copy binary to installation directory
 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 plistPath := filepath.Join("/Library/LaunchDaemons", ServiceName+".plist")
 content := fmt.Sprintf(plistTemplate, ServiceName, installedBinary, installDir)

 if err := os.WriteFile(plistPath, []byte(content), 0644); err != nil {
  return fmt.Errorf("failed to write plist file: %w", err)
 }

 if err := exec.Command("launchctl", "load", plistPath).Run(); err != nil {
  return fmt.Errorf("failed to load service: %w", err)
 }
 return nil
}

func (s *darwinService) Uninstall() error {
 plistPath := filepath.Join("/Library/LaunchDaemons", ServiceName+".plist")

 if err := exec.Command("launchctl", "unload", plistPath).Run(); err != nil {
  return fmt.Errorf("failed to unload service: %w", err)
 }

 if err := os.Remove(plistPath); err != nil {
  return fmt.Errorf("failed to remove plist file: %w", err)
 }
 return nil
}

func (s *darwinService) Status() (bool, error) {
 err := exec.Command("launchctl", "list", ServiceName).Run()
 return err == nil, nil
}

func (s *darwinService) Start() error {
 if err := exec.Command("launchctl", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *darwinService) Stop() error {
 if err := exec.Command("launchctl", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Utiliser les commandes de tâches

Gestion des services (nécessite les privilèges root/administrateur) :

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
)

type linuxService struct{}

const systemdServiceTemplate = `[Unit]
Description=%s

[Service]
ExecStart=%s -run
Restart=always
User=root
WorkingDirectory=%s

[Install]
WantedBy=multi-user.target
`

func (s *linuxService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 servicePath := filepath.Join("/etc/systemd/system", ServiceName+".service")
 content := fmt.Sprintf(systemdServiceTemplate, ServiceDesc, installedBinary, installDir)

 if err := os.WriteFile(servicePath, []byte(content), 0644); err != nil {
  return fmt.Errorf("failed to write service file: %w", err)
 }

 commands := [][]string{
  {"systemctl", "daemon-reload"},
  {"systemctl", "enable", ServiceName},
  {"systemctl", "start", ServiceName},
 }

 for _, args := range commands {
  if err := exec.Command(args[0], args[1:]...).Run(); err != nil {
   return fmt.Errorf("failed to execute %s: %w", args[0], err)
  }
 }
 return nil
}

func (s *linuxService) Uninstall() error {
 _ = exec.Command("systemctl", "stop", ServiceName).Run()
 _ = exec.Command("systemctl", "disable", ServiceName).Run()

 servicePath := filepath.Join("/etc/systemd/system", ServiceName+".service")
 if err := os.Remove(servicePath); err != nil {
  return fmt.Errorf("failed to remove service file: %w", err)
 }
 return nil
}

func (s *linuxService) Status() (bool, error) {
 output, err := exec.Command("systemctl", "is-active", ServiceName).Output()
 if err != nil {
  return false, nil
 }
 return string(output) == "active\n", nil
}

func (s *linuxService) Start() error {
 if err := exec.Command("systemctl", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *linuxService) Stop() error {
 if err := exec.Command("systemctl", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Construisez pour votre plateforme :

package platform

import (
 "fmt"
 "os"
 "os/exec"
 "path/filepath"
 "strings"
)

type windowsService struct{}

func (s *windowsService) Install(execPath string) error {
 installDir := GetInstallDir()
 if err := os.MkdirAll(installDir, 0755); err != nil {
  return fmt.Errorf("failed to create installation directory: %w", err)
 }

 installedBinary := filepath.Join(installDir, "bin", filepath.Base(execPath))
 if err := os.MkdirAll(filepath.Dir(installedBinary), 0755); err != nil {
  return fmt.Errorf("failed to create bin directory: %w", err)
 }

 if err := copyFile(execPath, installedBinary); err != nil {
  return fmt.Errorf("failed to copy binary: %w", err)
 }

 cmd := exec.Command("sc", "create", ServiceName,
  "binPath=", fmt.Sprintf("\"%s\" -run", installedBinary),
  "DisplayName=", ServiceDisplay,
  "start=", "auto",
  "obj=", "LocalSystem")

 if err := cmd.Run(); err != nil {
  return fmt.Errorf("failed to create service: %w", err)
 }

 descCmd := exec.Command("sc", "description", ServiceName, ServiceDesc)
 if err := descCmd.Run(); err != nil {
  return fmt.Errorf("failed to set service description: %w", err)
 }

 if err := exec.Command("sc", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *windowsService) Uninstall() error {
 _ = exec.Command("sc", "stop", ServiceName).Run()
 if err := exec.Command("sc", "delete", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to delete service: %w", err)
 }

 // Clean up installation directory
 installDir := GetInstallDir()
 if err := os.RemoveAll(installDir); err != nil {
  return fmt.Errorf("failed to remove installation directory: %w", err)
 }
 return nil
}
func (s *windowsService) Status() (bool, error) {
 output, err := exec.Command("sc", "query", ServiceName).Output()
 if err != nil {
  return false, nil
 }
 return strings.Contains(string(output), "RUNNING"), nil
}

func (s *windowsService) Start() error {
 if err := exec.Command("sc", "start", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }
 return nil
}

func (s *windowsService) Stop() error {
 if err := exec.Command("sc", "stop", ServiceName).Run(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 return nil
}

Builds multiplateformes :

package main

import (
 "context"
 "flag"
 "fmt"
 "log"
 "os"
 "os/signal"
 "syscall"
 "time"

 "go-service/internal/platform"
 "go-service/internal/service"
)

func main() {
 log.SetFlags(log.LstdFlags | log.Lmicroseconds)

 install := flag.Bool("install", false, "Install the service")
 uninstall := flag.Bool("uninstall", false, "Uninstall the service")
 status := flag.Bool("status", false, "Check service status")
 start := flag.Bool("start", false, "Start the service")
 stop := flag.Bool("stop", false, "Stop the service")
 runWorker := flag.Bool("run", false, "Run the service worker")
 flag.Parse()

 if err := handleCommand(*install, *uninstall, *status, *start, *stop, *runWorker); err != nil {
  log.Fatal(err)
 }
}

func handleCommand(install, uninstall, status, start, stop, runWorker bool) error {
 platformSvc, err := platform.NewService()
 if err != nil {
  return err
 }

 execPath, err := os.Executable()
 if err != nil {
  return fmt.Errorf("failed to get executable path: %w", err)
 }

 switch {
 case install:
  return platformSvc.Install(execPath)
 case uninstall:
  return platformSvc.Uninstall()
 case status:
  running, err := platformSvc.Status()
  if err != nil {
   return err
  }
  fmt.Printf("Service is %s\n", map[bool]string{true: "running", false: "stopped"}[running])
  return nil
 case start:
  return platformSvc.Start()
 case stop:
  return platformSvc.Stop()
 case runWorker:
  return runService()
 default:
  return fmt.Errorf("no command specified")
 }
}

func runService() error {
 svc, err := service.New()
 if err != nil {
  return fmt.Errorf("failed to create service: %w", err)
 }

 ctx, cancel := context.WithCancel(context.Background())
 defer cancel()

 sigChan := make(chan os.Signal, 1)
 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

 log.Println("Starting service...")
 if err := svc.Start(ctx); err != nil {
  return fmt.Errorf("failed to start service: %w", err)
 }

 log.Println("Service started, waiting for shutdown signal...")
 <-sigChan
 log.Println("Shutdown signal received, stopping service...")

 if err := svc.Stop(); err != nil {
  return fmt.Errorf("failed to stop service: %w", err)
 }
 log.Println("Service stopped successfully")
 return nil
}

Liste de toutes les tâches disponibles :

GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o go-service.exe ./cmd/service

Conclusion

En suivant cette approche structurée, vous pouvez créer un service propre et modulaire dans Go qui fonctionne de manière transparente sur plusieurs plates-formes. Les spécificités de chaque plateforme sont isolées dans leurs fichiers respectifs, et le fichier main.go reste simple et facile à maintenir.

Pour le code complet, veuillez vous référer à mon référentiel de service Go sur GitHub.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn