将命令行实用程序从Go / Rust移植到D

几天前,Paulo Henrique Cuchi在“编程”上重述了他在Rust和Go译为Habré中开发命令行实用程序的经验该实用程序是其宠物项目Hashtrack的客户。Hashtrack提供了GraphQL API,客户可以使用该API跟踪特定的Twitter哈希标签并实时获取相关推文列表。评论的提示下,我决定在D中编写一个端口,以演示如何将D用于类似目的。我将尝试保持他在博客文章中使用的结构相同。点击



GitHub



视频上的来源



我如何来到D



主要原因是原始博客比较了Go和Rust之类的静态类型语言,并尊重地引用了Nim和Crystal,但没有提及D,D也属于此类。因此,我认为它将使比较有趣。



我也喜欢D作为一种语言,并在其他各种博客文章中都提到了D。



当地环境



该手册包含有关如何下载和安装参考编译器DMD的大量信息。Windows用户可以获取安装程序,而macOS用户可以使用自制软件。在Ubuntu上,我只是添加了apt信息库,然后按照常规安装进行安装。这样,您不仅可以获取DMD,还可以获取包管理器dub。



我安装了Rust,这样我就可以开始使用它了。我很惊讶这有多么容易。我只需要运行交互式安装程序即可,其余的工作都已完成。我需要在路径中添加〜/ .cargo / bin。您只需重新启动控制台即可使更改生效。



编辑的支持



我在Vim中编写Hashtrack并没有太大的困难,但这可能是因为我对标准库中发生的事情有一些了解。我总是打开文档,因为有时我使用的符号不是从正确的包中导入的,或者我用错误的参数调用了函数。请注意,对于标准库,您可以简单地编写“ import std;”。并准备好一切。但是,对于第三方库,您是一个人。



我对工具箱的状态感到好奇,所以我研究了我最喜欢的IDE Intellij IDEA的插件。我发现了这个并安装它。我还通过克隆它们各自的存储库并构建它们,然后配置IDEA插件以指向正确的路径来安装DCD和DScanner。请联系此博客文章的作者以进行澄清。



起初我遇到了一些问题,但在更新IDE和插件后已解决。我遇到的问题之一是她无法识别我自己的包裹,并一直将其标记为“可能未定义”。后来我发现,为了使它们被识别,我必须放入“ module package_module_name;”。在文件的顶部。



我认为至少在我的机器上仍然存在无法识别.length的错误。我在Github上开了一个问题,您可以在这里关注如果你很好奇。



如果您使用Windows,我听说过有关VisualD的消息



包装管理



Dub是D中的事实软件包管理器。它从code.dlang.org下载并安装依赖。对于该项目,我需要一个HTTP客户端,因为我不想使用cURL。最后,我得到了两个依赖关系,即请求和它的依赖关系,cachetools,它们没有自己的依赖关系。但是,由于某种原因,他又选择了十二个依赖项:







我认为Dub在内部使用了它们,但是我不确定。



Rust装载了很多箱子大约228个),但这可能是因为Rust版本具有比我更多的功能。例如,他下载了rpassword,这是一个在将密码字符输入终端时隐藏密码字符的工具,类似于Python的getpass函数。这是我代码中没有的许多内容之一。由于此建议我添加了对Linux的getpass支持由于我从原始Go来源复制了转义序列,因此我还在终端中添加了文本格式设置。



图书馆



对graphql不太了解,我不知道从哪里开始。在code.dlang.org上搜索“ graphql”将我带到相应的库,恰当地命名为“ graphqld ”。但是,在研究它之后,在我看来,它看起来更像是vibe.d插件,而不是真正的客户端(如果有)。



在Firefox中检查了网络请求之后,我意识到对于这个项目,我可以简单地模拟将使用HTTP客户端发送的graphql请求和转换。响应只是我可以使用std.json包提供的工具解析的JSON对象。考虑到这一点,我开始寻找HTTP客户端并满足了request,这是一个易于使用的HTTP客户端,但更重要的是,它已经达到一定的成熟度。



我复制了来自网络分析器的传出请求,并将它们粘贴到单独的.graphql文件中,然后导入并与适当的变量一起发送。大多数功能已放入GraphQLRequest结构中,因为我想根据项目需要将各种端点和配置插入其中:



资源
struct GraphQLRequest
{
    string operationName;
    string query;
    JSONValue variables;
    Config configuration;

    JSONValue toJson()
    {
        return JSONValue([
            "operationName": JSONValue(operationName),
            "variables": variables,
            "query": JSONValue(query),
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }

    Response send()
    {
        auto request = Request();
        request.addHeaders(["Authorization": configuration.get("token", "")]);
        return request.post(
            configuration.get("endpoint"),
            toString(),
            "application/json"
        );
    }
}




这是数据包交换代码段。以下代码处理身份验证:
struct Session
{
    Config configuration;

    void login(string username, string password)
    {
        auto request = createSession(username, password);
        auto response = request.send();
        response.throwOnFailure();
        string token = response.jsonBody
            ["data"].object
            ["createSession"].object
            ["token"].str;
        configuration.put("token", token);
    }

    GraphQLRequest createSession(string username, string password)
    {
        enum query = import("createSession.graphql").lineSplitter().join("\n");
        auto variables = SessionPayload(username, password).toJson();
        return GraphQLRequest("createSession", query, variables, configuration);
    }
}

struct SessionPayload
{
    string email;
    string password;

    //todo : make this a template mixin or something
    JSONValue toJson()
    {
        return JSONValue([
            "email": JSONValue(email),
            "password": JSONValue(password)
        ]);
    }

    string toString()
    {
        return toJson().toPrettyString();
    }
}




剧透警报-我以前从未这样做过。



一切都是这样的:main()函数从命令行参数创建一个Config结构,并将其注入到Session结构中,该结构实现了login,logout和status命令的功能。 createSession()方法通过从相应的.graphql文件读取实际查询并将变量与之一起传递来构造一个graphQL查询。我不想使用graphQL突变和查询来污染我的源代码,所以我将它们移到.graphql文件中,然后在编译时使用enum和import进行导入。后者需要一个编译器标志来指向stringImportPaths(默认为view /)。



至于login()方法,它的唯一职责是发送HTTP请求并处理响应。在这种情况下,它可以处理潜在的错误,尽管不是很小心。然后,它将令牌存储在配置文件中,实际上只不过是一个不错的JSON对象。



throwOnFailure方法不是查询库的核心功能的一部分。它实际上是一个辅助函数,可以执行一些快速而肮脏的错误处理:



void throwOnFailure(Response response)
{
    if(!response.isSuccessful || "errors" in response.jsonBody)
    {
        string[] errors = response.errors;
        throw new RequestException(errors.join("\n"));
    }
}


由于D支持UFCS,因此throwOnFailure(响应)语法可以重写为response.throwOnFailure()。这样可以很容易地嵌入到其他方法调用中,例如send()。我可能在整个项目中都过度使用了此功能。



错误处理



D在处理错误时更喜欢使用异常。基本原理在这里详细解释。我喜欢的一件事是,除非明确堵塞,否则最终会弹出未处理的错误。这就是为什么我能够摆脱简化的错误处理。例如,在这些行中:



string token = response.jsonBody
    ["data"].object
    ["createSession"].object
    ["token"].str;
configuration.put("token", token);


如果响应主体不包含令牌或导致令牌的任何对象,则将引发异常,该异常将在主要功能中冒泡,然后在用户面前爆炸。如果要使用Go,则必须非常小心每个步骤的错误。而且,坦率地说,由于每次调用该函数时err!= Null都会令人烦恼,因此我很想忽略该错误。但是,我对Go的理解是原始的,如果编译器因没有返回错误而没有做任何事情而对您咆哮,我也不会感到惊讶,因此,如果我错了,请随时纠正我。



如原始博客文章中所述,Rust风格的错误处理很有趣。我认为D标准库中没有这样的东西,但是已经有关于将其实现为第三方库的讨论。



网络套接字



我只想简要指出一点,我没有使用websockets来实现watch命令。我尝试使用Vibe.d的websocket客户端,但是它无法与hashtrack后端一起使用,因为它一直关闭连接。最后,我放弃了它,转而使用循环法,即使它被皱眉了。自从我与另一个Web服务器进行测试以来,该客户端一直在工作,因此将来可能会再次使用它。



持续集成



对于CI,我设置了两个构建作业:常规分支构建和主发行版,以确保下载了优化的工件构建。









大约 图片显示了组装时间。考虑到依赖项的加载。重建无依赖〜4s



内存消耗



我使用了/ usr / bin / time -v ./hashtrack --list命令来测量内存使用情况,如原始博客文章中所述。我不知道内存使用情况是否取决于用户遵循的主题标签,但这是使用dub build -b release编译的D程序的结果:

最大常驻集大小(千字节):10036

最大常驻集大小(千字节):10164

最大常驻集大小(千字节):9940

最大常驻集大小(千字节):10060

最大常驻集大小(千字节):10008


不错。我使用hashtrack用户运行了Go和Rust版本,并得到了以下结果:



使用go build -ldflags“ -s -w”构建的Go:

最大常驻集大小(千字节):13684

最大常驻集大小(千

字节):13820

最大常驻集大小(千字节):13904

最大常驻集大小(千字节):13796最大常驻集大小(千字节):13600

Rust编译为build --release:

最大居民集大小(千字节):9224

最大居民集大小(千字节):9192

最大居民集大小(千字节):9384

最大居民集大小(千字节):9132

最大居民集大小(千字节):9168
更新:Reddit用户skocznymroczny建议也测试LDC和GDC编译器。结果如下:

由dub build -b release编译的LDC 1.22 --compiler = ldc2(添加颜色输出和getpass之后)

最大居民集大小(KB):7816

最大居民集大小(KB):7912

最大居民集大小(KB):7804

最大居民集大小(KB):7832

最大居民集大小(KB):7804


D具有垃圾回收功能,但它也支持智能指针,并且最近还支持受Rust启发实验性内存管理方法我不确定这些功能与标准库的集成程度如何,因此我决定让GC为我处理内存。考虑到我在编写代码时没有考虑内存消耗,我认为结果相当不错。



二进制大小



Rust, cargo build --release: 7.0M



D, dub build -b release: 5.7M



D, dub build -b release --compiler=ldc2: 2.4M



Go, go build: 7.1M



Go, go build -ldflags "-s -w": 5.0M


.. — , , . Windows dub build -b release 2 x64 ( 1.5M x86-mscoff) , Rust Ubuntu18 - openssl, ,





我认为D是编写这样的命令行工具的可靠语言。我很少去外部依赖项,因为标准库包含了我所需的大部分内容。标准库中提供了诸如解析命令行参数,处理JSON,单元测试,发送HTTP请求(使用cURL)之类的功能。如果标准库缺少您所需的资源,则可以使用第三方软件包,但是我认为这方面仍有改进的余地。在另一方面,如果你的心态NIH这里没有发明,或者如果你想轻松地作为一个开源开发者产生影响,那么你一定会喜欢d生态系统。



原因我会用d






All Articles