search
HomeBackend DevelopmentGolangBuilding Cross-Platform System Services in Go: A Step-by-Step Guide

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

What Are System Services?

System Services are lightweight programs that operate in the background without a graphical user interface . They start automatically during system boot and run independently.Their lifecycle, which includes operations like start, stop, and restart are managed by Service Control Manager on Windows, systemd on Linux(in most of the destro) and launchd on macOS.

Unlike standard applications, services are designed for continuous operation and are essential for tasks such as monitoring, logging, and other background processes. On linux, these services are generally referred to as daemons, while on macOS, they known as Launch Agents or Daemons.

Why Go for Building System Services*?*

Creating cross-platform system services demands a language that balances efficiency, usability, and reliability. Go excels in this regard for several reasons:

  • Concurrency and Performance: Go’s goroutines make it easy to run multiple tasks at once, improving efficiency and speed on different platforms. Coupled with a robust standard library, this minimizes external dependencies and enhances cross-platform compatibility.

  • Memory Management and Stability: Go’s garbage collection prevents memory leaks, keeping systems stable. Its clear error handling also makes it easier to debug complex services.

  • Simplicity and Maintainability: Go’s clear syntax simplifies writing and maintaining services . Its capability to produce statically linked binaries results in single executable files that include all necessary dependencies, eliminating the need for separate runtime environments.

  • Cross-Compilation and Flexibility: Go’s support for cross-compilation allows building executables for various operating systems from a single codebase. With CGO, Go can interact with low-level system APIs, such as Win32 and Objective-C, providing developers the flexibility to leverage native features.

Writing Services in Go

This code walk through assumes that GO is installed on your machine and you have a basic knowledge on GO’s syntax, if not i would highly recommend you to Take A Tour .

Project Overview

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

Step 1: Define Configurations

Initialize the Go module:

go mod init go-service

Define configuration constants in config.go within the internal/platform directory. This file centralizes all configurable values, making it easy to adjust settings.

File: 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
}

Key features:

  • Service Constants which will be used during Platform-Specific Service Configurations.

  • GetInstallDir() provides appropriate service installation and log file paths for each OS.

  • copyFile() used during service installation to copy the executable to our specific path provided by GetInstallDir().

Step 2: Defining Core Service Logic

In the internal/service, implement the core functionality for your service.The core service implementation handles the main functionality of our service.

In this example, the service appends "Hello World" to a file in the user's home directory every 5 minutes.

File: internal/service/service.go

Service Structure

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

Service Creation

go mod init go-service

Service Lifecycle

The service implements Start and Stop methods for lifecycle management:

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
}

Main Service Loop

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

The run method handles the core service logic:

Key features:

  • Interval-based execution using ticker

  • Context cancellation support

  • Graceful shutdown handling

  • Error logging

Log Writing

The service appends “Hello World” with a timestamp every 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
}

Step 3: Creating Platform-Specific Service Configurations

The internal/platform directory contains platform-specific configurations to install, uninstall, and manage the service.

macOS (darwin.go)

In darwin.go, define macOS-specific logic for creating a .plist file, which handles service installation and uninstallation using launchctl.

File: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)

On Linux, we use systemd to manage the service. Define a .service file and related methods.

File: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 



<h4>
  
  
  Windows (windows.go)
</h4>

<p>For Windows, use the sc command to install and uninstall the service.</p>

<p><strong>File:</strong>internal/platform/windows.go<br>
</p>

<pre class="brush:php;toolbar:false">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
}

Step 4: Main File Setup (main.go)

Finally, configure main.go in cmd/service/main.go to handle installation, uninstallation, and starting the service.

File: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"?>

<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></true>
    <key>KeepAlive</key>
    <true></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
}

Building and Managing Your Service

To build your service for different operating systems, use the GOOS and GOARCH environment variables. For example, to build for 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
}

For 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
}

For 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

Managing Your Service

Once you have built your service for the respective operating system, you can manage it using the following commands.

Note: Ensure to execute the commands with root privileges, as these actions require elevated permissions on all platforms.

  • Install the Service: Use the --install flag to install the service.
go mod init go-service
  • Check the Status: To check if the service is running, use:
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
}
  • Uninstall the Service: If you need to remove the service, use the --uninstall flag:
