MoreRSS

site iconRamsay Leung修改

软件工程师,蚂蚁金服 - 微信 - AWS,使用Emacs 与Linux 
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Ramsay Leung的 RSS 预览

软件工程师的软技能指北(六):谈薪篇

2025-02-18 07:06:00

1 目录

  1. 软件工程师的软技能指北(一):总览篇
  2. 软件工程师的软技能指北(二):事业篇
  3. 软件工程师的软技能指北(三):高效交流篇
  4. 软件工程师的软技能指北(四):简历篇
  5. 软件工程师的软技能指北(五):面试篇(暂时跳过, 后面再填)
  6. 软件工程师的软技能指北(六):谈薪篇

2 前言

打了这么多年工,要说最后悔和最遗憾的是什么,「拿到 Offer 之后没有好好和 HR 谈薪资绝对算是(甚至没有之一)」。

如果说面试前的简历准备,刷题,面试中应付面试官的各种提问以及面试之外的学习与积累是一场马拉松比赛的话,那么拿到 Offer 就意味着你已经成功完成征程的前九十九步了,而商谈薪资待遇就是最后一步,亦是最关键的一步。

如果这一步没有做好,前面的九十九步无论多完美都等于白费了,可谓是令人痛惜。

谈得成功的话,可能一年工资就多了一辆车了(至于是五菱MINI 还是保时捷911就看实力了)。

3 认知

回想我之前曾经关于「谈薪资」的错误认知,可谓是非常经典,可惜当初没有师长可以为我指正。

3.1 学生思维

刚毕业时的「这是家大公司,我是来学东西的,开什么薪资都可以」, 这个可以算是典型的「学生思维」,甚至出发点就是错的。

工作的根本目的就是「为了赚钱养活自己,过上更好的生活」,而不是去企业里面学习,「学习新本领」也只是「打工赚钱」的副产品而已。

经历过各种残酷的「末位淘汰」后我就发现,企业招「应届生」进去也不是因为看中你潜力大啥的,而是需要你这个年龄段的员工, 企业也有各种政府下达的就业指标,而招你进去也不是让你学东西的,而是让你干活的。

最明显的佐证就是,我所经历过的大公司每年的「末位淘汰」会有很多的应届生被打低绩效, 毕竟刚出校门的学生在能力,经验等方面是肯定是不如老员工的。

「末位淘汰」从来都不是激励你学习的,而是让你多干活的。

既然公司招聘员工是为了公司利益的最大化,员工自然就要最大化自己的利益。

3.2 心理负担

不要把公司 拟人化 ,不要觉得和公司「谈判」不好,因此产生心理负担,公司只是众多利益相关的人组成的共同体而已。

3.3 不愿意承担风险

诚然, 现在整体就业大环境肯定不如2010-2020年的黄金十年,各种裁员降薪的消息层出不穷, 但如果持有「能找到个工作就不错了,还谈什么薪资」的认知,可能很难去HR谈薪资, 毕竟你潜意识就已经选择接受 Offer, 除非是 HR 的报价是远低于你的底线.

须知,收益永远是风险挂钩的,你永远不可能有高收益,零风险的投资标的。

3.4 默认公平幻想

「工资由能力决定,相信公司会公平对待,肯定可以拿到对应的薪资,不需要自己去谈」

我曾经也持有种这样的「幻想」,以为凭借面试的表现折服了面试官,他自己会帮我争取, 甚至给我一个好的薪资.

首先,在较大的公司,面试官都是无法决定或者甚至知道薪资的,会有专门的 HR 团队来和你的谈判薪资的;

其次,你也很难期望面试官能把技术面试的结果如实呈现给非技术出身的 HR.

总而言之,「有枣子没枣子打一棒就好了」,争取了不一定要有,但是不争取绝对不会有人给你加工资的.

4 风险收益分析

我用博弈论模型来简单分析下分析公司和求职者的策略互动过程中风险与收益, 用收益矩阵来表示双方的选择和收益。

假设公司有三种策略:

  1. 接受谈薪(给更高薪资)
  2. 拒绝谈薪(维持原 Offer)
  3. 撤回 Offer

求职者也有三种策略:

  1. 不谈
  2. 温和谈判
  3. 强硬谈判

分析策略:

  • 不谈薪(保守策略)
    • 公司不会撤回 Offer,但求职者没有额外收益。
  • 温和谈判(合理博弈)
    • 60% 机会加薪 5W,40% 机会薪资不变,公司很少会撤回 Offer(5%)。
  • 强硬谈判(高风险高回报)
    • 50% 机会加薪 10W,30% 机会薪资不变,但 20% 可能失去 Offer。

收益矩阵:

公司接受 (+5W) 公司拒绝 (0W) 公司撤回 (-5W)
不谈 (30W) 30W 30W 30W
温和谈判 (35W) 35W(60%) 30W(40%) 30W(5%)
强硬谈判 (40W) 40W (50%) 30W (30%) 5W (20%)

纳什均衡:

  • 如果求职者选择温和谈判,公司最优策略是接受或拒绝,而非撤回 Offer,因为撤回 Offer 也意味着公司损失招聘成本。
  • 如果求职者选择强硬谈判,公司可能更倾向于撤回 Offer,因为不愿承担过高的薪资成本。
  • 温和谈判是最优策略,因为它在风险和收益之间达到了较好的平衡, 既有提升空间,又降低 Offer 撤回风险.

也就是对于求职者来说,如果你选择谈判,谈判失败,你的结果大概率也是接受原 Offer而已,而一旦谈判成功,你将会获得额外的收益。

可以说在接 Offer 谈判时 风险低, 收益中, 甚至高 , 而不谈薪是最保守的策略,但也是收益最低的.

5 公司的策略

公司的终极目的肯定是为了创造营收,而在招聘中直接目的是希望可以招到想要的,能干活的员工。

每个职位肯定都是有级别,不同级别有不同的工资范围,但是级别只是定义了上下限,拿到上限和下限之间可谓有天渊之别。 而且,绝大部分职位,都不会给上限工资,人人都给上限,工资预算肯定超标了。

HR 给你 offer 数字之前,要考虑几个因素:

  1. 公司招人有预算,能用低价招到的,就肯定不会出高价
  2. 你可能会讨价还价,所以不能直接给上限,要留有提价的余地
  3. 如果工资太低,你可能一怒之下就拒了。

6 求职者的手牌

虽然求职者与公司谈判过程中处于相对弱势地位,但是这并不意味着我们完全没有牌可打,所以我们要利用好手上的牌。

6.1 公司内部的助力

说起来可能难以置信,虽然求职者可能与公司的人还毫无联系,但是求职者会在公司内有相当的助力,因为基本上你接触到的公司每个人都是想你接 offer 的.

对于公司来说,在你身上已经花费了相当的成本,HR 筛选简历,安排面试官面试,2-4轮的面试,都要花费面试官与HR时间的,公司付给员工工资,这些时间都是要算钱的。

而一旦你拒绝,这些花费的时间成本都会成为沉没成本。

对于 HR 来说,她们也是有考核指标的,招聘人数也是她们的考核指标,所以她们也有强烈的动机尽快招到人。

而对于招聘你的团队老板来说,肯定是希望赶紧招到人来干活的,工资多少他不关心,反正出的又不是他的钱。

所以这些人都是希望你能接 offer 的潜在助力。

6.2 多拿Offer

拿到多个的 offer,这个会成为你和 HR 谈判的最大底气,你就不会担心谈崩了,这家给不了,了不起去下家;如果多个公司愿意相互竞价,你就能渔翁得利,利益最大化。

对于公司来说,肯定是希望招到能力强,能干活的候选人,但是怎么去量化能力强呢,即便候选人通过了面试,也不能说明他们能力一定能胜任。

但是如果你能通过多个公司的面试,对于HR来说,相当你额外再通过了十几二十轮的面试, 从概率来说,你是没能力的候选人的机率大大降低,既然大家都说你好,那么他们就有更强烈的意愿与你洽谈。

6.3 薪资是底牌

上文提到,公司的策略是希望以尽量低的薪资来招到人,那么两个非常重要的参考标准:

一个就是别的公司给你开的工资,另外一个就是你现在的工资,

假如你现在的工资是20W 一年, 那我就只给你加个20%, 最多给加个30%, 既然你20W都愿意干,为什么我要开高价.

所以你现在的薪资就是你的底牌, 千万不要给人看到, 不然就定死上限了。

不然你拿了一次低工资,后面次次跳槽都要拿低工资,凭什么呢?真的是闻者伤心。

所以HR问你的时候,不要正面回答,尽量模糊化,以此争取最大利益。

比如HR换个问法,问你的预期薪资是多少?

这个问题非常坑,相当于给自己划定上限,如果你回答30W, 她就绝对不可能给你40W, 你可以回答说:我心里没个具体数字,但我对任何有竞争力的 offer 持开放态度.

一定要给的话,就给个模糊的范围, 比如阿里P7的水平吧。

7 谈判的手段

7.1 表达强烈的兴趣

“你想加入这家公司"这个前提是一切谈判的基础。

如果你表现出对加入这家公司兴趣寥廖, 那么即使你强如 Linus Torvalds, 也不会有HR会想和你谈薪资的,因为这个注定是不会有结果,她们当然也不会浪费时间。

所以你需要表示出对这家公司有强烈兴趣,无论是文化,工作方式,业务或者技术栈都与你非常契合,而你加入公司目前唯一的障碍就是没有拿到满意的薪资,这样HR才会非常乐意来为你争取。

你总要创造些条件给 HR, 她们才能为你争取更多。

所以无论你是否对这家公司感兴趣,起码你在谈薪资阶段都要表现出强烈的兴趣。

7.2 少说多听

求职者在谈薪资过程中是处于相对弱势的地位的,所以在谈判过程中要多听少说, 因为你的底牌不能被她们套出来,说得越多,不经意间漏出来的可能就越大。

而在电话中,当对方HR 试探性给出一个报价后,你都不要太快回应或者表达出来,因为对面的大概率也是谈判的老手,如果你表现得很欣喜, 那么她们就知道这个已经比你的底线要高了.

所以当听到报价之后,你可以适当地保持沉默,表示犹豫,然后让她们进一步加码, 即使她们不加码,你也用沉默表达了对报价的不满意.

7.3 多个公司竞价

如果只向一个公司谈 offer, 最多两轮,可能就到了摊牌的时刻了。

只有能让多个公司竞价,你才能让你的「未来雇主们」卷起来,实现利益最大化, 另外其他公司的报价也可以成为的谈薪资的底价.

比如你工资是20W, 你想要30W, 公司的HR就会挑战你,说你这30W 太高了,他们给不来, 其实也是换种说法说你不值30W.

但是如果A公司给了你30W, 那么你就有底气问 B 公司要高于30W 的薪资, 毕竟这是市场对你的认可.

假设B 公司给了你35W, 那么你就可以回头向A 公司说,你拿到个35W 的offer, 让A公司提高下标准, A 公司可能给到个37W,

然后你再回到 B 公司说:

我真的很想加入贵公司,和你们一起工作,只是A公司给了37W, 着实很为难,我真的很加入你们, 你们可以再匹配一下嘛?

B 如果能给到40W, 再把同样的话和A 说一次, 两家公司工作竞价两次, 就相当于谈了4次了.

如果手握两家以上公司的 offer, 那么这个可以再用到其他公司身上,所以这个就要求你要在差不多的时间拿到全部的 offer.

7.4 不要当场答应

无论 HR 给你的报价多么诱人,又或者这个是你梦寐以求的公司,都不要当场答应下来, 万一其他公司给你更好的报价呢,当场答应而后又反悔给人的印象太不好了, 要给自己留有余地。

如果 HR 要你马上给出答案,你也可以这样回应:

我非常珍惜贵公司提供的这个机会,我也非常希望加入贵公司,但是我意识到这个机会会对我的职业生涯产生重大影响, 我希望可以和家人商量之后再下决定。

8 总结

几轮谈薪之后, 你可能拿到满意或者仍然不满意的报价,你就可以根据自己的诉求选择公司了。

你不一定会选钱最多的,可能你还会考虑公司前景,职业前景,工作强度等因素,但是起码选择之后你不会因没有为争取更高的收入进行谈判而后悔。

此外,这个可能是你在这家公司最容易调薪的一次了。

一旦你加入公司之后,你就会受限于绩效,级别,部门业绩等各种限制,再也不会有打几通电话, 发几封邮件就能涨薪的美事了。

所以,从这个角度来说,谈薪资的电话可能是你最应该打,且性价比最高的电话(大概率也没有之一)

9 参考

重新造轮子系列(一):从0开发单元测试框架

2025-02-17 14:27:00

1 前言

单元测试的重要性无须多言,它是保证项目质量的基石.

如果没有单元测试,根本没有信心说自己开发的功能是符合要求的,更没法在没有测试的保证进行项目的重构。

既然单元测试如此重要,今天就用Typescript来写一个简单但五脏俱全的单元测试框架。

2 历史

Javascript 比较流行的测试框架是 MochaJest , Java 具有统治地位的单元测试框架就是 JUnit, 现在做单元测试的框架, 一般称为 xUnit 家族, 而 xUnit 家族最早的成员, 不是 JUnit, 而是 SUnit(Smalltalk Unit), SUnit 的历史比 Junit 悠久得多, 大约在1994年的时候, Kent Beck, 也就是 Junit 的作者之一, 写了 SUnit, 而后才有了 JUnit (1998).

所以, 在 SUnit 的网站上, 极其显摆的写着”一切单元测试框架之母” (The mother of all unit testing frameworks).

事实上这是大实话 — 所有单元测试框架里面的名词术语, 都从 Sunit 来的, 如 TestCase, Fixture 等等.

3 实现

3.1 需求

先定义需求, 一个单元测试框架应该可以做到下面的事:

  1. 找到包含测试的文件
  2. 找到上述文件的测试 case
  3. 运行测试case
  4. 捕获测试运行结果,并输出所有的测试的运行总结

3.2 原型

一条 assert 语句就可以看作是最简单的测试 case, 对于测试case, 我们会有以下三种结果:

  • Pass: 运行成功, 测试结果与预期一致
  • Fail: 运行失败, 测试结果与预期不一致
  • Error: 运行测试过程中出现错误,我们不确定测试结果是否与预期一致

我们用以下的状态机来判断测试的结果:

我们把要实现的单元测试框架命名为 Hope, 根据上面的状态机,我们很快就可以写出一个原型:

单元测试用例接收一个函数作为参数,然后又集中运行所有的测试用例,并根据是否抛出异常以及异常的类型来判断结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import assert from 'assert';

const HopeTests: [string, () => void][] = [];
let HopePass = 0;
let HopeFail = 0;
let HopeError = 0;

// Record a single test for running later.
const hopeThat = (message: string, callback: () => void) => {
    HopeTests.push([message, callback]);
}

const main = () => {
    HopeTests.forEach(([message, test]) => {
        try {
            test();
            HopePass += 1;
        } catch (e) {
            if (e instanceof assert.AssertionError) {
                HopeFail += 1;
            } else {
                HopeError += 1;
            }
        }
    });

    console.log(`pass ${HopePass}`);
    console.log(`fail ${HopeFail}`);
    console.log(`error ${HopeError}`);
}

