Table Of Contents
- Introduction
- What exactly is a windows "service"?
- Why Golang?
- Writing a windows service in Go
- Installing and Starting the Service
- Conclusion
- Complete Code
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
- 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
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 containsmain
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 fromgolang.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)
}
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{}
- After creating the type
myService
, add the above mentionedExecute
as a method to it so that it implementsHandler
interface.
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
// to be filled
}
- 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
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)
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}
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)
}
}
}
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,
-
svc.Interrogate
- Signal requested by SCM on a timely fashion to check the current status of the service. -
svc.Stop
andsvc.Shutdown
- Signal sent by SCM when our service needs to be stopped or Shut Down. -
svc.Pause
- Signal sent by SCM to pause the service execution without shutting it down. -
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
- 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.")
}
}
}
- Finally we can call the
runService
function in ourmain
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
}
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"
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>
To start our service, run the following command,
PS C:\> sc.exe start MyService
To delete our service, run the following command,
PS C:\> sc.exe delete MyService
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)
}
Top comments (6)
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.
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 :)
excellent work
Thanks mate. Love your work on golang too ❤️
Very informative.
Excellent work thanks for sharing this. very informative :)