1

研究背景

go程序部署时,直接将编译好的文件在服务器上运行即可,一般无需安装所依赖的第三方库。

Linux下部署分为以下几种方式:

  • 使用nohup 命令

  • 使用 Supervisord管理

  • 使用systemd管理

windows下部署方式:

  • 注册成windows服务,方便进行管理

其实golang自己也可以实现以服务的形式常驻后台。

现在介绍一个包 github.com/kardianos/service ,它可以让 go 程序支持在不同系统上作为服务:注册,卸载,启动,停止。

2

安装部署

go get github.com/kardianos/service

3

使用方法

通过两个例子,我们来了解其用法。

3.1 示例程序: simple

package main

import (
 "log"

 "github.com/kardianos/service"
)

var logger service.Logger //service的日志对象

type program struct{} //定义结构体,并实现service.Interface的start和stop接口

func (p *program) Start(s service.Service) error {
 // Start should not block. Do the actual work async.
 go p.run()
 return nil
}
func (p *program) run() {
 // Do work here
}
func (p *program) Stop(s service.Service) error {
 // Stop should not block. Return with a few seconds.
 return nil
}

func main() {
 svcConfig := &service.Config{
  Name:        "GoServiceExampleSimple",
  DisplayName: "Go Service Example",
  Description: "This is an example Go service.",
 }

 prg := &program{}
 s, err := service.New(prg, svcConfig)
 if err != nil {
  log.Fatal(err)
 }
 logger, err = s.Logger(nil)
 if err != nil {
  log.Fatal(err)
 }
 err = s.Run()
 if err != nil {
  logger.Error(err)
 }
}

套路如下:

一、定义结构体program,并实现service的接口函数Start和Stop

需要实现的service接口Interface如下:

type Interface interface {
   // Start provides a place to initiate the service. The service doesn't
   // signal a completed start until after this function returns, so the
   // Start function must not take more then a few seconds at most.
   Start(s Service) error

   // Stop provides a place to clean up program execution before it is terminated.
   // It should not take more then a few seconds to execute.
   // Stop should not call os.Exit directly in the function.
   Stop(s Service) error
}

二、初始化service.Config结构体,并调用service.New创建service对象

三、为service对象设置日志Logger

四、调用service的Run方法来运行程序

3.2 示例程序2

package main

import (
 "encoding/json"
 "flag"
 "fmt"
 "log"
 "os"
 "os/exec"
 "path/filepath"

 "github.com/kardianos/service"
)

// Config is the runner app config structure.
type Config struct {
 Name, DisplayName, Description string

 Dir  string
 Exec string
 Args []string
 Env  []string

 Stderr, Stdout string
}

var logger service.Logger

type program struct {
 exit    chan struct{}
 service service.Service

 *Config

 cmd *exec.Cmd
}

