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:
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:
and it does its "work" of writing log messages:
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:
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.