跳到主要内容

· 阅读需 5 分钟

我不长的工作经验内,接触的历史项目,怎么说呢,可能是各种历史原因导致代码可能不那么好维护(客气),其中有一种 Pattern 让我看到就头疼,那就是嵌套好多好多层的容器类型。

比如, Map<String, Map<String, List<String>>> 这样的复杂嵌套结构,我知道你是一堆有层次结构的String,但我有限的大脑容量,真是没办法时刻维护每一层String的真实含义,这种难以阅读、理解和维护的代码,我会放弃维护....或者,我一定要维护(工作嘛),我可能会用下面的方法稍微重构一下,只是为了理解代码的时候更加轻松点(降低我大脑的内存消耗)。

其实这种Pattern,我重构的目标就是去掉这种嵌套的结构,用 data class 来显式地展出他的上下文。那重构的步骤就是 从内到外 一步步构建 data class

例子分析

假设我们有这样一个数据结构 Map<String, Map<String, List<String>>>, 我真不想他出现在我需要维护的代码仓库里面, 他太模糊,比如,下面两个例子都是这样的数据结构,但是他们表达的含义完全不一样

例子 1: 员工和项目信息

  • 第一个 String 表示员工的姓名。
  • 第二个 Map<String, List<String>> 表示员工参与的每个项目及其详细信息。

重构方法

1. 内层建模: 首先,我们定义 Project 类,它包含项目名称和相关细节。

class Project {
private String name;
// 其他项目相关的属性
}

2. 中间层建模: 然后,我们创建 Employee 类,包含员工姓名和他们参与的项目列表。

class Employee {
private String name;
private List<Project> projects;
}

3. 外层建模: 最后,定义 Company 类,用于表示整个公司的员工信息。

class Company {
private Map<String, Employee> employees;
}

例子 2: 用户和社交媒体帖子

再来看另一个例子,同样的结构 Map<String, Map<String, List<String>>> 用于表示用户在不同社交媒体平台上的帖子。这里:

  • 第一个 String 表示用户的用户名。
  • 第二个 Map<String, List<String>> 表示用户在不同平台上的帖子。

重构方法

1. 内层建模: 首先,定义 Post 类来表示一个帖子。

class Post {
private String content;
// 其他帖子相关的属性
}

2. 中间层建模: 接着,创建 SocialMedia 类,它包含平台名称和帖子列表。

class SocialMedia {
private String platformName;
private List<Post> posts;
}

3. 外层建模: 最后,定义 User 类,表示社交媒体上的用户及其账户信息。

class User {
private String username;
private Map<String, SocialMedia> socialMediaAccounts;
}

重构的效果

  • 提高可读性:通过将复杂的嵌套结构转换为清晰定义的类,代码变得更加易于理解。
  • 上下文清晰:每个类都具体反映了其在应用中的角色和功能,为数据提供了清晰的上下文。
  • 减少维护难度:这种结构使得添加新功能或调整现有功能更加方便,减少了错误的可能性。

· 阅读需 2 分钟

你可以在 https://github.com/LintaoAmons/CoolStuffes/tree/main/lazyvim/.config/nvim/snippets/lua_snippets 找到一些例子

搞定 VS Code 代码片段

用 VS Code 插件的片段

用过 VS Code 的朋友肯定对那些方便的代码片段不陌生。要是你想在 Neovim 里继续用,LuaSnip 能帮你轻松搞定。比如,你安装了 rafamadriz/friendly-snippets 这样的插件,只要在你的 Neovim 配置里加上这么一行:

require("luasnip.loaders.from_vscode").lazy_load()

这样一来,Neovim 启动时就会自动把这些片段给你准备好,多方便啊!

弄自己的 VS Code 片段

当然,如果你有自制的 VS Code 片段也完全没问题。只需告诉 LuaSnip 你的片段在哪儿:

-- 加载你 Neovim 配置目录下的那些酷炫的自定义片段
require("luasnip.loaders.from_vscode").lazy_load({ paths = { "./my-cool-snippets" } })

Lua 片段也能轻松加

直接用 Lua 加片段

LuaSnip 还支持用 Lua 直接加片段,比如这样:

local ls = require("luasnip")
ls.add_snippets("lua", {
ls.parser.parse_snippet("enable", "enabled = false"),
s("local", fmt("local {} = require('{}')", { i(1, "default"), rep(1) })),
})

Lua 片段加载器更给力

如果你的 Lua 片段多到数不清,用加载器会更方便。这样你的片段可以整整齐齐地放在不同的文件里,用的时候一叫一个准:

-- 加载 '~/snippets' 目录下的 Lua 片段
require("luasnip.loaders.from_lua").load({paths = "~/snippets"})

  1. 片段出错: 如果加载的片段中哪个有问题,可能那个文件里所有的snippet就都挂了。(Debug 了我半天)
  2. 格式占位符: 记得 fmt 里的 {}{{ 是不一样的,别搞混了哦!

· 阅读需 1 分钟

Nextjs 导出静态网站并部署到 cloudflare

Nextjs 直接在 cloudflare 上进行部署会报错,但是我也不知道啥原因

我的想法很简单,就是一个静态的网站,所以思路是简单解决,本地build出静态资源之后,直接 cloudflare 部署静态资源目录就好了

然后本来用的 template 的 README 里面写的用 npm run export 来导出静态资源,但是运行之后发现报错,并给出了解释网页

https://nextjs.org/docs/pages/building-your-application/deploying/static-exports

· 阅读需 2 分钟

在 Vim 中复制所有匹配特定模式的行

使用 :g(全局)命令和 :y(复制)命令的组合。以下是执行此操作的一般格式:

  1. 首先,在 Vim 中打开你想要执行此操作的文件。

  2. 然后,使用以下命令:

    :g/pattern/y A

    在这个命令中,pattern 是你要查找的模式。:g 命令将在文件中全局搜索这个模式。/y 命令复制(拷贝)匹配该模式的行。命令末尾的 A 是一个寄存器,所有匹配的行都会被添加到这个寄存器中。

  3. 运行这个命令后,所有匹配模式的行都会被复制到寄存器 A 中。

  4. 如果你想把复制的行粘贴到某处,可以使用从寄存器 A 粘贴的命令:

    :put A

    这将把寄存器 A 的内容粘贴到当前光标位置。

查看更多 Vim Regex 批处理用法: https://lintao-index.pages.dev/docs/Vim/vim-regex

· 阅读需 1 分钟

Dosaurous 3.x 版本已经出来一段时间了,今天想把之前的搜索换成本地 build index 然后搜索,而不是用那个 Algolia 服务

然后发现 2.x 本版本,我在本地尝试 npm install 会报冲突 Error

索性就把 dosaurous 也升级了

重启解决一切问题

我直接重新起了一个 dosaurous 项目,直接把之前的文章给拷贝到这个新项目里面来了

修改配置 & 添加插件

MD, 啥时候不生病,圣诞节生病... 还好基本感觉快好了

Dosaurous 还是好用的

· 阅读需 2 分钟

我又在思考什么是好的代码,结论是能减少工作量却能实现业务要求的代码,就是好的代码。

让同组小伙伴能够写更少的代码,写更轻松的代码,就是代码设计的核心诉求。程序员的开发体验就是效率与结果的最大保证之一。

什么样的代码写起来更轻松,大家配合起来体验更好呢?方便改动,更少冲突。

其实就是提了无数遍的 “高内聚,低耦合”,就是“单一职责”。道理是这么个道理,真要写的时候,有什么具体的纬度或者参考标的呢?有的,我现在的体验就是:稳定的代码!单元测试足够好写!

稳定的代码

稳定的代码通常是从 Signature 来首先反映出来的。一个方法、函数的签名,再写下来之后,就不应该再频繁地改变了。

单元测试足够好写

当你发现你的单元测试有点难写,超过一定的行数的时候,是时候反思这个方法是不是做了太多的事情。通常 mock 掉不属于这个方法的职责,会让单元测试只考虑对应职责那一部分逻辑,从而降低复杂度,并且更加容易理解

未完待续...

· 阅读需 9 分钟

概念的描述在后面,我们先直接上用例,当你觉得熟悉了之后,再去看看概念,就会瞬间理解了

用例

多行合并

line 1
line 2
line 3
  1. 选择文本:首先,需要选择要合并的文本行。这可以通过在视觉模式(Visual mode)中选择,或者使用命令范围指定。

  2. 应用替换命令:一旦选择了文本,就可以应用替换命令。在示例中,:'<,'>s/\n/, 是这个命令的一个形式。这里是它的详细解释:

    • :'<,'>:这部分指定了命令的范围,即当前选择的文本块。'<'> 分别是选择区域的开始和结束。

    • s:这是替换命令。

    • /\n/:这是正则表达式,\n 匹配换行符。

    • ,:这是替换后的文本,即用逗号替换换行符。

应用该命令后,文本将变为:

line 1,line 2,line 3

此命令的一个变体是使用不同的分隔符,或者在合并时添加额外的空格或文本。例如,若要在每个逗号后添加空格,可以修改命令为 :'<,'>s/\n/, /

删除所有空行

:g/^$/d
  • :g 是 Vim 中的全局命令,用于对符合特定模式的所有行执行某个操作。

  • /^$/ 是正则表达式,用于匹配空行。其中,^ 表示行的开始,$ 表示行的结束。当这两个符号紧挨在一起时,它们匹配的是空行。

  • d 是删除操作。在此上下文中,它会删除匹配到的每一行。

这个方法是 Vim 中处理空行的常用技巧,简洁而高效。还可以通过修改正则表达式来定制匹配模式,以适应更复杂的需求。例如,如果想删除那些只包含空格或制表符的行,可以使用类似 :g/^\s*$/d 的命令。在这个例子中,\s* 匹配任何数量的空白字符(包括空格和制表符)。

跨行数字递增

假设你有一个类型,是CSV文件在内存中的数据结构体,现在因为插入了新的字段在 position3, 所有 3 以及 3 以后的数字都需要递增 1

你有 70 个字段,而且每一个 position matched 的行的中间还隔着一行,下面这个复杂一点的命令可以完成这个工作

// Before
@CsvBindByPosition(position = 3)
val name: String? = null,
@CsvBindByPosition(position = 4)
val age: String? = null,
@CsvBindByPosition(position = 5)
val address: String? = null,
@CsvBindByPosition(position = 6)
val phone: String? = null,
// ...
@CsvBindByPosition(position = 70)
val phone2: String? = null,

// After
@CsvBindByPosition(position = 4)
val name: String? = null,
@CsvBindByPosition(position = 5)
val age: String? = null,
@CsvBindByPosition(position = 6)
val address: String? = null,
@CsvBindByPosition(position = 7)
val phone: String? = null,
// ...
@CsvBindByPosition(position = 71)
val phone2: String? = null,

Command:

:%s/position = \zs\d\+/\=submatch(0)+1/g
  • %:表示对文件中的所有行执行此命令。
  • s:这是替换命令。
  • /position = \zs\d\+/:这是匹配的正则表达式模式。
    • position = :匹配文本 "position = "。
    • \zs:设置匹配替换的开始位置。这里用来确保只匹配 "position = " 之后的数字,并进行替换,而不是替换掉 "position = " 文本本身。
    • \d\+:匹配一个或多个数字(要递增的数字)。
  • /\=submatch(0)+1:这是替换模式。
    • \=:表示替换内容是一个表达式。
    • submatch(0)+1:取匹配到的数字(submatch(0))并将其增加 1。
  • g:此标志表示在每行上全局进行替换。

执行这个命令后,文本中的 position 参数中的数字将会逐一增加一。例如,position = 3 将变为 position = 4position = 4 将变为 position = 5,依此类推。

概念

正则的三个起手式

  • 命令格式: /pattern
  • 含义: 在文档中向前搜索符合特定模式(pattern)的文本。Vim 使用正则表达式作为搜索模式。
  • 例子: 输入 /hello 将会在文档中向前搜索 "hello" 这个词。

替换 (substitute)

  • 命令格式: :s/pattern/replacement/[flags]
  • 含义: 在当前行(或指定范围)中查找符合 pattern 的文本,并将其替换为 replacement 文本。可以通过 [flags] 控制替换的行为(例如,替换所有匹配项、进行确认等)。
  • 例子: 命令 :%s/old/new/g 会在整个文件中查找 "old" 并将其替换为 "new"。

全局 (global)

  • 命令格式: :g/pattern/command
  • 含义: 对匹配特定模式(pattern)的每一行执行一个 Vim 命令(command)。global 命令非常强大,可以用于执行复杂的文本操作。
  • 例子: :g/error/d 将会删除所有包含 "error" 的行。

这些命令结合 Vim 的正则表达式能力,可以执行从简单的文本搜索到复杂的文本处理操作。由于 Vim 的正则表达式语法和其他一些编辑器或编程语言中的正则表达式有所不同,因此在使用这些命令时需要注意语法差异。

全局模式

在 Vim 的 :g/pattern/command 命令中,command 可以是几乎任何 Vim 正常模式下的命令。这个命令的强大之处在于它的灵活性和广泛的应用范围。以下是一些常见的 command 示例,这些可以在 :g/pattern/command 结构中使用:

  1. 删除: d

    • 示例: :g/pattern/d 会删除所有包含 pattern 的行。
  2. 复制: tcopy

    • 示例: :g/pattern/t$ 将包含 pattern 的所有行复制到文件的末尾。
  3. 移动: m

    • 示例: :g/pattern/m0 将所有包含 pattern 的行移动到文件的开始。
  4. 替换: s

    • 示例: :g/pattern/s/old/new/g 会在所有包含 pattern 的行中,将 "old" 替换为 "new"。
  5. 标记: markma

    • 示例: :g/pattern/ma A 将所有包含 pattern 的行标记为 A(你可以后续通过 'A 跳转到这个位置)。
  6. 执行普通模式命令: normalnorm

    • 示例: :g/pattern/normal dd 将执行普通模式下的 dd 命令(删除整行),对所有包含 pattern 的行进行操作。
  7. 缩进: >

    • 示例: :g/pattern/> 将增加所有包含 pattern 的行的缩进级别。
  8. 取消缩进: <

    • 示例: :g/pattern/< 将减少所有包含 pattern 的行的缩进级别。
  9. 执行外部命令: !

    • 示例: :g/pattern/!sort 将对包含 pattern 的行应用外部的 sort 命令。
  10. 折叠: fold

    • 示例: :g/pattern/fold 将对所有包含 pattern 的行创建折叠。

这些仅仅是 :g/pattern/command 命令可能性的一小部分示例。由于 Vim 的强大和灵活性,你可以根据需要组合使用几乎任何命令来实现复杂的文本处理操作。

未完待续...

· 阅读需 2 分钟

在多个地方配置同一个插件

这个需求有很多应用场景

mason 为例,因为我们会有不同的文件需要配置 mason,不同的语言可能放在不同的文件中管理配置

但是每个配置需要同时生效,而不是最后一个载入的配置直接覆盖掉了整个之前的配置项

下面是来自 Lazyvim.extrago 语言配置代码

-- ...
{
"williamboman/mason.nvim", -- ❶ 配置 mason 的 opts.ensure_installed
opts = function(_, opts)
opts.ensure_installed = opts.ensure_installed or {}
vim.list_extend(opts.ensure_installed, { "goimports", "gofumpt" })
end,
},
{
"stevearc/conform.nvim",
opts = {
formatters_by_ft = {
go = { "goimports", "gofumpt" },
},
},
},
{
"mfussenegger/nvim-dap",
optional = true,
dependencies = {
{
"williamboman/mason.nvim", -- ❷ 又来配置 mason 的 opts.ensure_installed
opts = function(_, opts)
opts.ensure_installed = opts.ensure_installed or {}
vim.list_extend(opts.ensure_installed, { "delve" })
end,
},
{
"leoluz/nvim-dap-go",
config = true,
},
},
},
-- ...

可以看到

  1. mason 先配置了 go 语言相关的 lsp
  2. 然后在 nvim-dapdependencies 中,再一次配置了 mason, 但是这一次是增加了 debug 相关的 delve
opts = function(_, opts)
opts.ensure_installed = opts.ensure_installed or {}
vim.list_extend(opts.ensure_installed, { "delve" })
end,

这个 fucntion 的第二个入参是当前的mason配置,之后通过 vim.list_extend 方法添加了一个元素到这个节点,保证了之前的配置存在的基础上,新增一个元素,而不是复写掉整个节点

· 阅读需 3 分钟
CREATE TABLE "table_name"(
id VARCHAR PRIMARY KEY,
some_duplicatable_id VARCHAR NOT NULL,
some_other_info VARCHAR,
created_time TIMESTAMP WITH TIME ZONE,
)

-- 1. MAX sub query
-- Done after 0.312496 sec
SELECT "table_name".id, "table_name".some_duplicatable_id, "table_name".some_other_info, "table_name".created_time
FROM table_name LEFT JOIN (
SELECT some_duplicatable_id, MAX(created_time) as max_created_time
from table_name
GROUP BY some_duplicatable_id
) with_max_created_time on table_name.some_duplicatable_id = with_max_created_time.some_duplicatable_id
AND "table_name".created_time = with_max_created_time.max_created_time
ORDER BY created_time DESC
LIMIT 10;


-- 2. DISTINCT
-- Done after 0.738822 sec
SELECT DISTINCT (some_duplicatable_id) some_duplicatable_id, id, some_other_info, created_time
FROM table_name
ORDER by created_time DESC
LIMIT 10;


-- 3. Window Function
-- Done after 14.556224 sec
WITH RankedRecords AS (
SELECT id,
some_duplicatable_id,
some_other_info,
created_time,
ROW_NUMBER() over (PARTITION by some_duplicatable_id ORDER by created_time DESC) as rn
FROM table_name
)
SELECT id, some_duplicatable_id, some_other_info, created_time
FROM RankedRecords
WHERE rn = 1
ORDER BY created_time DESC
LIMIT 10;

-- 1

id | some_duplicatable_id | some_other_info | created_time
--------+----------------------+-----------------+------------------------
89144 | 32270 | 0x14000101170 | 2031-04-30 23:49:48+00
939189 | 40791 | 0x140003de500 | 2031-04-30 23:41:11+00
874730 | 13230 | 0x140004d83a0 | 2031-04-30 22:57:16+00
62080 | 29289 | 0x140000101f0 | 2031-04-30 22:11:43+00
204721 | 10791 | 0x1400051e200 | 2031-04-30 21:50:01+00
356485 | 31820 | 0x14000100850 | 2031-04-30 21:39:43+00
698920 | 40484 | 0x14000101460 | 2031-04-30 21:13:35+00
549194 | 44993 | 0x14000010820 | 2031-04-30 21:04:29+00
611477 | 28330 | 0x1400052ef70 | 2031-04-30 20:42:19+00
613039 | 28025 | 0x140004a8110 | 2031-04-30 20:22:05+00
(10 rows)

-- 2

some_duplicatable_id | id | some_other_info | created_time
----------------------+--------+-----------------+------------------------
32270 | 89144 | 0x14000101170 | 2031-04-30 23:49:48+00
40791 | 939189 | 0x140003de500 | 2031-04-30 23:41:11+00
13230 | 874730 | 0x140004d83a0 | 2031-04-30 22:57:16+00
29289 | 62080 | 0x140000101f0 | 2031-04-30 22:11:43+00
10791 | 204721 | 0x1400051e200 | 2031-04-30 21:50:01+00
31820 | 356485 | 0x14000100850 | 2031-04-30 21:39:43+00
40484 | 698920 | 0x14000101460 | 2031-04-30 21:13:35+00
44993 | 549194 | 0x14000010820 | 2031-04-30 21:04:29+00
28330 | 611477 | 0x1400052ef70 | 2031-04-30 20:42:19+00
28025 | 613039 | 0x140004a8110 | 2031-04-30 20:22:05+00
(10 rows)


-- 3

id | some_duplicatable_id | some_other_info | created_time
--------+----------------------+-----------------+------------------------
89144 | 32270 | 0x14000101170 | 2031-04-30 23:49:48+00
939189 | 40791 | 0x140003de500 | 2031-04-30 23:41:11+00
874730 | 13230 | 0x140004d83a0 | 2031-04-30 22:57:16+00
62080 | 29289 | 0x140000101f0 | 2031-04-30 22:11:43+00
204721 | 10791 | 0x1400051e200 | 2031-04-30 21:50:01+00
356485 | 31820 | 0x14000100850 | 2031-04-30 21:39:43+00
698920 | 40484 | 0x14000101460 | 2031-04-30 21:13:35+00
549194 | 44993 | 0x14000010820 | 2031-04-30 21:04:29+00
611477 | 28330 | 0x1400052ef70 | 2031-04-30 20:42:19+00
613039 | 28025 | 0x140004a8110 | 2031-04-30 20:22:05+00
(10 rows)

· 阅读需 1 分钟

这里的样例数据结构比较简单,但是在我的真实项目中,造 几百万条数据并加载进远端数据库也仅耗时 10 分钟以内

完整代码: https://github.com/LintaoAmons/load-data-demo

go--load-data-to-db on  go--load-data-to-db [!?] via 🐳 lima-docker_rootful_x86 via
🐹 v1.20.6
❯ ./load.sh
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000
COPY 100000

go--load-data-to-db on  go--load-data-to-db [!?] via 🐳 lima-docker_rootful_x86 via
🐹 v1.20.6 took 2m48s

generate struct out of db schema by jet

https://github.com/go-jet/jet

  1. make sure db is running, mock this by docker-compose

  2. install jet

go install github.com/go-jet/jet/v2/cmd/jet@latest
  1. generate
jet -source=postgres -host=localhost -port=5499 -user=world -password=world123 -dbname=world-db -path=./gen
  1. run main file

load csv data into db

PGPASSWORD=world123 psql -h localhost -p 5499 -d world-db -U world -c "\copy public.city from 'city-1.csv' with (format csv);"