标签 OS 下的文章

Go语言之道[译]

本文永久链接 – https://tonybai.com/2022/09/25/the-tao-of-go

近期阅读了John Arundel的文章《The Tao of Go》,看完后我都有心去阅读一遍《道德经》了:)。作者将Go语言设计哲学与惯例与“道”学三宝有机的联系到一起,给了我不小的启发。这里译成中文,供大家参考。


你可以让水牛去任何地方,只要它们想去 – 杰拉尔德・温伯格 《咨询的奥秘》(译注:原文似乎将Gerald M. Weinberg 写成了Jerry Weinberg,应该是笔误)

“道”是指事物的内在本质或自然趋势。例如,水向低处流:这就是水的“道”。你可以筑坝、疏导、抽水,或以其他方式干扰它,但尽管你做了种种努力,它最终可能还是会流向它要去的地方。

你遵循“道”并将其作为你的处事原则,就是要对事物的自然趋势保持敏感,不浪费精力与之抗争,而是顺势而为,而非逆势而为

扩展一下水的比喻,一个泳技糟糕的游泳者在水中乱扑腾,制造了很多噪音和骚动,但却没有真正前进。而道家,则是在水中冲浪(译注:借助波浪,顺势而为)。

那么什么是Go语言之道呢?如果我们以一种敏感的、聪明的方式来进行Go的软件开发,遵循语言和问题的自然轮廓,而不是试图用推土机把它们推开,那会是什么样子呢?让我们试着建立一些一般原则

一. 仁慈(Kindness)

我有三宝,持而保之。一曰慈(kindness),二曰俭(simplicity),三曰不敢为天下先(humility,谦逊)。- 《道德经》

用仁慈(译注:最大程度的善意)和同情心编写程序是什么意思呢?它意味着我们为人而不是为计算机写代码。人容易犯错,没有耐心,缺乏经验,注意力不集中,而且在其他方面也不完美。我们可以通过在Go代码的设计和细节上花些心思,使大家的生活(和工作)变得更加轻松。

我们可以善待我们的用户:给我们的库起一个描述性的名字(译注:比如像net、os这样的名字),让它们容易导入,提供良好的文档,并为它们提供宽松的开源许可。我们可以设计深度抽象,让用户利用小而简单的API来访问强大而有用的行为。

我们可以善待那些运行我们程序的人,使他们易于安装和更新,要求最少的配置和依赖,捕捉最常见的使用错误和运行时的错误,给用户提供有用的、准确的和友好的信息,告诉他们出现了什么错误以及如何解决。

聪明是一种天赋,仁慈是一种选择。 – 杰夫・贝佐斯

我们可以通过尽可能的清晰(clear)、简单(simple)和明确(explicit)来善待那些不得不阅读我们代码的人。我们可以给类型和函数起一个在上下文中有意义的名字,让用户用我们的抽象概念在直接的、逻辑的组合中创建自己的程序。我们可以通过遵守惯例、实现标准接口和以明显的方式做明显的事情来消除认知上的障碍和速度上的障碍。

在代码审查中,我们保持温和并多用鼓励性的语言。我们在别人的工作中找到值得称赞的地方,我们不会因为别人犯错或忽略细节而把他们当成傻瓜。如果我们对自己坦诚,我们会承认我们也会犯同样的错误。我们也知道,接受批评对我们来说是痛苦和困难的,但我们可以用仁慈来调节以应对对自己的批评。

最后,我们也要对自己仁慈,通过编写优秀的测试,使我们的程序在未来容易理解、修正和改进,当我们发现错误或设计缺陷时,不要对自己生气。

一旦代码仓库变成了意大利面条,就几乎不可能修复。 – John Ousterhout, 《软件设计的哲学》

