发布于 2026-01-06 5 阅读
0

用 Go 编写 Windows 服务

用 Go 编写 Windows 服务

目录

介绍

各位开发者好,好久没写关于 Windows 的东西了。今天我想指导大家如何用 Go 语言编写 Windows 服务应用程序。没错,就是 Go 语言。在本教程博客中,我们将介绍一些关于 Windows 服务应用程序的基础知识,之后我会通过一个简单的代码演示,带领大家编写一个将信息记录到文件的 Windows 服务。废话不多说,让我们开始吧!

Windows“服务”究竟是什么?

Windows 服务应用程序(也称为 Windows 服务)是运行在后台的小型应用程序。与普通的 Windows 应用程序不同,它们没有图形用户界面 (GUI) 或任何形式的用户界面。这些服务应用程序在计算机启动时开始运行,并且无论使用哪个用户帐户运行,它们都会运行。它们的生命周期(启动、停止、暂停、继续等)由名为服务控制管理器 (SCM) 的程序控制。

因此,由此我们可以理解,我们应该以这样的方式编写我们的 Windows 服务:SCM 应该与我们的 Windows 服务进行交互并管理其生命周期。

为什么选择 Golang?

在编写 Windows 服务时,您可能会考虑使用 Go 语言。

并发性

Go 的并发模型能够实现更快、更高效的资源处理。它goroutines允许我们编写可以执行多任务而不会出现阻塞或死锁的应用程序。

简单

传统上,Windows 服务使用 C++ 或 C(有时也用 C#)编写,这不仅导致代码复杂,而且开发者体验 (DX) 很差。Go 语言实现的 Windows 服务简洁明了,每一行代码都清晰易懂

静态二进制文件

你可能会问:“为什么不使用像 Python 这样更简单的语言呢?”原因在于 Python 的解释执行特性。Go 编译后会生成静态链接的单文件二进制文件,这对于 Windows 服务的高效运行至关重要。Go 二进制文件不需要任何运行时环境/解释器。Go 代码还可以进行交叉编译。

低层通道

尽管 Go 是一种垃圾回收语言,但它仍然提供了强大的底层交互支持。我们可以轻松地在 Go 中调用 Win32 API 和通用系统调用。

好了,信息足够了。开始写代码吧……

用 Go 编写 Windows 服务

本代码讲解假设您已具备 Go 语言的基本语法知识。如果您还不了解,可以参考《 Go 语言入门指南》(A Tour of Go)来学习。

  • 首先,我们来给项目命名。我将其命名为cosmic/my_service。创建一个 go.mod 文件,
PS C:\> go mod init cosmic/my_service
Enter fullscreen mode Exit fullscreen mode
  • 现在我们需要安装golang.org/x/sys一个软件包。该软件包为 Windows 操作系统相关的应用程序提供 Go 语言支持。
PS C:\> go get golang.org/x/sys
Enter fullscreen mode Exit fullscreen mode

注意:此软件包还包含对基于 UNIX 的操作系统(如 Mac OS 和 Linux)的操作系统级 Go 语言支持。

  • 创建一个main.go文件。main.go 文件包含main一个函数,该函数作为我们 Go 应用程序/服务的入口点。

  • 要创建服务实例,我们需要编写一个名为Service Context的东西,它实现了Handler接口golang.org/x/sys/windows/svc

所以,接口定义看起来大概是这样的

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

Execute该函数将在服务启动时由包代码调用,服务将在函数Execute完成后退出。

我们从只读通道读取服务变更请求r并采取相应措施。我们还应该通过向只发通道发送信号来保持服务更新s。我们可以向args参数传递可选参数。

退出时,exitCode如果执行成功,我们可以返回值为 0。我们也可以用svcSpecificEC它来实现这一点。

  • 现在,创建一个名为myService“服务上下文”的类型。
type myService struct{}
Enter fullscreen mode Exit fullscreen mode
  • 创建类型后myService,将上述内容Execute作为方法添加到该类型中,使其实现Handler接口。
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
  • 既然我们已经成功实现了Handler接口,现在就可以开始编写实际逻辑了。

创建一个常量,其中包含我们的服务可以从 SCM 接收的信号。

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

我们的主要目标是每30秒记录一些数据。因此,我们需要为此定义一个线程安全的定时器。

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

初始化工作已经完成。现在是时候START向SCM发送信号了。我们这就去做。

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

现在我们要编写一个循环,作为应用程序的主循环STOP。在循环中处理事件会使应用程序永不结束,只有当 SCM 发送信号时,我们才能跳出循环SHUTDOWN

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

这里我们使用了一个select语句来接收来自通道的信号。在第一种情况下,我们处理定时器的tick信号。正如我们之前声明的,这种情况下每 30 秒接收一次信号。在这种情况下,我们会记录字符串“Tick Handled...!”

其次,我们通过只读r通道处理来自 SCM 的信号。因此,我们将信号值赋给r一个变量c,并使用switch语句来处理我们服务的所有生命周期事件/信号。我们可以在下面看到每个生命周期的详细说明:

  1. svc.Interrogate- SCM及时请求信号以检查服务的当前状态。
  2. svc.Stop以及svc.Shutdown- 当我们的服务需要停止或关闭时,SCM 发送的信号。
  3. svc.Pause- SCM 发送信号暂停服务执行,但不关闭服务。
  4. svc.Continue- SCM 发送信号以恢复服务暂停的执行状态。

因此,当收到svc.Stop任一信号时svc.Shutdown,我们就断开循环。需要注意的是,我们需要STOP向服务控制模块 (SCM) 发送信号,告知 SCM 我们的服务即将停止。

status <- svc.Status{State: svc.StopPending}
return false, 1
Enter fullscreen mode Exit fullscreen mode
  • 现在我们编写一个名为 `on` 的函数,runService该函数允许我们的服务以调试模式或服务控制模式运行。

注意:在服务控制模式下调试 Windows 服务应用程序非常困难。因此,我们正在编写一个额外的调试模式。

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
  • 最后,我们可以runService在函数中调用该函数main
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

注意:我们会将日志记录到日志文件中。在高级场景下,我们会将日志记录到 Windows 事件日志记录器中。(呼,这听起来像个绕口令😂)

  • 现在运行命令go build生成二进制文件“.exe”。此外,我们还可以使用以下命令优化并减小二进制文件的大小:
PS C:\> go build -ldflags "-s -w"
Enter fullscreen mode Exit fullscreen mode

安装并启动服务

我们使用一个名为“内置工具”的工具来安装、删除、启动和停止我们的服务。sc.exe

要安装我们的服务,请以管理员身份在 PowerShell 中运行以下命令

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

要启动我们的服务,请运行以下命令:

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

要删除我们的服务,请运行以下命令:

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

您可以探索更多命令,只需输入sc.exe不带任何参数的命令即可查看可用命令。

结论

如我们所见,用 Go 语言实现 Windows 服务非常简单,只需极少的编写工作。您可以编写自己的 Windows 服务,使其充当 Web 服务器等等。感谢阅读,别忘了点个赞❤️。

完整代码

以下是完整的代码,供您参考。

// 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
文章来源:https://dev.to/cosmic_predator/writing-a-windows-service-in-go-1d1m