Levin's blog

Writing a Windows Service in Go

A Windows service is a program for Windows that runs in the background, without any user interface, like a demon process on Unix. If you're on Windows you can see which services are running with the Services tool:

Services tool screenshot

This article shows how to write a Windows service in Go by going through the source code for a service that simply prints a log message every 30 seconds. You can see the full sample code here: main.go

Getting started

Windows services have to use a specific API -- you can't just take any command-line program and set it up as a service. Fortunately, the Go package golang.org/x/sys/windows/svc has everything we need with a nice Go interface.

To write our sample service, we start by importing the library:

import "golang.org/x/sys/windows/svc"

Then we'll define a couple of constants for errors that might occur:

const (
    exitCodeDirNotFound     = 1
    exitCodeErrorOpeningLog = 2
)

In main, all we need to do is call the svc.Run function with a name for the service and a "handler" that has the actual implementation:

func main() {
    svc.Run("sample-service", new(handler))
}

The service handler

The handler needs to implement the one-method handler interface. We can define it as an empty struct with the required method:

type handler struct{}
func (h *handler) Execute(args []string, r <-chan svc.ChangeRequest,
    s chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {

As an aside, I'm not sure why they made this an interface rather than having the caller just pass a function. Then we wouldn't need that odd-looking handler type. Anyways, let's take a look at the method parameters: args is the equivalent to os.Args for comment-line programms, with the service name and any arguments passed to the service. The r and s channels are used to communicate with the service control manager (the part of Windows that, well, controls services) about the current status of the service.

The first thing we do is set the state to "start pending:"

s <- svc.Status{State: svc.StartPending}

Next, we'll want some way to log messages about the service. The Windows Event Log is one option, but for this tutorial we'll just use the standard library log package and write to a file in the directory where the service executable is:

executable, err := os.Executable()
if err != nil {
    return true, exitCodeDirNotFound
}
dir := filepath.Dir(executable)
logPath := filepath.Join(dir, "service.log")
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    return true, exitCodeErrorOpeningLog
}
defer logFile.Close()
logger := log.New(logFile, "", log.LstdFlags)

Most of this is just calling standard library functions, except for those return statements:

return true, exitCodeErrorOpeningLog

The execute method is a bit like the main method for a service, so once it returns, the service terminates. Like main, it returns an exit code that's supposed to be zero for success and a non-zero value to signal a specific error. That's the second return value; the first one indicates if the exit code is a standard Windows error code or something specific to this service. Here we just have a couple of error codes defined as constants, so the first return value will be true.

Getting the logger ready was the only setup code we needed for this service, so next we'll send another message to the service control manager telling it two things: the current state of the service, and a list of commands the service will accept on the r channel.

s <- svc.Status{
    State:   svc.Running,
    Accepts: svc.AcceptStop | svc.AcceptShutdown,
}

When we first sent a message to s, at the very start of the method, we didn't set the Accepts field because the service wasn't ready to accept any commands.

To keep the Execute method short, I've moved the "main loop" of the service to a separate function:

    loop(r, s, logger)

Once loop returns, the service prints one last log message and returns 0 to indicate success:

logger.Print("service: shutting down")
return true, 0

A main loop for the service

The loop function will do two things: it prints a log message every 30 seconds, so we can see the service at work, and it handles messages from the service control manager. It'll do those things in a loop until the service is ready to shut down.

We'll use time.Tick for the "every 30 seconds" bit, and a select statement to determine what to do next:

func loop(r <-chan svc.ChangeRequest, s chan<- svc.Status, logger *log.Logger) {
    tick := time.Tick(30 * time.Second)
    logger.Print("service: up and running")
    for {
        select {
        case <-tick:
            work(logger)
        case c := <-r:
            switch c.Cmd {
            case svc.Interrogate:
                s <- c.CurrentStatus
            case svc.Stop:
                logger.Print("service: got stop signal, exiting")
                return
            case svc.Shutdown:
                logger.Print("service: got shutdown signal, exiting")
                return
            }
        }
    }
}

One thing you might notice is we're handling the Interrogate command even though we didn't include it in the Accepts field before. The documentation says "Interrogate is always accepted," although it doesn't say why and when I tried using the service without the Interrogate case I didn't see any problems. But it's probably safer to have it.

Finally, the work function prints that log message:

func work(logger *log.Logger) {
    logger.Print("service says: hello")
}

Running the service

We build the service in the usual way with go build:

GOOS=windows GOARCH=amd64 go build -o service.exe cmd/service/main.go

If you're building it on Windows you won't need to set GOOS and GOARCH -- I'm building it on an ARM MacBook so I need both. I assume you can also build the service for Windows on ARM with GOARCH=arm, but I haven't tested that. (Maybe I should sign up for one of the new Azure ARM-based VMs and try it out...)

Once we have the service.exe ready, the easiest way to install it is on the command line. Open the Windows Command Prompt as Administrator and run

sc create sample-service start=auto binPath=C:\sample-service\service.exe

With start=auto, the service will be started when Windows starts, but it's not running yet. We can either start it in the Services GUI or on the command-line with

sc start sample-service

As expected, the service now shows up in the Services tool:

Services tool screenshot with sample-service
running

and it does its "work" of writing log messages:

Notepad++ showing log messages from
sample-service

Removing the service

To remove the service, we first stop it:

sc stop sample-service

and check the service log to verify that it handled the stop signal correctly:

Notepad++ showing "shutting down" log
message

Then we can delete the service:

sc delete sample-service

Up next

Wouldn't it be nice if we had an installer for the service so we won't have to remember that sc command-line anymore? Part 2 of this series is going to show how to write a simple Go program that can install or uninstall a Windows service.