标签 C 下的文章

Go是一门面向对象编程语言吗

本文永久链接 – https://tonybai.com/2023/03/12/is-go-object-oriented

Go语言已经开源13年了,在近期TIOBE发布的2023年3月份的编程语言排行榜中,Go再次冲入前十,相较于Go在2022年底的排名提升了2个位次:

《Go语言第一课》专栏中关于Go在这两年开始飞起的“预言”也正在逐步成为现实^_^,大家学习Go的热情也在快速提升, 《Go语言第一课》专栏的学习的人数年后也快速增加,快突破2w了。

很多专栏的订阅者都是第一次接触Go,他们中的很多是来自像Java, Ruby这样的OO(面向对象)语言阵营的,他们学习Go之后的第一个问题便是:Go是一门OO语言吗?在这篇博文中,我们就来探讨一下。

一. 溯源

在公认的Go语言“圣经”《Go程序设计语言》一书中,有这样一幅Go语言与其主要的先祖编程语言的亲缘关系图:

从图中我们可以清晰看到Go语言的“继承脉络”:

  • C语言那里借鉴了表达式语法、控制语句、基本数据类型、值参数传递、指针等;
  • Oberon-2语言那里借鉴了package、包导入和声明的语法,而Object Oberon提供了方法声明的语法。
  • Alef语言以及Newsqueak语言中借鉴了基于CSP的并发语法。

我们看到,从Go先祖溯源的情况来看,Go并没有从纯面向对象语言比如Simula、SmallTalk等那里取经。

Go诞生于2007年,开源于2009年,那正是面向对象语言和OO范式大行其道的时期。不过Go设计者们觉得经典OO的继承体系对程序设计与扩展似乎并无太多好处,还带来了较多的限制,因此在正式版本中并没有支持经典意义上的OO语法,即基于类和对象实现的封装、继承和多态这三大OO主流特性。

但这是否说明Go不是一门OO语言呢?也不是! 带有面向对象机制的Object Oberon也是Go的先祖语言之一,虽然Object Oberon的OO语法又与我们今天常见的语法有较大差异。

就此问题,我还特意咨询了ChatGPT^_^,得到的答复如下:

ChatGPT认为:Go支持面向对象,提供了对面向对象范式基本概念的支持,但支持的手段却并不是类与对象。

那么针对这个问题Go官方是否有回应呢?有的,我们来看一下。

二. 官方声音

Go官方在FAQ中就Go是否是OO语言做了简略回应

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

粗略翻译过来就是:

Go是一种面向对象的语言吗?

是,也不是。虽然Go有类型和方法,并且允许面向对象的编程风格,但却没有类型层次。Go中的“接口”概念提供了一种不同的OO实现方案,我们认为这种方案更易于使用,而且在某些方面更加通用。还有一些可以将类型嵌入到其他类型中以提供类似子类但又不等同于子类的机制。此外,Go中的方法比C++或Java中的方法更通用:Go可以为任何数据类型定义方法,甚至是内置类型,如普通的、“未装箱的”整数。Go的方法并不局限于结构体(类)。

此外,由于去掉了类型层次,Go中的“对象”比C++或Java等语言更轻巧。

“是,也不是”!我们看到Go官方给出了一个“对两方都无害”的中庸的回答。那么Go社区是怎么认为的呢?我们来看看Go社区的一些典型代表的观点。

三. 社区声音

Jaana DoganSteve Francia都是前Go核心团队成员,他们在加入Go团队之前对“Go是否是OO语言”这一问题也都有自己的观点论述。

Jaana Dogan在《The Go type system for newcomers》一文中给出的观点是:Go is considered as an object-oriented language even though it lacks type hierarchy,即“Go被认为是一种面向对象的语言,即使它缺少类型层次结构”。

而更早一些的是Steve Francia在2014年发表的文章《Is Go an Object Oriented language?》中的结论观点:Go,没有对象或继承的面向对象编程,也可称为“无对象”的OO编程模型。

两者表达的遣词不同,但含义却异曲同工,即Go支持面向对象编程,但却不是通过提供经典的类、对象以及类型层次来实现的

那么Go究竟是以何种方式实现对OOP的支持的呢?我们继续看!

四. Go的“无对象”OO编程

