使用IDE扫描Go依赖项的漏洞

本文翻译自《Vulnerability Scanning in IDE》。

点此回到《Go安全》

Go语言服务器集成的编辑器,例如装有Go扩展的VS Code,可以检测依赖项中的漏洞。

检测依赖关系中的漏洞有两种模式。两者都由Go漏洞数据库支持,并相互补充。

  • 基于导入的分析:在这种模式下,编辑器通过扫描工作区中导入的一组包来报告漏洞,并在go.mod文件中显示诊断结果。这很快,但如果你的代码导入了有漏洞的包,但并没有实际调用具有该漏洞的函数,则可能会误报。此模式可以通过“vulncheck”: “Imports”设置启用。
  • Govulncheck分析:这是基于gopls中嵌入的govulnchick命令行工具。这提供了一种低噪声、可靠的方法来确认代码是否真的调用了易受攻击的函数。由于此分析的计算成本可能很高,因此必须与基于导入的分析中的诊断报告相关的“Run govuncheck to verify”代码操作或使用go.mod文件上的“codelens.Run_govulcheck”代码操作手动触发。

切换Vulncheck (vulncheck.mp4)

这些功能在gopls v0.11.0或更新版本中可用。请在https://go.dev/s/vsc-vulncheck-feedback上分享你的反馈。

编辑器相关指导

VS Code

Go扩展提供了与gopls的集成。需要以下设置才能启用漏洞扫描功能:

"go.diagnostic.vulncheck": "Imports", // 默认启用基于导入的Govulncheck分析
"gopls": {
  "ui.codelenses": {
    "run_govulncheck": true  // 在go.mod文件里,鼠标移到依赖项上时显示"Run govulncheck"菜单选项
  }
}

Go Toggle Vulcheck”命令可用于打开和关闭当前工作空间的基于导入的分析。

Vim/NeoVim

使用coc.nvim插件时,以下设置将启用基于导入的分析。

{
    "codeLens.enable": true,
    "languageserver": {
        "go": {
            "command": "gopls",
            ...
            "initializationOptions": {
                "vulncheck": "Imports",
            }
        }
    }
}

注意事项和警告

  • 该扩展不扫描私有包,也不发送任何有关私有模块的信息。所有分析都是通过从Go漏洞数据库中提取已知有漏洞风险的模块的列表,然后在本地计算交集来完成的。
  • 基于导入的分析使用工作区(workspace)模块中的包列表,如果使用了go.work文件或replace/exclude指令,则该列表可能与你在go.mod文件中看到的不同。
  • 在修改代码或更新Go漏洞数据库后,govulcheck分析结果可能会变得过时。要手动使分析结果失效,请使用go.mod文件顶部显示的“Reset go.mod diagnostics”选项。否则,结果将在一小时后自动失效。
  • 这些功能目前不会报告标准库或工具链中的漏洞。我们仍在调查用户体验,了解在哪里显示这些发现比较合适,以及如何帮助用户处理这些问题。

_seq_no和_primary_term这两个字段在Elasticsearch里的作用是什么?

它们用来实现基于乐观锁的版本控制。

Elasticsearch跟踪上次操作的序列号(sequence number)和主项(primary term),以更改它存储的每个文档。通过GET API响应信息中的_seq_no和_primary_term字段返回序列号和主项。

_primary_term

在故障转移期间,每当不同的分片成为主分片时,primary term的值就会增加。这有助于解决重新联机的旧的主分片上发生的更改,与新的主分片上发生的更改的冲突问题,一般新的主分片会获胜。

这里我们要先理解Elasticsearch的高性能和高可用机制:一个索引的数据可以存储到多个分片上,以均衡负载,提升读写性能;而每个分片(主分片)又可以有多个副本(副分片),在主分片脱机后Elasticsearch可以快速把其中一个副分片推举为新的主分片,保障Elasticsearch集群的高可用性。

副分片的primary term只是主分片更改次数的计数器。

这些primary term是递增的,并且在主分片升级时会发生变化。它们作为集群状态的一部分被持久化,因此代表了集群所在的主分片的一种“版本号”或“年代(generation)”。

为了确保文档的旧版本不会覆盖新版本,对文档执行的每个操作都由对应更改的主分片分配一个序列号。

假设你的索引由5个主分片组成(在Elasticsearch 7之前是默认的)。索引(新增文档)和更新的请求是针对主分片执行的。如果你有多个主分片,Elasticsearch能够将传入请求(例如,巨大的批量插入文档的请求)并行化/分发到多个分片,以提高性能。

因此,_primary_term字段提供了关于执行或协调更改主分片(本例中为#1、#2、#3、#4或#5)的信息。

_seq_no

序列号在Elasticsearch 6.0.0中引入。

一旦我们对primary term进行了保护,我们就添加了一个简单的计数器,并开始向每个操作发出该计数器的序列号。因此,这些序列号使我们能够理解发生在主分片上的对索引的操作。

_version字段存储了一个序列号,用于统计文档更新的次数。_seq_no字段也是一个序列号,用于统计索引上发生的操作次数。因此,如果你创建第二个文档,你将看到该文档的_seq_no和第一个文档的_seq_no有所不同。

实例

创建多个文档:

POST test/_doc/_bulk
{"index": {}}
{"test": 1}
{"index": {}}
{"test": 2}
{"index": {}}
{"test": 3}

返回响应信息:

{
  "took" : 166,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "d2zbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "eGzbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "eWzbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

正如你所看到的:

  • 对于所有文档,版本号_version为1;
  • 对于文档1,_seq_no为0(第一次索引操作);
  • 对于文档2,_seq_no为1(第二次索引操作);
  • 对于文档3,_seq_no为2(第三次索引操作)。

教程:使用VS Code Go插件查找和修复可能有安全风险的依赖项

本文翻译自《Tutorial: Find and fix vulnerable dependencies with VS Code Go》。

点此回到《Go安全》

目录

先决条件

如何使用VS Code Go扫描漏洞

其他资源

你可以使用Visual Studio Code编辑器的Go插件直接在编辑器中扫描代码中的漏洞。

注意:有关以下图像中包含的漏洞修复相关的教程,请参阅govulcheck教程

先决条件

  • Go 1.18或更高版本。Govulncheck旨在与Go 1.18及以后的版本配合使用。有关安装说明,请参阅安装Go。我们建议你使用最新版本的Go来学习本教程。
  • VS Code编辑器,更新到最新版本。请在此处下载。你也可以使用Vim(有关详细信息,请参阅此处),但本教程的重点是VS Code Go插件。
  • VS Code Go插件,可以在这里下载。
  • VS Code编辑器特定设置更改。你需要根据这些规范修改VS Code的设置,然后才能复制下文的代码示例,运行后得到相应的结果。

如何使用VS Code Go扫描漏洞

第一步,运行“Go: Toggle Vulncheck”

Toggle Vulcheck命令显示在你的模块中列出的所有依赖项的漏洞分析。要使用此命令,请打开IDE中的命令面板(在Linux/Windows上的快捷键为Ctrl+Shift+P,在Mac OS上的快捷键为Cmd+Shift+P),然后运行“Go:Thoggle Vulcheck”。在Go.mod文件中,你将看到代码中可能会被直接和间接攻击的依赖项的诊断。

注意:要在自己的编辑器上重现本教程,请将下面的代码复制到main.go文件中。

// 这个程序从命令行获取一个或多个“语言标签(language tag)”参数,然后解析它们
package main

import (
  "fmt"
  "os"

  "golang.org/x/text/language"
)

func main() {
  for _, arg := range os.Args[1:] {
    tag, err := language.Parse(arg)
    if err != nil {
      fmt.Printf("%s: error: %v\n", arg, err)
    } else if tag == language.Und {
      fmt.Printf("%s: undefined\n", arg)
    } else {
      fmt.Printf("%s: tag %s\n", arg, tag)
    }
  }
}

然后,确保程序的go.mod文件的内容如下所示:

module module1

go 1.18

require golang.org/x/text v0.3.5

运行go mod tidy命令以确保你的go.sum文件已更新。

第二步,运行govulcheck。

使用代码操作(code action)运行govulcheck可以让你专注于代码中实际调用的依赖项。VS Code中的代码操作由灯泡图标标记;将鼠标悬停在相关依赖项上以查看有关该漏洞的信息,然后选择“快速修复(Quick Fix)”以显示选项菜单。再选择“运行govulcheck进行验证(run govulncheck to verify)”。这将在你的终端中返回相关的govulceck输出。

第三步,将鼠标悬停在go.mod文件中列出的依赖项上。

将鼠标悬停在go.mod文件中的依赖项上,也可以找到关于此依赖项的govulcheck输出。为了快速查看依赖项相关信息,这种方式甚至比使用代码操作更高效。

第四步,把你的依赖项升级到修复后的版本。

代码操作还可以用于快速升级到修复漏洞后的依赖项的版本。通过在代码操作的下拉菜单中选择“升级”选项来完成此操作。

其他资源

  • 有关IDE中漏洞扫描的详细信息,请参阅此页。特别是“注意和警告”小节讨论了漏洞扫描可能比上例中更复杂的特殊情况。
  • Go漏洞数据库包含来自许多现有源代码的信息,此外还有Go包维护人员向Go安全团队的直接报告。
  • 请参阅Go漏洞管理页面,该页面提供了Go用于检测、报告和管理漏洞的体系结构的高级视图。

字符集,编码、显码和存码,ANSI,GB2312,GBK,Unicode,UTF-8名词解释

ANSI是美国国家标准协会,系统预设的标准文字储存格式。

简体中文编码GB2312,实际上它是ANSI的一个代码页936

一种代码页就是一种字符集。

例如代码页936(Codepage 936)是Microsoft的简体中文字符集标准,是东亚语文的四种双字节字符集(DBCS)之一。其最初版本和GB 2312一模一样,但在Windows 95时扩展成GBK。现时中国大陆强制要求所有软件皆要支持GB 18030(Microsoft称之为代码页54936)。

根据微软资料,GBK(汉字内码扩展规范)是对GB2312-80的扩展,也就是CP936字码表(Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但GBK也是一种编码方式并向下兼容GB2312;而GB 13000.1-93等同于Unicode 1.1是一种字符集,它的几种编码方式如UTF8、UTF16LE等,与GBK完全不兼容。

UTF-8是一种通用的字符集编码格式,这是为传输而设计的编码,2进制,以8位为一个单元对Unicode进行编码。

在UTF-8里,英文字符的编码仍然跟ASCII编码一样,因此原先的函数库可以继续使用。而中文的编码范围在0080-07FF之间,因此是2个字节表示(但这两个字节和GB编码的两个字节是不同的),用专门的Unicode处理程序可以对UTF-8编码进行处理。”

GBK、GB2312是ANSI字符集的扩展字符集,UTF-8是Unicode字符集的一种编码方案,在编码层面,它们对ASCII字符的编码是一样的,但是对中文字符的编码就不一样了、不兼容了!

Unicode狭义来说只是一个字符集,而UTF-8是其一种编码规则(编码方式),Unicode有多种编码方式,还有ucs-2(utf-16)等。

字符集在计算机系统里的实现技术,分为存码技术和显码技术,编码规则属于存码技术,字体属于显码技术。一个字符可以有多种编码方式,例如GBK、UTF-8等,编码方式告诉计算机如何用二进制编码(此处“编码”是动词)和存储一个字符;一个字符也可以有多种显示方式,例如使用不同的字体,将在屏幕上显示不同的字形。一种字体就是一种显码。

参考

https://blog.csdn.net/l1028386804/article/details/46583279

https://zh.wikipedia.org/wiki/%E4%BB%A3%E7%A2%BC%E9%A0%81936

https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97%E5%86%85%E7%A0%81%E6%89%A9%E5%B1%95%E8%A7%84%E8%8C%83

电报Telegram隐藏手机号的方法

电报Telegram使用手机号注册成功后,默认不隐藏你的手机号,你的手机号会被Telegram上面的其他人看到,很不安全。

如何在Telegram里隐藏手机号,网上教程有很多,搜索一下。

最后记得重启Telegram,隐藏手机号的设置才会生效!

Go数据结构

本文翻译自《Go Data Structures》。

2009/11/24

在向新程序员解释Go程序时,我发现解释Go值在内存中的样子通常有助于建立正确的直觉,了解哪些操作是昂贵的,哪些不昂贵。这篇文章是关于Go的基本类型、结构体、数组和切片的。

基本类型

让我们从一些简单的例子开始:

变量i的类型为int,在内存中表示为一个32位的字(word)。(所有这些图片都显示了32位内存布局;在当前的实现中,只有指针在64位机器上变长了——int仍然是32位——尽管可以选择使用64位的int64类型。)

由于显式转换,变量j的类型为int32。即使ij有相同的内存布局,它们也有不同的类型:赋值i=j会引起一个类型错误,必须使用显式转换:i=int(j)

变量f的类型为float,当前实现将其表示为32位浮点值。它具有与int32相同的内存占用空间,但内部布局不同。

结构体及其指针

现在情况开始变得有趣。变量bytes的类型为[5]byte,是一个由5个字节组成的数组。它的内存表示就是这5个字节,一个接一个,就像一个C数组。类似地,primes是一个由4个int组成的数组。

Go与C一样,但与Java不一样,它可以让程序员控制什么是指针,什么不是指针。例如,此类型定义:

type Point struct { X, Y int }

定义了一个名为Point的简单结构体类型,表现在内存中就是两个相邻的int字段。

复合字面量语法Point{10, 20}表示已初始化的一个Point实例。获取Point{10, 20}的地址&Point{10, 20}表示指向Point{10, 20}的指针。前者是内存中的两个字(word);后者是指向内存中这两个字的指针。

结构体中的字段在内存中并排排列。

type Rect1 struct { Min, Max Point }
type Rect2 struct { Min, Max *Point }

Rect1是一个具有两个Point字段的结构体,在内存中由一行中的两个Point字段(四个int)表示。Rect2是一个具有两个*Point字段的结构体。

使用过C语言的程序员可能不会对Point字段和*Point字段之间的区别感到惊讶,而只使用过Java或Python(或…)的程序员可能会感到惊讶。通过让程序员控制基本的内存布局,Go提供了控制给定数据结构集合的总大小、分配的元素的数量和内存访问模式的能力,所有这些对于构建性能良好的系统都很重要。

字符串

有了这些预备知识,我们可以继续研究更有趣的数据类型。

(灰色箭头表示字符串实现中存在但在程序中不直接可见的指针。)

一个字符串string在内存中表示为一个2字结构体,里面包含一个指向字符串数据(是一个字节数组)的指针和一个长度字段。由于字符串是不可变类型,因此多个字符串共享同一底层存储是安全的,因此对s进行切片会产生一个新的2字结构体,该结构体具有不同的指针和长度字段,但仍然引用相同的底层字节序列。这意味着可以在不重新分配或复制的情况下进行切片,从而使字符串切片与显式地使用下标索引一样高效。

(顺便说一句,Java和其他语言中有一个众所周知的难题,当你对一个字符串进行切片以保存一小段时,对原始字符串的引用会将整个原始字符串保留在内存中,即使只需要少量的字符串。Go也有这个难题。我们尝试过但拒绝了另一种选择,那就是让字符串切片变得如此昂贵——一次再分配和一个新副本——大多数程序都应该避开它。)

切片(slice)

切片是对某个数组的部分引用。在内存中,它是一个3字结构体,包含指向第一个数组元素的指针、切片的长度和容量。长度是x[i]等索引操作的上限,而容量是x[i:j]等切片操作的上限。

与对字符串进行切片一样,对数组进行切片不会产生新副本:它只会创建一个包含不同指针、长度和容量的新结构体。在本例中,一开始创建切片[]int{2,3,5,7,11}在底层会创建一个包含五个值的新数组,然后设置切片x的字段来描述该数组。但切片表达式x[1:3]没有分配更多的数据:它只是创建一个新的切片头结构体,以引用相同的底层数组。在本例中,它的长度为2,即y[0]y[1]是唯一有效的索引,但容量为4,即y[0:4]是有效的切片表达式。(有关长度和容量以及切片使用方式的详细信息,请参阅Effective Go。)

因为切片是多字结构体,而不是指针,所以切片操作不需要分配内存,甚至不需要为切片头分配内存,因为切片头通常可以保存在栈上。这种切片的使用成本与在C语言中显式传递指针和长度对一样低。Go最初将切片表示为指向上述结构体的指针,但这样做意味着每个切片操作都会分配一个新的内存对象。即使使用快速的内存分配器,也会给垃圾收集器带来很多不必要的工作。不使用指针和分配内存使得切片足够便宜。

new和make

Go有两个数据结构创建函数:newmake。它们的区别在早期是一个常见的混淆点,但似乎很快就变得很自然了。基本区别是new(T)返回一个*T,Go程序可以隐式地解引用该指针(下图中的黑色指针),而make(T,args)返回普通的T,而不是指针。通常,T内部有一些隐式指针(下图中的灰色指针)。new返回一个指向值全是0的一块内存区域的指针(如下图所示),而make返回一个复杂的结构体。

有一种方法可以将这两者统一起来,但这将是对C和C++传统的重大突破:定义make(*T)返回一个指向新分配的T的指针,这样当前的new(Point)就可以被改写为make(*Point)。我们尝试了几天,但认为这与人们对分配函数的期望太不一样了。

更多……

这已经有点长了。接口(interface)、映射(map)和通道(channel)将不得不等待将来的发布。

你应该写坚持写博客,即使没有一个读者

本文翻译自《You should blog even if you have no readers》,作者是开源的分布式实时大数据处理框架Apache Storm的作者。

Spencer Fry的《为什么企业家应该写作》是一篇很棒的文章。我想进一步补充一点,写作的好处是如此非凡,即使你没有读者(无论你是否是企业家),你也应该写博客。

我有50多份未完成的草稿。其中一些只是我和自己争论时写下的一些想法。它们中的大多数永远不会出版,但我从所有这些写作中获得了价值。

写作使你成为更好的读者

博客改变了我阅读别人文章的方式。

在努力寻找正确的方式来构建和展示我的帖子的过程中,我更加适应什么是好的论点,什么是坏的论点。我也变得更善于发现别人推理中的漏洞。

同时,在阅读时,我不太可能陷入以微弱的反驳证据而诋毁帖子的陷阱。在大多数帖子中,都可能有基于特殊案例的反驳。网络评论者喜欢指出这些。然而,这些特殊的案例错过了帖子的主旨。通过理解帖子论点背后的隐含背景,我从阅读中获得了更多的价值。

我也更了解优秀作家的风格。我在脑海中注意优秀作家表达自己想法的方式。我一直很喜欢Paul Graham的写作,但现在我真的很欣赏他组织自己的帖子的方式。他有一种很棒的能力,可以把你吸引到他的世界里,并向你展示他眼中的世界。通过阅读Bradford Cross的博客,我学到了很多关于优秀写作的知识;他的帖子有一个清晰的弧线,很好地利用简短的段落来保持帖子的流畅性。

写作使你更加聪明

写作可以揭示你思维中的漏洞。当你的想法被写下来并回头看时,它们的说服力要比它们在你脑海中时低得多。写作迫使你通过思考和反驳来使你的想法更成熟。

写作可以帮助你以连贯的方式组织你的思想。当这些话题出现时,你就能侃侃而谈。我记不清有多少次我和其他人进行了更深层次的对话,因为我已经让自己的想法变得成熟。

把其他任何东西都视为附带收益

写作给你的其他一切东西——个人品牌、人际网络、工作机会——都只是附带的好处。这些好处有时候非常大,但它们不是你应该写作的主要原因。

你应该写作,因为写作会让你成为一个更好的人。

为什么企业家应该写作

本文翻译自《Why entrepreneurs should write》。

Spencer Fry

2010/07/14

人们问我为什么要花时间写文章。我从制定和分享我的想法中得到了什么价值?我没有引入广告,所以不会在月底领到广告薪水——那为什么要这样做呢?我认为,作为企业家,把你的想法用语言表达出来可以帮助你思考自己的想法,帮助推销自己和产品,并带来良好的人际关系机会。但首先反驳一下大家对简讯(newsletter)的看法……

简讯

正如GigaOM的Mathew Ingram在一篇题为《是时候停止写博客并开始写电子邮件简讯?》的文章中所写的那样,一群人——主要是纽约市的企业家社区——已经从写博客转向写简讯。Jason Calacanis是2008年第一个这样做的人,他说这是一种解决他收到的辱骂言论的方式。通过简讯,人们可以直接向他发表评论,而不是向所有阅读他的帖子的人发表评论。问题解决了。

直到最近,Sam Lessin关闭了他的博客,推出了Letter.ly,作为一项简讯服务,你可以对自己的内容收费,尽管价格不高。“所以,是的,旧的又变成了新的,”Sam写道。

但我不使用简讯!

信息是用来消费的。它应该是免费的。它旨在让尽可能多的人接触到,分享和讨论。一堵围绕信息内容的墙——无论是付费的还是其他的——注定要倒塌。你只需要看看Jason Calacanis,当他真的想发出自己的声音时,他会在博客上重新发布他的简讯。

这个简讯“运动”,如果你真的可以这么称呼它的话——只有少数人真正在做——我猜订阅的人更少,它已经成为了一个过渡阶段。我非常尊重Sam、Michael、David、Andrew和其他转而撰写简讯的人——他们都是我的朋友——但如果看到他们在这件事上坚持己见,我会感到惊讶。如果他们真的重视写作的意义,那么他们将再次公开分享自己的内容。

博客已死?不全是

我认为博客在某种程度上已经死了。我不认为我的个人网站spencerfry.com是一个博客。这是一本散文集。传统意义上的博客——你对X、Y和Z的想法的片段——已经被推特、脸书和汤博乐所取代。几乎任何大小的段落都可以压缩到140个字符。

如果人们要坐下来阅读你要说的话,那么你必须认真写出一些值得阅读的内容。这年头信息爆炸,如果你想让别人阅读你的文章,那么你必须选择一个有趣的话题,深思熟虑地阐述你的想法,并支持你的论点。所有这些都需要不止140个字符,如果做得好,确实值得一读。

思考想法

大多数时候,当我坐下来写一篇文章时,我的脑海里都没有一个清晰的画面,我要说什么。只要我有一个我想谈论的话题和立场,总有回旋的余地让我形成自己的想法。把所有的东西都写下来有助于我完成思考的过程,并让我对某个话题做出有力的决定。如果它是书面的形式,那么当我点击“提交”时,我必须100%支持它。

通常先思考我将要写什么内容。然后以一个标题(尽管这通常会改变)、各个章节的标题以及每个标题下的一些潦草的想法开始。接下来,我开始写一些简介(在每个标题下面),最后把每一章节下面的段落都整理出来。

当我写完一篇文章时,我已经彻底地审视了这个话题的各个角度,做了研究,找到了一些外部证据,并在谷歌搜索栏上搜索了关于这个话题的相关材料。这个过程有助于巩固我的思考,像这样系统地充实你的想法的过程本就对你有所帮助。

教会你的读者

写下你的想法不仅有助于你形成自己的想法,而且教育读者也是一件非常值得做的事情。《通过教育促进真实生活》一书中指出,“你可以回馈支持你的社区,同时获得一些不错的宣传曝光率。”

俗话说,分享就是关爱。通过你的产品和你的名字,你的文章能得到进一步讨论的评论,你会收到读者的电子邮件,以及你遇到的那些读过你的文章的读者。我每次都很快乐。

网络(在线)

我在2010年4月写的一篇题为《如何建立网络》的文章给出了一些关于如何成功地在线下建立网络的基本技巧,但对于一个想要为自己和产品扬名立万的企业家来说,在线网络同样重要。

写一篇深思熟虑的文章意味着你有140个字之外的话要说,赢得你的尊重,并让你接触到有趣的人。我写作的许多粉丝都成了Carbonmade的粉丝。我们甚至被推荐为合作伙伴,成为了朋友。

推特、脸书、汤博乐等等都值得你花时间,但我发现没有什么比深思熟虑的散文集更能吸引大批粉丝了。如果你想从人群中脱颖而出,你首先必须在地面上划出一条线,表明你在什么问题上的什么立场。

Ubuntu22安装PHP8.2的方法

首先更新系统:

sudo apt update && sudo apt upgrade -y

Ondrej sury PPA仓库是目前维护PHP最新版本的仓库,我们添加这个仓库:

sudo add-apt-repository ppa:ondrej/php

再次更新系统以使添加的Ondrej sury PPA仓库生效:

sudo apt update && sudo apt upgrade -y

安装php8.2:

sudo apt install php8.2 -y

安装完成后,检查php的版本:

php --version

使用sudo apt-get install php8.2-PACKAGE_NAME命令安装php常用扩展,把PACKAGE_NAME替换为具体的扩展名:

sudo apt-get install -y php8.2-cli php8.2-common php8.2-fpm php8.2-mysql php8.2-zip php8.2-gd php8.2-mbstring php8.2-curl php8.2-xml php8.2-bcmath

查看已经安装了哪些php扩展:

php -m

参考

https://techvblogs.com/blog/install-php-8-2-ubuntu-22-04

Go数组、切片以及字符串的append函数的机制

本文翻译自《Arrays, slices (and strings): The mechanics of ‘append’》。

Rob Pike

2013/09/26

介绍

面向过程的编程语言的最常见的特性之一是数组的概念。数组看起来很简单,但在将它们添加到编程语言中时,必须回答许多问题,例如:

  • 固定长度还是可变长度?
  • 长度是这个类型的一部分吗?
  • 多维数组是什么样子的?
  • 空数组有意义吗?

这些问题的答案会决定数组是编程语言的一个特性还是其设计的核心部分。

在Go的早期开发中,大约花了一年时间来决定这些问题的答案,然后才觉得设计是正确的。关键的一步是引入切片(slice),它建立在固定大小的数组上,以提供灵活的、可扩展的数据结构。然而,时至今日,刚接触Go的程序员经常会在切片的工作方式上磕磕碰碰,也许是因为其他语言的经验影响了他们的思维。

在这篇文章中,我们将试图消除混乱。我们将通过构建代码片段的方式来解释内置函数append是如何工作的,以及为什么它会这样工作。

数组

数组是Go中的一个重要的组成元素,但与建筑的基础一样,它通常隐藏在更显眼的组件下面。在我们继续讨论切片这个更有趣、更强大、更突出的概念之前,我们必须先简要地讨论一下数组。

数组在Go程序中并不常见,因为数组的大小是其类型的一部分,这限制了它的表达能力。

声明

var buffer [256]byte

声明了buffer变量,是一个数组,可容纳256个字节。buffer的类型包括其大小[256]byte。而具有512个字节长度的数组将是不同的类型:[512]byte

与数组相关联的数据就是数组的元素们。从以下示意图来看,我们的buffer数组在内存中是这样的,

buffer: byte byte byte ... 256 times ... byte byte byte

也就是说,该变量包含256字节的数据。我们可以通过buffer[255]使用熟悉的索引语法:buffer[0]buffer[1]等来访问它的元素。这里的buffer数组的索引范围是0到255,包含256个元素。试图用超出此范围的值对buffer数组进行索引将导致程序崩溃。

有一个名为len的内置函数,它返回数组或切片以及其他一些数据类型的元素数量。对于数组,len返回的内容是显而易见的。在我们的示例中,len(buffer)返回固定值256。

数组有自己的作用——例如,它们可以很好地表示矩阵——但它们在Go中最常见的用途是作为切片的底层存储。

切片

想使用好切片,必须准确地了解它们是什么以及它们的作用。

切片是一种数据结构,描述与切片变量本身分开存储的数组的连续部分。切片不是数组。切片描述某一个数组的一部分。

对于上一节中的buffer数组,我们可以通过对该数组进行切片来创建元素下标100到150(准确地说,是100到149,包括100到149)的切片:

var slice []byte = buffer[100:150]

在该代码段中,我们使用了显式的完整的变量声明:变量slice的类型为[]byte,发音为“slice of bytes”,通过对buffer数组元素从下标100(包含)到150(不包含)进行切片,来初始化。更惯用的语法是不写出切片的类型:

var slice = buffer[100:150]

在函数体中,我们也可以使用海象运算符来初始化一个切片:

slice := buffer[100:150]

切片变量究竟是什么?虽然这还不是全貌,但现在可以将切片视为一个包含两个元素的小数据结构:长度和指向数组元素的指针。你可以把它想象成是在底层构造的如下所示的结构:

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

当然,这只是一个例子。尽管这个sliceHeader结构对程序员来说是不可见的,并且元素指针的类型取决于元素的类型,但这给出了切片底层机制的一般性概念。

到目前为止,我们已经对数组使用了切片操作,但我们也可以对切片进行切片,如下所示:

slice2 := slice[5:10]

与之前一样,此操作创建一个新的切片,具有原始切片的下标从5到9(包含9)的元素,这意味着这个新切片具有原始数组的下标从105到109的元素。slice2变量的底层sliceHeader结构如下所示:

slice2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}

请注意,此结构的指针仍然指向存储在buffer变量中的底层数组。

我们也可以重新切片(再切片,reslice),也就是说对切片进行切片:

slice = slice[5:10]

这个slice变量的sliceHeader结构与slice2变量的结构类似。你将经常使用重新切片,例如截断一个切片。以下这行代码截除切片的第一个和最后一个元素:

slice = slice[1:len(slice)-1]

[练习:写出上述赋值后的slice变量的sliceHeader结构的样子。]

你经常会听到有经验的Go程序员谈论“切片头sliceHeader”,因为它实际上是存储在切片变量中的东西。例如,当你调用一个以切片为参数的函数时,例如bytes.IndexRune,切片头就是传递给函数的内容。在以下调用中,

slashPos := bytes.IndexRune(slice, '/')

传递给IndexRune函数的slice参数实际上是一个“切片头”。

切片头中还有一个数据项,我们将在下面讨论,但首先让我们看看当使用切片编程时,切片头的存在意味着什么。

把切片传递给函数

重要的是要理解,即使切片包含指针,它本身也是一个值。在底层,它是一个结构体值,包含一个指针和一个长度,而不是指向某个结构体值的指针。

这很重要。

当我们在前面的例子中调用IndexRune函数时,传递了一个切片头的副本。这种行为具有重要的影响。

考虑一下这个简单的函数:

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

顾名思义,该函数迭代切片的索引(使用for range循环),使其元素的值加1。试试看:

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("before", slice)
    AddOneToEachElement(slice)
    fmt.Println("after", slice)
}

(如果你想探索,可以编辑并执行上述可运行的代码段。)

即使切片头是按值传递的,它也包含指向数组元素的指针,因此原始切片头和传递给函数的切片头副本都描述了同一个底层数组。因此,当函数返回时,可以通过原始切片头看到被修改后的元素。

函数的切片实参确实是一个副本,如本例所示:

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
}

在这里,我们看到切片参数的内容可以由函数修改,但其切片头不能。存储在切片变量中的长度不会被函数的调用所修改,因为函数传递的是切片头的副本,而不是原始切片头。因此,如果我们想编写一个修改切片头的函数,我们必须将其作为结果参数返回,就像我们在这里所做的那样。slice变量不变,但返回的值具有新的长度,然后将其存储在newSlice中,

切片指针:方法的接收者

让函数修改切片头的一种方法是将指针传递给它。下面是我们前面示例的一个变体:

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

这个例子使用了指向切片的指针,但看起来很笨拙。要修改切片,我们通常使用指针接收者。

假设我们有一个方法,在最后一个斜杠处截断切片。我们可以这样写:

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

func main() {
    pathName := path("/usr/bin/tso") // Conversion from string to path.
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

如果运行此示例,你将看到它能合理地工作,更改函数中的切片。

[练习:将接收者的类型更改为值而不是指针,然后再次运行。解释会发生什么。]

另一方面,如果我们想为path编写一个方法,使path中的ASCII字母大写(简单地忽略非英文字母),则该方法可以传入一个切片值,因为切片值接收者仍将指向相同的底层数组。

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if 'a' <= b && b <= 'z' {
            p[i] = b + 'A' - 'a'
        }
    }
}

func main() {
    pathName := path("/usr/bin/tso")
    pathName.ToUpper()
    fmt.Printf("%s\n", pathName)
}

在这里,ToUpper方法使用for range中的两个迭代变量来捕获slice的下标和元素。

[练习:将ToUpper方法转换为使用指针接收者,并查看其行为是否发生变化。]

[高级练习:改写ToUpper方法以处理Unicode字母,而不仅仅是ASCII。]

容量

看看下面的函数,它将元素是int类型的切片扩展了一个元素:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

看看切片是如何生长的,直到不能生长为止。

现在是时候讨论切片头的第三个组成部分了:它的容量。除了数组指针和长度之外,切片头还存储其容量:

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

Capacity字段记录切片的底层数组实际拥有的空间;它是长度Length可以达到的最大值。试图将切片增长到超出其容量的程度将超出底层数组空间的限制,并引发panic

上面的代码中,我们这么创建切片:

slice := iBuffer[0:0]

它的切片头类似于如下结构:

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}

Capacity字段等于切片的底层数组的长度,减去切片的第一个元素在数组中的索引(在上述情况下为零)。如果你想查询切片的容量,可以使用Go内置函数cap:

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

make

如果我们想让切片超出其容量,该怎么办?你不做不到!根据定义,容量是切片增长的极限。但是,你可以创建一个容量更大的新数组,复制旧切片的数据到这个新数组,然后让旧切片指向这个新数组。

让我们开始创建。我们可以使用内置函数new来分配一个更大的数组,然后对结果进行切片,但使用内置函数make会更简单,它创建一个新数组并创建一个切片头来指向它。函数make接受三个参数:切片的类型、初始长度和容量。容量即make创建的用于保存切片数据的底层数组的长度。以下这个调用创建了一个长度为10的切片,还有5个的额外的空间可以扩展:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

以下这个代码片段使int切片的容量增加了一倍,但长度保持不变:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
    newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

运行此代码后,在需要再次重新分配空间之前,切片已经有更多的增长空间。

在创建切片时,长度和容量通常是相同的。内置函数make对这种常见情况有一个简写。length参数默认等于容量,因此可以省略容量参数,将两者设置为相同的值:

gophers := make([]Gopher, 10)

切片gophers的长度和容量都设置为10。

Copy

当我们在上一节中将切片的容量加倍时,我们编写了一个循环来将旧数据复制到新切片。Go有一个内置函数copy,可以让这变得更容易。它的参数是两个切片,并将数据从右侧参数复制到左侧参数。以下是我们使用copy的示例:

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

copy很智能,它只复制它可以复制的内容,并注意两个切片参数的长度。 换句话说,它复制的元素数量是两个切片长度中的小的那个。此外,copy返回一个整数值,即它复制的元素数量,尽管并不总是值得检查这个返回值。