type Service struct {
 logFile string
 stop    chan struct{}
 wg      sync.WaitGroup
 started bool
 mu      sync.Mutex
}

Building and Managing Your Service using TaskFile (Optional)

Although you can use Go commands and flags to build and manage the service, I highly recommend using TaskFile. It automates these processes and provides:

  • Consistent commands across all platforms

  • Simple YAML-based configuration

  • Built-in dependency management

Setting Up Task

First, check if Task is installed:

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
}

If not present, install using:

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 



<p>Windows<br>
</p>

<pre class="brush:php;toolbar:false">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
}

Task Configuration

Create a Taskfile.yml in your project root:

package platform

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

type darwinService struct{}

const plistTemplate = `<?xml version="1.0" encoding="UTF-8"?>

<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></true>
    <key>KeepAlive</key>
    <true></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
}

Using Task Commands

Service management (requires root/administrator privileges):

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
}

Build for your platform:

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
}

Cross-platform builds:

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...")
 



<p>List all available tasks:<br>
</p>

<pre class="brush:php;toolbar:false">GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o go-service.exe ./cmd/service

Conclusion

By following this structured approach, you can create a clean and modular service in Go that works seamlessly across multiple platforms. Each platform’s specifics are isolated in their respective files, and the main.go file remains straightforward and easy to maintain.

For the complete code, please refer to my Go service repository on GitHub.

The above is the detailed content of Building Cross-Platform System Services in Go: A Step-by-Step Guide. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
How do you use the "strings" package to manipulate strings in Go?How do you use the "strings" package to manipulate strings in Go?Apr 30, 2025 pm 02:34 PM

The article discusses using Go's "strings" package for string manipulation, detailing common functions and best practices to enhance efficiency and handle Unicode effectively.

How do you use the "crypto" package to perform cryptographic operations in Go?How do you use the "crypto" package to perform cryptographic operations in Go?Apr 30, 2025 pm 02:33 PM

The article details using Go's "crypto" package for cryptographic operations, discussing key generation, management, and best practices for secure implementation.Character count: 159

How do you use the "time" package to handle dates and times in Go?How do you use the "time" package to handle dates and times in Go?Apr 30, 2025 pm 02:32 PM

The article details the use of Go's "time" package for handling dates, times, and time zones, including getting current time, creating specific times, parsing strings, and measuring elapsed time.

How do you use the "reflect" package to inspect the type and value of a variable in Go?How do you use the "reflect" package to inspect the type and value of a variable in Go?Apr 30, 2025 pm 02:29 PM

Article discusses using Go's "reflect" package for variable inspection and modification, highlighting methods and performance considerations.

How do you use the "sync/atomic" package to perform atomic operations in Go?How do you use the "sync/atomic" package to perform atomic operations in Go?Apr 30, 2025 pm 02:26 PM

The article discusses using Go's "sync/atomic" package for atomic operations in concurrent programming, detailing its benefits like preventing race conditions and improving performance.

What is the syntax for creating and using a type conversion in Go?What is the syntax for creating and using a type conversion in Go?Apr 30, 2025 pm 02:25 PM

The article discusses type conversions in Go, including syntax, safe conversion practices, common pitfalls, and learning resources. It emphasizes explicit type conversion and error handling.[159 characters]

What is the syntax for creating and using a type assertion in Go?What is the syntax for creating and using a type assertion in Go?Apr 30, 2025 pm 02:24 PM

The article discusses type assertions in Go, focusing on syntax, potential errors like panics and incorrect types, safe handling methods, and performance implications.

How do you use the "select" statement in Go?How do you use the "select" statement in Go?Apr 30, 2025 pm 02:23 PM

The article explains the use of the "select" statement in Go for handling multiple channel operations, its differences from the "switch" statement, and common use cases like handling multiple channels, implementing timeouts, non-b

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

MantisBT

MantisBT

Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.

EditPlus Chinese cracked version

EditPlus Chinese cracked version

Small size, syntax highlighting, does not support code prompt function

SublimeText3 English version

SublimeText3 English version

Recommended: Win version, supports code prompts!

SublimeText3 Linux new version

SublimeText3 Linux new version

SublimeText3 Linux latest version

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor