MoreRSS

site iconISLAND修改

一名全栈开发者,前百度,现在一家数字货币交易所。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

ISLAND的 RSS 预览

分布式理论

2024-03-25 13:45:30

在分布式系统中最重要的一条理论为 CAP 理论。这个理论是由加州大学伯克利分校的计算机科学家 Eric Allen Brewer 在 2000 年提出的一个猜想,由 2002 年,麻省理工两位科学家发表了该猜想的证明,使得该猜想变成了一个定理。

CAP 定理中对分布式系统提出了三点,分别为:

  • 一致性(Consistency):多个副本的数据之间能够保持一致;
  • 可用性(Availability):每次请求都可以获取到正常的、非错误的响应,但是无法保证数据是最新的;
  • 分区容错性(Partition tolerance):系统的某个节点发生故障,仍然还可以对外提供一致性和可用性的服务。

例如当前系统有两个 DB 分别为 DB0 和 DB1。

一致性是指在数据发生变化的时候(也就是写操作发生后),无论是谁获取到的数据(也就是读操作)也是一样的。

当用户1 通过写操作对 DB0 的数据进行修改后,那么无论用户1 还是 用户2 ,无论从 DB0 还是 DB1 读取,读取后的数据都应该是完全一样的,这就是所谓的一致性。

也就是 DB0 的数据发生了修改,应该由相关的机制告诉 DB1 也将相关的数据发生修改,保证该数据在不同的 DB 中是一样的。

当用户发出相关请求,无论 DB0 还是 DB1 都会返回相关的数据,但是这里不需要关心数据是否一致。

DB0 和 DB1 例如出现了问题,这个问题可能是网络问题,也有可能是其他硬件问题,导致了 DB0 和 DB1 的系统无法通信。这样 DB0 和 DB1 就成为了两个分区。即使 DB0 和 DB1 无法进行通信,但是 DB0 和 DB1 仍旧可以对外提供服务。

但是这种情况,在实际系统中无法避免这种情况,所以分区容错性是一个必选的条件。

既然 CAP 三条规则无法同时满足,那么就出现了上图中的三种情况,满足任意两条规则,也就是 CA,CP 和 AP 三种架构,但是分区容错性是必选的,这样我们就剩下 CP 和 AP 两种关系。

常见的 CP 软件有 Zookeeper,Zookeeper 为了保证数据的一致性,牺牲了可用性。任何时候 Zookeeper 的访问都能获取一致的结果,但是不保证每次服务请求都可以用。

而 AP 架构中,要求数据一致性并不是那么重要,允许不同的服务可以返回不同的数据。

CAP 理论并不是完美的,存在很多问题。例如 DB0 和 DB1 要保持数据的一致性,那么就会发生相关通信,通信是需要时间,这就导致了某些时刻数据是不同步的,常见的情况在主从的机器上的主从延迟,当延迟过大的时候,用户读取的数据是不一致的。

CAP 理论也并不完全是三选二(或者说二选一)的问题,例如分区容错性发生的概论很小,那么就没必要牺牲了 A 和 C。

BASE 理论算是 CAP 理论的延伸,是对 CAP 理论中一致性和可用性的权衡。在 CAP 中,所谓的一致性是指数据时时刻刻的都保持一致,也就是强一致性。上文中 CAP 的不足也说到,要保证时时刻刻数据的一致性是一件很困难的事情。而 BASE 理论就是对改问题的补充,既然很难做到强一致性,那么系统根据自身的业务特点,确保系统中的数据保证最终一致性也是可以的。

BASE 理论是 ebay 工程师 Dan Pritchett

BASE 是指 Basically Available, Soft State 和 Eventually Consistent 三个短语的缩写。

基本可用指的是系统出现故障后,但是还可以使用,但是可能比较正常系统上可能出现一些问题,例如响应时间上,服务降级牺牲部分功能等。

软状态指的是系统数据允许出现中间状态,例如数据库主从同步过程中会出现中间状态,这就是一种软状态。

最终一致性强调经过上述的软状态后,最后数据保持一致性。

BASE 理论的提出是通过牺牲系统的强一致性来保证系统的可用性,允许系统在一段时间内的数据是不一致的,但是要求最终数据的一致性。

一文看懂|分布式系统之CAP理论

分布式架构之CAP理论/AP架构/CP架构

neovim入门指南(四):LSP配置(下)

2024-03-02 08:06:25

neovim入门指南(三):LSP配置 上 中说了 lsp 是什么如何配置和启动。那么接下来就完成 lsp 的其他配置。本章节主要介绍下面几个方面的介绍:代码高亮,文件格式化,lsp 相关的 UI 美化。

在有了 lsp 之后,当我们编写代码发生错误的时候,就会有相应的提醒。如图下提示

目前的错误还是字母,例如警告是W 。不太美观。对于这个来说,vim 开放了相关的接口,可以通过接口进行设置, neovim-diagnostic

仍旧在 lsp 的文件夹下建立 ui.lua 目录。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- lsp/ui.lua
-- 对错误警告的图标
vim.diagnostic.config({
        virtual_text = true,
        signs = true,
 -- 在输入模式下也更新提示,设置为 true 也许会影响性能
 update_in_insert = true,
})
local signs = { Error = "󰅙", Info = "󰋼", Hint = "󰌵", Warn = "" }
for type, icon in pairs(signs) do
 local hl = "DiagnosticSign" .. type
 vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end

其中 vim.diagnostic.config 是在配置提示文本,virtual_text 为错误信息提示.

目前的设置中,对于代码的高亮并不完美,需要采用一个插件了完成。tree-sitter 是一个用 rust 编写的高性能代码高亮的渲染工具,很多编辑器会采用它作为高亮功能的实现,比如说 Zed 。基于 tree sitter ,可以让我们的 neovim 的高亮更加完美。

这是来源于 nvim-treesitter 的一张图,图中左侧为未启用 treesitter 的代码高亮,右侧为启用后的效果。启用后的效果还是很明显的。

安装 tree-sitter 后,可以通过 TSInstall 来安装具体的语言高亮,例如 Rust 的高亮,可以直接使用 TSInstall rust 命令,或者使用 TSUpdate rust 命令进行更新。整体安装和配置还是比较简单的,这里不过多的解释。