func (p *program) Start(s service.Service) error {
 // Look for exec.
 // Verify home directory.
 fullExec, err := exec.LookPath(p.Exec)
 if err != nil {
  return fmt.Errorf("Failed to find executable %q: %v", p.Exec, err)
 }

 p.cmd = exec.Command(fullExec, p.Args...)
 p.cmd.Dir = p.Dir
 p.cmd.Env = append(os.Environ(), p.Env...)

 go p.run()
 return nil
}
func (p *program) run() {
 logger.Info("Starting ", p.DisplayName)
 defer func() {
  if service.Interactive() {
   p.Stop(p.service)
  } else {
   p.service.Stop()
  }
 }()

 if p.Stderr != "" {
  f, err := os.OpenFile(p.Stderr, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
  if err != nil {
   logger.Warningf("Failed to open std err %q: %v", p.Stderr, err)
   return
  }
  defer f.Close()
  p.cmd.Stderr = f
 }
 if p.Stdout != "" {
  f, err := os.OpenFile(p.Stdout, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0777)
  if err != nil {
   logger.Warningf("Failed to open std out %q: %v", p.Stdout, err)
   return
  }
  defer f.Close()
  p.cmd.Stdout = f
 }

 err := p.cmd.Run()
 if err != nil {
  logger.Warningf("Error running: %v", err)
 }

 return
}
func (p *program) Stop(s service.Service) error {
 close(p.exit)
 logger.Info("Stopping ", p.DisplayName)
 if p.cmd.Process != nil {
  p.cmd.Process.Kill()
 }
 if service.Interactive() {
  os.Exit(0)
 }
 return nil
}

func getConfigPath() (string, error) {
 fullexecpath, err := os.Executable()
 if err != nil {
  return "", err
 }

 dir, execname := filepath.Split(fullexecpath)
 ext := filepath.Ext(execname)
 name := execname[:len(execname)-len(ext)]

 return filepath.Join(dir, name+".json"), nil
}

func getConfig(path string) (*Config, error) {
 f, err := os.Open(path)
 if err != nil {
  return nil, err
 }
 defer f.Close()

 conf := &Config{}

 r := json.NewDecoder(f)
 err = r.Decode(&conf) //将json字符串转为Config对象
 if err != nil {
  return nil, err
 }
 return conf, nil
}

func main() {
 svcFlag := flag.String("service", "", "Control the system service.")
 flag.Parse()

 configPath, err := getConfigPath() //获取配置文件路径
 if err != nil {
  log.Fatal(err)
 }
 config, err := getConfig(configPath) //获取配置
 if err != nil {
  log.Fatal(err)
 }

 //初始化service配置属性
 svcConfig := &service.Config{
  Name:        config.Name,
  DisplayName: config.DisplayName,
  Description: config.Description,
 }

 prg := &program{
  exit: make(chan struct{}),

  Config: config,
 }
 s, err := service.New(prg, svcConfig)
 if err != nil {
  log.Fatal(err)
 }
 prg.service = s

 errs := make(chan error, 5)
 logger, err = s.Logger(errs)
 if err != nil {
  log.Fatal(err)
 }

 go func() {
  for {
   err := <-errs
   if err != nil {
    log.Print(err)
   }
  }
 }()

 if len(*svcFlag) != 0 {
     //通过命令行参数来控制服务
  err := service.Control(s, *svcFlag)
  if err != nil {
   log.Printf("Valid actions: %q\n", service.ControlAction)
   log.Fatal(err)
  }
  return
 }
 err = s.Run()
 if err != nil {
  logger.Error(err)
 }
}

其配置文件runner.json如下

{
 "Name": "builder",
 "DisplayName": "Go Builder",
 "Description": "Run the Go Builder",
 
 "Dir": "C:\\dev\\go\\src",
 "Exec": "C:\\windows\\system32\\cmd.exe",
 "Args": ["/C","C:\\dev\\go\\src\\all.bat"],
 "Env": [
  "PATH=C:\\TDM-GCC-64\\bin;C:\\Program Files (x86)\\Git\\cmd",
  "GOROOT_BOOTSTRAP=C:\\dev\\go_ready",
  "HOMEDRIVE=C:",
  "HOMEPATH=\\Documents and Settings\\Administrator"
 ],
 
 "Stderr": "C:\\builder_err.log",
 "Stdout": "C:\\builder_out.log"
}

可将service相关的配置信息存储到配置文件中。通过读取配置文件中的配置信息来生成service。

4

接口定义(源码剖析)

这个库有两个接口

4.1 Service接口

其中Service接口有Run、Start、Stop、Restart、Install、Uninstall等核心方法可实现,以对应服务的运行、启动、停止、重启、安装、卸载操作。

type Service interface {
 // Run should be called shortly after the program entry point.
 // After Interface.Stop has finished running, Run will stop blocking.
 // After Run stops blocking, the program must exit shortly after.
 Run() error

 // Start signals to the OS service manager the given service should start.
 Start() error

 // Stop signals to the OS service manager the given service should stop.
 Stop() error

 // Restart signals to the OS service manager the given service should stop then start.
 Restart() error

 // Install setups up the given service in the OS service manager. This may require
 // greater rights. Will return an error if it is already installed.
 Install() error

 // Uninstall removes the given service from the OS service manager. This may require
 // greater rights. Will return an error if the service is not present.
 Uninstall() error

 // Opens and returns a system logger. If the user program is running
 // interactively rather then as a service, the returned logger will write to
 // os.Stderr. If errs is non-nil errors will be sent on errs as well as
 // returned from Logger's functions.
 Logger(errs chan<- error) (Logger, error)

 // SystemLogger opens and returns a system logger. If errs is non-nil errors
 // will be sent on errs as well as returned from Logger's functions.
 SystemLogger(errs chan<- error) (Logger, error)

 // String displays the name of the service. The display name if present,
 // otherwise the name.
 String() string
}
image-20210910150157094

其中aixService、freebsdService、solarisService、systemd、sysv、upstart、windowsService分别对应不同的操作系统aix,freebsd,solaris,linux,windows操作系统上的服务。

4.2 Interface接口

Interface接口中有Start和Stop

type Interface interface {
 // Start provides a place to initiate the service. The service doesn't not
 // signal a completed start until after this function returns, so the
 // Start function must not take more then a few seconds at most.
 Start(s Service) error

 // Stop provides a place to clean up program execution before it is terminated.
 // It should not take more then a few seconds to execute.
 // Stop should not call os.Exit directly in the function.
 Stop(s Service) error
}

《酷Go推荐》招募:

各位Gopher同学,最近我们社区打算推出一个类似GoCN每日新闻的新栏目《酷Go推荐》,主要是每周推荐一个库或者好的项目,然后写一点这个库使用方法或者优点之类的,这样可以真正的帮助到大家能够学习到

新的库,并且知道怎么用。

大概规则和每日新闻类似,如果报名人多的话每个人一个月轮到一次,欢迎大家报名!戳「阅读原文」,即可报名

扫码也可以加入 GoCN 的大家族哟~