CentOS7安装PHP8的方法

前置条件

在Centos7上安装PHP8之前,必须安装EPEL(企业Linux的额外软件包)存储库。你可以通过运行以下命令进行安装:

sudo yum install epel-release

在Centos7上安装PHP8

1.将Remi存储库添加到你的CentOS7的系统中。此存储库为各种Linux发行版提供了PHP的更新版本:

sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm

2.通过运行以下命令禁用Remi存储库中的旧PHP版本的安装包:

sudo yum install yum-utils
sudo yum-config-manager --disable remi-php*
sudo yum-config-manager --disable php-5*

否则你运行sudo yum install php命令安装PHP时,可能找到的是PHP 5.x版本的安装包。

3.通过运行以下命令启用Remi存储库中的PHP 8.x版本的安装包:

sudo yum-config-manager --enable remi-php82

截至2023/09/22,Remi存储库中还没有php8.3版本的安装包,于是我们启用php8.2的安装包。

4.通过运行以下命令更新系统里的程序包列表:

sudo yum update

5.通过运行以下命令安装PHP:

sudo yum install php

如果yum说php8.2依赖httpd,但是系统里没有安装httpd。CentOS7系统的默认仓库/etc/yum.repos.d/CentOS-Base.repo里已经包含了httpd软件的安装包,执行以下命令安装:

sudo yum install httpd

成功安装httpd后,再次运行以下命令安装PHP:

sudo yum install php

可以看到yum找到的是PHP8.2版本的安装包:

键入y再按回车键,开始下载安装……

6.通过运行以下命令查看PHP是否安装成功:

php -v

如果输出如下信息,就表示我们成功安装PHP8.2了:

PHP 8.2.10 (cli) (built: Aug 29 2023 15:31:38) (NTS gcc x86_64)
Copyright (c) The PHP Group
Zend Engine v4.2.10, Copyright (c) Zend Technologies

7.通过运行以下命令安装常用PHP扩展库:

sudo yum install php-fpm php-mysqlnd

在yum解析依赖过程中的输出信息中,我们需要注意一下这些即将被安装的PHP扩展库的版本应该也是8.2,并且应该也是从remi-php82仓库里下载的。

可以通过运行以下命令查看目前安装了哪些PHP扩展库:

php -m

参考

https://baransel.dev/post/how-to-install-php8-on-centos/

https://www.tecmint.com/install-php-8-on-centos/

运行yum update更新系统时提示This system is not registered with an entitlement server. You can use subscription-manager to register

这一提示产生原因是什么?当系统安装了RHEL(RedHat)软件仓库时,可能会产生此提示。

如何禁止这一提示?有以下两种方法。

方法一,使用你喜欢的文本编辑器(nano、vi或vim)打开subscription-manager.conf配置文件:

sudo vim /etc/yum/pluginconf.d/subscription-manager.conf

设置enabled配置项的值为0:

enabled=0

保存文件并退出文本编辑器。

方法二,使用Red Hat Subscription Manager工具将RHEL系统注册并订阅到Red Hat客户的门户网站,怎么注册参考https://access.redhat.com/solutions/253273

参考

https://serverfault.com/questions/764900/how-to-remove-this-warning-this-system-is-not-registered-to-red-hat-subscriptio

Go中的字符串,字节,rune和字符(character)

本文翻译自《Strings, bytes, runes and characters in Go》。

Rob Pike

2013/08/23

介绍

上一篇博客文章解释了切片(slice)在Go中的工作方式,并使用了一些示例来说明它背后的机制。在此背景下,本文将讨论Go中的字符串。起初,字符串对于一篇博客文章来说可能太简单了,但想要很好地使用它们,不仅需要了解它们是如何工作的,还需要了解字节、字符(character)和符文(rune)之间的区别,Unicode和UTF-8之间的区别、字符串和字符串字面量(string literal)之间的区别以及其他更微妙的区别。

编写这个话题的一种方法是,给出常见问题的答案,例如“当我在位置n索引Go字符串时,为什么我得不到第n个字符?”,正如你所看到的,这个问题的答案可以让我们了解文本在现代世界中是如何工作的。

Joel Spolsky的著名博客文章“每个软件开发人员绝对、积极地必须了解的Unicode和字符集”,是对其中一些问题的一个极好的介绍,它独立于Go语言。它提出的许多观点将在这里重复提及。

什么是字符串?

让我们从一些基础知识开始。

在Go中,一个字符串实际上是一个只读字节片。如果你不确定字节片是什么或者它是如何工作的,请阅读上一篇博客文章;我们在这里假设你已阅读。

重要的是要提前声明:一个字符串可以包含任意的字节,不一定是Unicode文本、UTF-8文本或任何其他预定义的格式。就字符串的内容而言,它完全等同于一个字节片。

下面是一个字符串字面量(稍后将详细介绍),它使用\xNN表示法来定义一个包含一些特殊字节值的字符串常量。(当然,字节的范围从十六进制值00到FF,包括00和FF。)

const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"

打印输出字符串

因为我们的示例字符串中的一些字节不是有效的ASCII,甚至不是有效的UTF-8,所以直接打印字符串会产生难看的输出。以下是简单的打印输出这个字符串的语句:

fmt.Println(sample)

产生这种混乱的输出(确切的输出与你的系统环境有关,不同的系统环境可能有不同的输出):

��=� ⌘

为了弄清楚这个字符串里到底装着什么,我们需要把它拆开,检查一下每个部分。有几种方法可以做到这一点。最明显的是对其内容进行循环,并单独取出字节,如以下for循环中所示:

for i := 0; i < len(sample); i++ {
        fmt.Printf("%x ", sample[i])
    }

正如上面所示,对字符串进行索引访问到的是单个字节,而不是一个个字符(character)。我们将在下面详细讨论这个主题。现在,让我们只使用字节。这是逐字节遍历循环的输出:

bd b2 3d bc 20 e2 8c 98

注意各个字节如何与定义字符串的十六进制转义符相匹配。 把混乱的字符串输出为人类可读的形式的较简单的方法是,使用fmt.Printf%x(十六进制数)格式。它将字符串的顺序字节输出为十六进制数字,每个字节对应两个十六进制数字。

fmt.Printf("%x\n", sample)

输出如下:

bdb23dbc20e28c98

你可以与之前的输出比较一下。

一个很好的技巧是在该格式中使用“空格”标志,在%x之间加一个空格:

fmt.Printf("% x\n", sample)

输出如下:

bd b2 3d bc 20 e2 8c 98

注意字节之间的空格。

还有更多。%q(带引号)格式将转义字符串中任何不可打印的字节序列,因此输出是明确的。

当字符串的大部分内容可以理解为文本,但也有一些特殊字符需要清除时,这种技巧很方便;对于上文中的字符串,它输出:

"\xbd\xb2=\xbc ⌘"

如果我们注视一下,我们可以看到隐藏在噪音中的是一个ASCII等号和一个普通空格,最后出现了著名的瑞典“兴趣地点(Place of Interest)”符号。该符号的Unicode码值为U+2318,被编码为UTF-8字节:e2 8c 98,位于空格(十六进制值20)之后。

如果我们对字符串中的奇怪字符感到陌生或困惑,我们可以在%q格式中使用“加号+”标志。此标志不仅转义不可打印的字节序列,而且转义任何非ASCII字节,都按UTF-8编码来解析。结果是,它打印输出了格式正确的UTF-8编码的Unicode码值,该值表示字符串中的非ASCII数据:

fmt.Printf("%+q\n", sample)

使用该格式,上述瑞典语符号的Unicode值显示为\u开头的转义符:

"\xbd\xb2=\xbc \u2318"

这些打印输出技巧在调试字符串内容时很有用,在后续的讨论中也很方便。同样值得指出的是,所有这些方法对字节片的行为与对字符串的行为完全相同。

以下是我们在上文列出过的打印输出的选项(标志),作为一个完整的程序示例给出:

package main

import "fmt"

func main() {
    const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"

    fmt.Println("Println:")
    fmt.Println(sample)

    fmt.Println("Byte loop:")
    for i := 0; i < len(sample); i++ {
        fmt.Printf("%x ", sample[i])
    }
    fmt.Printf("\n")

    fmt.Println("Printf with %x:")
    fmt.Printf("%x\n", sample)

    fmt.Println("Printf with % x:")
    fmt.Printf("% x\n", sample)

    fmt.Println("Printf with %q:")
    fmt.Printf("%q\n", sample)

    fmt.Println("Printf with %+q:")
    fmt.Printf("%+q\n", sample)
}

[练习:修改上面的例子,输出字节切片而不是字符串。提示:使用转换来创建切片。]

[练习:在每个字节上使用%q格式对字符串进行循环。输出会告诉你什么?]

UTF-8和字符串字面量

正如我们所看到的,对字符串进行索引会返回字节,而不是字符(character):字符串只是一堆字节。这意味着,当我们在字符串中存储一个字符值时,我们是按字节存储它的。让我们看一个更可控的例子,看看这是如何发生的。

这里有一个简单的程序,它用三种不同的方式打印带有单个字符的字符串常量,一种是打印输出纯字符串,一种是只打印输出ASCII字符,还有一种是打印输出十六进制数的单个字节。为了避免混淆,我们创建了一个“原始字符串(raw string)”,用后引号(back quotes)括起来,这样它就只能包含字符串字面量。(用双引号括起来的常规字符串里面可以包含转义字符,如上文所示。但用后引号括起来的原始字符串里面的字符不会被转义。)

func main() {
    const placeOfInterest = `
⌘`

    fmt.Printf("plain string: ")
    fmt.Printf("%s", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("quoted string: ")
    fmt.Printf("%+q", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("hex bytes: ")
    for i := 0; i < len(placeOfInterest); i++ {
        fmt.Printf("%x ", placeOfInterest[i])
    }
    fmt.Printf("\n")
}

输出:

plain string: ⌘
quoted string: "\u2318"
hex bytes: e2 8c 98

这提醒我们,Unicode码值U+2318,即符号,在字符串中由字节e2 8c 98表示,并且这些字节是十六进制数2318的UTF-8编码。

根据你对UTF-8的熟悉程度,它可能很明显,也可能很微妙,但值得花点时间解释一下字符串的UTF-8表示是如何创建的。简单的事实是:它是在编写源代码时创建的。

Go语言的源代码被定义为UTF-8文本;不允许使用其他编码。这意味着,当我们在源代码中编写以下文本时

`⌘`

用于编写源代码的文本编辑器将符号⌘的UTF-8编码放入源代码文本中。当我们打印输出十六进制数的字节时,我们只是简单地输出文本编辑器放置在文件中的字节数据。

简而言之,Go的源代码是UTF-8文本,因此其字符串字面量也是UTF-8文本。如果该字符串字面量里不包含转义序列(原始字符串就不包含),则构造的字符串就是引号之间的源代码文本。因此,通过定义和构造,原始字符串将始终包含其内容里的有效UTF-8文本。类似地,除非像本文开头示例中的字符串(用\xNN表示法来定义一个包含一些特殊字节值的字符串字面量)那样包含不能被解析为UTF-8编码的字节序列,否则普通字符串字面量也将始终包含有效的UTF-8文本。

有些人认为Go字符串总是UTF-8文本,但事实并非如此,正如我们在本文开头所展示的,字符串值可以包含任意字节,里面可能包含不能被解析为UTF-8编码的字节序列。

总之,Go字符串可以包含任意字节,但当我们从字符串字面量(非\xNN表示法)构建字符串时,里面的字节序列(几乎总是)符合UTF-8编码的。

码点(Code point)、字符(character)和rune

到目前为止,我们在使用“字节(byte)”和“字符(character)”这两个词时非常小心。这部分是因为字符串包含字节,部分是因为“字符”的概念有点难以定义。Unicode标准使用术语“码点(code point,也有翻译为‘码值’的)”来指代由单个数字表示的字符。例如码点U+2318,具有十六进制数值2318,表示符号“⌘”。(有关该码点的更多信息,请参阅其Unicode页面。)

举一个更普通的例子,Unicode码点U+0061是小写拉丁字母“a”。

但是小写带重音的字母“à”呢?这也是一个字符,也是一个码点(U+00E0),但它有其他表示形式。例如,我们可以使用“组合”重音码点U+0300,并将其附加到小写字母a(码点是U+0061),来创建相同的字符“à”。通常,一个字符可以由许多不同的码点序列表示,因此也可以编码为不同的UTF-8字节序列。

因此,计算机中的字符(character)的概念是模糊的,或者至少是令人困惑的,所以我们谨慎地使用它。为了使事情变得可靠,有一些规范化的技术可以保证给定的字符总是由相同的码点表示,但这个主题偏离本文的主题太远了。稍后的博客文章将解释Go库如何解决规范化问题。

“码点”这个词有点晦涩难懂,所以Go为这个概念引入了一个较短的术语:rune。这个术语出现在库和源代码中,其含义与“码点”完全相同,还有一个有趣的补充。

Go语言将rune定义为类型int32类型的别名,因此当整数值表示码点时,程序就会很清晰。此外,你可能认为的字符常量在Go中被称为“rune常量”。例如'⌘'的类型是rune,值是整数0x2318

总之,以下是一些重点:

  • Go源代码总是UTF-8文本。
  • 字符串可以包含任意字节。
  • 字符串字面量,不存在字节级转义字符的话,始终包含有效的UTF-8字节序列。
  • 代表Unicode码点的序列,称为rune。
  • Go中不能保证字符串中的字符是标准化的。

范围循环

除了Go源代码是UTF-8文本之外,实际上Go还有一个特殊对待UTF-8的地方,那就是在字符串上使用for range循环时。

我们已经看到了普通for循环的情况。相比之下,for range循环在每次迭代中解码一个UTF-8编码的rune。每次循环时,循环的索引是当前rune的起始位置,以字节为单位,循环的值是当前rune的Unicode码点。下面是一个使用另一种方便的Printf函数的格式%#U的示例,它显示了rune的Unicode码点的值及其打印输出的字符:

    const nihongo = "日本語"
    for index, runeValue := range nihongo {
        fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
    }

输出显示每个Unicode码点如何占用多个字节:

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

[练习:将一个非法的UTF-8字节序列放入字符串中。循环的迭代会发生什么?]

Go的标准库为解析UTF-8文本提供了强大的支持。

如果for range循环不足以满足你的目的,那么你需要的设施很可能是由库中的包提供的。 最重要的包是unicode/utf8,它包含用于验证、反组装(disassemble)和重新组装UTF-8字符串的辅助函数代码。这里有一个与上面的for range示例等效的程序,但使用该包中的DecodeRunInString函数来完成这项工作。函数的返回值是rune及其宽度(以UTF-8编码的字节为单位)。

    const nihongo = "日本語"
    for i, w := 0, 0; i < len(nihongo); i += w {
        runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
        fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
        w = width
    }

运行它以查看它是否执行相同的操作。for range循环和DecodeRunInString函数被定义为生成完全相同的迭代序列。

你可以查看unicode/utf8包的官方文档,了解它还提供了哪些其他功能。

结论

为了回答开头提出的问题:字符串是从字节构建的,因此对字符串进行索引会产生字节,而不是字符(character)。字符串甚至可能不包含字符(character)。事实上,“字符(character)”这一定义是模糊的,试图通过“定义字符串是由字符组成的”来解决二义性是一种错误的做法。

关于Unicode、UTF-8和多语言文本处理,还有很多话要说,但这应该写成另一篇文章。目前,我们希望你能更好地了解Go字符串的行为,尽管它们可能包含任意字节,但UTF-8是其设计的核心部分。

在Elasticsearch的文档中如何管理嵌套对象(nested objects)?

本文翻译自《How to manage nested objects in Elasticsearch documents》。

如何使用Update API和painless脚本在Elasticsearch文档中添加、更新和删除嵌套对象。

2019/05 /02

在这篇文章中,我们将管理使用Elasticsearch索引的文档的嵌套对象(nested objects)

嵌套类型(nested)是对象类型(object)的一个特殊版本,它对对象数组进行索引,以使数组的元素可以被相互独立地查询。

先决条件

要跟随此帖子继续下去,你需要:

  • 一个正在运行的Elasticsearch实例。我在这里用6.7版本
  • 一个正在运行的Kibana实例,用于与Elasticsearch交互

准备

我们创建一个索引iridakos_nested_objects,它有一个名为human的类型(type)(Elasticsearch 8开始没有type这个概念了),其中有一个嵌套对象cats。

创建这个索引

打开Kibana开发控制台并键入以下内容以创建这个索引。

PUT iridakos_nested_objects
{
  "mappings": {
    "human": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
  }
}

// ES 8版本的语句
PUT iridakos_nested_objects
{
  "mappings": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
}

返回响应信息:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "iridakos_nested_objects"
}

human具有:

  • text类型的name属性
  • 嵌套类型(nested)的cats属性

每只cat都有:

  • integer类型的colors属性
  • text类型的name属性
  • text类型的一个品种特性

添加一个human

在Kibana控制台中,执行以下操作以添加一个human和三只cat。

POST iridakos_nested_objects/human/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

// ES 8版本的语句
PUT iridakos_nested_objects/_doc/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

你应该能看到类似以下的响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      }
    ]
  }
}

管理嵌套对象

添加一个新的嵌套对象

假设iridakos得到了一只新的波斯猫,名叫Leon。要将其添加到iridakos索引的cats集合中,我们将使用更新API。 在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了human的嵌套对象cats。这给了我们一个集合(collection)
  • 我们对集合执行了add方法以添加一只新的cat
  • 新cat(params.cat)作为参数传递给add方法,其属性在params中给出。

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见,已插入Leon到cats集合。

删除一个嵌套对象

假设我们想把Nino从cats集合中移除。在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对集合执行removeIf方法,以有条件地移除里面的条目项
  • 我们为removeIf方法提供了一个谓词(Predicate),在该方法中指定要删除的条目项。此谓词将在集合的每个条目项上执行,并返回为布尔值。如果返回true,则该条目项将被删除。在我们的例子中,条件是对cat的name属性进行简单的相等性检查。
  • cat_name是作为参数(params.cat_name)传递的,而不是将其固定到source脚本里。

执行以下语句查看一下是否删除Nino成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见name为Nino的cat信息已在cats集合中删除。

更新嵌套对象

假设我们想把所有的猫品种从欧洲(European)改为欧洲短毛猫(European Shorthair)(在我们的案例中目前只有Phoebe的品种是European)。

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对cats集合执行了findAll方法来选择特定的条目项
  • 我们为findAll方法提供了一个谓词(Predicate),在其中我们指定要选择的条目项。此谓词将在集合的每个条目项上执行,并返回布尔值。如果返回true,就会选择该条目项。在我们的例子中,条件是对猫的品种(breed)属性进行简单的相等性检查。
  • current_breed作为参数(params.current_breed)传递,而不是将其固定到source脚本里。
  • 然后,我们循环遍历选中的猫(其品种属性值为European),并将它们的品种更改为我们通过另一个参数params.breed传递的新值。

查看是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见更新成功。

更新满足多个条件的嵌套对象的多个属性

现在,在一个更高级的示例中,我们将使用一个更灵活的脚本来:

  • 基于多种条件来匹配目标对象(此处为color和breed)
  • 更新多个属性(此处为color和breed)

假设我们想改变有4种颜色的猫的品种,它们的品种从Persian到Aegean,它们的颜色改为3种。使用下面的脚本:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

为了方便起见,下面是带有适当缩进的脚本的源代码。

注意事项:

  • 我们通过检查cat的属性是否具有params.conditions中指定的值来选择要更新的cat。
  • 对于每个选定的cat,我们按照params.changes中给出的值更改它对应的属性值。

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 5,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

确认一下是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "name": "Leon",
        "colors": 3,
        "breed": "Aegean"
      }
    ]
  }
}

可见,更新成功。

参考

https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

https://www.elastic.co/guide/en/elasticsearch/painless/6.7/painless-api-reference.html

Go模糊测试

本文翻译自《Go Fuzzing》。

从Go 1.18开始,Go的标准工具链支持模糊测试。OSS-fuzz支持原生的Go模糊测试。

尝试一下教程:模糊测试入门

概览

模糊测试是一种自动测试,它不断地操纵程序的输入来发现Bug。Go的模糊测试使用覆盖率导向来智能地遍历被模糊测试的代码,以查找Bug并向用户报告。由于模糊测试可以触及程序员经常错过的边缘情况,因此它对于发现安全漏洞和Bug尤其有用。

下面是一个模糊测试的例子,突出显示了它的主要组件。

编写模糊测试

要求

以下是模糊测试必须遵循的规则:

  • 模糊测试必须使用一个名为FuzzXxx的函数,它只接受一个*testing.F参数,并且没有返回值。
  • 模糊测试的代码必须放在*_test.go文件中才能运行。
  • 模糊测试的目标必须是对(*testing.F).fuzz方法的调用,该方法接受*testing.T作为第一个参数,然后是用于模糊测试的参数,没有返回值。
  • 每个模糊测试必须只有一个目标。
  • 所有种子语料库的条目,都必须匹配模糊测试的目标函数的参数的类型,并且顺序也要相同。对(*testing.F).Add和模糊测试的testdata/fuzz目录中的任何语料库文件的调用都是如此。
  • 模糊测试的参数只能是以下类型:string, []byte、int, int8, int16, int32/rune, int64、uint, uint8/byte, uint16, uint32, uint64、float32, float64、bool

建议

以下是一些建议,可以帮助你充分利用模糊测试:

  • 模糊测试的目标函数应该是能快速运行完毕的和结果确定的,这样模糊测试的引擎才能有效地工作,Bug和代码覆盖率可以很容易地再现。
  • 由于模糊测试的目标函数是在多个工作线程之间以不确定的顺序并行执行的,因此模糊测试的目标函数的状态不应持续到每次调用结束,其行为也不应依赖于全局状态。

运行模糊测试

有两种运行测试的模式:使用单元测试(默认的go test),或者使用模糊测试(go test -fuzz=FuzzTestName)。

默认情况下,模糊测试的运行方式与单元测试非常相似。每个种子语料库条目都将针对模糊测试的目标函数进行测试,在退出之前报告任何发现的Bug。

要启用模糊测试,请使用-fuzz标志运行go test,提供一个匹配单个模糊测试函数的正则表达式。默认情况下,该包中的所有其他测试都将在模糊测试开始之前运行。这是为了确保模糊测试不会报告任何现有测试已经发现了的问题。

请注意,运行模糊测试的时间长短由你决定。如果模糊测试的执行没有发现任何错误,那么它很有可能无限期地运行下去。未来将支持使用OSS fuzz等工具持续运行这些模糊测试,请参阅Issue#50192

注意:模糊测试应该在支持代码覆盖率检测的平台上运行(目前AMD64和ARM64平台都支持),这样语料库就可以在运行时有意义地增长,并且在模糊测试的同时可以覆盖到更多的代码。

命令行输出

当模糊测试正在运行时,模糊测试的引擎生成新的输入数据,并使用模糊测试的目标函数运行它们。默认情况下,它会继续运行,直到找到一个导致测试失败的输入数据,或者用户终止测试程序(例如使用组合键Ctrl^C)。

输出将如下所示:

~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok      foo 12.692s

第一行显示了“基线代码覆盖率”,是在模糊测试开始之前收集的。

为了收集基线代码覆盖率,模糊测试的引擎会执行种子语料库生成的语料库,以确保没有错误发生,并了解到现有语料库已经提供的代码覆盖率是多少。

以下几行信息提供了对模糊测试的执行过程的深入了解:

  • elapsed:测试开始后过去的时间。
  • execs:针对模糊测试的目标函数运行的输入数据总数(后面括号里的数据是统计每秒运行了多少数据)。
  • new interesting:在模糊测试执行期间添加到生成的语料库中的“interesting”输入数据的总数(整个语料库的总大小)

为了让输入数据变得“interesting”,它必须将代码覆盖范围扩展到现有的语料库(种子语料库和之前生成的语料库)所能达到的范围之外。通常情况下,新的interesting的数据在测试的一开始时快速增长,最终放缓,随着新的代码分支的发现,偶尔会爆发式增长。

随着语料库中的数据开始覆盖更多的代码行,你应该会看到新的interesting数字随着时间的推移逐渐减少,如果模糊测试的引擎找到了新的代码路径,interesting数字偶尔会爆发式增长。

导致测试失败的输入数据

模糊测试时可能会出现失败,原因有以下几种:

  • 代码或测试中引发panic
  • 模糊测试的目标函数里面调用了t.Fail函数,可以直接调用,也可以通过t.Errort.Fatal等函数简介调用。
  • 出现不可恢复的错误,例如os.Exit或堆栈溢出。
  • 模糊测试的目标函数花了太长时间才完成。目前,执行目标函数的超时时间是1秒。这可能是由于死锁或无限循环,或者代码就是要执行很长时间。这就是为什么我们在上文建议你的目标函数执行得要快的原因之一。

如果发生了一个错误,模糊测试的引擎将尝试将输入数据最小化到尽可能小、人类最容易读的值,这个值仍然会引发这个错误。要对此进行配置,请参阅下文自定义设置小节。

最小化完成后,模糊测试的引擎将记录错误消息,输出将以以下内容作为结束信息:

 Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
    To re-run:
    go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL    foo 0.839s

模糊测试的引擎会将这个导致测试失败的输入数据写入模糊测试的种子语料库,现在它将被go test命令默认运行,在修复这个Bug后用作回归测试。

你的下一步将是诊断这个Bug并修复,通过重新运行go test命令来验证是否修复成功。如果修复成功,就与对应的测试数据文件(作为回归测试)一起提交补丁。

默认的go命令的设置应该适用于模糊参数的大多数用例。因此,通常情况下,在命令行上执行模糊参数应该如下所示:

$ go test -fuzz={FuzzTestName}

但是,go命令在运行模糊测试时确实提供了一些设置,在cmd/go软件包的文档中进行了说明。

以下几个设置值得关注:

  • -fuzztime:模糊测试的目标函数在退出之前执行的总时间或迭代次数,默认为无限期。
  • -fuzzminimizetime:在每次最小化尝试期间,执行模糊测试的目标函数的时间或迭代次数,默认为60秒。你可以通过设置-fluzzminimizetime 0来完全禁用最小化。
  • -parallel:每一次运行模糊测试时的进程数,默认为$GOMAXPROCS。目前,在模糊测试期间设置-cpu标志没有任何效果。

语料库文件的格式

语料库文件以一种特殊的格式编码。种子语料库程序生成的语料库都使用相同的格式。

以下是语料库文件的一个示例:

go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)

第一行用于通知模糊测试的引擎,语料库文件使用的编码的版本。尽管目前还没有计划编码格式的未来版本,但设计时必须支持这种可能性。

下面的每一行都是组成语料库条目的值,如果需要,可以直接复制到Go代码中。

在上面的例子中,我们有一个[]byte,后面跟一个int64。这些类型必须按顺序与模糊测试的目标函数的参数完全匹配,如下所示:

f.Fuzz(func(*testing.T, []byte, int64) {})

指定自己的种子语料库的条目的最简单方法是使用(*testing.F).Add方法。在上面的例子中,看起来是这样的:

f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))

然而,你可能有一些大的二进制文件,不希望将其作为代码复制到测试中,而是保留为testdata/fuzz/{FuzzTestName}目录中的单个种子语料库的条目。golang.org/x/toolscmd/file2fuzz上的file2fuzz工具可用于将这些二进制文件转换为[]byte编码的语料库文件。下载然后安装这个工具:

$ go install golang.org/x/tools/cmd/file2fuzz@latest

查看帮助:

$ file2fuzz -h

资源

教程:

文档:

  • testing包的文档描述了测试,包括编写模糊测试时使用的testing.F类型。
  • cmd/go包的文档描述了与模糊相关联的标志。

技术细节:

术语

语料库条目(corpus entry):语料库中的一个输入,可以在进行模糊测试时使用。这可以通过一个特殊格式的文件给出,也可以通过调用 (*testing.F).Add函数添加。

覆盖率导向(coverage guidance):一种模糊测试方法,它根据代码覆盖率是否扩展来确定哪些语料库条目值得保留以备将来使用。

失败的输入数据(failing input):失败的输入数据是指一个语料库条目,让模糊测试的目标函数再运行时导致运行出错或引发panic

模糊测试的目标函数(fuzz target):模糊测试的目标函数,在进行模糊测试时执行语料库条目和模糊测试的引擎生成的测试数据。把函数传递给 (*testing.F).Fuzz,就变成了模糊测试的目标函数。

模糊测试(fuzz test):测试文件中的一个函数,形式为FuzzXxx(*testing.F),可用于模糊测试。

模糊测试的过程(fuzzing):一种自动测试,它不断地操纵程序的输入,以发现代码里可能的Bug或容易受到攻击的漏洞等问题。

模糊测试的参数(fuzzing arguments):将传递给模糊测试的目标函数并由变异器(mutator)进行变化的参数值。

模糊测试的引擎(fuzzing engine):一个管理模糊测试的工具,功能包括维护语料库、调用变异器、识别新的代码覆盖率和报告发现的Bug。

生成的语料库(generated corpus):由模糊测试的引擎在进行模糊测试时随时间维护的语料库,以跟踪测试进度。它存储在$GOCACHE/fuzz目录中。这些条目仅在模糊测试时使用。

变异器(mutator):模糊测试时使用的一种工具,在将语料库条目传递给模糊测试的目标函数之前,随机地修改它们。

(package):同一个目录中的Go源文件的集合,这些文件被编译在一起。请参阅Go语言规范中的Packages部分

种子语料库(seed corpus):用户提供的用于模糊测试的语料库,用于引导模糊测试的引擎。它由f.Add函数添加的语料库条目,以及testdata/fuzz/{FuzzTestName}目录中的文件组成。默认情况下,无论是否模糊测试,这些条目都会被go test运行。

测试文件(test file):xxx_test.go文件,可能包含普通测试、基准测试、示例和模糊测试等代码。

漏洞(vulnerability):代码中对安全性敏感的弱点,可被攻击者利用。

反馈

如果你遇到任何问题或对某个功能有想法,请提交一个issue

对于有关该功能的讨论和一般性反馈,你也可以参加Gophers Slack中的#fuzzing频道

库和框架有什么区别?

库的英语为Library(简写为Lib),框架的英语为Framework。

库主要是为了复用解决了某一个(某一类)问题的代码,将代码组织形成一个产品,供程序员下载、加入自己的项目。面向对象的代码组织形成的库也叫类库。面向过程的代码组织形成的库也叫函数库。在函数库中的可直接调用的函数叫库函数。

开发者在使用库的时候,一般只需使用库中的一部分类或函数,解决自己的某个问题,实现自己的功能。

框架则主要为了解决某个较大领域里的多个问题而开发的产品。框架用户一般只需要使用框架提供的类或函数,即可实现全部功能。可以说,框架是库的升级版,一个框架由多个库组成。

开发者在使用框架的时候,必须使用这个框架的全部代码。

以前和一些老程序员讨论,到底什么是framework,什么是library,为啥都说Spring是framework,jackson是library?结论是你得把代码交给framework管理,但是却可以自由的使用library。有一定编程经验的人,有时候会觉得framework限制了自己的表达能力,从而倾向于自己写library给自己用。

所谓:you call library,framework calls you。

假如我们要买一台电脑。框架为我们提供了已经装好的电脑,我们只要买回来就能用,但你必须把整个电脑买回来。这样用户自然轻松许多,但会导致很多人用一样的电脑,或你想自定义某个部件将需要修改这个框架。而库就如自己组装的电脑。不同的库为我们提供了不同的部件,我们需要自己组装电脑,如果某个部件库未提供,我们也可以自己做。库在开发小程序时使用起来非常灵活,但开发大型程序时,使用框架会更加方便。

参考

https://www.zhihu.com/question/521822847/answer/3141874937

https://www.freecodecamp.org/news/the-difference-between-a-framework-and-a-library-bd133054023f/

https://stackoverflow.com/questions/148747/what-is-the-difference-between-a-framework-and-a-library

教程:模糊测试入门

本文翻译自《Tutorial: Getting started with fuzzing》。

本教程介绍Go中模糊测试的基本知识。通过模糊测试,随机数据会针对你的测试程序运行,试图找到程序漏洞或导致程序崩溃的输入。模糊测试可以发现的一些漏洞包括SQL注入、缓冲区溢出、拒绝服务和跨站点脚本攻击等。

有关本教程中的术语,请参阅Go Fuzzing词汇表

你将完成以下部分:

1 为代码创建一个文件夹

2 添加要测试的代码

3 添加一个单元测试

4 添加一个模糊测试

5 修复两个Bug

6 探索其他资源

注意:有关其他教程,请参见教程

注意:Go fuzzing目前支持Go fuzzing文档中列出的一个内置类型子集,并会在未来添加更多的内置类型。

先决条件

  • 安装Go 1.18或更高版本。有关安装说明,请参阅安装Go
  • 用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
  • 一种命令行终端。Go在Linux和Mac上使用任何终端都能很好地工作,以及Windows中的PowerShell或CMD。
  • 支持模糊测试的运行环境。带覆盖检测的Go fuzzing目前仅在AMD64和ARM64架构上可用。

为代码创建一个文件夹

首先,为要编写的代码创建一个文件夹。

1 打开命令提示符并转到家目录。 在Linux或Mac上:

$ cd

在Windows:

C:\> cd %HOMEPATH%

本教程的其余部分将显示一个$作为提示符。本教程使用的命令也适用于Windows。

2.在命令提示符下,为代码创建一个名为fuzz的目录。

$ mkdir fuzz
$ cd fuzz

3.创建一个模块来保存代码。

运行go mod init命令,给出代码的模块路径:

$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz

注意:对于生产代码,你应该指定一个更适合你自己需求的模块路径。有关详细信息,请参阅管理依赖关系

接下来,你将添加一些简单的代码来反转字符串,稍后我们将对其进行模糊测试。

添加要测试的代码

在这一步中,你将添加一个函数来反转字符串。

编写代码

1.使用文本编辑器,在fuzz目录中创建一个名为main.go的文件。

2.在main.go文件顶部,粘贴以下包声明:

package main

独立程序(与库相对)始终位于main包中。

3.在包声明下面,粘贴以下函数声明:

func Reverse(s string) string {
    b := []byte(s)
    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}

此函数将接受一个字符串,一次循环一个字节,最后返回反转后的字符串。

:此代码基于golang.org/x/example中的stringutil.Revirse函数。

4.在main.go的顶部,在包声明的下面,粘贴以下main函数来初始化字符串,反转它,打印输出,然后重复:

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev := Reverse(input)
    doubleRev := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q\n", rev)
    fmt.Printf("reversed again: %q\n", doubleRev)
}

此函数将运行一些反转字符串的操作,然后将输出打印到命令行。这有助于查看运行中的代码,并可能有助于调试。

5.main函数使用fmt包,因此需要导入它。 第一行代码应该如下所示:

package main

import "fmt"

运行代码

在包含main.go的目录的命令行中,运行代码:

$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"

你可以看到原始字符串,反转后的结果,然后再次反转的结果,这等于原始字符串。

现在代码已经能够运行,是时候对其进行测试了。

添加一个单元测试

在这一步中,你将为Reverse函数编写一个基本的单元测试。

编写代码

1.使用文本编辑器,在fuzz目录中创建一个名为reverse_test.go的文件。

2.将以下代码粘贴到reverse_test.go中:

package main

import (
    "testing"
)

func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev := Reverse(tc.in)
        if rev != tc.want {
                t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}

这个简单的测试将断言列出的输入字符串将被正确地反转。

运行代码

使用go test运行单元测试:

$ go test
PASS
ok      example/fuzz  0.013s

接下来,你将把单元测试更改为模糊测试。

添加一个模糊测试

单元测试有局限性,即每个输入都必须由开发人员添加到测试中。模糊测试的一个好处是,它为你的代码提供输入,并可能识别出你给出的测试用例没有达到的边缘条件。

在本节中,你将把单元测试转换为模糊测试,这样你就可以用更少的工作生成更多的输入!

请注意,你可以将单元测试、基准测试和模糊测试放在同一个*_test.go文件中,但在本例中,你将把单元测试转换为模糊测试。

编写代码

在文本编辑器中,将reverse_test.go中的单元测试替换为以下模糊测试:

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // 使用f.Add函数提供一个种子测试用例
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

模糊测试也有一些局限性。在单元测试中,你可以预测Reverse函数的预期输出,并验证实际输出是否满足这些预期。

例如,在测试用例Reverse("Hello, world")中,单元测试预期其输出为"dlrow, olleH"

使用模糊测试时,你无法预测输出,因为你无法控制输入。

不过,你可以在模糊测试中验证Reverse函数的一些特性。在这个模糊测试中检查的两个属性是:

  • 将字符串反转两次后将和原始值相同。
  • 反转后的字符串的编码应该是有效的UTF-8。

请注意单元测试和模糊测试之间的语法差异:

  • 该函数以FuzzXxx而不是TestXxx开始,并且参数类型采用*testing.F而不是*test.T
  • 在你期望看到t.Run的地方,你会看到f.Fuzz,它接受一个目标函数,其参数的类型是*testing.T和要模糊测试的类型。单元测试的输入使用f.Add作为一个种子语料库提供给模糊测试。