当源切片和目标切片重叠时,copy函数也能正确处理,这意味着它可以用于在单个切片中移动元素。以下示例如何使用copy将值插入切片的中间:

// Insert函数在切片slice指定的下标index处插入元素值value,index不能超出切片slice的下标范围,并且切片slice必须还有额外容量可供插入新元素
func Insert(slice []int, index, value int) []int {
    // 先给切片slice扩展一个元素的空间
    slice = slice[0 : len(slice)+1]
    // 使用copy函数把切片slice的从index下标开始的右半部分元素,往右移动一格位置,以在index处空出一个位置
    copy(slice[index+1:], slice[index:])
    // 把值value存入index处
    slice[index] = value
    
    return slice
}

在这个函数中有几点需要注意。首先,当然,它必须返回更新后的切片,因为它的长度已经改变。其次,它使用了一种方便的简写。表达式:

slice[i:]

与以下表达式等价:

slice[i:len(slice)]

此外,尽管我们还没有使用这个技巧,但我们也可以省略切片表达式的第一个元素;它默认为零。因此:

slice[:]

指切片本身,这在对数组进行切片时很有用。以下这个表达式是创建一个“描述数组所有元素的切片”的最短的表达式:

array[:]

现在,让我们运行Insert函数:

slice := make([]int, 10, 20) // 注意创建的切片的容量要大于长度,才能插入元素
for i := range slice {
    slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)