代码格式化是一个很重要的功能,也是我们使用频率很高的功能。针对 Neovim 的代码格式化有很多种方式。这里主要介绍 null-ls。不幸的是,null-ls 已经归档来,不再进行维护,万幸的是,它的 fork 项目 none-ls 重新维护起来,而且目前还兼容 null-ls 的配置,如果你之前使用的是 null-ls,那么只需要将依赖地址由 jose-elias-alvarez/null-ls.nvim 改为 nvimtools/none-ls.nvim 即可。

安装好 none-ls,后可以通过 Mason 安装相关的 formatter,例如安装 lua 的 formatter。通过命令 :Mason 打开安装界面。

选择 stylua,进行安装。安装完成后就可以进行配置,在 lua/lsp 下新建文件,命名为 nonels.lua

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- lsp/nonels.lua
-- 即使是采用了 none-ls, 这里也是获取 null-ls
local status, null_ls = pcall(require, "null-ls")
if not status then
	vim.notify("没有找到 null-ls")
	return
end

local formatters = null.builtins.format

null_ls.setup({
    sources = {
        -- Stylua
        formatters.stylua,
        -- 其他 formatter 方式
    },
})

这样在我们进行编写代码的时候,进行相关格式化。在上一文中也进行了介绍,对快捷键进行了绑定。使用 <Leader>= 进行格式化。

关于前面,介绍了通过 cmp 插件进行了自动补全,通过 cmp 可以补充不同来源的代码。

我们使用一些 IDE 或者编辑器的时候,在补全的选项中是会展示一些图标,用来标识补全项的类型,例如变量、类名、方法或是接口等。

RustRover 自动补充样式。

VS Code 自动补全样式。

通过配置,也可以让 neovim 的提示实现上图效果。

当前自动补全的效果如图,前面为补全的字段,后面通过文字来标识补全的类型。

有一个插件叫做 lspkind ,可以通过安装该插件结合我们的 cmp 完成上述的样式。

在 lsp 下新建文件 kind.lua,配置 kind,这个可以在 lspkind,找到相关配置。

 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
-- lsp/kind.lua
local lspkind = require("lspkind")
lspkind.init({
	mode = "symbol_text",
	preset = "codicons",
	symbol_map = {
		Text = "󰉿",
		Method = "󰆧",
		Function = "󰊕",
		Constructor = "",
		Field = "󰜢",
		Variable = "󰀫",
		Class = "󰠱",
		Interface = "",
		Module = "",
		Property = "󰜢",
		Unit = "󰑭",
		Value = "󰎠",
		Enum = "",
		Keyword = "󰌋",
		Snippet = "",
		Color = "󰏘",
		File = "󰈙",
		Reference = "󰈇",
		Folder = "󰉋",
		EnumMember = "",
		Constant = "󰏿",
		Struct = "󰙅",
		Event = "",
		Operator = "󰆕",
		TypeParameter = ""
	},
})

这里可以配置每个类别的图标,配置完成后在 cmp 中进行配置。主要是修改提示的样式。这里主要参考了 Github 上的一个样式,。在 formatting 中进行配置。

 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
-- cmp.lua
cmp.setup({
    -- 省略其他代码
    formatting = {
        		completion = { border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, scrollbar = "║" },
		documentation = {
			border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" },
			scrollbar = "║",
		},
		format = lspkind.cmp_format({
			mode = "symbol",
			maxwidth = 20,
			ellipsis_char = "...",
			before = function(entry, vim_item)
				-- Get the full snippet (and only keep first line)
				local word = entry:get_insert_text()
				if entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
					word = vim.lsp.util.parse_snippet(word)
				end
				word = str.oneline(word)
				if
					entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
					and string.sub(vim_item.abbr, -1, -1) == "~"
				then
					word = word .. "~"
				end
				vim_item.abbr = word
				return vim_item
			end,
		}),
    }
    -- 省略其他代码
})

完成后的样式如图,基本和 VS Code 一样。

lspsage 可以极大的提高 nvim lsp 的体验。lspsage 的功能基本都是在增强和美化原有的 lsp 。具体的文档可以查看 nvimdev

lspsage 主要功能:

Finder: 用于高级 LSP 符号搜索的 UI
Diagnostic: 在诊断之间跳转并在漂亮的浮动窗口中显示它们
Peek Definition / Type Definition:查看定义/类型定义
悬停: 更漂亮的悬停操作
重命名: LSP 重命名和异步项目搜索和替换
调用层次传入/传出调用
Code Action: 具有实时预览的漂亮代码操作 UI
LightBulb: 类似 VSCode 的灯泡,指示可能的代码操作
面包屑: 类似于 WinBar 上的 IDE 符号纲要
大纲: 类似于 IDE 的符号纲要 实现: 轻松查看实现数量并快速跳转到它们
浮动终端: 一个简单的浮动终端
杂项: 适用于所有模块的选项

上面来自于官方文档的介绍。

Finder 的功能是一个类似 VS Code 的变量/函数 查看的界面,可以在界面上看到这个变量/函数的定义和调用地方。

1
2
3
4
5
6
7
-- keybindings.lua
-- 其他代码
pluginKeys.mapLSP = function(mapbuf) {
    -- 其他代码
    mapbuf("n", "gf", ":Lspsaga lsp_finder<CR>", opt) -- 新增
    -- 其他代码
}

这样在 normal 情况下按下 gf 就会出现上面的 finder 窗口。

Code Action 是一个非常重要的功能,可以根据代码上下文给出相关的提示或者代码改进。例如这块代码,Code Action 提醒我们可以直接转换为字面量的拼接。

Code Action 并不是 lspsaga 提供的,但是 lspsaga 给提供了一个漂亮的界面。在有 Code Action 的地方,会有一个黄色灯泡 💡 的提示。

下面绑定我们的快捷键。

1
2
3
4
5
6
7
-- keybindings.lua
pluginKeys.lspKeybinding = function(mapbuf)
 -- 省略其他代码
 -- code action
 -- mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt) -- 原有的
 mapbuf("n", "<leader>ca", ":Lspsaga code_action<CR>", opt) -- 替换为这个
end

lspsaga 提供了一个浮动终端窗口,可以在终端上执行任何命令。可以在 keybindings 中绑定这个快捷键,或者直接使用默认的快捷键 t ,来进行终端的打开和关闭。

lspsaga 还提供了很多功能,基本每个功能对我们都有帮助,这里就不做过多介绍了,大家可以看官方文档。

这里基本介绍完了 lsp 的常见功能和配置,剩下的大家可以通过需要进行安装和配置。

这里是我的配置 youngxhui/nvim 大家可以适当参考。

vim/neovim 使用的时候,的确是有一个较高的上手成本,尤其是方向键的习惯,当完全习惯了 h,j,kl 进行光标移动的时候,其实就离入门 vim/neovim 不远了。

当习惯了 vim 这一套风格之后,就会尝试把手头的编辑器/IDE 的快捷键都采用 vim ,而且在熟悉 vim 的快捷键后,真正的感觉到了手指在键盘上飞舞。

无论是 neovim 还是相关插件,更新迭代是很快的,也许当你看到这篇文章的时候,上述的很多配置都已经失效,或者相关插件已经停止维护,文章只是抛砖引玉,我相信你会找到合适自己的 nvim 配置。

我相信喜欢 vim/neovim 的人都是有一颗专研,喜欢折腾的心。正是这样的心才陪伴这个你我一步一步前进。

写到这里有感而发,下一篇我们将会介绍 DAP 。

neovim入门指南(三):LSP配置(上)

2023-09-04 18:10:02

对于一个编辑器来说,如果要完成例如自动补全,查找相关定义等功能是需要进行大量的开发的。不同的编辑器为了不同的语言都需要进行开发,而 LSP 的存在就是将这个过程检化。LSP 的全称为 Language Server Protocol,定义了编辑器和语言服务之间使用的协议。只要相关语言支持 LSP,那么编辑器只要符合相关要求实现即可完成例如自动补全等功能,而且不同的编辑器使用的体验是一致的。

目前支持 LSP 的编辑器有很多,例如大名鼎鼎的 Vscode。当然 vim 8 以后版本和 neovim 也都支持,具体支持的编辑器/IDE 列表可以看 LSP 的官方网站,同时支持 LSP 的语言也可以找到 支持语言

neovim 已经是支持 LSP 了,具体可以在相关的配置文档看到,该文档详细的描述了如何配置一个 LSP。相对来说,配置过程比较繁琐,所以官方又提供了另一个库 nvim-lspconfig。接下来我们就通过这个插件来配置 neovim 的 lsp。

与安装其他插件是一样的,只需要我们在 plugins_config.lua 中添加相关配置即可,这里不进行赘述了。安装完成后其实进行配置就可以启用 LSP 了,就是这么简单。例如支持 rust 的 LSP,只需要进行简单的配置。

1
2
3
4
5
lspconfig.rust_analyzer.setup {
  settings = {
    ['rust-analyzer'] = {},
  },
}

但是,nvim 只是 lsp 的客户端,那么就存在 lsp 的服务端。上面配置的 rust_analyzer 就是 rust 语言的服务端,就需要我们进行服务端的安装。rust_analyzer 的服务端地址是 rust-lang/rust-analyzer,需要将服务端下载并且安装好,这样每次编写rust的时候就会享受 lsp 的服务加成了。

但是这样做有几个问题,当然也不能算问题,只是不太方便。

  1. 对于多语言使用者来说,需要手动安装多个 lsp 服务端,目前 lsp 的服务端应该是没有统一的下载安装地址,需要手动寻找;
  2. 每次服务端进行更新,都需要重新下载安装;
  3. 新换设备之后,无法开箱即用,需要重复上述的方式重新开始一次。

面对上面的不方便,你可能已经想到很多解决方法,例如写个脚本进行一键安装和更新常用的 lsp 服务端。这样基本解决了上面说的所有问题。正如你想的那样,今天的第二位主角 williamboman/mason.nvim

mason 是一个可以方便的管理 LSP 服务端,DAP 服务端,Linter 和 格式化工具的插件。安装它之后,上面所说的问题将不是问题。

为了让 mason 和 nvim-lspconfig 更好的配合,这里还需要安装另一个插件 williamboman/mason-lspconfig.nvim

同样的安装这里不多赘述,主要是进行相关的配置。这里为了区别其他的插件,我们在 lua 目录下建立新的文件夹 lsp,用来专门存放 lsp 的配置。

首先还是加载我们的插件。在 lsp 文件夹中新建 mason.lua 文件,在文件中新增下面的配置。配置主要是在加载插件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- mason.lua
local mason_status, mason = pcall(require, "mason")
if not mason_status then
 vim.notify("没有找到 mason")
 return
end

local nlsp_status, nvim_lsp = pcall(require, "lspconfig")
if not nlsp_status then
 vim.notify("没有找到 lspconfig")
 return
end

local mlsp_status, mason_lspconfig = pcall(require, "mason-lspconfig")
if not mlsp_status then
 vim.notify("没有找到 mason-lspconfig")
 return
end


mason.setup()
mason_lspconfig.setup({})

配置完成后,重新启动 nvim,此时就可以采用 mason 进行 LSP 的服务端进行管理了。只需要按下 :Mason 即可。你将会看到如下的界面。

通过界面上的帮助可以看到如何使用,通过数字可以选择不同的服务端项目,2 为 LSP , 3 为 DSP 等。今天只是使用 LSP,可以直接按 2,选择到 LSP 界面,进行 LSP 安装。仍旧是通过 jk 进行滑动。第一个安装的 lsp 服务端为 lua 语言的服务端:lua-language-server。这个是 lua 语言的语言服务,有 lsp 之后,我们之后无论是配置 nvim 还是编写 lua 都会有 lsp 服务的加持。按下 i 进行安装。

稍等片刻,安装完成。接下来就是配置,让 nvim 知道我们的 lsp 已经安装,在合适的时候进行启动。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- mason.lua
nvim_lsp.lua_ls.setup({
 on_init = function(client)
  local path = client.workspace_folders[1].name
  if not vim.loop.fs_stat(path .. "/.luarc.json") and not vim.loop.fs_stat(path .. "/.luarc.jsonc") then
   client.config.settings = vim.tbl_deep_extend("force", client.config.settings, {
    Lua = {
     runtime = {
      version = "LuaJIT",
     },
     workspace = {
      checkThirdParty = false,
      library = {
       vim.env.VIMRUNTIME,
      },
     },
    },
   })

   client.notify("workspace/didChangeConfiguration", { settings = client.config.settings })
  end
  return true
 end,
})

这样 lua 的 lsp 就配置成功了,当我们编写 lua 脚本的时候,如果发生错误就会有相关提醒。当然这只是 lsp 最基础的功能,例如代码跳转,代码补全等需要我们进行配置。

基本所有的 lsp 的配置都可以在 server_configurations.md 中找到,当然 lua_ls 也不例外,上面的配置就是直接从文档中复制的 😄。

当前的 LSP 配置已经支持代码跳转,code action 等功能。例如查看当前变量或者函数的文档,可以使用这个命令 :lua vim.lsp.buf.hover()

相关的命令还有其他

功能 命令
文档显示 :lua vim.lsp.buf.hover()
查看定义 :lua vim.lsp.buf.definition()
重命名 :lua vim.lsp.buf.rename()
查询实现 :lua vim.lsp.buf.implementation()
查询引用 :lua vim.lsp.buf.refreences()
查询声明 :lua vim.lsp.buf.declaration()
格式化 :lua vim.lsp.buf.format()
Code action :lua vim.lsp.buf.code_action()

对于这些基础功能来说,每次需要的时候都在命令模式下敲一堆,速度的确是很慢的。所以,可以将上述的命令定义为快捷键,这样每次只需要进行快捷键进行完成上述功能。

打开我们之前设置快捷键的配置文件 keybinding.lua,新增上述功能的配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- keybinding.lua
-- lsp 快捷键设置
pluginKeys.lspKeybinding = function(mapbuf)
 -- rename
 mapbuf("n", "<leader>r", ":lua vim.lsp.buf.rename<CR>", opt)
 -- code action
 mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt)
 -- go to definition
 mapbuf("n", "gd", ":lua vim.lsp.buf.definition()<CR>", opt)
 -- show hover
 mapbuf("n", "gh", ":lua vim.lsp.buf.hover()<CR>", opt)
 -- format
 mapbuf("n", "<leader>=", ":lua vim.lsp.buf.format { async = true }<CR>", opt)
end

完成快捷键的配置,那么就可以将快捷键绑定到刚刚配置的 lsp 服务端了。

1
2
3
4
5
6
7
8
-- mason.lua
function LspKeybind(client, bufnr)
 local function buf_set_keymap(...)
  vim.api.nvim_buf_set_keymap(bufnr, ...)
 end
 -- 绑定快捷键
 require("keybinding").lspKeybinding(buf_set_keymap)
end

接下来可以完成快捷键的绑定。

1
2
3
4
5
6
7
-- mason.lua
nvim_lsp.lua_ls.setup({
 on_attach = LspKeybind,
 on_init = function(client)
    -- 省略其他配置
 end,
})

这样就完成了 lua 的 lsp 的配置,在编写 lua 的时候就可以使用文档查看,code Action 等功能。

目前这些 lsp 的服务都要手动下载,对于一些日常使用的服务,我们可以通过配置,在第一次加载配置的时候,当机器上没有相关的服务的时候,自动下载,这样来说,基本实现了我们上述提出的问题。

1
2
3
4
5
-- mason.lua
mason_lspconfig.setup({
 automatic_installation = true,
 ensure_installed = { "lua_ls", "rust_analyzer" },
})

这样配置,如果我们本地没有安装 lua 和 rust 的 lsp,会自动进行下载安装。

直到目前,在 lsp 的加持下,其实编辑体验已经变得非常棒了,而且开发速率也会大幅提升,虽然 lsp 是支持自动补全功能的,但是上面其实一直没有提及。主要是单单靠 neovim 的功能还不够强大,需要插件的配置。

hrsh7th/nvim-cmp 是一个采用 lua 编写的补全引擎,通过 cmp 及 cmp 的相关插件,会将 neovim 的自动补全达到一个新的高度。

这里除了 nvim-cmp,再推荐几个 cmp 的相关插件。更多的相关插件可以在 wiki 中找到

同样不赘述安装。在 lsp 文件夹中新建 cmp.lua 文件夹。

1
2
3
4
5
6
-- cmp.lua
local status, cmp = pcall(require, "cmp")
if not status then
    vim.notify("找不到 cmp")
    return
end

剩下了就可以将之前的补全源进行配置。这里贴出我的补全源。

1
2
3
4
5
6
7
8
9
sources = cmp.config.sources({
	{ name = "codeium" }, -- 需要安装 codeium.nvim
	{ name = "nvim_lsp" },
	-- For vsnip users.
	{ name = "vsnip" },
}, {
	{ name = "buffer" },
	{ name = "path" },
}),

此时当我们进行输入的时候就可以看到自动补全的提示了。对于自动补全的提示,上下选择并且上屏,我们可以设置快捷键,来满足我们的使用习惯。

和之前设置快捷键一样,在 keybindings.lua 中添加相关配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
-- keybindings.lua
pluginKeys.cmp = function(cmp)
 return {
  -- 出现补全
  ["<A-.>"] = cmp.mapping(cmp.mapping.complete(), { "i", "c" }),
  -- 取消
  ["<A-,>"] = cmp.mapping({
   i = cmp.mapping.abort(),
   c = cmp.mapping.close(),
  }),
  -- 上一个
  ["<C-k>"] = cmp.mapping.select_prev_item(),
  -- 下一个
  ["<C-j>"] = cmp.mapping.select_next_item(),
  -- 确认
  ["<CR>"] = cmp.mapping.confirm({
   select = true,
   behavior = cmp.ConfirmBehavior.Replace,
  }),
 }
end

最后在 cmp.lua 中使用这些快捷键即可。

1
2
3
4
cmp.setup({
    -- 省略其他配置
    mapping = require("keybindings").cmp(cmp),
})

这样便可以完成自动补全的配置了。

目前为止,已经完成 nvim 的 lsp 的相关配置,并且添加了自动补全。篇幅限制,剩下如何美化 lsp 提示,美化自动补全等我们下篇再说。

我的 neovim 相关配置,可以提供大家进行参考 youngxhui/nvim

neovim入门指南(二):常用插件

2023-08-28 21:30:02

经过前面章节的介绍,当前的 neovim 已经可以开始基本的使用了。下面会推荐一下常见的插件,让我们快速的开始。

nvim tree 是一个文件浏览器,可以实现侧栏快速选择文件。

当前 neovim 的插件安装都很简单,根据我们之前所了解的方式,先在 Github 上找到相关仓库:https://github.com/nvim-tree/nvim-tree.lua,然后安装。具体如何安装,其实在项目的 README 中会有详细说明。

这里需要安装两个包,第一个就是 nvim-tree,而第二个是一个可选包,主要是用来显示图标的。在 plugins.lua 中新增配置。

1
2
3
4
5
-- plugins.lua
require("lazy").setup(
	-- 省略其他配置
    { "kyazdani42/nvim-tree.lua", event = "VimEnter", dependencies = "nvim-tree/nvim-web-devicons" },
)

之后在 lua 目录中新建一个 plugins-config 目录,目录中新建 nvim-tree.lua 文件。之后就可以开始我们的配置了。大部分配置其实可以参考官方的 Wiki。

这里我们通过 pcall 函数来加载相关插件。

为什么要使用 pcall ? 当插件没有安装或者出现其他问题的时候,nvim 在启动时,无法加载相关查询,就会抛出异常,通过 pcall 就可以进行相关捕获,从而不影响 nvim 的使用。

1
2
3
4
5
local status, nvim_tree = pcall(require, "nvim-tree")
if not status then
	vim.notify("没有找到 nvim-tree")
	return
end

这样在加载不到 nvim-tree 的时候,就会通过 vim.notify 报出来。

剩下的就是配置我们的 nvim-tree 了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
nvim_tree.setup({
  sort_by = "case_sensitive",
	-- 是否显示 git 状态
	git = {
		enable = true,
	},
	-- 过滤文件
	filters = {
		dotfiles = true, -- 过滤 dotfile
		custom = { "node_modules" }, -- 其他过滤目录
	},
  view = {
	-- 文件浏览器展示位置,左侧:left, 右侧:right
	side = "left",
	-- 行号是否显示
	number = false,
	relativenumber = false,
	signcolumn = "yes" -- 显示图标
	width = 30,
  },
  renderer = {
    group_empty = true,
  },
})

最后在最外层的 init.lua 中添加配置。

1
require("plugins-config.nvim-tree")

重新退出后打开,通过命令 :NvimTreeToggle 或者 A-m,可以打开或关闭侧边栏。

如果需要修改快捷键,可以在 keybindings.lua 中新增相关快捷键,之后在 nvim-tree 中引用。

1
2
3
4
5
6
-- keybindings.lua
local pluginKeybinding = {}

pluginKeyBinding.nvim-tree = {
	{ key = "<F5>", action = "refresh" },
}

在 nvim-tree 中,设置相关快捷键。首先要在 nvim-tree.lua 中引入这个变量,并在 setup 中设置相关值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
local keymap = require('keybindings').nvim-tree

nvim_tree.setup({
	-- ...
	view = {
		mappings = {
			custom_only = false,
			list = keymap,
		}
	}
  -- ...
})

bufferline 是一个对 buffer 进行管理的插件,可以像现代IDE或者编辑器一样打开多个 Tab,而且可以快速进行切换。安装部分不多赘述,在 README 中上有详细描述。bufferline 地址:https://github.com/akinsho/bufferline.nvim

在 plugins-configs 目录下新建 bufferline.lua 文件,进行配置,仍旧是采用 pcall 进行加载,之后就可以进行配置。

1
2
3
4
5
6
7
8
-- bufferline.lua
local status, bufferline = pcall(require, "bufferline")
if not status then
    vim.notify("没有找到 bufferline")
  return
end

bufferline.setup({})

再次打开 neovim,同时打开多个文件,就会发现在上方出现了新的 tab。

需要进行一些设置,例如当前打开的前面安装的 nvim-tree , tab 会显示在 nvim-tree 上面,这显然是不符合预期的。通过命令 :h bufferline-configuration 来查看插件支持的配置。

通过帮助文档可以看到 offset 配置和 nvim tree 有关系,这样我们就可以通过相关的配置完成配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- bufferline.lua
bufferline.setup({
	options = {
		offsets = {
			{
				filetype = "NvimTree",
				text = "File Explorer",
				text_align = "left",
				separator = true,
			},
		},
	},
})

通过这样的设置就可以发现,当打开 nvim tree 的时候,tab 栏会自动向右偏移,而不会出现在 nvim tree 上方。 其他的设置可以通过上述的方式,通过帮助来查看。这里贴出我的配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- bufferline.lua
options = {
		close_command = "bdelete! %d",       -- 点击关闭按钮关闭
		right_mouse_command = "bdelete! %d", -- 右键点击关闭
		indicator = {
			icon = '▎', -- 分割线
			style = 'underline',
		},
		buffer_close_icon = '󰅖',
		modified_icon = '●',
		close_icon = '',
		offsets = {
			{
				filetype = "NvimTree",
				text = "File Explorer" ,
				text_align = "left",
				separator = true,
			}
		},
}

这里使用了 bdelete 相关命令,这个是 moll/vim-bbye 这个插件提供的,使用上面的配置需要安装该插件。 同时,可以在 keybinding.lua 中设置切换 tab 的快捷键。我这里设置为 Ctrl + h/l 来左右切换。

1
2
3
-- keybindings.lua
map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt)
map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt)

lualine 插件可以提供类似的效果,在编辑器上提供一些信息提示,例如 Git 状态,文本编码等,下图是 Github 上的效果。

如何安装可以直接看 nvim-lualine/lualine.nvim 上介绍,不多赘述。同样在 plugins-configs 下建立相关的配置文件 lualine.lua,通过 pcall 方式引入,使用 lualine.setup({}) 引入插件,最后在 init.lua 中添加配置文件。每个插件的安装使用方式都基本相似,如果有不同情况,会另外说明,后续的插件安装将不在说明这些步骤。

在 lualine 中,显示区域被分成了 6 个部分,分被用 A,B,C,X,Y,Z 组成。

1
2
3
+-------------------------------------------------+
| A | B | C                             X | Y | Z |
+-------------------------------------------------+

上面的每个部分都可以进行定制。在仓库中有三种样例,可以直接在 example 中找到。

相关配置:

 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
local status, lualine = pcall(require, "lualine")
if not status then
	vim.notify("没有找到 lualine")
	return
end

lualine.setup({
	options = {
		theme = "auto",
		component_separators = { left = "|", right = "|" },
		section_separators = { left = " ", right = "" },
	},
	extensions = { "nvim-tree", "toggleterm" },
	sections = {
		lualine_c = {
			"filename",
		},
		lualine_x = {
			"filesize",
			{
				"fileformat",
				symbols = {
					unix = '', -- e712
					dos = '', -- e70f
					mac = "", -- e711
				},
			},
			"encoding",
			"filetype",
		},
	},
})

经过配置就可以看到这样的效果了。

最近(2024 年 03 月 02 日)我更新了一下 lualine 样式,相较于之前更加简洁。相关样式如下

相关配置

 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
lualine.setup({
	options = {
		theme = "auto",
		component_separators = { left = "", right = "" },
		section_separators = { left = " ", right = "" },
	},
	extensions = { "nvim-tree", "toggleterm" },
	sections = {
		lualine_a = {
			{
				"mode",
				icons_enabled = true,
			},
		},
		lualine_b = {
			{
				"filetype",
				colored = true,
				icon_only = true,
			},
			"filename",
		},
		lualine_c = {
			"branch",
			"diff",
		},
		lualine_x = {},

		lualine_y = {},

		lualine_z = {
			"progress",
		},
	},
})

经过上面的配置,Neovim 的编辑界面已经逐渐“现代化”。Telescope 可以让我们的查找过程更加丝滑,主要可以用来做模糊查询。

telescope 安装也比较简单,可以参考 github 上的 README 进行安装。但是往往安装后无法进行模糊查询。这里就需要我们的另外的配置了。其实在 README 中也写的很明白,安装完成后,需要运行 :checkhealth telescope 命令。 通过该命令,可以看到当前插件的状态,是否可用,如果是首次安装,会提示 ERROR 和 WARNING,如图所示:

从提示可以看到缺少 rg 和 fd,同时在后面的说明中给出了相关的安装地址。这两个软件是进行模糊搜索的关键,可以通过以下两个地址进行安装,在 Github 的 README 中都明确的写出了两个软件在不同的系统上的安装方式。

如果你是用 mac,恰好已经安装 brew,那么只需要下面的两行命令即可完成安装。

1
2
brew install rg
brew install fd

安装完成后,重新 :checkhealth telescope,如果都是 OK,则证明安装正确,如下图所示:

安装完成后就可以通过命令使用 telescope,进行快捷的模糊查询。:Telescope find_file 为查找文件,Telescope live_grep 为全局查询。为了方便,可以在 keybindings.lua 配置中绑定为对应的快捷键。下面是我快捷键的对应绑定。

1
2
3
4
5
-- keybindings.lua
-- 查找文件
map("n", "<C-p>", ":Telescope find_files<CR>", opt)
-- 全局搜索
map("n", "<C-f>", ":Telescope live_grep<CR>", opt)

更多有趣的配置可以看 Github 仓库。

在上面的很多配置中,很多图标是无法显示的,会导致文本中或者界面中显示有问题,常常会显示为一个,这里就需要一个 nerd 字体进行支持。

Nerd 字体可以看做原始字体的一个补丁,这个补丁对原始字体新增了大量的图标。

针对上面无法显示的图标,需要安装好相关的 Nerd 字体。相关字体可以在官方网站下载,一些知名的字体都会有 nerd 版本,例如:FiraMono、JetBrainsMono、UbuntuMono 等。大家可以依照自己的喜好进行下载安装。

不同的终端模拟器有不同的配置方式,这里列出常见的配置方式。

iterm2 设置如下

Profiles -> Text 中,将 font 修改为 Nerd 字体即可。

WezTern 修改配置文件 ~/.config/wezterm/wezterm.lua

1
2
3
4
5
6
7
8
local wezterm = require("wezterm")
local config = {}

config.font = wezterm.font_with_fallback({
	{ family = "JetBrainsMono Nerd Font Propo", weight = "Bold" },
	{ family = "苹方-简", weight = "Bold" },
	"Noto Color Emoji",
})

neovim入门指南(一):基础配置

2023-07-05 10:23:17

在编程的世界中,有两个上古神器。一个叫做 “神之编辑器 Emacs”,另一个叫做 “编辑器之神 vim”。这两个编辑器从诞生到现在,圣战从未结束。无论是 vim 还是 emacs 都在不断的进化和发展,渐渐的 vim 的分支上出现了一颗夺目的新星,他就是 neovim。

neovim 从名称来看:新星的vim。按照官方说明: nvim 是一个 vim 的 fork,主要关注了扩展性和易用性。大量的 vim 用户迁移到 nvim,而 vim 的魅力出了经典的快捷键还有丰富的插件系统,这些 nvim 都继承了下来,同时内置了 LSP,增加了 异步 IO 等新特性。

以下是 Neovim 的一些主要特点和优势:

  1. 兼容性:Neovim 是 Vim 的兼容版本,几乎可以无缝地使用现有的 Vim 配置文件和插件。它支持 Vim 的命令和操作方式,因此 Vim 用户可以很容易地切换到 Neovim。
  2. 异步支持:Neovim 引入了异步任务处理的机制,使得编辑器可以在后台执行长时间运行的任务,而不会阻塞用户界面。这使得插件和脚本可以更高效地处理耗时操作,提高了编辑器的响应性。
  3. 现代化的插件系统:Neovim 提供了更灵活、更易于扩展的插件系统。它支持各种编程语言编写的插件,并提供了对外部进程通信的接口,使得插件可以与其他程序进行交互。
  4. 社区活跃:Neovim 拥有一个活跃的社区,不断推动编辑器的发展和改进。社区提供了大量的插件、主题和配置文件,以及对新功能的贡献和支持。

neovim 中配置可以通过 init.vim 或者 init.lua 进行配置,当前大部分的配置都采用了 lua ,本文也将会通过 lua 进行配置 nvim。如果你还不会使用 lua 也不需要担心,lua 可以快速上手。你可以直接通过 :h lua-guide 进行查看 lua 教程。

在类 unix 系统中,该配置文件位于 ~/.config/nvim/ 目录下,而在 windows 系统中,该目录位于 %USERPROFILE%\AppData\Local\nvim\。nvim 启动时会加载该目录下的 init.lua 文件,那么只需要在该文件中进行配置即可。

首先对文件的编码格式设置,统一为 UTF-8 。只需要在 init.lua 中添加相关配置。当然这是 nvim 的默认配置,不进行添加也是可以的。

1
2
vim.g.encoding = "UTF-8"
vim.o.fileencoding = "UTF-8"

这里的 og 是什么意思呢?

vim.o 意味着可以对全局的选项进行设置,而 vim.g 是进行全局设置。

类似的相关方法还有 vim.wo 设置 window-local 选项、vim.bo 设置 buffer-local 选项等。

对 tab 进行设置, tab 默认为 4 个空格。

1
2
3
4
vim.o.tabstop = 4
vim.bo.tabstop = 4
vim.o.softtabstop = 4
vim.o.shiftround = true

还可以将其他的基础配置添加进来,例如显示行号等,具体配置如下

 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
75
76
77
78
-- 编码方式 utf8
vim.g.encoding = "UTF-8"
vim.o.fileencoding = "utf-8"
-- jkhl 移动时光标周围保留8行
vim.o.scrolloff = 8
vim.o.sidescrolloff = 8
-- 显示行号
vim.wo.number = true
-- 使用相对行号
vim.wo.relativenumber = true
-- 高亮所在行
vim.wo.cursorline = true
-- 显示左侧图标指示列
vim.wo.signcolumn = "yes"
-- 右侧参考线
vim.wo.colorcolumn = "160"
-- 缩进字符
vim.o.tabstop = 4
vim.bo.tabstop = 4
vim.o.softtabstop = 4
vim.o.shiftround = true
-- >> << 时移动长度
vim.o.shiftwidth = 4
vim.bo.shiftwidth = 4
-- 空格替代
tabvim.o.expandtab = true
vim.bo.expandtab = true
-- 新行对齐当前行
vim.o.autoindent = true
vim.bo.autoindent = true
vim.o.smartindent = true
-- 搜索大小写不敏感,除非包含大写
vim.o.ignorecase = true
vim.o.smartcase = true
-- 搜索不要高亮
vim.o.hlsearch = false

vim.o.incsearch = true
-- 命令模式行高
vim.o.cmdheight = 1
-- 自动加载外部修改
vim.o.autoread = true
vim.bo.autoread = true
-- 禁止折行
vim.wo.wrap = false
-- 光标在行首尾时<Left><Right>可以跳到下一行
vim.o.whichwrap = "<,>,[,]"
-- 允许隐藏被修改过的buffer
vim.o.hidden = true
-- 鼠标支持
vim.o.mouse = "a"
-- 禁止创建备份文件
vim.o.backup = false
vim.o.writebackup = false
vim.o.swapfile = false
-- smaller updatetime
vim.o.updatetime = 300

vim.o.timeoutlen = 500

vim.o.splitbelow = true
vim.o.splitright = true
-- 自动补全不自动选中
vim.g.completeopt = "menu,menuone,noselect,noinsert"
-- 样式
vim.o.background = "dark"
vim.o.termguicolors = true
vim.opt.termguicolors = true
-- 不可见字符的显示,这里只把空格显示为一个点
vim.o.list = false
vim.o.listchars = "space:·,tab:>-"

vim.o.wildmenu = true

vim.o.shortmess = vim.o.shortmess .. "c"
-- 补全显示10行
vim.o.pumheight = 10
vim.o.clipboard = "unnamedplus"

vim/neovim 经过多年的发展仍旧活跃,其中插件系统功不可没。丰富的插件可以使得 neovie 分分钟化身为 IDE 。

在社区的发展过程中,vim 的插件系统也不断的壮大,目前 vim 的插件基本可以涵盖编辑的方方面面。针对 neovim,可以通过 awesome-neovim 这个项目查询相关插件。

无论是 vim 还是 neovim,本身并没有插件管理器,并不像 vscode 或者其他编辑器一样,可以方便的对查件进行添加,删除或者更新等操作。当然你也无需要担心,各路大神已经开发出了多个插件管理工具。目前,对于 neovim 来说,比较有名的插件管理工具有 packer.nvimlazy.nvim (注意:不要和 LazyVim 混淆)。

本文采用将会采用 lazy.nvim 作为插件管理工具进行插件管理。如果你使用了 packer.nvim 作为插件管理,并不影响阅读,可以跳过插件管理器章节。

lazy.nvim 作为现在比较流行的插件管理工具,安装其实很简单。在 init.lua 中添加相关代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- latest stable release
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup()

保存退出后,再次进入 nvim。 nvim 会检查是否存在 lazy, 如果不存在会从 github 上 clone 下来。

通过 :Lazy 命令,如果看到下面的图像,那么就证明 lazy.nvim 安装成功了。

有了 lazy,之后就可以快速的安装、更新和卸载插件。

为了验证 lazy 功能是否正常,那么先安装个主题插件看看。主题插件我选择 catppuccin.nvim

通过 catppuccin 文档可以查到如何安装,将插件添加到 lazy 配置中。

1
2
3
require("lazy").setup(
    { "catppuccin/nvim", name = "catppuccin", priority = 1000 }
)

退出 neovim,再次进入,就会看到 lazy 在下载相关插件。

安装完成后,发现我们的主题已经被应用,但是等到退出 neovim 再次进入的时候,发现主题颜色又恢复了默认配色,这里我们只需要在配置文件末尾添加一句,用于设置相关的配色。

1
vim.cmd.colorscheme("catppuccin")

其中 catppuccin 还有很多配置,这里不多赘述,可以到相关仓库中查看配置内容。

有了插件管理器,那么就可以添加不同的插件了。

经过上面简单的了解,目前所有的配置都写在 init.lua 中,当添加更多的插件的时候, lua 中的代码也会不断的增加,那么当需要进行修改的时候,查询相关配置也成了一大问题,而通过 lua 的特性,我们可以将不同的配置文件进行拆分,做到高内聚低耦合。

对于 lua 来说,可以通过 require 函数来将不同的 lua 脚本进行导入。

现在同 init.lua 目录下建立 lua 文件夹,在其中新建两个 lua 文件,分别是 basic.lua 和 plugins.lua。

1
2
3
4
5
.
├── init.lua
└── lua
     ├── basic.lua
     └── plugins.lua

结构如上目录树所示,之后可以将配置内容分被复制到相关的文件中,将基础配置放在 basic.lua 中,将 lazy 相关的配置复制到 plugins.lua 中。

最后将 init.lua 中引入相关的配置文件即可。

1
2
require("basic")
require("plugins")

其中,对于颜色主题的配置我们可以新建一个 theme.lua 文件,将主题配色相关的配置存放,最后不要忘记在 init.lua 中添加该文件。

最后的目录结构如下:

1
2
3
4
5
6
7
.
├── init.lua
├── lazy-lock.json
└── lua
    ├── basic.lua
    ├── plugins.lua
    └── theme.lua

为了让 vim 更加的顺手,方便我们的使用,针对一些操作需要绑定我们的快捷键,这样让你的操作效率如虎添翼。按照上面的配置规则,新建文件为 .lua,并在 init.lua 中添加。

vim 的一个重要点就是可以通过快捷键快速高效的完成任务,指哪里打哪里,而在配置快捷键中,其实有怎么几个键需要我们熟知,分被为 CtrlAltShift 键。这里并不是让大家知道这些按键在哪里,而是说配置中,这些键位很常用,并且在配置中这些键位常常简写。

