DEV Community

Cover image for Writing a Windows Service in Go
Barani Kumar S
Barani Kumar S

Posted on • Edited on

Writing a Windows Service in Go

Table Of Contents

Introduction

Hello Devs, It's been a while since I wrote something windows-ish. So, today I want to guide you guys on how to write a Windows Service Application in Go. Yes, you heard it right, it's go language. In this tutorial blog, we'll cover some basic stuffs about Windows Service Applications and at the later part, I'll guide you through a simple code walk through where we write code for a Windows service which logs some information to a file. Without further ado, Let's get started...!

What exactly is a windows "service"?

A Windows Service Application a.k.a. Windows Services are tiny applications that runs in background. Unlike normal windows applications, they don't have a GUI or any forms of user interface. These service applications starts to run when the computer boots up. It runs irrespective of which user account it's running in. It's lifecycle (start, stop, pause, continue etc.,) are controlled by a program called Service Control Manager (SCM).

So, from this we can understand that we should write our Windows Service in such a way that the SCM should interact with our Windows Service and manage it's lifecycle.

Why Golang?

There are several factors where you may consider Go for writing Windows Services.

Concurrency

Go's concurrency model allows for faster and resource efficient processing. Go's goroutines allows us to write applications which can do multi-tasking without any blocking or deadlocking.

Simplicity

Traditionally, Windows Services are written using either C++ or C (sometimes C#) which not only results in a complex code, but also poor DX (Developer Experience). Go's implementation of Windows Services is straightforward and every line of code makes sense.

Static Binaries

You may ask, "Why not use even more simple language like python?". The reason is due to the interpreted nature of Python. Go compiles to a statically linked single file binary which is essential for a Windows Service to function efficiently. Go binaries doesn't require any runtime / interpreter. Go code can also be cross compiled.

Low Level Access

Though being a Garbage Collected language, Go provides solid support to interact with low level elements. We can easily invoke win32 APIs and general syscalls in go.

Alright, enough information. Let's code...

Writing a windows service in Go

This code walk-through assumes that you have a basic knowledge on Go syntax. If not, A Tour of Go would be a nice place to learn it.

  • Firstly, let's name our project. I'll name mines as cosmic/my_service. Create a go.mod file,
PS C:\> go mod init cosmic/my_service
Enter fullscreen mode Exit fullscreen mode
  • Now we need to install golang.org/x/sys package. This package provides go language support for Windows OS related applications.
PS C:\> go get golang.org/x/sys
Enter fullscreen mode Exit fullscreen mode

Note: This package also contains OS level go language support for UNIX based OS'es like Mac OS and Linux.

  • Create a main.go file. The main.go file contains main function, which acts as a entry-point for our Go application/service.

  • For creating a service instance, we need to write something called Service Context, which implements Handler interface from golang.org/x/sys/windows/svc.

So, the interface definition looks something like this

type Handler interface {
    Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32)
}
Enter fullscreen mode Exit fullscreen mode

Execute function will be called by the package code at the start of the service, and the service will exit once the Execute completes.

We read service change requests from receive-only channel r and act accordingly. We should also keep our service updated with sending signals to send-only channel s. We can pass optional arguments to args parameter.

On exiting, we can return with the exitCode being 0 on a successful execution. We can also use svcSpecificEC for that.

  • Now, create a type named myService which will act as our Service Context.
type myService struct{}
Enter fullscreen mode Exit fullscreen mode
  • After creating the type myService, add the above mentioned Execute as a method to it so that it implements Handler interface.
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
    // to be filled
}
Enter fullscreen mode Exit fullscreen mode
  • Now that we have successfully implemented the Handler interface, we can now start writing the actual logic.

Create a constant, with the signals that our service can accept from SCM.

const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
Enter fullscreen mode Exit fullscreen mode

Our main goal is the log some data every 30 seconds. So we need to define a thread safe timer for that.

tick := time.Tick(30 * time.Second)
Enter fullscreen mode Exit fullscreen mode

So, we have done all the initialization stuffs. It's time to send START signal to the SCM. we're going to do exactly that,

status <- svc.Status{State: svc.StartPending}
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
Enter fullscreen mode Exit fullscreen mode

Now we're going to write a loop which acts as a mainloop for our application. Handling events in loop makes our application never ending and we can break the loop only when the SCM sends STOP or SHUTDOWN signal.

loop:
    for {
        select {
        case <-tick:
            log.Print("Tick Handled...!")
        case c := <-r:
            switch c.Cmd {
            case svc.Interrogate:
                status <- c.CurrentStatus
            case svc.Stop, svc.Shutdown:
                log.Print("Shutting service...!")
                break loop
            case svc.Pause:
                status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
            case svc.Continue:
                status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
            default:
                log.Printf("Unexpected service control request #%d", c)
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Here we used a select statement to receive signals from channels. In first case, we handle the Timer's tick signal. This case receives signal every 30 seconds, as we declared before. We log a string "Tick Handled...!" in this case.

Secondly, we handle the signals from SCM via the receive-only r channel. So, we assign the value of the signal from r to a variable c and using a switch statement, we can handle all the lifecycle event/signals of our service. We can see about each lifecycle below,

  1. svc.Interrogate - Signal requested by SCM on a timely fashion to check the current status of the service.
  2. svc.Stop and svc.Shutdown - Signal sent by SCM when our service needs to be stopped or Shut Down.
  3. svc.Pause - Signal sent by SCM to pause the service execution without shutting it down.
  4. svc.Continue - Signal sent by SCM to resume the paused execution state of the service.

So, when on receiving either svc.Stop or svc.Shutdown signal, we break the loop. It is to be noted that we need to send STOP signal to the SCM to let the SCM know that our service is stopping.

status <- svc.Status{State: svc.StopPending}
return false, 1
Enter fullscreen mode Exit fullscreen mode
  • Now we write a function called runService where we enable our service to run either in Debug mode or in Service Control Mode.

Note: It's super hard to debug Windows Service Applications when running on Service Control Mode. That's why we are writing an additional Debug mode.

func runService(name string, isDebug bool) {
    if isDebug {
        err := debug.Run(name, &myService{})
        if err != nil {
            log.Fatalln("Error running service in debug mode.")
        }
    } else {
        err := svc.Run(name, &myService{})
        if err != nil {
            log.Fatalln("Error running service in Service Control mode.")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Finally we can call the runService function in our main function.
func main() {

    f, err := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln(fmt.Errorf("error opening file: %v", err))
    }
    defer f.Close()

    log.SetOutput(f)
    runService("myservice", false) //change to true to run in debug mode
}
Enter fullscreen mode Exit fullscreen mode

Note: We are logging the logs to a log file. In advanced scenarios, we log our logs to Windows Event Logger. (phew, that sounds like a tongue twister 😂)

  • Now run go build to create a binary '.exe'. Optionally, we can optimize and reduce the binary file size by using the following command,
PS C:\> go build -ldflags "-s -w"
Enter fullscreen mode Exit fullscreen mode

Installing and Starting the Service

For installing, deleting, starting and stopping our service, we use an inbuilt tool called sc.exe

To install our service, run the following command in powershell as Administrator,

PS C:\> sc.exe create MyService <path to your service_app.exe>
Enter fullscreen mode Exit fullscreen mode

To start our service, run the following command,

PS C:\> sc.exe start MyService
Enter fullscreen mode Exit fullscreen mode

To delete our service, run the following command,

PS C:\> sc.exe delete MyService
Enter fullscreen mode Exit fullscreen mode

You can explore more commands, just type sc.exe without any arguments to see the available commands.

Conclusion

As we can see, implementing Windows Services in go is straightforward and requires minimal implementation. You can write your own windows services which acts as a web server and more. Thanks for reading and don't forget to drop a ❤️.

Complete Code

Here is the complete code for your reference.

// file: main.go

package main

import (
    "fmt"
    "golang.org/x/sys/windows/svc"
    "golang.org/x/sys/windows/svc/debug"
    "log"
    "os"
    "time"
)

type myService struct{}

func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {

    const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
    tick := time.Tick(5 * time.Second)

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

    status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}

loop:
    for {
        select {
        case <-tick:
            log.Print("Tick Handled...!")
        case c := <-r:
            switch c.Cmd {
            case svc.Interrogate:
                status <- c.CurrentStatus
            case svc.Stop, svc.Shutdown:
                log.Print("Shutting service...!")
                break loop
            case svc.Pause:
                status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
            case svc.Continue:
                status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
            default:
                log.Printf("Unexpected service control request #%d", c)
            }
        }
    }

    status <- svc.Status{State: svc.StopPending}
    return false, 1
}

func runService(name string, isDebug bool) {
    if isDebug {
        err := debug.Run(name, &myService{})
        if err != nil {
            log.Fatalln("Error running service in debug mode.")
        }
    } else {
        err := svc.Run(name, &myService{})
        if err != nil {
            log.Fatalln("Error running service in Service Control mode.")
        }
    }
}

func main() {

    f, err := os.OpenFile("E:/awesomeProject/debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalln(fmt.Errorf("error opening file: %v", err))
    }
    defer f.Close()

    log.SetOutput(f)
    runService("myservice", false)
}
Enter fullscreen mode Exit fullscreen mode

Top comments (8)

Collapse
 
javier_jimenezmatilla profile image
Javier Jimenez Matilla • Edited

Great explanation, thank you!. Some thoughts: It's better to use build tags and separate the debug mode code into one file and the production code into another. What do you intend to compile when the debug code is sent to production? It’s like send test code to production.

Collapse
 
cosmic_predator profile image
Barani Kumar S

Hey there,

For the sake of simplicity and beginner friendly approach, I did mentioned the debug / srv mode in the code. But yeah, when on production, we can use build tags. Thanks for the suggestion :)

Collapse
 
leoantony72 profile image
Leo Antony

excellent work

Collapse
 
cosmic_predator profile image
Barani Kumar S

Thanks mate. Love your work on golang too ❤️

Collapse
 
nirmaltb profile image
Nirmalraj TB

Very informative.

Collapse
 
vinod_kumarmani_bf446de0 profile image
Vinod Kumar Mani

Excellent work thanks for sharing this. very informative :)

Collapse
 
h3bzzz profile image
Heber Moreira

awesome article thank you so much for getting me on the right path !

Collapse
 
cosmic_predator profile image
Barani Kumar S

Glad that my article helped 😊