经典OO的三大特性是封装、继承与多态,这里我们看看Go中是如何对应的。

1. 封装

封装就是把数据以及操作数据的方法“打包”到一个抽象数据类型中,这个类型封装隐藏了实现的细节,所有数据仅能通过导出的方法来访问和操作。 这个抽象数据类型的实例被称为对象。经典OO语言,如Java、C++等都是通过类(class)来表达封装的概念,通过类的实例来映射对象的。熟悉Java的童鞋一定记得《Java编程思想》一书的第二章的标题:“一切都是对象”。在Java中所有属性、方法都定义在一个个的class中。

Go语言没有class,那么封装的概念又是如何体现的呢?来自OO语言的初学者进入Go世界后,都喜欢“对号入座”,即Go中什么语法元素与class最接近!于是他们找到了struct类型。

Go中的struct类型中提供了对真实世界聚合抽象的能力,struct的定义中可以包含一组字段(field),如果从OO角度来看,你也可以将这些字段视为属性,同时,我们也可以为struct类型定义方法(method),下面例子中我们定义了一个名为Point的struct类型,它拥有一个导出方法Length:

type Point struct {
    x, y float64
}

func (p Point) Length() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

我们看到,从语法形式上来看,与经典OO声明类的方法不同,Go方法声明并不需要放在声明struct类型的大括号中。Length方法与Point类型建立联系的纽带是一个被称为receiver参数的语法元素。

那么,struct是否就是对应经典OO中的类呢? 是,也不是!从数据聚合抽象来看,似乎是这样, struct类型可以拥有多个异构类型的、代表不同抽象能力的字段(比如整数类型int可以用来抽象一个真实世界物体的长度,string类型字段可以用来抽象真实世界物体的名字等)。

但从拥有方法的角度,不仅是struct类型,Go中除了内置类型的所有其他具名类型都可以拥有自己的方法,哪怕是一个底层类型为int的新类型MyInt:

type MyInt int

func(a MyInt)Add(b int) MyInt {
    return a + MyInt(b)
}

2. 继承

就像前面说的,Go设计者在Go诞生伊始就重新评估了对经典OO的语法概念的支持,最终放弃了对诸如类、对象以及类继承层次体系的支持。也就是说:在Go中体现封装概念的类型之间都是“路人”,没有亲爹和儿子的关系的“牵绊”

谈到OO中的继承,大家更多想到的是子类继承了父类的属性与方法实现。Go虽然没有像Java extends关键字那样的显式继承语法,但Go也另辟蹊径地对“继承”提供了支持。这种支持方式就是类型嵌入(type embedding),看一个例子:

type P struct {
    A int
    b string
}

func (P) M1() {
}

func (P) M2() {
}

type Q struct {
    c [5]int
    D float64
}

func (Q) M3() {
}

func (Q) M4() {
}

type T struct {
    P
    Q
    E int
}

func main() {
    var t T
    t.M1()
    t.M2()
    t.M3()
    t.M4()
    println(t.A, t.D, t.E)
}

我们看到类型T通过嵌入P、Q两个类型,“继承”了P、Q的导出方法(M1~M4)和导出字段(A、D)。

关于类型嵌入的具体语法说明,大家可以温习一下《十分钟入门Go语言》《Go语言第一课》专栏

不过实际Go中的这种“继承”机制并非经典OO中的继承,其外围类型(T)与嵌入的类型(P、Q)之间没有任何“亲缘”关系。P、Q的导出字段和导出方法只是被提升为T的字段和方法罢了,其本质是一种组合,是组合中的代理(delegate)模式的一种实现。T只是一个代理(delegate),对外它提供了它可以代理的所有方法,如例子中的M1~M4方法。当外界发起对T的M1方法的调用后,T将该调用委派给它内部的P实例来实际执行M1方法。

以经典OO理论话术去理解就是T与P、Q的关系不是is-a,而是has-a的关系

3. 多态

经典OO中的多态是尤指运行时多态,指的是调用方法时,会根据调用方法的实际对象的类型来调用不同类型的方法实现。

下面是一个C++中典型多态的例子:

#include <iostream>

class P {
        public:
                virtual void M() = 0;
};

class C1: public P {
        public:
                void M();
};

void C1::M() {
        std::cout << "c1.M()\n";
}

class C2: public P {
        public:
                void M();
};

void C2::M() {
        std::cout << "c2.M()\n";
}

int main() {
        C1 c1;
        C2 c2;
        P *p = &c1;
        p->M(); // c1.M()
        p = &c2;
        p->M(); // c2.M()
}

这段代码比较清晰,一个父类P和两个子类C1和C2。父类P有一个虚拟成员函数M,两个子类C1和C2分别重写了M成员函数。在main中,我们声明父类P的指针,然后将C1和C2的对象实例分别赋值给p并调用M成员函数,从结果来看,在运行时p实际调用的函数会根据其指向的对象实例的实际类型而分别调用C1和C2的M。

显然,经典OO的多态实现依托的是类型的层次关系。那么对应没有了类型层次体系的Go来说,它又是如何实现多态的呢?Go使用接口来解锁多态

和经典OO语言相比,Go更强调行为聚合与一致性,而非数据。因此Go提供了对类似duck typing的支持,即基于行为集合的类型适配,但相较于ruby等动态语言,Go的静态类型机制还可以保证应用duck typing时的类型安全。

Go的接口类型本质就是一组方法集合(行为集合),一个类型如果实现了某个接口类型中的所有方法,那么就可以作为动态类型赋值给接口类型。通过该接口类型变量的调用某一方法,实际调用的就是其动态类型的方法实现。看下面例子:

type MyInterface interface {
    M1()
    M2()
    M3()
}

type P struct {
}

func (P) M1() {}
func (P) M2() {}
func (P) M3() {}

type Q int
func (Q) M1() {}
func (Q) M2() {}
func (Q) M3() {}

func main() {
    var p P
    var q Q
    var i MyInterface = p
    i.M1() // P.M1
    i.M2() // P.M2
    i.M3() // P.M3

    i = q
    i.M1() // Q.M1
    i.M2() // Q.M2
    i.M3() // Q.M3
}

Go这种无需类型继承层次体系、低耦合方式的多态实现,是不是用起来更轻量、更容易些呢!

五. Gopher的“OO思维”

到这里,来自经典OO语言阵营的小伙伴们是不是已经找到了当初在入门Go语言时“感觉到别扭”的原因了呢!这种“别扭”就在于Go对于OO支持的方式与经典OO语言的差别:秉持着经典OO思维的小伙伴一上来就要建立的继承层次体系,但Go没有,也不需要。

要转变为正宗的Gopher的OO思维其实也不难,那就是“prefer接口,prefer组合,将习惯了的is-a思维改为has-a思维”。

六. 小结

是时候给出一些结论性的观点了:

  • Go支持OO,只是用的不是经典OO的语法和带层次的类型体系;
  • Go支持OO,只是用起来需要换种思维;
  • 在Go中玩转OO的思维方式是:“优先接口、优先组合”。

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如何像gitlab-runner那样将Go应用安装为系统服务

本文永久链接 – https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner

《让reviewdog支持gitlab-push-commit,守住代码质量下限》一文中,gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤):

# Create a GitLab CI user
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash

# Install and run as service
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等,我们暂不care),系统服务就是由systemd管理的daemon process(守护进程)。

systemd是什么?linux主机上电后,os内核被加载并启动,os内核完成初始化以后,由内核第一个启动的程序是init程序,其PID(进程ID)为1,它为系统里所有进程的“祖先”,systemd便是主流新版linux中的那个init程序,它负责在主机启动后拉起所有安装为系统服务的程序

这些被systemd拉起的服务程序以守护进程(daemon process)的形式运行,那什么又是守护进程呢?《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的:

Daemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities.

守护进程是长期存在的进程。它们通常在系统启动时被启动,并在系统关闭时才终止。因为它们没有控制终端,我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。

该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules)),并给出了一个C语言示例:

#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void
daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    /*
     * Clear file creation mask.
     */
    umask(0);
    /*
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can’t get file limit", cmd);
    /*
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    setsid();

    /*
     * Ensure future opens won’t allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);

    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can’t ignore SIGHUP", cmd);
    if ((pid = fork()) < 0)
        err_quit("%s: can’t fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);
    /*
     * Change the current working directory to the root so
     * we won’t prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can’t change directory to /", cmd);
    /*
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);
    /*
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
     * Initialize the log file.
     */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }
}