请确保已导入新的程序包unicode/utf8

package main

import (
    "testing"
    "unicode/utf8"
)

随着单元测试转换为模糊测试,是时候再次运行测试了。

运行代码

1.在不模糊的情况下运行模糊测试,以确保种子输入通过。

$ go test
PASS
ok      example/fuzz  0.013s

如果该文件中有其他测试,并且你只希望运行模糊测试,那么你可以执行go test -run=FuzzReverse

2.运行FuzzReverse函数进行模糊测试,查看任何随机生成的字符串作为输入是否会导致该函数执行失败。这也是使用go test命令执行的,使用一个新的标志-fuzz,设置它的参数值为Fuzz

$ go test -fuzz=Fuzz

另一个有用的标志是-fuzzztime,它限制模糊测试所能花费的时间。例如,在下面的测试中指定-fuzztime 10s意味着,只要之前没有发生测试失败,测试将在10秒后退出。请参阅cmd/go文档的这一部分,以查看其他标志。

现在,运行刚才复制的命令:

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: minimizing 38-byte failing input file...
--- FAIL: FuzzReverse (0.01s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"

    Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    To re-run:
    go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
FAIL
exit status 1
FAIL    example/fuzz  0.030s

模糊测试时如果发生测试失败,导致问题的输入就会被写入种子语料库文件,该文件将在下次调用go test时运行,即使没有使用-fuzz标志。要查看导致测试失败的输入,请在文本编辑器中打开testdata/fuzz/FuzzReverse目录里的语料库文件。种子语料库文件可能包含不同的字符串,但格式是一样的。

go test fuzz v1
string("泃")

语料库文件的第一行指示编码版本。下面的每一行表示组成语料库条目的每种类型的值。由于本例模糊测试的目标只接受1个输入,因此版本号之后只有1个值。

3.在没有使用-fuzz标志的情况下再次运行go test;将使用新的导致测试失败的种子语料库条目:

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
        reverse_test.go:20: Reverse produced invalid string
FAIL
exit status 1
FAIL    example/fuzz  0.016s

既然我们的测试失败了,是时候调试了。

修复两个Bug

修复非法字符串Bug

在本节中,你将调试故障并修复Bug。

在继续之前,你可以花一些时间思考这个问题,并尝试自己解决这个问题。

诊断这个错误

有几种不同的方法可以调试此错误。如果使用VS代码作为文本编辑器,则可以设置调试器进行调查。

在本教程中,我们将把有用的调试信息记录到终端。 首先,考虑utf8.ValidString的文档:

ValidString reports whether s consists entirely of valid UTF-8-encoded runes.

当前的Reverse函数逐字节反转字符串,这就是我们的问题所在。为了保留原始字符串的UTF-8编码的字符,我们必须逐字符反转字符串。

检查为什么输入(在本例是汉字“泃”) 导致反转时Reverse生成一个无效字符串,你可以检查反转后的字符串中的字符个数。

编写代码

在文本编辑器中,用以下内容替换FuzzReverse函数中的模糊测试的目标函数:

f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
    if orig != doubleRev {
        t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
        t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
})

如果发生错误,或者使用-v执行测试,则此t.Logf将打印日志信息到命令行,这可以帮助你调试此特定问题。

运行代码

使用go test运行测试:

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
        reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL    example/fuzz    0.598s

整个种子语料库使用的字符串,其中每个字符都是一个字节。但是,诸如“泃”字符可能占用几个字节。因此,逐字节反转字符串将使多字节字符无效。

注意:如果你对Go如何处理字符串感到好奇,请阅读博客文章《Go中的字符串、字节、rune和字符》,以获得更加深入的理解。

对上述Bug有了更好的了解后,请你更正Reverse函数中的错误。

修复Bug

要更正Reverse函数,让我们按rune而不是按字节遍历字符串。

编写代码

在文本编辑器中,将现有的Reverse函数替换为以下函数:

func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

关键的区别在于,Reverse现在迭代字符串中的每个符文,而不是每个字节。

运行代码

1.使用go test运行测试

$ go test
PASS
ok      example/fuzz  0.016s

现在测试通过了!

2.用go test -fuzz再次模糊测试它,看看是否有任何新的bug

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
fuzz: minimizing 506-byte failing input file...
fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
--- FAIL: FuzzReverse (0.02s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:33: Before: "\x91", after: "�"

    Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
    To re-run:
    go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
FAIL
exit status 1
FAIL    example/fuzz  0.032s

我们可以看到,经过两次反转后,字符串与原来的字符串不同。这一次输入本身就是无效的unicode。这怎么可能发生呢?

让我们再次调试。

修复两次逆转字符串产生的Bug

在本节中,你将调试两次逆转字符串产生的Bug并修复该Bug。

在继续之前,你可以花一些时间思考这个问题,并尝试自己解决这个问题。

诊断这个错误

和以前一样,有几种方法可以调试此Bug。使用调试器(debugger)将是一种很好的方法。

在本教程中,我们将在Reverse函数中记录有用的调试信息。 仔细观察反转的字符串以发现此Bug。在Go中,一个字符串是一个只读的字节切片(a string is a read only slice of bytes),可以包含非UTF-8的字节。原始字符串是一个带有一个字节“\x91”的字节切片。当输入字符串被转换为[]rune时,Go将字节切片编码为UTF-8字符切片,并将第一个字节“\x91”替换为一个UTF-8字符。当我们将这个UTF-8字符与原始字节切片进行比较时,它们显然不相等。

编写代码

1.在文本编辑器中,将Reverse函数替换为以下内容。

func Reverse(s string) string {
    fmt.Printf("input: %q\n", s)
    r := []rune(s)
    fmt.Printf("runes: %q\n", r)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

这将帮助我们了解在将字符串转换为rune切片时出现的问题。

运行代码

这一次,我们只想运行失败的测试来检查日志。为此,我们将使用go test -run。 要在FuzzXxx/testdata中运行特定的语料库条目,可以提供{FuzzTestName}/{filename}来运行。这在调试时很有帮助。在这种情况下,将-run标志设置为等于测试失败的测试用例的散列值。从你的终端复制并粘贴唯一的散列值,它可能与下面的不同:

$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
        reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL    example/fuzz    0.145s

我们知道了输入是无效的unicode字符,让我们修复Reverse函数中的Bug。

修复Bug

为了解决这个问题,如果Reverse的输入不是有效的UTF-8编码的字符串,我们就返回一个错误。

编写代码

1.在文本编辑器中,用以下内容替换现有的Reverse函数。

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

如果输入的字符串包含无效的UTF-8字符,则返回一个错误。

2.由于Reverse函数现在返回一个错误,修改main函数的代码以丢弃额外的错误值。

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

这些对Reverse函数的调用应该返回nil错误(没有错误发生),因为输入的字符串是有效的UTF-8编码。

3.将需要导入errorsunicode/utf8包。main.go中的import语句应该如下所示。

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

4.修改reverse_test.go文件以检查错误,如果Reverse函数返回了一个错误,则跳过本次测试。

func FuzzReverse(f *testing.F) {
    testcases := []string {"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
             return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

如果不想用直接return的话,你也可以调用t.Skip()函数来停止执行本次模糊测试。

运行代码

1.使用go test运行测试

$ go test
PASS
ok      example/fuzz  0.019s

2.用go test -fuzz=Fuzz运行模糊测试,然后几秒钟后,用组合键ctrl-C停止模糊测试。模糊测试将一直运行,直到遇到失败的输入,或者你通过-fuzztime标志指定最长运行时间。默认情况下,如果没有发生测试失败,模糊测试将永远运行下去,但可以使用组合键ctrl-C中断进程。

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok      example/fuzz  228.000s

3.使用go test -fuzz=Fuzz -fuzztime 30s,如果没有遇到测试失败的用例,总共将进行30秒时间的模糊测试。

$ go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
PASS
ok      example/fuzz  31.025s

模糊测试通过了!

除了-fuzz标志之外,还添加了几个新的标志,可以在文档中查看。

有关模糊测试输出中使用的术语的更多信息,请参阅文章Go模糊测试。例如,“new interesting”指的是扩展了代码覆盖率的已存在的模糊测试语料库的输入。在模糊测试的一开始,“new interesting”输入的数量会急剧增加,随着新的代码路径的发现,“new interesting”输入的数量还会激增几次,然后随着时间的推移逐渐减少。

结论

得好!你刚刚向自己介绍了Go中的模糊测试。

下一步是在代码中选择一个你想模糊测试的函数,并尝试它!如果模糊测试在你的代码中发现了一个错误,请考虑将其添加到trophy case(译者注:向别人炫耀你发现的Go标准库Bug)中。

如果你遇到任何问题或对某个功能有想法,请在此提交问题

对于有关模糊测试的功能的讨论和反馈,你也可以加入Gophers Slack中的#fuzzing频道。 请查看go.dev/security/fuzz上的文档以获得进一步的阅读。

完整的代码

以下是本文中出现过的全部代码:

— main.go —

package main

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

— reverse_test.go —

package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

多个Elasticsearch节点无法组成集群,报错master_not_discovered_exception或NotMasterException的原因和解决方法

多个Elasticsearch节点无法自动组成集群,查看日志发现错误信息:“master_not_discovered_exception”。

原因是运维人员通过克隆虚拟机来获得多台Elasticsearch服务器,这样每个Elasticsearch节点都具有相同的节点ID,因此在组成集群时,无法选举出master节点。

这可以通过以下命令进行验证,列出所有节点ID:

GET /_cat/nodes?v&h=id,ip,name&full_id=true

请注意,由于Elasticsearch集群尚未形成,因此需要单独查询每个节点,即: 

curl 192.168.110.111:9200/_cat/nodes?v&h=id,ip,name&full_id=true
curl 192.168.110.112:9200/_cat/nodes?v&h=id,ip,name&full_id=true
......

Elasticsearch节点ID必须是唯一的。要解决这个问题,我们需要删除每个节点上的索引(RPM方式安装的Elasticsearch的索引数据默认位于/var/lib/elasticsearch)。重启Elasticsearch就会重置节点ID。 

参考

https://www.656463.com/wenda/jdbhjrjqNotMasterExceptionqgddxc_359

教程:使用Go和Gin开发RESTful API

本文翻译自《Tutorial: Developing a RESTful API with Go and Gin》。

目录

先决条件

设计API端点

为代码创建文件夹

创建数据

编写处理程序以返回所有条目

编写处理程序以添加一个新条目

编写处理程序以返回一个特定条目

结论

全部代码

本教程介绍了使用Go和Web开发框架Gin编写RESTful web服务API的基本知识。

如果你对Go及其工具有基本的熟悉,你将从本教程中获得最大的收获。如果这是你第一次接触Go,请先参阅教程:Go快速入门

Gin简化了许多与构建web应用程序(包括web服务)相关的编程任务。在本教程中,你将使用Gin来路由请求、检索请求详细信息,并为发送JSON响应。

在本教程中,你将构建一个具有两个端点的RESTful API服务器。你的示例项目将是一个关于老式爵士乐唱片的数据存储库。

本教程包括以下部分:

1 设计API端点。

2 为代码创建一个文件夹。

3 创建数据。

4 编写一个处理程序以返回所有条目。

5 编写一个处理程序来添加一个新条目。

6 编写一个处理程序以返回一个特定条目。

先决条件

  • 安装Go 1.16或更高版本。有关安装说明,请参阅安装Go
  • 用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
  • 一种命令行终端。Go在Linux和Mac上使用任何终端都能很好地工作,以及Windows中的PowerShell或CMD。
  • curl程序。在Linux和Mac上,应该已经安装了。在Windows上,它包含在Windows 10 Insider版本17063及更高版本中。对于早期的Windows版本,你可能需要安装它。有关更多信息,请参阅Tar and Curl Come to Windows

设计API端点

你将建立一个API,提供对一家出售老式黑胶唱片的商店的访问。因此,你需要提供API端点,用户可以通过客户端访问这些端点来获取和添加相册。

在开发API时,通常从设计端点开始。如果端点易于理解,将方便API的用户使用。

以下是你将在本教程中创建的API端点:

/albums

  • GET–获取所有相册的列表,以JSON形式返回。
  • POST–以JSON形式发送的请求数据,添加一个新相册。

/albums/:id

  • GET–通过相册ID获取相册,并以JSON形式返回相册数据。

为代码创建文件夹

首先,为你要编写的代码创建一个项目。

1 打开命令行终端并转到家目录。 在Linux或Mac上:

$ cd

在Windows上:

C:\> cd %HOMEPATH%

2 使用命令行终端,为代码创建一个名为web-service-gin的目录:

$ mkdir web-service-gin
$ cd web-service-gin

3 创建一个可以在其中管理依赖关系的模块。

运行go mod init命令,为其提供代码所在模块的路径:

$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

此命令创建一个go.mod文件,你添加的依赖项将列在该文件中进行跟踪。有关使用路径命名模块的详细信息,请参阅管理依赖关系

接下来,你将设计用于处理数据的数据结构。

创建数据

为了简化教程,你将把数据存储在内存中。更典型的API将与数据库交互。

请注意,将数据存储在内存中意味着每次停止服务器时,相册相关数据都会丢失,然后在启动服务器时重新创建。

编写代码

1 使用文本编辑器,在web-service-gin目录中创建一个名为main.go的文件。你将在该文件中编写Go代码。

2 在文件顶部的main.go中,粘贴以下包声明。

package main

独立可运行的程序(与库相对)始终位于main包中。

3 在包声明下面,粘贴album结构体的以下声明。你将使用它将相册数据存储在内存中。

代码中的结构体标记(json:"artist"等)指定在将结构体的内容序列化为JSON时,字段的名称如何转换。如果没有它们,JSON将使用结构体的大写的字段名——这种风格在JSON中并不常见。

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

在刚添加的结构体声明下面,粘贴下面的album结构体片段,其中包含将启动你的项目的数据。

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

接下来,你将编写代码来实现你的第一个API端点。

编写处理程序以返回所有条目

当客户端在GET /albums上发出请求时,你希望以JSON的形式返回所有的相册信息。

为此,你将编写以下代码:

  • 响应的逻辑
  • 将请求路径映射到响应的逻辑

但你首先要添加依赖项,然后添加依赖于它们的代码。

编写代码

1 在上一节中添加的结构体代码下面,粘贴以下代码以获得相册信息的列表。

这个getAlbums函数从相册结构体的切片albums创建JSON,并将JSON写入响应。

// getAlbums以JSON格式的数据响应一个相册信息的列表。
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

在此代码中,你:

  • 编写了一个接受gin.Context参数的getAlbums函数。请注意,你可以给这个函数起任何名称——Gin和Go都不需要特定的函数名称格式。

gin.Context是Gin最重要的组成部分。它携带请求的详细信息、验证和序列化JSON等。(尽管名称相似,但这与Go内置的context包不同。)

该函数的第一个参数是要发送到客户端的HTTP状态代码。在这里,你传递net/http包的StatusOK常量,表示200 OK这一HTTP状态代码。

请注意,你可以将Context.IndetedJSON函数替换为Context.JSON函数,以发送更紧凑的JSON数据。在实践中,缩进形式的JSON数据在调试时更具可读性,而且也不会比紧凑的JSON数据大很多。

2 在main.go顶部附近的albums切片声明下方,粘贴下面的代码,将处函数分配给API端点。

这设置了一个关联,getAlbums函数处理对/albums路径的请求。

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

在此代码中,你:

  • 使用Default函数初始化Gin路由器。
  • 使用GET函数将GET HTTP方法和/albums路径与处理函数相关联。

请注意,你传递的是getAlbums函数的名称。这与传递函数的结果不同,传递函数的结果是传递getAlbums()(注意括号)。

  • 使用Run函数将路由器关联到一个http服务器并启动服务器。

3 在main.go的顶部,就在包声明的下方,导入用到的包。

第一行代码应该如下所示:

package main

import (
    "net/http"

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

4 保存main.go文件。

运行代码

1 引入Gin模块作为依赖项。 在命令行中,使用go get添加github.com/gin-gonic/gin模块作为你的example/web-service-gin模块的依赖项。使用句点参数表示“下载当前目录中代码的所有依赖项”:

$ go get .
go get: added github.com/gin-gonic/gin v1.7.2

Go解析并下载依赖项,以满足你在上一步中添加的import声明。

2 在包含main.go文件的目录的命令行中,运行代码。使用句点参数表示“在当前目录中运行代码”:

$ go run .

一旦代码运行,你就有了一个正在运行的HTTP服务器,可以向其发送请求。

3 在一个新的命令行窗口中,使用curl工具向正在运行的web服务发出请求。

$ curl http://localhost:8080/albums

将会返回以下JSON格式的数据:

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        }
]

你已经启动了一个API!在下一节中,你将使用代码创建另一个API端点,以处理添加一个信息条目的POST请求。

编写处理程序以添加一个新条目

当客户端在/albums上发出POST请求时,你希望将请求正文中描述的相册信息添加到现有的数据中。

为此,你将编写以下内容:

  • 将一条新相册的信息添加到现有列表里的一段代码。
  • 将POST请求路由到你的上述代码的一段代码。

编写代码

1 添加代码以将相册信息数据添加到相册列表中。

import语句之后的某个位置,粘贴以下代码。(文件的末尾是粘贴这段代码的好位置,但Go并没有强制函数的声明顺序。)

// postAlbums函数从请求体中获取JSON数据添加一条相册信息数据。
func postAlbums(c *gin.Context) {
    var newAlbum album

    // 调用BindJSON函数把接收到的JSON数据转换为newAlbum。
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // 把newAlbum添加到albums列表里。
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

在此代码中,你:

  • 使用Context.BindJSON函数将请求正文绑定到newAlbum结构体变量。
  • 将从JSON数据转换得到的album结构体变量添加到albums切片。
  • 在响应中添加一个201状态代码,以及表示你成功创建一条新相册信息数据。

2 更改main函数,使用router.POST函数添加路由,如下所示:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

在此代码中,你:

  • /albums路径上的POST方法与postAlbums函数相关联。

使用Gin,你可以将处理程序与HTTP方法和API路径相关联。通过这种方式,你可以根据客户端使用的HTTP方法将发送到某个API路径的请求单独路由到某个处理函数。

运行代码

1 如果服务器仍在运行,请停止它。

2 在包含main.go的目录的命令行中,运行代码:

$ go run .

3 从另一个命令行窗口,使用curl工具向正在运行的web服务发出请求:

$ curl http://localhost:8080/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'

该命令运行后应该会显示添加的相册信息的JSON数据和HTTP响应状态数据:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

4 与上一节一样,使用curl工具检索相册信息的完整列表,你可以使用该列表来确认是否添加了一个新相册的信息:

$ curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

该命令会显示相册信息的列表:

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        },
        {
                "id": "4",
                "title": "The Modern Sound of Betty Carter",
                "artist": "Betty Carter",
                "price": 49.99
        }
]

在下一节中,你将添加代码来处理对特定一个相册信息的GET请求。

编写处理程序以返回一个特定条目

当客户端请求GET /albums/[id]时,你希望返回ID值与id路径参数匹配的相册的信息。

为此,你将:

  • 添加代码以检索请求的相册的信息数据。
  • 将API路径映射到上述代码。

编写代码

1 在上一节中添加的postAlbums函数下面,粘贴以下代码以检索特定的相册。

getAlbumByID函数将提取请求路径中的id,然后查找匹配的相册的信息。

// getAlbumByID函数使用客户端发送过来的id参数定位并返回相册数据作为响应。
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // 循环遍历albums列表,查找匹配id参数的album。
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

在这段代码中,你:

  • 使用Context.Param从URL中检索id路径参数。将此处理程序映射到API路径时,将在API路径中包含id参数的占位符。
  • 循环遍历albums切片中的album结构体变量,查找ID字段值与id参数值匹配的那个结构体。如果找到了,则将该album序列化为JSON,并将其作为响应返回,并返回一个200 OK的HTTP状态码。

如上所述,真实世界的服务可能会使用数据库查询来执行此查找。

2 最后,更改main函数,添加一个router.GET路由到getAlbumByID函数,其中的API路径是/albums/:id,如以下所示:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

在此代码中,你:

  • /albums/:id这个API路径与getAlbumByID函数相关联。在Gin中,API路径中前面的冒号表示这个条目是一个路径参数。

运行代码

1 停止运行之前的程序。

2 在包含main.go的目录的命令行中,运行代码:

$ go run .

3 从另一个命令行窗口,使用curl工具向正在运行的web服务发出请求:

$ curl http://localhost:8080/albums/2

该命令执行后应该显示你给出ID的相册信息的JSON数据:

{
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
}

如果找不到对应的相册信息,你将获得带有错误消息的JSON。

结论

恭喜你刚刚使用Go和Gin编写了一个简单的RESTful web服务。

建议的下一个主题:

全部代码

本节包含使用本教程构建的应用程序的全部代码:

package main

import (
    "net/http"

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

// album represents data about a record album.
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// albums slice to seed record album data.
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
    var newAlbum album

    // Call BindJSON to bind the received JSON to
    // newAlbum.
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // Add the new album to the slice.
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // Loop through the list of albums, looking for
    // an album whose ID value matches the parameter.
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

Linux系统执行sudo node和sudo npm报错找不到命令的解决方法

从官网下载Node.js二进制包,在Linux操作系统里解压安装后,在用户(非root用户)的家目录下的.bashrc文件里,把可执行文件node所在目录(也是npm所在目录)的路径加入PATH环境变量,例如:

export PATH=$PATH:/opt/nodejs/latest/bin

保存后source一下.bashrc文件,让里面的配置生效:

$ source ~/.bashrc

然后即可直接执行node和npm程序,例如:

$ node -v
v16.20.0
$ npm -v
8.19.4

但是执行sudo node xxx和sudo npm xxx还是报错找不到命令:

$ sudo node -v
sudo: node: command not found

解决办法是为node和npm创建符号链接到/usr/bin/目录:

# 先分别打印输出node和npm的绝对路径看看
$ which node
/opt/nodejs/latest/bin/node
$ which npm
/opt/nodejs/latest/bin/npm
# 创建符号链接
$ sudo ln -s /opt/nodejs/latest/bin/node /usr/bin/node
$ sudo ln -s /opt/nodejs/latest/bin/npm /usr/bin/npm

然后执行sudo node xxx和sudo npm xxx就不会报错找不到命令了:

$ sudo node -v
v16.20.0
$ sudo npm -v
8.19.4