另一个对我们未来的自己和我们的继任者仁慈的方法是,对程序的结构和整体设计进行小的、持续的改进。好的程序能活很久(当然,有一些糟糕的程序也能活很久),许多小改动的累积效应通常会使代码库变得混乱、复杂和笨拙。我们可以花一点额外的时间来帮助避免这种情况的发生,每当我们为某些事情访问代码库时,我们就可以重构和清理它。因为我们很少有机会从头开始重写系统,所以长期投入少量时间进行微改进是保持系统健康的唯一实用方法。

二. 简单(Simplicity)

“道”教给我们的第二种美德是节俭(frugality)、谦虚(modesty)、简单(simplicity):以小博大,消除杂乱。Go本身就是一种“节俭”的语言,拥有较少的语法和表面积(译注:可能是较少的API的意思)。它并不试图做所有的事情,或者取悦所有人

我们的生活被琐碎的细节浪费掉了。简化,再简化”— 亨利・大卫・梭罗《瓦尔登湖》

我们也应该这样做,使我们的程序小而聚焦,不混乱,做好一件事。深度抽象为强大的机器提供了一个简单的接口。我们不会让用户为了获得调用我们库的特权而做大量的文书工作。只要我们能够为最常见的情况提供一个简单的API和合理的默认值,我们就会这么做。

灵活性是一件好事,但我们不应该试图处理每一种情况,或提供每一种功能。可扩展性是很好的,但我们不应该为了我们目前还不需要的东西做出妥协而损害一个简单的设计。事实上,一个简单的程序比一个复杂的程序更容易扩展。

我有最简单的口味。我总是对最好的东西感到满意。 – 奥斯卡-王尔德,引自Edgar Saltus,”奥斯卡-王尔德,一个闲人的印象”

我们不会用函数、类型、接口、回调、参数和功能选项(options)来压垮用户。最小的API是最好的,因为它需要最少的知识来使用。我们不会用几十个包和子文件夹的子文件夹来将我们的module复杂化。我们不会采用无休止的命令行标志或要求用户编写冗长的配置文件。

我们满足于重复大块的代码,而不是纯粹为了满足我们保持代码不重复的愿望而发明不必要的抽象概念。如果我们可以通过为几种不同的类型实现相同的函数来解决问题,我们就不写复杂的代码生成器或泛型函数(译注:Go 1.18泛型落地后,有些时候使用泛型函数感官上代码更为简洁)。如果一个方法自然有点长,我们会让它长,而不是积极地把它重构为不必要的子函数,只是为了让每个子函数都能有几行长。

如果一个就够了,我们就不写十个测试。如果只需要一个函数,我们就不创建一个接口。我们不要让用户实现我们的接口,而是要实现他们的接口。

谁能出不由户,何莫由斯道也? – 孔子

我们是明确的/显式的(explicit);我们避免魔法。我们不在没有帮助的地方使用并发性。我们让包自成一体,与其他包解耦,我们避免让一个包或API的类型泄漏到我们代码库的其他部分。我们设定明显的内部和外部界限,并加以执行。

我们节约资源;我们避免泄漏,并在必要时使用尽可能少的内存或CPU。我们高效地处理数据流,而不是将其放入大块的内存中。我们产生的垃圾越少,需要收集的就越少。我们不在不需要的地方传递Context。

我们不纠结于性能问题。Go是高性能的。但我们的代码可能不需要这么快;至少,不需要以牺牲简单性为代价来换取高性能。

正如Go谚语所说:我们接受接口值。这样我们就需要对它们是什么做出最少的假设,但我们会返回具体的值(结构),这样用户就不用为它们编写大量的类型断言。

三. 谦逊(Humility)

第三件宝物是谦逊。像水一样,道家寻求低调,不争,不比,不试图打动别人。Go本身是谦逊和务实的:它没有其他语言的所拥有高科技功能特性和理论优势。事实上,它故意忽略了许多其他语言的重要卖点。它的设计者对创造一种令人印象深刻的编程语言或在人气调查中名列前茅并不感兴趣,而是想为人们提供一种小型而简单的工具,以最实用和最直接的方式完成有用的工作。

Go认识到我们很容易犯错,因此它提供了很多方法来保护我们不犯错。它负责分配内存,清理我们已经用完的东西,并警告我们有未使用的包导入或变量。它是为那些知道自己不是什么都懂,并且了解自己的错误倾向的人(换句话说,是谦虚的人)设计的语言。

最危险的错误是没有认识到我们自己的错误倾向。 – 巴塞尔-利德尔-哈特

作为Go程序员,我们写代码不要显得过于聪明,我们通过这种方式来保持谦逊。我们写代码并不是为了给大家留下我们是多么了不起的程序员的印象:相反,我们满足于做明显的事情。我们清楚而直截了当地表达自己,而不觉得有必要把自己的个性强加在代码中。

当标准库能解决问题时,我们就使用它,而只有在它不能解决问题时才使用第三方库。如果有一个堪称事实标准的包,我们就使用它:如果它对别人足够好,对我们也足够好。

我们避免通过panic或调用os.Exit或log.Fatal来意外地终止用户的程序,因为我们认识到,我们没有足够的智慧来事先确定问题是否真的是致命的。相反,我们会在问题发生时处理一切可以处理的事情,而当我们不能处理时,我们会谦逊地返回一个错误,并提供有用的上下文信息,让我们的用户来决定如何处理。

我们可以认识到,我们并不了解所有的事情,我们也无法做出非常准确的预测(尤其是对未来的预测),所以我们不应该浪费时间和精力来预先设计我们可能永远不需要的东西。我们不认为我们最了解其他的软件是否会想和我们的软件一起使用,所以我们不会硬性规定对它的依赖。

我们假设我们写的任何东西都会包含bug,所以我们写了详尽的测试代码,试图引出意外的行为或不正确的结果。我们明白不可避免地会有一些我们不知道或无法正确预测的重要事情,所以我们不会为了现状而过多地优化代码,因为很多工作最终都会被浪费掉。

以学生的姿态(不断学习),永远不要觉得已经长大而不问问题,永远不要觉得知道得太多而拒绝学习新事物。- 奥格·曼狄诺, 《世界上最伟大的销售员》的作者

当我们审查别人的代码时,我们不会自动假设我们是最了解的:我们很乐意向任何有东西可以教我们的人学习。如果有些东西看起来很奇怪或不对,我们就会问:”从什么角度看这是有意义的?我没有什么信息可以解释为什么这是必要的?”

我们把我们的评论当作问题,以真诚而不是讽刺的方式提出:这有必要吗?如果……会怎样?你有没有考虑过……?如果……会不会更好?我们尊重别人的时间,就像尊重自己的时间一样,所以我们不要求他们提供不必要的信息,或者只为了符合我们喜欢的风格而做出微小的改变,或者坐在走形式的会议中浪费时间,或者写多余的状态报告。

我们知道我们并不总是正确的。明智的人可以以一种文明和建设性的方式对事情提出异议。如果我们把人们当做白痴,那么当他们反应不好的时候就不应该感到惊讶。相反,我们一开始就假设对方是理性的、体面的,并根据他们对情况的最佳理解,真诚地行事。有时情况并非如此,但这仍然是正确的默认假设,直到他们最终证明不是这样。

在要求别人审查自己的代码之前,我们会谦虚地审查自己的代码,因为如果我们这都懒得做,他们为什么要做?我们花时间逐行阅读,像一个新的用户或开发者那样阅读,遵循逻辑逐一论证:从哪里开始阅读是否很清楚?程序是否在一开始就引入了关键的类型或常量并说明它们是如何使用的?命名是否清楚并准确地指明了它们的作用,或者它们在十几次重构中是否变得混乱和过时了?程序是否整齐自然地融入了它的结构,或者它在某些部分过度堆砌,而在其他部分奇怪地留下了空白?