键位 简写
Ctrl C-
shift S-
Alt A-

这三个键位在配置中会非常常见。当然,并不是只有这三个键位可以找到相关的缩写,可以通过 :h key-notations 看到所有的键位说明。

另外,对于 mac 来说是没有 Alt 键的,这样就需要我们修改 Option 键位,这部分可以看 附录/修改 Alt 键位。

leader 键对于 vim 来说是一个非常重要的键,它可以说是在 vim 中使用频率最高的键之一。 leader 顾名思义,处于领导位置,一般作为组合快捷键的先驱,使用其他按键的时候先按下 leader。对于 leader,vim 并没有规定谁是 leader,只要你想,哪个键也可以为 leader。一般会将 空格 等按键设置为 leader 键。

上面这些都是我们配置前的基础知识。下面开始设置相关快捷键。

在 lua 文件夹下新建文件, keybindings.lua,并且在 init.lua 中添加, require("keybindings")

首先便是上面说过的 leader 键。这里我采用空格作为 leader 键。

1
2
3
-- keybindings.lua
vim.g.mapleader = " "
vim.g.maplocalleader = " "

关于 mapleadermaplocalleader 的区别可以看 https://luciaca.cn/posts/vimscript-learning-on-leaders 相关文档。

在 neovim 中,需要通过 vim.keymap.set() 函数进设置。这个函数需要传入四个参数,分别是 mode , lhs , rhs , opts

参数名 说明
mode 模式的简写,常见的有 n(normal), i(insert), v(view) 等
lhs 可以理解为对应的按键
rhs 对应的功能
opts 相关的设置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
vim.g.mapleader = " "
vim.g.maplocalleader = " "
local opt = { noremap = true, silent = true }

-- visual模式下缩进代码
vim.keymap.set("v", "<", "<gv", opt)
vim.keymap.set("v", ">", ">gv", opt)

-- 左右Tab切换
map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt)
map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt)

-- 省略其他基础配置

这样就可以自定义添加相关按键。

我的相关配置,提供给大家参考 youngxhui/nvim

在 setting 中, Profiles-Keys 中, 将 Left Option Key 设置为 Esc+ 。

编辑 alacritty 的配置文件中window.option_as_alt

1
2
3
4
5
6
7
window:
  # Make `Option` key behave as `Alt` (macOS only):
  #   - OnlyLeft
  #   - OnlyRight
  #   - Both
  #   - None (default)
  option_as_alt: Both

为何 Emacs 和 Vim 被称为两大神器

BurntSushi/ripgrep

Gin源码分析一:引擎 Engine

2023-06-19 17:55:56

HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。

大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。

先来回顾一下标准库的 HTTP 处理流程。

  1. 启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。

  2. 处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。

  3. 调用处理程序:服务器会调用 ServeHTTP 方法,并将请求相关的信息作为参数传递给该方法。

  4. 路由匹配:在 ServeHTTP 方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。

  5. 调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。

  6. 写入响应:处理函数通过 ResponseWriter 接口写入响应数据,以返回给客户端。

根据上述的处理方式,目前需要关注两个函数:ServeHTTPResponseWriter 。这是两个主要的处理方式。

http.ListenAndServehttp.ListenAndServeTLS 用于启动 HTTP 服务器;以及 http.ResponseWriterhttp.Request 分别用于写入响应和处理请求。

同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux ,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP 这个接口即可。

采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子中实现也是非常简单,先通过 gin.Default() 生成一个 engine。 之后添加相关的处理方式,最后用 r.Run() 启动。下面将会从最开始的方法开始剖析 gin 框架。

Default 是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。

1
2
3
4
5
6
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

其中主要通过 New() 函数初始化 Engine 结构体。在 gin 中,通过 Engine 这个结构体进行管理,这个结构体其实是实现了 ServeHTTP 这个方法。

在这里 Engine 的功能和标准库中的 ServeMux 的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。

在上面的例子中, 路由和 handler 是通过 r.GET() 方法进行注册。从源码来看,可以发现不仅仅是 GET 方法,其他的请求方法也一样,都是直接调用了 group.handler 方法。

1
2
3
4
5
6
7
8
9
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// 计算绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合并 handler
	handlers = group.combineHandlers(handlers)
	// 添加相关路由
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。

combineHandlers方法中,首先计算了当前handlers的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers中的数据复制到mergedHandlers中,第二次是将handlers的数据传入mergedHandlers中。

1
2
3
4
5
6
7
8
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	// ...
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	// ...
}

这样 mergeHandler 中就存在两部分数据, handlers 可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers 中又是什么呢?

其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default 方法可以看到,engine.Use(Logger(), Recovery()) 项目在初始化的时候注册了两个中间件,而这个 Use 方法,其实就是将中间件添加到上面的 group.Handlers 中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。

确切地说,mergeHandlers的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。

路由添加方法也比较简单,核心代码一共就 6 行。

1
2
3
4
5
6
7
root := engine.trees.get(method)
if root == nil {
  root = new(node)
  root.fullPath = "/"
  engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)

先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。

直到目前,路由的注册工作已经完成。

在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。

在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe 方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。

在之前说过,要想成为一个 Handler,只要实现 ServeHTTP 方法即可,而 gin 的 engine 就实现了这个方法。

根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP 方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。

1
2
3
4
5
6
7
8
9
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()
	engine.handleHTTPRequest(c)
	engine.pool.Put(c)
}

首先使用到的变量有 pool。这里的 pool 使用的 sync.Pool 这个类型,主要是用来重复使用的 Context。这里直接从 pool 中取出 Context,并对 Context 的一些参数进行设置,最后调用 engine.handleHTTPRequest 方法。

这也是目前常常使用

engine.handleHTTPRequest 这个方法主要处理用户的 HTTP 请求,确定该请求的处理方法。简单的来说就:首先,获取请求的 HTTP 方法(如 GET 或 POST)和 URL 路径,并在需要时解码路径。然后,它搜索匹配该请求的处理树。如果找到了一个匹配的节点,它会将处理程序分配给请求的上下文(c),并写入 HTTP 响应头。如果未找到匹配的节点,则会通过 serverError 写入 “405 Method Not Allowed” 或 “404 Not Found” 的错误响应。

这样基本就是一个简单的 http 请求的处理过程。在代码中可以看到很多事情其实是由上下文 (Context) 进行处理的。