Logo

site iconhenix | 遥远的街市

程序员,居住在深圳。对开放互联网和代码美学有浓厚兴趣,是日本漫画和CLAMP老师的粉丝。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

henix | 遥远的街市 RSS 预览

简单实现 C++ 字符串格式化

2024-04-28 00:00:00

  字符串格式化是很常见的功能,传统上,我们使用 C 语言的 printf 来格式化。但作为一位 C++ 爱好者,printf 的缺点也很明显:

  1. 非类型安全
  2. 无法添加自定义类型

  std::cout 的问题在于:

  1. 进制和 padding 是通过设置流的全局状态实现的
  2. std::ostringstream 的 str() 方法会复制底层的 buffer ,不够高效
  3. 无法将结果追加到一个现有的字符串上,只能新建字符串再合并,会多一次拷贝

  流行的新一代格式化库如 fmt 的问题在于:

  1. 基于格式串的替换,实现比较繁杂

  市面上其他的 C++ 格式化库也无法让我满意,于是我在自己的 C++ 通用库 xlib 中实现了一个简单的 fmt 模块,使用方法如下:

BasicFormat fmt;

// pad 可实现用 0 补齐
fmt("pi = ", fmt.pad(4, 314), ' '); // => "pi = 0314 "

  可以通过继承来添加自定义类型:

struct Date {
    int year, month, day;
};

struct MyFormat: public BasicFormat {
    using BasicFormat::append;
    static void append(const Date& d, StringPusher& push) {
        append_all(push, d.year, '-', pad(2, d.month), '-', pad(2, d.day));
    }
    X_FMT_IMPLEMENT_FORMAT
};

MyFormat fmt;

fmt(Date { 2012, 4, 1 }); // => "2012-04-01"

  支持自定义类型的方法有这么几种:

  1. 在全局命名空间定义 operator<< ,std::cout 就用的这种
  2. 模板偏特化 + 后期 namespace 写入
  3. 继承

  我一开始用的是模板偏特化的方法,但这种方案的问题是,相关定义是全局的。所以对某一个自定义类型,同一个程序里只能定义一种格式化方法。而且 C++ 的 namespace 是开放的:即后面 include 进来的文件可以往任意 namespace 添加东西,我认为这样太动态。

  我后来改为用继承实现,因为:

  1. 对某一个类型的格式化方法是定义在类上的,同一个程序里可以定义多个不同的类,从而实现对同一类型的多种不同的格式化方法
  2. 类定义好后就不能往里面添加东西了,更有利于程序阅读和分析

附:fmt 的简化实现

class StringPusher {
    std::string& str;
public:
    StringPusher(std::string& str): str(str) {}
    void operator()(char c) {
        str.push_back(c);
    }
    void operator()(std::string_view buf) {
        str.append(buf.data(), buf.size());
    }
};

// 待格式化整数
template<class Int>
struct FormattedInt {
    Int n;
    uint8_t radix; // 进制
    CharCase charCase; // 大小写
    uint8_t padTo; // 补齐位数,0 表示不补齐
};

/**
 * https://stackoverflow.com/questions/27375089/what-is-the-easiest-way-to-print-a-variadic-parameter-pack-using-stdostream
 */
#define X_FMT_IMPLEMENT_FORMAT template<class... Args>\
static void append_all(StringPusher& push, Args&&... args) {\
    using _expander = int[];\
    (void)_expander{ (append(std::forward<Args>(args), push), 0)... };\
}\
template<class... Args>\
std::string operator()(Args&&... args) {\
    std::string res;\
    StringPusher push(res);\
    append_all(push, args...);\
    return res;\
}

struct BasicFormat {
    // 字符串
    static void append(char c, StringPusher& push) {
        push(c);
    }
    static void append(const char* s, StringPusher& push) {
        push(s);
    }
    static void append(std::string_view s, StringPusher& push) {
        push(s);
    }
    static void append(const std::string& s, StringPusher& push) {
        push(s);
    }
    // 整数
    template<class Int, typename std::enable_if_t<std::is_integral_v<Int>>* = nullptr>
    static void append(Int n, StringPusher& push) {
        x::ser::formatInt(n, 10, CharCase::Lower, 0, push); // 省略 formatInt 定义
    }
    template<class Int>
    static FormattedInt<Int> pad(uint8_t n, Int x) {
        return FormattedInt<Int> { x, 10, CharCase::Lower, n };
    }
    template<class Int>
    static void append(FormattedInt<Int> fn, StringPusher& push) {
        x::ser::formatInt(fn.n, fn.radix, fn.charCase, fn.padTo, push); // 省略 formatInt 定义
    }
    X_FMT_IMPLEMENT_FORMAT
};

状态机与函数式编程(二)

2023-12-26 00:00:00

  读了我的上一篇文章的读者可能仍然会觉得状态机,或者说 Puhser 这个概念太抽象,不知道该怎么用。因此我在这篇文章中讲一个实际的例子。

  考虑一个常见的数据汇总问题:有一个文件,每行是一个数据,每行数据中包含一个日期和一些统计数字(比如销售额、用户活跃数、用户付费等),需要按月度汇总这些统计数字,并且输出到另一个文件。

  我们还有一些限制条件和设计要点:

  1. 文件已经按照日期从小到大排序
  2. 输出时也应按日期从小到大排序
  3. 每个时间分组中的数据行的数量并不确定,例如一个月可能有 31 天,也可能有 28 天
  4. 这个文件很大,为了控制内存的使用,不能把所有行读进一个大数组里处理
  5. 扩展性:如果以后要求不仅仅是汇总每月,还要按每周、每季度、每年汇总,能否很方便地支持?能否支持任意自定义的日期汇总方式?
  6. 扩展性2:汇总方法可能不仅仅是求和,还可能是求平均、求中位数、求标准差之类的,能否方便地支持自定义?
  7. 扩展性3:可能在其他部分的代码中也有这种“按时间统计汇总”的需求,能否抽象出一个公共函数 / 类?

  这个问题我们一般称为“resample”,在处理时间序列型数据时非常常见。著名的 Python 数据处理库 pandas 还有专门的教程页面

  回到函数式编程,哪种序列变换函数对这个问题最合适?可以一步步地分解思考这个问题:

  1. 我们需要一个将原始日期变换成汇总后日期的函数,例如将“2023-10-06”变换成“2023-10”,即提取出“月”的部分
  2. 上一步得到的值可以看作一个 key ,我们需要将序列中相邻的且 key 相同的元素放进一个组中
  3. 对每个组进行汇总,然后输出

  可见这里的重点是,如何将序列中相邻的且 key 相同的元素分组,有什么现成的函数吗?

  我首先想到的是,这个需求有点像 GroupBy ,但 GroupBy 跟这个需求有一些不同:

  1. GroupBy 的输出结果是一个 Map[Key, Item[]] ,而我们需要输出另一个序列
  2. GroupBy 是全局的,它会把所有 key 相同的汇总,而这个问题中,我们需要汇总的是 key 相同且相邻的元素

  然后我找到一个看上去很接近的 lo.PartitionBy ,但仔细观察后发现,这个函数会调整元素的顺序,依然不是我们需要的。

  所以只能自己实现了,我称之为“SeqGroupBy”:

type Pusher[T any] interface {
  Push(T)
  Flush()
}

type PusherSeqGroupBy[K comparable, A any] struct {
  next   Pusher[[]A]
  getKey func(A) K
  curKey K
  items  []A
}

func NewPusherSeqGroupBy[K comparable, A any](getKey func(A) K, next Pusher[[]A]) *PusherSeqGroupBy[K, A] {
  return &PusherSeqGroupBy[K, A]{next: next, getKey: getKey}
}

func (t *PusherSeqGroupBy[K, A]) Push(a A) {
  newKey := t.getKey(a)
  if len(t.items) == 0 {
    t.items = append(t.items, a)
    t.curKey = newKey
  } else {
    if newKey == t.curKey {
      t.items = append(t.items, a)
    } else {
      t.next.Push(t.items)
      t.curKey = newKey
      t.items = []A{a}
    }
  }
  return
}

func (t *PusherSeqGroupBy[K, A]) Flush() {
  if len(t.items) > 0 {
    t.next.Push(t.items)
    t.next.Flush()
  }
  t.items = nil
  return
}

  构造一个 PusherSeqGroupBy 需要传入两个参数:

  1. getKey: 从一行数据中提取出 key ,这个函数的实现是,先从一行数据中提取出日期,再把日期变成汇总后的日期,比如按月的话就是将“2023-10-06”变成“2023-10”
  2. next: 数据需要传给的下一个处理环节,类型是 Pusher[[]Row] 。接受一个已经分好组的数据组,汇总然后输出

  我们可以画出程序的数据流图(dataflow diagram)如下:

  按行读文件 → SeqGroupBy 分组 → 每个组放进汇总器 → 每条汇总结果输出

  每个右箭头“→”都对应了一个 Pusher.Push() 的调用。最终可以实现内存中最多持有一个汇总组的数据,做到了随用随销毁。

  程序的剩余部分已经很显然了,我想可以留给读者自己完成。从这个例子也可以看出为什么 Pusher 的接口定义中需要有 Flush 。

  这个例子我们还可以看出基于状态机的,或 push-style 函数式编程的一个特点:如果说基于迭代器的函数式编程是以数据的源(source)为基础,经过层层变换,最终输出到数据的汇(sink);那么基于状态机的函数式编程则是反过来将数据的汇(sink)层层包装,最后接入数据源。当然,实际上我们也可以同时使用这两种风格,既用迭代器把源变形,又用状态机把汇变形,然后在中间的某个地方拼接在一起,这样,我们可以自由地选择最合适的工具。

状态机与函数式编程

2023-12-22 00:00:00

  在编程中,我们常用的迭代器(iterator)与我今天要说的状态机(state machine)都是对“流”(stream)的抽象,只不过一个是拉(pull),一个是推(push)。

  迭代器的定义一般是这样的:

class Iterator[T] {
  T next();
}

  即调用一个函数,它不断返回流中的下一个元素。通常还需要其他机制来指示迭代结束,比如添加一个 bool hasNext() 方法(Java),返回两个值,第二个 bool 值表示迭代是否结束(js),抛一个特定异常(Python)等。

  任何数据流都有生产者(Producer)和消费者(Consumer),迭代器是生产者的一种抽象,那如果我们把消费者抽象出来,会是什么样的?

  • 在计算机科学中,称这种东西称为状态机(state machine)或自动机(automata)
  • Rx 框架称其为 Observer ,也就是可以订阅由 Observable 发出的事件的东西,由 OnNext, OnComplete, OnError 三个回调函数构成,只不过 Rx 框架更强调自己是异步的
  • 一个更函数式的抽象是从 (状态, 输入) 变换成新的状态,比如 reducer
  • Haskell 将它抽象为 iteratee

  我这篇文章还是使用计算机科学的术语,称其为状态机。我理解的状态机的接口定义是这样的:

class Pusher[T] {
  void Push(T); // 将一个元素推给状态机
  void Flush(); // 标记一个阶段结束
}

  如果跟 Go 语言中的 io.Writer 接口对比,是不是非常相似?事实上,我认为 Go 的 io.Reader 可以类比为迭代器,io.Writer 可以类比为状态机。

  下面就是有意思的部分了:如果我们可以在迭代器的接口上实现各种函数式编程的操作(如 filter, map ),那我们能否在状态机的接口上实现同样的操作呢(只不过是以 push 的视角)?答案是肯定的。

  当然,Rx 框架也可以看作一种实现,只不过我认为它太复杂了,还不如我自己实现。

  下面是我用 Go 语言实现的简易 Pusher 及其 filter 定义:

type Pusher[T any] interface {
  Push(T)
  Flush()
}

type PusherFilter[A any] struct {
  next   Pusher[A]
  filter func(A) bool
}

func NewPusherFilter[A any](f func(A) bool, next Pusher[A]) *PusherFilter[A] {
  return &PusherFilter[A]{next, f}
}

func (t *PusherFilter[A]) Push(a A) {
  if t.filter(a) {
    t.next.Push(a)
  }
}

func (t *PusherFilter[A]) Flush() {
  t.next.Flush()
}

  使用:

pusher := NewPusherFilter(func(item *Item) bool { return productSet[item.Product] }, files)

  这种东西有什么用?我们通常更习惯使用迭代器和基于迭代器的函数式操作,但很多时候,设计一个基于迭代器的流式接口可能是很麻烦的。考虑一个例子:遍历文件目录树,流式地返回每个文件的信息。如果一定要做成迭代器,调用一个函数才返回下一个文件信息,那我们不得不手动构造一个栈(stack)来保存中间状态,程序写起来并不直观。

  但如果我们用状态机来设计这个东西的接口又如何?我们可能会得到如下接口:

void walk_dir(string dirname, Pusher[FileInfo] pusher);

  这样做的好处是让 walk_dir 可以用递归来实现。这里的 pusher 就是一个状态机,你也可以把它近似地看成一个回调函数,但跟回调函数的不同之处在于,Pusher 是可组合(composable)的!我们可以通过 map 、filter 等组合子(combinators)构造出新的匿名 Pusher 。这种可组合性(composability)跟迭代器如出一辙。当然也有不好的一面:失去了迭代器的可以在中间任意位置停下来的能力。

  回到 Go 语言本身,由于 Go 语言的迭代器接口还遥遥无期,我这里提出的 Pusher 是一种围魏救赵的办法。关于 pull 和 push 的对偶性(duality),也可以参考 Go 语言核心开发者 rsc 的讨论:user-defined iteration using range over func values · golang/go · Discussion #56413

相关文章:

筛选、编辑与网络社区

2023-08-17 00:00:00

  筛选即 Curated Computing ,也就是像苹果生态这种“所有软件只能从一个经过筛选的应用商店下载”的模式。

  编辑指 Editor ,在以前的杂志、报纸时代,人们看的东西都经过编辑编辑,编辑的权力甚至比作者还大,有时可以决定部分内容(想想《三体》和《科幻世界》杂志的编辑,再想想日本漫画杂志编辑对漫画连载内容的影响)。不过随着互联网的兴起,编辑似乎越来越被人们轻视。

  以下引用自《科技创业者不是好编辑》:

她们认为自己可以做编辑,但是又打心眼里轻视编辑。这种轻视就她们的立场而言是正确的,因为编辑就是反规模化。

还是去读真正的编辑编出来的东西吧。

  过去信息匮乏的时代,重要的是多找信息;而在如今这个信息过剩的时代,重要的是从海量的信息中筛选出真正有价值的东西。而这项工作,是可以外包的,因为别人做得可能比我们自己做得更好,而且可以节省我们的时间。

  做这种工作的人就是编辑。

领域 筛选 不筛选
软件 苹果 安卓 / Windows
信息获取 杂志 网络
论坛 控评 自由发言
政治 独裁 民主

  选择不筛选就是选择自由,你可以做任何事,比如在你的设备上安装任何软件,但你也要承担相应的责任或代价:你要为自己设备的安全负责。下载到流氓软件?中勒索病毒?微软公司会告诉你:你要自己负责,至少学习一下如何安全使用计算机。但苹果公司告诉你:交给我们吧,你只能用我们挑选过的这些应用,但你不必再劳心费神地负责电脑的安全了。我相信这对很多人来说是一种理性的选择,可能也是更好的选择。

  以前我更相信自由,现在我明白,很多时候,限制自由可能是更好的选择。当然前提是,你有选择筛选或者不筛选的自由。

  再比如网络文章,理论上你可以看网络上所有的东西,但实际上你能做到吗?所以我们一定要有某种筛选机制。这种信息的筛选可以由人——编辑来完成,也可以由机器——推荐算法来实现。

  现在流行的信息流推荐,背后隐含的假设是,编辑这项工作,推荐算法可以做得比人更好。但经过这么多年对网络的观察,我认为,这个假定可能是不正确的。

  很多基于算法推荐的网站让我感觉推荐的内容质量很差,所以我还是更倾向阅读博客或听播客,也就是让另一个人来告诉我上网应该去看什么、了解什么,而不是推荐算法。

  编辑是有观点(不中立)但有品味的,每个人都有自己的个性,你能从编辑筛选出来的文章上学到一个人的品味和个性。而推荐算法在我看来是无情的、“中立”的,“客观”的、没有观点,同时也没有品味。

  再比如网络社区建设:让所有人都自由地发言,就能够自发出现一个良好互动的社区吗?这很可能会让社区变成人们倾倒情绪垃圾的“垃圾场”,比如某著名网络垃圾场,所以我认为一定程度的管理还是有必要的。

  另一个例子:我发现 QQ 音乐有一些歌是 VIP ,另外一些不是。如果这些 VIP 歌曲是经过 QQ 音乐内部的人工标注,这何尝又不是一种筛选呢?

寻找中间地带

  汉娜·阿伦特在她的《人的境况》中提出了著名的劳动、工作、行动三分法,这启发了我:很多时候事物不是两分的,而可能是三分的。

领域 筛选 中间地带 不筛选
软件 苹果 社区软件仓库,如 F-Droid, Chocolatey 用户自由下载安装软件
信息获取 官媒 民间杂志 网络
论坛 站长控评 版主自治 自由发言
政治 独裁 民主 无政府主义

  这里推荐一下我最近看到的一篇文章:The Federation Fallacy

  这篇文章告诉我们,从电子邮件、BT 下载,再到最近的长毛象,每次人们都心怀“去中心化之梦”创建一种社区,但每次都失败了。最后的结果证明,互联网的中心化是不可避免的。似乎人们从来没能从历史中学习到经验教训。

  原因在于,所谓“去中心化”的理想固然美好,但对每个人的技术要求未免太高。

  每个人理论上都可以部署自己的电子邮件服务器,但现在有多少人在用自己的邮件服务器收发邮件?

  每个人理论上都可以部署自己的长毛象实例,但又有多少人有技术能力去维护它?

  BT 下载对技术能力的要求够低了吧,只要打开一个软件一直挂着就行。为什么一样衰落了?(提示:做种需要带宽、磁盘、电费、法务)

  所以作者在文章中提出,也许现实中更可行的,并不是完全的去中心化,而是中心化的网站 + 民主的治理机制。作者举的例子是维基百科。我认为广义上也包括各种维基(比如 Fandom)。

  我能想到的例子是过去的论坛有站长,站长可以为每个版选择版主,论坛坛友也可以自荐或投票产生版主。

  其他例子包括众包做 minecraft 地图、开源社区管理等等。

总结

  世界上不只有两条路,还有第三条路等待我们去发现。

P.S. 有什么不错的网络杂志?我认为作为一本面向程序员的杂志,阮一峰的网络日志就不错。关键是它的创作机制:可以由读者投稿,再由编辑挑选。有人说阮一峰的东西深度不够,但我认为,作为编辑,阮一峰的工作是不错的。

谈谈我最近的编程语言选择

2023-08-13 00:00:00

  说起来你可能不信,我最近在做个人项目的时候,放弃了 Ruby 和 F# ,而选择 Go 语言。下面我将说明这样做的理由。

缘起

  首先我确实需要一门带 GC 的语言来做一些 fast prototyping 式的开发。我对这门语言的期望是,可以快速开发,有一定表达力。

  之前我的快速开发首选语言是 Ruby 。但我最近越来越发现 Ruby 的问题:

  1. 社区越来越不活跃了,似乎人们在慢慢离开 Ruby ,很多 gems 不怎么维护了
  2. 作为动态类型语言,写出来的代码的长期可维护性不太好

  所以我希望找一门静态语言。我一开始选择了 F# 。因为:

  1. 函数式,有强大的类型系统,非常强的表达力
  2. 背靠 .net 社区,再不济还有微软撑着,标准库不会差到哪里去

  当时当我真正开始写程序的时候,我查了 .net 的文档,尤其是实现 http server 的 HttpListener 。我发现 F# 的问题是:标准库跟 C# 共享一套,而 C# 的标准库基本上是学 Java ,充斥着 OOP 设计。他们并没有利用 F# 的函数式特性专门为 F# 设计一套至少涵盖 IO 、 HTTP 、 JSON 的标准库。所以我开始转向 Go 。

重要的是标准库啊,混蛋!

  如果我写 F# ,我要忍受的就是这充斥着面向对象遗毒的标准库。而如果我写 Go ,我要忍受的是这简陋不堪的语法。

  两相其害取其轻,我更愿意忍受 Go 的语法(何况 Go 1.18 还支持了泛型)。Go 的语法虽然简陋,但够用。

  对我来说,Go 的加分项是它的标准库采用可组合接口(composable interfaces)的设计,我认为非常漂亮,我自己写 C 代码的时候,常常参考 Go 标准库的设计。

为什么面向对象有毒(或:我为什么更喜欢胖指针)

  任何实用的程序不可能没有抽象,动态多态(polymorphism)的抽象方法一般有两种:

  1. 面向对象、继承、封装。一个类必须声明自己实现了哪些接口。实现上采用虚表(vtable)。代表语言有 C++、Java、Python、C#
  2. 可组合的接口。一个类不需要声明自己实现了哪些接口。类实现某个接口的代码跟类的代码可以是分开的。实现上采用胖指针(fat pointer)。代表语言有 Rust、Go、Haskell(不知道类型类算不算)

  还有一个比较特殊的 C 语言,这两种范式都能支持,不过都要自己模拟实现。

  如果你的程序需要动态分派(dynamic dispatch),你必然需要为抽象付出某种代价。重点是,这个代价是由哪部分代码来支付?

  在面向对象的语言中,代价由实现接口的类定义来支付。其形式就是虚表。例如,C++ 中的每个对象,如果继承了某个带 virtual 方法的基类,就会在对象的开头多出一个指向虚表的指针。

  而在可组合接口的语言(范式)中,代价是由接口的使用方来支付的。使用方需要两个指针,一个指向对象,一个指向虚表。

  举个例子,很多时候我们都会遇到一个任务:如何将任意对象转为字符串?

  假设我们定义了一个接口 Stringable 表示可以转为 String:

interface Stringable {
    string toString();
}

  在传统的面向对象的编程语言中,每一个我们希望它可以转为字符串的类,比如 Integer 、Date 等,都需要实现这个接口。所以我们不如搞出一个超级基类,然后在这个超级基类上定义一个 toString 方法(想想 Java 的 Object.toString)。

  但问题在于,这种设计不具备可扩展性。如果以后出现了 JSON 、 BSON 或者其他某种序列化格式,难道每种格式都要往这个基类上添加一个方法吗?很多时候基类定义在基础库中,我们不可能修改它的定义。

  这个问题的另一种解决方案是,不要折腾定义方了,我们折腾使用方。

  比如我定义了一个 Date 类,上面根本没有什么 toString:

class Date {
    int year, month, day;
}

  但是在使用这个 Date 的地方,传入两个指针:一个是 Date 对象本身,另一个是 Date_toString ,即将这个对象转为 String 的函数(指针)。

void printDate(Date d, func dateToString);

  这里的 dateToString 是一个函数,它的使用方法是,传入一个 Date 对象,返回一个 String 。

  这样,对象和接口就分离开了,以后如果需要 toJSON 、 toBSON ,也不用修改原始的 Date 对象的源代码,可以把这些代码放在新的模块。

  面向对象的核心理念是:数据和相关的操作应该绑定在一起。但从这个例子我们可以看到,在很多情况下,数据和操作应该是分离的,强行绑定在一起会增加不必要的耦合。从我自己的编程实践来看,对于一些高层次的模块,数据和操作分离的话可以让代码更容易复用。

  也许有人会说:用 Java 也可以这样写程序啊。但是 Java 的整个标准库都是围绕面向对象来设计的,已经积重难返。而 Go 语言没有历史包袱,标准库是完全围绕可组合接口来设计的,所以我认为 Go 的标准库非常值得学习。

为什么我认为 Go 的错误处理不难用

  网上经常能看到的对 Go 的另一个抱怨是错误处理不好用。我认为 Go 的错误处理相比异常,使用体验上其实差不多。

  而且在使用 Go 的过程中,我对错误处理又有一些新理解。

  我们可以把错误分成 3 类:

  1. 用户输入错误(包括 URL 参数、配置文件等输入),这类错误需要返回、展示给用户。这类错误最好不要用编程语言内置的错误类型来表示,而是用自定义的类型,比如一个 struct ,或者最简单的一个 string ,又或者用类型中的空值来表示错误,例如 ““, -1, nil 等

  2. 程序错误。又可以分成 2 类:

    1. 意料之外的、不可恢复的错误。这类错误最好的处理方法是 fail-fast ,打印一个调用栈之后退出
    2. 程序员意料之内的,可以恢复或者重试的错误。比如网络错误。这类错误用语言自带的机制(如 Go 的 error 或其他语言的异常)来表示,可以在函数之间返回、传递、保存

  看到了吗,我认为只有最后那种错误才适合用 error ,其他错误要么用自定义类型,要么直接 fail-fast 。如果用这种思路来处理错误,我认为 Go 的“将错误作为值”的方式并不难用。

  你要做的是更多地使用如下代码片段来 fail-fast:

func Ok(err error) {
    if (err != nil) {
        panic(err)
    }
}

总结

  选择个人项目的语言其实是很私人的事情,重点是,你能从使用这门语言的过程中学习到什么。我从 Go 语言学习到的主要是如何使用可组合的接口设计标准库,这对我来说很有帮助,这就足够了。

P.S. 因为同样的原因,我的个人项目的“重型语言”也从 C++ 转向 C 了。

P.S.2 现在甚至我的一些 bash 脚本都开始用 Go 写了,用 Go 写这类运维脚本的优势在于:可以很容易地利用多核并行。

相关链接

相关文章