因为我们没有被自己的聪明和优雅所束缚,我们不需要把三四个不同的想法塞进一行代码中。相反,我们把逻辑清晰、简单、显式地列出,一步一步地,一个一个地陈述,一点一点地,以读者所期望的方式准确地做必要的事情。在无法做到这一点的地方,我们会不厌其烦地向读者解释他们需要知道什么才能理解发生了什么。

因为我们知道我们不是天才,我们不可能把程序写得那么出色,不言自明,所以我们在解释上花了一些功夫。我们为代码提供文档,不仅说明程序的作用,而且说明如何用它完成用户可能想要做的事情。文档还包含了详细的使用例子,准确地显示出需要做什么,从头开始,以执行现实的任务,当它完成时,用户应该期望看到什么,以及他们接下来应该做什么,并且我们严格地定期检查这些例子,以确保它们仍然有效。

四. 无为(Not Striving)

我们已经谈到了一些将仁慈、简单和谦逊这三件宝物应用于用Go编写软件的方法。这些品质已经存在于每个人身上,即使它们在一些人身上隐藏得很好。同样地,每个人都已经知道如何在编程和生活中遵循“道”。的确,他们不能不这样做。但是,一旦你理解了这个事实,并停止在所有事情上做这样的挣扎,生活就会变得更加有趣。

道给我们的最后一个教导是无为(not striving)。这有时会被误解为懒惰、退缩或被动;恰恰相反。努力工作并不总是意味着工作出色。我们都知道那些长期忙碌的人,总是匆匆忙忙,手忙脚乱,活动频繁,但他们似乎从未真正取得过什么成就。而且,他们过得很痛苦,因为他们也知道这一点。

相反,我们经常在别人看来我们什么都没做的时候以最佳状态完成我们的工作:在一个美丽的日子里在河边散步,或者坐在门廊上看蜘蛛结网。如果我们能聪明地从忙碌中停下来,只需一分钟,正确的想法往往就会直接出现在我们的头脑中。

与其把每个问题都当作要攻击的敌人、要攀登的山峰或要拆毁的墙壁,我们可以使用“无为”原则(有时“不强迫(not forcing)”会是更好的翻译)。我们可能都有过这样的尴尬经历:无果地推一扇顽固的门,最后才意识到这扇门对拉动的反应更好。在我们的日常工作中,我们忽略了哪些小迹象表明我们应该拉而不是推?

解决问题的心态是好的,但消除问题则更好。我们怎样才能重新规划这个问题,使其消失?对需求的重述会使解决方案变得微不足道,甚至显而易见?是否有一个简单而优雅的设计是我们没有看到的,因为我们专注于一些被证明是不相关的细节?我们是否可以不用尝试解决这个问题?最好的优化是根本不做这件事。

把编程和打字混为一谈是一个常见的错误。如果有人只是坐在那里盯着空间,那就不像是在做什么有用的事情。但是,如果他们在键盘上疯狂地敲击,我们就会认为他们在做什么。事实上,真正的编程发生在打字之前,有时甚至代替了打字。当我们构思了一段非常好的程序,我们唯一需要按的键往往是删除键。

真正有趣的是,你不需要相信我的话来证明道家原则在编程中的有效性,或者在生活的其他领域。世界本身会教你什么是有效的,什么是无效的,以及如何分辨它们。在行使你的仁慈、简单和谦逊方面做一些小实验,看看会发生什么,感觉如何。

你不必称它为“道”,如果这让你感到恼火。这只是一个人为编造的词。如果你一直知道做事情有正确的方法和错误的方法,而且你认为我只是用了一个花哨的中文名字而没有说什么新的或有价值的东西,你是对的。

下次你遇到问题时,试一次不努力或不强迫,看看是否可以温和地鼓励问题自己解决。如果你发现自己在努力把水牛送到你想去的地方,就不要再挣扎了。问问自己,你是否能找到水牛想去的地方,也许那可能不是它的最佳位置。


“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

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

如何像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 商务合作请联系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