那么,Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢?很遗憾!Go团队说很难做到。Go社区倒是有很多第三方的方案,比如像go-daemon这样的第三方实现,不过我并没有验证过这些方案,不保证完全ok。

Go团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换。gitlab-runner就是将自己安装为system服务,并由systemd对其进行管理的。

题外话:其实,自从有了容器技术(比如:docker)后,daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器,应用本身就运行于后台,使用–restart=always/on-failure选项,容器引擎(比如docker engine)会帮我们管理service,并在service宕掉后重启service。

那么,我们如何像gitlab-runner那样将自己安装为一个systemd service呢?我们继续向下看。

注意:这里只是将Go应用安装成一个systemd service,并不是自己将自己转换为守护进程,安装为systemd service本身是可行的,也是安全的。

翻看gitlab-runner源码,你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包,这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包,该包屏蔽了os层的差异,为开发人员提供了相对简单的Service操作接口,包括下面这些控制动作:

// github.com/kardianos/service/blob/master/service.go
var ControlAction = [5]string{"start", "stop", "restart", "install", "uninstall"}

好了,下面我们就用一个例子myapp来介绍一下如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力

myapp是一个http server,它在某个端口上提供服务,当收到请求时,返回”Welcome”字样的应答:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

func run(config string) error {
    ... ...

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("[%s]: receive a request from: %s\n", c.Server.Addr, r.RemoteAddr)
        w.Write([]byte("Welcome"))
    })
    fmt.Printf("listen on %s\n", c.Server.Addr)
    return http.ListenAndServe(c.Server.Addr, nil)
}

现在我们要为myapp增加一些能力,让它支持将自己安装为systemd service,并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。

我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包,只是用标准库的flag包。由于myapp支持subcommand,我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例,如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上,比如下面代码init函数体中的内容。

另外由于使用了subcommand,默认的flag.Usage不再能满足我们的要求了,我们需要自己实现一个usage函数并赋值给flag.Usage:

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go

var (
    installCommand = flag.NewFlagSet("install", flag.ExitOnError)
    runCommand     = flag.NewFlagSet("run", flag.ExitOnError)
    user           string
    workingdir     string
    config         string
)

const (
    defaultConfig = "/etc/myapp/config.ini"
)

func usage() {
    s := `
USAGE:
   myapp command [command options] 

COMMANDS:
     install               install service
     uninstall             uninstall service
     start                 start service
     stop                  stop service
     run                   run service

OPTIONS:
     -config string
        config file of the service (default "/etc/myapp/config.ini")
     -user string
        user account to run the service
     -workingdir string
        working directory of the service`

    fmt.Println(s)
}

func init() {
    installCommand.StringVar(&user, "user", "", "user account to run the service")
    installCommand.StringVar(&workingdir, "workingdir", "", "working directory of the service")
    installCommand.StringVar(&config, "config", "/etc/myapp/config.ini", "config file of the service")
    runCommand.StringVar(&config, "config", defaultConfig, "config file of the service")
    flag.Usage = usage
}

func main() {
    var err error
    n := len(os.Args)
    if n <= 1 {
        fmt.Printf("invalid args\n")
        flag.Usage()
        return
    }

    subCmd := os.Args[1] // the second arg

    // get Config
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }
... ...
}

这些都完成后,我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息:

// https://github.com/bigwhite/experiments/blob/master/system-service/config.go

func getServiceConfig(subCmd string) (*service.Config, error) {
    c := service.Config{
        Name:             "myApp",
        DisplayName:      "Go Daemon Service Demo",
        Description:      "This is a Go daemon service demo",
        Executable:       "/usr/local/bin/myapp",
        Dependencies:     []string{"After=network.target syslog.target"},
        WorkingDirectory: "",
        Option: service.KeyValue{
            "Restart": "always", // Restart=always
        },
    }   

    switch subCmd {
    case "install":
        installCommand.Parse(os.Args[2:])
        if user == "" {
            fmt.Printf("error: user should be provided when install service\n")
            return nil, errors.New("invalid user")
        }
        if workingdir == "" {
            fmt.Printf("error: workingdir should be provided when install service\n")
            return nil, errors.New("invalid workingdir")
        }
        c.UserName = user
        c.WorkingDirectory = workingdir

        // arguments
        // ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"
        c.Arguments = append(c.Arguments, "run", "-config", config)
    case "run":
        runCommand.Parse(os.Args[2:]) // parse config
    }   

    return &c, nil
}