让我们编写点代码来测试下我们的「单元测试框架」:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Something to test(doesn't handle zero properly)

const sign = (value: number) => {
    if (value < 0) {
        return -1;
    } else {
        return 1;
    }
}

// These two should pass
hopeThat('Sign of negative is -1', () => assert(sign(-3) === -1));
hopeThat('Sign of positive is 1', () => assert(sign(10) === 1));

// This one should fail.
hopeThat('Sign of zero is 0', () => assert(sign(0) === 0));

// This one is an error.
hopeThat('Sign mispelled is erorr', () => assert(sign(sgn(1) === 1)));

// Call the main driver
main()

输出的结果是:

1
2
3
4
-> npx tsx dry_run.ts
pass 2
fail 1
error 1

我们的第一版单元测试框架 Hope 能正常运行了,不过它有几个问题:

  1. 它只是输出结果,但没有告诉我们是哪个单元测试成功了,哪个失败了,哪个报错,没法 debug
  2. 可变全局变量通常是有很大副作用的,我们应该把它封装起来
  3. 如果我们要测的函数里面,预期是要抛出 assert.AssertionError, 那么这个函数对应的测试用例就会被识别成失败的测试用例,也就是意味着我们不应该依赖 assert.AssertError 来作运行结果判断。

3.3 单例版本

我们可以将上面的测试代码地址封装在一个类里,然后通过单例设计模式(Singleton pattern)来确保只初始化出一个实例,这样就可以模拟出全局变量的效果,以此来解决前面的两个问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import assert from "assert";
import caller from 'caller';

class Hope {
  private todo: [string, () => void][] = []; // 记录所有需要运行的测试case.
  private passes: string[] = [];
  private fails: string[] = [];
  private errors: string[] = [];
  constructor() {
    this.todo = [];
    this.passes = [];
    this.fails = [];
    this.errors = [];
  }

  test(comment: string, callback: () => void) {
      // 通过caller 获取单元测试用例对应的文件名
    this.todo.push([`${caller()}::${comment}`, callback]);
  }

  run() {
    this.todo.forEach(([comment, test]) => {
      try {
        test();
        this.passes.push(comment);
      } catch (e) {
        if (e instanceof assert.AssertionError) {
          this.fails.push(comment);
        } else {
          this.errors.push(comment);
        }
      }
    })
  }
}
export default new Hope()

上面的代码又是如何实现单例模式的呢?依靠的是 Node 的两个运行机制:

  1. 在加载一个 module 的时候, 它就会解释并执行 module 的代码,这意味着它会运行 new Hope() 并且导出新创建的实例
  2. 那么是否意味着,每个 import 语句都会运行一下 new Hope() 呢? 并不是,Node会缓存导入的 module ,也就是说无论一个 module 被导入多少次, 它也只会执行一次代码。

只要导入 hope.ts 之后, 就可以使用 hope.test() 会注册单元测试用例,以便后续执行:

最后, 我们只需要再实现下输出测试结果的功能,既支持输出一行的简短结果,又可以支持详尽的输出. 如果需要的话,后续还可以支持输出JSON, CSV, 或者HTML 格式的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
terse() {
  return this.cases()
    .map(([title, results]) => `${title}: ${results.length}`)
    .join(' ');
}

verbose() {
  let report = '';
  let prefix = '';
  for (const [title, results] of this.cases()) {
    report += `${prefix}${title}:`;
    prefix = '\n';
    for (const r of results) {
      report += `${prefix} ${r}`
    }
  }
  return report;
}

cases() {
  return [
    ['passes', this.passes],
    ['fails', this.fails],
    ['errors', this.errors]
  ]
}

万事具备,接下来就让我们写个函数验证下 Hope 框架:

1
2
3
4
import assert from "assert";
import hope from "./hope";

hope.test('Sum of 1 and 2', () => assert((1 + 2) === 3));

看起来挺不错,但是要怎么运行这个测试case 呢? 总不能每个测试文件都调用下 hope.run() 嘛? 人家 Jest 都可以自动扫描并运行测试用例。

让我们参考 Jest, 实现一个 Runner, 也实现动态加载测试文件.

import 不仅可以用来导入其他的模块,它可以当作是一个 async 函数,加载指定路径的文件, 如:

1
await import(module_path);

为了更好地控制我们的单元测试, 我们可以给 Hope 框架增加上一些命令行参数以控制其行为, CLI + Runner 的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import minimist from 'minimist';
import { glob } from 'glob';
import hope from './hope';
import { fileURLToPath } from 'url';

const parse = (args: string[]) => {
  const parsed = minimist(args)

  return {
    // Default root directory is current directory if not specified
    root: parsed.root || '.',

    // Output format can be 'terse' or 'verbose' (default)
    output: parsed.output || 'verbose',

    // Array of test filenames if explicitly provided
    filenames: parsed._ || []
  }
}

const main = async (args: Array<string>) => {
  const options = parse(args);
  if (options.filenames.length == 0) {
    options.filenames = await glob(`${options.root}/**/test*.{ts,js}`);
  }

  for (const f of options.filenames) {
    const absolutePath = fileURLToPath(new URL(f, import.meta.url));
    await import(absolutePath);
  }
  hope.run()
  const result = (options.output === 'terse') ? hope.terse() : hope.verbose();
  console.log(result);
}

main(process.argv.slice(2))

我们默认会匹配所有以 test 为前缀的 ts 和 js 文件, 然后通过 import 导入, 因为 hope 是单例模式,所以所有的测试文件用的都是同一个实例, hope.run 就将注册的所有单元测试运行.

整个框架的工作流程如下:

大功告成,现在就来运行下我们的单元测试:

1
2
3
4
5
> npx tsx pray.ts
passes:
 file:///private/tmp/reinvent/unit_test/test_add.ts::Sum of 1 and 2
fails:
errors:

3.4 优化

3.4.1 增加运行时间

我们还可以记录每个测试用例的运行时间, 纳秒有点太小了,就精确到微秒即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
run() {
  this.todo.forEach(([comment, test]) => {
    try {
      const now = process.hrtime.bigint();
      test();
      const elapsedInMicro = (process.hrtime.bigint() - now) / (BigInt(1000));
      this.passes.push(comment + `, execution time: ${elapsedInMicro}us`);
    } catch (e) {
      if (e instanceof assert.AssertionError) {
        this.fails.push(comment);
      } else {
        this.errors.push(comment);
      }
    }
  })
}
1
2
3
4
5
> npx tsx pray.ts
passes:
 file:///private/tmp/reinvent/unit_test/test_add.ts::Sum of 1 and 2, execution time: 5us
fails:
errors:

3.4.2 增加 assert 函数

内置的 assert 函数只支持比较输入值是否为 True, 现代的测试框架都有很多的 helper 函数来简化 assert 语句,就让我们来实现下 assertEqual, assertThrows, assertMapEqual, assertSetEqual, assertArraySame 这几个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * assert 抛出指定的异常
 */
export function assertThrows<T extends Error>(expectedType: new (...args: any[]) => T, func: () => void) {
    try {

        // expected to throw exception
        func();
        // unreachable
        assert(false, `Expected function to throw ${expectedType.name} but it did not throw`);
    } catch (error) {
        assert(error instanceof expectedType, `Expected function to throw ${expectedType.name} but it threw ${error instanceof Error ? error.constructor.name : typeof error}`);
    }
}

/**
 * assert 两个元素相等
 */
export function assertEqual<T>(actual: T, expected: T, message: string) {
    assert(actual === expected, message);
}

/**
 * assert 两个 Set 相同
 */
export function assertSetEqual<T>(actual: Set<T>, expected: Set<T>, message: string) {
    assert(actual.size == expected.size, message);
    for (const element of actual) {
        assert(expected.has(element), message);
    }
}

/**
 * assert 两个 Map 相同
 */
export function assertMapEqual<K extends string | number | symbol, V>(actual: Record<K, V>, expected: Record<K, V>, message: string) {
    const actualKeys = Object.keys(actual) as K[];
    const expectedKeys = Object.keys(expected) as K[];

    assert(actualKeys.length === expectedKeys.length, message);
    for (const actualKey of actualKeys) {
        assert(expected[actualKey] && actual[actualKey] == expected[actualKey], message);
    }
}

/**
 * assert两个列举的值相等,如元素相等,但是顺序不同也被视为相同
 */
export function assertArraySame<T>(actual: Array<T>, expected: Array<T>, message: string) {
    assert(actual.length === expected.length, message);
    assertSetEqual(new Set(actual), new Set(expected), message);
}

针对上述函数的测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import assert from "assert";
import hope, { assertArraySame, assertMapEqual, assertSetEqual, assertThrows } from "./hope";

hope.test('test assertSetEqual happy path', () => {
  const setA = new Set([1, 2, 3, 4, 5]);
  const setB = new Set([5, 1, 2, 4, 3]);
  assertSetEqual(setA, setB, 'Set supposed to be equal');

  assertSetEqual(new Set([]), new Set([]), 'Empty Set');
});

hope.test('test assertMapEqual unhappy path', () => {
  assertThrows(assert.AssertionError, () => {
    const setA = new Set([1, 2, 3, 4, 5]);
    const setB = new Set([1, 2, 4, 3]);
    assertSetEqual(setA, setB, 'Set supposed to be equal');
  })
});

hope.test('test assertMapEqual happy path', () => {
  const mapA = {
    'a': 1,
    'b': 2,
  };
  const mapB = {
    'b': 2,
    'a': 1
  };
  assertMapEqual(mapA, mapB, 'Map supposed to be map');
});

hope.test('test assertMapEqual unhappy path', () => {
  const mapA = {
    'a': 1,
    'b': 3
  };
  const mapB = {
    'b': 2,
    'a': 1
  };
  assertThrows(assert.AssertionError, () => {
    assertMapEqual(mapA, mapB, 'Map supposed to be map');
  });
});


hope.test('test assertArraySame happy path', () => {
  const arr1 = [1, 2, 3, 2];
  const arr2 = [2, 1, 2, 3];
  assertArraySame(arr1, arr2, "Arrays should have same elements"); // Passe
});

hope.test('test assertArraySame unhappy path', () => {
  const arr1 = [1, 2, 3, 2];
  const arr2 = [2, 1, 2, 4];

  assertThrows(assert.AssertionError, () => {
    assertArraySame(arr1, arr2, "Arrays should have same elements"); // Passe
  });
});

3.4.3 增加 -s/–select 参数指定测试文件

我们的 Runner 默认匹配的是以 test 为前缀的测试文件, 我们可以增加一个 -s/--select 参数,用来指定需要匹配的测试文件名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const parse = (args: string[]) => {
  const parsed = minimist(args)

  return {
    ...
    select: parsed.select || parsed.s // 增加select 参数
  }
}

const main = async (args: Array<string>) => {
  const options = parse(args);
  if (options.filenames.length == 0) {
    const namePattern = options.select ?? 'test*'; // 使用传入的模式
    options.filenames = await glob(`${options.root}/**/${namePattern}.{ts,js}`);
  }

  ...
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> ls -al test*
-rw-r--r--@ 1 ramsayleung  wheel   115 17 Feb 10:01 test_add.ts
-rw-r--r--@ 1 ramsayleung  wheel   762 17 Feb 10:01 test_approx_equal.ts
-rw-r--r--@ 1 ramsayleung  wheel  1536 17 Feb 10:38 test_assert.ts
-rw-r--r--@ 1 ramsayleung  wheel   187 17 Feb 10:38 test_async.ts
-rw-r--r--@ 1 ramsayleung  wheel   275 17 Feb 10:38 test_setup_teardown.ts
-rw-r--r--@ 1 ramsayleung  wheel   140 17 Feb 10:38 test_tag.ts

> npx tsx pray.ts -s "test_a*"
passes:
 file:///private/tmp/reinvent/unit_test/test_async.ts::delayed test, execution time: 412us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertSetEqual happy path, execution time: 31us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertMapEqual unhappy path, execution time: 1175us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertMapEqual happy path, execution time: 32us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertMapEqual unhappy path, execution time: 85us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertArraySame happy path, execution time: 17us
 file:///private/tmp/reinvent/unit_test/test_assert.ts::test assertArraySame unhappy path, execution time: 54us
 file:///private/tmp/reinvent/unit_test/test_approx_equal.ts::Default margin throws exception, execution time: 111us
 file:///private/tmp/reinvent/unit_test/test_approx_equal.ts::Large margin not throws exception, execution time: 6us
 file:///private/tmp/reinvent/unit_test/test_approx_equal.ts::Relative error throw exception, execution time: 51us
 file:///private/tmp/reinvent/unit_test/test_approx_equal.ts::Default Relative error not throw exception: , execution time: 5us
 file:///private/tmp/reinvent/unit_test/test_add.ts::Sum of 1 and 2, execution time: 4us
fails:
errors:

3.4.4 增加 -t/–tag 参数按标签运行测试case

对于 hope.test 函数,我们还可以提供一个额外的参数,用于给这个test case 打标签:

1
2
3
hope.test('Difference of 1 and 2',
          () => assert((1 - 2) === -1),
          ['math', 'fast'])

然后通过 -t/--tag 按指定的tag来运行测试用例, 实现起来很容易:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
test(comment: string, callback: () => void, tags: Array<string> = []) {
    this.todo.push([`${caller()}::${comment}`, callback, tags]);
}

run(tag: string = '') {
    this.todo
        .filter(([comment, test, tags]) => {
            if (tag.length === 0) { return true; }
            return tags.indexOf(tag) > - 1;
        })
        .forEach(([comment, test, tags]) => {
            // run the test, nothing change
        })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const parse = (args: string[]) => {
    const parsed = minimist(args)

    return {
        ...
            tag: parsed.tag || parsed.t
    }

    const main = async (args: Array<string>) => {
        ...
            hope.run(options.tag);
        ...
    }

test_tag.ts:

1
2
3
import assert from "assert";
import hope from "./hope";
hope.test('Differene of 1 and 2', () => assert((1 - 2) === -1), ['math', 'fast']);
1
2
3
4
5
> npx tsx pray.ts -t "math"
passes:
 file:///private/tmp/reinvent/unit_test/test_tag.ts::Differene of 1 and 2, execution time: 5us
fails:
errors:

3.4.5 setup与teardown

正常的测试框架都是有 setupteardown 函数的,可以指定在每个测试case 运行之前或之后的函数,比如运行测试case 前的数据准备,以为运行结束时的数据清理,我们的测试框架也可以支持这个功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type CallbackType = () => void;
class Hope {
  ...
  private setupFn: CallbackType | null = null;
  private teardownFn: CallbackType | null = null;

  setup(setupFn: CallbackType) {
    this.setupFn = setupFn;
  }

  teardown(teardownFn: CallbackType) {
    this.teardownFn = teardownFn;
  }

  run(tag: string = '') {
    this.todo
      .filter(([comment, test, tags]) => {
        if (tag.length === 0) { return true; }
        return tags.indexOf(tag) > - 1;
      })
      .forEach(([comment, test, tags]) => {
        try {
          if (this.setupFn) {
            this.setupFn();
          }

          const now = microtime.now();
          test();
          const elapsedInMicro = microtime.now() - now;
          this.passes.push(comment + `, execution time: ${elapsedInMicro}us`);

          if (this.teardownFn) {
            this.teardownFn();
          }
        } catch (e) {
          if (e instanceof assert.AssertionError) {
            this.fails.push(comment);
          } else {
            this.errors.push(comment);
          }
        }
      })
  }
}

针对上述函数的测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import hope, { assertEqual } from "./hope";

let x = 0;

const createFixtures = () => {
  x = 1;
}

hope.setup(createFixtures);
hope.test('Validate x should be 1', () => {
  assertEqual(x, 1, 'X should be 1');
});

const cleanUp = () => {
  x = 0;
}

hope.teardown(cleanUp);

3.4.6 增加对 async 测试case 的支持

目前我们的test case 都只支持同步的函数, 我们可以增加上对 Promise 的支持, 这样我们可以使用以下的语法:

1
hope.test('delayed test', async () => {...})

实现方式也很直接: 一种就是判断传入函数的类型, 如果是同步函数则直接调用,如果是 async 函数, 那么就加上 await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
type SyncCallbackType = () => void;
type AsyncCallbackType = () => Promise<void>;
type CallbackType = SyncCallbackType | AsyncCallbackType;

class Hope {
    private todo: [string, CallbackType, Array<string>][] = [];
    private setupFn: CallbackType | null = null;
    private teardownFn: CallbackType | null = null;

    setup(setupFn: CallbackType) {
        this.setupFn = setupFn;
    }

    teardown(teardownFn: CallbackType) {
        this.teardownFn = teardownFn;
    }

    test(comment: string, callback: () => void, tags: Array<string> = []) {
        this.todo.push([`${caller()}::${comment}`, callback, tags]);
    }

    private async runTest(comment: string, test: CallbackType, tags: string[]) {
        try {
            if (this.setupFn) {
                if (this.isAsync(this.setupFn)) {
                    await this.setupFn();
                } else {
                    this.setupFn();
                }
            }

            const now = process.hrtime.bigint()
            if (this.isAsync(test)) {
                await test();
            } else {
                test();
            }

            const elapsedInMicro = (process.hrtime.bigint() - now) / (BigInt(1000));
            this.passes.push(comment + `, execution time: ${elapsedInMicro}us`);

            if (this.teardownFn) {
                if (this.isAsync(this.teardownFn)) {
                    await this.teardownFn();
                } else {
                    this.teardownFn();
                }
            }
        } catch (e) {
            if (e instanceof assert.AssertionError) {
                this.fails.push(comment);
            } else {
                this.errors.push(comment);
            }
        }
    }

    async run(tag: string = '') {
        const tests = this.todo
              .filter(([comment, test, tags]) => {
                  if (tag.length === 0) { return true; }
                  return tags.indexOf(tag) > - 1;
              });


        for (const [comment, test, tags] of tests) {
            await this.runTest(comment, test, tags);
        }
    }

    private isAsync(fn: CallbackType): fn is AsyncCallbackType {
        return fn.constructor.name === 'AsyncFunction';
    }
}

pray.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const main = async (args: Array<string>) => {
  const options = parse(args);
  if (options.filenames.length == 0) {
    const namePattern = options.select ?? 'test*';
    options.filenames = await glob(`${options.root}/**/${namePattern}.{ts,js}`);
  }

  for (const f of options.filenames) {
    const absolutePath = fileURLToPath(new URL(f, import.meta.url));
    await import(absolutePath);
  }

  await hope.run(options.tag); // 增加上await
  const result = (options.output === 'terse') ? hope.terse() : hope.verbose();
  console.log(result);
}await hope.run(options.tag);

4 参考

ReInvent: 重新造轮子系列(序言)

2025-02-17 14:10:00

1 起因与动机

最近在看 System Design By Example 这本书,主旨是通过设计和实现各种日常开发中常见的软件,以理解和提高系统设计(System Design)的能力。

每个章节都会实现一个软件,然后还会有大量的习题来完善这个软件,以练带学。

而我最推崇,并且认为最好的学习方法就是:费曼学习法(Feynman Technique), 其核心理念就是:

学习一种新事物最好的方法是,用你的话讲给别人听。

通过向别人清楚的解说某一事物,来确认自己是否真的弄懂了这件事。

所以说,学习最好的方式,是把你学到的东西教给别人。

因此,这个项目就是我在学习和理解 System Design By Example 这本书后,结合参考的论文和个人经验内化出来的学习成果。

也希望其他人也可以从中受益。

2 项目

GitHub: https://github.com/ramsayleung/reinvent

原书是用 node + Javascript 编写的,部分代码因API变更而变得不可用,因此本项目也做了对应修改,并将 Javascript 替换成 Typescript, 通过类型系统来降低维护成本。

  1. 单元测试框架

杂谈AI取代程序员

2025-02-15 13:15:00

1 前言

最近国产的DeepSeek 模型的爆火,让「AI取代程序员」的说法又甚嚣尘上, 比如在国内著名的程序员社区 V2ex 搜索「AI取代程序员1,可以搜索出 69627 条结果(截止2025-02-15):

以至于我的一位非程序员朋友都来关心我,问我是否有被取代的担忧。

在感谢朋友的关心之后,我也开始静下心来思考这个问题,以及形成这种想法背后可能的原因,然后就有了这篇文章。

2 AI取代程序员

在谈论AI能否真的取代程序员之前,我想先聊一下程序员的工作是什么, 毕竟定义好了问题才会分析.

2.1 研发流程

既然是程序员,那工作肯定是写代码嘛, 但是就我个人经验来说,我真正花费在写代码的时间可能连我工作时间30%都占不到.

一个新功能大体的研发流程如下: 当产品经理或者老板提出一个新需求的时候,程序员需要评估需求的可行性,然后撰写设计文档,评审设计方案,而后编码,测试再灰度上线功能,最后对新功能增加新监控告警,提供各项数据指标。

由此可知, 产品经理只会跟你说他/她想要什么(what), 但是产品经理肯定不知道如何实现这个功能的(how), 需要程序员把模糊又朦胧的问题具体化和细致化,然后再根据方案来编码。

遇到不合理的需求时,还需要告知产品经理,你的想法是不切实际的,比如经典的「根据手机壳颜色切换壁纸」需求

但是我们在使用AI的时候,更多是一个具体而明确的任务,并且是我们针对的需求已经拆分好的任务, 所以AI在做的只是将具体的设计方案翻译成代码的步骤。

这不禁让我想起自己当年在微信支付7级晋升8级的答辩项目:一个研发提效的代码生成框架。

我当时可谓信心满满,我的项目可以根据模板直接生成代码,不再需要手写CRUD的代码,以此来给工程师提效。

评委就询问我:

你有对项目开发的各个环节的耗时和阻塞时间做量化分析么?

编码是整个研发流程效能最低的部分么?

从拿到需求到开发上线,只有编码可以提效么?

评委这么一顿批头盖脸的输出,直接把我的信心给浇没了。

虽然我写的代码生成工具远不如大模型加持的AI强大,但看起来都只局限于解决编码问题,研发流程的其他问题并没有解决。

2.2 功能性与非功能性需求

在程序开发中,除了有功能性需求,还需要非功能性需求.

而功能性需求恰恰是其中最简单直接的,是满足「又不是不能用」的最基本要求。

比如我需要实现加法,这个就是功能性需求,AI就需要帮我把加法实现出来。

而所谓的非功能性需求,比较常见的是可扩展性(scalability),可维护性(maintainability) 和性能(performance).

还是以加法为例, 我可能需要后面扩展到乘法和除法, 或者扩展到复数或者矩阵加法,要怎么易于扩展呢?

这些都是AI生成代码是没有考虑到的因素,AI比较擅长的可能是给它一个需求,它生成一段代码给我们。

但是在软件研发的生命周期中,有大概80%的时间,都是维护已有的系统,谁家没有个已经在跑的系统呢。

而给已有的系统上增加功能,就需要考虑各种奇怪的兼容性,相当于带着锁链来跳舞,并不能像从无到有,什么都不用考虑的那般洒脱。

而这样的限制,又是AI生成代码时未曾考虑的。所以从零开始的日抛型,不需要考虑维护成本的项目,很适合由AI来生成.

但即使功能性的需求,有时候AI也会做不好。

我之前在工作中需要根据已有的Schema, 使用Rust 写入Parquet数据,Rust本来就新,加之操作Parquet的库就更少了, 所以ChatGPT, Claude或者Gemini 都没有给出我满意能跑的结果,最后还是靠自己去读 Parquet库的源码找出的解决办法。

无论模型怎么变,AI现阶段还需要预训练数据的投喂,所以没有相关的预处理数据,AI也只能胡扯。

所以总结下来,在真实项目中,AI还没有办法取代程序员。

2.3 AI取代论背后的动机

既然你说AI现阶段还没有办法取代程序员,为什么我看到各种各样程序员要被AI取代的新闻,甚至有公司用AI来替代员工了?

除去真的担心自己被AI取代的从业者外,我觉得看到两种扯着AI大旗的人。

一种就是懂程序开发,装不懂程序开发,然后贩卖焦虑的人,最近非常火的就是,「我用Cursor从0 xx 天开发了一个App」 (Cursor是最近一个很火的AI编辑器,可以通过输入的指令生成代码).

大体都是说自己完全不懂程序开发,借助AI/Cursor, xx天内开发出了一个App, 并且冲到了苹果App Store热门榜第一.

普通程序员看到这种说法,一个小白从无到有都能开发出一个App, 难免会焦虑,不得不说,这种反向装弱再变强真的令人防不胜防, 想着不能被落下,就斥资加入各种付费群或者购买各种课程。

神智清明的你可能在心平气和下很难做出这样的决定嘛。

另外一种就是要用AI来取代程序的公司,说要通过AI来增效,裁撤工程师,比如国外的 Workday 裁员说要再招人做 AI 的新闻 2, 还有2023年 Google 因为AI裁员而30000 名员工的新闻 3

怎么说呢,AI这块牌子太好用了, 什么都可以往里面套,裁撤工程师降本是真,AI增效大概是假。

裁员就裁员呗,为什么还要说是用AI来取代程序员呢?

因为说是AI取代了程序员,被毕业的员工们和待毕业的员工们都只会把裁员的原因归罪到AI上, 管理层的无所作为,墨守成规,决策失误,无序扩张这些失误都不需要承诺后果了,反正背锅有AI。

苦一苦员工,骂名AI来担。

3 AI一无用处

当然凡事都有例外,有人觉得AI会取代程序员,也有人觉得AI一无用处,不值一提。

我对此也是持反对态度了,虽然AI无法取代程序员,但是它的确在很多方面可以给我提升相当大的效率,其效率跃升堪比计算机连上互联网。

所以我也藉此来分享下我的AI用法

4 我的AI用法

我使用对AI的定位把它当作一位非常耐心,能力全面的助理,所以我给它分配的都是明确而繁琐的任务,并且我有能力对其输出进行评判的情景。 (也就是让它干脏活累活)

先声明一下我使用的AI 模型,日常事务我会使用ChatGPT, 响应速度快,量大够饱; 编码我会使用Claude 3.5 Sonnet,目前是我使用过效果最好的; 中文相关会使用DeepSeek, 但是经常服务器繁忙,体验不好。

4.1 生成测试用例

测试用例太适合让AI来写了,毕竟任务是非常明确的,需要被测的函数也是非常明确的,你只需要使用把测试的场景说清楚。

我觉得都可以把这个内容扩展写成 测试技能进阶系列 的第四篇了

假设我需要测试一个计算字符串中元音数量的函数 count_vowels(s: str) -> int, 我的指示是:

请为以下函数 `count_vowels(s: str) -> int` 生成pytest测试用例:

  1. 正常输入包含大小写元音的情况
  2. 空字符串边界条件
  3. 全元音字符的特殊情况
  4. 不含元音的字符串
  5. 非法输入类型校验

AI生成结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pytest

def test_count_vowels_normal_cases():
    # 混合大小写元音测试
    assert count_vowels("Hello World") == 3
    assert count_vowels("AEiou") == 5

def test_count_vowels_edge_cases():
    # 空字符串/全元音测试
    assert count_vowels("") == 0
    assert count_vowels("aei") == 3

def test_count_vowels_special_cases():
    # 无元音字符串测试
    assert count_vowels("rhythm") == 0
    assert count_vowels("12345!@#") == 0

def test_count_vowels_invalid_input():
    # 类型校验测试
    with pytest.raises(TypeError):
        count_vowels(123)
    with pytest.raises(TypeError):
        count_vowels(["hello"])

4.2 语言学习

AI的多语言翻译功能对比其他传统基于单词的翻译软件简直是降维打击bb,毕竟Google的科学家最初做 Transform 模型就是为了做翻译的。

记得我之前考雅思的时候,口语和写作考前复习就是把题库的复制给AI,然后让他来给我答题,我就把思路背下来,做题这方面,AI实在是太擅长了。

Given the following IELTS Speaking Part1, you are an examinee, could you give me some band 9 answer:

Ielts Speaking 3 Practice 5 (Change)

  1. Do you think change is good?
  2. What are some of the major changes that occur to people throughout their lives?
  3. Is your country changing rapidly?
  4. In what ways have changes in technology changed people’s lives?
  5. Why do old people not accept change?

Here are some Band 9 answers for the IELTS Speaking Part 1 topic “Change”, with a natural, fluent style and advanced vocabulary.

  1. Do you think change is good?

Absolutely. Change is an inevitable part of life, and in most cases, it drives progress and personal growth. While change can be daunting at first, it often leads to new opportunities and valuable experiences. Without change, we would stagnate, both individually and as a society. …

让AI做词典, 帮我解释和翻译我不懂的单词,或者做中译英方便交流:

You are a professional translator. Translate all input Chinese paragraphs into English and all input English into Chinese, and help me explain its meaning

而后我输入中文,它就会给我英文翻译;输入英文,它会给我中文翻译:

“爱哭的孩子有奶喝”


“The crying baby gets the milk.” Meaning:

This Chinese proverb “爱哭的孩子有奶喝” (ài kū de háizi yǒu nǎi hē) means that those who complain, express their needs, or fight for their interests are more likely to get attention and benefits. It suggests that speaking up and advocating for oneself can lead to better outcomes, similar to how a baby who cries gets fed first.

illiterate


“Illiterate” 的中文翻译是 “文盲” 或 “不识字”。 含义:

  1. 基本含义:指一个人 不识字或无法阅读和书写。

    • 例如:Many people in the past were illiterate because they had no access to education.(过去,许多人因为无法接受教育而是文盲。)
  2. 引申含义:在某个领域缺乏基本知识或技能。

    • 例如:“He is financially illiterate."(他在财务方面一窍不通。)
    • 在科技、法律、文化等方面,也可以用 illiterate 来形容缺乏基础知识的人,例如 “technologically illiterate”(科技文盲)。

还有一个就是让AI 给我写的内容纠错,这个也比传统的语法工具类如 Grammarly 好用.

You are a professional and seasoned translator, correct me if I am wrong.

4.3 代码审计

有时候我会查看一些智能合约的代码或者TG bot的代码,在实际交互前,我一般会把代码丢给AI,让它检查一下是否有可疑的代码片段,类如窃取用户的私钥,把资金转到可疑账户等等。

或者把我自己写的代码丢给AI,让它看下是否有安全漏洞:

Given the following code, help me spot if there is any vulnerability here:

1
2
3
4
5
@app.route('/search')
def search():
    query = request.args.get('q')
    cursor = db.execute(f"SELECT * FROM products WHERE name LIKE '%{query}%'")
    return render_template('results.html', data=cursor.fetchall())

Yes, there are several serious security vulnerabilities in this code. Let me break them down:

  • SQL Injection Vulnerability (Critical):

The most severe issue is that the code uses string formatting (f-string) to directly insert user input into the SQL query. This allows SQL injection attacks. For example, a malicious user could input:

Here’s a secure version of the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@app.route('/search')
def search():
    query = request.args.get('q')
    if query is None:
        return render_template('results.html', data=[])

    # Using parameterized query with placeholders
    cursor = db.execute(
        "SELECT * FROM products WHERE name LIKE ?",
        ('%' + query + '%',)
    )
    return render_template('results.html', data=cursor.fetchall())

4.4 反编译

现代前端基本都会使用类似webpack 的打包工具进行打包压缩, 甚至还会进行代码混淆,所以F12 查看源码,并不能查出端倪.

因此我一般都是让AI 来帮我反编译和还原, 这样就免去了复杂的人工转译的过程:

Given the obfuscated and compressed code, could you reverse it back to the original source code?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var _0x3f2a = ['\x48\x65\x6c\x6c\x6f', '\x6c\x6f\x67'];
(function(_0x12d3f0, _0x3f2a8c) {
    var _0x553e8f = function(_0x5e7a28) {
        while (--_0x5e7a28) {
            _0x12d3f0['push'](_0x12d3f0['shift']());
        }
    };
    _0x553e8f(++_0x3f2a8c);
}(_0x3f2a, 0x1a4));
var _0x553e = function(_0x12d3f0, _0x3f2a8c) {
    _0x12d3f0 = _0x12d3f0 - 0x0;
    var _0x553e8f = _0x3f2a[_0x12d3f0];
    return _0x553e8f;
};
function _ax342() {
    var _0x5e7a28 = _0x553e('0x0');
    console[_0x553e('0x1')](_0x5e7a28 + ' World!');
}
_ax342();

Let me help you deobfuscate this code. Based on the patterns and structure, this appears to be JavaScript code that has been obfuscated using common techniques like array-based string storage and variable name mangling. The original source code would be:

1
2
3
4
function greet() {
    console.log("Hello World!");
}
greet();

着实有点东西

4.5 排查问题

在出现大模型AI之前,当遇到某个工具或者库的错误信息时,我一般的做法是:

  1. 复制错误信息
  2. 粘贴到Google
  3. 点击排名最靠前的Stackoverflow 链接
  4. 扫一下是否是我想问的问题,如果不是就再回到2
  5. 按照最高赞的解决方案尝试下
  6. 如果还不行,再回到2

有了AI之后,我一般的做法是:

  1. 复制错误信息
  2. 问AI “How to fix this erro”, 粘贴上错误信息
  3. 按照AI的建议一步步尝试
  4. 如果不行,把最新的错误信息复制粘贴给AI, 告诉它这不管用.

与Google 的方案相比,我现在有了个再反馈的过程, 可以把最新的结果再告知AI, 这样更方便AI基于上下文做出解答.

4.6 数据处理

非结构化数据转换成结构化数据,如JSON, 相当于让AI做了数据清洗的脏活:

Given the following output of ls command, could you convert it to JSON?

1
2
3
4
5
6
7
8
9

-> % ls -alrt
total 828
-rw-rw----+   1 ramsay ramsay   4672 Mar 21  2019 abbrev_defs
-rw-rw----+   1 ramsay ramsay  10079 Mar 21  2019 recentf~
drwxr-xr-x    2 ramsay ramsay   4096 Apr 16  2021 images
drwxrw----+  44 ramsay ramsay   4096 Apr 16  2021 snippets
-rw-rw----+   1 ramsay ramsay   3740 Dec 10  2021 projectile-bookmarks.eld
-rw-rw----+   1 ramsay ramsay 241501 May 27  2022 company-statistics-cache.el

Here is the JSON representation of the given ls -alrt output: Explanation:

  • Each file/directory is represented as an object inside the “files” array.
  • “name” is the file/directory name.
  • “permissions” is the file permissions.
  • “links” is the number of hard links.
  • “owner” is the owner of the file.
  • “group” is the group the file belongs to.
  • “size” is the size of the file in bytes.
  • “modified” is the date of the last modification.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
     {
  "files": [
    {
      "name": "abbrev_defs",
      "permissions": "-rw-rw----+",
      "links": 1,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 4672,
      "modified": "Mar 21 2019"
    },
    {
      "name": "recentf~",
      "permissions": "-rw-rw----+",
      "links": 1,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 10079,
      "modified": "Mar 21 2019"
    },
    {
      "name": "images",
      "permissions": "drwxr-xr-x",
      "links": 2,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 4096,
      "modified": "Apr 16 2021"
    },
    {
      "name": "snippets",
      "permissions": "drwxrw----+",
      "links": 44,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 4096,
      "modified": "Apr 16 2021"
    },
    {
      "name": "projectile-bookmarks.eld",
      "permissions": "-rw-rw----+",
      "links": 1,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 3740,
      "modified": "Dec 10 2021"
    },
    {
      "name": "company-statistics-cache.el",
      "permissions": "-rw-rw----+",
      "links": 1,
      "owner": "ramsay",
      "group": "ramsay",
      "size": 241501,
      "modified": "May 27 2022"
    }
  ]
}

对于非结构化的数据,或者缺失格式的数据, AI都能处理.

5 总结

这是我用即梦AI生成的一张街舞海报,我注明了地址,联系电话,以及价格,生成出来的结果是这样的

远看是那么一回事!细看是什么回事?

AI现阶段还是无法取代程序员的,未来AI我也觉得很难取代程序员(当然,取代把需求翻译成代码的人肉翻译器还是有可能的),因为程序员虽然是和机器打交道,但是本质上还是解决人的问题。

担忧程序员被AI取代的人可能只看到了测试场景里面的 happy path, 也就是AI能生成老板们想要的代码,没有看到unhappy path, 也就是老板们想要的代码, AI生成不出来要怎么办?

毕竟人可以根据已有的经验进行归纳总结,把已有的经验应用在未踏足的领域,且持续学习,但是AI还是受限于预训练数据,做不出来就是做不出来。

但是对程序员而言,AI不失为一个好用且提效的工具,好用的工具就应该多学多用多玩,而不是担忧被其取代。

AI无法取代持续精进者, 傲慢自满方为淘汰之源

(什么,你问我当初的答辩结果?自然是


挂了)

推荐阅读

qrcode_gh_e06d750e626f_1.jpg 公号同步更新,欢迎关注👻

编程十年的感悟

2024-12-16 13:09:00

1 前言

马尔科姆·格拉德威尔的“一万小时定律”指出,持续投入一万小时的努力,足以使人在某个领域达到专家水平。 按照每周20小时的练习量计算,每天大约需要投入3小时,十年左右才能达成这一目标。

从我写下第一行C代码算起,至今已超过十年。 期间,我编写了超过三十万行代码,其中一部分在微信编写的代码,曾服务过超过一亿的用户。

尽管写了这么多代码,我仍不敢自诩为专家。 但多年的“打工”生涯,日复一日地敲代码,也让我积累了不少感悟。 “工多艺熟”,这些感悟既是对编程技术的思考,更是对职场人生的体味。 毕竟,除了最初在学校学习的几年,我的编程生涯几乎都伴随着“打工”的酸甜苦辣(多是苦辣)。

2 持续学习

虽然大学是从C语言入门编程的,但是我在大学时主修的语言是Java ,毕竟Java是门非常成熟的工业语言,有非常丰富的框架,在国内的企业非常受欢迎,工作岗位也多。

我当时从Java Servlets入门Web开发,再学习了非常流行的JavaEE 企业开发框架SSH, 即 Structs2 1+ Spring 2+ Hibernate 3, Struct2 负责控制逻辑关系,Spring 负责解耦, Hibernate 负责操作数据库.

而到我开始找工作时,SSH的概念就变了, Struct2 被SpringMVC 4所取代, SSH 变成了 SpringMVC + Spring + Hibernate.

到我实习入职蚂蚁金服的时候,发现组里代码库操作数据库的ORM框架用的并不是Hibernate,而是 Ibatis 5, 后面又切换成了新的 MyBatis 6

而蚂蚁金服内部使用的也并不是Spring/SpringMVC, 而是自主研发出发的 Sofa框架 7, Spring 社区后来觉得Spring框架过于重量级,不利于快速开发,又开发了更轻量级的 SpringBoot 8, 而蚂蚁内部又推出了Sofa版本的 Sofaboot 9

去了微信支付后,前期都是在写C++, 使用微信内部自研的svrkit 框架,到后期因为负责数据治理相关项目的缘故,开始使用 Spark + Python + Hive SQL

现在在AWS S3, 因为业务对性能和资源使用有非常高的要求,又开始使用Rust, 而历史业务又是使用Java, 兜兜转转之后,又回到Java的路子上。

细数下来, 这些年来,我写过Java,C++,Python,Rust,Javascript/Typescript 这些语言的生产代码.

除去工作之外, 我还因为学习SICP 学习了Scheme, 因为使用Emacs 而学习了Emacs Lisp, 想做独立开发赚钱学习了Swift, 想感受Ruby on Rails的魅力而学习的Ruby, 还有以前为了压测写的Golang, 还有各种语言对应的框架和库.

自我学习编程以来,学过的编程语言没有10种也有半打了.

我也从来不会把自己定义为某门语言的程序员,如Java程序员,C++程序员等等, 我只叫自己做Software Development Engineer. 语言从来只是工具,只要你持续学习,遇到新的场景,自然就会学习新的编程语言了.

计算机的世界日新月异,可能几个月就会出个新框架,几年又会流行一门新语言,只有持续学习,才能持续保持自己的竞争力。

3 学好英语

领袖常说,「东升西降」,虽然不知道此种变化何时才能实现,但起码说明,目前是「西尚在上,东尚在下」,在计算机领域,尤其如此。

最前沿的技术都是英文资料,英语又是世界通行的语言,来自不同国家的开发者又会不约而同地使用英语来交流, 因此学好英语既可以了解最新的技术潮流,又可以融入社区,建立自己的影响力。

疫情之后,越来越多的公司都开始推行远程办公,从全世界招聘开发者. 这就意味着如果你英文过硬,甚至可以离开一线城市,避免高额的生活开销,在老家工作,陪伴在父母身边,同时赚取外汇;这对于饱受996困扰的程序员来说,未尝不是一条出路.

于我个人而言,坚持学习英语可能是我收获最大的投资之一。

熟悉我的朋友,尤其是我的高中同学可能知道,十年以前,我的英文可以说着实挺烂的: 满分150分的英语,只考个及格的90分可谓是家常便饭,后来也只会笨学英语,到高三的时候能考个120分已经是巅峰水平。

但上大学之后,我也没有就此懈怠放下英语,大一还每天去晨读英语。

没有口语交流的条件,就自己创造,去网上找人聊天, 当时还在一个叫 Interpals 10 聊天网站认识了全世界好多的人, 其中还有一个是年龄相仿的土耳其女孩,我们还加了Facebook, 经常用Skype 视频聊天.

大学毕业后就没有那么多的时间闲聊后就断了联系, 最近看Facebook的动态,看她也穿上婚纱了.

工作后也一直阅读英文的技术文章,用英文搜索内容,在Stackoverflow 和 GitHub 用英文回答问题,在Discord 的英语学习频道找人聊天, 把电脑和手机系统语言都换成英文的,从学习英语变成用英语。

后来在机缘巧合之下,从国内找到了加拿大AWS的工作,幸而有机会来加.

人们常说,路应该要越走越宽,而不是越走越窄;

而在我看来,英语就是夜里走路时手上拿着的手电筒,可以让我们走自己的路的同时, 扫一下旁边那条道的情况,需要时及时转向,不至于一条路走到黑.

4 独立思考

微信以前一直有发最新iPhone手机的传统,但是那已经是4年前的美好时光了。

记得2021年是小龙明确年会不会发手机的第一年,他当时透露,那一年会发个铝片。

当时同事之间还在讨论,iPhone也是一块铝片冲压而成的嘛,那发的是否还是iPhone呢,不发手机只是烟雾弹?

拆开年会礼物之后发现,的确是一块铝片,上面写着「2022保持独立思考」.

小龙一直强调「独立思考」对微信的重要性,认为如果要选择一个最重要的品质,他会选择「独立思考」。

上级说的不一定是对的,老师说的不一定是对,学术机构说的也不一定是对,媒体说的也不一定是对,声音大的更不一定是对,毕竟有理不在言高。

比如微服务架构非常流行,许多公司都在搞微服务,那么单体架构是否就应该不使用?

作为初创公司或小团队,新业务是否要上微服务架构呢?还是先使用单体架构,业务发展起来再迁移到服务呢?

开发过程免不了要做各种决策,比如技术选型,针对你的需求,你可能会找到一打「看似」符合要求的组件, 可能还会去网上找找对各个组件的评价,会发现众说纷纭,就需要自己独立对每个组件做出分析,找出其优劣,再结合自身团队的特点,做出决策.

关于独立思考,我最喜欢的是一句话是HBO出品短剧《切尔诺贝利》里面, 科学家瓦列里·列加索夫希望克格勃释放调查真相同事乌拉娜·霍缪克的要求,说可以保证她是没问题的,克格勃头子回答的那句话:

Trust, but verify.(相信,但要核实)

5 先跑起来再说

这句话还有一个广为人知的变种:「又不是不能用」

很多的程序员都是完美主义者,尤其是读过《重构》和《设计模式》的程序员,会倾向于把很多时间来优化代码,做重构。

以前的我也会有类似的冲动,总会想时间去优化代码, 但是项目肝多了之后,有种强烈的感觉,还是先把MVP上线,及早让用户体验。

如果没有用户使用,再好再漂亮的代码也没有任何意义了。

所以经常看到社区有人问做副业的时候,应该用什么语言和框架,PHP/Python/Ruby 会不会太慢,我的观点一直都是,先做个原型跑起来,先找到第一个用户再说。

当运行速度成为瓶颈时, 你的业务已经非常大,肯定有足够的钱可以招一打的程序员把你的项目换成Golang/Java了。

对此,我很赞同坐我旁边大佬关于代码质量的说法:

make it run, make it fast, make it beautiful.

最近在做副业的尝试,有个深刻的体会,技术可能是商业里面最不重要的。

从零把产品做出来,推广给用户,用户只会关注你的产品是否好用,能否解决他们的问题.

他们既不会关注你是用C++/Java还是Javascript 写的,也不会关注你代码写得是否优雅,与其执着于技术选型,不如先把产品干出来让用户试用。

6 顺手的才是最好的

经常会看到有人在社区提问,什么语言最好,什么框架最好,什么编辑器最好,什么操作系统最好。

「最好」是个相当主观的结论,也并没有针对所有场景的「最好」的解决方案, 但是经常能看到社区有人因为哪个语言更好而吵起来.

或者有人在分享A的时候,有人会在下面回复B/C/D更好, 然后又争吵起来.

让不禁让我想起《社会性动物》这本著名的社会心理学著作里面提到的团队认同现象, 当球迷与某支球队产生强烈的认同感后,会将球队视为自我认同的一部分,这里他们会:

  1. 用「我们」而不是「他们」来称呼球队
  2. 将球队的成功视为个人的成功
  3. 对批评球队的言论产生防御性反应,将这些批评视为对自我的攻击

如果有人问我这个问题,我会回答「你顺手熟悉的工具的最好」。

即使是出于乐趣,编程的目的还是利用计算机解决问题,而解决问题最好的工具就是你最熟悉的工具。

除非你了解的工具不适用于你的问题,那么自然就需要一个新工具,也不要削足适履,矫枉过正。

当然,如果是为了满足求知欲而想去学习一个新的语言,那选择你感兴趣的就可以了。

当初在2017年学习Rust, 也只是因为大四没有课,时间充裕, 想学点有趣的新东西,那时候Rust1.0才发布2年, 可没指望能靠Rust找到工作

记不清在哪里看过的一段话:

我也曾问过自己类似的问题:

  1. 是不是好的东西就能流行?不一定
  2. 是不是我喜欢的东西就是好的东西?不一定
  3. 我会不会花时间精力在一个不一定会流行但是我喜欢的东西上?会

7 多与人交流

程序员固然是和机器打交道,但是本质解决的还是人的问题.

当初学习编程的时候,曾经有个误区,认为自己只要把技术搞好,就可以不去关心什么「人情世故」。

因此初入职场之后,我既是这么持有这样的想法,又是这样行动的,虽然不至于对其他人冷脸相对,但是难免会如好友形容那般:「孤傲」

但是被毒打时间久了才会发现,无论是在国内或国外,都难免会有「人情世故」,用英文来说,那叫 network and connection.

即使我技术能力过硬,也需要被人见到才行,和同事领导相处关系好,才可以在做出成绩的时候,「花花轿子被众人抬」。

所以我现在都是有事没事都和同事们聊天,既可以提升下熟悉度,也可以了解到许多部门八卦, 还可以从同事们抱怨中找到潜在优化点,践行自己「Work hard and be nice to people」的理念.

这行做久了,会发现软件工程其实说到底,就是人的系统工程。

8 代码不是万能的

程序写多了之后就会有种幻觉,就是觉得什么事情都可以用代码来解决。

手里拿着锤子的时候,把什么都当成钉子来砸。

被毒打多才认清的事实就是,有很多事情是无法用代码来解决,代码只是个工具,只能在个合适的场景使用, 避免路径依赖.

酒香也怕巷子深,只会写代码没啥用,还要写文章,在公司内部做分享,让别人能「看到你」。

编程肝项目的专业能力固然重要,但是也要有营销自己的软实力,就像一位长者说的那样: 两手抓,两手都要硬.

不知道是中国人讲究谦虚内敛的品质,还是程序员「木讷呆板」的刻板印象,导致大家都不怎么营销自己。

有事没事和老板聊下天,增进下交流,经常露个脸,可能比肝十个项目还有用。

9 与优秀的人共事

从业多年,去过蚂蚁金服,微信支付和AWS 搬砖, 和各种各样的同事都共事过,有个越发强烈的感悟:

要与优秀的人共事

不仅能从他们身上学到非常到的优点,提升技术能力,可以学到最佳实践和工程经验,在Code Review 的时候可以学到更好的编程方式,遇到问题时又有靠谱的队友帮忙和指导。

由优秀的程序员开发出来的系统的独特之处,知道什么叫简单好用的系统,形成自己的技术品味。

品味与美感这个词是很抽象,但是用过了好用的系统,自然就不会对那些粗制滥造,还靠老板背书强行推广的系统感兴趣。

而提高技术品味在提高我们的技术认知的前提下,又能反过来帮我们提高设计能力.

和优秀的同事共事的另外一个好处是可以建立高质量的人脉网络,利于职业发展,跳槽换赛道也多个选择。

虽然初始公司也有优秀的开发者,但是平均而言,大公司优秀程序员的比例会更高,毕竟他们更有竞争力的薪资福利,自然也有更高的招聘门槛。

比如微信就有所谓的面试委员会,除了招聘部门的面试官之外,还要通过面委面试官的考核,避免为了快速招人而降低标准。

所以个人建议应届毕业生,有机会还是去大公司,见识下。

虽然离职微信快两年了,我仍然想念当初同组共事的同事们,他们真的是技术过硬,人又超nice, 还乐于帮忙.

正如孔子所言:与善人居,如入兰芷之室,久而不闻其香,则与之化矣;与恶人居,如入鲍鱼之肆,久而不闻其臭,亦与之化矣

10 身体是一切的本钱

编程这么多年,落下一堆的职业病。

大学时候就有的鼠标手(腱鞘炎), 工作几年之后「喜提」腰椎间盘突出,久坐下半身会麻痹,还有我曾经浓密黝黑的头发,现在也日渐凋零。

因为腾讯总部有免费的健身房,所以我基本工作日都会去健身房薅公司羊毛,2天有氧慢跑,2天无氧器械,坚持了快3年。 也开始注意自己的饮食,尽量少油少糖不喝酒。

健身虽然不是包治百病,但是起码人显得有精神了,也有精力应付高强度的工作了。

只有失去才会懂得珍惜,也真的只有在开始吃药,去医院复诊,才会开始注意身体。

虽然编程很有趣,虽然养家很重要,但是还是要注意身体,毕竟身体是一切的本钱,垮就没有其他的精彩故事了。

11 总结

无论是编程,还是其他的技能,我感觉都是「马太效应」,你学得越多,你懂得越多,再学新的东西,你就会学得越快。

代码写多了才意识到,程序员的竞争力并不是写代码,也并不是哪门语言或者框架, 其核心竞争力是通过技术解决问题的能力,又何必再去拘泥于哪门具体的编程语言或技术呢。

希望编程十年只是个起点,十年后可以再写一篇「编程二十年的感悟」

推荐阅读

qrcode_gh_e06d750e626f_1.jpg 公号同步更新,欢迎关注👻

单独使用Emacs的Magit插件

2024-12-12 08:00:00

1 Emacs 与 Magit

不知不觉,我已经使用Emacs 快10年了,在我使用过的编辑器中,Emacs是扩展性最强的编辑器,毕竟Emacs是个披着编辑器外衣的Lisp虚拟机。

在Emacs无所不能的扩展性之下,诞生了非常多强大的插件, 也让Emacs有了「伪装成操作系统的编辑器」的美名,而Emacs公认的杀手锏插件有两个,一个是 org-mode,另一个是 magit. (我个人觉得还有个 dired)

Orgmode是类似Markdown,与Emacs深度绑定优化的标记语言,使用Emacs来编写org-mode 文档就有下笔有神,文思泉涌,如丝般顺滑(这篇文章也是用org-mode写的)。

因为org-mode 与Emacs 深度结合,自然无法脱离Emacs单独使用,而其他编辑器模仿org-mode 开发的插件,如 vim-orgmode, nvim-orgmodevscode-orgmode, 难免只得其形,未得其神,还不如用Markdown.

而 Magit 是 Git的Emacs图形化客户端, 也是我用过的最好用的Git 客户端软件,既直观又易用(看看Emacs 道友们夸 Magit 的帖子):

虽然我已经用了Emacs很多年,但是已经过了Live with Emacs的境界, 不会用Emacs处理所有事情, 比如用VSCode 写Rust, 用Intellij Idea写Java, 既然 Magit 那么好用,有没可能独立于Emacs使用呢?

2 Emacs daemon

作为无所不能的「操作系统」, Emacs 作为server 一直在后台运行,然后再使用 emacsclient 连接 server:

前文提到, Emacs 是批着编辑器外衣的Lisp VM, 而 Magit 本质也只是一个 lisp function, 只要在启动emacsclient的时候,再调用 magit的函数, 那么就可以启动 Magit:

1
alias magit="emacsclient -nw -eval '(magit-status)'"

这样就可以在VSCode和Idea里面愉快地使用 magit了.

3 总结

在Emacs-China 论坛搜索Magit的时候, 发现了也有一个帖子讨论把 magit当作的单独的工具, 没想到有个回复的思路和我一样,使用Emacs作为daemon 来启用.

只是没有想到他更evil, 在Nvim 里面使用Magit, 我也学习一下 Nvim+Emacs 的组合 :)

4 参考

qrcode_gh_e06d750e626f_1.jpg 公号同步更新,欢迎关注👻