Append函数示例

在前几节中,我们编写了一个Extend函数,该函数将切片扩展一个元素。不过,它有缺陷,因为如果切片的容量太小,该函数就会崩溃。(我们的Insert函数也有同样的问题。)现在我们已经准备好了解决这个问题的代码,所以让我们为整数切片编写一个健壮的Extend实现吧:

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // 切片容量满了,必须扩容。在这里把切片的容量变成原来的2倍
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

在这个函数里,尤为重要的是最后要返回切片,因为当源切片被重新分配容量时,得到的切片描述的是一个完全不同的底层数组。以下是一个小片段来演示填充切片时会发生什么:

slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
    slice = Extend(slice, i)
    fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
    fmt.Println("address of 0th element:", &slice[0])
}

当大小为5的初始切片被填满时,会重新分配一个底层数组。分配新数组时,第零个元素的地址和数组容量都会发生变化。

有了强大的Extend函数作为指导,我们可以编写一个更好的函数,通过多个元素来扩展切片。为此,我们使用Go的语法,在调用函数时将函数参数列表转换为切片。也就是说,我们使用Go的参数列表长度可变的函数。

让我们调用Append函数。对于第一个版本,我们可以重复调用Extend函数,这样参数列表变长的函数的机制就很清楚了。Append函数的签名如下:

func Append(slice []int, items ...int) []int

这意味着Append函数接受一个参数,即一个切片,然后是零个或多个int参数。就Append的实现而言,这些参数正是int切片的一部分:

// Append函数把items添加到切片slice。
// 第一个版本:仅仅循环调用Extend函数。
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

请注意,for range循环在items参数的元素上迭代,该参数具有隐含的类型[]int。还要注意使用空白标识符_来丢弃循环中的索引,在这种情况下我们不需要它。

试试看:

slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)

本例中的另一项新技术是,我们通过编写一个字面量来初始化切片slice,由切片的类型及其大括号中的元素组成:

slice := []int{0, 1, 2, 3, 4}

Append函数之所以有趣,还有一个原因。我们不仅可以附加元素,还可以通过使用符号来展开一个切片的所有元素:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // ...符号
fmt.Println(slice1)

当然,我们可以在Extend函数的内部,通过不超过一次的分配来提高Append函数的效率:

// 更加高效的Append版本。
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        // 重新分配容量为原来的1.5倍
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

在这里,请注意我们如何使用copy函数两次,一次是将切片数据移动到新分配的内存,另一次是将要添加的元素复制到切片slice旧数据的末尾。

试试看;行为与以前相同:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...)
fmt.Println(slice1)

Append内置函数

因此,我们得出了设计append内置函数的动机。它与我们的Append示例完全一样,具有同等的效率,但它适用于任何切片类型。

Go的一个弱点是任何泛型类型的操作都必须由运行时提供。总有一天,这种情况可能会改变,但目前,为了更容易地处理切片,Go提供了一个内置的通用的append函数。它的工作原理与我们的int切片版本相同,但适用于任何切片类型。

请记住,由于切片头总是会被append函数更新,因此需要在调用后保存返回的切片头。事实上,编译器不允许在不保存结果的情况下调用append函数。

// 创建两个切片
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

// 添加元素到切片
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

// 添加一个切片里的所有元素到另一个切片
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

// 复制一个切片,然后赋值给另一个切片
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

// 复制一个切片里的所有元素,然后追加到这个切片的尾部
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

值得花点时间详细思考以上代码的最后三行。

在社区构建的“Slice Tricks”Wiki页面上,还有更多关于函数appendcopy和其他使用切片的方法的示例。

Nil

根据我们新学到的知识,我们可以知道nil切片是什么。自然,它是切片头的零值(zero value):

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

或者仅是:

sliceHeader{}

关键细节是其指向底层数组元素的指针也是nil。由一下代码创建的切片:

array[0:0]

长度是0,也许容量也是0,但是其指向底层数组元素的指针不是nil,因此它不是nil切片。

应该清楚的是,空(empty)切片可以增长(假设它具有非零容量),但nil切片没有可放入值的数组,并且永远不能增长到容纳哪怕一个元素。

也就是说,nil切片在功能上等同于零长度的切片,即使它什么都不指向。它的长度为零,但可以被append函数使用。举个例子,看看上面的那一行代码,通过附加到一个nil切片来复制一个切片。

String

现在简要介绍一下Go中的与切片相关的字符串。

字符串实际上非常简单:它们只是只读的字节片,再加上Go语言提供了一些额外的语法支持。

因为它们是只读的,所以不需要容量(不能增长它们),但在其他方面,对于大多数目的,你可以将它们视为只读的字节片。 对于初学者,我们可以对它们进行索引以访问单个字节:

slash := "/usr/ken"[0] // 返回'/'

我们可以通过切片一个字符串来获取它的子串:

usr := "/usr/ken"[0:4] // 返回字符串"/usr"

现在,当我们切片一个字符串时,幕后发生的事情应该很明显了。

我们还可以从一个普通的字节切片,通过简单的强制类型转换从中创建一个字符串:

str := string(slice)

反过来也一样:

slice := []byte(usr)

字符串下面的数组在视图中是隐藏的;除了通过字符串之外,无法访问其内容。这意味着,当我们进行这两种转换时,必须制作数组的一个副本。Go当然会处理好这一点,所以你不必自己这么做。在这两种转换之后,对字节片底层的数组的修改就不会影响相应的字符串。

这种类似切片的字符串设计的一个重要结果是,创建子字符串非常高效。所需要做的就是创建一个字符串头。由于字符串是只读的,原始字符串和切片操作产生的字符串可以安全地共享同一个底层数组。

在Go的早期版本,字符串的最早实现是总会被分配一个底层数组,但当切片被添加到Go中时,它们提供了一个高效的字符串处理的模型。因此,在一些性能测试里表现出了巨大的加速。

当然,字符串还有很多内容可讲,另外一篇博客文章《Go中的字符串,字节,rune和字符(character)》对它进行了更深入的介绍。

结论

要了解切片是如何工作的,了解它们是如何实现的会有所帮助。有一个小数据结构,即切片头。当我们四处传递切片值时,切片头会被复制,但它指向的底层数组总是共享的。

一旦你了解了它们的工作原理,切片不仅易于使用,而且功能强大且富有表现力,尤其是在copyappend内置函数的帮助下。

更多切片相关文章

关于Go中的切片,还有很多值得学习的文章。如前所述,Wiki页面“Slice Tricks”有许多示例。Go Slices博客文章用清晰的图表描述了内存布局的细节。Russ Cox的《Go切片(slice):用法和内部结构》文章包括了对切片的讨论以及Go的一些其他内部数据结构。

还有更多的资料,但了解切片的最好方法是使用它们。