这里要注意的是service.Config中的Option和Arguments,前者用于在systemd service unit配置文件中放置任意的键值对(比如这里的Restart=always),而Arguments则会被组成为ExecStart键的值,该值会在start service时传入使用。

接下来,我们便利用service包基于加载的Config创建操作服务的实例(srv),然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。

// https://github.com/bigwhite/experiments/blob/master/system-service/main.go
func main() {

    // ... ...
    c, err := getServiceConfig(subCmd)
    if err != nil {
        fmt.Printf("get service config error: %s\n", err)
        return
    }

    prg := &NullService{}
    srv, err := service.New(prg, c)
    if err != nil {
        fmt.Printf("new service error: %s\n", err)
        return
    }

    err = runServiceControl(srv, subCmd)
    if err != nil {
        fmt.Printf("%s operation error: %s\n", subCmd, err)
        return
    }

    fmt.Printf("%s operation ok\n", subCmd)
    return
}

func runServiceControl(srv service.Service, subCmd string) error {
    switch subCmd {
    case "run":
        return run(config)
    default:
        return service.Control(srv, subCmd)
    }
}

好了,代码已经完成!现在让我们来验证一下myapp的能力。

我们先来完成编译和二进制程序的安装:

$make
go build -o myapp main.go config.go

$sudo make install
cp ./myapp /usr/local/bin
$sudo make install-cfg
mkdir -p /etc/myapp
cp ./config.ini /etc/myapp

接下来,我们就来将myapp安装为systemd的服务:

$sudo ./myapp install -user tonybai -workingdir /home/tonybai
install operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead)

我们看到安装后,myApp已经成为了myApp.service,并处于inactive状态,其systemd unit文件/etc/systemd/system/myApp.service内容如下:

$sudo cat /etc/systemd/system/myApp.service
[Unit]
Description=This is a Go daemon service demo
ConditionFileIsExecutable=/usr/local/bin/myapp

After=network.target syslog.target 

[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/myapp "run" "-config" "/etc/myapp/config.ini"

WorkingDirectory=/home/tonybai
User=tonybai

Restart=always

RestartSec=120
EnvironmentFile=-/etc/sysconfig/myApp

[Install]
WantedBy=multi-user.target

接下来,我们来启动一下该服务:

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.3M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432

我们看到myApp服务成功启动,并在65432这个端口上监听!

我们利用curl向这个端口发送一个请求:

$curl localhost:65432
Welcome                                                                         

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago
   Main PID: 623859 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348

我们看到myApp服务运行正常并返回预期应答结果。

现在我们利用stop subcommand停掉该服务:

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago
    Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM)
   Main PID: 623859 (code=killed, signal=TERM)

Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432
Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348
Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo...
Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded.
Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo.

修改配置/etc/myapp/config.ini(将监听端口从65432改为65431),然后再重启该服务:

$sudo cat /etc/myapp/config.ini
[server]
addr=":65431"

$sudo ./myapp start
start operation ok

$sudo systemctl status myApp
● myApp.service - This is a Go daemon service demo
     Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled)
     Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago
   Main PID: 624046 (myapp)
      Tasks: 6 (limit: 12651)
     Memory: 1.4M
     CGroup: /system.slice/myApp.service
             └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini

Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo.
Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431

从systemd的状态日志中我们看到myApp服务启动成功,并改为监听65431端口,我们访问一下该端口:

$curl localhost:65431
Welcome                                                                                                                      

$curl localhost:65432
curl: (7) Failed to connect to localhost port 65432: Connection refused

从上述结果可以看出,我们的配置更新和重启都是成功的!

我们亦可以使用myapp的uninstall功能从systemd中卸载该服务:

$sudo ./myapp uninstall
uninstall operation ok
$sudo systemctl status myApp
Unit myApp.service could not be found.

好了,到这里我们看到:在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。

最后小结一下:service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力,不过这个作业就留给大家了:)。大家如有问题,欢迎在评论区留言。

本文涉及的代码可以在这里下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats