diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/How-To-Code-in-Go.iml b/.idea/How-To-Code-in-Go.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/How-To-Code-in-Go.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c0b027d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..0b0a727 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/content/zh/docs/05-Understanding_the_GOPATH.md b/content/zh/docs/05-Understanding_the_GOPATH.md index c706e2a..495b0e1 100644 --- a/content/zh/docs/05-Understanding_the_GOPATH.md +++ b/content/zh/docs/05-Understanding_the_GOPATH.md @@ -14,7 +14,7 @@ 想要了解更多关于设置 `$GOPATH` 变量的信息,可以参考 Go [文档](https://golang.org/doc/code.html#Workspaces)。 -此外,[本系列教程]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}}) 简单介绍了安装 Go 和设置 Go 开发环境的方法。 +此外,[本系列教程](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean) 简单介绍了安装 Go 和设置 Go 开发环境的方法。 ## `$GOPATH` 不是 `$GOROOT` diff --git a/content/zh/docs/07-Understanding_Data_Types_in_Go.md b/content/zh/docs/07-Understanding_Data_Types_in_Go.md index b66814e..0c07cc1 100644 --- a/content/zh/docs/07-Understanding_Data_Types_in_Go.md +++ b/content/zh/docs/07-Understanding_Data_Types_in_Go.md @@ -343,7 +343,7 @@ output Say "hello" to Go! ``` -你几乎总会使用解释字符串,因为它们允许在其中使用转义字符。想要了解更多关于字符串使用的信息,请查看[Go 中使用字符串的简介]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})。 +你几乎总会使用解释字符串,因为它们允许在其中使用转义字符。想要了解更多关于字符串使用的信息,请查看[Go 中使用字符串的简介](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)。 ### UTF-8 字符的字符串 @@ -586,4 +586,4 @@ blue 此时,你应该对 Go 中可用的一些主要数据类型有了更好的理解。当你使用 Go 语言开发编程项目时,这些数据类型中的每一种都将变得非常重要。 -一旦掌握了 Go 中可用的数据类型,就可以学习[如何转换数据类型]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}}),以便根据具体情况更改数据类型。 +一旦掌握了 Go 中可用的数据类型,就可以学习[如何转换数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/12-How_To_Convert_Data_Types_in_Go),以便根据具体情况更改数据类型。 diff --git a/content/zh/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md b/content/zh/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md index db40e76..cde93fa 100644 --- a/content/zh/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md +++ b/content/zh/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md @@ -113,7 +113,7 @@ invalid operation: "Sammy" + 27 (mismatched types string and int) ## 在变量中保存字符串 -**[变量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})**是在程序中可以用来保存数据的符号。你可以将它们看作是一个可以在其中填充一些数据或值的空盒子。字符串是数据,因此你可以使用它们来填充变量。将字符串声明为变量可以使得在 Go 程序中处理字符串更加容易。 +**[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go)**是在程序中可以用来保存数据的符号。你可以将它们看作是一个可以在其中填充一些数据或值的空盒子。字符串是数据,因此你可以使用它们来填充变量。将字符串声明为变量可以使得在 Go 程序中处理字符串更加容易。 要在变量中存储字符串,只需将一个变量分配给字符串。在下面的例子中,`s` 被声明为变量: diff --git a/content/zh/docs/09-How_To_Format_Strings_in_Go.md b/content/zh/docs/09-How_To_Format_Strings_in_Go.md index 458fda9..e10e259 100644 --- a/content/zh/docs/09-How_To_Format_Strings_in_Go.md +++ b/content/zh/docs/09-How_To_Format_Strings_in_Go.md @@ -28,7 +28,7 @@ 在组合反引号和双引号的方式中,我们可以控制字符串中引号和反引号的显示方式。 -这里有个重点,在 Go 中使用反引号会创建一个 `raw` 字符串文字,而使用双引号会创建一个 `interpreted` 字符串文字。要了解有关差异的更多信息,请阅读 [Go 中处理字符串的介绍]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) 教程。 +这里有个重点,在 Go 中使用反引号会创建一个 `raw` 字符串文字,而使用双引号会创建一个 `interpreted` 字符串文字。要了解有关差异的更多信息,请阅读 [Go 中处理字符串的介绍](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go) 教程。 ## 转义字符 @@ -154,4 +154,4 @@ Sammy says,\"The balloon\'s color is red.\" ## 结论 -本教程通过使用字符串介绍了几种在 Go 中格式化文本的方法。 通过使用转义字符或原始字符串等技术,我们能够确保程序的字符串正确的呈现在屏幕上,以便用户最终能够轻松阅读所有输出文本。 \ No newline at end of file +本教程通过使用字符串介绍了几种在 Go 中格式化文本的方法。 通过使用转义字符或原始字符串等技术,我们能够确保程序的字符串正确的呈现在屏幕上,以便用户最终能够轻松阅读所有输出文本。 diff --git a/content/zh/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md b/content/zh/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md index 1cef949..2561bd3 100644 --- a/content/zh/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md +++ b/content/zh/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md @@ -2,7 +2,7 @@ ## 介绍 -Go 的 [`strings`](https://golang.org/pkg/strings/) 包有几个函数可用于 [string 数据类型]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})。这些函数可以轻松地修改和操作字符串。我们可以将函数视为对代码元素执行的操作。内置函数是那些在 Go 编程语言中定义并且可供我们随时使用的函数。 +Go 的 [`strings`](https://golang.org/pkg/strings/) 包有几个函数可用于 [string 数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)。这些函数可以轻松地修改和操作字符串。我们可以将函数视为对代码元素执行的操作。内置函数是那些在 Go 编程语言中定义并且可供我们随时使用的函数。 在本教程中,我们将回顾几个可用于在 Go 中处理字符串的不同函数。 @@ -222,6 +222,6 @@ Sammy had a balloon. 本教程介绍了一些用于字符串数据类型的常见字符串包函数,你可以使用这些函数在 Go 程序中处理和操作字符串。 -你可以在 [理解 Go 的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}) 中了解有关其他数据类型的更多信息,并在 [Go 中处理字符串的介绍]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) 了解更多有关字符串的信息。 +你可以在 [理解 Go 的数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go) 中了解有关其他数据类型的更多信息,并在 [Go 中处理字符串的介绍](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go) 了解更多有关字符串的信息。 ------- \ No newline at end of file +------ diff --git a/content/zh/docs/12-How_To_Convert_Data_Types_in_Go.md b/content/zh/docs/12-How_To_Convert_Data_Types_in_Go.md index c865d48..9817818 100644 --- a/content/zh/docs/12-How_To_Convert_Data_Types_in_Go.md +++ b/content/zh/docs/12-How_To_Convert_Data_Types_in_Go.md @@ -468,4 +468,4 @@ my string 本 Go 教程主要演示了如何通过内置方法将几种重要的数据类型转换为其他数据类型。能够在 Go 中转换数据类型将允许你执行诸如接受用户输入和跨不同数字类型进行数学运算之类的事情。稍后,当你使用 Go 编写接受来自许多不同来源(如数据库和 API)的数据的程序时,你将使用这些转换方法来确保你可以对数据进行操作。你还可以通过将数据转换为更小的数据类型来优化存储。 -如果你想深入分析 Go 中的数据类型,请查看我们的 [理解 Go 中的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}) 文章。 +如果你想深入分析 Go 中的数据类型,请查看我们的 [理解 Go 中的数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go) 文章。 diff --git a/content/zh/docs/13-How_To_Do_Math_in_Go_with_Operators.md b/content/zh/docs/13-How_To_Do_Math_in_Go_with_Operators.md index 2f5656c..5dcaba2 100644 --- a/content/zh/docs/13-How_To_Do_Math_in_Go_with_Operators.md +++ b/content/zh/docs/13-How_To_Do_Math_in_Go_with_Operators.md @@ -6,7 +6,7 @@ 在编程中进行数学运算是一项重要的技能,因为你会经常与数字打交道。尽管对数学的理解肯定能帮助你成为一个更好的程序员,但它不是一个先决条件。如果你没有数学背景,试着把数学看作是完成你想实现的目标的工具,并作为提高你的逻辑思维能力的一种方式。 -我们将使用 Go 中最常用的两种数字[数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}),整数和浮点数。 +我们将使用 Go 中最常用的两种数字[数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go),整数和浮点数。 * [整数](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go/#%E6%95%B4%E6%95%B0)是可以是正数、负数或 0 的整数(...,`-1`,`0`,`1`,...)。 * [浮点数](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go/#%E6%B5%AE%E7%82%B9%E6%95%B0)是包含小数点的实数,如 `9.0` 或 `2.25` ... @@ -442,4 +442,4 @@ y %= 3 ## 总结 -本教程涵盖了许多你将在整数和浮点数数据类型中使用的运算符。你可以在[理解 Go 的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})和[如何在 Go 中转换数据类型]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}})中了解更多关于不同的数据类型。 \ No newline at end of file +本教程涵盖了许多你将在整数和浮点数数据类型中使用的运算符。你可以在[理解 Go 的数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go)和[如何在 Go 中转换数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/12-How_To_Convert_Data_Types_in_Go)中了解更多关于不同的数据类型。 diff --git a/content/zh/docs/16-Understanding_Arrays_and_Slices_in_Go.md b/content/zh/docs/16-Understanding_Arrays_and_Slices_in_Go.md index f03b153..d8aa786 100644 --- a/content/zh/docs/16-Understanding_Arrays_and_Slices_in_Go.md +++ b/content/zh/docs/16-Understanding_Arrays_and_Slices_in_Go.md @@ -16,7 +16,7 @@ ## 定义一个数组 -数组的定义是在大括号 `[]` 中声明数组的大小,然后是各元素的数据类型。Go 中的数组必须使其所有元素都是相同的[数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})。在数据类型之后,你可以用大括号 `{ }` 来声明数组元素的单个值。 +数组的定义是在大括号 `[]` 中声明数组的大小,然后是各元素的数据类型。Go 中的数组必须使其所有元素都是相同的[数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go)。在数据类型之后,你可以用大括号 `{ }` 来声明数组元素的单个值。 下面是声明一个数组的一般模式: @@ -552,4 +552,4 @@ seaNames[1][3] = "Jamie" 在本教程中,你学到了在 Go 中使用数组和切片的基础。通过多个练习来证明数组的长度是固定的,而切片的长度是可变的,并发现这种差异是如何影响这些数据结构的用途场景。 -要继续学习 Go 中的数据结构,请查看我们的文章[理解 Go 中的 Map]({{< relref "/docs/15-Understanding_Maps_in_Go.md" >}}),或探索整个[如何在 Go 中编码](https://gocn.github.io/How-To-Code-in-Go/)系列。 \ No newline at end of file +要继续学习 Go 中的数据结构,请查看我们的文章[理解 Go 中的 Map](https://gocn.github.io/How-To-Code-in-Go/docs/15-Understanding_Maps_in_Go),或探索整个[如何在 Go 中编码](https://gocn.github.io/How-To-Code-in-Go/)系列。 diff --git a/content/zh/docs/19-Handling_Panics_in_Go _DigitalOcean.md b/content/zh/docs/19-Handling_Panics_in_Go _DigitalOcean.md index 609e45a..fea55f3 100644 --- a/content/zh/docs/19-Handling_Panics_in_Go _DigitalOcean.md +++ b/content/zh/docs/19-Handling_Panics_in_Go _DigitalOcean.md @@ -2,9 +2,9 @@ ## 介绍 -程序遇到的错误分为两个广泛的类别:程序员已经预料到的错误和程序员没有预料到的错误。我们在前两篇关于 [错误处理]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}}) 的文章中介绍过的 `error` 接口主要用于处理我们在编写 Go 程序时可能遇到的错误。`error` 接口甚至允许我们去确认在调用一个函数时发生罕见性错误的可能性,因此我们可以在这些情况下进行适当的响应。 +程序遇到的错误分为两个广泛的类别:程序员已经预料到的错误和程序员没有预料到的错误。我们在前两篇关于 [错误处理](https://gocn.github.io/How-To-Code-in-Go/docs/12-How_To_Convert_Data_Types_in_Go) 的文章中介绍过的 `error` 接口主要用于处理我们在编写 Go 程序时可能遇到的错误。`error` 接口甚至允许我们去确认在调用一个函数时发生罕见性错误的可能性,因此我们可以在这些情况下进行适当的响应。 -Panics 属于第二类错误,这些错误是程序员意料之外的。这些意料之外的错误导致一个 GO 程序自发终止并退出运行。常见的错误通常是造成 panic 的原因。在本教程中,我们将研究哪些常见操作可以引起 panic ,我们还将看到避免这些 panic 的方法。我们还将使用 [`defer`]({{< relref "/docs/29-Understanding_defer_in_Go.md" >}}) 语句与 `recover` 函数一起捕获 panic,以免 panic 有机会意外终止我们正在运行的 GO 程序。 +Panics 属于第二类错误,这些错误是程序员意料之外的。这些意料之外的错误导致一个 GO 程序自发终止并退出运行。常见的错误通常是造成 panic 的原因。在本教程中,我们将研究哪些常见操作可以引起 panic ,我们还将看到避免这些 panic 的方法。我们还将使用 [`defer`](https://gocn.github.io/How-To-Code-in-Go/docs/29-Understanding_defer_in_Go) 语句与 `recover` 函数一起捕获 panic,以免 panic 有机会意外终止我们正在运行的 GO 程序。 ## 了解 panics diff --git a/content/zh/docs/20-Importing_Packages_in_Go_DigitalOcean.md b/content/zh/docs/20-Importing_Packages_in_Go_DigitalOcean.md index 5c53df6..60bf000 100644 --- a/content/zh/docs/20-Importing_Packages_in_Go_DigitalOcean.md +++ b/content/zh/docs/20-Importing_Packages_in_Go_DigitalOcean.md @@ -123,7 +123,7 @@ GO 工具链带有 `go get` 命令。此命令使你可以将第三方软件包 go get github.com/gobuffalo/flect ``` -在这种情况下,使用 `go get` 工具将在 GitHub 上找到软件包,并将其安装到你的 [`$Gopath`]({{< relref "/docs/05-Understanding_the_GOPATH.md" >}}) 中。 +在这种情况下,使用 `go get` 工具将在 GitHub 上找到软件包,并将其安装到你的 [`$Gopath`](https://gocn.github.io/How-To-Code-in-Go/docs/05-Understanding_the_GOPATH) 中。 对于此示例,代码将安装在此目录中: @@ -223,4 +223,4 @@ import ( 当我们导入软件包时,我们可以调用未内置的功能。有些软件包是随着 GO 安装的标准库的一部分,有些软件包将通过 `go get` 来安装。 -使用软件包可以使我们在利用现有代码时使程序更加健壮和强大。我们还可以为自己和其他程序员 [创建自己的软件包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}}),以便将来使用。 +使用软件包可以使我们在利用现有代码时使程序更加健壮和强大。我们还可以为自己和其他程序员 [创建自己的软件包](https://gocn.github.io/How-To-Code-in-Go/docs/21-How_To_Write_Packages_in_Go),以便将来使用。 diff --git a/content/zh/docs/21-How_To_Write_Packages_in_Go.md b/content/zh/docs/21-How_To_Write_Packages_in_Go.md index bf254be..8066cb1 100644 --- a/content/zh/docs/21-How_To_Write_Packages_in_Go.md +++ b/content/zh/docs/21-How_To_Write_Packages_in_Go.md @@ -5,12 +5,12 @@ 本教程将指导你如何编写 Go 包,以便在其他编程文件中使用。 ## 前提条件 -- 按照[如何安装和设置 Go 的本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})系列教程中的一个教程设置 Go 编程环境。按照本地编程环境教程中的步骤5创建你的 Go 工作区。要遵循本文的例子和命名规则,请阅读第一节「编写和导入软件包」。 -- 为了加深你对 GOPATH 的了解,请阅读文章[了解 GOPATH]({{< relref "/docs/05-Understanding_the_GOPATH.md" >}})。 +- 按照[如何安装和设置 Go 的本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)系列教程中的一个教程设置 Go 编程环境。按照本地编程环境教程中的步骤5创建你的 Go 工作区。要遵循本文的例子和命名规则,请阅读第一节「编写和导入软件包」。 +- 为了加深你对 GOPATH 的了解,请阅读文章[了解 GOPATH](https://gocn.github.io/How-To-Code-in-Go/docs/05-Understanding_the_GOPATH)。 ## 编写和导入软件包 -编写包就像编写任何其他 Go 文件一样,包可以包含函数、[类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})和[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go/#%E7%90%86%E8%A7%A3%E5%8F%98%E9%87%8F)的定义,然后可以在其他 Go 程序中使用。 +编写包就像编写任何其他 Go 文件一样,包可以包含函数、[类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go)和[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go/#%E7%90%86%E8%A7%A3%E5%8F%98%E9%87%8F)的定义,然后可以在其他 Go 程序中使用。 在我们创建一个新的包之前,我们需要进入我们的 Go 工作区。这通常是在我们的`gopath`下。对于这个例子,本教程中我们将把包称为`greet`。为了做到这一点,在我们的项目空间下的`gopath`中创建了一个名为`greet`的目录。当使用 Github 作为代码库,组织名称为`gopherguides`,想在此组织下创建`greet`包,那么我们的目录会是这样的: @@ -379,4 +379,4 @@ The octopus's name is "" and is the color . ## 总结 -编写 Go 包与编写其他 Go 文件是一样的,但把它放在另一个目录中可以隔离代码,以便在其他地方重复使用。本教程介绍了如何在包中编写定义,演示了如何在另一个 Go 文件中使用这些定义,并解释了控制包是否可访问的选项。 \ No newline at end of file +编写 Go 包与编写其他 Go 文件是一样的,但把它放在另一个目录中可以隔离代码,以便在其他地方重复使用。本教程介绍了如何在包中编写定义,演示了如何在另一个 Go 文件中使用这些定义,并解释了控制包是否可访问的选项。 diff --git a/content/zh/docs/22-Understanding_Package_Visibility_in_Go.md b/content/zh/docs/22-Understanding_Package_Visibility_in_Go.md index 94bef0e..600ad6f 100644 --- a/content/zh/docs/22-Understanding_Package_Visibility_in_Go.md +++ b/content/zh/docs/22-Understanding_Package_Visibility_in_Go.md @@ -2,7 +2,7 @@ ## 介绍 -当创建一个[Go 中的包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}})时,最终的目标通常是让其他开发者可以使用这个包,无论是高阶包还是整个程序。通过[导入包]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}}),你的这段代码可以作为其他更复杂的工具的构建模块。然而,只有某些包是可以导入的。这是由包的可见性决定的。 +当创建一个[Go 中的包](https://gocn.github.io/How-To-Code-in-Go/docs/21-How_To_Write_Packages_in_Go)时,最终的目标通常是让其他开发者可以使用这个包,无论是高阶包还是整个程序。通过[导入包](https://gocn.github.io/How-To-Code-in-Go/docs/20-Importing_Packages_in_Go_DigitalOcean),你的这段代码可以作为其他更复杂的工具的构建模块。然而,只有某些包是可以导入的。这是由包的可见性决定的。 这里的*可见性*是指一个包或其他构造可以被引用的文件空间。例如,如果我们在一个函数中定义一个变量,那么这个变量的可见性(范围)只在定义它的那个函数中。同样,如果你在一个包中定义了一个变量,你可以让它只在该包中可见,或允许它在包外也可见。 @@ -14,7 +14,7 @@ 要遵循本文中的示例,你将需要: -- 按照[如何安装 Go 并设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作区。 本教程将使用以下文件结构: +- 按照[如何安装 Go 并设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置的 Go 工作区。 本教程将使用以下文件结构: ```text . @@ -97,7 +97,7 @@ func Log(statement string) { 保存并退出该文件。 -为了在我们代码的其他地方使用这个包,我们可以[`import`它到一个新的包]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})。我们将创建这个新的包,但需要一个新的目录来首先存储这些源文件。 +为了在我们代码的其他地方使用这个包,我们可以[`import`它到一个新的包](https://gocn.github.io/How-To-Code-in-Go/docs/20-Importing_Packages_in_Go_DigitalOcean)。我们将创建这个新的包,但需要一个新的目录来首先存储这些源文件。 让我们离开`logging`目录,创建一个名为`cmd`的新目录,然后进入这个新目录: @@ -468,4 +468,4 @@ cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or me 这篇文章展示了如何在包之间共享代码,同时也保护你的包的实现细节。这允许你输出一个简单的 API,为了向后兼容而很少改变,但允许在你的包中根据需要私下改变,使其在未来更好地工作。这被认为是创建包和它们相应的 API 时的最佳做法。 -要了解更多关于 Go 中的包,请查看我们的[在 Go 中导入包]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})和[如何在 Go 中编写包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}})文章,或者探索我们整个[如何在 Go 中编码系列](https://gocn.github.io/How-To-Code-in-Go/)。 \ No newline at end of file +要了解更多关于 Go 中的包,请查看我们的[在 Go 中导入包](https://gocn.github.io/How-To-Code-in-Go/docs/20-Importing_Packages_in_Go_DigitalOcean)和[如何在 Go 中编写包](https://gocn.github.io/How-To-Code-in-Go/docs/21-How_To_Write_Packages_in_Go)文章,或者探索我们整个[如何在 Go 中编码系列](https://gocn.github.io/How-To-Code-in-Go/)。 diff --git a/content/zh/docs/23-How_To_Write_Conditional_Statements_in_Go.md b/content/zh/docs/23-How_To_Write_Conditional_Statements_in_Go.md index e372d1d..8e4f0c4 100644 --- a/content/zh/docs/23-How_To_Write_Conditional_Statements_in_Go.md +++ b/content/zh/docs/23-How_To_Write_Conditional_Statements_in_Go.md @@ -36,9 +36,9 @@ func main() { } ``` -在这段代码中,我们有一个变量`grade`,并给它一个整数值`70`。然后我们使用`if`语句来评估变量`grade`是否大于或等于(`>=`)`65`。如果它确实满足这个条件,我们告诉程序打印出[字符串]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) `Passing grade`。 +在这段代码中,我们有一个变量`grade`,并给它一个整数值`70`。然后我们使用`if`语句来评估变量`grade`是否大于或等于(`>=`)`65`。如果它确实满足这个条件,我们告诉程序打印出[字符串](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go) `Passing grade`。 -将程序保存为`grade.go`,并在[终端窗口]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})中用`go run grade.go`命令运行它。 +将程序保存为`grade.go`,并在[终端窗口](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)中用`go run grade.go`命令运行它。 在这种情况下,70分的成绩*符合大于或等于65分的条件,因此,一旦你运行该程序,你将收到以下输出: @@ -147,7 +147,7 @@ Your balance is 0 or above. ## Else if 语句 -到目前为止,我们已经为条件语句提出了一个[布尔]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})选项,每个`if`语句的评估结果为真或假。在许多情况下,我们会希望一个程序能评估出两个以上的可能结果。为此,我们将使用**else if**语句,在 Go 中写成`else if`。`else if`或 else if 语句看起来和`if`语句一样,将评估另一个条件。 +到目前为止,我们已经为条件语句提出了一个[布尔](https://gocn.github.io/How-To-Code-in-Go/docs/14-Understanding_Boolean_Logic_in_Go)选项,每个`if`语句的评估结果为真或假。在许多情况下,我们会希望一个程序能评估出两个以上的可能结果。为此,我们将使用**else if**语句,在 Go 中写成`else if`。`else if`或 else if 语句看起来和`if`语句一样,将评估另一个条件。 在银行账户程序中,我们可能希望在三种不同的情况下有三个离散的输出。 @@ -377,4 +377,4 @@ Passing grade of: A 通过使用像 `if` 语句这样的条件语句,你将对你的程序执行内容有更大的控制。条件性语句告诉程序要评估是否满足某个条件。如果满足条件,它将执行特定的代码,但如果不满足条件,程序将继续执行其他代码。 -要继续练习条件语句,请尝试使用不同的[运算符]({{< relref "/docs/13-How_To_Do_Math_in_Go_with_Operators.md" >}})来获得对条件语句的更多熟悉。 \ No newline at end of file +要继续练习条件语句,请尝试使用不同的[运算符](https://gocn.github.io/How-To-Code-in-Go/docs/13-How_To_Do_Math_in_Go_with_Operators)来获得对条件语句的更多熟悉。 diff --git a/content/zh/docs/24-How_To_Write_Switch_Statements_in_Go.md b/content/zh/docs/24-How_To_Write_Switch_Statements_in_Go.md index df7fe8b..72b1732 100644 --- a/content/zh/docs/24-How_To_Write_Switch_Statements_in_Go.md +++ b/content/zh/docs/24-How_To_Write_Switch_Statements_in_Go.md @@ -2,7 +2,7 @@ ## 介绍 -[条件语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})使程序员有能力指导他们的程序在某个条件为真时采取某些行动,在条件为假时采取另一种行动。经常,我们想把一些[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go/#%E7%90%86%E8%A7%A3%E5%8F%98%E9%87%8F)与多个可能的值进行比较,在每种情况下采取不同的行动。仅仅使用[`if`语句](https://gocn.github.io/How-To-Code-in-Go/docs/23-How_To_Write_Conditional_Statements_in_Go/#if-%E8%AF%AD%E5%8F%A5)就可以做到这一点。然而,编写软件不仅是为了让事情顺利进行,也是为了向未来的自己和其他开发者传达你的意图。`switch`是一个替代性的条件语句,对于传达你的 Go 程序在遇到不同选项时采取的行动很有用。 +[条件语句](https://gocn.github.io/How-To-Code-in-Go/docs/23-How_To_Write_Conditional_Statements_in_Go)使程序员有能力指导他们的程序在某个条件为真时采取某些行动,在条件为假时采取另一种行动。经常,我们想把一些[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go/#%E7%90%86%E8%A7%A3%E5%8F%98%E9%87%8F)与多个可能的值进行比较,在每种情况下采取不同的行动。仅仅使用[`if`语句](https://gocn.github.io/How-To-Code-in-Go/docs/23-How_To_Write_Conditional_Statements_in_Go/#if-%E8%AF%AD%E5%8F%A5)就可以做到这一点。然而,编写软件不仅是为了让事情顺利进行,也是为了向未来的自己和其他开发者传达你的意图。`switch`是一个替代性的条件语句,对于传达你的 Go 程序在遇到不同选项时采取的行动很有用。 我们可以用 switch 语句编写的所有内容也可以用`if`语句编写。在本教程中,我们将看几个例子,看看 switch 语句能做什么,它所取代的`if`语句,以及它最合适的应用场合。 @@ -48,7 +48,7 @@ strawberry is my favorite! I've never tried banana before ``` -在`main`中,我们定义了一个[slice](https://gocn.github.io/How-To-Code-in-Go/docs/12-How_To_Convert_Data_Types_in_Go/)的冰激凌口味。然后我们使用一个[`for loop`]({{< relref "/docs/25-How_To_Construct_For_Loops_in_Go.md" >}})来迭代它们。我们使用三个`if`语句来打印不同的信息,表明对不同冰淇淋口味的偏好。每个`if`语句必须使用`continue`语句来停止`for`循环的执行,这样就不会在最后打印出首选冰淇淋口味的默认信息。 +在`main`中,我们定义了一个[slice](https://gocn.github.io/How-To-Code-in-Go/docs/12-How_To_Convert_Data_Types_in_Go/)的冰激凌口味。然后我们使用一个[`for loop`](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go)来迭代它们。我们使用三个`if`语句来打印不同的信息,表明对不同冰淇淋口味的偏好。每个`if`语句必须使用`continue`语句来停止`for`循环的执行,这样就不会在最后打印出首选冰淇淋口味的默认信息。 当我们添加新的偏好时,我们必须不断添加`if`语句来处理新的情况。重复的信息,如 "香草"和 "巧克力"的情况,必须有重复的`if`语句。对于我们代码的未来读者(包括我们自己)来说,`if`语句的重复性掩盖了它们所做的重要部分--将变量与多个值进行比较并采取不同的行动。另外,我们的回退信息与条件语句分开,使得它看起来不相关。转换器 "语句可以帮助我们更好地组织这个逻辑。 @@ -146,7 +146,7 @@ You win! 我们的猜谜游戏需要一个随机数来比较猜测的结果,所以我们使用`math/rand`包中的`rand.Intn`函数。为了确保我们每次玩游戏都能得到不同的`target`值,我们使用`rand.Seed`来根据当前时间随机化随机数发生器。`rand.Intn`的参数`100`将给我们一个0-100范围内的数字。然后我们使用`for`循环来开始收集玩家的猜测。 -`fmt.Scanf`函数为我们提供了一种方法来读取用户的输入到我们选择的变量中。它接受一个格式化的字符串动词,将用户的输入转换为我们期望的类型。这里的`%d`意味着我们期望一个 `int`,我们传递 `guess` 变量的地址,这样 `fmt.Scanf` 就能够设置该变量。在[处理任何解析错误]({{< relref "/docs/17-Handling_Errors_in_Go_DigitalOcean.md" >}})之后,我们使用两个`if`语句来比较用户的猜测和`target`值。它们返回的`string`和`bool`一起控制显示给玩家的信息,以及游戏是否会退出。 +`fmt.Scanf`函数为我们提供了一种方法来读取用户的输入到我们选择的变量中。它接受一个格式化的字符串动词,将用户的输入转换为我们期望的类型。这里的`%d`意味着我们期望一个 `int`,我们传递 `guess` 变量的地址,这样 `fmt.Scanf` 就能够设置该变量。在[处理任何解析错误](https://gocn.github.io/How-To-Code-in-Go/docs/17-Handling_Errors_in_Go_DigitalOcean)之后,我们使用两个`if`语句来比较用户的猜测和`target`值。它们返回的`string`和`bool`一起控制显示给玩家的信息,以及游戏是否会退出。 这些 `if` 语句掩盖了一个事实,即变量被比较的数值范围都有某种联系。一眼就能看出我们是否遗漏了该范围的某些部分,这也是很困难的。下一个例子重构了前面的例子,用`switch`语句代替: @@ -242,4 +242,4 @@ Go 开发人员不经常使用`fallthrough`关键字。通常情况下,通过 `switch`语句帮助我们向阅读代码的其他开发者传达出彼此有某种联系。使我们在将来添加新的情况时更容易添加不同的行为,并有可能确保任何忘记的事情也能通过`default`子句得到正确处理。下次你发现自己写的多个`if`语句都涉及同一个变量时,试着用`switch`语句重写它--你会发现当需要考虑其他值时,它将更容易重写。 -如果你想了解更多关于 Go 编程语言的信息,请查看整个[How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/) \ No newline at end of file +如果你想了解更多关于 Go 编程语言的信息,请查看整个[How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/) diff --git a/content/zh/docs/25-How_To_Construct_For_Loops_in_Go.md b/content/zh/docs/25-How_To_Construct_For_Loops_in_Go.md index 681a754..18b807e 100644 --- a/content/zh/docs/25-How_To_Construct_For_Loops_in_Go.md +++ b/content/zh/docs/25-How_To_Construct_For_Loops_in_Go.md @@ -6,7 +6,7 @@ 在 Go 中,`for` 循环是基于循环计数器或循环变量实现代码的重复执行。与其他具有多个循环结构(例如 `while` , `do` 等 )的编程语言不同,Go 只有 `for` 循环。这有助于使您的代码更清晰和更具可读性,因为您不必担心会有多种策略来实现相同的循环结构。在开发过程中,这种强可读性和低认知负担也将使您的代码比其他语言更不容易出错。 -在本教程中,您将了解 Go 中 `for` 循环是如何工作的,包括其使用的三个主要变体。我们将首先展示如何创建不同类型的 `for` 循环,然后介绍如何[在 Go 中遍历顺序数据类型]({{< relref "/docs/16-Understanding_Arrays_and_Slices_in_Go.md" >}})。最后,我们将解释如何使用嵌套循环。 +在本教程中,您将了解 Go 中 `for` 循环是如何工作的,包括其使用的三个主要变体。我们将首先展示如何创建不同类型的 `for` 循环,然后介绍如何[在 Go 中遍历顺序数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go)。最后,我们将解释如何使用嵌套循环。 ## 声明 ForClause 和 Condition 循环 @@ -192,13 +192,13 @@ func main() { } ``` -在前面的代码中,`buf :=bytes.NewBufferString("one\ntwo\nthree\nfour\n")` 声明了一个包含一些数据的缓冲区。因为我们不知道缓冲区何时会完成读取,所以我们创建了一个没有子句的 `for` 循环。在 `for` 循环内部,我们使用 `line, err := buf.ReadString('\n')` 从缓冲区读取一行并检查从缓冲区读取是否有错误。如果有,我们解决错误,并[使用 `break` 关键字退出 for 循环]({{< relref "/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md" >}})。有了`break`,您就不需要使用停止循环的条件。 +在前面的代码中,`buf :=bytes.NewBufferString("one\ntwo\nthree\nfour\n")` 声明了一个包含一些数据的缓冲区。因为我们不知道缓冲区何时会完成读取,所以我们创建了一个没有子句的 `for` 循环。在 `for` 循环内部,我们使用 `line, err := buf.ReadString('\n')` 从缓冲区读取一行并检查从缓冲区读取是否有错误。如果有,我们解决错误,并[使用 `break` 关键字退出 for 循环](https://gocn.github.io/How-To-Code-in-Go/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go)。有了`break`,您就不需要使用停止循环的条件。 在本节中,我们学习了如何声明 ForClause 循环并使用它来迭代已知范围的值。我们还学习了如何使用 Condition 循环进行迭代,直到满足特定条件。接下来,我们将了解 RangeClause 如何用于迭代顺序数据类型。 ## 使用 RangeClause 循环遍历顺序数据类型 -在 Go 中,使用 `for` 循环来迭代连续或集合数据类型(如[切片、数组]({{< relref "/docs/16-Understanding_Arrays_and_Slices_in_Go.md" >}})和[字符串]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}))的元素是很常见的。为了更容易做到这一点,我们可以使用带有 _RangeClause_ 语法的 `for` 循环。虽然您可以使用 ForClause 语法遍历顺序数据类型,但 RangeClause 更简洁且更易于阅读。 +在 Go 中,使用 `for` 循环来迭代连续或集合数据类型(如[切片、数组](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go)和[字符串](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go))的元素是很常见的。为了更容易做到这一点,我们可以使用带有 _RangeClause_ 语法的 `for` 循环。虽然您可以使用 ForClause 语法遍历顺序数据类型,但 RangeClause 更简洁且更易于阅读。 在我们研究使用 RangeClause 之前,让我们看看如何使用 ForClause 语法遍历切片: @@ -394,7 +394,7 @@ m y ``` -当遍历 [map]({{< relref "/docs/15-Understanding_Maps_in_Go.md" >}}) 时,`range`将返回**键**和**值**: +当遍历 [map](https://gocn.github.io/How-To-Code-in-Go/docs/15-Understanding_Maps_in_Go) 时,`range`将返回**键**和**值**: ```go package main @@ -543,4 +543,4 @@ Output ## 结论 -在本教程中,我们学习了如何声明和使用 `for` 循环来解决 Go 中的重复任务。我们还学习了 `for` 循环的三种不同变体以及何时使用它们。要了解有关 `for` 循环以及如何控制它们的流程的更多信息,请阅读[在 Go 中使用循环时的 Break 和 Continue 语句]({{< relref "/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md" >}})。 +在本教程中,我们学习了如何声明和使用 `for` 循环来解决 Go 中的重复任务。我们还学习了 `for` 循环的三种不同变体以及何时使用它们。要了解有关 `for` 循环以及如何控制它们的流程的更多信息,请阅读[在 Go 中使用循环时的 Break 和 Continue 语句](https://gocn.github.io/How-To-Code-in-Go/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go)。 diff --git a/content/zh/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md b/content/zh/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md index b02279f..a3c34d2 100644 --- a/content/zh/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md +++ b/content/zh/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md @@ -8,7 +8,7 @@ ## Break 语句 -在 Go 中, `break` 语句终止当前循环的执行。`break`几乎总是与[条件`if`语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})配对。 +在 Go 中, `break` 语句终止当前循环的执行。`break`几乎总是与[条件`if`语句](https://gocn.github.io/How-To-Code-in-Go/docs/23-How_To_Write_Conditional_Statements_in_Go)配对。 让我们看一个在循环中使用`break`语句的示例: diff --git a/content/zh/docs/27-How_To_Define_and_Call_Functions_in_Go.md b/content/zh/docs/27-How_To_Define_and_Call_Functions_in_Go.md index 7741016..d12616d 100644 --- a/content/zh/docs/27-How_To_Define_and_Call_Functions_in_Go.md +++ b/content/zh/docs/27-How_To_Define_and_Call_Functions_in_Go.md @@ -15,7 +15,7 @@ Go 附带了强大的标准库,其中包含许多预定义的函数。您可 ## 定义一个函数 -让我们从经典的[“Hello, World!”程序]({{< relref "/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md" >}})开始理解函数。 +让我们从经典的[“Hello, World!”程序](https://gocn.github.io/How-To-Code-in-Go/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean)开始理解函数。 我们将在一个文本编辑器中创建一个新的文本文件,然后调用程序 `hello.go`。然后,我们将在里面定义函数。 @@ -80,7 +80,7 @@ func main() { } ``` -函数可以比我们定义的 `hello()` 函数更复杂。我们可以在函数中使用[`for`循环]({{< relref "/docs/25-How_To_Construct_For_Loops_in_Go.md" >}})、[条件语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})等。 +函数可以比我们定义的 `hello()` 函数更复杂。我们可以在函数中使用[`for`循环](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go)、[条件语句](https://gocn.github.io/How-To-Code-in-Go/docs/23-How_To_Write_Conditional_Statements_in_Go)等。 例如,以下函数使用条件语句检查 `name` 变量的输入是否包含元音,并使用 `for` 循环遍历 `name` 字符串中的字母。 @@ -120,7 +120,7 @@ func names() { 到目前为止,我们已经研究了带有空括号且不带参数的函数,但我们是可以在函数定义中的括号内定义参数的。 -_参数_ 是函数定义中的命名实体,指定函数可以接受的参数。在 Go 中,您必须为每个参数指定[数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})。 +_参数_ 是函数定义中的命名实体,指定函数可以接受的参数。在 Go 中,您必须为每个参数指定[数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go)。 让我们创建一个将单词重复指定次数的程序。它将接受一个 `string` 类型的 `word` 参数和一个用于重复单词的次数的 `int` 类型参数 `reps`。 @@ -346,4 +346,4 @@ invalid value of -1 provided for reps. value must be greater than 0. 函数是在程序中执行操作指令的代码块,有助于使我们的代码更好地可重用和模块化。 -要了解有关如何使您的代码更模块化的更多信息,您可以阅读我们关于[如何在 Go 中编写包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}})的指南。 +要了解有关如何使您的代码更模块化的更多信息,您可以阅读我们关于[如何在 Go 中编写包](https://gocn.github.io/How-To-Code-in-Go/docs/21-How_To_Write_Packages_in_Go)的指南。 diff --git a/content/zh/docs/28-How_To_Use_Variadic_Functions_in_Go.md b/content/zh/docs/28-How_To_Use_Variadic_Functions_in_Go.md index 0db7d4d..f45707d 100644 --- a/content/zh/docs/28-How_To_Use_Variadic_Functions_in_Go.md +++ b/content/zh/docs/28-How_To_Use_Variadic_Functions_in_Go.md @@ -10,7 +10,7 @@ _可变参数函数_ 是可以接受零个、一个或多个值作为单个参 func Println(a ...interface{}) (n int, err error) ``` -参数前面带有一组省略号 ( `...` )的[函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})被视为可变参数函数。省略号表示提供的参数可以是零个、一个或多个。对于`fmt.Println`包,它声明参数`a`是可变参数。 +参数前面带有一组省略号 ( `...` )的[函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)被视为可变参数函数。省略号表示提供的参数可以是零个、一个或多个。对于`fmt.Println`包,它声明参数`a`是可变参数。 让我们创建一个使用 `fmt.Println` 函数并传入零个、一个或多个值的程序: @@ -385,4 +385,4 @@ Sammy - 当输入参数的数量未知或调用时会发生变化。 - 使您的代码更具可读性。 -要了解有关创建和调用函数的更多信息,您可以阅读[如何在 Go 中定义和调用函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})。 +要了解有关创建和调用函数的更多信息,您可以阅读[如何在 Go 中定义和调用函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)。 diff --git a/content/zh/docs/29-Understanding_defer_in_Go.md b/content/zh/docs/29-Understanding_defer_in_Go.md index fb2b29e..4f8b343 100644 --- a/content/zh/docs/29-Understanding_defer_in_Go.md +++ b/content/zh/docs/29-Understanding_defer_in_Go.md @@ -10,7 +10,7 @@ Go 有许多其他编程语言中常见的控制流关键字,如 `if`、`switc ## 什么是 `defer` 语句 -`defer` 语句将 `defer` 关键字后面的[函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})调用添加到一个栈中。当该语句所在的函数返回时,将执行堆栈中所有的函数调用。由于这些调用位于堆栈上,因此将按照后进先出的顺序进行调用。 +`defer` 语句将 `defer` 关键字后面的[函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)调用添加到一个栈中。当该语句所在的函数返回时,将执行堆栈中所有的函数调用。由于这些调用位于堆栈上,因此将按照后进先出的顺序进行调用。 让我们看看 `defer` 是如何工作的,打印出一些文本: diff --git a/content/zh/docs/30-Understanding_init_in_Go.md b/content/zh/docs/30-Understanding_init_in_Go.md index e5c1037..8a896e0 100644 --- a/content/zh/docs/30-Understanding_init_in_Go.md +++ b/content/zh/docs/30-Understanding_init_in_Go.md @@ -1,7 +1,7 @@ # 了解 Go 中的 init ## 简介 -在 Go 中,预定义的 `init()` 函数设置了一段代码,在你的包的任何其他部分之前运行。这段代码将在[包被导入]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})后立即执行,当你需要你的应用程序在一个特定的状态下初始化时,例如你有一个特定的配置或一组资源,你的应用程序需要用它来启动。它也可以在*导入副作用*时使用,这是一种通过导入特定包来设置程序状态的技术。这经常被用于 `register` 一个包和另一个包,以确保程序考虑任务的正确代码。 +在 Go 中,预定义的 `init()` 函数设置了一段代码,在你的包的任何其他部分之前运行。这段代码将在[包被导入](https://gocn.github.io/How-To-Code-in-Go/docs/20-Importing_Packages_in_Go_DigitalOcean)后立即执行,当你需要你的应用程序在一个特定的状态下初始化时,例如你有一个特定的配置或一组资源,你的应用程序需要用它来启动。它也可以在*导入副作用*时使用,这是一种通过导入特定包来设置程序状态的技术。这经常被用于 `register` 一个包和另一个包,以确保程序考虑任务的正确代码。 尽管 `init()` 是一个有用的工具,但它有时会使代码难以阅读,因为难以找到的 `init()` 实例会大大影响代码的运行顺序。正因为如此,对于刚接触 Go 的开发者来说,了解这个函数的方方面面是非常重要的,这样他们在写代码时就能确保以可读的方式使用 `init()`。 @@ -11,7 +11,7 @@ 对于本文中的一些例子,你将需要: -* 按照 [如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作空间。本教程将使用以下文件结构: +* 按照 [如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置的 Go 工作空间。本教程将使用以下文件结构: ```shell . ├── bin @@ -40,7 +40,7 @@ func main() { } ``` -在这个程序中,我们声明了一个全局[变量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}}),叫做 `weekday`。默认情况下,`weekday` 的值是一个空字符串。 +在这个程序中,我们声明了一个全局[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go),叫做 `weekday`。默认情况下,`weekday` 的值是一个空字符串。 让我们运行这段代码: @@ -85,7 +85,7 @@ Today is Monday ## 导入时初始化软件包 -首先,我们将写一些代码,从[切片]({{< relref "/docs/16-Understanding_Arrays_and_Slices_in_Go.md" >}})中选择一个随机的生物并打印出来。然而,我们不会在初始程序中使用 `init()`。这将更好地展示我们的问题,以及 `init()` 将如何解决我们的问题。 +首先,我们将写一些代码,从[切片](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go)中选择一个随机的生物并打印出来。然而,我们不会在初始程序中使用 `init()`。这将更好地展示我们的问题,以及 `init()` 将如何解决我们的问题。 在你的 `src/github.com/gopherguides/ `目录中,用以下命令创建一个名为 `creature` 的文件夹。 @@ -517,4 +517,4 @@ func decode(reader io.Reader) image.Rectangle { 在本教程中,我们了解到 `init()` 函数是在你的包中的其他代码被加载之前加载的,它可以为一个包执行特定的任务,如初始化一个期望的状态。我们还了解到,编译器执行多个 `init()` 语句的顺序取决于编译器加载源文件的顺序。如果你想了解更多关于 `init()` 的信息,请查看官方的[Golang 文档](https://golang.org/doc/effective_go.html#init),或者阅读[Go 社区中关于该函数的讨论](https://github.com/golang/go/issues/25885)。 -你可以通过我们的[如何在 Go 中定义和调用函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})文章阅读更多关于函数的信息,或者探索[整个 Go 中如何编程系列](https://gocn.github.io/How-To-Code-in-Go/)。 +你可以通过我们的[如何在 Go 中定义和调用函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)文章阅读更多关于函数的信息,或者探索[整个 Go 中如何编程系列](https://gocn.github.io/How-To-Code-in-Go/)。 diff --git a/content/zh/docs/31-Customizing_Go_Binaries_with_Build_Tags.md b/content/zh/docs/31-Customizing_Go_Binaries_with_Build_Tags.md index 8f9f3ec..defef29 100644 --- a/content/zh/docs/31-Customizing_Go_Binaries_with_Build_Tags.md +++ b/content/zh/docs/31-Customizing_Go_Binaries_with_Build_Tags.md @@ -12,7 +12,7 @@ 要遵循本文的例子,你将需要: -* 按照 [如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作区。 +* 按照 [如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置的 Go 工作区。 ## 构建免费版本 @@ -56,7 +56,7 @@ func main() { } } ``` -在这个文件中,我们创建了一个程序,声明了一个名为 `features` 的[切片](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go/#%E5%88%87%E7%89%87),它容纳了两个[字符串]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}),代表我们免费版本的应用程序的特征。应用程序中的 `main()` 函数使用一个 `for` [循环](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go/#%E4%BD%BF%E7%94%A8-rangeclause-%E5%BE%AA%E7%8E%AF%E9%81%8D%E5%8E%86%E9%A1%BA%E5%BA%8F%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) `range` 遍历 `features` 切片,并将所有可用的功能打印到屏幕上。 +在这个文件中,我们创建了一个程序,声明了一个名为 `features` 的[切片](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go/#%E5%88%87%E7%89%87),它容纳了两个[字符串](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go),代表我们免费版本的应用程序的特征。应用程序中的 `main()` 函数使用一个 `for` [循环](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go/#%E4%BD%BF%E7%94%A8-rangeclause-%E5%BE%AA%E7%8E%AF%E9%81%8D%E5%8E%86%E9%A1%BA%E5%BA%8F%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) `range` 遍历 `features` 切片,并将所有可用的功能打印到屏幕上。 保存并退出该文件。现在这个文件已经保存了,在文章的其余部分,我们将不再需要编辑它。相反,我们将使用构建标签来改变我们将从中构建的二进制文件的功能特性。 @@ -192,7 +192,7 @@ go build -tags pro ## 构建标签布尔逻辑 -当一个 Go 包中有多个构建标签时,这些标签会使用[布尔逻辑]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})相互作用。为了证明这一点,我们将同时使用 `pro` 标签和 `enterprise` 标签来添加我们应用程序的企业级。 +当一个 Go 包中有多个构建标签时,这些标签会使用[布尔逻辑](https://gocn.github.io/How-To-Code-in-Go/docs/14-Understanding_Boolean_Logic_in_Go)相互作用。为了证明这一点,我们将同时使用 `pro` 标签和 `enterprise` 标签来添加我们应用程序的企业级。 为了建立一个企业级二进制文件,我们将需要包括默认功能、专业级功能和一套新的企业级功能。首先,打开一个编辑器并创建一个新文件,`enterprise.go`,它将添加新的企业级功能: diff --git a/content/zh/docs/32-Understanding_Pointers_in_Go.md b/content/zh/docs/32-Understanding_Pointers_in_Go.md index a46b2a9..d5d84f3 100644 --- a/content/zh/docs/32-Understanding_Pointers_in_Go.md +++ b/content/zh/docs/32-Understanding_Pointers_in_Go.md @@ -153,7 +153,7 @@ creature = jellyfish ## 函数指针接收器 -当你写一个函数时,你可以定义参数,以 *值* 或 *引用* 的方式传递。通过 *值* 传递意味着该值的副本被发送到函数中,并且在该函数中对该参数的任何改变 *只* 在该函数中影响该变量,而不是从哪里传递。然而,如果你通过 *引用* 传递,意味着你传递了一个指向该参数的指针,你可以在函数中改变该值,也可以改变传递进来的原始变量的值。你可以在我们的[如何在 Go 中定义和调用函数]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})中阅读更多关于如何定义函数的信息。 +当你写一个函数时,你可以定义参数,以 *值* 或 *引用* 的方式传递。通过 *值* 传递意味着该值的副本被发送到函数中,并且在该函数中对该参数的任何改变 *只* 在该函数中影响该变量,而不是从哪里传递。然而,如果你通过 *引用* 传递,意味着你传递了一个指向该参数的指针,你可以在函数中改变该值,也可以改变传递进来的原始变量的值。你可以在我们的[如何在 Go 中定义和调用函数](https://gocn.github.io/How-To-Code-in-Go/docs/32-Understanding_Pointers_in_Go)中阅读更多关于如何定义函数的信息。 什么时候传递一个指针,什么时候发送一个值,都取决于你是否希望这个值发生变化。如果你不希望数值改变,就把它作为一个值来发送。如果你希望你传递给你的变量的函数能够改变它,那么你就把它作为一个指针传递。 @@ -236,7 +236,7 @@ func changeCreature(creature *Creature) { 注意,现在当我们在 `changeCreature` 函数中把 `Species` 的值改为 `jellyfish` 时,它也改变了 `main` 函数中定义的原始值。这是因为我们通过 *引用* 传递了 `creature` 变量,它允许访问内存里的原始值并可以根据需要改变它。 因此,如果你想让一个函数能够改变一个值,你需要通过引用来传递它。要通过引用传递,你就需要传递变量的指针,而不是变量本身。 -然而,有时你可能没有为一个指针定义一个实际的值。在这些情况下,有可能在程序中出现[恐慌]({{< relref "/docs/19-Handling_Panics_in_Go _DigitalOcean.md" >}})。让我们来看看这种情况是如何发生的,以及如何对这种潜在的问题进行规划。 +然而,有时你可能没有为一个指针定义一个实际的值。在这些情况下,有可能在程序中出现[恐慌](https://gocn.github.io/How-To-Code-in-Go/docs/19-Handling_Panics_in_Go _DigitalOcean)。让我们来看看这种情况是如何发生的,以及如何对这种潜在的问题进行规划。 ## 空指针 diff --git a/content/zh/docs/33-Defining_Structs_in_Go.md b/content/zh/docs/33-Defining_Structs_in_Go.md index ffef492..cc4ad19 100644 --- a/content/zh/docs/33-Defining_Structs_in_Go.md +++ b/content/zh/docs/33-Defining_Structs_in_Go.md @@ -2,7 +2,7 @@ ## 简介 -围绕具体的细节建立抽象,是编程语言能给开发者的最大工具。结构体使我们可以谈论 `Address` 而不是通过描述 `Street`, `City`, 或 `PostalCode` 字符串来进行推断。它们作为[文档]({{< relref "/docs/06-How_To_Write_Comments_in_Go.md" >}})的一个自然纽带,致力于告诉未来的开发者(包括我们自己)哪些数据对我们的 Go 程序是重要的,以及未来的代码应该如何正确使用这些数据。结构体可以用几种不同的方式来定义和使用。在本教程中,我们将会逐一看下这些技术。 +围绕具体的细节建立抽象,是编程语言能给开发者的最大工具。结构体使我们可以谈论 `Address` 而不是通过描述 `Street`, `City`, 或 `PostalCode` 字符串来进行推断。它们作为[文档](https://gocn.github.io/How-To-Code-in-Go/docs/06-How_To_Write_Comments_in_Go)的一个自然纽带,致力于告诉未来的开发者(包括我们自己)哪些数据对我们的 Go 程序是重要的,以及未来的代码应该如何正确使用这些数据。结构体可以用几种不同的方式来定义和使用。在本教程中,我们将会逐一看下这些技术。 ## 定义结构体 @@ -139,4 +139,4 @@ Sammy the Shark ## 总结 -结构体是开发者为组织信息而定义的各种各样的数据的集合。大多数程序都要处理大量的数据,如果没有结构体,就很难记住哪些 `string` 或 `int` 变量相关,哪些无关。下一次,当你发现自己在组织一些变量时,问问自己,也许这些变量用 `struct` 来分组会更好。这些变量可能一直都在描述更高层级的概念。 \ No newline at end of file +结构体是开发者为组织信息而定义的各种各样的数据的集合。大多数程序都要处理大量的数据,如果没有结构体,就很难记住哪些 `string` 或 `int` 变量相关,哪些无关。下一次,当你发现自己在组织一些变量时,问问自己,也许这些变量用 `struct` 来分组会更好。这些变量可能一直都在描述更高层级的概念。 diff --git a/content/zh/docs/34-Defining_Methods_in_Go.md b/content/zh/docs/34-Defining_Methods_in_Go.md index 129db89..efd4b7a 100644 --- a/content/zh/docs/34-Defining_Methods_in_Go.md +++ b/content/zh/docs/34-Defining_Methods_in_Go.md @@ -2,7 +2,7 @@ ## 简介 -[函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})允许你将逻辑组织成可重复的程序,每次运行时可以使用不同的参数。在定义函数的过程中,你常常会发现,可能会有多个函数每次对同一块数据进行操作。Go 可以识别这种模式,并允许您定义特殊的函数,称为方法,其目的是对某些特定类型(称为接收器)的实例进行操作。将方法添加到类型中,不仅可以传达数据是什么,还可以传达如何使用这些数据。 +[函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)允许你将逻辑组织成可重复的程序,每次运行时可以使用不同的参数。在定义函数的过程中,你常常会发现,可能会有多个函数每次对同一块数据进行操作。Go 可以识别这种模式,并允许您定义特殊的函数,称为方法,其目的是对某些特定类型(称为接收器)的实例进行操作。将方法添加到类型中,不仅可以传达数据是什么,还可以传达如何使用这些数据。 ## 定义一个方法 @@ -197,7 +197,7 @@ src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument want String() string ``` -在到目前为止的例子中,我们已经在值接收器上定义了方法。也就是说,如果我们使用方法的功能调用,第一个参数(指的是方法所定义的类型)将是一个该类型的值,而不是一个[指针]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})。因此,我们对方法实例所做的任何修改都会在方法执行完毕后被丢弃,因为收到的值是数据的副本。此外,我们也可以在一个类型的指针接收器上定义方法。 +在到目前为止的例子中,我们已经在值接收器上定义了方法。也就是说,如果我们使用方法的功能调用,第一个参数(指的是方法所定义的类型)将是一个该类型的值,而不是一个[指针](https://gocn.github.io/How-To-Code-in-Go/docs/32-Understanding_Pointers_in_Go)。因此,我们对方法实例所做的任何修改都会在方法执行完毕后被丢弃,因为收到的值是数据的副本。此外,我们也可以在一个类型的指针接收器上定义方法。 ## 指针接收器 @@ -328,6 +328,6 @@ Go 编译器很好心地告诉我们,`Shark` 确实有一个 `Dive` 方法, ## 总结 -在 Go 中声明方法与定义接收不同类型变量的函数本质上没有区别。同样,[使用指针]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})规则也适用。Go 为这种极其常见的函数定义提供了一些便利,并将这些方法收集到可以通过接口类型进行要求的方法集中。有效地使用方法可以让你在代码中使用接口来提高可测试性,并为你的代码的未来读者留下更好的结构。 +在 Go 中声明方法与定义接收不同类型变量的函数本质上没有区别。同样,[使用指针](https://gocn.github.io/How-To-Code-in-Go/docs/32-Understanding_Pointers_in_Go)规则也适用。Go 为这种极其常见的函数定义提供了一些便利,并将这些方法收集到可以通过接口类型进行要求的方法集中。有效地使用方法可以让你在代码中使用接口来提高可测试性,并为你的代码的未来读者留下更好的结构。 -如果你想了解更多关于 Go 编程语言的一般信息,请查看我们的 [How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)。 \ No newline at end of file +如果你想了解更多关于 Go 编程语言的一般信息,请查看我们的 [How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)。 diff --git a/content/zh/docs/35-How_To_Build_and_Install_Go_Programs.md b/content/zh/docs/35-How_To_Build_and_Install_Go_Programs.md index 46c9d2a..8dae8ff 100644 --- a/content/zh/docs/35-How_To_Build_and_Install_Go_Programs.md +++ b/content/zh/docs/35-How_To_Build_and_Install_Go_Programs.md @@ -12,11 +12,11 @@ 要遵循本文的例子,你将需要: -- 按照[如何安装 Go 与设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作区。 +- 按照[如何安装 Go 与设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置的 Go 工作区。 ## 第 1 步 - 设置和运行 Go 二进制文件 -首先,创建一个应用程序,作为演示 Go 工具链的例子。要做到这一点,你将使用[如何用 Go 写第一个程序]({{< relref "/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md" >}})教程中的经典程序 "Hello, World!"。 +首先,创建一个应用程序,作为演示 Go 工具链的例子。要做到这一点,你将使用[如何用 Go 写第一个程序](https://gocn.github.io/How-To-Code-in-Go/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean)教程中的经典程序 "Hello, World!"。 在你的 `src` 目录下创建一个名为 `greeter` 的目录: @@ -114,7 +114,7 @@ greeter main.go go.mod 注:在Windows上,你的可执行文件将是 greeter.exe。 ``` -默认情况下,`go build` 将为当前[平台和架构](https://gocn.github.io/How-To-Code-in-Go/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures/#goos%E5%92%8Cgoarch%E5%8F%AF%E8%83%BD%E6%94%AF%E6%8C%81%E7%9A%84%E5%B9%B3%E5%8F%B0)生成一个可执行文件。例如,如果在 `linux/386` 系统上构建,可执行文件将与任何其他 `linux/386` 系统兼容,即使并没有安装 Go。Go 支持为其他平台和架构进行构建,你可以在我们的[为不同操作系统和架构构建 Go 应用程序]({{< relref "/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md" >}})文章中了解更多信息。 +默认情况下,`go build` 将为当前[平台和架构](https://gocn.github.io/How-To-Code-in-Go/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures/#goos%E5%92%8Cgoarch%E5%8F%AF%E8%83%BD%E6%94%AF%E6%8C%81%E7%9A%84%E5%B9%B3%E5%8F%B0)生成一个可执行文件。例如,如果在 `linux/386` 系统上构建,可执行文件将与任何其他 `linux/386` 系统兼容,即使并没有安装 Go。Go 支持为其他平台和架构进行构建,你可以在我们的[为不同操作系统和架构构建 Go 应用程序](https://gocn.github.io/How-To-Code-in-Go/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures)文章中了解更多信息。 现在,你已经创建了你的可执行文件,运行它以确保二进制文件已被正确构建。在 macOS 或 Linux 上,运行以下命令: @@ -203,7 +203,7 @@ Output $HOME/go ``` -由于 `go install` 会将生成的可执行文件放入 `$GOPATH` 的一个子目录,名为 `bin`,这个目录必须被添加到 `$PATH` 环境变量中。这在先决条件文章[如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})的**创建 Go 工作空间**步骤中有所涉及。 +由于 `go install` 会将生成的可执行文件放入 `$GOPATH` 的一个子目录,名为 `bin`,这个目录必须被添加到 `$PATH` 环境变量中。这在先决条件文章[如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)的**创建 Go 工作空间**步骤中有所涉及。 设置好 `$GOPATH/bin` 目录后,切回你的 `greeter` 目录: @@ -259,4 +259,4 @@ Hello, World! 在本教程中,你演示了 Go 工具链是如何从源代码中轻松构建可执行二进制文件的。这些二进制文件可以分发到其他系统上运行,甚至是那些没有 Go 工具链和环境的系统。你还使用 `go install` 自动构建并将程序作为可执行文件安装在系统的 `$PATH` 中。有了 `go build` 和 `go install`,你现在可以随意分享和使用你的应用程序。 -现在你了解了 `go build` 的基础知识,你可以通过[用 Build 标签定制 Go 二进制文件]({{< relref "/docs/31-Customizing_Go_Binaries_with_Build_Tags.md" >}})教程来探索如何制作模块化的源代码,或者通过[为不同的操作系统和架构构建 Go 应用程序]({{< relref "/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md" >}})来探索如何为不同的平台构建。如果你想了解更多关于 Go 编程语言的信息,请查看整个[How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)。 \ No newline at end of file +现在你了解了 `go build` 的基础知识,你可以通过[用 Build 标签定制 Go 二进制文件](https://gocn.github.io/How-To-Code-in-Go/docs/31-Customizing_Go_Binaries_with_Build_Tags)教程来探索如何制作模块化的源代码,或者通过[为不同的操作系统和架构构建 Go 应用程序](https://gocn.github.io/How-To-Code-in-Go/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures)来探索如何为不同的平台构建。如果你想了解更多关于 Go 编程语言的信息,请查看整个[How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)。 diff --git a/content/zh/docs/36-How_To_Use_Struct_Tags_in_Go.md b/content/zh/docs/36-How_To_Use_Struct_Tags_in_Go.md index 5a993f1..7f50ab2 100644 --- a/content/zh/docs/36-How_To_Use_Struct_Tags_in_Go.md +++ b/content/zh/docs/36-How_To_Use_Struct_Tags_in_Go.md @@ -2,7 +2,7 @@ ## 简介 -结构,或称结构体,被用来将多个信息聚合在一个单元中。这些[信息集合]({{< relref "/docs/33-Defining_Structs_in_Go.md" >}})被用来描述更高层次的概念,例如由 `Street`、`City`、`State` 和 `PostalCode` 组成的 `Address`。当你从数据库或 API 等系统中读取这些信息时,你可以使用结构体标签来控制这些信息如何被分配到结构体的字段中。结构体标签是附加在结构体字段上的小块元数据,为与该结构体一起工作的其他 Go 代码提供指示。 +结构,或称结构体,被用来将多个信息聚合在一个单元中。这些[信息集合](https://gocn.github.io/How-To-Code-in-Go/docs/33-Defining_Structs_in_Go)被用来描述更高层次的概念,例如由 `Street`、`City`、`State` 和 `PostalCode` 组成的 `Address`。当你从数据库或 API 等系统中读取这些信息时,你可以使用结构体标签来控制这些信息如何被分配到结构体的字段中。结构体标签是附加在结构体字段上的小块元数据,为与该结构体一起工作的其他 Go 代码提供指示。 ## 结构体标签是怎么样的 @@ -333,4 +333,4 @@ Output ## 总结 -结构体标签提供了一种强大的手段来拓展了使用你定义的结构体代码的功能。许多标准库和第三方包提供了通过使用结构体标签来定制其操作的方法。在你的代码中有效地使用它们,既能提供这种定制行为,又能为未来的开发者简要记录这些字段的使用方法。 \ No newline at end of file +结构体标签提供了一种强大的手段来拓展了使用你定义的结构体代码的功能。许多标准库和第三方包提供了通过使用结构体标签来定制其操作的方法。在你的代码中有效地使用它们,既能提供这种定制行为,又能为未来的开发者简要记录这些字段的使用方法。 diff --git a/content/zh/docs/37-How_To_Use_Interfaces_in_Go.md b/content/zh/docs/37-How_To_Use_Interfaces_in_Go.md index c904b20..f01d6c5 100644 --- a/content/zh/docs/37-How_To_Use_Interfaces_in_Go.md +++ b/content/zh/docs/37-How_To_Use_Interfaces_in_Go.md @@ -47,7 +47,7 @@ func main() { } ``` -第一件事是我们创建了一个新的类型叫做`Article`。这个类型有一个`Title`和一个`Author`字段,两个都是 string 的 [数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}): +第一件事是我们创建了一个新的类型叫做`Article`。这个类型有一个`Title`和一个`Author`字段,两个都是 string 的 [数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go): ```go ... @@ -58,7 +58,7 @@ type Article struct { ... ``` -接着,我们为 Article 类型定义了一个叫做 String 的 [`方法`]({{< relref "/docs/34-Defining_Methods_in_Go.md" >}})。`String`方法将会返回一个用于表示`Article`类型的字符串: +接着,我们为 Article 类型定义了一个叫做 String 的 [`方法`](https://gocn.github.io/How-To-Code-in-Go/docs/34-Defining_Methods_in_Go)。`String`方法将会返回一个用于表示`Article`类型的字符串: ```go ... @@ -68,7 +68,7 @@ func (a Article) String() string { ... ``` -然后,在我们的`main`[function]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})里,我们创建一个`Article`类型的实例,并且将它赋值给一个[变量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})叫`a`。我们给`Title`字段设置了一个值,为`"理解Go中的Interfaces"`,给`Author`字段赋值`"Sammy Shark"`: +然后,在我们的`main`[function](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go)里,我们创建一个`Article`类型的实例,并且将它赋值给一个[变量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go)叫`a`。我们给`Title`字段设置了一个值,为`"理解Go中的Interfaces"`,给`Author`字段赋值`"Sammy Shark"`: ```go ... @@ -171,7 +171,7 @@ type Stringer interface { ... ``` -`Stringer`interface 只有一个方法,叫做`String()`,返回一个`string`。[method]({{< relref "/docs/34-Defining_Methods_in_Go.md" >}})是一个特殊的函数,在 Go 中被限定于一个特殊类型。不像函数,一个方法只能从它所定义的类型的实例中被调用。 +`Stringer`interface 只有一个方法,叫做`String()`,返回一个`string`。[method](https://gocn.github.io/How-To-Code-in-Go/docs/34-Defining_Methods_in_Go)是一个特殊的函数,在 Go 中被限定于一个特殊类型。不像函数,一个方法只能从它所定义的类型的实例中被调用。 然后我们更新`Print`方法的签名来接收一个`Stringer`,而不是一个`Article`的具体类型。因为编译器知道`Stringer`接口定义了`String`方法,所以它只接收也有`String`方法的类型。 diff --git a/content/zh/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md b/content/zh/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md index 8a68c9d..b205316 100644 --- a/content/zh/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md +++ b/content/zh/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md @@ -2,15 +2,15 @@ 在软件开发中,重要的是要考虑你想为之编译二进制的[操作系统](https://en.wikipedia.org/wiki/Operating_system)和底层处理器[架构](https://en.wikipedia.org/wiki/Microarchitecture)。因为在不同的操作系统/架构平台上运行一个二进制文件通常很慢或不可能,所以通常的做法是为许多不同的平台编译你最终的二进制文件,以最大化你的程序的受众。然而,这通常是很困难的,当你开发软件的平台和你想要部署的平台不是同一个的时候。例如,在过去,在 Windows 上开发一个程序并将其部署到 Linux 或 macOS 机器上,需要为每一个你想要的二进制文件的环境设置构建机器。你还需要保持你的工具同步,此外还有其他考虑因素,这些因素会增加成本,使协作测试和分布式更加困难。 -Go 通过在`go build`工具中直接建立对多平台的支持,以及 Go 工具链的其他部分解决了这个问题。通过使用[环境变量](https://www.digitalocean.com/community/tutorials/how-to-read-and-set-environmental-and-shell-variables-on-a-linux-vps)和[构建标签]({{< relref "/docs/31-Customizing_Go_Binaries_with_Build_Tags.md" >}}),你可以控制你最终的二进制文件是为哪个操作系统和架构构建的,此外还可以把一个工作流程放在一起,在不改变你的代码库的情况下快速切换对平台依赖的代码。 +Go 通过在`go build`工具中直接建立对多平台的支持,以及 Go 工具链的其他部分解决了这个问题。通过使用[环境变量](https://www.digitalocean.com/community/tutorials/how-to-read-and-set-environmental-and-shell-variables-on-a-linux-vps)和[构建标签](https://gocn.github.io/How-To-Code-in-Go/docs/31-Customizing_Go_Binaries_with_Build_Tags),你可以控制你最终的二进制文件是为哪个操作系统和架构构建的,此外还可以把一个工作流程放在一起,在不改变你的代码库的情况下快速切换对平台依赖的代码。 -在本教程中,你将把一个将[strings]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})连接成文件路径的示例应用程序放在一起,创建并有选择地包括与平台有关的片段,并在你自己的系统上为多个操作系统和系统架构构建二进制文件,向你展示如何使用 Go 编程语言的这一强大能力。 +在本教程中,你将把一个将[strings](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)连接成文件路径的示例应用程序放在一起,创建并有选择地包括与平台有关的片段,并在你自己的系统上为多个操作系统和系统架构构建二进制文件,向你展示如何使用 Go 编程语言的这一强大能力。 ## 前期准备 为了跟随本文的例子,你将需要: -- 按照[如何安装 Go 和设置本地程序环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 的 workspace +- 按照[如何安装 Go 和设置本地程序环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置的 Go 的 workspace ## `GOOS`和`GOARCH`可能支持的平台 @@ -107,7 +107,7 @@ func main() { } ``` -在这个文件的`main()`函数用`filepath.Join()`将三个[strings]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})用正确的,平台依赖的路径分隔符连接起来。 +在这个文件的`main()`函数用`filepath.Join()`将三个[strings](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)用正确的,平台依赖的路径分隔符连接起来。 保存并退出文件,然后运行程序: @@ -129,7 +129,7 @@ Output a/b/c ``` -这表明,由于这些操作系统使用的文件系统协议不同,程序将不得不为不同的平台构建不同的代码。但由于它已经根据操作系统使用了不同的文件分隔符,所有我们知道`filepath.Join()`已经考虑了平台的差异。这是因为 Go 工具链会自动检测你的机器的`GOOS`和`GOARCH`,并使用这些信息来使用具有正确[构建标签]({{< relref "/docs/31-Customizing_Go_Binaries_with_Build_Tags.md" >}})和文件分隔符的代码片段。 +这表明,由于这些操作系统使用的文件系统协议不同,程序将不得不为不同的平台构建不同的代码。但由于它已经根据操作系统使用了不同的文件分隔符,所有我们知道`filepath.Join()`已经考虑了平台的差异。这是因为 Go 工具链会自动检测你的机器的`GOOS`和`GOARCH`,并使用这些信息来使用具有正确[构建标签](https://gocn.github.io/How-To-Code-in-Go/docs/31-Customizing_Go_Binaries_with_Build_Tags)和文件分隔符的代码片段。 让我们思考一下`filepath.Join()`函数的分隔符是从哪里来的。运行以下命令来查看 Go 标准库中的相关片段: @@ -215,7 +215,7 @@ func main() { } ``` -Join 函数接收若干`parts`,并使用[strings 包]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})中的[`strings.Join()`](https://godoc.org/strings.Join)方法将它们连接起来,使用`PathSeparator`将各部分连接起来。 +Join 函数接收若干`parts`,并使用[strings 包](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)中的[`strings.Join()`](https://godoc.org/strings.Join)方法将它们连接起来,使用`PathSeparator`将各部分连接起来。 你还没有定义`PathSeparator`,所以现在在另一个文件中做。保存并退出`main.go`,打开你喜欢的编辑器,创建一个名为`path.go`的新文件。 @@ -245,7 +245,7 @@ Output a/b/c ``` -这样运行成功,得到一个 Unix 风格的文件路径。但这还不是我们想要的:无论在什么平台上运行,输出总是 a/b/c。为了添加创建 Windows 风格文件路径的功能,你需要添加一个 Windows 版本的`PathSeparator`,并告诉`go build`命令使用哪个版本。在下一节中,你将使用[构建标签]({{< relref "/docs/31-Customizing_Go_Binaries_with_Build_Tags.md" >}})来完成这个任务。 +这样运行成功,得到一个 Unix 风格的文件路径。但这还不是我们想要的:无论在什么平台上运行,输出总是 a/b/c。为了添加创建 Windows 风格文件路径的功能,你需要添加一个 Windows 版本的`PathSeparator`,并告诉`go build`命令使用哪个版本。在下一节中,你将使用[构建标签](https://gocn.github.io/How-To-Code-in-Go/docs/31-Customizing_Go_Binaries_with_Build_Tags)来完成这个任务。 ## 使用`GOOS`或`GOARCH`构建标签 diff --git a/content/zh/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md b/content/zh/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md index 18d1841..9c6878c 100644 --- a/content/zh/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md +++ b/content/zh/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md @@ -2,7 +2,7 @@ ## 简介 -当把应用程序部署到生产环境中时,用版本信息和其他元数据构建二进制文件将改善你的监控、日志和调试过程,增加识别信息来帮助跟踪随着时间推移后,应用程序的构建信息。这种版本信息通常包括高度动态的数据,如构建时间、构建二进制文件的机器或用户、[版本控制系统(VCS)](https://www.atlassian.com/git/tutorials/what-is-version-control)的提交 ID,等其他更多信息。因为这些值是不断变化的,将这些数据直接编码到源代码中,并在每次新的构建之前进行修改,是很繁琐的,而且容易出错:源文件可能会移动,[变量/常量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})在整个开发过程中可能会随着切换文件而改动,打断构建过程。 +当把应用程序部署到生产环境中时,用版本信息和其他元数据构建二进制文件将改善你的监控、日志和调试过程,增加识别信息来帮助跟踪随着时间推移后,应用程序的构建信息。这种版本信息通常包括高度动态的数据,如构建时间、构建二进制文件的机器或用户、[版本控制系统(VCS)](https://www.atlassian.com/git/tutorials/what-is-version-control)的提交 ID,等其他更多信息。因为这些值是不断变化的,将这些数据直接编码到源代码中,并在每次新的构建之前进行修改,是很繁琐的,而且容易出错:源文件可能会移动,[变量/常量](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go)在整个开发过程中可能会随着切换文件而改动,打断构建过程。 在 Go 中解决这个问题的一个方法是在使用`go build`命令时加上`-ldflags`,在构建时将动态信息插入二进制文件中,而不需要修改源代码。在这个标志中,`ld`代表[*linker*](https://en.wikipedia.org/wiki/Linker_(computing)),这个程序将编译后的源代码的不同部分连接成最终的二进制文件。`ldflags`就代表*linker 的标志*。之所以这样说,是因为它向底层的 Go 工具链 linker[`cmd/link`](https://golang.org/cmd/link)传递了一个标志,允许你在构建时从命令行中改变导入的包的值。 @@ -12,7 +12,7 @@ 为了接下去在文章中的例子,你需要: -- 按照[如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置 Go 的 workspace。 +- 按照[如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean)设置 Go 的 workspace。 ## 构建你的范例应用程序 @@ -52,7 +52,7 @@ func main() { } ``` -在`main()`函数内,你宣告了`Version`变量,然后打印[string]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})类型的`Version`:紧跟着 tab 的字符,`\t`,然后是声明的变量。 +在`main()`函数内,你宣告了`Version`变量,然后打印[string](https://gocn.github.io/How-To-Code-in-Go/docs/08-An_Introduction_to_Working_with_Strings_in_Go)类型的`Version`:紧跟着 tab 的字符,`\t`,然后是声明的变量。 现在,参数`Version`被定义为`development`,将作为 app 的默认版本。稍后,你将会修改这个值来符合官方版本编号,根据[semantic versioning format](https://semver.org/)来定义。 @@ -80,7 +80,7 @@ Version: development go build -ldflags="-flag" ``` -在这个例子中,我们向作为`go build`的一部分运行的`go tool link`命令传递了`flag`。这个命令在传递给`ldflags`的内容周围使用双引号,以避免其中字符串被分开,或者被命令行翻译为与我们想要的不同的字符。从这里,你可以传入[许多不同的`linker`标志](https://golang.org/cmd/link/)。为了本教程中的目的,我们将使用`-X`标志在链接时将信息写入变量,跟着的是参数的[package]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})路径和它的新值: +在这个例子中,我们向作为`go build`的一部分运行的`go tool link`命令传递了`flag`。这个命令在传递给`ldflags`的内容周围使用双引号,以避免其中字符串被分开,或者被命令行翻译为与我们想要的不同的字符。从这里,你可以传入[许多不同的`linker`标志](https://golang.org/cmd/link/)。为了本教程中的目的,我们将使用`-X`标志在链接时将信息写入变量,跟着的是参数的[package](https://gocn.github.io/How-To-Code-in-Go/docs/20-Importing_Packages_in_Go_DigitalOcean)路径和它的新值: ```bash go build -ldflags="-X 'package_path.variable_name=new_value'" diff --git a/content/zh/docs/40-How_To_Use_the_Flag_Package_in_Go.md b/content/zh/docs/40-How_To_Use_the_Flag_Package_in_Go.md index c03088b..e23853f 100644 --- a/content/zh/docs/40-How_To_Use_the_Flag_Package_in_Go.md +++ b/content/zh/docs/40-How_To_Use_the_Flag_Package_in_Go.md @@ -10,7 +10,7 @@ 使用 flag 包包括三个步骤:首先,定义变量以捕获标志值,然后定义你的 Go 应用程序将使用的标志,最后解析执行时提供给应用程序的标志。`flag`包内的大多数函数都与定义标志和将它们与你定义的变量绑定有关。解析阶段由`Parse()`函数处理。 -为了阐述这一点,你将创建一个程序,定义一个 [Boolean]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})标志,改变这个标志将会把信息打印到标准输出上。如果提供一个`-color`标志,程序会用蓝色来打印消息。如果没有这个标志,则打印消息不会有颜色。 +为了阐述这一点,你将创建一个程序,定义一个 [Boolean](https://gocn.github.io/How-To-Code-in-Go/docs/14-Understanding_Boolean_Logic_in_Go)标志,改变这个标志将会把信息打印到标准输出上。如果提供一个`-color`标志,程序会用蓝色来打印消息。如果没有这个标志,则打印消息不会有颜色。 创建一个叫`boolean.go`的文件: @@ -59,7 +59,7 @@ func main() { 在`main`中,我们使用`flag.Bool`函数来定义一个名为`color`的 Boolean 标志。这个函数的第二个参数,`false`,在没有提供这个标志的情况下,设置这个标志的默认值。与你可能有的期望相反,将其设置为`true`并不会颠倒行为,如提供一个标志会导致它变成 false。因此,这个参数的值在布尔标志下几乎总是`false`。 -最后一个参数是一个可以作为使用信息打印出来的文档 string。从这个函数返回的值是一个指向`bool`的指针。下一行的`flag.Parse`函数使用这个指针,然后根据用户传入的标志,设置`bool`变量。 然后我们就可以通过取消引用这个指针来检查这个`bool`指针的值。更多关于指针变量的信息可以在[指针教程]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})找到。使用这个 Boolean,我们就可以在设置`-color`标志时调用`colorize`,而在没有这个标志时调用`fmt.Println`变量。 +最后一个参数是一个可以作为使用信息打印出来的文档 string。从这个函数返回的值是一个指向`bool`的指针。下一行的`flag.Parse`函数使用这个指针,然后根据用户传入的标志,设置`bool`变量。 然后我们就可以通过取消引用这个指针来检查这个`bool`指针的值。更多关于指针变量的信息可以在[指针教程](https://gocn.github.io/How-To-Code-in-Go/docs/32-Understanding_Pointers_in_Go)找到。使用这个 Boolean,我们就可以在设置`-color`标志时调用`colorize`,而在没有这个标志时调用`fmt.Println`变量。 保存文件,并在未传入没有任何标志的情况下运行该程序: @@ -139,11 +139,11 @@ func main() { } ``` -首先,我们定义了一个`count`变量,用来保存程序应该从文件中读取的行数。然后,我们使用`flag.IntVar`定义`-n`标志,模拟原始`head`程序的行为。 这个函数允许我们将自己的[pointer]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})传递给一个变量,与没有`Var`后缀的标志函数相反。除了这个区别之外,`flag.IntVar`的其他参数与`flag.Int`对应的参数相同:标志名称、默认值和描述。 和前面的例子一样,我们随后调用`flag.Parse()`来处理用户的输入。 +首先,我们定义了一个`count`变量,用来保存程序应该从文件中读取的行数。然后,我们使用`flag.IntVar`定义`-n`标志,模拟原始`head`程序的行为。 这个函数允许我们将自己的[pointer](https://gocn.github.io/How-To-Code-in-Go/docs/32-Understanding_Pointers_in_Go)传递给一个变量,与没有`Var`后缀的标志函数相反。除了这个区别之外,`flag.IntVar`的其他参数与`flag.Int`对应的参数相同:标志名称、默认值和描述。 和前面的例子一样,我们随后调用`flag.Parse()`来处理用户的输入。 下一节读取文件。我们首先定义一个`io.Reader`变量,该变量将被设置为用户请求的文件,或传递给程序的标准输入。在`if`语句中,我们使用`flag.Arg`函数来访问所有标志之后的第一个位置参数。如果用户提供了文件名,这个位置参数会被设置。否则,它将为空 string(`""`)。当文件名提供时,我们使用`os.Open`函数来打开该文件,并将我们之前定义的`io.Reader`设置为该文件。否则,我们使用`os.stdin`来读取标准输入。 -最后一节使用一个用`bufio.NewScanner`创建的`*bufio.Scanner`从`io.Reader`变量`in`中读取行数据。我们使用[`for`loop]({{< relref "/docs/25-How_To_Construct_For_Loops_in_Go.md" >}})遍历到 count 的值,如果用`buf.Scan`扫描该行结果为`false`,则调用`break`,表示行数少于用户要求的数量。 +最后一节使用一个用`bufio.NewScanner`创建的`*bufio.Scanner`从`io.Reader`变量`in`中读取行数据。我们使用[`for`loop](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go)遍历到 count 的值,如果用`buf.Scan`扫描该行结果为`false`,则调用`break`,表示行数少于用户要求的数量。 运行这个程序,用`head.go`作为文件参数,显示你刚才写的文件的内容: @@ -274,9 +274,9 @@ func main() { } ``` -这个程序分为几个部分:`main`函数,`root`函数,以及实现子命令的各个函数。`main`函数处理从命令返回的错误。如果任何函数返回[错误]({{< relref "/docs/17-Handling_Errors_in_Go_DigitalOcean.md" >}}),`if`语句将捕捉到它,打印出错误,程序将以`1`的状态码退出,向操作系统的其他部分表明发生了错误。在`main`中,我们将程序被调用的所有参数传递给`root`。我们通过先将`os.Args`切片来删除第一个参数,也就是程序的名称(在前面的例子中是`./subcommand`)。 +这个程序分为几个部分:`main`函数,`root`函数,以及实现子命令的各个函数。`main`函数处理从命令返回的错误。如果任何函数返回[错误](https://gocn.github.io/How-To-Code-in-Go/docs/17-Handling_Errors_in_Go_DigitalOcean),`if`语句将捕捉到它,打印出错误,程序将以`1`的状态码退出,向操作系统的其他部分表明发生了错误。在`main`中,我们将程序被调用的所有参数传递给`root`。我们通过先将`os.Args`切片来删除第一个参数,也就是程序的名称(在前面的例子中是`./subcommand`)。 -`root`函数定义了`[]Runner`,所有的子命令都会在这里定义。`Runner`是一个子命令的 [interface]({{< relref "/docs/37-How_To_Use_Interfaces_in_Go.md" >}}) ,允许`root`使用`Name()`获取子命令的名称,并将其与变量`subcommand`内容进行比较。一旦在遍历`cmds`变量后找到了正确的子命令,我们就用其余的参数初始化子命令,并调用该命令的`Run()`方法。 +`root`函数定义了`[]Runner`,所有的子命令都会在这里定义。`Runner`是一个子命令的 [interface](https://gocn.github.io/How-To-Code-in-Go/docs/37-How_To_Use_Interfaces_in_Go) ,允许`root`使用`Name()`获取子命令的名称,并将其与变量`subcommand`内容进行比较。一旦在遍历`cmds`变量后找到了正确的子命令,我们就用其余的参数初始化子命令,并调用该命令的`Run()`方法。 我们只定义了一个子命令,尽管这个框架很容易让我们创建其他子命令。`GreetCommand`是使用`NewGreetCommand`实例化的,在这里我们使用`flag.NewFlagSet`创建一个新的`*flag.FlagSet`。`flag.NewFlagSet`需要两个参数:一个标志集的名称,和一个报告解析错误的策略。用`flag.(*FlagSet).Name`方法获取`*flag.FlagSet`的名称。我们在`(*GreetCommand).Name()`方法中使用这个方法,所以子命令的名字与我们给`*flag.FlagSet`的名字一致。 `NewGreetCommand`也用了类似于以前的例子的方式定义了一个`-name`标志,但它改为从`*GreetCommand`的`*flag.FlagSet`字段中调用这个方法,`gc.fs`。当`root`调用`*GreetCommand`的`Init()`方法时,我们将传入的参数传递给`*flag.FlagSet`字段的`Parse`方法。 diff --git a/content/zh/docs/44-How_To_Run_Multiple_Functions_Concurrently_in_Go.md b/content/zh/docs/44-How_To_Run_Multiple_Functions_Concurrently_in_Go.md index fab895d..e20335b 100644 --- a/content/zh/docs/44-How_To_Run_Multiple_Functions_Concurrently_in_Go.md +++ b/content/zh/docs/44-How_To_Run_Multiple_Functions_Concurrently_in_Go.md @@ -1,61 +1,65 @@ -# How To Run Multiple Functions Concurrently in Go +# 如何在 Go 中并发运行多个函数 -## Introduction +## 介绍 -One of the popular features of the Go language is its first-class support for [_concurrency_](https://en.wikipedia.org/wiki/Concurrency_(computer_science)), or the ability of a program to do multiple things at once. Being able to run code concurrently is becoming a larger part of programming as computers move from running a single stream of code faster to running more streams of code simultaneously. To run programs faster, a programmer needs to design their programs to run concurrently, so that each concurrent part of the program can be run independently of the others. Two features in Go, [goroutines](https://golangdocs.com/goroutines-in-golang) and [channels](https://golangdocs.com/channels-in-golang), make concurrency easier when used together. Goroutines solve the difficulty of setting up and running concurrent code in a program, and channels solve the difficulty of safely communicating between the code running concurrently. +Go 受欢迎的特性之一是它对 [_并发_](https://en.wikipedia.org/wiki/Concurrency_(computer_science)) 的一流且原生的支持,或者说能让程序同时做多件事情。随着计算机从快速地运行单道程序,逐渐转变到现在的倾向于同时运行多个程序,能够并发地运行代码在现代编程中的重要程度越来越高。为了更快地运行程序,程序员通常需要将程序设计成并发运行的,因而程序的每个可并发的部分都能独立地运行。同时使用 Go 的两个特性 [_协程(goroutines)_](https://golangdocs.com/goroutines-in-golang) 和 [_通道(channels)_](https://golangdocs.com/channels-in-golang), 将会让并发变得更容易。协程解决了创建与运行并发代码的难度,而通道解决了并发运行的协程之间的安全通信问题。 -In this tutorial, you will explore both goroutines and channels. First, you will create a program that uses goroutines to run multiple functions at once. Then you will add channels to that program to communicate between the running goroutines. Finally, you’ll add more goroutines to the program to simulate a program running with multiple worker goroutines. +在这个教程中,你将探索协程与通道的知识。首先,你将创建一个使用协程来同时运行多个函数的程序。然后,你将在程序中加入通道,以实现运行着的协程之间的通信。最后你将在程序中加入更多的协程,以模拟一个运行着多个工作协程的程序。 -## Prerequisites +> 注: 下文的 goroutines 指 Go 的协程这一特性,goroutine 指创建的某个协程(并发运行的函数)本身。(若都翻译成协程,将会混淆二者,为方便读者理解,故不译)。 -To follow this tutorial, you will need: +## 前置条件 -- Go version 1.16 or greater installed. To set this up, follow the [How To Install Go](https://www.digitalocean.com/community/tutorials/how-to-install-go-on-ubuntu-20-04) tutorial for your operating system. -- Familiarity with Go functions, which you can find in the [How to Define and Call Functions in Go](https://www.digitalocean.com/community/tutorials/how-to-define-and-call-functions-in-go) tutorial. +为了更顺畅地阅读本教程,你需要: -## Running Functions at the Same Time with Goroutines +* Go 版本 >= 1.16。你可以按照 [如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean) 来安装 Go。 +* 熟悉 Go 的 [函数](https://gocn.github.io/How-To-Code-in-Go/docs/27-How_To_Define_and_Call_Functions_in_Go) 相关知识。 -In a modern computer, the processor, or [CPU](https://en.wikipedia.org/wiki/Central_processing_unit), is designed to run as many streams of code as possible at the same time. These processors have one or more “cores,” each capable of running one stream of code at the same time. So, the more cores a program can use simultaneously, the faster the program will run. However, in order for programs to take advantage of the speed increase that [multiple cores](https://en.wikipedia.org/wiki/Multi-core_processor) provide, programs need to be able to be split into multiple streams of code. Splitting a program into parts can be one of the more challenging things to do in programming, but Go was designed to make this easier. +## 使用协程同时运行函数 -One way Go does this is with a feature called _goroutines_. A goroutine is a special type of function that can run while other goroutines are also running. When a program is designed to run multiple streams of code at once, the program is designed to run [_concurrently_](https://en.wikipedia.org/wiki/Concurrency_(computer_science)). Typically, when a function is called, it will finish running completely before the code after it continues to run. This is known as running in the “foreground” because it prevents your program from doing anything else before it finishes. With a goroutine, the function call will continue running the next code right away while the goroutine runs in the “background”. Code is considered running in the background when it doesn’t prevent other code from running before it finishes. +在现代计算机中,处理器或 [CPU](https://en.wikipedia.org/wiki/Central_processing_unit) 被设计成在同一时间运行尽可能多的代码流。这些处理器拥有一个或更多的“核”,每个核都能同时运行一个代码流。因此,一个程序可以同时使用的内核越多,程序的运行速度就越快。然而,为了使程序能够利用 [多核](https://en.wikipedia.org/wiki/Multi-core_processor) 提供的速度提升,程序需要能够被分割成多个代码流。将一个程序分割成若干部分可能是编程中具有挑战性的事情之一,但 Go 的设计使之更容易。 -The power goroutines provide is that each goroutine can run on a processor core at the same time. If your computer has four processor cores and your program has four goroutines, all four goroutines can run simultaneously. When multiple streams of code are running at the same time on different cores like this, it’s called running in [_parallel_](https://en.wikipedia.org/wiki/Parallel_computing). +Go 实现这一功能的其中一个方式是使用 *goroutines*。goroutine 是一种特殊类型的函数,它可以在其他 goroutine 运行时同时运行。当一个程序被设计成同时运行多个代码流时,这个程序即被设计成 [_并发_](https://en.wikipedia.org/wiki/Concurrency_(computer_science)) 运行。通常,当一个函数被调用时,调用方接下来的代码,只有当被调用的函数执行完毕后才会开始执行。这被称为“前台”运行,因为它可以防止你的代码在它结束之前做任何事情。对于一个协程,调用它的函数将继续执行接下来的代码,而 goroutine 本身则会在“后台”运行。当代码在完成之前不妨碍其他代码的运行时,它就被认为是在后台运行。 -To visualize the difference between concurrency and parallelism, consider the following diagram. When a processor runs a function, it doesn’t always run it from beginning to completion all at once. Sometimes the operating system will interleave other functions, goroutines, or other programs on a CPU core when a function is waiting for something else to happen, such as reading a file. The diagram shows how a program designed for concurrency can run on a single core as well as multiple cores. It also shows how more segments of a goroutine can fit into the same timeframe (9 vertical segments, as seen in the diagram) when running in parallel than when running on a single core. +goroutines 提供的能力是,每个 goroutine 可以同时在一个处理器核心上运行。如果你的计算机有四个处理器核心,且你的程序有四个 goroutine,那么所有四个 goroutine 都可以同时运行。当多个代码流像这样在不同的内核上同时运行时,就叫做以 [*并行*](https://en.wikipedia.org/wiki/Parallel_computing) 方式运行。 + +为了直观地了解并发和并行之间的区别,请看下面的图表。当一个处理器运行一个函数时,它并不总是一次性地从开始运行到完成。有时,当一个函数在等待其他事情发生时,如读取文件,操作系统会在 CPU 核心上交错地运行其他函数、goroutine 或其他程序。该图展示了为并发而设计的程序如何在单核以及多核上运行。它还展示了在相同的时间区间内,一个 goroutine 在并行运行时比在单核上运行时,可以容纳更多的代码段。(如图所示,9个垂直的代码段)。 + +> 译者注:左侧为并发,右侧为并行。下图由上至下为时间轴。如左图,并发即为交错地快速地在多个 goroutine 间切换,因为切换足够快,在用户看来像同时运行了紫红绿三种 goroutine,但其实同一时间只有一个 goroutine 在运行;右图,并行为同一时间真的在运行多个 goroutine,横向地观察右图,可以看到每个时间窗口(每一行)都有两个 goroutine 同时在运行。 ![Diagram split into two columns, labeled Concurrency and Parallelism. The Concurrency column has a single tall rectangle, labeled CPU core, divided into stacked sections of varying colors signifying different functions. The Parallelism column has two similar tall rectangles, both labeled CPU core, with each stacked section signifying different functions, except it only shows goroutine1 running on the left core and goroutine2 running on the right core.](https://assets.digitalocean.com/articles/68067/diagram2.png) -The left column in the diagram, labeled “Concurrency”, shows how a program designed around concurrency could run on a single CPU core by running part of `goroutine1`, then another function, goroutine, or program, then `goroutine2`, then `goroutine1` again, and so on. To a user, this would seem like the program is running all the functions or goroutines at the same time, even though they’re actually being run in small parts one after the other. +图中左栏标有 "并发",显示了围绕并发设计的程序如何能够在单个 CPU 核上运行,先运行部分 "goroutine1",然后是另一个函数、goroutine 或程序,接着是 "goroutine2",再是 "goroutine1",如此循环。对用户来说,这似乎是程序在同时运行所有的函数或 goroutine,尽管它们实际上是在一个接一个的小部分中运行。 -The column on the right of the diagram, labeled “Parallelism”, shows how that same program could run in parallel on a processor with two CPU cores. The first CPU core shows `goroutine1` running interspersed with other functions, goroutines, or programs, while the second CPU core shows `goroutine2` running with other functions or goroutines on that core. Sometimes both `goroutine1` and `goroutine2` are running at the same time as each other, just on different CPU cores. +图中右边一栏标有 "并行",显示了同一个程序如何在一个有两个CPU核的处理器上并行运行。第一个CPU核显示 `goroutine1` 与其他函数、goroutine 或程序穿插运行,而第二个 CPU 核显示 `goroutine2` 与该核的其他函数或 goroutine 运行。有时 `goroutine1` 和 `goroutine2` 同时运行,只是在不同的 CPU 核上。 -This diagram also shows another of Go’s powerful traits, [_scalability_](https://en.wikipedia.org/wiki/Scalability). A program is scalable when it can run on anything from a small computer with a few processor cores to a large server with dozens of cores, and take advantage of those additional resources. The diagram shows that by using goroutines, your concurrent program is capable of running on a single CPU core, but as more CPU cores are added, more goroutines can be run in parallel to speed up the program. +这张图还展示了 Go 的另一个强大特性,[_高可拓展性_](https://en.wikipedia.org/wiki/Scalability)。当一个程序可以在从有几个处理器核心的小型计算机,切换到在有几十个核心的大型服务器上运行,并利用这些额外的资源时,它就是可扩展的。图中显示,通过使用 goroutines,你的并发程序能够在单个 CPU 核上运行,但随着更多 CPU 核的加入,更多的 goroutine 可以并行运行,以加快程序运行速度。 -To get started with your new concurrent program, create a `multifunc` directory in the location of your choosing. You may already have a directory for your projects, but in this tutorial, you’ll create a directory called `projects`. You can create the `projects` directory either through an IDE or via the command line. +为了开始写你的新的并发程序,请选择一个位置,创建一个 `multifunc` 目录。你可能已经有了一个项目目录,但在本教程中,你将创建一个名为 `projects` 的目录。你可以通过 IDE 或命令行来创建此目录。 -If you’re using the command line, begin by making the `projects` directory and navigating to it: +如果你正在使用命令行工具,可以创建 `projects` 目录并切换至此目录,从此开始你的并发编程之旅: ```shell mkdir projects cd projects ``` -From the `projects` directory, use the `mkdir` command to create the program’s directory (`multifunc`) and then navigate into it: +在 `projects` 目录下, 使用 `mkdir` 命令来创建项目文件夹 `multifunc` 并切换至此目录: ```shell mkdir multifunc cd multifunc ``` -Once you’re in the `multifunc` directory, open a file named `main.go` using `nano`, or your favorite editor: +当你已经进入 `multifunc` 目录时, 使用 `nano` 或者你喜欢的编辑器打开(创建) `main.go` : ```shell nano main.go ``` -Paste or type the following code in the `main.go` file to get started. +粘贴或手动输入以下代码至 `main.go` : -projects/multifunc/main.go +> projects/multifunc/main.go ```go package main @@ -82,15 +86,15 @@ func main() { } ``` -This initial program defines two functions, `generateNumbers` and `printNumbers`, then runs those functions in the `main` function. The `generateNumbers` function takes the amount of numbers to “generate” as a parameter, in this case one through three, and then prints each of those numbers to the screen. The `printNumbers` function doesn’t take any parameters yet, but it will also print out the numbers one through three. +这个初始的程序定义了两个函数,`generateNumbers` 和 `printNumbers`,然后在 `main` 函数中运行这两个函数。`generateNumbers` 函数把要“生成”的数字的个数作为一个参数,然后把每个数字打印到屏幕上(本例遍历了1-3)。`printNumbers` 函数没有任何参数,但它也会打印出1到3的数字。 -Once you’ve saved the `main.go` file, run it using `go run` to see the output: +一旦你保存了 `main.go` 文件,就可以用 `go run` 来运行它: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` Output @@ -102,21 +106,23 @@ Generating number 2 Generating number 3 ``` -You’ll see the functions run one after the other, with `printNumbers` running first and `generateNumbers` running second. +你会看到函数一个接一个地运行,先运行 `printNumbers` ,再运行 `generateNumbers`。 + +现在,想象一下,`printNumbers` 和 `generateNumbers` 分别需要3秒钟的时间来运行。_同步_ 运行时,或者像上面的例子那样一个接一个地运行,你的程序将运行6秒。首先,`printNumbers` 运行3秒,然后 `generateNumbers` 运行3秒。然而,在你的程序中,这两个函数是相互独立的,因为它们不依赖另一个函数的数据来运行。你可以利用它们的互不干扰的特性,使用 goroutines 并发运行这些函数,以加速这个假想的程序。理论上,当两个函数并发运行时,运行时间将减半。如果 `printNumbers` 和 `generateNumbers` 这两个函数都需要3秒的时间,且同时启动,那么程序可以在3秒内运行完毕。(实际速度可能会因为外部因素而变化,如计算机有多少个内核或有多少其他程序同时在计算机上运行)。 -Now, imagine that `printNumbers` and `generateNumbers` each takes three seconds to run. When running _synchronously_, or one after the other like the last example, your program would take six seconds to run. First, `printNumbers` would run for three seconds, and then `generateNumbers` would run for three seconds. In your program, however, these two functions are independent of the other because they don’t rely on data from the other to run. You can take advantage of this to speed up this hypothetical program by running the functions concurrently using goroutines. When both functions are running concurrently the program could, in theory, run in half the time. If both the `printNumbers` and the `generateNumbers` functions take three seconds to run and both start at exactly the same time, the program could finish in three seconds. (The actual speed could vary due to outside factors, though, such as how many cores the computer has or how many other programs are running on the computer at the same time.) +> 译者注:_同步_ 为计算机术语,与 _异步_ 相对,指为了保持多程序间的数据一致性而采取的机制。这里的语境,可以简单理解为:前一个函数执行完毕后才能执行下一个函数,即顺序地无交叠地执行。 -Running a function concurrently as a goroutine is similar to running a function synchronously. To run a function as a goroutine (as opposed to a standard synchronous function), you only need to add the `go` keyword before the function call. +作为 goroutine 并发地运行一个函数与同步地运行一个函数相似。要想让函数以 goroutine 方式运行(相对于标准的同步函数),你只需要在函数调用前添加 `go` 关键字。 -However, for the program to run the goroutines concurrently, you’ll need to make one additional change. You’ll need to add a way for your program to wait until both goroutines have finished running. If you don’t wait for your goroutines to finish and your `main` function completes, the goroutines could potentially never run, or only part of them could run and not complete running. +然而,为了使程序并发地运行多个 goroutine, 你需要做一些修改。你需要为你的程序添加一种方法,让它等待两个 goroutine 都运行完毕。如果你不等待所有 goroutine 运行完毕,那么当你的 `main` 函数运行完毕的时候,这两个 goroutine 可能永远都不会运行,或者只运行了一部分而没有运行完毕。 -To wait for the functions to finish, you’ll use a [`WaitGroup`](https://pkg.go.dev/sync#WaitGroup) from Go’s [`sync`](https://pkg.go.dev/sync) package. The `sync` package contains “synchronization primitives”, such as `WaitGroup`, that are designed to synchronize various parts of a program. In your case, the synchronization keeps track of when both functions have finished running so you can exit the program. +为了等待函数运行完毕,你可以使用 Go 的 [`sync`](https://pkg.go.dev/sync) 包中的 [`WaitGroup`](https://pkg.go.dev/sync#WaitGroup)。`sync` 包包含了 "同步原语",比如 `WaitGroup`,它们被设计用来同步程序的各个部分。在本例中,同步可以跟踪两个函数何时完成运行,以便你可以退出程序。 -The `WaitGroup` primitive works by counting how many things it needs to wait for using the `Add`, `Done`, and `Wait` functions. The `Add` function increases the count by the number provided to the function, and `Done` decreases the count by one. The `Wait` function can then be used to wait until the count reaches zero, meaning that `Done` has been called enough times to offset the calls to `Add`. Once the count reaches zero, the `Wait` function will return and the program will continue running. +`WaitGroup` 原语的工作方式是使用 `Add`、`Done` 和 `Wait` 函数计算它需要等待的"事情"的数量。`Add` 函数把传入的增量累加到该计数值,`Done` 将计数减一。`Wait` 函数可以用来等待(阻塞)后续程序,直到计数变为零,这意味着 `Done` 被调用的次数足以抵消对 `Add` 的调用。一旦计数达到零,`Wait` 函数将 return,程序将继续运行。 -Next, update the code in your `main.go` file to run both of your functions as goroutines using the `go` keyword, and add a `sync.WaitGroup` to the program: +接下来,更新 `main.go` 文件中的代码,使用 `go` 关键字将两个函数作为 goroutine 运行,并在程序中添加一个 `sync.WaitGroup`。 -projects/multifunc/main.go +> projects/multifunc/main.go ```go package main @@ -155,17 +161,17 @@ func main() { } ``` -After declaring the `WaitGroup`, it will need to know how many things to wait for. Including a `wg.Add(2)` in the `main` function before starting the goroutines will tell `wg` to wait for two `Done` calls before considering the group finished. If this isn’t done before the goroutines are started, it’s possible things will happen out of order or the code may panic because the `wg` doesn’t know it should be waiting for any `Done` calls. +在声明了 `WaitGroup` 之后,它需要知道要等待多少"事情"。在启动 goroutine 之前,在 `main` 函数中加入`wg.Add(2)` ,告诉 `wg` 需要等待两个 `Done` 调用才能继续执行 `wg.Wait` 的后续代码。如果未在 goroutine 启动前执行 `wg.Add`,程序执行顺序会不符合预期,甚至代码可能会 panic,因为 `wg` 不知道它应该等待多少 `Done` 调用。 -Each function will then use `defer` to call `Done` to decrease the count by one after the function finishes running. The `main` function is also updated to include a call to `Wait` on the `WaitGroup`, so the `main` function will wait until both functions call `Done` to continue running and exit the program. +然后,每个函数会通过 `defer` 来调用 `Done`,在函数运行结束后将计数值减一。`main` 函数也被更新了,包括在 `WaitGroup` 类型变量上的对 `Wait` 方法的调用,所以 `main` 函数将等待(被阻塞),直到两个函数都调用 `Done` 后,再继续运行,并退出程序。 -After saving your `main.go` file, run it using `go run` like you did before: +在保存了 `main.go` 文件后,像之前那样用 `go run` 来运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` Output @@ -179,45 +185,45 @@ Printing number 3 Done! ``` -Your output may differ from what is printed here, and it’s even likely to change every time you run the program. With both functions running concurrently, the output depends on how much time Go and your operating system give for each function to run. Sometimes there is enough time to run each function completely and you’ll see both functions print their entire sequences uninterrupted. Other times, you’ll see the text interspersed like the output above. +你的输出可能和教程中的输出不同,甚至,当你每次运行程序的时候,输出很有可能都不一样。因为两个函数在被并发地执行,实际输出完全取决于 Go 和操作系统给了每个函数多少时间来运行。有的时候,这个时间足够每个函数运行完毕,所以你会看到两个函数完整打印了整个数字序列,且不被另外一个函数所打断。有时,你可能会看到类似上面的相互交织的输出。 -An experiment you can try is removing the `wg.Wait()` call in the `main` function and running the program a few times with `go run` again. Depending on your computer, you may see some output from the `generateNumbers` and `printNumbers` functions, but it’s also likely you won’t see any output from them at all. When you remove the call to `Wait`, the program will no longer wait for both functions to finish running before it continues. Since the `main` function ends soon after the `Wait` function, there’s a good chance that your program will reach the end of the `main` function and exit before the goroutines finish running. When this happens, you’ll see a few numbers printed out, but you won’t see all three from each function. +你可以尝试做一个实验,删除 `main` 函数中的 `wg.Wait()` 调用,然后用 `go run` 再运行几遍程序。取决于你的电脑,你可能会看到 `generateNumbers` 和 `printNumbers` 函数输出了一些东西,但也可能根本看不到任何输出。当你删除对 `Wait` 的调用时,程序将不再等待两个函数运行完毕后再继续运行。由于 `main` 函数将在 `Wait` 函数后很快运行完毕,你的程序很有可能在所有的 goroutine 完成运行之前就执行到 `main` 函数的末尾并退出。当这种情况发生时,你会看到一些数字被打印出来,但看不到每个函数打印完毕全部的三个数字。 -In this section, you created a program that uses the `go` keyword to run two goroutines concurrently and print a sequence of numbers. You also used a `sync.WaitGroup` to make your program wait for those goroutines to finish before exiting the program. +在本节中,你创建了一个程序,使用 `go` 关键字来同时运行两个 goroutine 并打印一串数字。你还使用了一个 `sync.WaitGroup` 来使程序在退出前等待这些 goroutine 完成。 -You may have noticed that the `generateNumbers` and `printNumbers` functions do not have return values. In Go, goroutines aren’t able to return values like a standard function would. You can still use the `go` keyword to call a function that returns values, but those return values will be thrown out and you won’t be able to access them. So, what do you do when you need data from one goroutine in another goroutine if you can’t return values? The solution is to use a Go feature called “channels”, which allow you to send data from one goroutine to another. +你可能已经注意到,`generateNumbers` 和 `printNumbers` 函数没有返回值。在 Go 中,goroutine 并不能像标准函数那样返回某些值。你仍然可以使用 `go` 关键字来调用一个具有返回值的函数,但是这些返回值会被丢弃,你无法获取到它们。那么,当你在一个 goroutine 中需要使用另一个 goroutine 的数据时,如果不能返回值,该怎么办呢?解决办法是使用 Go 的另一个特性,即 "通道",它允许你从一个 goroutine 向另一个 goroutine 发送数据。 -## Communicating Safely Between Goroutines with Channels +## 使用通道在协程间安全地通信 -One of the more difficult parts of concurrent programming is communicating safely between different parts of the program that are running simultaneously. If you’re not careful, you might run into problems that are only possible with concurrent programs. For example, a [_data race_](https://en.wikipedia.org/wiki/Race_condition) can happen when two parts of a program are running concurrently, and one part tries to update a variable while the other part is trying to read it at the same time. When this happens, the reading or writing can happen out of order, leading to one or both parts of the program using the wrong value. The name “data race” comes from both parts of the program “racing” each other to access the data. +并发编程的难题之一,是在同时运行的程序的不同部分之间安全地通信。如果你不小心,可能会遇到只有在并发程序中才会出现的问题。例如,当程序的两个部分并发运行时,一个部分试图更新一个变量(写),而另一个部分同时试图读取它(读),就会发生 [_数据竞争_](https://en.wikipedia.org/wiki/Race_condition)。当这种情况发生时,读或写操作可能会发生错乱,导致程序的一个或两个部分使用了错误的值。"数据竞争"这一名称来自于程序的两部分互相"竞争"地访问数据。 -While it’s still possible to run into concurrency issues like data races in Go, the language is designed to make it easier to avoid them. In addition to goroutines, channels are another feature that makes concurrency safer and easier to use. A channel can be thought of like a pipe between two or more different goroutines that data can be sent through. One goroutine puts data into one end of the pipe and another goroutine gets that same data out. The difficult part of making sure the data gets from one to the other safely is handled for you. +虽然在 Go 中仍有可能遇到像数据竞争这样的并发问题,但该语言的底层设计使其更容易避免这些问题。除了 goroutines 之外,通道(channel)是另一个使并发更安全和更容易使用的特性。通道可以被看作是两个或多个不同的 goroutine 之间的管道,数据可以通过它来发送。一个 goroutine 将数据放入管道的一端,另一个 goroutine 将同样的数据取出。确保数据安全地从一端到达另一端是困难的,但这部分 Go 已经替你解决了。 -Creating a channel in Go is similar to how you would create a [slice](https://www.digitalocean.com/community/tutorials/understanding-arrays-and-slices-in-go), using the built-in `make()` function. The type declaration for a channel uses the `chan` keyword followed by the [type of data](https://www.digitalocean.com/community/tutorials/understanding-data-types-in-go) you want to send on the channel. For example, to create a channel for sending `int` values, you would use the type `chan int`. If you wanted a channel for sending `[]byte` vaules, it would be `chan []byte`, like so: +在 Go 中创建一个通道类似于创建一个 [切片](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go),需要使用内置的 `make()` 函数。通道的类型声明由两部分组成: `chan` 关键字在前,后面是你想在通道上发送的 [数据类型](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go)。例如,要创建一个用于发送 `int`类型值的通道,你可以使用 `chan int` 类型。如果你想要一个发送 `[]byte` 类型数据的通道,就用 `chan []byte`,像这样: ```go bytesChan := make(chan []byte) ``` -Once a channel is created, you can send or receive data on the channel by using the arrow-looking `<-` operator. The position of the `<-` operator in relation to the channel variable determines whether you’re reading from or writing to the channel. +一旦一个通道被创建,你可以通过使用箭头状的 `<-` 操作符在通道上发送或接收数据。`<-` 操作符相对于通道变量的位置决定了你是在读取还是写入通道。 -To write to a channel, begin with the channel variable, followed by the `<-` operator, then the value you want to write to the channel: +要向通道中写入数据,通道变量在前,`<-` 操作符在中间,最后是要写入通道的值: ```go intChan := make(chan int) intChan <- 10 ``` -To read a value from a channel, begin with the variable you want to put the value into, either `=` or `:=` to assign a value to the variable, followed by the `<-` operator, and then the channel you want to read from: +要从通道中读取数据,存放读取内容的变量在前,接着是赋值操作符(`=` 或 `:=`)和 `<-`,最后是被读取的通道: ```go intChan := make(chan int) intVar := <- intChan ``` -To keep these two operations straight, it can be helpful to remember that the `<-` arrow always points to the left (as opposed to `->`), and the arrow points to where the value is going. In the case of writing to a channel, the arrow points the value to the channel. When reading from a channel, the arrow points the channel to the variable. +为了更直观、容易地弄清这两种操作,可以记住 `<-` 箭头总是向左(相对于`->`),并指向了数据的去向。向通道写入数据时,箭头指向通道;从通道中读取数据时,箭头指向保存数据的变量。 -Like a slice, a channel can also be read using the `range` keyword in a [`for` loop](https://www.digitalocean.com/community/tutorials/how-to-construct-for-loops-in-go). When a channel is read using the `range` keyword, each iteration of the loop will read the next value from the channel and put it into the loop variable. It will then continue reading from the channel until the channel is closed or the `for` loop is exited in other ways, such as a `break`: +像切片那样,也可以在 [`for`循环](https://gocn.github.io/How-To-Code-in-Go/docs/25-How_To_Construct_For_Loops_in_Go) 中使用 `range` 关键字来读取通道。当使用 `range` 关键字读取一个通道时,循环的每次迭代都从通道中读取下一个值,并将其赋值给循环变量。然后,它将继续从通道中读取数据,直到通道关闭或以其他方式退出 `for` 循环,如`break`: ```go intChan := make(chan int) @@ -229,7 +235,7 @@ for num := range intChan { } ``` -In some cases, you may want only to allow a function to read from or write to a channel, but not both. To do this, you would add the `<-` operator onto the `chan` type declaration. Similar to reading and writing from a channel, the channel type uses the `<-` arrow to allow variables to constrain a channel to only reading, only writing, or both reading and writing. For example, to define a read-only channel of `int` values, the type declaration would be `<-chan int`: +在某些情况下,你可能只想让一个函数从通道中只读或只写数据,而不是同时读取和写入。要做到这一点,你可以在 `chan` 类型声明中添加 `<-` 操作符。与从通道中读写类似,通道类型使用 `<-` 箭头,将通道变量限制在只读、只写或同时读写。例如,要定义一个只读的 `int` 值的通道,类型声明是 `<-chan int`。 ```go func readChannel(ch <-chan int) { @@ -237,7 +243,7 @@ func readChannel(ch <-chan int) { } ``` -If you wanted the channel to be write-only, you would declare it as `chan<- int`: +如果你想要只写通道,使用 `chan<- int` 声明: ```go func writeChannel(ch chan<- int) { @@ -245,13 +251,13 @@ func writeChannel(ch chan<- int) { } ``` -Notice that the arrow is pointing out of the channel for reading, and pointing into the channel for writing. If the declaration doesn’t have an arrow, as in the case of `chan int`, the channel can be used for both reading and writing. +注意,当箭头指向通道外时是只读,指向通道时为只写。如果类型声明中没有使用箭头,例如 `chan int`,那么这个通道可读写。 -Finally, once a channel is no longer being used it can be closed using the built-in `close()` function. This step is essential because when channels are created and then left unused many times in a program, it can lead to what’s known as a [_memory leak_](https://en.wikipedia.org/wiki/Memory_leak). A memory leak is when a program creates something that uses up memory on a computer, but doesn’t release that memory back to the computer once it’s done using it. This leads to the program slowly (or sometimes not so slowly) using up more memory over time, like a water leak. When a channel is created with `make()`, some of the computer’s memory is used up for the channel, then when `close()` is called on the channel, that memory is given back to the computer to be used for something else. +最后,一旦某个通道不再被使用,可以使用内置的 `close()` 函数来关闭它。这一步非常重要,因为当通道被创建后却未被使用,多次后,会导致 [_内存泄漏_](https://en.wikipedia.org/wiki/Memory_leak)。内存泄漏是指程序创建的东西占用了计算机的内存,但在使用完后却没有将内存释放回计算机。这导致程序随着时间的推移慢慢地(有时不那么慢)占用更多的内存,就像漏水一样。当用 `make()` 创建一个通道时,计算机的一些内存分配给该通道,然后当 `close()` 在该通道上被调用时,这些内存会被还给计算机,可再被用于其他用途。 -Now, update the `main.go` file in your program to use a `chan int` channel to communicate between your goroutines. The `generateNumbers` function will generate numbers and write them to the channel while the `printNumbers` function will read those numbers from the channel and print them to the screen. In the `main` function, you’ll create a new channel to pass as a parameter to each of the other functions, then use `close()` on the channel to close it because it will no longer be used. The `generateNumbers` function should also not be a goroutine any more because once that function is done running, the program will have finished generating all the numbers it needs to. This way, the `close()` function is only called on the channel before both functions have finished running. +现在,更新你程序中的 `main.go` 文件,使用 `chan int` 类型的通道在你的 goroutine 之间进行通信。`generateNumbers` 函数将生成数字并写入通道,而 `printNumbers` 函数将从通道中读取这些数字并打印到屏幕上。在 `main` 函数中,你将创建一个新的通道,并将其作为参数传递给另外两个函数。接着,调用 `close()` 来关闭通道,因为后面不再使用这个通道了。`generateNumbers` 函数也不应该再是一个 goroutine,因为一旦该函数运行完毕,程序将已经生成完了所有需要的数字。这样,`close()` 函数只会在两个函数都运行完成之前在通道上被调用。 -projects/multifunc/main.go +> projects/multifunc/main.go ```go package main @@ -295,21 +301,28 @@ func main() { } ``` -In the parameters for `generateNumbers` and `printNumbers`, you’ll see that the `chan` types are using the read-only and write-only types. Since `generateNumbers` only needs to be able to write numbers to the channel, it’s a write-only type with the `<-` arrow pointing into the channel. `printNumbers` only needs to be able to read numbers from the channel, so it’s a read-only type with the `<-` arrow pointing away from the channel. +在 `generateNumbers` 和 `printNumbers` 的参数中,你会看到 `chan` 类型使用的是只读和只写的类型。因为 `generateNumbers` 只需要能够将数字写入通道,所以它是只写类型,`<-` 箭头指向通道。`printNumbers` 只需要能够从通道中读取数字,所以它是只读类型,`<-` 箭头指向远离通道的方向。 -Even though these types could be a `chan int`, which would allow both reading and writing, it can be helpful to constrain them to only what the function needs to avoid accidentally causing your program to stop running from something known as a [_deadlock_](https://en.wikipedia.org/wiki/Deadlock). A deadlock can happen when one part of a program is waiting for another part of the program to do something, but that other part of the program is also waiting for the first part of the program to finish. Since both parts of the program are waiting on each other, the program will never continue running, almost like when two gears seize. +尽管这两个地方的类型可以是 `chan int`,即允许读和写,但将它们限制在函数需要的范围内是有用的,以避免意外地导致你的程序因所谓的 [_死锁_](https://en.wikipedia.org/wiki/Deadlock) 而停止运行。当程序的 A 部分在等待 B 部分做某事,但 B 部分也在等待程序的 A 部分完成时,就会发生死锁。由于程序的两部分都在互相等待,程序将永远不会继续运行,几乎就像两个齿轮卡住一样。 -The deadlock can happen due to the way channel communication works in Go. When part of a program is writing to a channel, it will wait until another part of the program reads from that channel before continuing on. Similarly, if a program is reading from a channel it will wait until another part of the program writes to that channel before it continues. A part of a program waiting on something else to happen is said to be [_blocking_](https://en.wikipedia.org/wiki/Blocking_(computing)) because it’s blocked from continuing until something else happens. Channels block when they are being written to or read from. So if you have a function where you’re expecting to write to a channel but accidentally read from the channel instead, your program may enter a deadlock because the channel will never be written to. Ensuring this never happens is one reason to use a `chan<- int` or a `<-chan int` instead of just a `chan int`. +死锁的发生由 Go 中通道通信的工作方式导致。当一个程序的一部分正在向一个通道写入数据时,它将等待(被阻塞),直到程序的另一部分从该通道中读出该数据,然后继续运行。同样地,如果一个程序正在从一个通道中读出数据,它将等待程序另一部分将数据写到该通道后再继续。程序中等待其他事情发生的部分被称为[_阻塞_](https://en.wikipedia.org/wiki/Blocking_(computing)),因为它在其他事情发生之前被阻止继续运行。当通道被写入或读出时,它们就会被阻塞。因此,如果你有一个函数,期望写入一个通道,但不小心从该通道读出,你的程序可能会进入死锁,因为该通道将永远不会被写入。使用 `chan<- int` 或 `<-chan int` 而不是仅仅使用 `chan int` 的原因之一,是为了确保这种情况不会发生。 -One other important aspect of the updated code is using `close()` to close the channel once it’s done being written to by `generateNumbers`. In this program, `close()` causes the `for ... range` loop in `printNumbers` to exit. Since using `range` to read from a channel continues until the channel it’s reading from is closed, if `close` isn’t called on `numberChan` then `printNumbers` will never finish. If `printNumbers` never finishes, the `WaitGroup`’s `Done` method will never be called by the `defer` when `printNumbers` exits. If the `Done` method is never called from `printNumbers`, the program itself will never exit because the `WaitGroup`’s `Wait` method in the `main` function will never continue. This is another example of a deadlock because the `main` function is waiting on something that will never happen. +更新后的代码的另一个重要的地方是,一旦使用 `generateNumbers` 向通道写入数据完毕后,使用 `close()` 来关闭通道。 -Now, run your updated code using the `go run` command on `main.go` again. +* 在这个程序中,`close()` 使得 `printNumbers` 中的 `for ... range` 循环终止。 +* 程序使用 `range` 从一个通道读取数据,直到它所读取的通道被关闭。所以,如果 `close` 没有在 `numberChan` 上被调用,那么 `printNumbers` 将永远不会结束。 +* 如果 `printNumbers` 无法执行完毕,`WaitGroup` 的 `Done` 方法就不会在 `printNumbers` 退出时被 `defer` 调用。 +* 如果 `printNumbers` 从未调用 `Done` 方法,程序本身将永远不会退出,因为 `main` 函数中 `WaitGroup` 的 `Wait` 方法将永远被阻塞,无法继续往下运行。 + +这是另一个死锁的例子,因为 `main` 函数在等待永远不会发生的事情。 + +现在,使用 `go run` 再次运行被更新的代码: ```shell go run main.go ``` -Your output may vary slightly from what’s shown below, but overall it should be similar: +你得到的输出可能与下面展示的略有不同,但总体上是相似的: ``` Output @@ -323,13 +336,13 @@ read 3 from channel Done! ``` -The output from the program shows that the `generateNumbers` function is generating the numbers one through three while writing them to the channel shared with `printNumbers`. Once `printNumbers` receives the number, it then prints it to the screen. After `generateNumbers` has generated all three numbers it will exit, allowing the `main` function to close the channel and wait until `printNumbers` is finished. Once `printNumbers` finishes printing out the last number, it calls `Done` on the `WaitGroup` and the program exits. Similar to previous outputs, the exact output you see will depend on various outside factors, such as when the operating system or Go runtime choose to run specific goroutines, but it should be relatively close. +程序的输出显示,`generateNumbers` 函数正在生成1到3的数字,同时将它们写入与 `printNumbers` 共享的通道。`printNumbers` 一收到数字,就会将其打印到屏幕上。在 `generateNumbers` 生成了所有的三个数字后,它将会退出,允许 `main` 函数关闭通道并等待 `printNumbers` 执行完毕。一旦 `printNumbers` 完成最后一个数字的打印,它就会在 `WaitGroup` 上调用 `Done`,随后程序退出。与之前的输出类似,你看到的实际输出取决于各种外部因素,比如操作系统或 Go 运行时选择运行的特定 goroutine,但应该比较接近。 -The benefit of designing your programs using goroutines and channels is that once you’ve designed your program to be split up, you can scale it up to more goroutines. Since `generateNumbers` is just writing to a channel, it doesn’t matter how many other things are reading from that channel. It will just send numbers to anything that reads the channel. You can take advantage of this by running more than one `printNumbers` goroutine, so each of them will read from the same channel and handle the data concurrently. +使用 goroutines 和通道来设计程序的好处是,一旦你把程序设计成可分割的,你就可以把它扩展到更多的 goroutine。因为 `generateNumbers` 只是把数据写入至一个通道上,不管有多少其他的东西在从这个通道上读取数据都没问题,它将只是向任何读取该通道的东西发送数据。你可以通过运行多个 `printNumbers` goroutine 来利用这个优势,这样每个 `printNumbers` 都会从同一个通道读取数据并且并发地处理数据。 -Now that your program is using channels to communicate, open the `main.go` file again and update your program so it starts multiple `printNumbers` goroutines. You will need to tweak the call to `wg.Add` so it adds one for every goroutine you start. You don’t need to worry about adding one to the `WaitGroup` for the call to `generateNumbers` any more because the program won’t continue without finishing the whole function, unlike when you were running it as a goroutine. To ensure it doesn’t reduce the `WaitGroup` count when it finishes, you should remove the `defer wg.Done()` line from the function. Next, adding the number of the goroutine to `printNumbers` makes it easier to see how the channel is read by each of them. Increasing the amount of numbers being generated is also a good idea so that it’s easier to see the numbers being spread out: +现在你的程序正在使用通道进行通信,再次打开 `main.go` 文件,更新程序,使其启动多个 `printNumbers` goroutine。你需要调整对`wg.Add` 的调用方式,以便在启动每个 goroutine 时都能自动增加一个计数值。你不需要在调用 `generateNumbers`时将 `WaitGroup` 的计数值加一,因为程序在没有完成整个数字生成函数之前都不会继续运行(之前给计数值加一,是因为不加一会导致后面的程序直接运行完毕),这与先前将 `generateNumbers` 作为一个 goroutine 运行时不同。为了确保它在完成时不会减少 `WaitGroup` 的计数值,你应该从该函数中删除 `defer wg.Done()` 这一行。接着,在 `printNumbers` 中加入 goroutine 的编号,以更容易地观察每个通道的读取情况。增加被生成的数字的个数也是一个好主意,这样更容易看到数字被分散至各个 goroutine 中。 -projects/multifunc/main.go +> projects/multifunc/main.go ```go ... @@ -368,13 +381,13 @@ func main() { } ``` -Once your `main.go` is updated, you can run your program again using `go run` with `main.go`. Your program should start three `printNumbers` goroutines before continuing on to generating numbers. Your program should also now generate five numbers instead of three to make it easier to see the numbers spread out among each of the three `printNumbers` goroutines: +当 `main.go` 被更新后,你可以再次使用 `go run` 运行它。在继续运行数字生成函数之前,你的程序会启动三个 `printNumbers` goroutine。你的程序现在应该生成5个数字,而不是3个,以便更容易看到分布在3个 `printNumbers` goroutine 中的数字。 ```shell go run main.go ``` -The ouput may look similar to this (although your output might vary quite a bit): +输出可能类似于这样(尽管你的输出结果可能大不相同): ``` Outputsending 1 to channel @@ -391,16 +404,22 @@ Waiting for goroutines to finish... Done! ``` -When you look at your program output this time, there’s a good chance it will vary greatly from the output you see above. Since there are three `printNumbers` goroutines running, there’s an element of chance determining which one receives a specific number. When one `printNumbers` goroutine receives a number, it spends a small amount of time printing that number to the screen, while another goroutine reads the next number from the channel and does the same thing. When a goroutine has finished its work of printing the number and is ready to read another number, it will go back and read the channel again to print the next one. If there are no more numbers to be read from the channel, it will start to block until the next number can be read. Once `generateNumbers` has finished and `close()` is called on the channel, all three of the `printNumbers` goroutines will finish their `range` loops and exit. When all three goroutines have exited and called `Done` on the `WaitGroup`, the `WaitGroup`’s count will reach zero and the program will exit. You can also experiment with increasing or decreasing the amount of goroutines or numbers being generated to see how that affects the output. +你这次的程序输出,很可能与上面的输出有很大差别。因为有三个 `printNumbers` goroutine 在运行,偶然的因素决定了哪一个 goroutine 收到哪个特定的数字。 + +* 当一个 `printNumbers` goroutine 收到一个数字时,它会花费少量的时间将该数字打印到屏幕上,而另一个 goroutine 从通道中读取下一个数字并做同样的事情。 +* 当一个 goroutine 完成了打印数字的工作并准备读取另一个数字时,它将再次读取通道以打印下一个数字。 +* 如果没有更多的数字要从通道中读取,它将被阻塞,直到可以读取下一个数字。 +* 一旦 `generateNumbers ` 执行完毕,并且 `close()` 被调用,所有三个 `printNumbers` goroutine 将完成其 `range` 循环并退出。 +* 当所有三个 goroutine 都退出并在 `WaitGroup` 上调用 `Done` 时,`WaitGroup` 的计数将达到零,程序将退出。 -When using goroutines, avoid starting too many. In theory, a program could have hundreds or even thousands of goroutines. However, depending on the computer the program is running on, it could actually be slower to have a higher number of goroutines. With a high number of goroutines, there’s a chance it could run into [_resource starvation_](https://en.wikipedia.org/wiki/Starvation_(computer_science)). Every time Go runs part of a goroutine, it requires a little bit of extra time to start running again, in addition to the time needed to run the code in the next function. Due to the extra time it takes, it’s possible for the computer to take longer to switch between running each goroutine than to actually run the goroutine itself. When this happens, it’s called resource starvation because the program and its goroutines aren’t getting the resources they need to run, or are getting very few. In these cases, it may be faster to lower the number of parts in the program running concurrently because it will lower the time it takes to switch between them, and give more time to running the program itself. Remembering how many cores the program is running on can be a good starting point for deciding how many goroutines you want to use. +你也可以尝试增加或减少生成的 goroutine 或数字的个数,看看这对输出有什么影响。 -Using a combination of goroutines and channels makes it possible to create very powerful programs capable of scaling from running on small desktop computers up to massive servers. As you saw in this section, channels can be used to communicate between as few as a couple of goroutines to potentially thousands of goroutines with minimal changes. If you take this into consideration when writing your programs, you’ll be able to take advantage of the concurrency available in Go to provide your users a better overall experience. +当使用 goroutines 时,要避免启动太多 goroutine。理论上,一个程序可以有数百甚至数千个 goroutine。然而,取决于运行程序的计算机,启动更多的 goroutine 实际运行速度可能会更慢。在大量的 goroutine 被启动的情况下,有可能遇到 [_资源饥饿_](https://en.wikipedia.org/wiki/Starvation_(computer_science))。每次 Go 运行 goroutine 的一部分时,除了运行下一个函数的代码所需的时间外,它还需要一些额外的时间来重新开始运行。由于需要额外的时间,计算机在运行每个 goroutine 之间切换的时间有可能比实际运行 goroutine 本身的时间还长。当这种情况发生时,它被称为资源饥饿,因为程序和它的 goroutine 们没有得到它们运行所需的资源,或者得到的资源非常少。在这种情况下,降低程序的并发数可能会更快,因为这将降低在 goroutine 之间切换的时间,而将更多时间用于运行程序本身。记住,你可以把运行程序的机器的 CPU 核心的数量,当作决定使用多少个 goroutine 的首要考量因素。 -## Conclusion +使用 goroutines 和通道的组合,使创建非常强大的程序成为可能,并快速地将运行于小型桌面计算机上的程序,扩展到大型服务器。正如你在这一节中所看到的,通道可以用来在少数几个 goroutine 和潜在的数千个 goroutine 之间进行通信,并且只需要做最小的改动。如果你在编写程序时考虑到了这一点,你就能利用 Go 的可靠的并发机制,为你的用户提供更好的体验。 -In this tutorial, you created a program using the `go` keyword to start concurrently-running goroutines that printed out numbers as they run. Once that program was running, you created a new channel of `int` values using `make(chan int)`, then used the channel to generate numbers in one goroutine and send them to another goroutine to be printed to the screen. Finally, you started multiple “printing” goroutines at the same time as an example of how channels and goroutines can be used to speed up your programs on multi-core computers. +## 结论 -If you’re interested in learning more about concurrency in Go, the [Effective Go](https://golang.org/doc/effective_go#concurrency) document created by the Go team goes into much more detail. The [Concurrency is not parallelism](https://go.dev/blog/waza-talk) Go blog post is also an interesting follow-up about the relationship between concurrency and parallelism, two terms that are sometimes mistakenly conflated to mean the same thing. +在本教程中,你创建了一个程序,使用 `go` 关键字来启动并发运行的 goroutine,在它们运行时打印出数字。程序一运行,你就用 `make(chan int)` 创建了一个新的 `int` 值通道,然后用这个通道在一个 goroutine 中生成数字,并将它们发送到另一个 goroutine 中以打印到屏幕上。最后,你同时启动了多个 "打印 " goroutine,并将其作为一个例子,来说明如何利用通道和 goroutine 在多核计算机上加速你的程序。 -This tutorial is also part of the [DigitalOcean](https://www.digitalocean.com/) [How to Code in Go](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go) series. The series covers a number of Go topics, from installing Go for the first time to how to use the language itself. +如果你有兴趣了解更多关于 Go 编程的知识,可以看看由 Go 团队创建的更为详细的 [Effective Go](https://golang.org/doc/effective_go#concurrency) 文档。[并发不是并行](https://go.dev/blog/waza-talk) 这一 Go 博客也是一篇有趣的后续文章,讲述了并发和并行之间的关系,这两个术语有时被错误地混为一谈。 diff --git a/content/zh/docs/45-How_to_Add_Extra_Information_to_Errors_in_Go.md b/content/zh/docs/45-How_to_Add_Extra_Information_to_Errors_in_Go.md index 275a973..0cc8c8b 100644 --- a/content/zh/docs/45-How_to_Add_Extra_Information_to_Errors_in_Go.md +++ b/content/zh/docs/45-How_to_Add_Extra_Information_to_Errors_in_Go.md @@ -1,69 +1,69 @@ -# How to Add Extra Information to Errors in Go +# 如何在 Go 中给错误添加额外信息 -## Introduction +## 介绍 -When a function in Go fails, the function will return a value using the `error` interface to allow the caller to handle that failure. In many cases, developers will use the [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) function in the [`fmt`](https://pkg.go.dev/fmt) package to return these values. Prior to Go 1.13, though, a downside of using this function is that you would lose information about any errors that may have caused the error to be returned. To solve this, developers would either use packages to provide a way to “wrap” errors inside other errors or create custom errors by implementing the `Error() string` method on one of their `struct` error types. Sometimes it can be tedious to create these `struct` types if you have a number of errors that don’t need to be handled explicitly by the callers, though, so in Go 1.13, the language added features to make it easier to handle these cases. +当 Go 的函数运行失败时,通常会使用 `error` 接口返回一个值,以使调用者能够处理该失败。在很多情况下,开发者会使用 [`fmt`](https://pkg.go.dev/fmt#Errorf) 包中的[`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) 函数来返回这些值。不过在 Go 1.13 之前,使用此函数的一个缺点是,你会丢失可能导致函数返回错误的其他错误的信息。为了解决这个问题,开发者要么使用某些第三方库来提供一种"包装"其他错误的方法,要么通过创建结构体类型并实现 `Error() string` 方法来创建自定义错误类型。如果有大量的错误不需要被调用者明确处理,那么在有些时候创建这些结构体类型会很无趣且繁琐。因此,Go 在1.13版中新增了一些特性,以更容易处理这些情况。 -One feature is the ability to wrap errors using the `fmt.Errorf` function with an `error` value that can be unwrapped later to access the wrapped errors. This builds the error-wrapping functionality into the Go standard library, so there’s no longer any need to use a third-party library. +其中一个特性是使用 `fmt.Errorf` 函数,并传入 `error` 值,来包装错误,后续可以解除包装来访问被包装的实际错误。这将错误包装的特性内置在了 Go 标准库中,无需使用第三方库。 -Additionally, the functions [`errors.Is`](https://pkg.go.dev/errors#Is) and [`errors.As`](https://pkg.go.dev/errors#As) make it easier to determine if a specific error is wrapped anywhere inside a given error, and will also give you access to that specific error directly without needing to unwrap all the errors yourself. +此外,函数 [`errors.Is`](https://pkg.go.dev/errors#Is) 和 [`errors.As`](https://pkg.go.dev/errors#As) 可以更容易地判断某个特定的错误是否被包装在给定的错误中,也让你能够直接访问那个特定的错误而不需要自己解开所有的错误。 -In this tutorial, you’ll create a program that uses these functions to include additional information in errors returned from your functions, and then create your own custom error `struct` that supports the wrapping and unwrapping functionality. +在本教程中,你将创建一个程序,用这些函数来让你函数返回的错误附带上额外信息。然后创建你自己的自定义错误结构体,支持包装和解包功能。 -## Prerequisites +## 先决条件 -To follow this tutorial, you will need: +为了更顺畅地阅读本教程,你需要: -- Go version 1.13 or greater installed. To set this up, follow the [How To Install Go](https://www.digitalocean.com/community/tutorials/how-to-install-go-on-ubuntu-20-04) tutorial for your operating system. -- (Optional) Reading [Handling Errors in Go](https://www.digitalocean.com/community/tutorials/handling-errors-in-go) may be helpful in this tutorial for a more in-depth explanation of error handling, but this tutorial will also cover some of the same topics at a higher level. -- (Optional) This tutorial expands upon the [Creating Custom Errors in Go](https://www.digitalocean.com/community/tutorials/creating-custom-errors-in-go) tutorial with features added to Go since the original tutorial. Reading the previous tutorial is helpful, but is not strictly required. +* Go 版本 >= 1.13。你可以按照 [如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean) 来安装 Go。 +* (可选)阅读 [Go 的错误处理](https://gocn.github.io/How-To-Code-in-Go/docs/17-Handling_Errors_in_Go_DigitalOcean) 有助于加深对本教程的理解,但本教程也在更高的层次上覆盖了此文章的部分内容。 +* (可选)本教程在 [在 Go 中创建自定义错误](https://gocn.github.io/How-To-Code-in-Go/docs/18-Creating_Custom_Errors_in_Go_DigitalOcean) 的基础上,拓展介绍了 Go 加入的新特性。阅读之前的教程有助于理解本教程,但不是必须的。 -## Returning and Handling Errors in Go +## 在用 Go 中返回和处理错误 -When an error occurs in a program, it’s good practice to handle those errors so your users never see them — but to handle the errors, you need to know about them first. In Go, you can handle errors in your program by returning information about the error from your functions using a special `interface` type, the `error` interface. Using the `error` interface allows any Go type to be returned as an `error` value as long as that type has an `Error() string` method defined. The Go standard library provides functionality to create `error`s for these return values, such as the [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) function. +当程序出现错误时,最佳实践方式是处理这些错误,这样你的用户就不会看到这些错误。但要处理这些错误,你首先需要了解错误本身。在 Go 中,你可以通过使用 `error` 接口从函数中返回相关错误信息,来处理程序中的错误。使用 `error` 接口可以使任何 Go 类型作为 `error` 值返回,只要该类型定义了 `Error() string` 方法。Go 标准库提供了为这些返回值创建 `error` 的功能,例如 [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) 函数。 -In this section, you’ll create a program with a function that uses `fmt.Errorf` to return an error, and you will also add an error handler to check for the errors that the function could return. (If you’d like more information on handling errors in Go, please see the tutorial, [Handling Errors in Go](https://www.digitalocean.com/community/tutorials/handling-errors-in-go).) +在本节中,你将创建一个程序,用 `fmt.Errorf` 来返回错误的函数 ,同时你也会添加一个错误处理器来检查这些错误是否能被该函数返回。(如果你想了解更多关于在 Go 中处理错误的信息,请看教程:[Go 的错误处理](https://gocn.github.io/How-To-Code-in-Go/docs/17-Handling_Errors_in_Go_DigitalOcean) 。) -Many developers have a directory to keep current projects. In this tutorial, you’ll use a directory named `projects`. +许多开发者可能已经有了存放项目的文件夹,但在本教程中,我们使用 `projects` 文件夹。 -To begin, make the `projects` directory and navigate to it: +创建 `projects` 目录并切换至此目录: ```shell mkdir projects cd projects ``` -From the `projects` directory, create a new `errtutorial` directory to keep the new program in: +在 `projects` 目录下, 创建新文件夹 `errtutorial` 以存放新程序: ```shell mkdir errtutorial ``` -Next, navigate into the new directory with the `cd` command: +接着,使用 `cd` 命令进入此文件夹: ```shell cd errtutorial ``` -Once you’re in the `errtutorial` directory, use the `go mod init` command to create a new module named `errtutorial`: +进入 `errtutorial` 目录后,请使用 `go mod init` 命令,创建一个名为 `errtutorial` 的新模块: ```shell go mod init errtutorial ``` -After creating the Go module, open a file named `main.go` in the `errtutorial` directory using `nano`, or your favorite editor: +然后,在 `errtutorial` 目录下用 `nano` 或你喜欢的编辑器打开一个名为 `main.go` 的文件: ```shell nano main.go ``` -Next, you will write a program. The program will loop over the numbers `1` through `3` and try to determine if those numbers are valid or not using a function called `validateValue`. If the number is determined to be invalid, the program will use the `fmt.Errorf` function to generate an `error` value that is returned from the function. The `fmt.Errorf` function allows you to create an `error` value where the error message is the message you provide to the function. It works similarly to `fmt.Printf`, but instead of printing the message to the screen it returns it as an `error` instead. +接下来,你将写一个程序。该程序将循环处理数字 `1` 到 `3` ,并尝试使用 `validateValue` 函数来判断这些数字是否有效。如果数字被确定为无效,程序将使用 `fmt.Errorf` 函数生成一个 `error` 值,并返回。你可以使用 `fmt.Errorf` 函数创建一个`error` 值,其中的错误信息是你提供给函数的信息。它的工作原理与 `fmt.Printf` 类似,但它不是将信息打印到屏幕上,而是将其作为 `error` 返回。 -Then, in the `main` function, the error value will be checked to see if it’s a `nil` value or not. If it is a `nil` value, the function succeeded and the `valid!` message is printed. If it’s not, the error received is printed instead. +然后,我们将在 `main` 函数中检查错误值,看它是否为 `nil`。如果是 `nil` ,函数就运行成功了,`valid!` 信息会被打印出来。否则,就会打印收到的错误信息。 -To begin your program, add the following code into your `main.go` file: +在开始编程前,请将以下代码粘贴至 `main.go` 中。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go package main @@ -94,33 +94,41 @@ func main() { } ``` -The `validateValue` function from the program takes a number and then returns an `error` based on whether it was determined to be a valid value or not. In this program, the number `1` is invalid and returns the error `that's odd`. The number `2` is invalid and returns the error `uh oh`. The `validateValue` function uses the `fmt.Errorf` function to generate the `error` value being returned. The `fmt.Errorf` function is convenient for returning errors because it allows you to format an error message using formatting similar to `fmt.Printf` or `fmt.Sprintf` without needing to then pass that `string` to `errors.New`. +程序中的 `validateValue` 函数接收一个数字,然后判断它是否为有效值,若不是,返回一个 `error`。在本程序中,数字 `1` 是无效的,并返回错误 `that's odd(那是奇数)`。数字 `2` 也是无效的,并返回错误 `uh oh`。`validateValue` 函数使用 `fmt.Errorf` 函数来生成将被返回的 `error` 值。用 `fmt.Errorf` 函数来返回错误很方便,因为它能让你使用类似于 `fmt.Printf` 或 `fmt.Sprintf` 的方式来格式化错误信息,而不需要再将错误字符串传递给 `errors.New`。 -In the `main` function, the `for` loop will start by iterating over each number from `1` to `3` and will store the value in the `num` variable. Inside the loop body, a call to `fmt.Printf` will print the number the program is currently validating. Then, it will call the `validateValue` function and pass in `num`, the current number being validated, and store the error result in the `err` variable. Lastly, if `err` is not `nil` it means an error occured during validation and the error message is printed using `fmt.Println`. The `else` clause of the error check will print `"valid!"` when an error wasn’t encountered. +* 在 `main` 函数中,`for` 循环将迭代从 `1` 至 `3` 的每个数字,并将值存储在 `num` 变量中。 +* 在循环体中,调用 `fmt.Printf` 打印程序当前正在验证的数字。 +* 接着,它将调用 `validateValue` 函数并把 `num` (即当前正在验证的数字)作为参数传入 ,并将错误结果存储在 `err` 变量中。 +* 最后,如果 `err` 不是 `nil`,就意味着在验证过程中发生了错误,继而使用 `fmt.Println` 打印错误信息。若无错误,错误检查的 `else` 子句将打印 `valid`。 -After saving your changes, run your program using the `go run` command with `main.go` as the argument from the `errtutorial` directory: +保存程序,使用 `go run` 命令,以 `main.go` 为参数,在 `errtutorial` 目录下运行你的程序: ```shell go run main.go ``` -The output from running the program will show that validation was run for each number and number `1` and number `2` returned their appropriate errors: +程序的输出结果将显示,每个数字都进行了验证,数字 `1` 和数字 `2` 都返回了相应的错误。 ``` -Outputvalidating 1... there was an error: that's odd +Output +validating 1... there was an error: that's odd validating 2... there was an error: uh oh validating 3... valid! ``` -When you look at the output from the program, you’ll see the program tried to validate all three numbers. The first time it says the `validateValue` function returned the `that's odd` error, which would be expected for the value of `1`. The next value, `2`, also shows it returned an error, but it was the `uh oh` error this time . Finally, the `3` value returns `nil` for the error value, meaning there wasn’t an error and the number is valid. The way the `validateValue` function is written, the `nil` error value would be returned for any values that aren’t either `1` or `2`. +当你看程序的输出时,你会注意到程序试图验证所有的三个数字。 + +* 第一次它说 `validateValue` 函数返回了 `that's odd` 错误,符合对数字 `1` 的预期。 +* 下一个值是 `2`,也显示它返回了一个错误,但这次是 `uh oh` 错误。 +* 最后,值 `3` 的错误值为 `nil`,意味着没有错误,即数字是有效的。按照 `validateValue` 函数的写法,任何不是 `1` 或 `2` 的值都会返回 `nil` 错误值。 -In this section, you used `fmt.Errorf` to create `error` values you returned from a function. You also added an error handler to print out the error message when any `error` is returned from the function. At times, though, it can be useful to know what an error means, not just that an error occurred. In the next section, you’ll learn to customize error handling for specific cases. +在这一节中,你使用 `fmt.Errorf` 创建了一个 `error` 值,并从函数中返回。你还写了一个错误处理器,当函数返回任何 `error`时,打印出错误信息。但有时,知道错误的具体含义,而不仅仅是 "有错误发生了",是非常有用的。在下一节中,你将学习如何为特定情况自定义错误处理。 -## Handling Specific Errors Using Sentinel Errors +## 使用哨兵错误处理特定错误 -When you receive an `error` value from a function, the most basic error handling is to check if the `error` value is `nil` or not. This will tell you if the function had an error, but sometimes you may want to customize error handling for a specific error case. For example, imagine you have code connecting to a remote server, and the only error information you get back is “you had an error”. You may wish to tell whether the error was because the server was unavailable or if your connection credentials were invalid. If you knew the error meant a user’s credentials were wrong, you might want to let the user know right away. But if the error means the server was unavailable, you may want to try reconnecting a few times before letting the user know. Determining the difference between these errors allows you to write more robust and user-friendly programs. +当你从一个函数接收到一个 `error` 值时,最基本的错误处理方式是检查 `error` 值是否为 `nil`。这将告诉你该函数是否有错误,但有时你可能想为特定的错误情况自定义错误处理。例如,想象一下,你有个连接到远程服务器的程序,而你得到的唯一错误信息是 "你有一个错误"。你可能希望知道这个错误是由于服务器不可用,还是连接凭证无效导致的。如果你知道这个错误意味着用户的凭证是错误的,你就可以马上告诉用户;如果该错误意味着服务器不可用,你可能想先尝试重新连接几次再告诉用户。分清这些错误之间的区别可以让你写出更健壮和用户友好的程序。 -One way you could check for a particular type of error might be using the `Error` method on an `error` type to get the message from the error and compare that value to the type of error you’re looking for. Imagine that in your program, you want to show a message other than `there was an error: uh oh` when the error value is `uh oh`. One approach to handling this case would be to check the value returned from the `Error` method, like so: +为了判断特定类型的错误,一种可行的方式是在 `error` 类型的变量上调用 `Error` 方法来获取具体的错误信息,然后与你预期的错误类型信息进行比对。想象一下,当错误值是 `uh oh` 时,你想显示一个除了 `there was an error: uh oh` 以外的消息。处理这种情况的一种方法是检查 `Error` 方法返回的值,像这样: ```go if err.Error() == "uh oh" { @@ -129,7 +137,7 @@ if err.Error() == "uh oh" { } ``` -Checking the string value of `err.Error()` to see if it’s the value `uh oh`, as in the code above, would work in this case. But the code would not work if the `uh oh` error `string` is slightly different elsewhere in the program. Checking errors this way can also lead to significant updates to code if the error’s message itself needs to be updated because every place the error is checked would need to be updated. Take the following code, for example: +检查 `err.Error()` 返回的字符串值,看看是否为 `uh oh`,就像上面的代码那样,在这种情况下是可行的。但是,如果 `uh oh` 错误字符串在程序中的其他地方略有不同,那么这段代码就无法正常运行。如果错误信息本身需要更新,这种方式检查错误也会导致代码的重大变动,因为每一个检查错误的地方都需要更新。以下面的代码为例: ```go func giveMeError() error { @@ -142,11 +150,11 @@ if err.Error() == "uh h" { } ``` -In this code, the error message includes a typo and is missing the `o` in `uh oh`. If this is noticed and fixed at some point, but only after adding this error checking in several places, all those places will need to have their checks updated to `err.Error() == "uh oh"`. If one is missed, which could be easy because it’s only a single character change, the expected custom error handler will not run because it’s expecting `uh h` and not `uh oh`. +在这段代码中,错误信息里有一个错别字, `uh oh` 少了个 `o`。如果这个问题在某个时候被注意到并被修复,但只是在几个地方添加了这个错误检查后,所有这些地方都需要将错误检查更新为 `err.Error() == "uh oh"`。 但很容易漏掉一个,因为它只变动了一个字符,预期的自定义错误处理程序将不会正常运行,因为它期望的是 `uh h` 而不是 `uh oh`。 -In cases like these, where you may want to handle a specific error differently than others, it’s common to create a variable whose purpose is to hold an error value. This way, the code can check against that variable instead of a string. Typically, these variables begin with either `err` or `Err` in their names to signify they’re errors. If the error is only meant to be used within the package it’s defined in, you would want to use the `err` prefix. If the error is meant to be used elsewhere, you would instead use the `Err` prefix to make it an exported value, similar to a function or a `struct`. +在这样的情况下,你可能会想用另外的方式来处理一个特定的错误。通常会创建一个变量,以保存一个错误值。这样,代码就可以根据这个变量而不是某个字符串进行检查。通常,这些变量的名称以 `err` 或 `Err` 开头,来表示它们是错误。如果错误只在它所定义的包内使用,你可以使用 `err` 前缀;如果错误要在其他地方使用,你可以使用 `Err` 前缀,像导出函数和结构体那样,把它变成一个导出值。 -Now, let’s say you were using one of these error values in the typo example from before: +现在,我们假设你使用了之前的错别字例子中的某个错误: ```go var errUhOh = fmt.Errorf("uh h") @@ -161,19 +169,19 @@ if err == errUhOh { } ``` -In this example, the variable `errUhOh` is defined as the error value for an “uh oh” error (even though it’s misspelled). The `giveMeError` function returns the value of `errUhOh` because it wants to let the caller know that an “uh oh” error happened. Then, the error handling code compares the `err` value returned from `giveMeError` against `errUhOh` to see if an “uh oh” error is the one that happened. Even if the typo is found and fixed, all the code would still be working because the error check is checking against the value of `errUhOh`, and the value of `errUhOh` is the fixed version of the error value that `giveMeError` is returning. +在这个例子中,变量 `errUhOh` 被定义为 "uh oh" 错误的错误值(尽管它被拼错了)。`giveMeError` 函数返回 `errUhOh` 的值,因为它想让调用者知道发生了一个 "uh oh" 错误。然后,错误处理代码比较 `giveMeError` 返回的 `err` 值与 `errUhOh`,看是否发生了 "uh oh"错误。即使发现并修复了错别字,所有的代码仍然可以正常运行,因为错误检查是针对 `errUhOh` 进行的,而 `errUhOh` 的值是 `giveMeError` 返回的错误值的固定版本。 -An error value that is intended to be checked and compared in this way is known as a _sentinel error_. A sentinel error is an error that’s designed to be a unique value that can always be compared against for a specific meaning. The `errUhOh` value above will always have the same meaning, that an “uh oh” error occurred, so a program can rely on comparing an error to `errUhOh` to determine whether that error occurred. +一个以这种方式检查和比较的错误值被称为 _哨兵错误_。一个哨兵错误是被设计成唯一的、并在每次比较中都代表了同一个特定含义的值。上面的`errUhOh` 值总是有相同的含义,即,如果发生了一个"uh oh"错误,程序可以依靠将错误与 `errUhOh` 比较来确定是否发生了该错误。 -The Go standard library also defines a number of sentinel errors that are available when developing Go programs. One example would be the [`sql.ErrNoRows`](https://pkg.go.dev/database/sql#pkg-variables) error. The `sql.ErrNoRows` error is returned when a database query doesn’t return any results, so that error can be handled differently from a connection error. Since it’s a sentinel error, it can be compared against in error-checking code to know when a query doesn’t return any rows, and the program can handle that differently than other errors. +Go 标准库也定义了一些哨兵错误,在开发 Go 程序时可以使用,如 [`sql.ErrNoRows `](https://pkg.go.dev/database/sql#pkg-variables) 错误。`sql.ErrNoRows` 错误用来表示数据库查询没有返回任何结果,因此,就能把它和连接错误区分开来。由于它是一个哨兵错误,它可以在错误检查代码中进行比较,以知道什么时候查询没有返回任何数据行,并且程序可以以不同于其他错误的方式处理这个错误。 -Generally, when creating a sentinel error value, the [`errors.New`](https://pkg.go.dev/errors#New) function from the [`errors`](https://pkg.go.dev/errors) package is used instead of the `fmt.Errorf` function you’ve been using thus far. Using `errors.New` instead of `fmt.Errorf` does not make any foundational changes to how the error works, though, and both functions could be used interchangeably most of the time. The biggest difference between the two is the `errors.New` function will only create an error with a static message and the `fmt.Errorf` function allows formatting the string with values, similar to `fmt.Printf` or `fmt.Sprintf`. Since sentinel errors are fundamental errors with values that don’t change, it’s common to use `errors.New` to create them. +一般情况下,我们用 [`errors`](https://pkg.go.dev/errors) 包里的 `errors.New` 函数来创建一个前哨错误值,而不是你一直使用的 `fmt.Errorf` 函数。使用 `errors.New` 代替 `fmt.Errorf` 并不会对错误的运作方式产生任何根本性的改变,尽管如此,这两个函数在大多数时候都可以互换使用。两者最大的区别是 `errors.New` 函数只会创建一个带有静态信息的错误,而 `fmt.Errorf` 函数可以格式化字符串,类似于 `fmt.Printf` 或 `fmt.Sprintf` 。由于哨兵错误是基础错误,其值不会改变,所以通常使用 `errors.New` 来创建它们。 -Now, update your program to use a sentinel error for the “uh oh” error instead of `fmt.Errorf`. +现在,更新你的程序,把 "uh oh" 错误改造成哨兵错误,而不是 `fmt.Errorf`。 -First, open the `main.go` file to add the new `errUhOh` sentinel error and update the program to use it. The `validateValue` function is updated to return the sentinel error instead of using `fmt.Errorf`. The `main` function is updated to check for the `errUhOh` sentinel error and print `oh no!` when it encounters it instead of the `there was an error:` message it shows for other errors. +首先,打开 `main.go` 文件,添加新的 `errUhOh` 哨兵错误并更新程序。更新 `validateValue` 函数以返回哨兵错误,而不是使用 `fmt.Errorf`。更新 `main` 函数,以检查 `errUhOh` 哨兵错误,并在遇到它时打印 `oh no!`,而不是像显示其他错误那样显示 `there was an error:` 。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go package main @@ -211,37 +219,38 @@ func main() { } ``` -Now, save your code and use `go run` to run your program again: +保持代码并使用 `go run` 再次运行程序: ```shell go run main.go ``` -This time the output will show the generic error output for the `1` value, but it uses the custom `oh no!` message when it sees the `errUhOh` error returned from `validateValue` for `2`: +这次输出将为数字 `1` 显示通用的错误描述,但是检测到数字 `2` 从 `validateValue` 中返回的 `errUhOh` 错误时,将显示自定义的 `oh no!` 。 ``` -Outputvalidating 1... there was an error: that's odd +Output +validating 1... there was an error: that's odd validating 2... oh no! validating 3... valid! ``` -Using sentinel errors inside your error checking makes it easier to handle special error cases. For example, they can help determine whether the file you’re reading is failing because you’ve reached the end of the file, which is signified by the [`io.EOF`](https://pkg.go.dev/io#pkg-variables) sentinel error, or if it’s failing for some other reason. +在你的错误检查中使用哨兵错误可以更容易地处理特殊的错误情况。例如,它们可以帮助你判断正在读取的文件是因为已经到达了文件的末尾(由 [`io.EOF`](https://pkg.go.dev/io#pkg-variables) 哨兵错误表示)而失败,还是因为其他原因而失败。 -In this section, you created a Go program that uses a sentinel error using `errors.New` to signify when a specific type of error occurred. Over time as your program grows, though, you may get to the point where you’d like more information included in your error than just the `uh oh` error value. This error value doesn’t give any context on where the error happened or why it happened, and it can be hard to track down specifics of the error in larger programs. To aid in troubleshooting and to cut down the time for debugging, you can make use of error wrapping to include the specifics you need. +在本节中,你创建了一个 Go 程序,使用 `errors.New` 来表示特定类型的错误发生时的哨兵错误。随着时间的推移,随着程序的发展,你可能会希望在错误中包含更多的信息,而不仅仅是 `uh oh` 错误值。目前的这个错误值没有提供任何关于错误发生地点或原因的背景信息,导致在大型程序中很难追踪到错误的具体细节。为了帮助排除故障和减少调试时间,你可以利用错误包装来包含你需要的细节。 -## Wrapping and Unwrapping Errors +## 包装和解包错误 -Wrapping errors means taking one error value and putting another error value inside it, like a wrapped gift. Similar to a wrapped gift, though, you need to unwrap it to know what’s inside. Wrapping an error allows you to include additional information about where the error came from or how it happened without losing the original error value, since it’s inside the wrapper. +包装错误是指将一个错误值放在另一个错误值里面,就像一个包装好的礼物。不过,与包装好的礼物类似,你需要解开包装才能知道里面是什么。包装一个错误可以让你在不丢失原始错误值的前提下加入错误来源或如何发生的额外信息,因为它就在包装好的错误中。 -Before Go 1.13, it was possible to wrap errors since you could create custom error values that included the original error. But you would either have to create your own wrappers or use a library that already did the work for you. In Go 1.13, though, Go added support for wrapping and unwrapping errors as part of the standard library by adding the [`errors.Unwrap`](https://pkg.go.dev/errors#Unwrap) function and the `%w` verb for the `fmt.Errorf` function. In this section, you’ll update your program to use the `%w` verb to wrap errors with more information, and you’ll then use `errors.Unwrap` to retrieve the wrapped information. +在 Go 1.13 之前,由于你可以创建包含原始错误的自定义错误值,所以也可以对错误进行包装。但是你必须创建自己的包装器,或者使用已经替你做了这些工作的库。Go 在1.13中通过添加 [`errors.Unwrap`](https://pkg.go.dev/errors#Unwrap) 函数和 `fmt.Errorf` 函数的 `%w` 动词,增加了对包装和解包错误的支持,并将其作为标准库的一部分。在这一节中,你将更新程序,使用 `%w` 动词来包装带有更多信息的错误,并用 `errors.Unwrap` 来提取被包装的信息。 -### Wrapping Errors with `fmt.Errorf` +### 使用 `fmt.Errorf` 包装错误 -The first feature to examine when wrapping and unwrapping errors is an addition to the existing `fmt.Errorf` function. In the past, `fmt.Errorf` was used to create formatted error messages with additional information using verbs such as `%s` for strings and `%v` for generic values. Go 1.13 added a new verb with a special case, the `%w` verb. When the `%w` verb is included in a format string and an `error` is provided for the value, the error returned from `fmt.Errorf` will include the value of the `error` wrapped in the error being created. +在学习包装和解包错误时,让我们先来补充一下对现有的 `fmt.Errorf` 函数的认识。之前,`fmt.Errorf` 可以利用动词来创建带有附加信息的格式化错误信息,例如 `%s` 用于字符串,`%v` 用于普通值。Go 1.13增加了一个新的特殊动词, `%w`。当 `%w` 动词包含在格式化字符串中,并且提供了一个 `error` 值时,从 `fmt.Errorf` 返回的错误将包含被包装的 `error` 的值。 -Now, open the `main.go` file and update it to include a new function called `runValidation`. This function will take the number currently being validated and run any validation needed on that number. In this case, it only needs to run the `validateValue` function. If it encounters an error validating the value it will wrap the error using `fmt.Errorf` and the `%w` verb to show there was a `run error` that occurred, then return that new error. You should also update the `main` function so instead of calling `validateValue` directly it calls `runValidation` instead: +现在,打开 `main.go` 文件,加入一个名为 `runValidation` 的新函数。这个函数接收当前正在验证的数字,并对该数字进行任何需要的验证。在这种情况下,就只需要运行 `validateValue` 函数。如果它遇到一个错误的验证值,它将使用 `fmt.Errorf` 和 `%w` 动词来包装这个错误,以显示发生了 `run error` ,然后返回这个新的错误。你也应该更新 `main` 函数,这样就不会直接调用 `validateValue`,而是调用 `runValidation`。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go ... @@ -275,13 +284,13 @@ func main() { } ``` -Once you’ve saved your updates, run the updated program using `go run`: +保存更新后的代码,运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` Output @@ -290,17 +299,17 @@ validating 2... there was an error: run error: uh oh validating 3... valid! ``` -There are a few things to look at in this output. First, you’ll see the error message being printed for the value `1` now includes `run error: that's odd` in the error message. This shows the error was wrapped by `runValidation`’s `fmt.Errorf` and that the value of the error being wrapped, `that's odd`, is included in the error message. +在这个输出中,有几个地方值得注意。首先,你会看到值 `1` 对应的错误信息现在包含了 `run error: that's odd` 。这表明错误被 `runValidation` 的 `fmt.Errorf` 包装了,被包装的错误值 `that's odd` 包含在错误信息中。 -Next, though, there’s a problem. The special error handling that was added for the `errUhOh` error isn’t running. If you look at the line validating the `2` input, you’ll see it shows the default error message of `there was an error: run error: uh oh` instead of the expected `oh no!` message. You know the `validateValue` function is still returning the `uh oh` error because you can see it at the end of the wrapped error, but the error detection of `errUhOh` is no longer working. This happens because the error being returned by `runValidation` is no longer `errUhOh`, it’s the wrapped error created by `fmt.Errorf`. When the `if` statement tries to compare the `err` variable to `errUhOh`, it returns false because `err` isn’t equal to `errUhOh` any more, it’s equal to the error that’s _wrapping_ `errUhOh`. To fix the `errUhOh` error checking, you’ll need to retrieve the error from inside the wrapper, using the `errors.Unwrap` function. +但是,有一个问题。为 `errUhOh` 错误添加的特殊错误处理并没有正常运行。如果你看看检查参数 `2` 的那一行,你会发现它显示的是默认的错误信息 `there was an error: run error: uh oh`,而不是预期的 `oh no!` 信息。你知道 `validateValue` 函数仍在返回 `uh oh` 错误,因为你可以在包裹的错误末尾看到它,但 `errUhOh` 的错误检测不再如期运行。发生这种情况是因为 `runValidation` 返回的错误不再是 `errUhOh`,而是由 `fmt.Errorf` 创建的包装好的错误。当 `if` 语句试图比较 `err` 变量和 `errUhOh` 时,它将返回 `false` ,因为 `err` 不再等于 `errUhOh`,它等于*包装着* `errUhOh` 的错误。要修复 `errUhOh` 的错误检查,你需要使用 `errors.Unwrap` 函数,以从包装器中提取内部的错误。 -### Unwrapping Errors with `errors.Unwrap` +### 使用 `errors.Unwrap` 解包错误 -In addition to the `%w` verb being added in Go 1.13, a few new functions were added to the Go [`errors`](https://pkg.go.dev/errors) package. One of these, the `errors.Unwrap` function, takes an `error` as a parameter and, if the error passed in is an error wrapper, it will return the wrapped `error`. If the `error` provided isn’t a wrapper, the function will return `nil`. +Go 1.13 除了增加了 `%w` 动词外,还在 [`errors`](https://pkg.go.dev/errors) 包中增加了一些新的函数。其一是 `errors.Unwrap` 函数,接收一个 `error` 作为参数,如果传递的错误是一个错误包装器,它会返回被包装的 `error`。如果提供的 `error` 不是一个包装器,返回 `nil`。 -Now, open the `main.go` file again and, using `errors.Unwrap`, update the `errUhOh` error check to handle the case where `errUhOh` is wrapped inside an error wrapper: +现在,再次打开 `main.go` 文件,使用 `errors.Unwrap`,更新 `errUhOh` 错误检查,以处理 `errUhOh` 被包装在一个错误包装器中的情况。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go func main() { @@ -318,33 +327,34 @@ func main() { } ``` -After saving the edits, run the program again: +保存并运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... there was an error: run error: that's odd +Output +validating 1... there was an error: run error: that's odd validating 2... oh no! validating 3... valid! ``` -Now, in the output, you’ll see the `oh no!` error handling for the `2` input value is back. The additional `errors.Unwrap` function call you added to the `if` statement allows it to detect `errUhOh` both when `err` itself is an `errUhOh` value as well as if `err` is an error that is directly wrapping `errUhOh`. +现在,在输出中,你会看到对 `2` 输入值的 `oh no!` 错误处理回归正常了。你在 `if` 语句中增加的 `errors.Unwrap` 函数调用,让它在 `err` 本身是 `errUhOh` 值时,以及 `err` 是直接包装了 `errUhOh` 的错误时,检测到 `errUhOh`。 -In this section, you used the `%w` verb added to `fmt.Errorf` to wrap the `errUhOh` error inside another error and give it additional information. Then, you used `errors.Unwrap` to access the `errorUhOh` error that is wrapped inside another error. Including errors inside other errors as `string` values is OK for humans reading error messages, but sometimes you’ll want to include additional information with the error wrapper to aid the program in handling the error, such as the status code in an HTTP request error. When this happens, you can create a new custom error to return. +在本节中,你使用了添加到 `fmt.Errorf` 中的 `%w` 动词,把 `errUhOh` 错误包装在另一个错误中,并附加信息。然后,你使用 `errors.Unwrap` 来访问被包装在另一个错误中的 `errorUhOh` 错误。将错误以字符串的形式包含在其他错误中,对于人类来说阅读错误信息来说是可行的。但有时你又想在错误包装中包含额外的信息,以帮助程序处理错误,例如HTTP请求错误中的状态代码。这时候,你就可以创建一个新的自定义错误来返回。 -## Custom Wrapped Errors +## 自定义包装错误 -Since Go’s only rule for the `error` interface is that it includes an `Error` method, it’s possible to turn many Go types into a custom error. One way is by defining a `struct` type with extra information about the error, and then also including an `Error` method. +由于 Go 对 `error` 接口的唯一规定是它包含了一个 `Error` 方法,所以可以把许多 Go 类型变成一个自定义的错误。一种方法是通过定义一个结构体类型,包含了关于错误的额外信息,同时也拥有一个 `Error` 方法。 -For a validation error, it may be useful to know which value actually caused the error. Next, let’s create a new `ValueError` struct that contains a field for the `Value` that caused the error and an `Err` field that contains the actual validation error. Custom error types commonly use the `Error` suffix on the end of the type name to signify it’s a type that conforms to the `error` interface. +对于一个验证错误,知道哪个值实际导致了错误可能是有用的。接下来,让我们创建一个新的 `ValueError` 结构,它包含一个导致错误的 `Value` 字段和一个包含实际验证错误的 `Err` 字段。自定义错误类型通常在类型名称的末尾使用 `Error` 后缀,以表示它是一个实现了 `error` 接口的类型。 -Open your `main.go` file and add the new `ValueError` error struct, as well as a `newValueError` function to create instances of the error. You will also need to create a method called `Error` for `ValueError` so the struct will be considered an `error`. This `Error` method should return the value you want to be displayed whenever the error is converted to a string. In this case, it will use `fmt.Sprintf` to return a string that shows `value error:` and then the wrapped error. Also, update the `validateValue` function so instead of returning just the basic error, it uses the `newValueError` function to return a custom error: +打开 `main.go` 文件,添加新的 `ValueError` 错误结构体,以及一个 `newValueError` 函数来创建错误实例。你还需要为 `ValueError` 创建一个名为 `Error` 的方法,以使该结构体被视为 `error`。这个 `Error` 方法应该返回你希望在错误被转换为字符串时显示的值。在这种情况下,它将使用 `fmt.Sprintf` 来返回一个字符串,这个字符串以 `value error:` 开头,后面是被包装的错误。另外,再更新 `validateValue` 函数,这样它就不会只返回基本错误,而是使用 `newValueError` 函数来返回一个自定义错误: -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go ... @@ -383,25 +393,26 @@ func validateValue(number int) error { ... ``` -Once your updates are saved, run the program again with `go run`: +保存更新的代码并再次运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... there was an error: run error: value error: that's odd +Output +validating 1... there was an error: run error: value error: that's odd validating 2... there was an error: run error: value error: uh oh validating 3... valid! ``` -You’ll see that the output now shows the errors are wrapped inside of `ValueError` by the `value error:` before them in the output. However, the `uh oh` error detection is broken again because `errUhOh` is now inside two layers of wrappers, `ValueError` and the `fmt.Errorf` wrapper from `runValidation`. The code code only uses `errors.Unwrap` once on the error, so this results in the first `errors.Unwrap(err)` now only returning a `*ValueError` and not `errUhOh`. +你会看到,现在的输出显示错误被包装在 `ValueError` 内,`value error:` 在被包装的错误前。然而,`uh oh` 错误检测又被破坏了,因为 `errUhOh` 现在在两层包装内:`ValueError` 和 `runValidation` 的 `fmt.Errorf` 包装器。代码中只对错误使用了一次 `errors.Unwrap`,所以这导致第一个 `errors.Unwrap(err)` 现在只返回 `*ValueError` 类型的值而不是 `errUhOh`。 -One way to fix this would be to update the `errUhOh` check to add an additional error check that calls `errors.Unwrap()` twice to unwrap both layers. To add this, open your `main.go` file and update your `main` function to include this change: +修复这个问题的一种方法是更新 `errUhOh` 检查,增加一个额外的错误检查,调用 `errors.Unwrap()` 两次以解开两层包装。请打开 `main.go` 文件并更新 `main` 函数以实现此操作。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```shell ... @@ -423,25 +434,26 @@ func main() { } ``` -Now, save your `main.go` file and use `go run` to run your program again: +保存文件并再次运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... there was an error: run error: value error: that's odd +Output +validating 1... there was an error: run error: value error: that's odd validating 2... there was an error: run error: value error: uh oh validating 3... valid! ``` -You’ll see that, uh oh, the `errUhOh` special error handling is still not working. The line validating the `2` input where we’d expect to see the special error handling `oh no!` output still shows the default `there was an error: run error: ...` error output. This happens because the `errors.Unwrap` function doesn’t know how to unwrap the `ValueError` custom error type. In order for a custom error to be unwrapped, it needs to have its own `Unwrap` method that returns the inner error as an `error` value. When creating errors using `fmt.Errorf` with the `%w` verb earlier, Go was actually creating an error for you that already has an `Unwrap` method added, so you didn’t need to do it yourself. Now that you’re using your own custom function, though, you need to add your own. +你会看到,uh oh,`errUhOh` 的特殊错误处理仍然没有如期运行。在验证 `2` 输入的那一行,我们期望看到特殊错误处理 `oh no!` 输出仍然显示默认的 `there was an error: run error: ...` 错误输出。发生这种情况是因为 `errors.Unwrap` 函数不知道如何解包 `ValueError` 自定义错误类型。为了让自定义错误被成功解包,它需要有自己的 `Unwrap` 方法,将内部错误作为 `error` 值返回。先前使用 `fmt.Errorf` 和 `%w` 动词创建错误时,Go 实际上是为你创建了一个已经添加了 `Unwrap` 方法的错误,所以你不需要自己做。不过现在你使用了自定义函数,需要添加你自己的 `Unwrap` 方法。 -To finally fix the `errUhOh` error case, open `main.go` and add an `Unwrap` method to `ValueError` that returns `Err`, the field the inner wrapped error is stored in: +为了最终解决 `errUhOh` 错误的问题,打开 `main.go` ,给 `ValueError` 添加一个`Unwrap` 方法,返回 `Err` ,即存储内部的被包装错误的字段。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```shell ... @@ -457,37 +469,38 @@ func (ve *ValueError) Unwrap() error { ... ``` -Then, once you’ve saved the new `Unwrap` method, run your program: +保存新的 `Unwrap` 方法,再次运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... there was an error: run error: value error: that's odd +Output +validating 1... there was an error: run error: value error: that's odd validating 2... oh no! validating 3... valid! ``` -The output shows the `oh no!` error handling for the `errUhOh` error is working again because `errors.Unwrap` is now able to also unwrap `ValueError`. +输出显示 `oh no!` 错误处理的 `errUhOh` 错误又如期运行了,因为 `errors.Unwrap` 现在也能解包 `ValueError`。 -In this section you created a new, custom `ValueError` error to provide yourself or your users with information about the validation process as part of the error message. You also added support for error unwrapping to your `ValueError` so `errors.Unwrap` can be used to access the wrapped error. +在本小节中,你创建了一个新的、自定义的 `ValueError` 错误,给你自己或你的用户提供关于验证过程的信息,并将其作为错误信息的一部分。你还给你的 `ValueError` 添加了对错误解包的支持,所以 `errors.Unwrap` 可以访问到被包装的错误。 -The error handling is getting a bit clunky and hard to maintain, though. Every time there’s a new layer of wrapping you’ve had to add another `errors.Unwrap` to the error checking to handle it. Thankfully, the `errors.Is` and `errors.As` functions in the `errors` package are available to make working with wrapped errors easier. +不过,错误处理变得有点笨拙,难以维护。每次有一个新的包装层,你就必须在错误检查中添加一个 `errors.Unwrap` 方法来处理它。值得庆幸的是,`errors` 包中的 `errors.Is` 和 `errors.As` 函数可以使处理被包装的错误更容易。 -## Working with Wrapped Errors +## 与被包装的错误打交道 -If you needed to add a new `errors.Unwrap` function call for every potential layer of error wrapping your program had, it would get very long and difficult to maintain. For this reason, two additional functions were also added to the `errors` package in the Go 1.13 release. Both of these functions make it easier to work with errors by allowing you to interact with errors no matter how deeply they’re wrapped inside other errors. The `errors.Is` function allows you to check if a specific sentinel error value is anywhere inside a wrapped error. The `errors.As` function allows you to get a reference to a certain type of error anywhere inside a wrapped error. +如果你需要为程序的每一个潜在的错误包装层添加一个新的 `errors.Unwrap` 函数调用,它将变得非常长,而且难以维护。出于这个原因,Go 1.13版本中的 `errors` 包还增加了两个函数。这两个函数使你更容易与错误打交道,无论它们在其他错误中被包装得多深,都可以获取得到。`errors.Is` 函数检查一个特定的哨兵错误值是否被包装在某错误中;`errors.As` 函数可以获取某个包装错误内部中的特定错误类型,无论它被包装在哪一层。 -### Checking an Error Value with `errors.Is` +### 使用 `errors.Is` 检查错误 -Using `errors.Is` to check for a specific error makes the `errUhOh` special error handling much shorter because it handles all the nested error unwrapping you were doing manually. The function takes two `error` parameters, the first being the error you actually received and the second parameter being the error you want to check against. +使用 `errors.Is`来检查一个特定的错误,使得 `errUhOh` 的特殊错误处理变得更短,因为它处理了所有你需要手动进行的嵌套错误解包。该函数有两个 `error` 类型的参数,第一个是你实际收到的错误,第二个参数是你想验证的目标错误。 -To clean up the `errUhOh` error handling, open your `main.go` file and update the `errUhOh` check in the `main` function to use `errors.Is` instead: +为了让 `errUhOh` 错误处理更清晰,请打开你的 `main.go` 文件,更新 `main` 函数中的 `errUhOh` 检查,用 `errors.Is` 代替。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```go ... @@ -507,31 +520,32 @@ func main() { } ``` -Then, save your code and run the program again using `go run`: +接着,保存并运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... there was an error: run error: value error: that's odd +Output +validating 1... there was an error: run error: value error: that's odd validating 2... oh no! validating 3... valid! ``` -The output shows the `oh no!` error message, which means that even though there’s only one error check for `errUhOh`, it will still be found in the error chain. `errors.Is` takes advantage of an error type’s `Unwrap` method to keep digging deeper into a chain of errors until it either finds the error value you’re looking for, a sentinel error, or encounters an `Unwrap` method that returns a `nil` value. +输出显示了 `oh no!` 错误信息,这意味着即使只有一个对 `errUhOh` 的错误检查,它仍然会在错误链中被发现。`errors.Is` 利用错误类型的 `Unwrap` 方法,不断深入挖掘错误链,直到找到你要找的错误值,在遇到一个哨兵错误,或者 `Unwrap` 方法返回 `nil` 值时,停止检索。 -Using `errors.Is` is the recommended way to check for specific errors now that error wrapping exists as a feature in Go. Not only can it be used for your own error values, but it can also be used for other error values such as the `sql.ErrNoRows` error mentioned earlier in this tutorial. +推荐使用 `errors.Is` 检查特定错误,现在错误包装已经成为了 Go 的一个特性。它不仅可以用于你自己的错误值,还可以用于其他错误值,比如本教程前面提到的 `sql.ErrNoRows` 错误。 -### Retrieving an Error Type with `errors.As` +### 使用 `errors.As` 提取某个类型的错误 -The last function added to the `errors` package in Go 1.13 is the `errors.As` function. This function is used when you want to get a reference to a certain type of error to interact with it in more detail. For example, the `ValueError` custom error you added earlier gives access to the actual value being validated in the `Value` field of the error, but you can only access it if you have a reference to that error first. This is where `errors.As` comes in. You can give `errors.As` an error, similar to `errors.Is`, and a variable for a type of error. It will then go through the error chain to see if any of the wrapped errors match the type provided. If one matches, the variable passed in for the error type will be set with the error `errors.As` found, and the function will return `true`. If no error types match, it will return `false`. +Go 1.13中的 `errors` 包添加的最后一个函数是 `errors.As` 函数。当你想获得某种类型的错误的引用以与之进行更详细的交互时,可以使用这个函数。例如,你之前添加的 `ValueError` 自定义错误可以访问错误的 `Value` 字段中正在验证的实际值,但你只有在首先拥有对该错误的引用时才能访问它。这就是 `errors.As` 诞生的原因。你可以传给 `errors.As` 一个错误(类似于 `errors.Is`),以及期望的错误类型对应的变量。然后它将通过错误链查看是否有任何被包装的错误与提供的类型相匹配。如果有匹配的,传递进来的错误类型的变量将被设置为 `errors.As` 找到的错误,并且该函数将返回 `true`。如果没有匹配的错误类型,它将返回 `false`。 -Using `errors.As` you can now take advantage of the `ValueError` type to show additional error information in your error handler. Open your `main.go` file one last time and update the `main` function to add a new error handling case for `ValueError`\-type errors that prints out `value error`, the invalid number, and the validation error: +通过使用 `errors.As`,现在你可以利用 `ValueError` 类型,在错误处理中显示额外的错误信息。最后一次打开 `main.go` 文件,更新 `main` 函数,为 `ValueError` 类型的错误添加一个新的错误处理案例,打印出 `value error`、无效的数字,和验证错误。 -projects/errtutorial/main.go +> projects/errtutorial/main.go ```shell ... @@ -555,34 +569,33 @@ func main() { } ``` -In the code above, you declared a new `valueErr` variable and used `errors.As` to get a reference to the `ValueError` if it’s wrapped inside the `err` value. By getting access to the error as a `ValueError`, you’re then able to access any additional fields the type provides, such as the actual value that failed validation. This could be helpful if the validation logic happens deeper inside the program and you don’t normally have access to the values to give users hints on where something might have gone wrong. Another example of where this could be helpful is if you’re doing network programming and run into a [`net.DNSError`](https://pkg.go.dev/net#DNSError). By getting a reference to the error, you are able to see if the error was the result of not being able to connect, or if the error was caused by being able to connect, but your resource was not found. Once you know this, you can handle the error in different ways. +在上面的代码中,你声明了一个新的 `valueErr` 变量,并使用 `errors.As` 来获得对 `ValueError` 的引用(如果它被包装在 `err` 值中)。通过获得 `ValueError` 的引用,你就能访问该类型提供的任何额外字段,例如验证失败的实际值。如果验证逻辑发生在程序的更深处,而你通常不能访问这些值来给用户提示可能出错的地方,这将是有用的。另一个例子是,如果你在做网络编程时遇到 [`net.DNSError`](https://pkg.go.dev/net#DNSError),通过获得对错误的引用,你能够看到这个错误是由于无法连接导致的错误,还是由于能够连接、但没有找到你的资源而导致的错误。一旦你知道这一点,你就可以用不同的方式来处理这个错误。 -To see `errors.As` in action, save your file and run the program using `go run`: +保存并运行程序以看看 `errors.As` 的实际效果: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Outputvalidating 1... value error (1): that's odd +Output +validating 1... value error (1): that's odd validating 2... oh no! validating 3... valid! ``` -This time in the output you won’t see the default `there was an error: ...` message, because all the errors are being handled by other error handlers. The output for validating `1` shows that the `errors.As` error check returned `true` because the `value error ...` error message is being displayed. Since the `errors.As` function returned true, the `valueErr` variable is set to be a `ValueError` and can be used to print out the value that failed validation by accessing `valueErr.Value`. - -For the `2` value, the output also shows that even though the `errUhOh` is also wrapped inside a `ValueError` wrapper, the `oh no!` special error handler is still executed. This is because the special error handler using `errors.Is` for `errUhOh` comes first in the collection of `if` statements handling the errors. Since this handler returns `true` before the `errors.As` even runs, the special `oh no!` handler is the one executed. If the `errors.As` in your code came before the `errors.Is`, the `oh no!` error message would become the same `value error ...` as the `1` value, except in this case it would print `value error (2): uh oh`. +这次你不会在输出中看到默认的 `there was an error: ...` 信息,因为所有的错误都被其他错误处理程序处理了。验证 `1` 的输出表明 `errors.As` 错误检查返回 `true`,因为 `value error ...` 错误信息显示在了输出中。由于 `errors.As` 函数返回 `true`,`valueErr` 变量被设置为 `ValueError`,可以通过访问 `valueErr.Value` 来打印出验证失败的值。 -In this section, you updated your program to use the `errors.Is` function to remove a lot of additional calls to `errors.Unwrap` and make your error handling code more robust and future-proof. You also used the `errors.As` function to check if any of the wrapped errors is a `ValueError`, and then used fields on the value if one was found. +对于值 `2` ,输出结果还表明,即使 `errUhOh` 被包装在 `ValueError` 的包装器内,`oh no!` 特殊错误处理程序仍然被执行。这是因为使用 `errors.Is` 处理 `errUhOh` 的特殊错误处理器在处理错误的 `if` 语句集合中排在第一位。由于这个判断在 `errors.As` 判断之前返回`true`,所以特殊的 `oh no!` 处理程序被执行了。如果你的代码中的 `errors.As` 在 `errors.Is` 之前,`oh no!` 错误信息将变成与 `1` 值相同的 `value error ...`,只是在这种情况下它将打印 `value error (2): uh oh`。 -## Conclusion +在本节中,你更新了你的程序,使用了 `errors.Is` 函数,删除了大量对 `errors.Unwrap` 的额外调用,使你的错误处理代码更健壮,更适合未来的需要。你还使用了 `errors.As` 函数来检查任何被包装的错误是否是 `ValueError` 类型,如果是,就可以获得该值的所有字段。 -In this tutorial, you wrapped an error using the `%w` format verb and unwrapped an error using `errors.Unwrap`. You also created a custom error type that supports `errors.Unwrap` in your own code. Finally, you used your custom error type to explore the new helper functions `errors.Is` and `errors.As`. +## 结论 -Using these new error functions makes it easier to include deeper information about the errors you create or work with. It also future proofs your code to ensure your error checking continues to work even if errors become deeply nested going forward. +在本教程中,你使用 `%w` 格式动词包装了一个错误,使用 `errors.Unwrap` 解包了一个错误。你还创建了一个自定义的错误类型,在你自己的代码中支持 `errors.Unwrap`。最后,你使用你的自定义错误类型来探索新的辅助函数 `errors.Is` 和 `errors.As`。 -If you’d like to find more details about how to use the new error features, the Go blog has a post about [Working with Errors in Go 1.13](https://go.dev/blog/go1.13-errors). The documentation for the [`errors` package](https://pkg.go.dev/errors) package also includes more information. +使用这些新的错误函数,可以更容易地在你创建或处理的错误中加入更深层次的信息。它还可以确保你代码中的错误,即使在未来被深入嵌套时,错误检查也能正常运行。 -This tutorial is also part of the [DigitalOcean](https://www.digitalocean.com/) [How to Code in Go](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go) series. The series covers a number of Go topics, from installing Go for the first time to how to use the language itself. +如果你想找到更多关于如何使用新的错误特性的细节,Go 博客有一篇关于 [上手 Go 1.13 的错误处理](https://go.dev/blog/go1.13-errors) 的文章。[`errors`包](https://pkg.go.dev/errors) 的文档也包含了更多的信息。 diff --git a/content/zh/docs/46-How_To_Use_Dates_and_Times_in_Go.md b/content/zh/docs/46-How_To_Use_Dates_and_Times_in_Go.md index 151dce6..ee73ac8 100644 --- a/content/zh/docs/46-How_To_Use_Dates_and_Times_in_Go.md +++ b/content/zh/docs/46-How_To_Use_Dates_and_Times_in_Go.md @@ -1,46 +1,46 @@ -# How To Use Dates and Times in Go +# 如何在 Go 中使用日期和时间 -## Introduction +## 介绍 -Software is designed to make it easier to get work done, and for many people, that includes interacting with dates and times. Date and time values show up everywhere in modern software. For example, keeping track of when a car needs service and letting the owner know, keeping track of changes in a database to create an audit log, or just comparing one time to another to determine how long a process took. Therefore, retrieving the current time, manipulating time values to extract information from them, and displaying them to users in an easy-to-understand format are essential properties of an application. +软件旨在使完成工作更容易,对许多人来说,这包括与日期和时间的交互。日期和时间在现代软件中随处可见。例如,跟踪汽车何时需要维修并让车主知道,跟踪数据库的变化以创建审计日志,或者只是将一个时间与另一个时间相比较以确定一个过程花了多长时间。因此,检索当前时间,操作时间值以从中提取信息,并以易于理解的格式显示给用户,是对一个应用程序最基本的要求之一。 -In this tutorial, you will create a Go program to get the current local time of your computer, then print it to the screen in a format that is easier for people to read. Next, you will interpret a string to extract the date and time information. You will also translate the date and time values between two time zones, as well as add or subtract time values to determine the interval between two times. +在本教程中,你将创建一个 Go 程序来获取你的计算机的当前本地时间,然后以一种更容易让人阅读的格式将其打印到屏幕。接下来,你将从字符串中提取日期和时间信息。你还将转换两个时区之间的日期和时间值,以及加减时间值以确定两个时间之间的间隔。 -## Prerequisites +## 先决条件 -To follow this tutorial, you will need: +在阅读本教程前,你需要: -- Go version 1.16 or greater installed. To set this up, follow the [How To Install Go](https://www.digitalocean.com/community/tutorials/how-to-install-go-on-ubuntu-20-04) tutorial for your operating system. +- Go 版本 >= 1.16。你可以按照 [如何安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean) 来安装 Go。 -## Getting the Current Time +## 获取当前时间 -In this section, you will get the current time using Go’s `time` package. The `time` package in Go’s standard library provides a variety of date- and time-related functions, and can be used to represent a specific point in time using the `time.Time` type. In addition to a time and date, it can also hold information about the time zone the represented date and time are in. +在本节中,你将使用 Go 的 `time` 包获得当前时间。Go 标准库中的 `time` 包提供了各种与日期和时间有关的函数,并且可以使用 `time.Time` 类型来表示一个特定的时间点。除了时间和日期之外,它还可以保存时区信息。 -To begin creating a program to explore the `time` package, you’ll need to create a directory for the files. This directory can be created anywhere you’d like on your computer, but many developers tend to have a directory for their projects. In this tutorial, you’ll use a directory named `projects`. +在开始创建一个程序来探索 `time` 包前,你需要创建一个目录。这个目录可以在你的计算机上的任何地方创建,但许多开发者倾向于为他们的项目专门建立一个目录。在本教程中,你将使用一个名为 `projects` 的目录。 -Make the `projects` directory and navigate to it: +创建 `projects` 目录并切换至此目录: ```shell mkdir projects cd projects ``` -From the `projects` directory, run `mkdir` to create a `datetime` directory and then use `cd` to navigate to it: +在 `projects` 目录下,使用 `mkdir` 来创建一个 `datetime` 目录,然后使用 `cd` 命令进入此目录: ```shell mkdir datetime cd datetime ``` -Once you have your project directory created, open a `main.go` file using `nano`, or your preferred editor: +完成目录创建操作后,使用 `nano` 或你喜欢的文本编辑器创建一个名为 `main.go` 的文件: ```shell nano main.go ``` -In the `main.go` file, add a `main` function that will get the current time and print it out: +在 `main.go` 文件中,添加一个 `main` 函数以获取当前时间并打印出来: -projects/datetime/main.go +> projects/datetime/main.go ```go package main @@ -56,31 +56,32 @@ func main() { } ``` -In this program, the `time.Now` function from the `time` package is used to get the current local time as a `time.Time` value, and then stores it in the `currentTime` variable. Once it’s stored in the variable, the `fmt.Println` function prints `currentTime` to the screen using `time.Time`’s default string output format. +在这个程序中, `time` 包中的 `time.Now` 函数被用来获取当前本地时间作为 `time.Time` 值,然后将其存储在 `currentTime` 变量中。接着,`fmt.Println` 函数以 `time.Time` 类型的默认字符串输出格式把 `currentTime` 打印到屏幕上。 -Run the program using `go run` with the `main.go` file: +使用 `go run` 命令,以 `main.go` 为参数,运行程序: ```shell go run main.go ``` -The output showing your current date and time will look similar to this: +这将会输出你当前的日期和时间,看起来类似于这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 ``` -The output will show your current date and time, which will differ from the example. Additionally, the time zone you see (`-0500 CDT` in this output) will likely be different, since `time.Now()` returns the time in the local time zone. +输出将显示你当前的日期和时间,显示会与例子不同。此外,你看到的时区(本输出中的 `0500 CDT`)也可能会不同,因为 `time.Now()` 返回的是本地时区的时间。 -You may also notice an `m=` value in your output. This value is the [_monotonic clock_](https://pkg.go.dev/time#hdr-Monotonic_Clocks), and is used internally by Go when measuring differences in time. The monotonic clock is designed to compensate for any potential changes to the date and time of a computer’s system clock while a program is running. By using the monotonic clock, a `time.Now` value compared to a `time.Now` value five minutes later will still end up with the correct result (a five-minute interval), even if the system clock for the computer is changed an hour forward or backward during that five-minute interval. You don’t need to understand it thoroughly for the code or examples in this tutorial, but if you’d like to learn more about monotonic clocks and how Go uses them, the [Monotonic Clocks](https://pkg.go.dev/time#hdr-Monotonic_Clocks) section in the `time` package documentation provides additional details. +你可能还注意到输出中的 `m=` 值。这个值是 [_单调时钟(monotonic clock)_](https://pkg.go.dev/time#hdr-Monotonic_Clocks),被 Go 用来在内部计算时间差时使用。单调时钟的设计是为了保证程序运行时计算机系统时钟的日期和时间发生了任何潜在变化时,时间计算仍能正常运行。通过使用单调时钟,一个 `time.Now` 值与五分钟后的 `time.Now` 值相比,即使计算机的系统时钟在这五分钟的间隔内向前或向后改变了一个小时,最终仍会得出正确的结果(五分钟的间隔)。对于本教程中的代码或例子,你不需要彻底了解它,但如果你想了解更多关于单调时钟以及 Go 如何使用它们,`time` 包文档中的 [单调时钟](https://pkg.go.dev/time#hdr-Monotonic_Clocks) 部分提供了更多细节。 -Now, while you do have the current time displayed, it might not be useful for users. It may not be the format you’re looking for, or it may include more parts of the date or time than you want to display. +现在,虽然你确实显示了当前时间,但它对用户来说可能并不实用。它可能不是你期望的格式,或者它可能包含了比你想显示的更多的日期或时间部分。 -Fortunately, the `time.Time` type includes various methods to get specific parts of the date or time you want. For example, if you only wanted to know the year portion of the `currentTime` variable, you could use the `Year` method, or get the current hour using the `Hour` method. +幸运的是,`time.Time` 类型有着各种方法来获得你想要的日期或时间的特定部分。例如,如果你只想知道 `currentTime` 变量的年份部分,你可以使用 `Year` 方法,或者使用 `Hour` 方法获得当前小时。 -Open your `main.go` file again and add a few of the `time.Time` methods to your output to see what they produce: +再次打开 `main.go` 文件,在输出中添加一些 `time.Time` 的方法,看看发生了什么: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -98,16 +99,17 @@ func main() { } ``` -Next, run your program again using `go run`: +现在,使用 `go run` 再次运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 The year is 2021 The month is August The day is 15 @@ -116,11 +118,11 @@ The minute is 14 The second is 45 ``` -Like the previous output, your current date and time will be different from the example, but the format should be similar. This time in the output, you’ll see the full date and time printed as it was before, but you also have a list of the year, month, day of the month, hour, minute, and second. Note that instead of the month printing as a number (like it does in the full date), it appears as the English string `August`. This is because the `Month` method returns the month as a `time.Month` type instead of just a number, and that type is designed to print out the full English name when it’s printed as a `string`. +和前面的输出一样,你看到的输出中的日期和时间会和例子中的不同,但格式应该是相似的。在这次在输出中,你不但能看到完整的日期和时间被打印出来,就像以前一样,也能看到年、月、日、时、分、秒被分别打印出来。请注意,月份不是以数字的形式出现(就像在完整日期中那样),而是以英文字符串 `August` 的形式出现。这是因为 `Month` 方法将月份作为 `time.Month` 类型返回,而不仅仅是一个数字。同时,该类型被设计为,在以 `string` 的格式打印时输出完整的英文名称。 -Now, update the `main.go` file in your program again and replace the various function outputs with a single function call to `fmt.Printf`, so you can print the current date and time in a format that’s closer to what you may want to display to a user: +现在,再次更新程序中的 `main.go` 文件,使用 `fmt.Printf` 函数来格式化各种函数输出,这样你就可以用更接近你可能想要显示给用户的格式来打印当前日期和时间: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -139,32 +141,33 @@ func main() { } ``` -Once you’ve saved your updates to the `main.go` file, run it using the `go run` command as you did before: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626 2021-8-15 14:14:45 ``` -This time your output may be much closer to what you’d like, but there are still a few things that could be tweaked about the output. The month is now showing up in number format again, because the `fmt.Printf` format used `%d` to tell the `time.Month` type it should use a number and not a `string`, but it’s only showing up as a single digit. If you want to display two digits, you could change the `fmt.Printf` format to say that, but what if you also wanted to show a 12-hour time instead of a 24-hour time as shown in the output above? Using the `fmt.Printf` method, you would have to do your own math to calculate it. Printing dates and times using `fmt.Printf` is possible, but as you can see, it can eventually become cumbersome. Doing it this way, you could either end up with a large number of lines for each part you want to display, or you’d need to do a number of your own calculations to determine what to display. +这次的输出可能更接近于你想要的,但仍有一些可以调整的地方。月份现在又显示为数字格式了,因为 `fmt.Printf` 使用 `%d` 来告诉 `time.Month` 类型应该作为数字格式渲染而不是 `string`。但它只显示了一位数的月份,如果你想显示两位数,你可以改变 `fmt.Printf` 的格式化控制字符。但如果你还想显示12小时的时间而不是上面输出中显示的24小时的时间呢?使用 `fmt.Printf` 方法,你将不得不自己做数学运算。使用 `fmt.Printf` 打印日期和时间是可行的,但正如你所看到的,它最终会变得很麻烦。这样做,你可能需要为每个时间字段都要写大量的显示控制代码,甚至需要额外做一些数学运算。 -In this section, you created a new program to get the current time using `time.Now`. Once you had the current time, you then used various functions, such as `Year` and `Hour` on the `time.Time` type to print out information about the current time. It did start to become a lot of work to display it in a custom format, though. To make this type of common work easier, many programming languages, Go included, provide a special way to format dates and times, similar to how `fmt.Printf` can be used to format a string. +在本节中,你创建了一个新的程序,使用 `time.Now` 获得当前时间。一旦你得到了当前时间,就可以使用各种函数,如 `time.Time` 类型上的 `Year` 和 `Hour` 函数来打印出关于当前时间的信息。不过,以自定义的格式来显示确实很麻烦。为了使这种对时间的基本操作变得更容易,许多编程语言,包括 Go,都提供了一种特殊的方式来格式化日期和时间,类似于 `fmt.Printf` 可以用来格式化字符串。 -## Printing and Formatting Specific Dates +## 打印和格式化特定日期 -In addition to the `Year`, `Hour`, and other data-related methods the `time.Time` type provides, it also provides a method called `Format`. The `Format` method allows you to provide a layout `string`, similar to how you would provide `fmt.Printf` or `fmt.Sprintf` a format, that will tell the `Format` method how you’d like the date and time printed out. In this section, you will replicate the time output you added in the last section, but in a much more concise manner using the `Format` method. +除了 `time.Time` 类型提供的 `Year`、`Hour` 和其他与数据相关的方法外,它还提供了一个叫做 `Format` 的方法。`Format` 方法允许你提供一个格式化字符串,类似于你提供给 `fmt.Printf` 或 `fmt.Sprintf` 的格式那样,这将告诉 `Format` 方法你希望以什么样的格式打印出日期和时间。在本节中,你将复制在上一节中添加的时间输出,但使用 `Format` 方法来控制输出格式要简洁得多。 -Before you use the `Format` method, though, it will be easier to see how `Format` affects the output of a date and time if it’s not changing every time you run the program. Up until now, you were getting the current time using `time.Now`, so each time you ran it a different number would show up. The Go `time` package provides another useful function, the `time.Date` function, which will allow you to specify a specific date and time for the `time.Time` to represent. +在你使用 `Format` 方法之前,如果每次运行程序时 `Format` 不发生变化,就能更容易看到 `Format` 如何影响日期和时间的输出。到目前为止,你是用 `time.Now` 来获得当前时间的,所以每次运行都会显示不同的数字。Go的 `time` 包提供了另一个有用的函数,`time.Date` 函数,它允许你指定一个特定的日期和时间让 `time.Time` 来表示。 -To begin using `time.Date` instead of `time.Now` in your program, open the `main.go` file again and update it to replace `time.Now` with `time.Date`: +为了使用 `time.Date` 而不是 `time.Now` 来获取时间,请再次打开 `main.go` 文件并更新它: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -175,33 +178,34 @@ func main() { } ``` -The parameters to the `time.Date` function include the year, month, day of the month, hour, minute, and second of the date and time you want a `time.Time` for. The first of the last two parameters accounts for nanoseconds, and the last parameter is the time zone to create the time for. Using time zones themselves are covered later in this tutorial. +`time.Date` 函数的参数包括你想要的 `time.Time` 的日期和时间的年、月、日、时、分、秒。最后两个参数中的第一个是纳秒,最后一个参数是要创建的时间的时区。本教程将在后面介绍和使用时区。 -After saving your updates, run your program using `go run`: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT ``` -The output you see should now be much closer to the output above because you’re using a specific local date and time instead of the current one when you run the program. (Depending on the time zone your computer is set to, the time zone may show up differently, though.) The output format still looks similar to what you saw before because it’s still using the default `time.Time` format. Now that you have a standard date and time to work with, you can use it to start tweaking how the time is formatted when displayed using the `Format` method. +现在你看到的输出应该更接近上面的输出,因为你之前在运行程序时使用的是一个特定的本地日期和时间,而不是当前的日期和时间。(根据你计算机设置的时区的不同,时区可能会显示得不同)。输出格式看起来仍然与你之前看到的相似,因为它仍然使用默认的 `time.Time` 格式。现在你可以使用一个固定的日期和时间,并基于它在使用 `Format` 方法格式化时间时,来调整时间的显示格式。 -### Customizing Date Strings Using the `Format` Method +### 使用 `Format` 方法来自定义日期字符串 -Many other programming languages include a similar way to format dates and times to be displayed, but the way Go constructs the layout of those formats may be slightly different than what you’re used to if you’ve used them in other languages. In other languages, the date formatting uses a style similar to how `Printf` works in Go, with a `%` character followed by a letter representing the part of the date or time to insert. For example, a 4-digit year might be represented by `%Y`. In Go, though, these parts of a date or time are represented by characters that represent a specific date. To include a 4-digit year in a Go date format, you would actually include `2006` in the string itself. The benefit of this type of layout is that what you see in the code actually represents what you’ll see in the output. When you’re able to see a representation of your output, it makes it easier to double-check that your format matches what you’re looking for, and also makes it easier for other developers to understand the output of a program without running the program first. +许多其他编程语言有类似的方式来格式化日期和时间的显示,但如果你在其他语言中使用过这些格式,Go 的日期格式化方式可能与你习惯的方式略有不同。在其他语言中,日期格式化使用类似于 `Printf` 在 Go 中的格式化方式,在 `%` 字符后面有一个字母,代表要插入的日期或时间的部分。例如,一个4位数的年份可以用 `%Y` 表示。但在 Go 中,日期或时间的特定部分是由代表特定日期的字符来表示的。要在 Go 的日期格式中包含一个4位数的年份,你要在字符串本身中包含 `2006`。这种布局的好处是,你在代码中看到的东西真实代表了你将在输出中看到的东西。当你能够看到你的输出格式化字符串时,你也能更容易仔细检查你的格式是否符合你所期望的,也使其他开发者更容易理解程序的输出,而不需要先运行程序。 -The specific date Go uses for date and time layouts in string formatting is `01/02 03:04:05PM '06 -0700`. If you look at each component of the date and time, you’ll see that they increase by one for each part. The month comes first at `01`, followed by the day of the month at `02`, then the hour at `03`, the minute at `04`, the second at `05`, the year at `06` (or `2006`) and, finally, the time zone at `07`. Remembering this order will make it easier to create date and time formats in the future. Examples of the options available for formatting can also be found in Go’s [documentation of the `time` package](https://pkg.go.dev/time#pkg-constants). +Go 在字符串格式化中用于日期和时间布局的具体日期是 `01/02 03:04:05PM '06 -0700`。如果你看一下日期和时间的每个组成部分,你会发现每一部分都会加一。月 `01`、日 `02` 、时 `03`、分 `04`、秒 `05`、年 `06`(或`2006`),时区 `07`。记住这个顺序将使今后创建日期和时间格式更加容易。在 Go 的 [`time`包的文档](https://pkg.go.dev/time#pkg-constants) 中也可以找到格式化选项的详细例子。 -Now, use this new `Format` method to replicate and clean up the date format you printed in the last section. It required several lines and function calls to display, and using the `Format` method should make it much easier and cleaner to duplicate. +现在,使用这个新的 `Format` 方法来复制和清理你在上一节打印的日期格式。它需要几行函数调用来显示,使用 `Format` 方法应该可以使它更容易、简明地复制。 -Open the `main.go` file and add a new `fmt.Println` call and pass it `theTime` with a date formatted using the `Format` method: +打开 `main.go` 文件,使用 `Format` 方法格式化 `theTime` 这个日期,并用 `fmt.Println` 来输出格式化的结果: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -214,26 +218,27 @@ func main() { } ``` -If you look at the layout being used for the format, you’ll see it’s using the same time from above to specify how the date and time should be formatted (January 2, 2006). One thing to notice is that the hour uses `15` instead of `03` like the previous example. This shows that you’d like the hour displayed in 24-hour format instead of 12-hour format. +如果你看一下正在使用的格式布局,你会发现它使用了和上面相同的时间来指定日期和时间的格式(2006年1月2日)。有一点需要注意的是,小时使用的是 `15`,而不是像前面的例子中的 `03`。这表明你希望以24小时格式而不是12小时格式显示小时。 -To see the output from this new format, save your program and run it using `go run`: +保存程序并使用 `go run` 运行,以看看新的格式化方式的输出: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-8-15 14:30:45 ``` -The output you see now will be similar to the output from the end of the last section, but it was much simpler to accomplish. All you needed to include was a single line of code and a layout string. The `Format` function does the rest for you. +你现在看到的输出将类似于上一节末尾的输出,但它的代码实现要简单得多。你只要写一行代码和一个布局字符串,`Format` 函数为你完成了剩下的工作。 -Depending on the date or time you’re displaying, using a variable-length format like the one you ended up with when printing out numbers directly can potentially be hard to read for yourself, your users, or other code trying to read the value. Using `1` for the month format would result in March being displayed as `3`, while October would use two characters and show up as `10`. Now, open `main.go` and add an additional line to your program with another, more structured layout. In this layout, include a `0` prefix on the components and update the hour to use a 12-hour format: +取决于你要显示的日期或时间,使用变长格式打印数字(就像上一段程序那样),有可能对你自己、你的用户或其他试图读取数值的代码造成困难。使用 `1` 作为月份格式会导致三月显示为 `3` ,而十月会占用两个字符,显示为 `10` 。现在,打开 `main.go`,在你的程序中添加一行,加入另一个更加结构化的布局。在这个布局中,在组件上包括一个 `0` 前缀,并更新小时,使用12小时格式: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -247,35 +252,36 @@ func main() { } ``` -After saving your code, run the program again using `go run`: +保存并运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-8-15 14:30:45 2021-08-15 02:30:45 pm ``` -You’ll see that by adding a `0` prefix to the numbers in the layout string, the `8` for the month in the new output becomes `08` to match the layout. The hour, which is now in the 12-hour format, also has its own `0` prefix. In the end, though, what you end up seeing in the output mirrors the format you see in the code, so it’s easier to tweak the format if you need to. +你会看到,通过给布局字符串中的数字添加 `0` 前缀,新的输出中的月份的 `8` 变成了 `08` 以匹配布局。小时,现在是12小时的格式,也有自己的 `0` 前缀。不过,最终你在输出中看到的东西反映了你在代码中看到的格式,所以如果你需要,很容易调整格式化布局字符串。 -Many times, formatted dates are intended to be interpreted by other programs, and it can become a burden to re-create those formats every time you want to use them. In some cases, you can use a pre-defined format. +很多时候,格式化的日期是要被其他程序读取并解析的,每次你想使用它们时,重新创建这些格式会成为一种负担。在某些情况下,你可以使用一个预定义的格式。 -### Using a Pre-Defined Format +### 使用预定义格式 -There are many commonly used date formats, such as timestamps for log messages, and re-creating those every time you want to use them could end up being a hassle. For some of these cases, the `time` package includes pre-defined formats you can use. +有许多常用的日期格式,如日志信息的时间戳,每次你想使用它们时,重新创建这些格式可能会麻烦。对于其中的一些情况,`time` 包涵盖了你可以使用的预定义格式。 -One of the formats available and used often is the one defined in [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339). An _[RFC](https://en.wikipedia.org/wiki/Request_for_Comments)_ is a document used to define how standards on the internet work, and other RFCs can then build upon each other. There is an RFC that defines how HTTP works ([RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616)), for example, and others that build on top of that to further define HTTP. So, in the case of RFC 3339, the RFC defines a standard format to use for timestamps on the internet. The format is well-known and well-supported around the internet, so chances of seeing it elsewhere are high. +其中一个常用的格式由 [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339) 定义。[_RFC_](https://en.wikipedia.org/wiki/Request_for_Comments) 是一份用来定义互联网上的标准如何工作的文件,然后其他 RFC 可以在此基础上发展。例如,有一个 RFC 定义了 HTTP 的工作方式([RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616)),而其他 RFC 则在此基础上进一步定义 HTTP。因此,就 RFC 3339 而言,该 RFC 定义了一种用于互联网上的时间戳的标准格式。这种格式在互联网上是众所周知的,并且得到了广泛的支持,所以能在很多地方看到它。 -Each of the pre-defined time formats in the `time` package are represented by a `const string` named after the format they represent. The RFC 3339 format happens to have two formats available, one called `time.RFC3339` and another called `time.RFC3339Nano`. The difference between the formats is that the `time.RFC3339Nano` version includes nanoseconds in the format. +在 `time` 包中的每个预定义的时间格式都由一个 `const string` 表示,并以它们所代表的格式命名。RFC3339 刚好有两种格式,一种叫做 `time.RFC3339`,另一种叫做 `time.RFC3339Nano` 。这两种格式的区别在于,`time.RFC3339Nano` 版本的格式中包括纳秒。 -Now, open your `main.go` file again and update your program to use the `time.RFC3339Nano` format for its output: +打开 `main.go` 文件,更新程序,使其使用 `time.RFC3339Nano` 格式输出时间: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -288,32 +294,33 @@ func main() { } ``` -Since the pre-defined formats are `string` values with the desired format, you just need to replace the format you’d usually use with one of them. +由于预定义的格式是所期望的时间格式的 `string` 值,所以你只需要选取其中一种 `RFC3339` 格式来替换之前的格式化字符串就好。 -To see the output, run your program with `go run` again: +再次运行程序以查看输出: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-08-15T14:30:45.0000001-05:00 ``` -The RFC 3339 format is good to use if you need to save a time value as a `string` somewhere. It can be read by many other programming languages and applications, and is about as compact as a date and time can be in a flexible `string` format. +如果你需要把时间值保存为一个 `string` ,RFC 3339格式是很好的选择。它可以被许多其他的编程语言和应用程序读取,`string` 格式很灵活,又与日期和时间一样紧凑。 -In this section, you updated your program to use the `Format` method to print out a date and time. Using this flexible layout also allowed your code to look similar to the final output. Lastly, you used one of the pre-defined layout strings to print out a date and time using a well-supported format. In the next section, you’ll update your program to convert that same `string` value back into a `time.Time` value that you can work with. +在这一节中,你更新了程序,使用 `Format` 方法来打印出一个日期和时间。使用这种灵活的布局也使你的代码看起来与最终的输出相似。最后,你使用了一个预定义的布局字符串来打印出一个日期和时间,并使用了一个被广泛应用的格式。在下一节中,你将继续更新程序,将同样的 `string` 值转换回 `time.Time` 值。 -## Parsing Dates and Times in Strings +## 从字符串中解析出日期和时间 -Often when developing applications, you’ll come across dates represented as `string` values that you’ll need to interpret in some way. Sometimes, you’ll need to know the date portion of the value, other times you might need to know the time portion, and yet others you might need the whole value. In addition to using the `Format` method to create `string` values from `time.Time` values, the Go `time` package provides a `time.Parse` function to convert a `string` into a `time.Time` value. The `time.Parse` function works similar to the `Format` method, in that it takes the expected date and time layout as well as the `string` value as parameters. +在开发应用程序时,你经常会遇到以 `string` 形式表示的日期,你需要以某种方式来读取并解析它。有时,你需要知道该值的日期部分,有时你可能需要知道时间部分,还有些时候,你可能需要整个值。除了使用 `Format` 方法根据 `time.Time` 值创建 `string` 值外,Go 的 `time` 包还提供了一个 `time.Parse` 函数来将 `string` 转换成 `time.Time` 值。`time.Parse` 函数的工作原理与 `Format` 方法类似,它有两个参数,一个是预期的日期和时间布局,另一个是待解析的日期和时间的 `string` 值。 -Now, open the `main.go` file in your program and update it to use the `time.Parse` function to parse a `timeString` into a `time.Time` variable: +现在,打开你程序中的 `main.go` 文件,更新它以使用 `time.Parse` 函数来把 `timeString` 解析到 `time.Time` 变量: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -330,26 +337,31 @@ func main() { } ``` -Unlike the `Format` method, the `time.Parse` method also returns a potential `error` value in case the `string` value passed in does not match the layout provided as the first parameter. If you look at the layout used, you’ll see that the layout given to `time.Parse` uses the same `1` for month, `2` for day of month, etc, that is used in the `Format` method. +与 `Format` 方法不同,`time.Parse` 方法还会返回一个潜在的 `error` 值,以防传入的 `string` 值与提供的布局(第一个参数)不符。如果你看一下使用的布局,你会发现传给 `time.Parse` 的布局使用了与 `Format` 方法中相同的布局:使用 `1` 表示月,`2` 表示日,等等。 -After saving your updates, you can run your updated program using `go run`: +保存、更新,并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 02:30:45 +0000 UTC +Output +The time is 2021-08-15 02:30:45 +0000 UTC 2021-08-15T02:30:45Z ``` -There are a couple of things to notice in this output. One is that the time zone being parsed from `timeString` is using the default time zone, which is a `+0` offset and known as [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) (UTC). Since neither the time value nor the layout includes the time zone, the `time.Parse` function doesn’t know which time zone to associate it with. If you need it at some point, though, the `time` package does include a [`time.ParseInLocation`](https://pkg.go.dev/time#ParseInLocation) function so you can provide the location to use. The other part to notice is in the RFC 3339 output. The output is using the `time.RFC3339Nano` layout, but the output doesn’t include any nanoseconds. This is because the `time.Parse` function is not parsing any nanoseconds, so the value is set to the default of `0`. When nanoseconds are `0`, the `time.RFC3339Nano` format will not include nanoseconds in the output. +在这次的输出中,有这么几个地方需要注意: -The `time.Parse` method can also use any of the pre-defined time layouts provided in the `time` package when parsing a `string` value. To see this in practice, open your `main.go` file and update the `timeString` value to match the output from `time.RFC3339Nano` earlier, and update the `time.Parse` parameter to match: +一个是,解析 `timeString` 时使用了默认的时区,它是一个 `+0` 的偏移量,被称为[协调世界时](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) (UTC)。由于时间值和布局都不包括时区,所以 `time.Parse` 函数不知道该把它与哪个时区联系起来。不过,如果你在某些时候需要它,`time` 包确实包含了一个 [`time.ParseInLocation`](https://pkg.go.dev/time#ParseInLocation) 函数,所以你可以提供期望的时区。 -projects/datetime/main.go +另一个需要注意的地方是,在 RFC 3339的输出中,输出使用 `time.RFC3339Nano` 布局,但输出不包括任何纳秒。这是因为 `time.Parse` 函数没有解析出任何纳秒信息,所以该值被设置为默认的 `0`。当纳秒为 `0` 时,`time.RFC3339Nano` 格式将不在输出中显示纳秒。 + +`time.Parse` 方法在解析 `string` 值时也可以使用 `time` 包中提供的任何预定义的时间布局。要想在实操中看到这一点,请打开 `main.go` 文件,更新 `timeString` 值,使之与前面 `time.RFC3339Nano` 的输出相匹配,并更新 `time.Parse` 参数,使之相匹配: + +> projects/datetime/main.go ```go ... @@ -366,36 +378,37 @@ func main() { } ``` -Once you have the code updated, you can save your program and run it again using `go run`: +更新、保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-08-15T14:30:45.0000001-05:00 ``` -This time, the output from the `Format` method shows that `time.Parse` was able to parse both the time zone and the nanoseconds from `timeString`. +这一次,`Format ` 方法的输出显示,`time.Parse` 能够从 `timeString` 中解析出时区和纳秒。 -In this section, you used the `time.Parse` function to parse an arbitrarily formatted date and time string value as well as a pre-defined layout. Thus far, though, you haven’t interacted with the various time zones you’ve seen. Another set of features Go provides for the `time.Time` type is the ability to convert between different time zones, as you’ll see in the next section. +在本节中,你用 `time.Parse` 函数解析了一个任意方式格式化的日期和时间字符串值,以及一个预定义的布局。不过到目前为止,你还没有与你所看到的各种时区进行交互。Go 为 `time.Time` 类型提供的另一组特性是能够在不同的时区之间进行转换,你将在下一节中看到这一点。 -## Working with Time Zones +## 处理时区 -When developing an application with users across the world, or even across only a few time zones, a common practice is to store dates and times using [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) (UTC) and then convert to a user’s local time when required. This allows the data to be stored in a consistent format and makes calculations between them easier, since conversion is needed only when displaying the date and time to a user. +当开发一个有着来自世界各地、或只有几个时区的用户的应用程序时,一个常见的做法是使用 [协调世界时](https://en.wikipedia.org/wiki/Coordinated_Universal_Time)(UTC) 来存储日期和时间,然后在需要时转换为用户的当地时间。这能让数据以一致的格式存储,并使它们之间的计算更容易,因为只有在向用户显示日期和时间时才需要转换。 -In earlier sections of this tutorial, you created a program that worked primarily on times based in your own local time zone. To save your `time.Time` values as UTC, you’ll first need to convert them to UTC. You will do this using the `UTC` method, which will return a copy of the time you call it on, but in UTC. +在本教程的前几节中,你创建了一个基于你自己的本地时区的时间的程序。如果要把你的 `time.Time` 值以 UTC 格式保存,你首先需要把它们转换为 UTC。你可以用 `UTC` 方法来完成这一转换,该方法将返回你所调用的时间的 UTC 格式的副本。 -**Note:** This section converts times between your computer’s local time zone and UTC. If the computer you’re using has the local time zone set to one that matches UTC, you’ll notice that converting between UTC and back again won’t show a difference in the times. +**注意:**本节在你计算机的本地时区和 UTC 之间进行时间转换。如果你使用的计算机的本地时区设置与 UTC 一致,你会发现在 UTC 之间转换与转回不会显示出差别。 -Now, open your `main.go` file to update your program to use the `UTC` method on `theTime` to return the UTC version of the time: +现在,打开 `main.go` 文件,更新程序,使用 `theTime` 的 `UTC` 方法来返回 UTC 版本的时间: -projects/datetime/main.go +> projects/datetime/main.go -```shell +```go ... func main() { @@ -409,28 +422,29 @@ func main() { } ``` -This time your program creates `theTime` as a `time.Time` value in your local time zone, prints it out in two different formats, then uses the `UTC` method to translate that time from your local time to UTC time. +这次程序在你的本地时区创建 `time.Time` 类型的时间值 `theTime` ,并以两种不同的格式打印出来,然后使用 `UTC` 方法把该时间从你的本地时间转换为 UTC 时间。 -To see the output from the program, you can run it using `go run`: +再次运行程序并看看输出: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-08-15T14:30:45.0000001-05:00 The UTC time is 2021-08-15 19:30:45.0000001 +0000 UTC 2021-08-15T19:30:45.0000001Z ``` -Your output will vary depending on your local time zone, but in the output above, you’ll see that the first time that’s printed out is in `CDT` (North American Central Daylight Time), which is `-5` hours from UTC. Once the `UTC` method is called and the UTC time prints, you can see the hour in the time goes from `14` to `19`, because converting the time to UTC added five hours. +你的输出会因你当地的时区而不同,但在上面的输出中,你会看到第一个打印出来的时间是 `CDT`(北美中部夏令时),与UTC相比是 `-5` 小时。一旦调用 `UTC` 方法,打印出 UTC 时间,你可以看到时间中的小时数从 `14` 变成了 `19`,因为把时间转换为 UTC 增加了 5 个小时。 -It’s also possible to convert a UTC time back to a local time using the `Local` method on a `time.Time` in the same way. Open your `main.go` file again and update it to add a call to the `Local` method on `utcTime` to convert it back to your local time zone: +也可以用同样的方法在 `time.Time` 上调用 `Local` 方法把 UTC 时间转换回本地时间。再次打开 `main.go` 文件,更新它,在 `utcTime` 上 添加对 `Local` 方法的调用,将其转换回你的本地时区: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -450,16 +464,17 @@ func main() { } ``` -Once your file is saved, run your program using `go run`: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 -0500 CDT +Output +The time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-08-15T14:30:45.0000001-05:00 The UTC time is 2021-08-15 19:30:45.0000001 +0000 UTC 2021-08-15T19:30:45.0000001Z @@ -467,21 +482,21 @@ The Local time is 2021-08-15 14:30:45.0000001 -0500 CDT 2021-08-15T14:30:45.0000001-05:00 ``` -You’ll see that the UTC time has been converted back to your local time zone. In the output above, the UTC conversion back to CDT means that five hours has been subtracted from the UTC, changing the hour from `19` to `14`. +你会看到,UTC 时间已经被转换回你的本地时区。在上面的输出中,UTC 转换回 CDT 意味着从 UTC 中减去了5个小时,将小时数从 `19` 变为 `14`。 -In this section, you updated your program to convert dates and times between your local time zone and the standard UTC time zone using the `UTC` method, then back again using the `Local` method. +在这一节中,你更新了程序,使用 `UTC` 方法在本地时区和标准 UTC 时区之间进行日期和时间的转换,然后使用 `Local` 方法再次转换回来。 -Once you have your date and time values available, there are a number of additional features the Go `time` package provides that can be useful in your applications. One of these features is determining whether a given time is before or after another. +一旦你有了日期和时间值,Go `time` 包提供了一些额外的特性,可以在你的应用程序中发挥作用。其中一个特性是判断一个给定的时间是在另一个时间之前还是之后。 -## Comparing Two Times +## 比较两个时间 -Comparing two dates with each other can be deceptively difficult at times, due to all the variables that need to be considered when comparing them. For example, needing to take time zones into account, or the fact that months have a different number of days from each other. The `time` package provides a few methods to make this easier. +比较两个时间,有时看似很困难,因为在比较时要考虑内部的所有变量。传说,需要考虑到时区,或者每个月都有不同的天数。不过,`time` 包提供了一些方法来让比较变得更简单。 -The `time` package provides two methods to make these comparisons easier: the `Before` and `After` methods, available on the `time.Time` type. These methods both accept a single time value and return either `true` or `false` depending on whether the time they’re being called on is before or after the time being provided. +`time` 包提供了两种方法使时间比较更加容易:`Before` 和 `After` 方法,可用于 `time.Time` 类型。这些方法都接受一个单一的时间值,并根据调用者的时间是在所提供的时间(函数参数)之前还是之后,返回 `true` 或 `false`。 -To see an example of these functions in action, open your `main.go` file and update it to include two different dates, then use `Before` and `After` to compare those dates to see the output: +要看这些函数的实际效果,请打开你的 `main.go` 文件,并更新它以创建两个不同的日期,然后使用 `Before` 和 `After` 来比较这些日期,看看输出: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -501,16 +516,17 @@ func main() { } ``` -Once you have your file updated, save it and run it using `go run`: +更新并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe first time is 2021-08-15 14:30:45.0000001 +0000 UTC +Output +The first time is 2021-08-15 14:30:45.0000001 +0000 UTC The second time is 2021-12-25 16:40:55.0000002 +0000 UTC First time before second? true First time after second? false @@ -518,13 +534,13 @@ Second time before first? false Second time after first? true ``` -Since the code is using explicit dates in the UTC time zone, your output should match the output above. You’ll see that when using the `Before` method on `firstTime` and providing it `secondTime` to compare to, it’s saying that it’s `true` that `2021-08-15` is before `2021-12-25`. When using the `After` method of `firstTime` and providing `secondTime`, it says it’s `false` that `2021-08-15` is after `2021-12-25`. Changing the order to call the methods on `secondTime` shows the opposite results, as expected. +因为代码使用的是 UTC 时区的确定的日期,所以你看到的输出应该与上面的输出一致。你会看到,当 `firstTime` 调用 `Before` 方法并与 `secondTime` 比较时,它说 `2021-08-15` 在 `2021-12-25` 之前是 `true`。当 `firstTime` 调用 `After`方法并与 `secondTime` 进行比较时,它说 `2021-08-15` 在 `2021-12-25` 之后是 `false`。改变顺序来调用 `secondTime` 上的方法,显示出相反的结果,正如预期。 -Another way to compare two dates and times with the `time` package is the `Sub` method. The `Sub` method will subtract one date from another and return a value using a new type, the `time.Duration`. Unlike a `time.Time` value, which represents an absolute point in time, a `time.Duration` value represents a difference in time. For example, “in one hour” would be a duration because it means something different based on the current time of day, but “at noon” represents a specific, absolute time. Go uses the `time.Duration` type in a number of places, such as when you’d want to define how long a function should wait before returning an error or here, where you need to know how much time is between one time and another. +用 `time` 包比较两个日期和时间的另一种方式是 `Sub` 方法。`Sub` 方法用一个日期减去另一个日期,并使用一个新的类型 `time.Duration` 返回一个值。与代表绝对时间点的 `time.Time` 值不同,`time.Duration` 值代表了时间间隔。例如,"一小时内" 是一个时间间隔,因为它会因当前时间的不同而表达出不同的意思,但 "中午" 代表一个特定的、绝对的时间。Go 在很多地方使用 `time.Duration` 类型,比如你想定义一个函数在返回错误之前应该等待多长时间,或者在本例中,你需要知道一个时间和另一个时间之间有多久。 -Now, update your `main.go` file to use the `Sub` method on the `firstTime` and `secondTime` values and print out the results: +现在,更新你的 `main.go` 文件,对 `firstTime` 和 `secondTime` 值使用 `Sub` 方法并打印出结果: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -540,30 +556,31 @@ func main() { fmt.Println("Duration between second and first time is", secondTime.Sub(firstTime)) ``` -Save your file and then run it using `go run`: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe first time is 2021-08-15 14:30:45.0000001 +0000 UTC +Output +The first time is 2021-08-15 14:30:45.0000001 +0000 UTC The second time is 2021-12-25 16:40:55.0000002 +0000 UTC Duration between first and second time is -3170h10m10.0000001s Duration between second and first time is 3170h10m10.0000001s ``` -The output above says there are 3,170 hours, 10 minutes, 10 seconds, and 100 nanoseconds between the two dates, and there are a few things to note about the output. The first is that the duration between the first and second times is a negative value. This says the second time is after the first, and would be similar to if you subtracted `5` from `0` and got `-5`. The second thing to note is that a duration’s largest unit of measure is an hour, so it doesn’t break it down into days or months. Since the number of days in a month is not consistent and a “day” could have a different meaning during a switch for daylight saving time, an hour is the most accurate measurement that wouldn’t fluctuate. +上面的输出说两个日期之间有3170小时、10分钟、10秒和100纳秒,输出中有几个需要注意的地方:首先是第一个和第二个时间之间的时间间隔是一个负值。这说明第二个时间在第一个时间之后,类似于 `0` 减 `5` 得到 `-5`。第二是,时间间隔的最大计量单位是小时,所以它不会分解成天或月。由于每个月的天数并不一致,而且 "日" 在夏令时的转换中可能有不同的含义,一小时是最准确的度量,不会有波动。 -In this section, you updated your program to compare two times using three different methods. First, you used the `Before` and `After` methods to determine if a time is before or after another time, then you used `Sub` to see how long it is between two times. Getting the duration between two times isn’t the only way the `time` package uses `time.Duration`, though. You are also able to use it to add or remove time from a `time.Time` value, as you’ll see in the next section. +在本节中,你更新了你的程序,用三种不同的方法比较两个时间。首先,你使用 `Before` 和 `After` 方法来判断一个时间是在另一个时间之前还是之后,然后你使用 `Sub` 来计算两个时间之间有多长。不过,获取两个时间之间的时间间隔并不是 `time` 包使用 `time.Duration` 的唯一方式。你还可以用它来增加或减少 `time.Time` 值中的时间,我们将在下一小节中学到。 -## Adding or Subtracting Times +## 增减时间 -When writing an application, one common operation using dates and times is determining a past or future time based on another time. It could be used for functionality, such as determining when a subscription will come up for renewal next, or if it’s been a certain amount of time since some value was checked. Either way, the Go `time` package provides a way to handle it. To find another date based on a date you already know, though, you’ll need to be able to define your own `time.Duration` values. +在编写应用程序时,使用日期和时间的一个常见操作是根据某个时间来生成一个过去或未来的时间。它可以用来实现一些功能,比如确定一个订阅的下一次续订时间,或者确定自某些值被检查以来已经过了一定的时间。无论是哪种方式,Go 的 `time` 包都提供了相应的处理方式。不过,要根据你已经知道的日期找到另一个日期,你需要能够自行创建 `time.Duration` 值。 -Creating a `time.Duration` value is similar to how you might write a duration on paper, just with multiplication for the unit of time. For example, to create a `time.Duration` to represent an hour, you would define it using the `time.Hour` value multiplied by the number of hours you want the `time.Duration` to represent: +创建 `time.Duration` 值类似于你在纸上写下一个时间间隔,只是乘了个时间单位。例如,要创建一个代表若干小时的 `time.Duration`,你可以用 `time.Hour` 值乘你希望的小时数: ```go oneHour := 1 * time.Hour @@ -571,16 +588,16 @@ twoHours := 2 * time.Hour tenHours := 10 * time.Hour ``` -Declaring smaller time units is handled in a similar way, except using `time.Minute`, `time.Second`, and so on: +声明更小的时间单位的方式也类似,例如使用 `time.Minute`, `time.Second` 等: ```go tenMinutes := 10 * time.Minute fiveSeconds := 5 * time.Second ``` -One duration can also be added to another duration to get the sum of the durations. To see this in action, open your `main.go` file and update it to declare a `toAdd` duration variable and add different durations to it: +一个时间间隔也可以加到另一个时间间隔上,得到时间间隔的总和。想看看实际效果的话,请打开你的 `main.go` 文件,并更新它,声明一个 `toAdd` 时间间隔变量,并向它添加不同的时间间隔: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -597,30 +614,33 @@ func main() { } ``` -Once you have your updates finished, save the file and run your program using `go run`: +更新、保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Output1: 1h0m0s +Output +1: 1h0m0s 2: 1h1m0s 3: 1h1m1s ``` -When you look at the output, you’ll se the first duration printed is for one hour, corresponding to the `1 * time.Hour` in your code. Next, you added `1 * time.Minute` to the `toAdd` value, which shows up as the one hour, one minute value. Finally, you added `1 * time.Second` to `toAdd`, which resulted in a `time.Duration` value of one hour, one minute and one second. +看看输出,你会发现打印的第一个时间间隔是1小时,对应于你代码中的 `1 * time.Hour` 。接下来,你把 `1 * time.Minute` 添加到 `toAdd` 值中,显示为1小时1分钟的值。最后,你把 `1 * time.Second` 加到 `toAdd` 中,结果为 `time.Duration` 值是1小时1分1秒钟。 -It’s also possible to combine adding the durations together in one statement, or subtracting a duration from another: +也可以在一条语句中把持续时间加在一起,或者把一个持续时间从另一个持续时间中减去,都是可以的。 ```go oneHourOneMinute := 1 * time.Hour + 1 * time.Minute tenMinutes := 1 * time.Hour - 50 * time.Minute ``` -Next, open your `main.go` file and update your program to use a combination like this to subtract one minute and one second from `toAdd`: +接下来,打开你的 `main.go` 文件,更新程序,使用这样的组合,从 `toAdd` 中减去1分1秒: + +> projects/datetime/main.go ```go ... @@ -637,28 +657,27 @@ func main() { } ``` -projects/datetime/main.go - -After saving your code, you can run the program using `go run`: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -Output1: 1h0m0s +Output +1: 1h0m0s 2: 1h1m0s 3: 1h1m1s 4: 1h0m0s ``` -The new fourth line added to the output shows the new code you included to subtract the sum of `1 * time.Minute` and `1 * time.Second` worked correctly, resulting in the original one-hour value again. +在输出中新增加的第四行显示,你在新代码的末尾减去了 `1 * time.Minute` 和 `1 * time.Second` 的总和,他们如期运行,最后又变回了原来的一小时零分零秒。 -Using these durations paired with the `Add` method of the `time.Time` type allows you to take a time value you already have and determine the time at some other arbitrary point before or after that time. To see an example of this running, open your `main.go` file and update it to set the `toAdd` value to 24 hours, or `24 * time.Hour`. Then, use the `Add` method on a `time.Time` value to see what the time would be 24 hours after that point: +使用这些时间间隔并配合 `time.Time` 类型的 `Add` 方法,可以使你基于一个已有的时间值,获取该时间之前或之后的其他任意的时间点。想看实际例子的话,请打开你的 `main.go` 文件并更新它,将 `toAdd` 值设置为24小时,或 `24 * time.Hour`。然后,在 `time.Time` 值上使用 `Add` 方法,看看24小时之后的时间是多少: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -675,27 +694,28 @@ func main() { } ``` -After saving your file, run your program using `go run`: +保存并运行程序: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 +0000 UTC +Output +The time is 2021-08-15 14:30:45.0000001 +0000 UTC Adding 24h0m0s The new time is 2021-08-16 14:30:45.0000001 +0000 UTC ``` -Looking at the output, you’ll see that adding 24 hours to `2021-08-15` results in the new date of `2021-08-16`. +看一下输出结果,你会发现在 `2021-08-15` 的基础上加上24小时,得到的新日期是 `2021-08-16`。 -To subtract time, you would also use the `Add` method, which is slightly counter-intuitive. Since the `Sub` method is used to get the difference in time between two dates, you subtract time from the `time.Time` value by using `Add` with a negative value. +要减去时间,你也需要使用 `Add` 方法,这稍微有点违反直觉。因为 `Sub` 方法是用来获取两个日期之间的时间差的,你通过使用 `Add` 方法,传入负的时间间隔,以减去时间。 -Once again, to see this running in your program, open your `main.go` file and update it to change the 24-hour `toAdd` value to a negative value: +打开 `main.go` 并更新它,将24小时的 `toAdd` 值改为负值: -projects/datetime/main.go +> projects/datetime/main.go ```go ... @@ -712,28 +732,32 @@ func main() { } ``` -After saving your file, run your program one more time using `go run`: +保存并运行: ```shell go run main.go ``` -The output will look similar to this: +输出看起来像这样: ``` -OutputThe time is 2021-08-15 14:30:45.0000001 +0000 UTC +Output +The time is 2021-08-15 14:30:45.0000001 +0000 UTC Adding -24h0m0s The new time is 2021-08-14 14:30:45.0000001 +0000 UTC ``` -This time in the output you’ll see that instead of adding 24 hours to the original time, the new date is 24 hours before the original time. +这次你会在输出中看到,新的日期不是在原来的时间上增加24小时,而是在原来的时间上减去24小时。 -In this section, you used `time.Hour`, `time.Minute`, and `time.Second` to create `time.Duration` values of varying degrees. You also used the `time.Duration` values with the `Add` method to get a new `time.Time` value both before and after the original value. By combining `time.Now`, the `Add` method, and `Before` or `After` methods, you’ll have access to powerful date and time functionality for your applications. +在这一节中,你用 `time.Hour` , `time.Minute` , 和 `time.Second` 来创建不同跨度的 `time.Duration` 值。你还使用 `time.Duration`值与 `Add` 方法来获得在原始值之前和之后的新的 `time.Time` 值。通过结合 `time.Now`、`Add` 方法和 `Before` 或 `After` 方法,你还可以让你的应用程序获得强大的日期和时间功能。 -## Conclusion +## 结论 -In this tutorial, you used `time.Now` to retrieve a `time.Time` value for the current local time on your computer, then used the `Year`, `Month`, `Hour`, and other methods to access specific information about that time. Then, you used the `Format` method to print the time using a custom format and a pre-defined format. Next, you used the `time.Parse` function to interpret a `string` value with a time in it and extract the time value from it. Once you had the time value, you used the `UTC` and `Local` methods to translate the time between UTC and your local time zone. After that, you used the `Sub` method to get the duration between two different times before using the `Add` method to find the time relative to a different time value. +* 在本教程中,你使用 `time.Now` 检索计算机上当前本地时间的 `time.Time` 值,然后使用 `Year`、`Month`、`Hour` 和其他方法来访问该时间的具体信息。 -Using the various functions and methods described in this tutorial will go a long way in your applications, but the Go [`time`](https://pkg.go.dev/time) package also includes a number of other features if you’re interested. +* 然后,你使用 `Format` 方法,使用自定义的格式和预定义的格式打印时间。 +* 接下来,你使用了 `time.Parse` 函数来解析一个包含时间的 `string` 值,并从中提取时间值。 +* 一旦你有了时间值,你就可以使用 `UTC` 和 `Local` 方法在 UTC 和你的本地时区之间进行时间转换。 +* 最后,你使用 `Sub` 方法来获得两个不同时间之间的时间间隔,然后使用 `Add` 方法来找到相对于不同时间值的时间点。 -This tutorial is also part of the [DigitalOcean](https://www.digitalocean.com/) [How to Code in Go](https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go) series. The series covers a number of Go topics, from installing Go for the first time to how to use the language itself. +使用本教程中描述的各种函数和方法对你的应用程序很有帮助,如果你有兴趣,也可以看看 Go [`time`](https://pkg.go.dev/time) 包的一些其他特性。 diff --git a/content/zh/menu/index.md b/content/zh/menu/index.md index b057776..467ec1f 100644 --- a/content/zh/menu/index.md +++ b/content/zh/menu/index.md @@ -46,7 +46,9 @@ headless: true - [41. 如何使用 Go 模块]({{< relref "/docs/41-How_to_Use_Go_Modules.md" >}}) - [42. 如何分发 Go 模块]({{< relref "/docs/42-How_to_Distribute_Go_Modules.md" >}}) - [43. 如何在自己的项目中使用私有的Go模块]({{< relref "/docs/43-How_to_Use_a_Private_Go_Module_in_Your_Own_Project.md" >}}) - + - [44. 如何在 Go 中并发运行多个函数]({{< relref "/docs/44-How_To_Run_Multiple_Functions_Concurrently_in_Go.md" >}}) + - [45. 如何在 Go 中给错误添加额外信息]({{< relref "/docs/45-How_To_Add_Extra_Information_to_Errors_in_Go.md" >}}) + - [46. 如何在 Go 中使用日期和时间]({{< relref "/docs/46-How_To_Use_Dates_and_Times_in_Go.md" >}}) - **附录:资源** - [**Fork on Github**](https://github.com/gocn/How-To-Code-in-Go)