├── .gitignore
├── .gitmodules
├── archetypes
└── default.md
├── resources
└── _gen
│ └── assets
│ └── scss
│ └── book.scss_50fc8c04e12a2f59027287995557ceff.json
├── .github
└── workflows
│ ├── contributors.yml
│ └── deploy.yml
├── Makefile
├── config.yml
├── static
└── README.md
├── content
└── zh
│ ├── _index.md
│ ├── docs
│ ├── 05-Understanding_the_GOPATH.md
│ ├── 08-An_Introduction_to_Working_with_Strings_in_Go.md
│ ├── 09-How_To_Format_Strings_in_Go.md
│ ├── 26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md
│ ├── 33-Defining_Structs_in_Go.md
│ ├── 10-An_Introduction_to_the_Strings_Package_in_Go.md
│ ├── 06-How_To_Write_Comments_in_Go.md
│ ├── 20-Importing_Packages_in_Go_DigitalOcean.md
│ ├── 04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md
│ ├── 18-Creating_Custom_Errors_in_Go_DigitalOcean.md
│ ├── 14-Understanding_Boolean_Logic_in_Go.md
│ ├── 39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md
│ ├── 35-How_To_Build_and_Install_Go_Programs.md
│ ├── 24-How_To_Write_Switch_Statements_in_Go.md
│ ├── 13-How_To_Do_Math_in_Go_with_Operators.md
│ ├── 27-How_To_Define_and_Call_Functions_in_Go.md
│ ├── 21-How_To_Write_Packages_in_Go.md
│ ├── 01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md
│ ├── 36-How_To_Use_Struct_Tags_in_Go.md
│ ├── 28-How_To_Use_Variadic_Functions_in_Go.md
│ ├── 23-How_To_Write_Conditional_Statements_in_Go.md
│ ├── 29-Understanding_defer_in_Go.md
│ ├── 40-How_To_Use_the_Flag_Package_in_Go.md
│ ├── 17-Handling_Errors_in_Go_DigitalOcean.md
│ ├── 19-Handling_Panics_in_Go _DigitalOcean.md
│ ├── 34-Defining_Methods_in_Go.md
│ ├── 31-Customizing_Go_Binaries_with_Build_Tags.md
│ └── 15-Understanding_Maps_in_Go.md
│ └── menu
│ └── index.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .hugo_build.lock
3 | public/
4 | .vscode
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "themes/hugo-book"]
2 | path = themes/hugo-book
3 | url = https://github.com/alex-shpak/hugo-book
4 |
--------------------------------------------------------------------------------
/archetypes/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "{{ replace .Name "-" " " | title }}"
3 | date: {{ .Date }}
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/resources/_gen/assets/scss/book.scss_50fc8c04e12a2f59027287995557ceff.json:
--------------------------------------------------------------------------------
1 | {"Target":"book.min.82c5dbd23447cee0b4c2aa3ed08ce0961faa40e1fa370eee4f8c9f02e0d46b5f.css","MediaType":"text/css","Data":{"Integrity":"sha256-gsXb0jRHzuC0wqo+0Izglh+qQOH6Nw7uT4yfAuDUa18="}}
--------------------------------------------------------------------------------
/.github/workflows/contributors.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 |
6 | name: contributes
7 |
8 | jobs:
9 | contrib-readme-en-job:
10 | runs-on: ubuntu-latest
11 | name: A job to automate contrib in readme
12 | steps:
13 | - name: Contribute List
14 | uses: akhilmhdh/contributors-readme-action@v2.3.4
15 | env:
16 | GITHUB_TOKEN: ${{ secrets.CONTRIBUTORS_TOKEN }}
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | MSG?=Generate site
2 |
3 | BASEDIR=$(CURDIR)
4 | OUTPUTDIR=$(BASEDIR)/public
5 |
6 | GITHUB_PAGES_BRANCH=gh-pages
7 |
8 | publish: clean build
9 |
10 | build:
11 | hugo
12 | touch $(OUTPUTDIR)/.nojekyll
13 |
14 | clean:
15 | [ ! -d $(OUTPUTDIR) ] || rm -rf $(OUTPUTDIR)
16 | git worktree prune
17 | rm -rf $(BASEDIR)/.git/worktrees/public/
18 | echo "Checking out gh-pages branch into output directory"
19 | git worktree add -B gh-pages $(OUTPUTDIR) origin/$(GITHUB_PAGES_BRANCH)
20 | echo "Removing existing files"
21 | rm -rf $(OUTPUTDIR)/*
22 |
23 | github: publish
24 | cd $(OUTPUTDIR) && git add --all && git commit -m "$(MSG)"
25 | git push origin $(GITHUB_PAGES_BRANCH)
26 |
27 | .PHONY: publish clean github
28 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: deploy
2 |
3 | on:
4 | push:
5 | workflow_dispatch:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | submodules: true
16 | fetch-depth: 0
17 |
18 | - name: Setup Hugo
19 | uses: peaceiris/actions-hugo@v2
20 | with:
21 | hugo-version: "latest"
22 |
23 | - name: Build
24 | run: hugo --minify
25 |
26 | - name: Deploy
27 | uses: peaceiris/actions-gh-pages@v3
28 | with:
29 | github_token: ${{ secrets.GITHUB_TOKEN }}
30 | publish_dir: ./public
31 | publish_branch: gh-pages
32 |
--------------------------------------------------------------------------------
/config.yml:
--------------------------------------------------------------------------------
1 | # hugo server --themesDir ...
2 |
3 | baseURL: https://gocn.github.io/How-To-Code-in-Go/
4 | title: How To Code in Go
5 | theme: hugo-book
6 |
7 | # language setting
8 | defaultContentLanguage: zh
9 | defaultContentLanguageInSubdir: true
10 |
11 | PygmentsCodeFences: true
12 |
13 | languages:
14 | zh:
15 | languageCode: zh
16 | languageName: 中文
17 | contentDir: content/zh
18 | weight: 1
19 |
20 | # en:
21 | # languageCode: en-US
22 | # languageName: English
23 | # contentDir: content/en
24 | # weight: 2
25 |
26 | # Book configuration
27 | disablePathToLower: true
28 |
29 | params:
30 | Description: How To Code in Go
31 |
32 | # show or hide table of contents for page
33 | BookShowToC: true
34 |
35 | # Set bundle to render side menu
36 | # if not specified file structure and weights will be used
37 | BookMenuBundle: /menu
38 |
39 | # specify section of content to render as menu
40 | # if bookMenuBundle is not set, 'docs' is value by default
41 | BookSection: docs
42 |
43 | # Enable "Edit this page" links for 'doc' page type.
44 | # Disabled by default. Uncomment to enable.
45 | BookEditURL: https://github.com/gocn/How-To-Code-in-Go/edit/main/content/zh/
46 |
--------------------------------------------------------------------------------
/static/README.md:
--------------------------------------------------------------------------------
1 | # How-To-Code-in-Go
2 |
3 | [《How-To-Code-in-Go》](https://github.com/gocn/How-To-Code-in-Go)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/How-To-Code-in-Go/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/How-To-Code-in-Go/pulls) 来共同参与贡献。
4 |
5 | 贡献者(按昵称首字母排序):
6 |
7 | > [astaxie](https://github.com/astaxie) | [Cluas](https://github.com/Cluas) | [cvley](https://github.com/cvley) | [Fivezh](https://github.com/fivezh) | [iddunk](https://github.com/iddunk) | [lsj1342](https://github.com/lsj1342) | [watermelon](https://github.com/watermelo) | [小超人](https://github.com/ddikvy) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang)
8 |
9 | 安装完 `hugo` 之后,需要先同步主题文件
10 |
11 | ```bash
12 | git submodule update --init --recursive
13 | ```
14 |
15 | 同步完成后,可在根目录执行以下指令来测试网站:
16 |
17 | ```bash
18 | hugo server
19 | ```
20 |
21 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。
22 |
23 | ## 目录
24 |
25 | 1. 如何在 Ubuntu 18.04 上安装 Go 和设置本地编程环境
26 | 2. 如何在 macOS 上安装 Go 和设置本地编程环境
27 | 3. 如何在 Windows 10 上安装 Go 和设置本地编程环境
28 | 4. 如何用 Go 编写你的第一个程序
29 | 5. 理解 GOPATH
30 | 6. 如何在 Go 中写注释
31 | 7. 理解 Go 的数据类型
32 | 8. Go 中处理字符串的介绍
33 | 9. 如何在 Go 中格式化字符串
34 | 10. 介绍 Go 中的 Strings 包
35 | 11. 如何在 Go 中使用变量和常量
36 | 12. 如何在 Go 中转换数据类型
37 | 13. 如何用运算符在 Go 中做数学计算
38 | 14. 了解 Go 中的布尔逻辑
39 | 15. 理解 Go 中的 Map
40 | 16. 理解 Go 中的数组和切片
41 | 17. 在 Go 中处理错误
42 | 18. 在 Go 中创建自定义错误
43 | 19. 在 Go 中处理恐慌
44 | 20. 在 Go 中导入包
45 | 21. 如何在 Go 中编写包
46 | 22. 理解 Go 中包的可见性
47 | 23. 如何在 Go 中编写条件语句
48 | 24. 如何在 Go 中编写 Switch 语句
49 | 25. 如何在 Go 中构造 for 循环
50 | 26. 在循环中使用 Break 和 Continue
51 | 27. 如何在 Go 中定义并调用函数
52 | 28. 如何在 Go 中使用可变参数函数
53 | 29. 了解 Go 中的 defer
54 | 30. 了解 Go 中的 init
55 | 31. 用构建标签定制 Go 二进制文件
56 | 32. 了解 Go 中的指针
57 | 33. 在 Go 中定义结构体
58 | 34. 在 Go 中定义方法
59 | 35. 如何构建和安装 Go 程序
60 | 36. 如何在 Go 中使用结构体标签
61 | 37. 如何在 Go 使用 interface
62 | 38. 在不同的操作系统和架构编译 Go 应用
63 | 39. 用 ldflags 设置 Go 应用程序的版本信息
64 | 40. 在 Go 里面如何使用 Flag 包
65 |
66 | ## 授权
67 |
68 | The articles in 《How-To-Code-in-Go》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/).
--------------------------------------------------------------------------------
/content/zh/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How-To-Code-in-Go
3 | type: docs
4 | ---
5 |
6 | # How-To-Code-in-Go
7 |
8 | [《How-To-Code-in-Go》](https://github.com/gocn/How-To-Code-in-Go)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/How-To-Code-in-Go/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/How-To-Code-in-Go/pulls) 来共同参与贡献。
9 |
10 | 贡献者(按昵称首字母排序):
11 |
12 | > [astaxie](https://github.com/astaxie) | [Cluas](https://github.com/Cluas) | [cvley](https://github.com/cvley) | [Fivezh](https://github.com/fivezh) | [iddunk](https://github.com/iddunk) | [lsj1342](https://github.com/lsj1342) | [watermelon](https://github.com/watermelo) | [小超人](https://github.com/ddikvy) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang)
13 |
14 | 安装完 `hugo` 之后,需要先同步主题文件
15 |
16 | ```bash
17 | git submodule update --init --recursive
18 | ```
19 |
20 | 同步完成后,可在根目录执行以下指令来测试网站:
21 |
22 | ```bash
23 | hugo server
24 | ```
25 |
26 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。
27 |
28 | ## 目录
29 |
30 | 1. 如何在 Ubuntu 18.04 上安装 Go 和设置本地编程环境
31 | 2. 如何在 macOS 上安装 Go 和设置本地编程环境
32 | 3. 如何在 Windows 10 上安装 Go 和设置本地编程环境
33 | 4. 如何用 Go 编写你的第一个程序
34 | 5. 理解 GOPATH
35 | 6. 如何在 Go 中写注释
36 | 7. 理解 Go 的数据类型
37 | 8. Go 中处理字符串的介绍
38 | 9. 如何在 Go 中格式化字符串
39 | 10. 介绍 Go 中的 Strings 包
40 | 11. 如何在 Go 中使用变量和常量
41 | 12. 如何在 Go 中转换数据类型
42 | 13. 如何用运算符在 Go 中做数学计算
43 | 14. 了解 Go 中的布尔逻辑
44 | 15. 理解 Go 中的 Map
45 | 16. 理解 Go 中的数组和切片
46 | 17. 在 Go 中处理错误
47 | 18. 在 Go 中创建自定义错误
48 | 19. 在 Go 中处理恐慌
49 | 20. 在 Go 中导入包
50 | 21. 如何在 Go 中编写包
51 | 22. 理解 Go 中包的可见性
52 | 23. 如何在 Go 中编写条件语句
53 | 24. 如何在 Go 中编写 Switch 语句
54 | 25. 如何在 Go 中构造 for 循环
55 | 26. 在循环中使用 Break 和 Continue
56 | 27. 如何在 Go 中定义并调用函数
57 | 28. 如何在 Go 中使用可变参数函数
58 | 29. 了解 Go 中的 defer
59 | 30. 了解 Go 中的 init
60 | 31. 用构建标签定制 Go 二进制文件
61 | 32. 了解 Go 中的指针
62 | 33. 在 Go 中定义结构体
63 | 34. 在 Go 中定义方法
64 | 35. 如何构建和安装 Go 程序
65 | 36. 如何在 Go 中使用结构体标签
66 | 37. 如何在 Go 使用 interface
67 | 38. 在不同的操作系统和架构编译 Go 应用
68 | 39. 用 ldflags 设置 Go 应用程序的版本信息
69 | 40. 在 Go 里面如何使用 Flag 包
70 |
71 | ## 授权
72 |
73 | The articles in 《How-To-Code-in-Go》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/).
--------------------------------------------------------------------------------
/content/zh/docs/05-Understanding_the_GOPATH.md:
--------------------------------------------------------------------------------
1 | # 理解 GOPATH
2 |
3 | ## 介绍
4 |
5 | 本文将带领你了解什么是 `GOPATH`,它是如何工作的,以及如何设置它。这是设置 Go 开发环境以及理解 Go 如何查找、安装和构建源文件的关键步骤。在本文中,我们将使用 `GOPATH` 来指代我们将要讨论的文件夹结构的概念。我们将使用 `$GOPATH` 来指代 Go 用来查找文件夹结构的环境变量。
6 |
7 | [Go 工作区](https://golang.org/doc/code.html#Workspaces) 是 Go 管理源码文件、编译的二进制文件和用于后续更快编译的缓存对象。虽然可能有多个空间,但只有一个 Go 工作区是典型的,也是被建议的使用方式。`GOPATH` 充当工作区的根文件夹。
8 |
9 | ## 设置 `$GOPATH` 环境变量
10 |
11 | `$GOPATH` 环境变量列出了 Go 用来寻找 Go 工作区的地方。
12 |
13 | 默认情况下,Go 假设 `GOPATH` 位于 `$HOME/go`,其中 `$HOME` 是电脑上上我们帐户的根目录。我们可以通过设置 `$GOPATH` 环境变量来修改它。为了进一步的研究,请参考[在 Linux 中阅读和设置环境变量](https://www.digitalocean.com/community/tutorials/how-to-read-and-set-environmental-and-shell-variables-on-a-linux-vps)的教程。
14 |
15 | 想要了解更多关于设置 `$GOPATH` 变量的信息,可以参考 Go [文档](https://golang.org/doc/code.html#Workspaces)。
16 |
17 | 此外,[本系列教程]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}}) 简单介绍了安装 Go 和设置 Go 开发环境的方法。
18 |
19 | ## `$GOPATH` 不是 `$GOROOT`
20 |
21 | `$GOROOT` 是 Go 的代码、编译器和工具所在的地方ーー这**不是**我们的源代码。`$GOROOT` 通常类似于 `/usr/local/go`。我们的 `$GOPATH` 通常类似于 `$HOME/go`。
22 |
23 | 虽然我们不再需要专门设置 `$GOROOT` 变量,但它仍然在旧材料中被引用。
24 |
25 | 现在,让我们讨论一下 Go Workspace 的结构。
26 |
27 | ## Go 工作区剖析
28 |
29 | 在一个 Go Workspace 或者 `GOPATH` 中,有三个目录: `bin`、 `pkg` 和 `src`。这些目录中的每一个对于 Go 工具链都有特殊的意义。
30 |
31 | ```text
32 | .
33 | ├── bin
34 | ├── pkg
35 | └── src
36 | └── github.com/foo/bar
37 | └── bar.go
38 | ```
39 |
40 | 让我们来看看每个目录。
41 |
42 | `$GOPATH/bin` 目录是 Go 放置 `go install` 编译的二进制文件的地方。我们的操作系统使用 `$PATH` 环境变量在没有完整路径的情况下找到二进制应用程序并执行。建议将此目录添加到全局 `$PATH` 变量中。
43 |
44 | 例如,如果我们不在 `$PATH` 中添加 `$GOPATH/bin` 来执行一个程序,我们需要运行:
45 |
46 | ```text
47 | $GOPATH/bin/myapp
48 | ```
49 |
50 | 当 `$GOPATH/bin` 被添加到 `$PATH` 时,我们可以像这样进行同样的调用:
51 |
52 | ```text
53 | $ myapp
54 | ```
55 |
56 | `$GOPATH/pkg` 目录是 Go 存储预编译目标文件的地方,以加速程序的后续编译。通常,大多数开发人员不需要访问这个目录。如果遇到编译问题,可以安全地删除该目录,然后 Go 将重新生成该目录。
57 |
58 | 在 `src` 目录是我们放置所有的 `.go` 文件,或源代码的地方。这不应与 Go 工具使用的源代码混淆,后者位于 `$GOROOT` 中。在编写 Go 应用程序、包和库时,我们将把这些文件放在 `$GOPATH/src/path/to/code` 下。
59 |
60 | ## 什么是包
61 |
62 | Go 代码是以包的形式组织的。包表示磁盘上单个目录中的所有文件。一个目录只能包含来自同一包的某些文件。包与所有用户编写的 Go 源文件一起存储在 `$GOPATH/src` 目录下。我们可以通过导入不同的软件包来理解软件包解析。
63 |
64 | 如果我们的代码是 `$GOPATH/src/blue/red`,那么它的包名应该是 `red`。
65 |
66 | ```go
67 | import "blue/red"
68 | ```
69 |
70 | `red` 包的导入声明如下:
71 |
72 | 存储在源代码仓库中的软件包,如 GitHub 和 BitBucket,将仓库的完整位置作为导入路径的一部分。
73 |
74 | 例如,我们可以使用下面的导入路径来导入 [https://github.com/gobuffalo/buffalo](https://github.com/gobuffalo/buffalo) 的源代码:
75 |
76 | ```go
77 | import "github.com/gobuffalo/buffalo"
78 | ```
79 |
80 | 因此,这个源代码应该位于磁盘上的下列位置:
81 |
82 | ```test
83 | $GOPATH/src/github.com/gobuffalo/buffalo
84 | ```
85 |
86 | ## 结论
87 |
88 | 在这篇文章中,我们讨论了 `GOPATH` 作为一个文件夹的集合,Go 期望我们的源代码保存在里面,以及这些文件夹是什么,它们包含什么。我们讨论了如何通过设置 `$GOPATH` 环境变量,将默认的 `$HOME/go` 位置改为用户选择的位置。最后,我们讨论了 Go 如何在该文件夹结构中搜索包。
89 |
90 | 在 Go 1.11 中引入的 [Go Modules](https://github.com/golang/Go/wiki/Modules)旨在取代 Go Workspaces 和 `GOPATH`。虽然建议开始使用模块,但是有些环境(如公司环境),可能还没有准备好使用模块。
91 |
92 | `GOPATH` 是 Go 设置中比较棘手的一个方面,但是一旦设置好了,我们通常会忘记它。
93 |
--------------------------------------------------------------------------------
/content/zh/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md:
--------------------------------------------------------------------------------
1 | # Go 中处理字符串的介绍
2 |
3 | _字符串_是由一个或多个字符(字母、数字、符号)组成的序列,这些字符可以是常量,也可以是变量。字符串由 [Unicode](https://unicode.org) 组成,是不可变的序列,这意味着它们是不变的。
4 |
5 | 因为文本是我们日常生活中使用的常见数据形式,所以字符串数据类型是编程中一个非常重要的基石。
6 |
7 | 本 Go 教程将介绍如何创建和打印字符串,如何连接和复制字符串,以及如何在变量中存储字符串。
8 |
9 | ## 字符串文字
10 |
11 | 在 Go 中,字符串存在于反引号 `` ` ``(有时叫做反勾号)或双引号 `"` 中。根据使用的引号不同,字符串将具有不同的特征。
12 |
13 | 使用反引号,如 `` ` `` ```bar``` `` ` ``,将创建一个_原始_字符串。在原始字符串中,除了反引号之外,任何字符都可以出现在引号之间。下面是一个原始字符串的例子:
14 |
15 | ```go
16 | `Say "hello" to Go!`
17 | ```
18 |
19 | 反斜杠在原始字符串中没有特殊含义。例如,`\n` 表示的是实际字符,以反斜杠 `\` 和字母 `n` 的形式出现。不像解释的字符串文字,`\n` 会插入一个实际的新行。
20 |
21 | 原始字符串也可用于创建多行字符串:
22 |
23 | ```Go
24 | `Go is expressive, concise, clean, and efficient.
25 | Its concurrency mechanisms make it easy to write programs
26 | that get the most out of multi-core and networked machines,
27 | while its novel type system enables flexible and modular
28 | program construction. Go compiles quickly to machine code
29 | yet has the convenience of garbage collection and the power
30 | of run-time reflection. It's a fast, statically typed,
31 | compiled language that feels like a dynamically typed,
32 | interpreted language.`
33 | ```
34 |
35 | 解释字符串是双引号之间的字符序列,如 `"bar"` 中所示。在引号中,除了换行符和非转义双引号之外,可以出现任何字符。
36 |
37 | ```go
38 | "Say \"hello\" to Go!"
39 | ```
40 |
41 | 您几乎总是使用解释字符串,因为它们允许使用转义字符。
42 |
43 | 现在你已经了解了 Go 中字符串是如何格式化的,接下来让我们看看如何在程序中打印字符串。
44 |
45 | ## 打印字符串
46 |
47 | 你可以使用系统库中的 `fmt` 包并调用 `Println()` 函数来打印字符串:
48 |
49 | ```go
50 | fmt.Println("Let's print out this string.")
51 | ```
52 |
53 | ```text
54 | output
55 | Let's print out this string.
56 | ```
57 |
58 | 当你使用系统库时需要 `import` 它们,因此一个简单打印字符串的程序如下所示:
59 |
60 | ```go
61 | package main
62 |
63 | import "fmt"
64 |
65 | func main() {
66 | fmt.Println("Let's print out this string.")
67 | }
68 | ```
69 |
70 | ## 字符串拼接
71 |
72 | _拼接_意味着把字符串收尾连接起来,创建一个新的字符串。你可以使用 `+` 号连接字符串。注意当你处理数字时,`+` 将是一个加和的操作符,但在用于字符串时是一个连接符。
73 |
74 | 让我们通过一个 `fmt.Println()` 声明语句把 `"Sammy"` 和 `"Shark"` 字符串连接到一起:
75 |
76 | ```go
77 | fmt.Println("Sammy" + "Shark")
78 | ```
79 |
80 | ```text
81 | output
82 | SammyShark
83 | ```
84 |
85 | 如果希望两个字符串之间有空格,只需在字符串中包含空格即可。在这个例子中,在 `Sammy` 之后的引号中添加空格:
86 |
87 | ```go
88 | fmt.Println("Sammy " + "Shark")
89 | ```
90 |
91 | ```text
92 | output
93 | Sammy Shark
94 | ```
95 |
96 | 不能在两种不同的数据类型之间使用 `+` 运算符。例如,你不能将字符串和整数连接在一起。如果你试着写下面的代码:
97 |
98 | ```go
99 | fmt.Println("Sammy" + 27)
100 | ```
101 |
102 | 你将会收到下面的错误:
103 |
104 | ```text
105 | output
106 | cannot convert "Sammy" (type untyped string) to type int
107 | invalid operation: "Sammy" + 27 (mismatched types string and int)
108 | ```
109 |
110 | 如果希望创建字符串 `"Sammy27"`,可以将数字 `27` 放在引号中(`"27"`)中 ,这样它就不再是一个整数,而是一个字符串。在处理邮政编码或电话号码时,将数字转换为字符串以进行连接非常有用。例如,你不希望在国家代码和地区代码之间执行添加操作,但是您希望它们可以放在一起。
111 |
112 | 当通过连接将两个或多个字符串组合在一起时,就创建了一个可以在整个程序中使用的新字符串。
113 |
114 | ## 在变量中保存字符串
115 |
116 | **[变量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})**是在程序中可以用来保存数据的符号。你可以将它们看作是一个可以在其中填充一些数据或值的空盒子。字符串是数据,因此你可以使用它们来填充变量。将字符串声明为变量可以使得在 Go 程序中处理字符串更加容易。
117 |
118 | 要在变量中存储字符串,只需将一个变量分配给字符串。在下面的例子中,`s` 被声明为变量:
119 |
120 | ```go
121 | s := "Sammy likes declaring strings."
122 | ```
123 |
124 | > **注意**: 如果你熟悉其他的编程语言,你可以把变量写成 `sammy`。但是,Go 倾向于使用较短的变量名。在这种情况下,选择 `s` 作为变量名被认为更适合编写 Go 的样式。
125 |
126 | 现在你有了设置为特定字符串的变量 `s`,你可以像下面的代码一样打印变量:
127 |
128 | ```go
129 | fmt.Println(s)
130 | ```
131 |
132 | 你将获得下面的输出:
133 |
134 | ```text
135 | output
136 | Sammy likes declaring strings.
137 | ```
138 |
139 | 通过使用变量来替代字符串,你不必每次都要重新键入字符串,从而使您在程序中处理和操作字符串更加简单。
140 |
141 | ## 结论
142 |
143 | 本教程介绍了使用 Go 编程语言处理字符串数据类型的基本知识。创建并打印字符串、连接和复制字符串以及将字符串存储在变量中,它们将为你提供在 Go 程序中使用字符串的基础知识。
144 |
--------------------------------------------------------------------------------
/content/zh/docs/09-How_To_Format_Strings_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中格式化字符串
2 |
3 | 由于字符串通常由书面文本组成,因此在许多情况下,我们可能希望更好的定制字符串的展示形式,以便通过定时、换行和缩进使其更易于阅读。
4 |
5 | 在本教程中,我们将介绍一些使用 Go 字符串的方法,以确保所有输出文本的格式正确。
6 |
7 | ## 字符串文字
8 |
9 | 我们先来看看 *字符串文字* 和 *字符串值* 的区别。 字符串文字是我们在计算机程序的源代码中看到的,包括引号。 当我们调用 `fmt.Println` 函数并运行程序时,我们会看到一个字符串值。
10 |
11 | 在“Hello, World!” 程序中,字符串文字是 `"Hello, World!"` 而字符串值是 `Hello, World!` 不带引号。 字符串值是我们在运行 Go 程序时在终端窗口中看到的输出。
12 |
13 | 但是某些字符串值可能需要包含引号,例如当我们引用某个资源时。由于字符串文字和字符串值不等价,因此通常需要为字符串文字添加额外的转换格式,以确保字符串值按照我们想要的方式显示。
14 |
15 | ## 引号
16 |
17 | 因为我们可以在 Go 中使用反引号 (`` `) 或双引号 (`"`),所以我们很容易在反引号里使用双引号来括住字符串:
18 |
19 | ```go
20 | `Sammy says, "Hello!"`
21 | ```
22 |
23 | 或者,要使用反引号,你可以将字符串括在双引号中:
24 |
25 | ```go
26 | "Sammy likes the `fmt` package for formatting strings.."
27 | ```
28 |
29 | 在组合反引号和双引号的方式中,我们可以控制字符串中引号和反引号的显示方式。
30 |
31 | 这里有个重点,在 Go 中使用反引号会创建一个 `raw` 字符串文字,而使用双引号会创建一个 `interpreted` 字符串文字。要了解有关差异的更多信息,请阅读 [Go 中处理字符串的介绍]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) 教程。
32 |
33 | ## 转义字符
34 |
35 | 格式化字符串的另一种方法是使用*转义字符*。转义字符用于告诉代码后面的字符具有特殊含义。转义字符都以反斜杠 (`\`) 开头,并结合字符串中的另一个字符以某种方式格式化给定的字符串。
36 |
37 | 以下是几个常见转义字符的列表:
38 |
39 | | 转义字符 | 如何格式化 |
40 | | -------- | ------------------ |
41 | | \\ | 反斜杠 |
42 | | \" | 双引号 |
43 | | \n | 换行 |
44 | | \t | 制表符(水平缩进) |
45 |
46 | 让我们使用转义字符将引号添加到上面的引号示例中,但这次将使用双引号来表示字符串:
47 |
48 | ```go
49 | fmt.Println("Sammy says, \"Hello!\"")
50 | ```
51 |
52 | ```text
53 | output
54 | Sammy says, "Hello!"
55 | ```
56 |
57 | 我们可以通过转义字符 `\"` 来转义掉双引号,从而实现双引号中还能嵌套使用双引号。
58 |
59 | 我们可以使用 `\n` 转义字符来换行,而无需使用 enter 或 return :
60 |
61 | ```go
62 | fmt.Println("This string\nspans multiple\nlines.")
63 | ```
64 |
65 | ```text
66 | output
67 | This string
68 | spans multiple
69 | lines.
70 | ```
71 |
72 | 我们也可以组合转义字符。 打印一个多行字符串且每行包含一个制表符,例如:
73 |
74 | ```go
75 | fmt.Println("1.\tShark\n2.\tShrimp\n10.\tSquid")
76 | ```
77 |
78 | ```go
79 | Output
80 | 1. Shark
81 | 2. Shrimp
82 | 10. Squid
83 | ```
84 |
85 | `\t` 转义字符提供的水平缩进确保在前面示例中的第二列内对齐,使输出结果非常易读。
86 |
87 | 转义字符用于向可能难以或不可能实现的字符串添加额外的格式。 如果没有转义字符,你将无法构造字符串 `Sammy says, "I like to use the `fmt` package"`。
88 |
89 | ## 多行
90 |
91 | 在多行上打印字符串可以使文本更具可读性。 通过多行,可以将字符串分组为干净有序的文本,格式化为字母,或用于维护诗歌或歌曲歌词的换行符。
92 |
93 | 要创建跨越多行的字符串,使用反引号将字符串括起来。 请记住,虽然这么写会保留换行,但它也创建了一个 `raw` 字符串文字。
94 |
95 | ```go
96 | `
97 | This string is on
98 | multiple lines
99 | within three single
100 | quotes on either side.
101 | `
102 | ```
103 |
104 | 如果你打印这个,你会注意到有一个前空行和后空行:
105 |
106 | ```go
107 | Output
108 |
109 | This string is on
110 | multiple lines
111 | within three single
112 | quotes on either side.
113 |
114 | ```
115 |
116 | 为避免这种情况,您需要将第一行紧跟在反引号之后,并以反引号结束最后一行。
117 |
118 | ```go
119 | `This string is on
120 | multiple lines
121 | within three single
122 | quotes on either side.`
123 | ```
124 |
125 | 如果你需要创建解释字符串文字,可以使用双引号和 `+` 运算符来完成,但你需要插入自己的换行符。
126 |
127 | ```go
128 | "This string is on\n" +
129 | "multiple lines\n" +
130 | "within three single\n" +
131 | "quotes on either side."
132 | ```
133 |
134 | 虽然反引号可以更轻松地打印和阅读冗长的文本,但如果你需要解释字符串文字,则需要使用双引号。
135 |
136 | ## 原始字符串文字
137 |
138 | 如果我们不想在字符串中使用特殊格式怎么办? 例如,我们可能需要比较或评估故意使用反斜杠的计算机代码字符串,因此我们不希望 Go 将其用作转义字符。
139 |
140 | **raw** 字符串文字告诉 Go 忽略字符串中的所有格式,包括转义字符。
141 |
142 | 我们通过在字符串周围使用反引号来创建一个原始字符串:
143 |
144 | ```go
145 | fmt.Println(`Sammy says,\"The balloon\'s color is red.\"`)
146 | ```
147 |
148 | ```go
149 | Output
150 | Sammy says,\"The balloon\'s color is red.\"
151 | ```
152 |
153 | 通过在给定字符串周围使用反引号来构造原始字符串,我们可以保留反斜杠和其他用作转义字符的字符。
154 |
155 | ## 结论
156 |
157 | 本教程通过使用字符串介绍了几种在 Go 中格式化文本的方法。 通过使用转义字符或原始字符串等技术,我们能够确保程序的字符串正确的呈现在屏幕上,以便用户最终能够轻松阅读所有输出文本。
--------------------------------------------------------------------------------
/content/zh/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md:
--------------------------------------------------------------------------------
1 | # 在循环中使用 Break 和 Continue
2 |
3 | ## 介绍
4 |
5 | 在 Go 中使用 **for 循环**可以让您以有效的方式自动化重复任务。
6 |
7 | 学习如何控制循环的操作和流程将允许在您的程序中自定义逻辑。您可以使用 `break` 和 `continue` 语句控制循环
8 |
9 | ## Break 语句
10 |
11 | 在 Go 中, `break` 语句终止当前循环的执行。`break`几乎总是与[条件`if`语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})配对。
12 |
13 | 让我们看一个在循环中使用`break`语句的示例:
14 |
15 | ```go
16 | package main
17 |
18 | import "fmt"
19 |
20 | func main() {
21 | for i := 0; i < 10; i++ {
22 | if i == 5 {
23 | fmt.Println("Breaking out of loop")
24 | break // break here
25 | }
26 | fmt.Println("The value of i is", i)
27 | }
28 | fmt.Println("Exiting program")
29 | }
30 | ```
31 |
32 | 这个小程序创建了一个 `for`循环,该循环在当 `i` 小于 `10` 时进行迭代。
33 |
34 | 在 `for` 循环中,有一个`if`语句。该 `if`语句会检查 `i` 的值是否小于 `5`。如果 `i` 的值不等于 `5`,则循环继续并打印出 `i` 的值。如果 `i` 的值等于 `5`,则循环将执行 `break` 语句,打印 `Breaking out of loop`,并停止循环。在程序结束时,我们打印出 `Exiting program` 表示我们已经退出了循环。
35 |
36 | 当我们运行此代码时,输出将如下所示:
37 |
38 | ```shell
39 | Output
40 | The value of i is 0
41 | The value of i is 1
42 | The value of i is 2
43 | The value of i is 3
44 | The value of i is 4
45 | Breaking out of loop
46 | Exiting program
47 | ```
48 |
49 | 这表明,一旦整数 `i` 被检查为等于 5,循环就会中断,因为程序使用 `break` 语句来这样做。
50 |
51 | ### 嵌套循环
52 |
53 | 要记住,`break` 语句只会停止调用它的最内层循环的执行。如果您有一组嵌套循环,如果需要的话,您将需要为每个循环设置 break。
54 |
55 | ```go
56 | package main
57 |
58 | import "fmt"
59 |
60 | func main() {
61 | for outer := 0; outer < 5; outer++ {
62 | if outer == 3 {
63 | fmt.Println("Breaking out of outer loop")
64 | break // break here
65 | }
66 | fmt.Println("The value of outer is", outer)
67 | for inner := 0; inner < 5; inner++ {
68 | if inner == 2 {
69 | fmt.Println("Breaking out of inner loop")
70 | break // break here
71 | }
72 | fmt.Println("The value of inner is", inner)
73 | }
74 | }
75 | fmt.Println("Exiting program")
76 | }
77 | ```
78 |
79 | 在这个程序中,我们有两个循环。虽然两个循环都迭代 5 次,但每个循环都有一个带有 `break` 语句的 `if` 条件语句。`outer` 如果等于 `3`,外部循环将中断。如果 `inner` 值为 `2` ,内部循环将中断。
80 |
81 | 如果我们运行程序,可以看到输出:
82 |
83 | ```shell
84 | Output
85 | The value of outer is 0
86 | The value of inner is 0
87 | The value of inner is 1
88 | Breaking out of inner loop
89 | The value of outer is 1
90 | The value of inner is 0
91 | The value of inner is 1
92 | Breaking out of inner loop
93 | The value of outer is 2
94 | The value of inner is 0
95 | The value of inner is 1
96 | Breaking out of inner loop
97 | Breaking out of outer loop
98 | Exiting program
99 | ```
100 |
101 | 请注意,每次内循环中断时,外循环都不会中断。这是因为`break` 只会中断调用它的最内层循环。
102 |
103 | 我们已经看到 `break` 是如何停止循环的。接下来,让我们看看 `continue` 如何继续循环的。
104 |
105 | ## Continue 语句
106 |
107 | 当您想要跳过循环的剩余部分并返回循环顶部继续新的迭代时,可以使用 `continue` 语句。
108 |
109 | 与 `break` 语句一样,`continue` 语句通常与 `if` 条件语句一起使用。
110 |
111 | 使用与前面的[Break 语句](https://gocn.github.io/How-To-Code-in-Go/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go/#break-%E8%AF%AD%E5%8F%A5)部分相同的 `for` 循环程序,这里我们将使用 `continue` 语句而不是 `break` 语句:
112 |
113 | ```go
114 | package main
115 |
116 | import "fmt"
117 |
118 | func main() {
119 | for i := 0; i < 10; i++ {
120 | if i == 5 {
121 | fmt.Println("Continuing loop")
122 | continue // break here
123 | }
124 | fmt.Println("The value of i is", i)
125 | }
126 | fmt.Println("Exiting program")
127 | }
128 | ```
129 |
130 | 使用 `continue` 语句而不是 `break` 语句的区别在于,当变量 `i` 等于 `5` 时,尽管出现中断,我们的代码仍将继续执行。让我们看看我们的输出:
131 |
132 | ```shell
133 | Output
134 | The value of i is 0
135 | The value of i is 1
136 | The value of i is 2
137 | The value of i is 3
138 | The value of i is 4
139 | Continuing loop
140 | The value of i is 6
141 | The value of i is 7
142 | The value of i is 8
143 | The value of i is 9
144 | Exiting program
145 | ```
146 |
147 | 在这里,我们看到 `The value of i is 5` 没有出现在输出中,但循环在该点之后继续打印数字 6-10 的行,然后结束循环。
148 |
149 | 您可以使用 `continue` 语句来避免深度嵌套的条件代码,或者通过消除您想要拒绝的一些频繁发生的情况来优化循环。
150 |
151 | `continue` 语句能够让程序跳过循环中出现的某些情况,然后继续循环的其余部分。
152 |
153 | ## 结论
154 |
155 | Go 中的 `break` and `continue` 语句将允许您在代码中更高效地使用 `for` 循环。
156 |
--------------------------------------------------------------------------------
/content/zh/docs/33-Defining_Structs_in_Go.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中定义结构体
2 |
3 | ## 简介
4 |
5 | 围绕具体的细节建立抽象,是编程语言能给开发者的最大工具。结构体使我们可以谈论 `Address` 而不是通过描述 `Street`, `City`, 或 `PostalCode` 字符串来进行推断。它们作为[文档]({{< relref "/docs/06-How_To_Write_Comments_in_Go.md" >}})的一个自然纽带,致力于告诉未来的开发者(包括我们自己)哪些数据对我们的 Go 程序是重要的,以及未来的代码应该如何正确使用这些数据。结构体可以用几种不同的方式来定义和使用。在本教程中,我们将会逐一看下这些技术。
6 |
7 | ## 定义结构体
8 |
9 | 结构体的工作方式类似于你可能正在使用的纸质表格,例如用来报税的表单。纸质表格可能有文本信息的字段,比如你的名字和姓氏。除了文本字段外,表单可能还有复选框来表示布尔值,如“已婚”或“单身”,或表示出生日期的日期字段。同样,结构体将不同数据收集在一起,并通过不同的字段名组织它们。当你用一个新的结构体初始化一个变量时,就好像你影印了一张表格并准备填写。
10 |
11 | 要创建一个新的结构体,你必须首先给 Go 定义一个蓝图来描述结构体所包含的字段。这个结构定义通常以关键字 `type` 开始,紧跟着结构体的名称。随后,使用 `struct` 关键字,后面跟着一对大括号 `{}`,在这里声明结构体将包含的字段。一旦你定义了结构体,就可以声明使用该结构体定义的变量。本例定义了一个结构并使用它。
12 |
13 | ```go
14 | package main
15 |
16 | import "fmt"
17 |
18 | type Creature struct {
19 | Name string
20 | }
21 |
22 | func main() {
23 | c := Creature{
24 | Name: "Sammy the Shark",
25 | }
26 | fmt.Println(c.Name)
27 | }
28 | ```
29 |
30 | 当你运行这段代码时,会看到这样的输出:
31 |
32 | ```bash
33 | output
34 | Sammy the Shark
35 | ```
36 |
37 | 在这个例子中,我们首先定义了一个 `Creature` 结构体,包含一个字符串类型的 `Name` 字段。在 `main` 方法中,我们通过在 `Creature` 类型名称后添加一对大括号来创建一个 `Creature` 实例,然后为该实例的字段设定值。`c` 实例的 `Name` 字段将被设置为 “Sammy the Shark”。在 `fmt.Println` 方法的调用中,我们通过在实例变量后加点号与我们想访问的字段名来检索实例的字段值。例如,`c.Name` 在本例中返回 `Name` 字段值。
38 |
39 | 当你声明一个新的结构体实例时,通常会列举字段名和它们的值,就像上一个例子。此外,如果每个字段的值都会在结构的实例化过程中提供,也可以省略字段名,如本例。
40 |
41 | ```go
42 | package main
43 |
44 | import "fmt"
45 |
46 | type Creature struct {
47 | Name string
48 | Type string
49 | }
50 |
51 | func main() {
52 | c := Creature{"Sammy", "Shark"}
53 | fmt.Println(c.Name, "the", c.Type)
54 | }
55 | ```
56 |
57 | 输出结果与上一个例子相同:
58 |
59 | ```bash
60 | output
61 | Sammy the Shark
62 | ```
63 |
64 | 我们为 `Creature` 增加了一个额外的字段,用字符串类型来追踪生物 `Type` 。当在 `main` 方法中实例化 `Creature` 时,我们选择使用较短的实例化方式,即按顺序为每个字段提供值,并省略其字段名。在 `Creature{"Sammy", "Shark"}` 的声明中,因为 `Name` 在类型声明中首先出现,然后为`Type`,所以`Name` 字段取值为 `Sammy`,`Type` 字段取值为 `Shark`。
65 |
66 | 这种较短的声明方式有一些缺点,导致 Go 社区在大多数情况下都倾向于采用较长的方式。使用简短声明时,你必须为结构体中的每个字段提供值,而不能省略你并不关心的字段。这很快就会导致包含很多字段的结构体短声明变得混乱。出于这个原因,简短声明通常用于字段少的结构体。
67 |
68 | 到目前为止,例子中的字段名都是以大写字母开头。这并不仅是风格上的偏好,而是在字段名中使用大写或小写字母会影响到你的字段名是否能被其他包中运行的代码所访问。
69 |
70 | ## 结构体字段导出
71 |
72 | 结构体的字段遵循与 Go 编程语言中其他标识符相同的导出规则。如果字段名以大写字母开头,则该字段可被定义该结构体的包之外的代码读写。如果字段以小写字母开头,则只有该结构体包内的代码才可以读写该字段。这个例子定义了可导出和不可导出的字段:
73 |
74 | ```go
75 | package main
76 |
77 | import "fmt"
78 |
79 | type Creature struct {
80 | Name string
81 | Type string
82 |
83 | password string
84 | }
85 |
86 | func main() {
87 | c := Creature{
88 | Name: "Sammy",
89 | Type: "Shark",
90 |
91 | password: "secret",
92 | }
93 | fmt.Println(c.Name, "the", c.Type)
94 | fmt.Println("Password is", c.password)
95 | }
96 | ```
97 |
98 | 这将输出:
99 |
100 | ```bash
101 | output
102 | Sammy the Shark
103 | Password is secret
104 | ```
105 |
106 | 我们在之前的例子中添加了一个额外的字段,`secret`。`secret` 是一个未导出的字符串类型字段,这意味着任何试图实例化 `Creature` 的其他包将无法访问或设置其 `secret` 字段。在同一个包内,我们能够访问这些字段,正如本例所做的那样。由于 `main` 方法也在 `main` 包中,它能够引用 `c.password` 并检索所存储的值。在结构中拥有未导出的字段是很常见的,对它们的访问由导出的方法来进行配置。
107 |
108 | ## 内联结构体
109 |
110 | 除了定义一个新的类型来表示一个结构体外,你还可以定义一个内联结构。在为结构类型想一个新的名称会造成浪费的情况下,这些即时创建的结构定义(不需要命名的 struct)会非常有用。例如,测试经常使用一个结构体来定义构成一个特定测试案例的所有参数。当该结构只会在一个地方使用时,想出 `CreatureNamePrintingTestCase` 这样的新名字会很麻烦。
111 |
112 | 内联结构定义出现在变量赋值的右侧。你必须立即使用一对额外的大括号进行实例化并为定义的每个字段赋值。下面的例子显示了一个内联结构定义:
113 |
114 | ```go
115 | package main
116 |
117 | import "fmt"
118 |
119 | func main() {
120 | c := struct {
121 | Name string
122 | Type string
123 | }{
124 | Name: "Sammy",
125 | Type: "Shark",
126 | }
127 | fmt.Println(c.Name, "the", c.Type)
128 | }
129 | ```
130 |
131 | 这个例子的输出结果将是:
132 |
133 | ```bash
134 | output
135 | Sammy the Shark
136 | ```
137 |
138 | 本例没有使用 `type` 关键字定义一个新的类型来描述我们的结构体,而是通过将 `struct` 定义放在短赋值运算符 `:=` 之后定义一个内联结构。我们像之前的例子一样定义了结构体的字段,但是必须立即提供一对大括号和每个字段将赋的值。现在我们可以和以前完全一样使用这个结构体,用点符号来访问字段名。内联结构最常用于测试过程中声明,因为经常会需要用到一次性结构体来定义包含特定测试案例的数据和预期测试结果。
139 |
140 | ## 总结
141 |
142 | 结构体是开发者为组织信息而定义的各种各样的数据的集合。大多数程序都要处理大量的数据,如果没有结构体,就很难记住哪些 `string` 或 `int` 变量相关,哪些无关。下一次,当你发现自己在组织一些变量时,问问自己,也许这些变量用 `struct` 来分组会更好。这些变量可能一直都在描述更高层级的概念。
--------------------------------------------------------------------------------
/content/zh/menu/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | headless: true
3 | ---
4 |
5 | - **开始学习**
6 | - [1. 如何在 Ubuntu 18.04 上安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})
7 | - [2. 如何在 macOS 上安装 Go 和设置本地编程环境]({{< relref "/docs/02-How_To_Install_Go_and_Set_Up_a_Local_Programming_Environment_on_macOS_DigitalOcean.md" >}})
8 | - [3. 如何在 Windows 10 上安装 Go 和设置本地编程环境]({{< relref "/docs/03-How_To_Install_Go_and_Set_Up_a_Local_Programming_Environment_on_Windows_10_DigitalOcean.md" >}})
9 | - [4. 如何用 Go 编写你的第一个程序]({{< relref "/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md" >}})
10 | - [5. 理解 GOPATH]({{< relref "/docs/05-Understanding_the_GOPATH.md" >}})
11 | - [6. 如何在 Go 中写注释]({{< relref "/docs/06-How_To_Write_Comments_in_Go.md" >}})
12 | - [7. 理解 Go 的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})
13 | - [8. Go 中处理字符串的介绍]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})
14 | - [9. 如何在 Go 中格式化字符串]({{< relref "/docs/09-How_To_Format_Strings_in_Go.md" >}})
15 | - [10. 介绍 Go 中的 Strings 包]({{< relref "/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md" >}})
16 | - [11. 如何在 Go 中使用变量和常量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})
17 | - [12. 如何在 Go 中转换数据类型]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}})
18 | - [13. 如何用运算符在 Go 中做数学计算]({{< relref "/docs/13-How_To_Do_Math_in_Go_with_Operators.md" >}})
19 | - [14. 理解 Go 中的布尔逻辑]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})
20 | - [15. 理解 Go 中的 Map]({{< relref "/docs/15-Understanding_Maps_in_Go.md" >}})
21 | - [16. 理解 Go 中的数组和切片]({{< relref "/docs/16-Understanding_Arrays_and_Slices_in_Go.md" >}})
22 | - [17. 在 Go 中处理错误]({{< relref "/docs/17-Handling_Errors_in_Go_DigitalOcean.md" >}})
23 | - [18. 在 Go 中创建自定义错误]({{< relref "/docs/18-Creating_Custom_Errors_in_Go_DigitalOcean.md" >}})
24 | - [19 在 Go 中处理恐慌]({{< relref "/docs/19-Handling_Panics_in_Go _DigitalOcean.md" >}})
25 | - [20. 在 Go 中导入包]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})
26 | - [21. 如何在 Go 中编写包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}})
27 | - [22. 理解 Go 中包的可见性]({{< relref "/docs/22-Understanding_Package_Visibility_in_Go.md" >}})
28 | - [23. 如何在 Go 中编写条件语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})
29 | - [24. 如何在 Go 中编写 Switch 语句]({{< relref "/docs/24-How_To_Write_Switch_Statements_in_Go.md" >}})
30 | - [25. 如何在 Go 中构造 for 循环]({{< relref "/docs/25-How_To_Construct_For_Loops_in_Go.md" >}})
31 | - [26. 在循环中使用 Break 和 Continue]({{< relref "/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go.md" >}})
32 | - [27. 如何在 Go 中定义并调用函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})
33 | - [28. 如何在 Go 中使用可变参数函数]({{< relref "/docs/28-How_To_Use_Variadic_Functions_in_Go.md" >}})
34 | - [29. 理解 Go 中的 defer]({{< relref "/docs/29-Understanding_defer_in_Go.md" >}})
35 | - [30. 理解 Go 中的 init]({{< relref "/docs/30-Understanding_init_in_Go.md" >}})
36 | - [31. 用构建标签定制 Go 二进制文件]({{< relref "/docs/31-Customizing_Go_Binaries_with_Build_Tags.md" >}})
37 | - [32. 理解 Go 中的指针]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})
38 | - [33. 在 Go 中定义结构体]({{< relref "/docs/33-Defining_Structs_in_Go.md" >}})
39 | - [34. 在 Go 中定义方法]({{< relref "/docs/34-Defining_Methods_in_Go.md" >}})
40 | - [35. 如何构建和安装 Go 程序]({{< relref "/docs/35-How_To_Build_and_Install_Go_Programs.md" >}})
41 | - [36. 如何在 Go 中使用结构体标签]({{< relref "/docs/36-How_To_Use_Struct_Tags_in_Go.md" >}})
42 | - [37. 如何在 Go 使用 interface]({{< relref "/docs/37-How_To_Use_Interfaces_in_Go.md" >}})
43 | - [38. 在不同的操作系统和架构编译 Go 应用]({{< relref "/docs/38-Building_Go_Applications_for_Different_Operating_Systems_and_Architectures.md" >}})
44 | - [39. 用 ldflags 设置 Go 应用程序的版本信息]({{< relref "/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md" >}})
45 | - [40. 在 Go 里面如何使用 Flag 包]({{< relref "/docs/40-How_To_Use_the_Flag_Package_in_Go.md" >}})
46 | - [41. 如何使用 Go 模块]({{< relref "/docs/41-How_to_Use_Go_Modules.md" >}})
47 | - [42. 如何分发 Go 模块]({{< relref "/docs/42-How_to_Distribute_Go_Modules.md" >}})
48 | - [43. 如何在自己的项目中使用私有的Go模块]({{< relref "/docs/43-How_to_Use_a_Private_Go_Module_in_Your_Own_Project.md" >}})
49 |
50 | - **附录:资源**
51 |
52 | - [**Fork on Github**](https://github.com/gocn/How-To-Code-in-Go)
53 |
--------------------------------------------------------------------------------
/content/zh/docs/10-An_Introduction_to_the_Strings_Package_in_Go.md:
--------------------------------------------------------------------------------
1 | # 介绍 Go 中的 Strings 包
2 |
3 | ## 介绍
4 |
5 | Go 的 [`strings`](https://golang.org/pkg/strings/) 包有几个函数可用于 [string 数据类型]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})。这些函数可以轻松地修改和操作字符串。我们可以将函数视为对代码元素执行的操作。内置函数是那些在 Go 编程语言中定义并且可供我们随时使用的函数。
6 |
7 | 在本教程中,我们将回顾几个可用于在 Go 中处理字符串的不同函数。
8 |
9 | ## 字符串大写和小写
10 |
11 | 函数 `strings.ToUpper` 和 `strings.ToLower` 将返回一个字符串,其中原始字符串的所有字母都转换为大写或小写字母。 因为字符串是不可变的数据类型,所以返回的字符串将是一个新字符串。 字符串中的任何非字母字符都不会更改。
12 |
13 | 要将字符串 `"Sammy Shark"` 转换为全大写,你可以使用 `strings.ToUpper` 函数:
14 |
15 | ```go
16 | ss := "Sammy Shark"
17 | fmt.Println(strings.ToUpper(ss))
18 | ```
19 |
20 | ```go
21 | Output
22 | SAMMY SHARK
23 | ```
24 |
25 | 要转换为小写:
26 |
27 | ```go
28 | fmt.Println(strings.ToLower(ss))
29 | ```
30 |
31 | ```go
32 | Output
33 | sammy shark
34 | ```
35 |
36 | 由于你使用的是 `strings` 包,因此首先需要将其导入程序中。 要将字符串转换为大写和小写,整个程序如下:
37 |
38 | ```go
39 | package main
40 |
41 | import (
42 | "fmt"
43 | "strings"
44 | )
45 |
46 | func main() {
47 | ss := "Sammy Shark"
48 | fmt.Println(strings.ToUpper(ss))
49 | fmt.Println(strings.ToLower(ss))
50 | }
51 | ```
52 |
53 | `strings.ToUpper` 和 `strings.ToLower` 函数使大小写始终保持一致,更容易评估和比较字符串。 例如,如果用户的姓名全小写,我们仍然可以通过检查全大写版本来确定他们的姓名是否在我们的数据库中。
54 |
55 | ## 字符串搜索函数
56 |
57 | `strings` 包有许多函数可以帮助确定字符串是否包含特定的字符。
58 |
59 | | 函数 | 用法 |
60 | | ------------------- | ---------------------- |
61 | | `strings.HasPrefix` | 从头开始搜索字符串 |
62 | | `strings.HasSuffix` | 从末尾开始搜索字符串 |
63 | | `strings.Contains` | 搜索字符串中的任何位置 |
64 | | `strings.Count` | 计算字符串出现的次数 |
65 |
66 | `strings.HasPrefix` 和 `strings.HasSuffix` 允许你检查字符串是否以特定字符集开头或结尾。
67 |
68 | 例如,要检查字符串 `"Sammy Shark"` 是否以 `Sammy` 开头并以 `Shark` 结尾:
69 |
70 | ```go
71 | ss := "Sammy Shark"
72 | fmt.Println(strings.HasPrefix(ss, "Sammy"))
73 | fmt.Println(strings.HasSuffix(ss, "Shark"))
74 | ```
75 |
76 | ```go
77 | Output
78 | true
79 | true
80 | ```
81 |
82 | 你将使用 `strings.Contains` 函数来检查 `"Sammy Shark"` 是否包含字符 `Sh`:
83 |
84 | ```go
85 | fmt.Println(strings.Contains(ss, "Sh"))
86 | ```
87 |
88 | ```go
89 | Output
90 | true
91 | ```
92 |
93 | 最后,看看 “Sammy Shark” 这个字符串中出现了多少次字母 “S”:
94 |
95 | ```go
96 | fmt.Println(strings.Count(ss, "S"))
97 | ```
98 |
99 | ```go
100 | Output
101 | 2
102 | ```
103 |
104 | **注意:** Go 中的所有字符串都区分大小写。 这意味着 `Sammy` 与 `sammy` 不同。
105 |
106 | 计算小写的 `s` 在 `Sammy Shark` 中出现的次数与使用大写的 `S` 计算结果并不同:
107 |
108 | ```go
109 | fmt.Println(strings.Count(ss, "s"))
110 | ```
111 |
112 | ```go
113 | Output
114 | 0
115 | ```
116 |
117 | 因为 `S` 与 `s` 不同,所以返回的计数将为 `0`。
118 |
119 | 当你想在程序中比较或搜索字符串时,字符串函数很有用。
120 |
121 | ## 确定字符串长度
122 |
123 | 内置函数 `len()` 返回字符串中的字符数。 当你需要强制最小或最大密码长度,或将较大的字符串截断以在特定限制内用作缩写时,此功能很有用。
124 |
125 | 为了演示这个功能,下面我们将得到一个长字符串的长度:
126 |
127 | ```go
128 | import (
129 | "fmt"
130 | "strings"
131 | )
132 |
133 | func main() {
134 | openSource := "Sammy contributes to open source."
135 | fmt.Println(len(openSource))
136 | }
137 | ```
138 |
139 | ```go
140 | Output
141 | 33
142 | ```
143 |
144 | 我们将变量 `openSource` 设置为字符串 `"Sammy contributes to open source."`,然后使用 `len(openSource)` 将该变量传递给 `len()` 函数。 最后,我们将函数传递给 `fmt.Println()` 函数,以便我们可以在屏幕上看到程序的输出。
145 |
146 | 请记住,`len()` 函数将计算由双引号绑定的任何字符——包括字母、数字、空白字符和符号。
147 |
148 | ## 字符串操作函数
149 |
150 | `strings.Join`、`strings.Split` 和 `strings.ReplaceAll` 函数是在 Go 中操作字符串的一些额外方法。
151 |
152 | `strings.Join` 函数用于将一组字符串组合成一个新的字符串。
153 |
154 | 要从字符串切片创建逗号分隔的字符串,我们将按以下方式使用此函数:
155 |
156 | ```go
157 | fmt.Println(strings.Join([]string{"sharks", "crustaceans", "plankton"}, ","))
158 | ```
159 |
160 | ```go
161 | Output
162 | sharks,crustaceans,plankton
163 | ```
164 |
165 | 如果我们想在我们的新字符串中的字符串值之间添加一个逗号和一个空格,我们可以简单地用逗号后的空格重写我们的表达式:`strings.Join([]string{"sharks", "crustaceans", "plankton "}, ", ")`。
166 |
167 | 就像我们可以将字符串连接在一起一样,我们也可以拆分字符串。 为此,我们可以使用 `strings.Split` 函数并拆分空格:
168 |
169 | ```go
170 | balloon := "Sammy has a balloon."
171 | s := strings.Split(balloon, " ")
172 | fmt.Println(s)
173 | ```
174 |
175 | ```go
176 | Output
177 | [Sammy has a balloon]
178 | ```
179 |
180 | 输出是一段字符串。 由于使用了`strings.Println`,因此很难通过肉眼来判断输出的是什么类型。 要查看它确实是一段字符串,请使用 `fmt.Printf` 函数和 `%q` 来格式化输出字符串:
181 |
182 | ```go
183 | fmt.Printf("%q", s)
184 | ```
185 |
186 | ```go
187 | Output
188 | ["Sammy" "has" "a" "balloon."]
189 | ```
190 |
191 | 除了 `strings.Split` 之外,另一个有用的函数是 `strings.Fields`。 不同之处在于 `strings.Fields` 将忽略所有空格,并且只会在字符串中拆分出实际的 `fields`:
192 |
193 | ```go
194 | data := " username password email date"
195 | fields := strings.Fields(data)
196 | fmt.Printf("%q", fields)
197 | ```
198 |
199 | ```go
200 | Output
201 | ["username" "password" "email" "date"]
202 | ```
203 |
204 | `strings.ReplaceAll` 函数可以将原始字符串进行一些替换,并返回更新后的字符串。
205 |
206 | 假设 Sammy 的气球丢失了。 由于 Sammy 不再有这个气球,我们将在新字符串中将子字符串 `"has"` 从原始字符串中更改为 `"had"`:
207 |
208 | ```go
209 | fmt.Println(strings.ReplaceAll(balloon, "has", "had"))
210 | ```
211 |
212 | 上面的函数参数,首先是 `balloon` ,用于存储原始字符串的变量; 第二个子字符串“has”是我们要替换的,第三个子字符串“had”是我们要替换第二个子字符串的。 当我们将其合并到程序中时,我们的输出将如下所示:
213 |
214 | ```go
215 | Output
216 | Sammy had a balloon.
217 | ```
218 |
219 | 使用字符串函数 `strings.Join`、`strings.Split` 和 `strings.ReplaceAll` 将为你在 Go 中操作字符串提供更好的帮助。
220 |
221 | ## 结论
222 |
223 | 本教程介绍了一些用于字符串数据类型的常见字符串包函数,你可以使用这些函数在 Go 程序中处理和操作字符串。
224 |
225 | 你可以在 [理解 Go 的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}) 中了解有关其他数据类型的更多信息,并在 [Go 中处理字符串的介绍]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) 了解更多有关字符串的信息。
226 |
227 | ------
--------------------------------------------------------------------------------
/content/zh/docs/06-How_To_Write_Comments_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中写注释
2 |
3 | ## 介绍
4 |
5 | 注释是存在于计算机程序中,被编译器和解释器忽略的代码行。在程序中包含注释使得代码对于人类来说更具可读性,因为它提供了一些关于程序的每个部分正在做什么的信息或解释。
6 |
7 | 根据你程序的目的,注释可以作为你自己的笔记或者提醒,或者它们可以是为了其他程序员能够理解你的代码在做什么而写的。
8 |
9 | 一般来说,在写或者更新程序的时候写评论是个不错的主意,因为以后很容易忘记你的思维过程,而且以后写的评论长期来看可能没那么有用。
10 |
11 | ### 注释语法
12 |
13 | Go 中的注释以一组向前斜杠(`//`)开始,一直到行尾。在前向斜杠集合之后有一个空白符号是惯用的方式。
14 |
15 | 一般来说,评论看起来是这样的:
16 |
17 | ```go
18 | // This is a comment
19 | ```
20 |
21 | 注释不会执行,因此在运行程序时没有注释的指示。注释在源代码中供人阅读,而不是供计算机执行。
22 |
23 | 在一个 “Hello, World!” 的程序中,注释可能如下所示:
24 |
25 | ```go
26 | package main
27 |
28 | import (
29 | "fmt"
30 | )
31 |
32 | func main() {
33 | // Print “Hello, World!” to console
34 | fmt.Println("Hello, World!")
35 | }
36 | ```
37 |
38 | 在一个迭代切片的 `for` 循环时,注释可能如下所示:
39 |
40 | ```go
41 | package main
42 |
43 | import (
44 | "fmt"
45 | )
46 |
47 | func main() {
48 | // Define sharks variable as a slice of strings
49 | sharks := []string{"hammerhead", "great white", "dogfish", "frilled", "bullhead", "requiem"}
50 |
51 | // For loop that iterates over sharks list and prints each string item
52 | for _, shark := range sharks {
53 | fmt.Println(shark)
54 | }
55 | }
56 | ```
57 |
58 | 注释应该与注释代码的缩进相同。也就是说,一个没有缩进的函数定义将有一个没有缩进的注释,并且下面的每个缩进级别中,注释与所注释的代码对齐。
59 |
60 | 比如,下面是 `main` 函数如何注释,以及各个缩进级别的代码和注释:
61 |
62 | ```go
63 | package main
64 |
65 | import "fmt"
66 |
67 | const favColor string = "blue"
68 |
69 | func main() {
70 | var guess string
71 | // Create an input loop
72 | for {
73 | // Ask the user to guess my favorite color
74 | fmt.Println("Guess my favorite color:")
75 | // Try to read a line of input from the user. Print out the error 0
76 | if _, err := fmt.Scanln(&guess); err != nil {
77 | fmt.Printf("%s\n", err)
78 | return
79 | }
80 | // Did they guess the correct color?
81 | if favColor == guess {
82 | // They guessed it!
83 | fmt.Printf("%q is my favorite color!\n", favColor)
84 | return
85 | }
86 | // Wrong! Have them guess again.
87 | fmt.Printf("Sorry, %q is not my favorite color. Guess again.\n", guess)
88 | }
89 | }
90 | ```
91 |
92 | 编写注释是为了帮助程序员,无论是最初的程序员还是在项目中使用或合作的其他人。如果注释不能与代码库一起适当地维护和更新,那么对于编写与代码相矛盾或将与将来的代码相矛盾的注释,不如不包含注释。
93 |
94 | 在给代码注释时,你应该回答代码背后的 _原因_,而不是 _什么_ 或 _怎么_ 回事。除非代码特别复杂,否则查看代码通常可以回答是 _什么_ 或者 _怎么_ 的问题,这就是为什么注释通常是围绕 _为什么_ 的原因。
95 |
96 | ### 注释块
97 |
98 | 注释块可以用来解释更复杂的代码或那些读者可能并不熟悉的代码。
99 |
100 | 你可以用两种方式在 Go 中创建注释块。第一种方法是使用一组双向前斜杠,并在每行中重复它们。
101 |
102 | ```go
103 | // First line of a block comment
104 | // Second line of a block comment
105 | ```
106 |
107 | 第二种是使用开始标记(`/*`)和结束标记(`*/`)。为了记录代码,惯例方式是使用 `//` 语法。你只能使用 `/* ... */` 语法进行调试,我们将在本文后面讨论这个问题。
108 |
109 | ```go
110 | /*
111 | Everything here
112 | will be considered
113 | a block comment
114 | */
115 | ```
116 |
117 | 在这个例子中,注释块定义了 `MustGet()` 函数中发生的事情:
118 |
119 | ```go
120 | // MustGet will retrieve a url and return the body of the page.
121 | // If Get encounters any errors, it will panic.
122 | func MustGet(url string) string {
123 | resp, err := http.Get(url)
124 | if err != nil {
125 | panic(err)
126 | }
127 |
128 | // don't forget to close the body
129 | defer resp.Body.Close()
130 | var body []byte
131 | if body, err = ioutil.ReadAll(resp.Body); err != nil {
132 | panic(err)
133 | }
134 | return string(body)
135 | }
136 | ```
137 |
138 | 在 Go 中导出函数的开头经常会看到注释块; 这些注释也是生成代码文档的元素。当操作不那么直观,而需要完全解释时,也会使用注释块。除了记录函数之外,你应该尽量避免对代码进行过度注释,并相信其他程序员能够理解 Go,除非你是为特定的用户编写代码。
139 |
140 | ### 内联注释
141 |
142 | 内联注释出现在语句的同一行,位于代码本身之后。像其他注释一样,它们以一组向前斜杠开始。同样,不需要在前向斜杠后面加空格,但是惯例是这样做的。
143 |
144 | 一般来说,内联注释是这样的:
145 |
146 | ```go
147 | [code] // Inline comment about the code
148 | ```
149 |
150 | 内联注释应该谨慎使用,但是对于解释代码中棘手或不明显的部分来说可能是有效的。如果你认为自己将来可能不记得正在编写的代码中的某一行,或者你正在与一个你认识的人合作,而这个人可能并不熟悉代码的所有方面,那么它们也会很有用。
151 |
152 | 例如,如果你在你的 Go 程序中没有使用大量的数学,你或者你的合作者可能不知道下面的代码创建了一个复杂的数字,所以你可能需要包含一个关于这个的内联注释:
153 |
154 | ```go
155 | z := x % 2 // Get the modulus of x
156 | ```
157 |
158 | 你也可以使用行内注释来解释做某事背后的原因,或者提供一些额外的信息,比如:
159 |
160 | ```go
161 | x := 8 // Initialize x with an arbitrary number
162 | ```
163 |
164 | 你应该只在必要的时候,并且当它们可以为阅读程序的人提供有用的指导时,使用内联注释。
165 |
166 | ### 注释测试代码
167 |
168 | 除了使用注释作为记录代码的方式之外,还可以使用开始标记(`/*`)和结束标记(`*/`)来创建块注释。这允许你在测试或调试当前正在创建的程序时注释掉不想执行的代码。也就是说,当你在实现新代码后遇到错误时,你可能希望对其中的一些代码行进行注释,以查看是否能够解决这个确切的问题。
169 |
170 | Using the `/*` and `*/` tags can also allow you to try alternatives while you’re determining how to set up your code. You can also use block comments to comment out code that is failing while you continue to work on other parts of your code.
171 | 在决定如何设置代码时,使用 `/*` 和 `*/` 标记还可以让你尝试其他选择。你也可以使用注释块来注释失败的代码,同时继续处理代码的其他部分。
172 |
173 | ```go
174 | // Function to add two numbers
175 | func addTwoNumbers(x, y int) int {
176 | sum := x + y
177 | return sum
178 | }
179 |
180 | // Function to multiply two numbers
181 | func multiplyTwoNumbers(x, y int) int {
182 | product := x * y
183 | return product
184 | }
185 |
186 | func main() {
187 | /*
188 | In this example, we're commenting out the addTwoNumbers
189 | function because it is failing, therefore preventing it from executing.
190 | Only the multiplyTwoNumbers function will run
191 |
192 | a := addTwoNumbers(3, 5)
193 | fmt.Println(a)
194 |
195 | */
196 |
197 | m := multiplyTwoNumbers(5, 9)
198 | fmt.Println(m)
199 | }
200 | ```
201 |
202 | **注意**: 注释掉代码只能用于测试目的。不要在最终的程序中保留注释掉的代码片段。
203 |
204 | 使用 `/*` 和 `*/` 标记注释代码可以让你尝试不同的编程方法,并通过系统地注释和运行程序的各个部分来帮助你找到错误的源代码。
205 |
206 | ### 结论
207 |
208 | 在你的 Go 程序中使用注释有助于使你的程序对于人类来说更具可读性,包括未来的你自己。添加适当相关和有用的注释可以使其他人更容易地与你合作编程项目,并使代码的价值更加明显。
209 |
210 | 在 Go 中正确注释代码还可以让你用到 [Godoc](https://godoc.org/golang.org/x/tools/cmd/godoc) 工具。Godoc 是一个从代码中提取注释并为 Go 程序生成文档的工具。
211 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # How-To-Code-in-Go
2 |
3 | [《How-To-Code-in-Go》](https://github.com/gocn/How-To-Code-in-Go)采用 [Hugo](https://gohugo.io) 发布。欢迎大家通过 [issue](https://github.com/gocn/How-To-Code-in-Go/issues) 提供建议,也可以通过 [pull requests](https://github.com/gocn/How-To-Code-in-Go/pulls) 来共同参与贡献。
4 |
5 | 贡献者(按昵称首字母排序):
6 |
7 | > [astaxie](https://github.com/astaxie) | [Cluas](https://github.com/Cluas) | [cvley](https://github.com/cvley) | [Fivezh](https://github.com/fivezh) | [iddunk](https://github.com/iddunk) | [lsj1342](https://github.com/lsj1342) | [watermelon](https://github.com/watermelo) | [小超人](https://github.com/ddikvy) | [Xiaomin Zheng](https://github.com/zxmfke) | [Yu Zhang](https://github.com/pseudoyu) | [朱亚光](https://github.com/zhuyaguang)
8 |
9 | 安装完 `hugo` 之后,需要先同步主题文件
10 |
11 | ```bash
12 | git submodule update --init --recursive
13 | ```
14 |
15 | 同步完成后,可在根目录执行以下指令来测试网站:
16 |
17 | ```bash
18 | hugo server
19 | ```
20 |
21 | 文档在 `content/zh/docs` 目录下,修改后可以通过 pull requests 提交。
22 |
23 | ## 目录
24 |
25 | 1. 如何在 Ubuntu 18.04 上安装 Go 和设置本地编程环境
26 | 2. 如何在 macOS 上安装 Go 和设置本地编程环境
27 | 3. 如何在 Windows 10 上安装 Go 和设置本地编程环境
28 | 4. 如何用 Go 编写你的第一个程序
29 | 5. 理解 GOPATH
30 | 6. 如何在 Go 中写注释
31 | 7. 理解 Go 的数据类型
32 | 8. Go 中处理字符串的介绍
33 | 9. 如何在 Go 中格式化字符串
34 | 10. 介绍 Go 中的 Strings 包
35 | 11. 如何在 Go 中使用变量和常量
36 | 12. 如何在 Go 中转换数据类型
37 | 13. 如何用运算符在 Go 中做数学计算
38 | 14. 了解 Go 中的布尔逻辑
39 | 15. 理解 Go 中的 Map
40 | 16. 理解 Go 中的数组和切片
41 | 17. 在 Go 中处理错误
42 | 18. 在 Go 中创建自定义错误
43 | 19. 在 Go 中处理恐慌
44 | 20. 在 Go 中导入包
45 | 21. 如何在 Go 中编写包
46 | 22. 理解 Go 中包的可见性
47 | 23. 如何在 Go 中编写条件语句
48 | 24. 如何在 Go 中编写 Switch 语句
49 | 25. 如何在 Go 中构造 for 循环
50 | 26. 在循环中使用 Break 和 Continue
51 | 27. 如何在 Go 中定义并调用函数
52 | 28. 如何在 Go 中使用可变参数函数
53 | 29. 了解 Go 中的 defer
54 | 30. 了解 Go 中的 init
55 | 31. 用构建标签定制 Go 二进制文件
56 | 32. 了解 Go 中的指针
57 | 33. 在 Go 中定义结构体
58 | 34. 在 Go 中定义方法
59 | 35. 如何构建和安装 Go 程序
60 | 36. 如何在 Go 中使用结构体标签
61 | 37. 如何在 Go 使用 interface
62 | 38. 在不同的操作系统和架构编译 Go 应用
63 | 39. 用 ldflags 设置 Go 应用程序的版本信息
64 | 40. 在 Go 里面如何使用 Flag 包
65 |
66 | ## 授权
67 |
68 | The articles in 《How-To-Code-in-Go》 are licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/).
69 |
70 | ## 贡献者
71 |
72 |
73 |
161 |
--------------------------------------------------------------------------------
/content/zh/docs/20-Importing_Packages_in_Go_DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中导入包
2 |
3 | ## 介绍
4 |
5 | 有时,你的代码需要当前程序的基础上增加更多的功能。在这些情况下,你可以使用软件包来丰富你的程序。在 Go 中, 一个包表示磁盘上单个目录中的所有文件。包可以定义可以在其他 Go 文件或包中引用的函数、类型和接口。
6 |
7 | 本教程将带你来完成软件包的安装, 导入和重命名。
8 |
9 | ## 标准库包
10 |
11 | 标准库是 Go 附带的一组软件包。这些软件包包含许多用于编写现代软件的基本模块。例如, [`fmt`](https://golang.org/pkg/fmt) 软件包包含用于格式和打印字符串的基本功能。 [`net/http`](https://golang.org/pkg/net/http/) 软件包包含允许开发人员创建 Web 服务,通过`HTTP` 协议发送和检索数据的功能,等等。
12 |
13 | 为了利用软件包中的功能,你需要使用 `import` 语句访问软件包。`import` 语句由 `import` 关键字以及软件包的名称组成。
14 |
15 | 例如,在 GO 程序中 `random.go` 文件。你可以导入 `math/rand` 包来生成随机数:
16 |
17 | ``` go
18 | import "math/rand"
19 | ```
20 |
21 | 当我们导入一个包时,我们把它在当前程序中作为一个单独 namespace 命名空间来使用。这意味着我们必须像 `package.function` 调用其中的函数。
22 |
23 | 实际上,`math/rand` 软件包的功能看起来像这些示例:
24 |
25 | - `rand.Int()` 调用函数返回随机整数。
26 | - `rand.Intn()` 调用函数将随机元素从 `0` 返回到所提供的指定数字。
27 |
28 | 让我们创建一个 `for` 循环,以显示我们如何在随机过程中调用 `math/rand` 软件包的函数。
29 |
30 | random.go
31 |
32 | ``` go
33 | package main
34 |
35 | import "math/rand"
36 |
37 | func main() {
38 | for i := 0; i < 10; i++ {
39 | println(rand.Intn(25))
40 | }
41 | }
42 | ```
43 |
44 | 该程序首先在第三行中导入 `math/rand` 软件包,然后移至将运行10次的循环中。在循环中,程序将打印一个在 `0` 到 `25` 范围内的随机整数。其中, 整数 `25` 是作为其参数传递给 `rand.Intn()`。
45 |
46 | 当我们使用 `go run random.go` 来运行程序时,我们将收到 10 个随机整数作为输出。因为这些是随机的,所以每次运行程序时,你都可能会获得不同的整数。输出看起来像这样:
47 |
48 | ``` shell
49 | # Output
50 | 6
51 | 12
52 | 22
53 | 9
54 | 6
55 | 18
56 | 0
57 | 15
58 | 6
59 | 0
60 | ```
61 |
62 | 整数永远不会低于 0 或 24 以上。
63 |
64 | 当需要导入多个包时,你可以使用 `()` 来创建一个块。通过使用块,可以避免在每行上重复 `import` 关键字。这将使你的代码看起来更整洁
65 |
66 | random.go
67 |
68 | ``` go
69 | import (
70 | "fmt"
71 | "math/rand"
72 | )
73 |
74 | ```
75 |
76 | 为了利用新增的软件包,我们现在可以格式化输出并打印出循环中每次迭代生成的随机数:
77 |
78 | random.go
79 |
80 | ``` go
81 | package main
82 |
83 | import (
84 | "fmt"
85 | "math/rand"
86 | )
87 |
88 | func main() {
89 | for i := 0; i < 10; i++ {
90 | fmt.Printf("%d) %d\n", i, rand.Intn(25))
91 | }
92 | }
93 |
94 | ```
95 |
96 | 现在,当我们运行程序时,我们将收到看起来像这样的输出:
97 |
98 | ``` shell
99 | # Output
100 | 0) 6
101 | 1) 12
102 | 2) 22
103 | 3) 9
104 | 4) 6
105 | 5) 18
106 | 6) 0
107 | 7) 15
108 | 8) 6
109 | 9) 0
110 | ```
111 |
112 | 在本节中,我们学会了如何导入软件包并使用它们来编写更复杂的程序。到目前为止,我们只使用了标准库中的软件包。接下来,让我们看看如何安装和使用其他开发人员编写的软件包。
113 |
114 | ## 安装软件包
115 |
116 | 虽然标准库包含了许多出色且有用的软件包,但它们的设计是通用的,本质上不是特定的。这使开发者可以根据自己的特定需求在标准库之上构建自己的软件包。
117 |
118 | GO 工具链带有 `go get` 命令。此命令使你可以将第三方软件包安装到本地开发环境中,并且将这些软件包应用到你的程序中。
119 |
120 | 使用 `go get` 来安装第三方软件包时,通常可以通过其规范路径引用软件包。这个路径也可能是通往公共项目的途径,该项目托管在诸如 GitHub 之类的代码存储库中。因此,如果要导入 [`flect`](https://github.com/gobuffalo/flect) 软件包,则将使用完整的规范路径:
121 |
122 | ``` shell
123 | go get github.com/gobuffalo/flect
124 | ```
125 |
126 | 在这种情况下,使用 `go get` 工具将在 GitHub 上找到软件包,并将其安装到你的 [`$Gopath`]({{< relref "/docs/05-Understanding_the_GOPATH.md" >}}) 中。
127 |
128 | 对于此示例,代码将安装在此目录中:
129 |
130 | ``` shell
131 | $GOPATH/src/github.com/gobuffalo/flect
132 | ```
133 |
134 | 原始作者通常会更新软件包,以解决 bug 或添加新功能。发生这种情况时,你可能需要使用该软件包的最新版本来利用新功能或已解决的 bug。要更新软件包,你可以使用 `go get` 命令使用 `-u` 标志:
135 |
136 | ``` shell
137 | go get -u github.com/gobuffalo/flect
138 | ```
139 |
140 | 如果在本地找不到该软件包,此命令也将安装该软件包。如果已经安装了它,Go 将尝试将软件包更新为最新版本。
141 |
142 | `go get` 命令始终检索可用的包装的最新版本。但是,可能还会对该软件包的之前的版本进行更新,这些版本仍然比你使用的更新,并且对你的程序中的更新非常有用。要检索包装的特定版本,你需要使用一个软件包管理工具,例如[Go Modules](https://github.com/golang/go/wiki/Modules)。
143 |
144 | 从 GO 1.11 开始,使用 Go Modules 来管理要导入的软件包的哪个版本。软件包管理的主题超出了本文的范围,但是你可以在[on the Go Modules GitHub page](https://github.com/golang/go/wiki/Modules)上阅读有关它的更多信息。
145 |
146 | ## 使用别名的方式导入软件包
147 |
148 | 如果你的本地软件包已经命名为与你正在使用的第三方软件包相同的包名时,则可能需要更改软件包名称。当发生这种情况时,使别名导入的方式是处理软件包名冲突的最佳方法。你可以通过将 `alias` 名称放在导入的软件包的前面来修改包装及其功能的名称及其功能。
149 |
150 | 该声明的结构看起来像这样:
151 |
152 | ``` go
153 | import another_name "package"
154 | ```
155 |
156 | 在此示例中,在 `random.go` 程序文件中修改 `fmt` 软件包的名称。我们将 `fmt` 的包名称更改为 `f`,以缩写它。我们的修改程序看起来像这样:
157 |
158 | random.go
159 |
160 | ``` go
161 | package main
162 |
163 | import (
164 | f "fmt"
165 | "math/rand"
166 | )
167 |
168 | func main() {
169 | for i := 0; i < 10; i++ {
170 | f.Printf("%d) %d\n", i, rand.Intn(25))
171 | }
172 | }
173 | ```
174 |
175 | 在程序中,我们现在将 `Printf` 函数称为 `f.Printf`,而不是 `fmt.Printf`。
176 |
177 | 虽然其他语言喜欢以别名的方式命名包以便于在程序中更加容易使用,但 GO 却不是。例如,与 `fmt` 软件包与 `f` 相反, [style guide](https://github.com/golang/go/wiki/CodeReviewComments#import-dot) 更加倾向于一致。
178 |
179 | 在重命名导入包以避免命名冲突时,你应该重命名本地导入的软件包或特定的项目中导入的包。例如,如果你有一个名为 `Strings` 的本地软件包,并且还需要导入称为 `strings` 的系统软件包,你应该重命名本地软件包而不是系统软件包。只要有可能,最好避免完全命名冲突。
180 |
181 | 在本节中,我们了解了如何以别名的方式导入软件包以避免与我们计划中的其他导入冲突。重要的是要记住,程序的可读性和清晰度很重要,因此你只能使用别名使代码更可读或何时需要避免命名冲突。
182 |
183 | ## 格式化导入
184 |
185 | 通过格式化导入,你可以将软件包分为特定的顺序,以使你的代码更加一致。此外,当惟一改变的是导入的排序顺序时,这将防止发生随机提交。由于格式化导入将防止随机提交,因此这将防止不必要的代码混乱和混淆代码审查。
186 |
187 | 大多数编辑器将自动为你格式化导入,或者让你配置编辑器以使用 [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) 工具。在编辑器中使用 `goimports` 被认为是标准实践,因为尝试手动维护导入的排序顺序可能是乏味的,而且容易出错。此外,如果进行了任何样式更改,则将更新 `goimports` 以反映这些样式更改。这样可以确保你和任何在代码上工作的人都将在你的 import 块中具有一致的样式。
188 |
189 | 这是格式化之前的示例导入块可能的样子:
190 |
191 | ``` go
192 | import (
193 | "fmt"
194 | "os"
195 | "github.com/digital/ocean/godo"
196 | "github.com/sammy/foo"
197 | "math/rand"
198 | "github.com/sammy/bar"
199 | )
200 |
201 | ```
202 |
203 | 运行 `goimport` 工具(或使用已安装它的大多数编辑器,保存文件将为你运行),现在你将具有以下格式:
204 |
205 | ``` go
206 | import (
207 | "fmt"
208 | "math/rand"
209 | "os"
210 |
211 | "github.com/sammy/foo"
212 | "github.com/sammy/bar"
213 |
214 | "github.com/digital/ocean/godo"
215 | )
216 | ```
217 |
218 | 请注意,它首先将所有标准库软件包分组,然后将第三方软件包与空白行分组。这使得更容易阅读和了解正在使用哪些软件包。
219 |
220 | 在本节中,我们了解到,使用 `goimports` 将保持我们所有导入块的正确格式,并防止在处理相同文件在开发人员之间产生不必要的代码混乱。
221 |
222 | ## 总结
223 |
224 | 当我们导入软件包时,我们可以调用未内置的功能。有些软件包是随着 GO 安装的标准库的一部分,有些软件包将通过 `go get` 来安装。
225 |
226 | 使用软件包可以使我们在利用现有代码时使程序更加健壮和强大。我们还可以为自己和其他程序员 [创建自己的软件包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}}),以便将来使用。
227 |
--------------------------------------------------------------------------------
/content/zh/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 如何用 Go 编写你的第一个程序
2 |
3 | ## 前言
4 |
5 | “Hello, World!” 程序是计算机编程中的经典且历史悠久的传统。 对于初学者来说,这是一个简单而完整的第一个程序,它是一个确保你的环境配置正确的好方法。
6 |
7 | 本教程将引导你在 Go 中创建此程序。 但是,为了使程序更有趣,你将修改传统的 “Hello, World!” 程序,以便它可以询问用户的姓名。 然后,你将在回复中使用该姓名。 完成本教程后,你将拥有一个运行起来如下所示的程序:
8 |
9 | ```text
10 | output
11 | Please enter your name.
12 | Sammy
13 | Hello, Sammy! I'm Go!
14 | ```
15 |
16 | ## 安装前提
17 |
18 | 在开始本教程之前,你需要一个本地的 Go 开发环境。你可以按照下面其中一个教程在你的计算机上进行设置:
19 |
20 | - [如何在 macOS 上安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/02-How_To_Install_Go_and_Set_Up_a_Local_Programming_Environment_on_macOS_DigitalOcean/)
21 | - [如何在 Ubuntu 18.04 上安装 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/)
22 | - [如何在 Windows 10 上安装 Go 和设置本地编程环境](https://gocn.github.io/How-To-Code-in-Go/docs/03-How_To_Install_Go_and_Set_Up_a_Local_Programming_Environment_on_Windows_10_DigitalOcean/)
23 |
24 | ## 第一步 — 编写最基本的 “Hello, World!” 程序
25 |
26 | 为了编写 Hello, World!” 程序,请打开一个命令行文本编辑器,例如 `nano`,然后创建一个新文件:
27 |
28 | ```bash
29 | nano hello.go
30 | ```
31 |
32 | 在 nano 中打开文本文件后,输入你的程序代码:
33 |
34 | ```go
35 | hello.go
36 | package main
37 |
38 | import "fmt"
39 |
40 | func main() {
41 | fmt.Println("Hello, World!")
42 | }
43 | ```
44 |
45 | 让我们分解下代码的各个部分。
46 |
47 | `package` 是一个 Go 关键字,它定义了这个文件属于哪个代码包。 每个文件夹只能有一个包,并且文件夹中每个 `.go` 文件必须在其文件顶部声明相同的包名。 在这个例子中,代码属于 `main` 包。
48 |
49 | `import` 是一个 Go 关键字,它告诉 Go 编译器你想在这个文件中使用哪些其他包。 在这里,你导入标准库附带的 `fmt` 包。 `fmt` 包提供了在开发时很有用的格式化和打印功能。
50 |
51 | `fmt.Println` 是一个 Go 函数,位于 `fmt` 包中,它告诉计算机将一些文本打印到屏幕上。
52 |
53 | 在 `fmt.Println` 函数后面跟着一系列字符,例如 `"Hello, World!"`,用引号括起来。 引号内的任何字符都称为 _string_。 `fmt.Println` 函数会在程序运行时将此字符串打印到屏幕上。
54 |
55 | 按 `CTRL` 和 `X` 键退出 `nano`。 当提示保存文件时,按 `Y`,然后按 `ENTER` 退出。
56 |
57 | 现在你可以试试你的程序了。
58 |
59 | ## 第二步 — 运行 Go 程序
60 |
61 | 你的 “Hello, World!” 程序写好之后,你就可以运行程序了。你可以用 `go` 命令然后后面跟着你刚刚创建的文件名。
62 |
63 | ```bash
64 | go run hello.go
65 | ```
66 |
67 | 程序执行后会显示以下输出:
68 |
69 | ```text
70 | output
71 | Hello, World!
72 | ```
73 |
74 | 让我们来探索下实际发生了什么。
75 |
76 | Go 程序在运行之前必须要先编译。当你使用文件名调用 `go run` 时,在本例中为 `hello.go`,`go` 命令将 _编译_ 应用程序,然后运行生成的二进制文件。 对于用 _编译型_ 编程语言编写的程序,编译器将获取程序的源代码并生成另一种类型的低级代码(例如机器代码)来生成可执行程序。
77 |
78 | Go 应用程序需要一个 `main` 包和一个确切的 **唯一** `main()` 函数,作为应用程序的入口点。 `main` 函数不接受任何参数并且不返回任何值。 相反,它告诉 Go 编译器应该将包编译为可执行包。
79 |
80 | 编译后,代码通过在 `main` 包中输入 `main()` 函数来执行。 它通过_调用_ `fmt.Println` 函数来执行`fmt.Println("Hello, World!")` 行。 `Hello, World!` 的字符串值被传递给函数。 在此示例中,字符串 `Hello, World!` 也称为 _参数_,因为它是传递给方法的值。
81 |
82 | `Hello, World!` 两边的引号不会打印到屏幕上,因为你用它们告诉 Go 你的字符串在哪里开始和结束。
83 |
84 | 在这一步中,你已经用 Go 编程创建了一个有效的“Hello, World!” 。 在下一步中,你将探索如何使程序更具交互性。
85 |
86 | ## 第三步 — 提示用户输入
87 |
88 | 每次运行程序时,它都会产生相同的输出。 在此步骤中,你可以把提示用户输入他们的姓名添加到程序中。 然后,你将在输出中使用他们的名字。
89 |
90 | 不要修改现有程序,而是使用 `nano` 编辑器创建一个名为 `greeting.go` 的新程序:
91 |
92 | ```bash
93 | nano greeting.go
94 | ```
95 |
96 | 首先,添加这段代码,提示用户输入他们的姓名:
97 |
98 | ```go
99 | package main
100 |
101 | import (
102 | "fmt"
103 | )
104 |
105 | func main() {
106 | fmt.Println("Please enter your name.")
107 | }
108 | ```
109 |
110 | 再一次,你使用 `fmt.Println` 函数将一些文本打印到屏幕上。
111 |
112 | 现在添加下面高亮行代码来存储用户的输入:
113 |
114 | ```go
115 | package main
116 |
117 | import (
118 | "fmt"
119 | )
120 |
121 | func main() {
122 | fmt.Println("Please enter your name.")
123 | var name string
124 | }
125 | ```
126 |
127 | `var name string` 行将使用 `var` _关键字_ 创建一个新变量。 你将变量命名为 “name” ,它的类型为 “string” 。
128 |
129 | 然后添加下面高亮行代码来捕捉用户的输入:
130 |
131 | ```go
132 | package main
133 |
134 | import (
135 | "fmt"
136 | )
137 |
138 | func main() {
139 | fmt.Println("Please enter your name.")
140 | var name string
141 | fmt.Scanln(&name)
142 | }
143 | ```
144 |
145 | `fmt.Scanln` 方法告诉计算机等待以换行符或 (`\n`) 字符结尾的键盘输入。 该方法会暂停程序,允许用户输入他们想要的任何文本。 当用户按下键盘上的 `ENTER` 键时,程序将继续。 然后捕获所有点击,包括 `ENTER` ,然后将其转换为字符串。
146 |
147 | 你想在程序中使用这些字符输出,因此你通过将这些字符 _写入_ 到被称为 `name` 的字符串 _变量_ 中来保存这些字符。 Go 将该字符串存储在计算机的内存中,直到程序完成运行。
148 |
149 | 最后,在程序中添加以下高亮行来打印输出:
150 |
151 | ```go
152 | package main
153 |
154 | import (
155 | "fmt"
156 | )
157 |
158 | func main() {
159 | fmt.Println("Please enter your name.")
160 | var name string
161 | fmt.Scanln(&name)
162 | fmt.Printf("Hi, %s! I'm Go!", name)
163 | }
164 | ```
165 |
166 | 这一次,你使用的是 `fmt.Printf`,而不是再次使用 `fmt.Println` 方法。 `fmt.Printf` 函数接受一个字符串,并使用特殊的打印 _占位符_, (`%s`),将 `name` 的值注入到字符串中。 你这样做是因为 Go 不支持 _字符串插值_ ,它可以让你获取分配给变量的值并将其插入在字符串中。
167 |
168 | 按 `CTRL` 和 `X` 键退出 `nano`。 当提示保存文件时,按 `Y`。
169 |
170 | 现在运行程序。 系统将提示你输入你的姓名,因此请输入并按 `ENTER`。 输出可能不完全符合你的预期:
171 |
172 | ```text
173 | output
174 | Please enter your name.
175 | Sammy
176 | Hi, Sammy
177 | ! I'm Go!
178 | ```
179 |
180 | 而不是 `Hi, Sammy! I'm Go!`,名字后面有一个换行符。
181 |
182 | 该程序捕获了我们 **所有** 的点击,包括我们按下以告诉程序继续的 `ENTER` 键。 在字符串中,按 `ENTER` 键会创建一个特殊字符,该字符会创建一个新行。 该程序的输出完全按照你的要求执行; 它正在显示你输入的文本,包括新行。 这不是你期望的输出,但你可以使用其他功能对其进行修复。
183 |
184 | 在你的编辑器里面打开 `greeting.go`
185 |
186 | ```bash
187 | nano greeting.go
188 | ```
189 |
190 | 在你的程序里定位到下面这一行:
191 |
192 | ```go
193 | ...
194 | fmt.Scanln(&name)
195 | ...
196 | ```
197 |
198 | 在其后面增加下面一行:
199 |
200 | ```go
201 | name = strings.TrimSpace(name)
202 | ```
203 |
204 | 上面使用了 Go 标准库 `strings` 包中的 `TrimSpace` 函数,来处理你使用 `fmt.Scanln` 捕获的字符串。 `strings.TrimSpace` 函数从字符串的开头和结尾删除任何空格字符,包括换行符。 在这种情况下,它会删除你按下 `ENTER` 时创建的字符串末尾的换行符。
205 |
206 | 要使用 `strings` 包,你需要在程序顶部导入它。
207 |
208 | 在你的程序找到下面这几行代码:
209 |
210 | ```go
211 | import (
212 | "fmt"
213 | )
214 | ```
215 |
216 | 增加下面几行来导入 `strings` 包:
217 |
218 | ```go
219 | import (
220 | "fmt"
221 | "strings"
222 | )
223 | ```
224 |
225 | 你的程序现在包含了以下代码:
226 |
227 | ```go
228 | package main
229 |
230 | import (
231 | "fmt"
232 | "strings"
233 | )
234 |
235 | func main() {
236 | fmt.Println("Please enter your name.")
237 | var name string
238 | fmt.Scanln(&name)
239 | name = strings.TrimSpace(name)
240 | fmt.Printf("Hi, %s! I'm Go!", name)
241 | }
242 | ```
243 |
244 | 按 `CTRL` 和 `X` 键退出 `nano`。 当提示保存文件时,按 `Y`。
245 |
246 | 再次运行程序:
247 |
248 | ```bash
249 | go run greeting.go
250 | ```
251 |
252 | 这一次,在你输入你的名字并按 `ENTER` 后,你会得到预期的输出:
253 |
254 | ```text
255 | output
256 | Please enter your name.
257 | Sammy
258 | Hi, Sammy! I'm Go!
259 | ```
260 |
261 | 你现在有一个 Go 程序,它接受用户的输入并将其打印回屏幕。
262 |
263 | ## 总结
264 |
265 | 在本教程中,你编写了 一个 “Hello, World!” 程序,可以从用户那里获取输入、处理结果并显示输出。 既然你有一个基本程序可以使用,请尝试进一步扩展你的程序。 例如,询问用户最喜欢的颜色,让程序说它最喜欢的颜色是红色。 你甚至可以尝试使用相同的技术来创建一个简单的 Mad-Lib 程序。
--------------------------------------------------------------------------------
/content/zh/docs/18-Creating_Custom_Errors_in_Go_DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中创建自定义错误
2 |
3 | ## 介绍
4 |
5 | GO 标准库提供了[`errors.New`and`fmt.Errorf`](https://gocn.github.io/How-To-Code-in-Go/docs/17-Handling_Errors_in_Go_DigitalOcean/#%E5%88%9B%E5%BB%BA%E9%94%99%E8%AF%AF) 这两种方法来在创建错误。但是这两种方法并不能满足你的用户或者后期调试时提供更加复杂的错误信息或者报告发生了什么。为了传递这种更复杂的错误信息并获得更多功能,我们可以实现标准库 `error` 接口类型。
6 |
7 | `error` 接口定义如下:
8 |
9 | ```go
10 | type error interface {
11 | Error() string
12 | }
13 |
14 | ```
15 |
16 | [`内置`](https://golang.org/pkg/builtin/) 软件包将 `error` 定义为具有单个 `Error()` 方法的接口,该接口将错误消息字符串作为返回。通过实现此方法,我们可以将定义的任何类型转换为自己的错误。
17 |
18 | 让我们尝试运行以下示例以查看 `error` 接口的实现:
19 |
20 | ```go
21 | package main
22 |
23 | import (
24 | "fmt"
25 | "os"
26 | )
27 |
28 | // 定义一个 MyError 的接口体
29 | type MyError struct{}
30 |
31 | // 实现 error 接口的 Error 方法
32 | func (m *MyError) Error() string {
33 | return "boom"
34 | }
35 |
36 | // 定义 sayHello 函数
37 | // 函数返回类型为 string 和 error
38 | func sayHello() (string, error) {
39 | // 由于 *MyError 实现了 error 接口
40 | // 所以 &MyError{} 可以作为 error 对象返回
41 | return "", &MyError{}
42 | }
43 |
44 | func main() {
45 | s, err := sayHello()
46 | if err != nil {
47 | fmt.Println("unexpected error: err:", err)
48 | os.Exit(1)
49 | }
50 | fmt.Println("The string:", s)
51 | }
52 |
53 | ```
54 |
55 | 我们将看到以下输出:
56 |
57 | ```shell
58 | # Output
59 | unexpected error: err: boom
60 | exit status 1
61 | ```
62 |
63 | 在这里,我们创建了一个新的空结构类型 `MyError`,并在其上定义了 `Error()` 方法。`Error()` 方法返回字符串 `"Boom"`。
64 |
65 | 在 `main()` 中,我们调用 `sayhello` 函数,该函数返回一个空字符串和一个新 `MyError` 实例。由于 `sayhello` 将始终返回错误,因此在`main()` 中的 `if` 语句主体内的 `fmt.Println` 调用将始终执行。我们使用 `fmt.Println` 来打印短前缀字符串 `"unexpected error:"`以及在保存`MyError` 实例中的在 `err` 变量。
66 |
67 | > 值得注意的是,我们不必直接调用 `Error()`,因为 `fmt` 包能够自动检测到已经实现了 `error` 接口。它 [透明]() 地调用 `Error()` 来获取字符串 `"hoom"`,并将其与前缀字符串 `"unexpected error: err:"` 相连。
68 |
69 | ## 自定义错误收集详细信息
70 |
71 | 有时,自定义错误是捕获详细错误信息的最有效的方式。例如,假设我们要捕获 HTTP 请求产生的错误的状态代码;运行以下程序以查看 `error` 的实现,使我们能够清晰捕获该信息:
72 |
73 | ```go
74 | package main
75 |
76 | import (
77 | "errors"
78 | "fmt"
79 | "os"
80 | )
81 |
82 | type RequestError struct {
83 | StatusCode int
84 |
85 | Err error
86 | }
87 |
88 | func (r *RequestError) Error() string {
89 | return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
90 | }
91 |
92 | func doRequest() error {
93 | return &RequestError{
94 | StatusCode: 503,
95 | Err: errors.New("unavailable"),
96 | }
97 | }
98 |
99 | func main() {
100 | err := doRequest()
101 | if err != nil {
102 | fmt.Println(err)
103 | os.Exit(1)
104 | }
105 | fmt.Println("success!")
106 | }
107 |
108 | ```
109 |
110 | 我们将看到以下输出:
111 |
112 | ```shell
113 | # Output
114 | status 503: err unavailable
115 | exit status 1
116 | ```
117 |
118 | 在此示例中,我们创建了创建一个错误的 `RequestError` 的新实例, 其中包含一个状态码和使用标准库提供的 `errors.New` 函数创建的 `err`。之后,如前所述,我们使用 `fmt.Println` 打印了错误信息。
119 |
120 | 在 `RequestError` 的 `Error()` 方法中,我们使用创建 `error` 对象时提供的信息和 `fmt.Sprintf` 函数构造字符串。
121 |
122 | ## 类型断言和自定义错误
123 |
124 | `error` 接口仅公开一种方法,但是为了正确处理错误, 我们可能需要访问 `error` 实现类型的其他方法。例如,我们可能有几个暂时的自定义错误实现,可以通过 `Temporary()` 方法的存在来重述。
125 |
126 | 接口为类型提供的更广泛的方法集提中供了一个狭窄的视图,因此,我们必须使用类型断言来更改视图正在显示的方法,或完全删除它。
127 |
128 | 下面的示例增加了前面显示的 `RequestError` 具有 `Temporary()` 方法,该方法将指示调用者是否应重试请求:
129 |
130 | ```go
131 | package main
132 |
133 | import (
134 | "errors"
135 | "fmt"
136 | "net/http"
137 | "os"
138 | )
139 |
140 | type RequestError struct {
141 | StatusCode int
142 |
143 | Err error
144 | }
145 |
146 | func (r *RequestError) Error() string {
147 | return r.Err.Error()
148 | }
149 |
150 | func (r *RequestError) Temporary() bool {
151 | // 如果状态码是 503 返回 true
152 | return r.StatusCode == http.StatusServiceUnavailable // 503
153 | }
154 |
155 | func doRequest() error {
156 | return &RequestError{
157 | StatusCode: 503,
158 | Err: errors.New("unavailable"),
159 | }
160 | }
161 |
162 | func main() {
163 | err := doRequest()
164 | if err != nil {
165 | fmt.Println(err)
166 | // 进行类型断言
167 | re, ok := err.(*RequestError)
168 | // 如果类型断言成功
169 | if ok {
170 |
171 | if re.Temporary() {
172 | fmt.Println("This request can be tried again")
173 | } else {
174 | fmt.Println("This request cannot be tried again")
175 | }
176 | }
177 | os.Exit(1)
178 | }
179 |
180 | fmt.Println("success!")
181 | }
182 |
183 | ```
184 |
185 | 我们将看到以下输出:
186 |
187 | ```shell
188 | # Output
189 | unavailable
190 | This request can be tried again
191 | exit status 1
192 | ```
193 |
194 | 在 `main()` 中,我们调用 `doRequest()` 将错误接口返回给我们。我们首先打印由 `Error()` 方法返回的错误消息。接下来,我们尝试通过使用类型的断言 `re, ok := err.(*RequestError)`。如果类型断言成功,我们然后使用 `Temporary()` 方法来查看此错误是否是临时错误。由于`doRequest()` 设置的状态代码为 `503`,它匹配 `HTTP.Statusserviceunavailable`,因此将返回 `true`,并且要打印`"This request can be tried again"` 的原因。实际上,我们将提出另一个请求,而不是打印消息。
195 |
196 | ## 包装错误
197 |
198 | 通常,错误是从程序的外部产生(例如:数据库,网络连接等)。这些错误提供的错误消息不能够帮助任何人找到错误的根源。有必要在错误消息开始时,将错误与额外信息包装,将为成功调试提供一些必要的上下文。
199 |
200 | 下面的示例说明了我们如何将一些上下文信息附加到从其他功能中返回的其他隐性错误:
201 |
202 | ```go
203 | package main
204 |
205 | import (
206 | "errors"
207 | "fmt"
208 | )
209 |
210 | // 定义一个错误的包装类型
211 | type WrappedError struct {
212 | // 上下文信息
213 | Context string
214 | // 具体错误内容
215 | Err error
216 | }
217 |
218 | func (w *WrappedError) Error() string {
219 | return fmt.Sprintf("%s: %v", w.Context, w.Err)
220 | }
221 |
222 | func Wrap(err error, info string) *WrappedError {
223 | return &WrappedError{
224 | Context: info,
225 | Err: err,
226 | }
227 | }
228 |
229 | func main() {
230 | err := errors.New("boom!")
231 | err = Wrap(err, "main")
232 |
233 | fmt.Println(err)
234 | }
235 |
236 | ```
237 |
238 | 我们将看到以下输出
239 |
240 | ```shell
241 | # Output
242 | main: boom!
243 | ```
244 |
245 | `WrappedError` 是一个具有两个字段的结构:字符串类型的 `context` 字段和 `error`, 这让 `WrappedError` 提供了更多信息。当调用 `Error()` 方法时,我们再次使用 `fmt.Sprintf` 打印上下文消息和 `error`(`fmt.Sprintf` 也会隐式调用 `err` 的 `Error()` 方法)。
246 |
247 | 在 `main()` 中,我们使用 `errors.New` 创建一个错误,然后我们使用定义的 `Wrap` 函数包装该错误。这使我们可以指出此错误是在 `"main"` 中生成的。另外,由于我们的 `WrappedError` 也是一个 `error`,因此我们也可以包装其它的`WrappedError` - 这将使我们看到链条来帮助我们追踪错误源。在标准库的一点帮助下,我们甚至可以在错误中嵌入完整的堆栈跟踪。
248 |
249 | ## 总结
250 |
251 | 由于 `error` 接口只提供一种方法,我们已经看到,在为不同情况提供不同类型的错误方面,我们有很大的灵活性。这可以包含所有内容,从传达多个信息作为错误的一部分到实现 [指数退回](https://en.wikipedia.org/wiki/Exponential_backoff)。尽管表面上的错误处理机制似乎很简单,但我们可以使用这些自定义错误来处理常见和不常见情况。
252 |
253 | GO 有另一种传达意外行为的机制,panic。在错误处理系列的下一篇文章中,我们将检查恐慌 - 它们是什么以及如何处理它们。
254 |
--------------------------------------------------------------------------------
/content/zh/docs/14-Understanding_Boolean_Logic_in_Go.md:
--------------------------------------------------------------------------------
1 | # 了解 Go 中的布尔逻辑
2 |
3 | 布尔数据类型(`bool`)可以是两个值中的一个,即**真**或**假**。布尔型数据在编程中被用来进行比较和控制程序的流程。
4 |
5 | Boolean 在 Go 中的数据类型是`bool`,全部小写。值 `true` 和 `false` 总是分别用小写的 `t` 和 `f`,因为它们是 Go 中的特殊值。
6 |
7 | 本教程将涵盖你需要了解 `bool` 数据类型如何工作的基础知识,包括布尔比较、逻辑运算符和真值表。
8 |
9 | ## 比较运算符
10 |
11 | 在编程中,比较运算符被用来比较数值,并计算为一个单一的布尔值,即真或假。
12 |
13 | 下表展示了布尔比较运算符。
14 |
15 | 运算符的含义
16 |
17 | `==` 等于
18 |
19 | `!=` 不等于
20 |
21 | `<` 少于
22 |
23 | `>` 大于
24 |
25 | `<=` 少于等于
26 |
27 | `>=` 大于等于
28 |
29 |
30 | 为了了解这些运算符的工作原理,我们在 Go 程序中把两个整数分配给两个变量:
31 |
32 | ```go
33 | x := 5
34 | y := 8
35 | ```
36 |
37 | 在这个例子中,由于 `x` 的值是`5`,`y` 的值是 `8`,所以 `x` 小于 `y`。
38 |
39 | 使用这两个变量和它们的相关值,让我们回忆一下之前的运算符。在这个程序中,你用 Go 打印出每个比较运算符的值是真还是假。为了帮助更好地理解这个输出,Go 打印一个字符串来显示它正在计算的内容。
40 |
41 | ```go
42 | package main
43 |
44 | import "fmt"
45 |
46 | func main() {
47 | x := 5
48 | y := 8
49 |
50 | fmt.Println("x == y:", x == y)
51 | fmt.Println("x != y:", x != y)
52 | fmt.Println("x < y:", x < y)
53 | fmt.Println("x > y:", x > y)
54 | fmt.Println("x <= y:", x <= y)
55 | fmt.Println("x >= y:", x >= y)
56 | }
57 | ```
58 |
59 | ```go
60 | Output
61 | x == y: false
62 | x != y: true
63 | x < y: true
64 | x > y: false
65 | x <= y: true
66 | x >= y: false
67 | ```
68 |
69 | 遵循数学逻辑,Go 从表达式中计算了以下内容:
70 |
71 | * 5(`x`)等于 8(`y`)吗?**假**
72 | * 5 不等于 8 吗?**真**
73 | * 5 小于 8 吗?**真**
74 | * 5 是否大于 8?**假**
75 | * 5 是否小于或等于 8?**真**
76 | * 5 不小于或等于 8 吗?**假**
77 |
78 | 虽然这里使用的是整数,但你可以用浮点数来代替。
79 |
80 | 字符串也可以和布尔运算符一起使用。它们是区分大小写的,除非你使用一个额外的字符串方法。
81 |
82 | 你可以看一下字符串在实践中是如何比较的:
83 |
84 | ```go
85 | Sammy := "Sammy"
86 | sammy := "sammy"
87 |
88 | fmt.Println("Sammy == sammy: ", Sammy == sammy)
89 | ```
90 |
91 | ```go
92 | Output
93 | Sammy == sammy: false
94 | ```
95 |
96 | 字符串 `Sammy` 不等于字符串 `sammy`,因为它们不完全相同;一个以大写字母 `S` 开头,另一个以小写字母 `s` 开头。但是,如果你添加了另一个变量,该变量被分配了 `Sammy` 的值,那么它们的值将相等。
97 |
98 | ```go
99 | Sammy := "Sammy"
100 | sammy := "sammy"
101 | alsoSammy := "Sammy"
102 |
103 | fmt.Println("Sammy == sammy: ", Sammy == sammy)
104 | fmt.Println("Sammy == alsoSammy", Sammy == alsoSammy)
105 | ```
106 |
107 | ```go
108 | Output
109 | Sammy == sammy: false
110 | Sammy == alsoSammy true
111 | ```
112 |
113 | 你还可以使用其他比较运算符,包括 `>` 和 `<` 来比较两个字符串。Go 将使用字符的 ASCII 值对这些字符串进行按字母顺序的比较。
114 |
115 | 你也可以用比较运算符计算布尔值:
116 |
117 | ```go
118 | t := true
119 | f := false
120 |
121 | fmt.Println("t != f: ", t != f)
122 | ```
123 |
124 | ```go
125 | Output
126 | t != f: true
127 | ```
128 |
129 | 前面的代码块计算了 `true` 不等于 `false`。
130 |
131 | 注意两个运算符 `=` 和 `==` 之间的区别。
132 |
133 | ```go
134 | x = y // Sets x equal to y
135 | x == y // Evaluates whether x is equal to y
136 | ```
137 |
138 | 第一个 `=` 是赋值运算符,它将设置一个值等于另一个值。第二个 `==` 是一个比较运算符,将评估两个值是否相等。
139 |
140 |
141 | ## 逻辑运算符
142 |
143 | 有两个逻辑运算符被用来比较数值。它们将表达式评估为布尔值,返回 `true` 或 `false`。这些运算符是 `&&`,`||`,和 `!`,下面的列表中为定义:
144 |
145 | **&&** (`x && y`) 是 `and` 运算符。如果两个语句都是真,它就是真。
146 | **||** (`x || y`) 是 `or` 运算符。如果至少有一个语句是真,它就是真。
147 | **!** (`!x`)是 `not` 运算符。只有当语句为假时,它才为真。
148 |
149 | 逻辑运算符通常用于计算两个或多个表达式是真还是假。例如,它们可以用来确定成绩是否合格,以及学生是否在课程中注册,如果这两种情况都是真,那么该学生将在系统中被分配一个成绩。另一个例子是根据用户是否有商店信用或在过去6个月内有购买行为,来确定用户是否是一个网上商店的有效活跃客户。
150 |
151 | 为了理解逻辑运算符的工作原理,我们来评估三个表达式:
152 |
153 | ```go
154 | fmt.Println((9 > 7) && (2 < 4)) // Both original expressions are true
155 | fmt.Println((8 == 8) || (6 != 6)) // One original expression is true
156 | fmt.Println(!(3 <= 1)) // The original expression is false
157 | ```
158 |
159 | ```go
160 | Output
161 | true
162 | true
163 | true
164 | ```
165 |
166 | 在第一种情况下,`fmt.Println((9 > 7) && (2 < 4))`,`9 > 7` 和 `2 < 4` 都计算为真,因为使用了`and` 运算符。
167 |
168 | 在第二种情况下,`fmt.Println((8 == 8) || (6 != 6))`,由于`8 == 8` 为真,`6 != 6` 为假,因为使用了 `or` 运算符所以结果为真。如果你使用的是 `and` 运算符,那么这个结果将是假。
169 |
170 | 在第三种情况下,`fmt.Println(!(3 <= 1))`,`not` 运算符否定了 `3 <=1` 返回的错误值。
171 |
172 | 让我们用浮点数代替整数,并以假为目标:
173 |
174 | ```go
175 | fmt.Println((-0.2 > 1.4) && (0.8 < 3.1)) // One original expression is false
176 | fmt.Println((7.5 == 8.9) || (9.2 != 9.2)) // Both original expressions are false
177 | fmt.Println(!(-5.7 <= 0.3)) // The original expression is true
178 | ```
179 |
180 | 在这个例子中:
181 |
182 | * `and` 必须至少有一个假则计算为假。
183 | * `or` 必须两个表达式都为假则计算为假。
184 | * `!` 必须使其内部表达式为真,新表达式才为假。
185 |
186 | 如果这些结果对你来说不清楚,可以通过一些[真值表](#truth-tables)来进一步澄清。
187 |
188 | 你也可以用 `&&`、`||` 和 `!` 来写复合语句:
189 |
190 | ```go
191 | !((-0.2 > 1.4) && ((0.8 < 3.1) || (0.1 == 0.1)))
192 | ```
193 |
194 | 先看一下最里面的表达式:`(0.8 < 3.1) || (0.1 == 0.1)`。这个表达式计算为 `true`,因为两个数学语句都是 `true`。
195 |
196 | 接下来,Go 将返回值 `true` 与下一个内部表达式相结合:`(-0.2 > 1.4) && (true)`。这个例子返回`false`,因为数学语句 `-0.2 > 1.4` 是假,而(`false`)和(`true`)返回 `false`。
197 |
198 | 最后,我们有一个外层表达式:`!(false)`,它的值是 `true`,所以如果我们把这个语句打印出来,最后的返回值是:
199 |
200 | ```go
201 | Output
202 | true
203 | ```
204 |
205 | 逻辑运算符 `&&`、`||` 和 `!` 用于计算并返回布尔值。
206 |
207 | ## 真值表
208 |
209 | 关于数学的逻辑分支,有很多东西需要学习,但你可以有选择地学习一些,以提高你编程时的算法思维。
210 |
211 | 下面是比较运算符 `==`,以及每个逻辑运算符 `&&`,`||` 和 `!` 的真值表。虽然你可能能够推理出它们,但记住它们也是有帮助的,因为这可以使你的编程决策过程更快。
212 |
213 | `==` (equal) 真值表
214 |
215 | x == y 返回
216 |
217 | true == true true
218 |
219 | true == false false
220 |
221 | false == true false
222 |
223 | false == false true
224 |
225 | `&&` (and) 真值表
226 |
227 | x and y 返回
228 |
229 | true and true true
230 |
231 | true and false false
232 |
233 | false and true false
234 |
235 | false and false false
236 |
237 | `||` (or) 真值表
238 |
239 | x or y 返回
240 |
241 | true or true true
242 |
243 | true or false true
244 |
245 | false or true true
246 |
247 | false or false false
248 |
249 | `!` (not) 真值表
250 |
251 | not x Returns
252 |
253 | not true false
254 |
255 | not false true
256 |
257 |
258 | 真值表是逻辑学中常用的数学表,在构建计算机编程中的算法(指令)时,真值表是很有用的,可以牢记在心。
259 |
260 | ## 使用布尔运算符进行流程控制
261 |
262 | 为了以流程控制语句的形式控制程序的流程和结果,你可以使用一个 _condition_ 和一个 _clause_ 。
263 |
264 | 一个 _condition_ 计算出一个布尔值的真或假,提出了一个在程序中做出决定的点。也就是说,一个条件会告诉你某个东西的值是真还是假。
265 |
266 | _clause_ 是跟在 _condition_ 后面的代码块,它决定了程序的结果。也就是说,"如果 `x` 是 `true`,那就往下执行"。
267 |
268 | 下面的代码块显示了一个比较运算符与条件语句协同工作的例子,以控制 Go 程序的流程:
269 |
270 | ```go
271 | if grade >= 65 { // Condition
272 | fmt.Println("Passing grade") // Clause
273 | } else {
274 | fmt.Println("Failing grade")
275 | }
276 | ```
277 |
278 | 这个程序将评估每个学生的成绩是合格还是不合格。如果一个学生的成绩是 `83`,第一条语句为 `true`,并且将触发 `Passing grade` 的打印语句。如果学生的成绩是 `59`,第一条语句为 `false`,所以程序将继续执行与 else 表达式相关的打印语句:`Failing grade`。
279 |
280 | 布尔运算符提出的条件,可以通过流程控制语句来决定程序的最终结果。
281 |
282 | ## 总结
283 |
284 | 本教程介绍了属于布尔类型的比较和逻辑运算符,以及真值表和使用布尔类型进行程序流程控制。
--------------------------------------------------------------------------------
/content/zh/docs/39-Using_ldflags_to_Set_Version_Information_for_Go_Applications.md:
--------------------------------------------------------------------------------
1 | # 用 ldflags 设置 Go 应用程序的版本信息
2 |
3 | ## 简介
4 |
5 | 当把应用程序部署到生产环境中时,用版本信息和其他元数据构建二进制文件将改善你的监控、日志和调试过程,增加识别信息来帮助跟踪随着时间推移后,应用程序的构建信息。这种版本信息通常包括高度动态的数据,如构建时间、构建二进制文件的机器或用户、[版本控制系统(VCS)](https://www.atlassian.com/git/tutorials/what-is-version-control)的提交 ID,等其他更多信息。因为这些值是不断变化的,将这些数据直接编码到源代码中,并在每次新的构建之前进行修改,是很繁琐的,而且容易出错:源文件可能会移动,[变量/常量]({{< relref "/docs/11-How_To_Use_Variables_and_Constants_in_Go.md" >}})在整个开发过程中可能会随着切换文件而改动,打断构建过程。
6 |
7 | 在 Go 中解决这个问题的一个方法是在使用`go build`命令时加上`-ldflags`,在构建时将动态信息插入二进制文件中,而不需要修改源代码。在这个标志中,`ld`代表[*linker*](https://en.wikipedia.org/wiki/Linker_(computing)),这个程序将编译后的源代码的不同部分连接成最终的二进制文件。`ldflags`就代表*linker 的标志*。之所以这样说,是因为它向底层的 Go 工具链 linker[`cmd/link`](https://golang.org/cmd/link)传递了一个标志,允许你在构建时从命令行中改变导入的包的值。
8 |
9 | 在本教程中,你将使用`-ldflags`在构建时改变变量的值,并将你自己的动态信息加入二进制,用一个将版本信息打印到屏幕上的应用程序作为示例应用程序。
10 |
11 | ## 前期准备
12 |
13 | 为了接下去在文章中的例子,你需要:
14 |
15 | - 按照[如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置 Go 的 workspace。
16 |
17 | ## 构建你的范例应用程序
18 |
19 | 在使用`ldflags`加入动态数据之前,你首先需要一个应用程序来插入信息。在这一步,你将制作这个应用程序,在这个阶段,它将只打印静态的版本信息。现在让我们来创建这个应用程序。
20 |
21 | 在你的`src`目录下,建立一个以你的应用程序命名的目录。本教程将使用叫`app`的应用程序:
22 |
23 | ```bash
24 | mkdir app
25 | ```
26 |
27 | 跳转你的目录到这个文件夹:
28 |
29 | ```bash
30 | cd app
31 | ```
32 |
33 | 然后,使用你喜欢的文本编辑器,在`main.go`创建你的程序的 entry point:
34 |
35 | ```bash
36 | nano main.go
37 | ```
38 |
39 | 现在,通过加入如下内容到你的程序内,来打印出版本信息:
40 |
41 | ```go
42 | package main
43 |
44 | import (
45 | "fmt"
46 | )
47 |
48 | var Version = "development"
49 |
50 | func main() {
51 | fmt.Println("Version:\t", Version)
52 | }
53 | ```
54 |
55 | 在`main()`函数内,你宣告了`Version`变量,然后打印[string]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}})类型的`Version`:紧跟着 tab 的字符,`\t`,然后是声明的变量。
56 |
57 | 现在,参数`Version`被定义为`development`,将作为 app 的默认版本。稍后,你将会修改这个值来符合官方版本编号,根据[semantic versioning format](https://semver.org/)来定义。
58 |
59 | 保存并退出该文件。完成后,构建并运行该应用程序,来确认它打印的是正确的版本:
60 |
61 | ```bash
62 | go build
63 | ./app
64 | ```
65 |
66 | 你将会看到如下输出:
67 |
68 | ```bash
69 | Output
70 | Version: development
71 | ```
72 |
73 | 你现在有一个打印默认版本信息的应用程序,但你还没有办法在构建时传入当前版本信息。在下一步,你将使用`-ldflags`和`go build`来解决这个问题。
74 |
75 | ## 在 `go build`中使用`ldflags`的方法
76 |
77 | 在前面提到的,`ldflags`代表*linker 标志*,用于向 Go 工具链中的底层 linker 传递标志。这是按以下语法进行的:
78 |
79 | ```bash
80 | go build -ldflags="-flag"
81 | ```
82 |
83 | 在这个例子中,我们向作为`go build`的一部分运行的`go tool link`命令传递了`flag`。这个命令在传递给`ldflags`的内容周围使用双引号,以避免其中字符串被分开,或者被命令行翻译为与我们想要的不同的字符。从这里,你可以传入[许多不同的`linker`标志](https://golang.org/cmd/link/)。为了本教程中的目的,我们将使用`-X`标志在链接时将信息写入变量,跟着的是参数的[package]({{< relref "/docs/20-Importing_Packages_in_Go_DigitalOcean.md" >}})路径和它的新值:
84 |
85 | ```bash
86 | go build -ldflags="-X 'package_path.variable_name=new_value'"
87 | ```
88 |
89 | 在引号内,现在有`X`选项和一个[键值对](https://gocn.github.io/How-To-Code-in-Go/docs/15-Understanding_Maps_in_Go/#%E9%94%AE%E5%92%8C%E5%80%BC),代表要改变的变量和它的新值。`.`字符将包路径和变量名称分开,单引号用于避免键值对被断开。
90 |
91 | 要在你的示例程序中替换`Version`变量,使用最后一个命令块中的语法,传入一个新的值并建立新的二进制。
92 |
93 | ```bash
94 | go build -ldflags="-X 'main.Version=v1.0.0'"
95 | ```
96 |
97 | 在这个命令中,`main`是`Version`变量的包路径,因为这个变量在`main.go`文件中。`Version`是你要写入的变量,`v1.0.0`是新的值。
98 |
99 | 为了使用`ldflags`,你想改变的值必须存在,并且是一个`string`类型的包级变量。这个变量可以是对外导出的也可以不是。变量的值不可以是`const`或者是需要通过调用函数后得到的结果赋值的。幸运的是,`Version`满足了所有的要求:它已经在`main.go`文件中被声明为一个变量,而且当前值(`development`)和期望值(`v1.0.0`)都是字符串。
100 |
101 | 一旦你的新`app`二进制文件构建起来,运行应用程序:
102 |
103 | ```bash
104 | ./app
105 | ```
106 |
107 | 你将会收到如下输出:
108 |
109 | ```bash
110 | Output
111 | Version: v1.0.0
112 | ```
113 |
114 | 通过`-ldflags`,你成功地把`Version`变量的值从`development`改成`v1.0.0`。
115 |
116 | 现在你已经在一个简单的应用程序构建时修改了一个`string`变量。使用`ldflags`,你可以在二进制文件中嵌入版本细节、许可信息等,只需使用命令行就可以发布。
117 |
118 | 在这个例子中,你改变的变量在`main`程序中,减少了确定路径名称的难度。但有时这些变量的路径寻找起来比较复杂。在下一步中,你将给子包中的变量赋值,来阐述确定更复杂的包路径的最佳方法。
119 |
120 | ## 锁定子包变量
121 |
122 | 在上一节中,你操作了`Version`变量,它位于应用程序的顶层包。但这不是常见的案例。通常情况下,将这些变量放在另一个包中更为实际,因为`main`不是一个可导入的包。为了在你的示例程序中模拟这一点,你将创建一个新的子包,`app/build`,它将存储关于二进制文件被构建的时间和发出构建命令的用户名称的信息。
123 |
124 | 要添加一个新的子包,首先在你的项目中添加一个名为`build'的新目录:
125 |
126 | ```bash
127 | mkdir -p build
128 | ```
129 |
130 | 然后创建一个名为`build.go`的新文件来保存新的变量:
131 |
132 | ```bash
133 | nano build/build.go
134 | ```
135 |
136 | 在你的文本编辑器中,添加`Time`和`User`这两个新变量
137 |
138 | ```go
139 | package build
140 |
141 | var Time string
142 |
143 | var User string
144 | ```
145 |
146 | `Time`变量将保存二进制文件建立的时间的字符串表示。`User`变量将保存构建二进制文件的用户名称。由于这两个变量总是有值,你不需要像对`Version`那样用默认值初始化这些变量。
147 |
148 | 保存并退出文件。
149 |
150 | 然后,打开`main.go`文件添加这些变量到你的应用程序中:
151 |
152 | ```bash
153 | nano main.go
154 | ```
155 |
156 | 在`main.go`中,添加如下高亮代码:
157 |
158 | ```go
159 | package main
160 |
161 | import (
162 | "app/build"
163 | "fmt"
164 | )
165 |
166 | var Version = "development"
167 |
168 | func main() {
169 | fmt.Println("Version:\t", Version)
170 | fmt.Println("build.Time:\t", build.Time)
171 | fmt.Println("build.User:\t", build.User)
172 | }
173 | ```
174 |
175 | 在这些代码里,你第一次引用`app/build`包,然后用打印`Version`的方式打印`build.Time`和`build.User`。
176 |
177 | 保存文件,然后从你的文本编辑器退出。
178 |
179 | 接下来,为了用`ldflags`锁定这些变量,你可以使用导入路径`app/build`,然后是`.User`或`.Time`,因为你已经知道导入的路径。 然而,为了模拟一种更复杂的情况,即不知道变量的导入路径,让我们改用 Go 工具链中的`nm`命令。
180 |
181 | `go tool nm`命令将输出在给定的可执行文件、对象文件或存档中涉及的符号。在这种情况下,符号指的是代码中的一个对象,例如一个定义的或导入的变量或函数。通过使用`nm`生成一个符号表,并使用`grep`搜索一个变量,你可以快速找到其路径信息。
182 |
183 | 注意:如果软件包名称中有任何非[ASCII](https://en.wikipedia.org/wiki/ASCII)字符,或者有`"`或`%`字符,`nm`命令将不能帮助你找到变量的路径,因为这是工具本身的限制。
184 |
185 | 要使用这个命令,首先要为`app`构建二进制文件:
186 |
187 | ```bash
188 | go build
189 | ```
190 |
191 | 现在`app`已经构建好了,将`nm`工具指向它,并在输出中搜索:
192 |
193 | ```bash
194 | go tool nm ./app | grep app
195 | ```
196 |
197 | 当运行时,`nm`工具将输出大量的数据。因为如此,前面的命令使用`|`将输出的数据输送给`grep`命令,然后搜索标题中带有一级`app`的数据。
198 |
199 | 你将会收到类似如下的输出:
200 |
201 | ```text
202 | Output
203 | 55d2c0 D app/build.Time
204 | 55d2d0 D app/build.User
205 | 4069a0 T runtime.appendIntStr
206 | 462580 T strconv.appendEscapedRune
207 | . . .
208 | ```
209 |
210 | 在这种情况下,结果集的前两行包含你要找的两个变量的路径。`app/build.Time`和`app/build.User`。
211 |
212 | 现在你知道了路径,再次构建应用程序,这次在构建时改变`版本`、`用户`和`时间`。要做到这一点,需要向`-ldflags`传递多个`-X`标志:
213 |
214 | ```bash
215 | go build -v -ldflags="-X 'main.Version=v1.0.0' -X 'app/build.User=$(id -u -n)' -X 'app/build.Time=$(date)'"
216 | ```
217 |
218 | 这里你传入了`id -u -n` Bash 命令来列出当前用户,以及`date`命令来列出当前日期。
219 |
220 | 构建好了可执行文件,运行该程序:
221 |
222 | ```bash
223 | ./app
224 | ```
225 |
226 | 该命令在 Unix 系统上运行时,将产生与下面类似的输出:
227 |
228 | ```text
229 | Output
230 | Version: v1.0.0
231 | build.Time: Fri Oct 4 19:49:19 UTC 2019
232 | build.User: sammy
233 | ```
234 |
235 | 现在你有一个包含版本和构建信息的二进制文件,在生产中解决问题时可以提供重要帮助。
236 |
237 | ## 总结
238 |
239 | 这个教程展示了,如果应用得当,`ldflags`可以成为一个强大的工具,在构建时向二进制文件注入有价值的信息。这样,你可以控制功能标志、环境信息、版本信息等等,而不需要对你的源代码进行修改。通过添加`ldflags`到你当前的构建工作流程中,你可以最大限度地发挥 Go 自成一体的二进制的发布格式的优势。
240 |
--------------------------------------------------------------------------------
/content/zh/docs/35-How_To_Build_and_Install_Go_Programs.md:
--------------------------------------------------------------------------------
1 | # 如何构建和安装 Go 程序
2 |
3 | ## 简介
4 |
5 | 到目前为止,在我们的 [How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)中,你已经使用了[`go run`](https://gocn.github.io/How-To-Code-in-Go/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean/#%E7%AC%AC%E4%BA%8C%E6%AD%A5--%E8%BF%90%E8%A1%8C-go-%E7%A8%8B%E5%BA%8F) 命令来自动编译你的源代码并生成可执行文件。虽然这个命令对于在命令行上测试你的代码很有用,但是分发或部署你的应用程序则需要将你的代码构建成一个可共享的二进制可执行文件,或者一个包含机器字节码的单一文件来运行你的应用程序。要做到这一点,你可以使用 Go 工具链来构建和安装你的程序。
6 |
7 | 在 Go 中,将源代码转译成二进制可执行文件的过程被称为构建。一旦这个可执行文件被构建,它将不仅包含你的应用程序,还包含在目标平台上执行二进制文件所需的所有支持代码。这意味着 Go 二进制文件不需要 Go 工具链等系统依赖就可以在新系统上运行。将这些可执行文件放在自己系统的可执行文件路径中,就可以在系统的任何地方运行程序,这与把程序安装到你的操作系统上是一样的。
8 |
9 | 在本教程中,你将使用 Go 工具链来运行、构建和安装一个示例 `Hello, World!` 程序,让你有效地使用、分发和部署未来的应用程序。
10 |
11 | ## 前置条件
12 |
13 | 要遵循本文的例子,你将需要:
14 |
15 | - 按照[如何安装 Go 与设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作区。
16 |
17 | ## 第 1 步 - 设置和运行 Go 二进制文件
18 |
19 | 首先,创建一个应用程序,作为演示 Go 工具链的例子。要做到这一点,你将使用[如何用 Go 写第一个程序]({{< relref "/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md" >}})教程中的经典程序 "Hello, World!"。
20 |
21 | 在你的 `src` 目录下创建一个名为 `greeter` 的目录:
22 |
23 | ```bash
24 | mkdir greeter
25 | ```
26 |
27 | 接下来,进入新创建的目录,在你选择的文本编辑器中创建 `main.go` 文件:
28 |
29 | ```bash
30 | cd greeter
31 | nano main.go
32 | ```
33 |
34 | 打开文件,添加以下内容:
35 |
36 | ```go
37 | # src/greeter/main.go
38 |
39 | package main
40 |
41 | import "fmt"
42 |
43 | func main() {
44 | fmt.Println("Hello, World!")
45 | }
46 | ```
47 |
48 | 当程序运行时,这个程序将在控制台打印 `Hello, World!` 这句话,然后程序将成功退出。
49 |
50 | 保存并退出该文件。
51 |
52 | 要测试这个程序,使用 `go run` 命令,就像你在之前的教程中做的那样:
53 |
54 | ```bash
55 | go run main.go
56 | ```
57 |
58 | 你会收到以下输出:
59 |
60 | ```bash
61 | Output
62 | Hello, World!
63 | ```
64 |
65 | 如前文所述,`go run` 命令将你的源文件构建成可执行的二进制文件,然后运行编译后的程序。然而,本教程的目的是以一种可以随意分享和分发的方式来构建二进制文件。要做到这一点,你将在下一步使用 `go build` 命令。
66 |
67 | ## 第 2 步 -- 创建 Go 模块,建立 Go 二进制文件
68 |
69 | Go 程序和库是围绕着模块的核心概念建立的。一个模块包含了你的程序所使用的库的信息,以及要使用这些库的什么版本。
70 |
71 | 为了告诉 Go 这是一个 Go 模块,你需要使用 go mod 命令[创建一个 Go 模块](https://www.digitalocean.com/community/tutorials/how-to-use-go-modules):
72 |
73 | ```bash
74 | go mod init greeter
75 | ```
76 |
77 | 这将创建 `go.mod` 文件,其中包含模块的名称和用于构建模块的 Go 的版本。
78 |
79 | ```bash
80 | Output
81 | go: creating new go.mod: module greeter
82 | go: to add module requirements and sums:
83 | go mod tidy
84 | ```
85 |
86 | Go 会提示你运行 `go mod tidy`,以便在将来该模块的需求发生变化时更新它,现在运行它不会有额外的效果。
87 |
88 | ## 第 3 步 - 用 `go build` 构建 Go 二进制文件
89 |
90 | 使用 `go build`,你可以为我们的示例 Go 应用程序生成可执行的二进制文件,以便在任何你想要的地方分发和部署该程序。
91 |
92 | 在 `main.go` 尝试一下,在你的 `greeter` 目录下,运行以下命令:
93 |
94 | ```bash
95 | go build
96 | ```
97 |
98 | 如果你没有给这个命令提供参数,`go build` 将自动编译你当前目录下的 `main.go` 程序。该命令将包括该目录中所有的 `*.go` 文件。它还将编译所有支持代码,以便能够在任何具有相同系统结构的计算机上执行二进制文件,无论该系统是否有 `.go` 源文件,甚至是否安装了 Go。
99 |
100 | 在这种情况下,你把你的 `greeter` 程序构建在一个可执行文件中,并被添加到你的当前目录中。通过运行 `ls` 命令来检查:
101 |
102 | ```bash
103 | ls
104 | ```
105 |
106 | 如果你运行的是 macOS 或 Linux 系统,你会发现一个以你构建程序的目录命名的新可执行文件:
107 |
108 | ```bash
109 | Output
110 | greeter main.go go.mod
111 | ```
112 |
113 | ```plaintext
114 | 注:在Windows上,你的可执行文件将是 greeter.exe。
115 | ```
116 |
117 | 默认情况下,`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" >}})文章中了解更多信息。
118 |
119 | 现在,你已经创建了你的可执行文件,运行它以确保二进制文件已被正确构建。在 macOS 或 Linux 上,运行以下命令:
120 |
121 | ```bash
122 | ./greeter
123 | ```
124 |
125 | 在 Windows 上,运行:
126 |
127 | ```bash
128 | greeter.exe
129 | ```
130 |
131 | 二进制文件的输出将与你用 `go run` 运行程序时的输出一致:
132 |
133 | ```bash
134 | Output
135 | Hello, World!
136 | ```
137 |
138 | 现在你已经创建了一个单一的可执行二进制文件,它不仅包含你的程序,还包含运行该二进制文件所需的所有系统代码。因为这个文件将始终运行同一个程序,你现在可以将这个程序分发到新的系统中,或者将其部署到服务器上。
139 |
140 | 在下一节中,本教程将解释二进制文件是如何命名的,以及你如何改变它,以便你能更好地控制程序的构建过程。
141 |
142 | ## 第 4 步 - 改变二进制名称
143 |
144 | 现在你知道了如何生成可执行文件,下一步则是确定 Go 如何为二进制文件选择一个名字,并为你的项目定制这个名字。
145 |
146 | 当你运行 `go build` 时,默认情况下 Go 会自动决定生成的可执行文件的名称。它通过使用你之前创建的模块来实现这一目的。当运行 `go mod init greeter` 命令时,它创建了名为 `greeter` 的模块,这就是为什么生成的二进制文件被命名为 `greeter`。
147 |
148 | 让我们仔细看看这个模块方法。如果你的项目中有一个 `go.mod` 文件,其中有一个 `module` 声明,像下面这样:
149 |
150 | ```go
151 | # go.mod
152 | module github.com/sammy/shark
153 | ```
154 |
155 | 那么生成的可执行文件默认名称将是 `shark`。
156 |
157 | 在需要特定命名规则的更复杂的程序中,这些默认值并不总是你命名二进制文件的最佳选择。在这些情况下,最好用 `-o` 标识来定制你的输出。
158 |
159 | 为了测试这一点,把你在上一节中制作的可执行文件的名称改为 `hello`,并把它放在一个名为 `bin` 的子文件夹中。你不需要创建这个文件夹,Go 会在构建过程中自行完成。
160 |
161 | 运行以下带有 `-o` 标志的 `go build` 命令:
162 |
163 | ```bash
164 | go build -o bin/hello
165 | ```
166 |
167 | `-o` 标志使 Go 将命令的输出与你选择的任何参数相匹配。在本例中,结果是在一个名为 `bin` 的子文件夹中产生一个名为 `hello` 的新可执行文件。
168 |
169 | 要测试新的可执行文件,请切换到新目录并运行二进制文件:
170 |
171 | ```bash
172 | cd bin
173 | ./hello
174 | ```
175 |
176 | 你将收到以下输出:
177 |
178 | ```bash
179 | Output
180 | Hello, World!
181 | ```
182 |
183 | 现在你可以自定义你的可执行文件名称,以适应项目需要,这完成了我们对如何在 Go 中构建二进制文件的调研。但是使用 `go build`,你仍然只限于在当前目录下运行你的二进制文件。为了在系统的任何地方使用新建立的可执行文件,你可以用 `go install` 来安装它们。
184 |
185 | ## 第 5 步 - 用 `go install` 安装 Go 程序
186 |
187 | 到目前为止,在这篇文章中,我们已经讨论了如何从我们的 `.go` 源文件中生成可执行二进制文件。这些可执行文件有助于分发、部署和测试,但它们还不能在其源文件目录之外执行。如果你想在 shell 脚本或其他工作流程中使用你的程序,这将是一个问题。为了使这些程序更容易使用,你可以把它们安装到你的系统中,并从任何地方访问它们。
188 |
189 | 为了理解这一点的含义,你将使用 `go install` 命令来安装你的示例程序。
190 |
191 | `go install` 命令与 `go build` 几乎相同,但它不是将可执行文件留在当前目录或由 `o` 标志编译到指定的目录中,而是将可执行文件放到 `$GOPATH/bin` 目录中。
192 |
193 | 要找到你的 `$GOPATH` 目录的位置,请运行以下命令:
194 |
195 | ```bash
196 | go env GOPATH
197 | ```
198 |
199 | 你收到的输出会有所不同,但默认是你的 `$HOME` 目录下的 `go` 目录:
200 |
201 | ```bash
202 | Output
203 | $HOME/go
204 | ```
205 |
206 | 由于 `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 工作空间**步骤中有所涉及。
207 |
208 | 设置好 `$GOPATH/bin` 目录后,切回你的 `greeter` 目录:
209 |
210 | ```bash
211 | cd ..
212 | ```
213 |
214 | 现在运行安装命令:
215 |
216 | ```bash
217 | go install
218 | ```
219 |
220 | 这将构建你的二进制文件并将其放在 `$GOPATH/bin` 中。要测试这一点,请运行以下程序:
221 |
222 | ```bash
223 | ls $GOPATH/bin
224 | ```
225 |
226 | 这将列出 `$GOPATH/bin` 的内容:
227 |
228 | ```bash
229 | Output
230 | greeter
231 | ```
232 |
233 | ```plaintext
234 | 注:`go install` 命令不支持 `-o` 标记,所以它将使用前面描述的默认名称来命名可执行文件。
235 | ```
236 |
237 | 安装好二进制文件后,测试一下程序是否能从其源目录外运行,切回你的主目录:
238 |
239 | ```bash
240 | cd $HOME
241 | ```
242 |
243 | 使用以下方法来运行该程序:
244 |
245 | ```bash
246 | greeter
247 | ```
248 |
249 | 这将产生以下结果:
250 |
251 | ```bash
252 | Output
253 | Hello, World!
254 | ```
255 |
256 | 现在,你可以把你编写的程序安装到你的系统中,让你在任何地方、任何时候都能使用它们。
257 |
258 | ## 总结
259 |
260 | 在本教程中,你演示了 Go 工具链是如何从源代码中轻松构建可执行二进制文件的。这些二进制文件可以分发到其他系统上运行,甚至是那些没有 Go 工具链和环境的系统。你还使用 `go install` 自动构建并将程序作为可执行文件安装在系统的 `$PATH` 中。有了 `go build` 和 `go install`,你现在可以随意分享和使用你的应用程序。
261 |
262 | 现在你了解了 `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/)。
--------------------------------------------------------------------------------
/content/zh/docs/24-How_To_Write_Switch_Statements_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中编写 Switch 语句
2 |
3 | ## 介绍
4 |
5 | [条件语句]({{< 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 程序在遇到不同选项时采取的行动很有用。
6 |
7 | 我们可以用 switch 语句编写的所有内容也可以用`if`语句编写。在本教程中,我们将看几个例子,看看 switch 语句能做什么,它所取代的`if`语句,以及它最合适的应用场合。
8 |
9 | ## Switch 语句的结构
10 |
11 | Switch 通常用于描述当一个变量被分配到特定值时程序所采取的行动。下面的例子演示了我们如何使用 `if` 语句来完成这个任务。
12 |
13 | ```go
14 | package main
15 |
16 | import "fmt"
17 |
18 | func main() {
19 | flavors := []string{"chocolate", "vanilla", "strawberry", "banana"}
20 |
21 | for _, flav := range flavors {
22 | if flav == "strawberry" {
23 | fmt.Println(flav, "is my favorite!")
24 | continue
25 | }
26 |
27 | if flav == "vanilla" {
28 | fmt.Println(flav, "is great!")
29 | continue
30 | }
31 |
32 | if flav == "chocolate" {
33 | fmt.Println(flav, "is great!")
34 | continue
35 | }
36 |
37 | fmt.Println("I've never tried", flav, "before")
38 | }
39 | }
40 | ```
41 |
42 | 这将输出如下信息:
43 |
44 | ```Output
45 | chocolate is great!
46 | vanilla is great!
47 | strawberry is my favorite!
48 | I've never tried banana before
49 | ```
50 |
51 | 在`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`循环的执行,这样就不会在最后打印出首选冰淇淋口味的默认信息。
52 |
53 | 当我们添加新的偏好时,我们必须不断添加`if`语句来处理新的情况。重复的信息,如 "香草"和 "巧克力"的情况,必须有重复的`if`语句。对于我们代码的未来读者(包括我们自己)来说,`if`语句的重复性掩盖了它们所做的重要部分--将变量与多个值进行比较并采取不同的行动。另外,我们的回退信息与条件语句分开,使得它看起来不相关。转换器 "语句可以帮助我们更好地组织这个逻辑。
54 |
55 | `switch` 语句以 `switch` 关键字开始,在其最基本的形式下,后面是一些要进行比较的变量。之后是一对大括号(`{}`),其中可以出现多个*case 子句*。case 子句描述了当提供给 switch 语句的变量等于 case 子句所引用的值时,Go 程序应该采取的行动。下面的例子将先前的例子转换为使用一个`switch`而不是多个`if`语句:
56 |
57 | ```go
58 | package main
59 |
60 | import "fmt"
61 |
62 | func main() {
63 | flavors := []string{"chocolate", "vanilla", "strawberry", "banana"}
64 |
65 | for _, flav := range flavors {
66 | switch flav {
67 | case "strawberry":
68 | fmt.Println(flav, "is my favorite!")
69 | case "vanilla", "chocolate":
70 | fmt.Println(flav, "is great!")
71 | default:
72 | fmt.Println("I've never tried", flav, "before")
73 | }
74 | }
75 | }
76 | ```
77 |
78 | 输出与之前相同:
79 |
80 | ```Output
81 | chocolate is great!
82 | vanilla is great!
83 | strawberry is my favorite!
84 | I've never tried banana before
85 | ```
86 |
87 | 我们再次在`main`中定义了一片冰淇淋的口味,并使用`range`语句来遍历每个口味。但是这一次,我们使用了一个`switch`语句来检查`flav`变量。我们使用两个`case'子句来表示偏好。我们不再需要`继续'语句,因为只有一个`case`子句将被`switch`语句执行。我们还可以将"巧克力"和 "香草"条件的重复逻辑结合起来,在 `case`子句的声明中用逗号将其分开。`default`子句是我们的万能子句。它将对我们在 `switch` 语句中没有考虑到的任何口味运行。在这种情况下,"香蕉"将导致 `default` 的执行,打印出 "I've never tried banana before"的信息。
88 |
89 | 这种简化形式的`switch`语句解决了它们最常见的用途:将一个变量与多个替代品进行比较。它还为我们提供了便利,当我们想对多个不同的值采取相同的行动,以及在没有满足所列的条件时,通过使用所提供的`default`关键字采取一些其他行动。
90 |
91 | 当这种简化的`switch`形式被证明太有局限性时,我们可以使用一种更通用的`switch`语句形式。
92 |
93 | ## 通常的 Switch 语句
94 |
95 | `switch`语句对于将更复杂的条件集合在一起以显示它们之间有某种联系是很有用的。这在将某些变量与一定范围的值进行比较时最常用,而不是像前面的例子中的特定值。下面的例子使用`if`语句实现了一个猜谜游戏,可以从`switch`语句中受益:
96 |
97 | ```go
98 | package main
99 |
100 | import (
101 | "fmt"
102 | "math/rand"
103 | "time"
104 | )
105 |
106 | func main() {
107 | rand.Seed(time.Now().UnixNano())
108 | target := rand.Intn(100)
109 |
110 | for {
111 | var guess int
112 | fmt.Print("Enter a guess: ")
113 | _, err := fmt.Scanf("%d", &guess)
114 | if err != nil {
115 | fmt.Println("Invalid guess: err:", err)
116 | continue
117 | }
118 |
119 | if guess > target {
120 | fmt.Println("Too high!")
121 | continue
122 | }
123 |
124 | if guess < target {
125 | fmt.Println("Too low!")
126 | continue
127 | }
128 |
129 | fmt.Println("You win!")
130 | break
131 | }
132 | }
133 | ```
134 |
135 | 输出将取决于所选择的随机数和你玩游戏的程度。下面是一个例子会话的输出:
136 | ```Output
137 | Enter a guess: 10
138 | Too low!
139 | Enter a guess: 15
140 | Too low!
141 | Enter a guess: 18
142 | Too high!
143 | Enter a guess: 17
144 | You win!
145 | ```
146 |
147 | 我们的猜谜游戏需要一个随机数来比较猜测的结果,所以我们使用`math/rand`包中的`rand.Intn`函数。为了确保我们每次玩游戏都能得到不同的`target`值,我们使用`rand.Seed`来根据当前时间随机化随机数发生器。`rand.Intn`的参数`100`将给我们一个0-100范围内的数字。然后我们使用`for`循环来开始收集玩家的猜测。
148 |
149 | `fmt.Scanf`函数为我们提供了一种方法来读取用户的输入到我们选择的变量中。它接受一个格式化的字符串动词,将用户的输入转换为我们期望的类型。这里的`%d`意味着我们期望一个 `int`,我们传递 `guess` 变量的地址,这样 `fmt.Scanf` 就能够设置该变量。在[处理任何解析错误]({{< relref "/docs/17-Handling_Errors_in_Go_DigitalOcean.md" >}})之后,我们使用两个`if`语句来比较用户的猜测和`target`值。它们返回的`string`和`bool`一起控制显示给玩家的信息,以及游戏是否会退出。
150 |
151 | 这些 `if` 语句掩盖了一个事实,即变量被比较的数值范围都有某种联系。一眼就能看出我们是否遗漏了该范围的某些部分,这也是很困难的。下一个例子重构了前面的例子,用`switch`语句代替:
152 |
153 | ```go
154 | package main
155 |
156 | import (
157 | "fmt"
158 | "math/rand"
159 | )
160 |
161 | func main() {
162 | target := rand.Intn(100)
163 |
164 | for {
165 | var guess int
166 | fmt.Print("Enter a guess: ")
167 | _, err := fmt.Scanf("%d", &guess)
168 | if err != nil {
169 | fmt.Println("Invalid guess: err:", err)
170 | continue
171 | }
172 |
173 | switch {
174 | case guess > target:
175 | fmt.Println("Too high!")
176 | case guess < target:
177 | fmt.Println("Too low!")
178 | default:
179 | fmt.Println("You win!")
180 | return
181 | }
182 | }
183 | }
184 | ```
185 |
186 | 这将产生类似以下的输出:
187 |
188 | ```Output
189 | Enter a guess: 25
190 | Too low!
191 | Enter a guess: 28
192 | Too high!
193 | Enter a guess: 27
194 | You win!
195 | ```
196 |
197 | 在这个版本的猜谜游戏中,我们用一个`switch`语句代替了`if`语句块。我们省略了`switch`的表达式参数,因为我们只对使用`switch`来收集条件语句感兴趣。每个`case`子句包含一个不同的表达式,将`guess`与`target`进行比较。与第一次用`switch`代替`if`语句类似,我们不再需要`continue`语句,因为只有一个`case`子句会被执行。最后,`default`子句处理`guess == target`的情况,因为我们已经用另外两个`case`子句覆盖了所有其他可能的值。
198 |
199 | 在我们目前看到的例子中,正好有一个 case 语句将被执行。偶尔,你可能希望结合多个`case`子句的行为。`switch`语句提供了另一个实现这种行为的关键字。
200 |
201 | ## Fallthrough
202 |
203 | 有时你想重复使用另一个 `case` 子句包含的代码。在这种情况下,可以使用 `fallthrough` 关键字要求 Go 运行下一个 `case` 子句的主体。下面这个例子修改了我们之前的冰淇淋口味的例子,以更准确地反映我们对草莓冰淇淋的热情:
204 |
205 | ```go
206 | package main
207 |
208 | import "fmt"
209 |
210 | func main() {
211 | flavors := []string{"chocolate", "vanilla", "strawberry", "banana"}
212 |
213 | for _, flav := range flavors {
214 | switch flav {
215 | case "strawberry":
216 | fmt.Println(flav, "is my favorite!")
217 | fallthrough
218 | case "vanilla", "chocolate":
219 | fmt.Println(flav, "is great!")
220 | default:
221 | fmt.Println("I've never tried", flav, "before")
222 | }
223 | }
224 | }
225 | ```
226 |
227 | 将得到如下输出:
228 |
229 | ```Output
230 | chocolate is great!
231 | vanilla is great!
232 | strawberry is my favorite!
233 | strawberry is great!
234 | I've never tried banana before
235 | ```
236 |
237 | 正如我们之前看到的,我们定义了一个 `string` 片段来表示口味,并使用 `for` 循环来迭代。这里的 `switch` 语句与我们之前看到的语句相同,但是在 `case` 子句的末尾添加了 `fallthrough` 关键字,即 `strawberry`。这将使 Go 运行`case "strawberry":`的主体,首先打印出字符串`strawberry is my favorite!`。当它遇到`fallthrough`时,它将运行下一个`case`子句的主体。这将导致`case "vanilla", "chocolate":`的主体运行,打印出`strawberry is great!`。
238 |
239 | Go 开发人员不经常使用`fallthrough`关键字。通常情况下,通过使用`fallthrough`实现的代码重用,可以通过定义一个具有公共代码的函数来更好地获得。由于这些原因,一般不鼓励使用`fallthrough`。
240 |
241 | ## 总结
242 |
243 | `switch`语句帮助我们向阅读代码的其他开发者传达出彼此有某种联系。使我们在将来添加新的情况时更容易添加不同的行为,并有可能确保任何忘记的事情也能通过`default`子句得到正确处理。下次你发现自己写的多个`if`语句都涉及同一个变量时,试着用`switch`语句重写它--你会发现当需要考虑其他值时,它将更容易重写。
244 |
245 | 如果你想了解更多关于 Go 编程语言的信息,请查看整个[How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)
--------------------------------------------------------------------------------
/content/zh/docs/13-How_To_Do_Math_in_Go_with_Operators.md:
--------------------------------------------------------------------------------
1 | # 如何用运算符在 Go 中做数学计算
2 |
3 | ## 介绍
4 |
5 | 数字在编程中很常见。它们被用来表示一些东西,如:屏幕大小的尺寸、地理位置、金钱和积分、视频中经过的时间、游戏头像的位置、通过分配数字代码表示的颜色等等。
6 |
7 | 在编程中进行数学运算是一项重要的技能,因为你会经常与数字打交道。尽管对数学的理解肯定能帮助你成为一个更好的程序员,但它不是一个先决条件。如果你没有数学背景,试着把数学看作是完成你想实现的目标的工具,并作为提高你的逻辑思维能力的一种方式。
8 |
9 | 我们将使用 Go 中最常用的两种数字[数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}}),整数和浮点数。
10 |
11 | * [整数](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`,...)。
12 | * [浮点数](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` ...
13 |
14 | 本教程将回顾我们在 Go 中对数字数据类型可以使用的运算符。
15 |
16 | ## 运算符
17 |
18 | 运算符是一个表示运算的符号或函数。例如,在数学中,加号或 `+` 是表示加法的运算符。
19 |
20 | 在 Go 中,我们将看到一些熟悉的运算符,这些运算符是从数学中带来的。然而,我们将使用的其他运算符是计算机编程中特有的。
21 |
22 | 下面是 Go 中与数学有关的运算符的快速参考表。在本教程中,我们将涵盖以下所有的运算。
23 |
24 | 预算符的返回
25 |
26 | `x + y` 是 `x` 和 `y` 的总和
27 |
28 | `x - y` 是 `x` 和 `y` 之差
29 |
30 | `-x` 表示 `x` 为负数特性
31 |
32 | `+x' 表示 `x' 为正数特性
33 |
34 | `x * y` 是 `x` 和 `y` 的积
35 |
36 | `x / y` 是 `x` 和 `y` 的商
37 |
38 | `x % y` 是 `x` / `y` 的余
39 |
40 | 我们还将讨论复合赋值运算符,包括 `+=` 和 `*=`,它们将算术运算符和 `=` 运算符结合起来。
41 |
42 | ## 加法和减法
43 |
44 | 在 Go 中,加法和减法运算符的表现与数学中一样。事实上,你可以把 Go 编程语言当作计算器来使用。
45 |
46 | 让我们看看一些例子,从整数开始:
47 |
48 | ```go
49 | fmt.Println(1 + 5)
50 | ```
51 |
52 | ```go
53 | Output
54 | 6
55 | ```
56 |
57 | 我们可以通过使用下面的语法来初始化变量以代表整数值,而不是直接将整数传入`fmt.Println` 语句:
58 |
59 | ```go
60 | a := 88
61 | b := 103
62 |
63 | fmt.Println(a + b)
64 | ```
65 |
66 | ```go
67 | Output
68 | 191
69 | ```
70 |
71 | 因为整数既可以是正数也可以是负数(也可以是 0),所以我们可以将一个负数与一个正数相加:
72 |
73 | ```go
74 | c := -36
75 | d := 25
76 |
77 | fmt.Println(c + d)
78 | ```
79 |
80 | ```go
81 | Output
82 | -11
83 | ```
84 |
85 | 浮点数的加法也类似:
86 |
87 | ```go
88 | e := 5.5
89 | f := 2.5
90 |
91 | fmt.Println(e + f)
92 | ```
93 |
94 | ```go
95 | Output
96 | 8
97 | ```
98 |
99 | 因为我们把两个浮点数加在一起,Go 返回了一个带有小数位的浮点数。然而,由于在这种情况下,小数位是零,`fmt.Println` 放弃了小数位的格式化。为了正确格式化输出,我们可以使用 `fmt.Printf` 和谓词 `%.2f`,它将格式化为两个小数位,就像这个例子:
100 |
101 | ```go
102 | fmt.Printf("%.2f", e + f)
103 | ```
104 |
105 | ```go
106 | Output
107 | 8.00
108 | ```
109 |
110 | 减法的语法与加法相同,只是我们将运算符从加号(`+`)改为减号(`-`):
111 |
112 | ```go
113 | g := 75.67
114 | h := 32.0
115 |
116 | fmt.Println(g - h)
117 | ```
118 |
119 | ```go
120 | Output
121 | 43.67
122 | ```
123 |
124 | 在 Go 中,我们只能对相同的数据类型使用运算符。我们不能把一个 `int` 和一个 [`float64`](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go/#%E6%95%B0%E5%AD%97%E7%B1%BB%E5%9E%8B%E7%9A%84%E5%A4%A7%E5%B0%8F) 加在一起:
125 |
126 | ```go
127 | i := 7
128 | j := 7.0
129 | fmt.Println(i + j)
130 | ```
131 |
132 | ```go
133 | Output
134 | i + j (mismatched types int and float64)
135 | ```
136 |
137 | 试图在不相同的数据类型上使用运算符将导致编译器错误。
138 |
139 | ## 单项算术运算
140 |
141 | 一个单数的数学表达式只由一个成员或元素组成。在 Go 中,我们可以使用加号和减号作为与一个值配对的单一元素:返回值的特性(`+`),或改变值的符号(`-`)。
142 |
143 | 虽然不常用,但加号表示值的特性。我们可以对正值使用加号:
144 |
145 | ```go
146 | i := 3.3
147 | fmt.Println(+i)
148 | ```
149 |
150 | ```go
151 | Output
152 | 3.3
153 | ```
154 |
155 | 当我们使用加号与一个负值时,它也将返回该值的特性,在这种情况下它将是一个负值:
156 |
157 | ```go
158 | j := -19
159 | fmt.Println(+j)
160 | ```
161 |
162 | ```go
163 | Output
164 | -19
165 | ```
166 |
167 | 对于一个负值,加号会返回同样的负值。
168 |
169 | 然而,减号会改变一个数值的符号。因此,当我们传递一个正值时,我们会发现值前的减号会返回一个负值:
170 |
171 | ```go
172 | k := 3.3
173 | fmt.Println(-k)
174 | ```
175 |
176 | ```go
177 | Output
178 | -3.3
179 | ```
180 |
181 | 另外,当我们使用负值的减号单选运算符时,将返回一个正值:
182 |
183 | ```go
184 | j := -19
185 | fmt.Println(-j)
186 | ```
187 |
188 | ```go
189 | Output
190 | 19
191 | ```
192 |
193 | 由加号和减号表示的单项算术运算,在 `+i` 的情况下会返回值的同一性,或者像 `-i` 那样返回值的相反符号。
194 |
195 | ## 乘法和除法
196 |
197 | 像加法和减法一样,乘法和除法看起来与数学中的情况非常相似。我们在 Go 中用于乘法的符号是 `*`,用于除法的符号是 `/`。
198 |
199 | 下面是一个在 Go 中对两个浮点数进行乘法的例子:
200 |
201 | ```go
202 | k := 100.2
203 | l := 10.2
204 |
205 | fmt.Println(k * l)
206 | ```
207 |
208 | ```go
209 | Output
210 | 1022.04
211 | ```
212 |
213 | 在 Go 中,除法有不同的特点,这取决于我们要除的数字类型。
214 |
215 | 如果我们要除以整数,Go 可以使用 `/` 运算符来执行除法,对于商 **x**,返回的数字是小于或等于 **x** 的最大整数。
216 |
217 | 如果你运行下面这个除法`80 / 6` 的例子,你会收到 `13` 作为输出,数据类型是`int`:
218 |
219 | ```go
220 | package main
221 |
222 | import (
223 | "fmt"
224 | )
225 |
226 | func main() {
227 | m := 80
228 | n := 6
229 |
230 | fmt.Println(m / n)
231 | }
232 | ```
233 |
234 | ```go
235 | Output
236 | 13
237 | ```
238 |
239 | 如果想要的输出是浮点数,你必须在除法之前明确地转换这些数值。
240 |
241 | 你可以用 `float32()` 或 `float64()` 包裹你想要的浮点数类型来实现:
242 |
243 | ```go
244 | package main
245 |
246 | import (
247 | "fmt"
248 | )
249 |
250 | func main() {
251 | s := 80
252 | t := 6
253 | r := float64(s) / float64(t)
254 | fmt.Println(r)
255 | }
256 | ```
257 |
258 | ```go
259 | Output
260 | 13.333333333333334
261 | ```
262 |
263 | ## 取模
264 |
265 | `%` 运算符是取模,它返回除法后的余数而不是商。这对于寻找同一数字的倍数是很有用的。
266 |
267 | 让我们看一个取模的例子:
268 |
269 | ```go
270 | o := 85
271 | p := 15
272 |
273 | fmt.Println(o % p)
274 | ```
275 |
276 | ```go
277 | Output
278 | 10
279 | ```
280 |
281 | 分开来看,`85` 除以 `15` 会返回 `5` 的商和 `10` 的余数。我们的程序在这里返回值 `10`,因为模运算符返回除法表达式的剩余部分。
282 |
283 | 要对 `float64` 数据类型进行模数计算,你将使用 `math` 包中的 `Mod` 函数:
284 |
285 | ```go
286 | package main
287 |
288 | import (
289 | "fmt"
290 | "math"
291 | )
292 |
293 | func main() {
294 | q := 36.0
295 | r := 8.0
296 | s := math.Mod(q, r)
297 |
298 | fmt.Println(s)
299 | }
300 | ```
301 |
302 | ```go
303 | Output
304 | 4
305 | ```
306 |
307 | ## 运算符优先级
308 |
309 | 在 Go 中,就像在数学中一样,我们需要牢记,运算符将按照优先顺序进行评估,而不是从左到右或从右到左。
310 |
311 | 如果我们看一下下面这个数学表达式:
312 |
313 | ```go
314 | u = 10 + 10 * 5
315 | ```
316 |
317 | 我们可以从左往右读,但是乘法会先进行,所以如果我们要打印 `u',我们会收到以下数值:
318 |
319 | ```go
320 | Output
321 | 60
322 | ```
323 |
324 | 这是因为 `10 * 5` 被计算为 `50`,然后我们加上 `10`,返回 `60` 作为最终结果。
325 |
326 | 如果我们想把`10`加到`10`上,然后把这个和乘以 `5`,我们在 Go 中使用括号,就像在数学中那样:
327 |
328 | ```go
329 | u := (10 + 10) * 5
330 | fmt.Println(u)
331 | ```
332 |
333 | ```go
334 | Output
335 | 100
336 | ```
337 |
338 | 记住操作顺序的一个方法是通过缩写 **PEMDAS**:
339 |
340 | 字母顺序代表的是
341 |
342 | 1 P Parentheses 括号
343 |
344 | 2 E Exponent 指数
345 |
346 | 3 M Multiplication 乘法
347 |
348 | 4 D Division 除法
349 |
350 | 5 A Addition 加法
351 |
352 | 6 S Subtraction 减法
353 |
354 | 你可能熟悉另一个关于运算顺序的缩写,如 **BEDMAS** 或 **BODMAS**。无论哪种缩写对你来说都是有效的,在 Go 中进行数学运算时,尽量记住它,以便返回你所期望的结果。
355 |
356 | ## 赋值运算符
357 |
358 | 最常见的赋值运算符是你已经使用过的:等号 `=`。`=` 赋值运算符将右边的值分配给左边的变量。例如,`v = 23` 将整数 `23` 的值分配给变量 `v`。
359 |
360 | 在编程时,通常使用复合赋值运算符,对一个变量的值进行运算,然后将得到的新值赋给该变量。这些复合运算符将一个算术运算符和 `=` 运算符结合起来。因此,对于加法,我们将 `+` 和 `=` 结合起来,得到复合运算符 `+=`。让我们看看这看起来像什么:
361 |
362 | ```go
363 | w := 5
364 | w += 1
365 | fmt.Println(w)
366 | ```
367 |
368 | ```go
369 | Output
370 | 6
371 | ```
372 |
373 | 首先,我们设置变量 `w` 等于 `5` 的值,然后我们使用 `+=` 复合赋值运算符将右边的数字加到左边变量的值上,然后将结果赋给 `w`。
374 |
375 | 复合赋值运算符在 `for` 循环的情况下经常使用,当你想重复一个过程几次时,就会用到它:
376 |
377 | ```go
378 | package main
379 |
380 | import "fmt"
381 |
382 | func main() {
383 |
384 | values := []int{0, 1, 2, 3, 4, 5, 6}
385 |
386 | for _, x := range values {
387 |
388 | w := x
389 |
390 | w *= 2
391 |
392 | fmt.Println(w)
393 | }
394 | }
395 | ```
396 |
397 | ```go
398 | Output0
399 | 2
400 | 4
401 | 6
402 | 8
403 | 10
404 | 12
405 | ```
406 |
407 | 通过使用 `for` 循环遍历名为 `values` 的切片,你能够自动完成 `*=` 运算符的过程,该运算符将变量 `w` 乘以数字 `2`,然后将结果分配回变量 `w`。
408 |
409 | Go 对本教程中讨论的每个算术运算符都有一个复合赋值运算符。
410 |
411 | 要添加然后赋值:
412 |
413 | ```go
414 | y += 1
415 | ```
416 |
417 | 做减法,然后赋值:
418 |
419 | ```go
420 | y -= 1
421 | ```
422 |
423 | 做乘法,然后再赋值:
424 |
425 | ```go
426 | y *= 2
427 | ```
428 |
429 | 做除法,然后再赋值:
430 |
431 | ```go
432 | y /= 3
433 | ```
434 |
435 | 取余,然后再赋值:
436 |
437 | ```go
438 | y %= 3
439 | ```
440 |
441 | 当需要逐步增加或减少时,或者当你需要将程序中的某些过程自动化时,复合赋值运算符就很有用。
442 |
443 | ## 总结
444 |
445 | 本教程涵盖了许多你将在整数和浮点数数据类型中使用的运算符。你可以在[理解 Go 的数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})和[如何在 Go 中转换数据类型]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}})中了解更多关于不同的数据类型。
--------------------------------------------------------------------------------
/content/zh/docs/27-How_To_Define_and_Call_Functions_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中定义并调用函数
2 |
3 | ## 介绍
4 |
5 | _函数_是一段一旦定义,就可以重用的代码。函数的作用在于可以通过将在整个程序中多次使用的代码分解为更小、更可理解的任务,从而使您的代码更易于理解。
6 |
7 | Go 附带了强大的标准库,其中包含许多预定义的函数。您可能已经熟悉了[fmt](https://golang.org/pkg/fmt/)包:
8 |
9 | - `fmt.Println()` 会将对象打印到标准输出(最可能在您的终端)。
10 | - `fmt.Printf()` 允许您将输出格式化打印。
11 |
12 | 函数名称包括括号,并且可能包括参数。
13 |
14 | 在本教程中,我们将介绍如何定义您自己的函数以在您的项目中使用。
15 |
16 | ## 定义一个函数
17 |
18 | 让我们从经典的[“Hello, World!”程序]({{< relref "/docs/04-How_To_Write_Your_First_Program_in_Go_DigitalOcean.md" >}})开始理解函数。
19 |
20 | 我们将在一个文本编辑器中创建一个新的文本文件,然后调用程序 `hello.go`。然后,我们将在里面定义函数。
21 |
22 | Go 中使用 `func` 关键字来定义函数。然后是您选择的名称和一组括号,其中包含函数将采用的任何参数(它们可以为空)。函数代码行用大括号 `{}` 括起来。
23 |
24 | 在这种情况下,我们将定义一个名为 `hello()` 的函数:
25 |
26 | ```go
27 | func hello() {}
28 | ```
29 |
30 | 这就是用于创建函数的初始语句。
31 |
32 | 基于此,我们将添加第二行来提供函数功能的说明。我们将打印 `Hello, World!` 到控制台:
33 |
34 | ```go
35 | func hello() {
36 | fmt.Println("Hello, World!")
37 | }
38 | ```
39 |
40 | 现在我们的函数已经完全定义好了,但是如果我们此时运行程序,什么都不会发生,因为我们没有调用函数。
41 |
42 | 因此,在我们的 `main()` 代码块中,来调用 `hello()` 函数:
43 |
44 | ```go
45 | package main
46 |
47 | import "fmt"
48 |
49 | func main() {
50 | hello()
51 | }
52 |
53 | func hello() {
54 | fmt.Println("Hello, World!")
55 | }
56 | ```
57 |
58 | 现在,让我们运行程序:
59 |
60 | ```shell
61 | $ go run hello.go
62 | ```
63 |
64 | 您将收到以下输出:
65 |
66 | ```shell
67 | Output
68 | Hello, World!
69 | ```
70 |
71 | 请注意,我们还引入了一个名为 `main()` 的函数。`main()` 函数是一个特殊的函数,它告诉编译器程序应该从这里**开始**。对于 _可执行_ 的任何程序(可以从命令行运行的程序),都需要一个 `main()` 函数。`main()` 函数只能在 `main()` _包_ 中出现一次,并且不接收和返回任何参数。在任何 Go 程序中[程序执行](https://golang.org/ref/spec#Program_execution)都是这样的。根据以下示例:
72 |
73 | ```go
74 | package main
75 |
76 | import "fmt"
77 |
78 | func main() {
79 | fmt.Println("this is the main section of the program")
80 | }
81 | ```
82 |
83 | 函数可以比我们定义的 `hello()` 函数更复杂。我们可以在函数中使用[`for`循环]({{< relref "/docs/25-How_To_Construct_For_Loops_in_Go.md" >}})、[条件语句]({{< relref "/docs/23-How_To_Write_Conditional_Statements_in_Go.md" >}})等。
84 |
85 | 例如,以下函数使用条件语句检查 `name` 变量的输入是否包含元音,并使用 `for` 循环遍历 `name` 字符串中的字母。
86 |
87 | ```go
88 | package main
89 |
90 | import (
91 | "fmt"
92 | "strings"
93 | )
94 |
95 | func main() {
96 | names()
97 | }
98 |
99 | func names() {
100 | fmt.Println("Enter your name:")
101 |
102 | var name string
103 | fmt.Scanln(&name)
104 | // Check whether name has a vowel
105 | for _, v := range strings.ToLower(name) {
106 | if v == 'a' || v == 'e' || v == 'i' || v == 'o' || v == 'u' {
107 | fmt.Println("Your name contains a vowel.")
108 | return
109 | }
110 | }
111 | fmt.Println("Your name does not contain a vowel.")
112 | }
113 | ```
114 |
115 | 我们在这里定义的 `names()` 函数设置一个带有输入的变量 `name`,然后在一个 `for` 循环中设置一个条件语句。这显示了如何在函数定义中组织代码。但是,根据我们对程序的意图以及我们对代码的安排,我们可能希望将条件语句和循环定义为两个独立的函数。
116 |
117 | 在程序中定义函数可使我们的代码更模块化和可重用,这样我们就可以调用相同的函数而无需重写它们。
118 |
119 | ## 使用参数
120 |
121 | 到目前为止,我们已经研究了带有空括号且不带参数的函数,但我们是可以在函数定义中的括号内定义参数的。
122 |
123 | _参数_ 是函数定义中的命名实体,指定函数可以接受的参数。在 Go 中,您必须为每个参数指定[数据类型]({{< relref "/docs/07-Understanding_Data_Types_in_Go.md" >}})。
124 |
125 | 让我们创建一个将单词重复指定次数的程序。它将接受一个 `string` 类型的 `word` 参数和一个用于重复单词的次数的 `int` 类型参数 `reps`。
126 |
127 | ```go
128 | package main
129 |
130 | import "fmt"
131 |
132 | func main() {
133 | repeat("Sammy", 5)
134 | }
135 |
136 | func repeat(word string, reps int) {
137 | for i := 0; i < reps; i++ {
138 | fmt.Print(word)
139 | }
140 | }
141 | ```
142 |
143 | 我们分别为 `word` 参数和 `reps` 参数传递了`Sammy` 和 `5` 值。这些值按照给定的顺序与每个参数相对应。`repeat` 函数有一个 `for` 循环,将循环参数 `reps` 指定的次数。对于每次循环,都会打印参数 `word` 的值。
144 |
145 | 这是程序的输出:
146 |
147 | ```shell
148 | Output
149 | SammySammySammySammySammy
150 | ```
151 |
152 | 如果你有一组参数都是相同的值,你可以不用每次指定类型。让我们创建一个小程序,它接受都是 `int` 值的参数 `x`, `y`, 和 `z` 这些。我们将创建一个函数,函数将打印他们的总和。下面我们将调用该函数并将数字传递给该函数。
153 |
154 | ```go
155 | package main
156 |
157 | import "fmt"
158 |
159 | func main() {
160 | addNumbers(1, 2, 3)
161 | }
162 |
163 | func addNumbers(x, y, z int) {
164 | a := x + y
165 | b := x + z
166 | c := y + z
167 | fmt.Println(a, b, c)
168 | }
169 | ```
170 |
171 | 当我们创建 `addNumbers` 的函数签名时,我们不需要每次都指定类型,而只需要在最后指定。
172 |
173 | 我们将数字 `1` 传入参数 `x`,`2` 传入参数 `y`,`3` 传入参数 `z`。这些值按照给定的顺序与每个参数对应。
174 |
175 | 该程序会根据我们传递参数的值进行以下数学运算:
176 |
177 | ```shell
178 | a = 1 + 2
179 | b = 1 + 3
180 | c = 2 + 3
181 | ```
182 |
183 | 该函数会打印 `a`,`b`, `c` 的值。基于这个数学运算,我们期望 `a` 等于 `3`, `b` 等于 `4`,`c` 等于 `5`。让我们运行程序:
184 |
185 | ```shell
186 | $ go run add_numbers.go
187 | ```
188 |
189 | ```shell
190 | Output
191 | 3 4 5
192 | ```
193 |
194 | 当我们将 `1`、`2` 和 `3` 作为参数传递给 `addNumbers()` 函数时,我们会收到预期的输出。
195 |
196 | 在函数定义中的参数通常是作为变量使用,当您运行方法时,会将参数传递给函数,并为它们赋值。
197 |
198 | ## 返回值
199 |
200 | 您可以将参数值传递给函数,一个函数也可以产生值。
201 |
202 | 函数可以通过 `return` 语句生成一个值,`return` 语句将退出函数并 _可选地_ 将表达式传递回调用者。返回的数据类型必须是指定过的。
203 |
204 | 到目前为止,我们在函数中使用了`fmt.Println()` 语句而不是 `return` 语句。让我们创建一个程序,返回一个变量。
205 |
206 | 在一个名为 `double.go` 的新文本文件中,我们将创建一个将参数 `x` 加倍并返回变量 `y` 的程序。我们将 `3` 作为`double()` 函数的参数,然后打印 `result` 的值。
207 |
208 | ```go
209 | package main
210 |
211 | import "fmt"
212 |
213 | func main() {
214 | result := double(3)
215 | fmt.Println(result)
216 | }
217 |
218 | func double(x int) int {
219 | y := x * 2
220 | return y
221 | }
222 | ```
223 |
224 | 我们可以运行程序并查看输出:
225 | ```shell
226 | $ go run double.go
227 | ```
228 |
229 | ```shell
230 | Output
231 | 6
232 | ```
233 |
234 | 整数 `6` 将作为输出返回,这正是所期望的 `3` 乘以 `2`的结果。
235 |
236 | 如果函数指定了返回值,则必须在代码中提供返回值。否则,将收到编译错误。
237 |
238 | 我们可以通过用 return 语句注释掉这一行来证明这一点:
239 |
240 | ```go
241 | package main
242 |
243 | import "fmt"
244 |
245 | func main() {
246 | result := double(3)
247 | fmt.Println(result)
248 | }
249 |
250 | func double(x int) int {
251 | y := x * 2
252 | // return y
253 | }
254 | ```
255 |
256 | 现在,让我们再次运行程序:
257 | ```shell
258 | $ go run double.go
259 | ```
260 |
261 | ```shell
262 | Output
263 | ./double.go:13:1: missing return at end of function
264 | ```
265 |
266 | 如果不使用此处的 `return` 语句,程序将无法编译。
267 |
268 | 函数在遇到 `return` 语句时立即退出,即使它们不在函数末尾:
269 |
270 | ```go
271 | package main
272 |
273 | import "fmt"
274 |
275 | func main() {
276 | loopFive()
277 | }
278 |
279 | func loopFive() {
280 | for i := 0; i < 25; i++ {
281 | fmt.Print(i)
282 | if i == 5 {
283 | // Stop function at i == 5
284 | return
285 | }
286 | }
287 | fmt.Println("This line will not execute.")
288 | }
289 | ```
290 |
291 | 这里我们设置一个 `for` 循环,循环运行 `25` 次。但是,在 `for` 循环内部,我们有一个条件语句来检查 `i` 的值是否等于 `5`。如果等于,我们将 `return` 进行返回。因为我们在 `loopFive` 函数中,所以函数中的任何一个 `return` 都会退出函数。所以,我们永远不会到达该函数的最后一行来打印 `This line will not execute.`语句。
292 |
293 | 在 `for` 循环内使用了 `return` 语句来结束函数,因此循环外的行将不会运行。相反,如果我们使用了[`break`语句](https://gocn.github.io/How-To-Code-in-Go/docs/26-Using_Break_and_Continue_Statements_When_Working_with_Loops_in_Go/#break-%E8%AF%AD%E5%8F%A5),那么此时只有循环会退出,最后 `fmt.Println()` 一行会被运行。
294 |
295 | `return` 语句能够退出一个函数,并且如果在函数签名中指定,则会返回一个值。
296 |
297 | ## 返回多个值
298 |
299 | 一个函数可以指定多个返回值。让我们编写 `repeat.go` 程序并让它返回两个值。第一个返回值是得到的最终重复值,第二个返回值在参数 `reps` 小于等于 `0` 时会得到一个错误。
300 |
301 | ```go
302 |
303 | package main
304 |
305 | import "fmt"
306 |
307 | func main() {
308 | val, err := repeat("Sammy", -1)
309 | if err != nil {
310 | fmt.Println(err)
311 | return
312 | }
313 | fmt.Println(val)
314 | }
315 |
316 | func repeat(word string, reps int) (string, error) {
317 | if reps <= 0 {
318 | return "", fmt.Errorf("invalid value of %d provided for reps. value must be greater than 0.", reps)
319 | }
320 | var value string
321 | for i := 0; i < reps; i++ {
322 | value = value + word
323 | }
324 | return value, nil
325 | }
326 | ```
327 |
328 | `repeat` 函数首先检查 `reps` 参数是否为有效值。任何不大于 `0` 的值都会导致错误。由于我们传入了 `-1`,因此该代码分支将执行。请注意,当我们从函数返回时,我们必须同时提供 `string` 和 `error` 的返回值。因为提供的参数导致了错误,我们将为第一个返回值传回一个空白字符串,为第二个返回值传回错误。
329 |
330 | 在 `main()` 函数中,我们可以通过声明两个新变量来接收两个返回值,`value` 和 `err`。因为返回中可能有错误,我们想在继续程序之前检查是否收到错误。在这个例子中,我们确实收到了一个错误。我们打印出错误并 `return` 返回退出`main()` 函数以退出程序。
331 |
332 | 如果没有错误,我们将打印出函数的返回值。
333 |
334 | **注意:** 最好只返回两个或三个值。此外,您应该始终将错误作为函数的最后一个返回值返回。
335 |
336 | 运行程序将产生以下输出:
337 |
338 | ```text
339 | output
340 | invalid value of -1 provided for reps. value must be greater than 0.
341 | ```
342 |
343 | 在本节中,我们回顾了如何使用 `return` 语句从函数返回多个值。
344 |
345 | ## 结论
346 |
347 | 函数是在程序中执行操作指令的代码块,有助于使我们的代码更好地可重用和模块化。
348 |
349 | 要了解有关如何使您的代码更模块化的更多信息,您可以阅读我们关于[如何在 Go 中编写包]({{< relref "/docs/21-How_To_Write_Packages_in_Go.md" >}})的指南。
350 |
--------------------------------------------------------------------------------
/content/zh/docs/21-How_To_Write_Packages_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中编写包
2 |
3 | 一个包由同一目录下的 Go 文件组成的,并且在文件开头有相同的包声明。你可以从包中加入额外的功能,使你的程序更加复杂。有些包可以通过 Go 标准库获得,因此在安装 Go 时就已经安装了。其他的可以用 Go 的`go get`命令来安装。你也可以通过在同一目录下创建 Go 文件来建立你自己的 Go 包,你可以通过使用必要的包声明来分享代码。
4 |
5 | 本教程将指导你如何编写 Go 包,以便在其他编程文件中使用。
6 | ## 前提条件
7 |
8 | - 按照[如何安装和设置 Go 的本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})系列教程中的一个教程设置 Go 编程环境。按照本地编程环境教程中的步骤5创建你的 Go 工作区。要遵循本文的例子和命名规则,请阅读第一节「编写和导入软件包」。
9 | - 为了加深你对 GOPATH 的了解,请阅读文章[了解 GOPATH]({{< relref "/docs/05-Understanding_the_GOPATH.md" >}})。
10 |
11 | ## 编写和导入软件包
12 |
13 | 编写包就像编写任何其他 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 程序中使用。
14 |
15 | 在我们创建一个新的包之前,我们需要进入我们的 Go 工作区。这通常是在我们的`gopath`下。对于这个例子,本教程中我们将把包称为`greet`。为了做到这一点,在我们的项目空间下的`gopath`中创建了一个名为`greet`的目录。当使用 Github 作为代码库,组织名称为`gopherguides`,想在此组织下创建`greet`包,那么我们的目录会是这样的:
16 |
17 | ```text
18 | └── $GOPATH
19 | └── src
20 | └── github.com
21 | └── gopherguides
22 | ```
23 |
24 | `greet`目录在`gopherguides`目录中:
25 |
26 | ```text
27 | └── $GOPATH
28 | └── src
29 | └── github.com
30 | └── gopherguides
31 | └── greet
32 | ```
33 |
34 | 最后,我们可以添加我们目录中的第一个文件。通常的做法是,包中的 `主要`或 `入口` 文件是以目录名来命名的。在这种情况下,将在`greet`目录下创建一个名为`greet.go`的文件:
35 |
36 | ```text
37 | └── $GOPATH
38 | └── src
39 | └── github.com
40 | └── gopherguides
41 | └── greet
42 | └── greet.go
43 | ```
44 |
45 | 创建了文件后,我们就可以开始编写我们想要重复使用或在不同项目中共享的代码。在本例中,我们将创建一个打印出 `Hello World`的 `Hello` 的函数。
46 |
47 | 在文本编辑器中打开 `greet.go` 文件,增加如下代码:
48 |
49 | ```go
50 | package greet
51 |
52 | import "fmt"
53 |
54 | func Hello() {
55 | fmt.Println("Hello, World!")
56 | }
57 | ```
58 |
59 | 让我们把这个文件分解一下,每个文件中第一行需要是所处的`包`名称。因为你在`greet`包里,所以通过使用`package`关键字,后面加包的名称:
60 |
61 | ```go
62 | package greet
63 | ```
64 |
65 | 这将告诉编译器把文件中的所有内容作为`greet`包的一部分。
66 |
67 | 接下来,你用 `import` 语句声明你需要使用的任何其他包。在这个文件中你只使用一个包,`fmt`包:
68 |
69 | ```go
70 | import "fmt"
71 | ```
72 |
73 | 最后,你创建函数`Hello`,它将使用`fmt`包来打印出`Hello, World!`。
74 |
75 | ```go
76 | func Hello() {
77 | fmt.Println("Hello, World!")
78 | }
79 | ```
80 |
81 | 现在已经编写了`greet`包,可以在你创建的任何其他包中使用它。让我们创建一个新的包,在其中使用`greet`包。
82 |
83 | 接下来创建一个名为`example`的包,这意味着需要一个名为`example`的目录。在`gopherguides`中创建这个包,所以目录结构看起来像这样:
84 |
85 | ```text
86 | └── $GOPATH
87 | └── src
88 | └── github.com
89 | └── gopherguides
90 | └── example
91 | ```
92 |
93 | 现在你有了新包的目录,可以创建入口文件。因为这将是一个可执行的程序,最好的做法是将入口文件命名为`main.go`:
94 |
95 | ```text
96 | └── $GOPATH
97 | └── src
98 | └── github.com
99 | └── gopherguides
100 | └── example
101 | └── main.go
102 | ```
103 |
104 | 在文本编辑器中,打开`main.go`,添加以下代码来调用`greet`包:
105 |
106 | ```go
107 | package main
108 |
109 | import "github.com/gopherguides/greet"
110 |
111 | func main() {
112 | greet.Hello()
113 | }
114 | ```
115 |
116 | 因为正在导入一个包,通过用点符号来调用指定包的函数。*点符号*是指在使用的包的名称和想使用的包中资源之间加一个句号`.`。例如,在`greet`包中,有`Hello`函数作为一个资源。如果想调用该资源,可以使用 `greet.Hello()` 的形式。
117 |
118 | 现在,可以打开终端,在命令行上运行该程序:
119 |
120 | ```bash
121 | go run main.go
122 | ```
123 |
124 | 完成后,你将收到以下输出:
125 |
126 | ```Output
127 | Hello, World!
128 | ```
129 |
130 | 为了解如何在包中使用变量,让我们在`greet.go`文件中添加一个变量定义:
131 |
132 | ```go
133 | package greet
134 |
135 | import "fmt"
136 |
137 | var Shark = "Sammy"
138 |
139 | func Hello() {
140 | fmt.Println("Hello, World!")
141 | }
142 | ```
143 |
144 | 接下来,打开`main.go`文件,添加以下高亮行,在`fmt.Println()`函数中调用`greet.go`中的变量:
145 |
146 | ```go
147 | package main
148 |
149 | import (
150 | "fmt"
151 |
152 | "github.com/gopherguides/greet"
153 | )
154 |
155 | func main() {
156 | greet.Hello()
157 |
158 | fmt.Println(greet.Shark)
159 | }
160 | ```
161 |
162 | 再次运行此程序:
163 |
164 | ```bash
165 | go run main.go
166 | ```
167 |
168 | 你会收到以下输出:
169 |
170 | ```Output
171 | Hello, World!
172 | Sammy
173 | ```
174 |
175 | 最后,让我们也在`greet.go`文件中定义一个类型。创建一个带有 `name` 和 `color`字段的 `Octopus` 类型,以及一个在调用时将打印出字段的函数:
176 |
177 | ```go
178 | package greet
179 |
180 | import "fmt"
181 |
182 | var Shark = "Sammy"
183 |
184 | type Octopus struct {
185 | Name string
186 | Color string
187 | }
188 |
189 | func (o Octopus) String() string {
190 | return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
191 | }
192 |
193 | func Hello() {
194 | fmt.Println("Hello, World!")
195 | }
196 | ```
197 |
198 | 打开`main.go`,在文件的末尾创建一个该类型的实例:
199 |
200 | ```go
201 | package main
202 |
203 | import (
204 | "fmt"
205 |
206 | "github.com/gopherguides/greet"
207 | )
208 |
209 | func main() {
210 | greet.Hello()
211 |
212 | fmt.Println(greet.Shark)
213 |
214 | oct := greet.Octopus{
215 | Name: "Jesse",
216 | Color: "orange",
217 | }
218 |
219 | fmt.Println(oct.String())
220 | }
221 | ```
222 |
223 | 一旦你用`oct := greet.Octopus`创建了一个`Octopus`类型的实例,就可以在`main.go`文件的命名空间中访问该类型的函数和字段。这使得在最后一行直接写`oct.String()`,而不用调用`greet`。同样的,也可以在不引用`greet`包的名字的情况下调用`oct.Color`等类型字段。
224 |
225 | `Octopus`类型上的`String`方法使用`fmt.Sprintf`函数来输出一段文本,并将结果即一个字符串,`返回`给调用者(在这里是指主程序)。
226 |
227 | 当你运行该程序时,你会收到以下输出:
228 |
229 | ```bash
230 | go run main.go
231 | ```
232 |
233 | ```Output
234 | Hello, World!
235 | Sammy
236 | The octopus's name is "Jesse" and is the color orange.
237 | ```
238 |
239 | 通过在`Octopus`上创建`String`方法,你现在有一个可重复使用的方法来打印出自定义类型的信息。如果想在将来改变这个方法的行为,只需要编辑这一个方法。
240 |
241 | ## 可导出代码
242 |
243 | 你可能已经注意到,调用的`greet.go`文件中所有的声明都是大写的。Go 没有像其他语言那样有`public`、`private`或`protected`修饰符的概念。外部可见性是由大写字母控制的。以大写字母开头的类型、变量、函数等等,在当前包之外是可以公开使用的。一个在其包外可见的符号被认为是 `可导出` 的。
244 |
245 | 如果你给`Octopus`添加了一个名为`reset`的新方法,可以在`greet`包内调用它,但是不能在`main.go`文件中调用,因为调用者在`greet`包之外:
246 |
247 | ```go
248 | package greet
249 |
250 | import "fmt"
251 |
252 | var Shark = "Sammy"
253 |
254 | type Octopus struct {
255 | Name string
256 | Color string
257 | }
258 |
259 | func (o Octopus) String() string {
260 | return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
261 | }
262 |
263 | func (o *Octopus) reset() {
264 | o.Name = ""
265 | o.Color = ""
266 | }
267 |
268 | func Hello() {
269 | fmt.Println("Hello, World!")
270 | }
271 | ```
272 |
273 | 如果你试图从`main.go`文件中调用`reset`:
274 |
275 | ```go
276 | package main
277 |
278 | import (
279 | "fmt"
280 |
281 | "github.com/gopherguides/greet"
282 | )
283 |
284 | func main() {
285 | greet.Hello()
286 |
287 | fmt.Println(greet.Shark)
288 |
289 | oct := greet.Octopus{
290 | Name: "Jesse",
291 | Color: "orange",
292 | }
293 |
294 | fmt.Println(oct.String())
295 |
296 | oct.reset()
297 | }
298 | ```
299 |
300 | 你会收到以下编译错误:
301 |
302 | ```Output
303 | oct.reset undefined (cannot refer to unexported field or method greet.Octopus.reset)
304 | ```
305 |
306 | 要从 `Octopus` 中`导出` `reset` 功能,请将`reset` 中的`R` 大写:
307 |
308 | ```go
309 | package greet
310 |
311 | import "fmt"
312 |
313 | var Shark = "Sammy"
314 |
315 | type Octopus struct {
316 | Name string
317 | Color string
318 | }
319 |
320 | func (o Octopus) String() string {
321 | return fmt.Sprintf("The octopus's name is %q and is the color %s.", o.Name, o.Color)
322 | }
323 |
324 | func (o *Octopus) Reset() {
325 | o.Name = ""
326 | o.Color = ""
327 | }
328 |
329 | func Hello() {
330 | fmt.Println("Hello, World!")
331 | }
332 | ```
333 |
334 | 如此一来,可以从其他包中调用`Reset'而不会得到错误:
335 |
336 | ```go
337 | package main
338 |
339 | import (
340 | "fmt"
341 |
342 | "github.com/gopherguides/greet"
343 | )
344 |
345 | func main() {
346 | greet.Hello()
347 |
348 | fmt.Println(greet.Shark)
349 |
350 | oct := greet.Octopus{
351 | Name: "Jesse",
352 | Color: "orange",
353 | }
354 |
355 | fmt.Println(oct.String())
356 |
357 | oct.Reset()
358 |
359 | fmt.Println(oct.String())
360 | }
361 | ```
362 |
363 | 现在,如果你运行这个程序:
364 |
365 | ```bash
366 | go run main.go
367 | ```
368 |
369 | 你将收到以下输出:
370 |
371 | ```Output
372 | Hello, World!
373 | Sammy
374 | The octopus's name is "Jesse" and is the color orange
375 | The octopus's name is "" and is the color .
376 | ```
377 |
378 | 通过调用`Reset`,清除了`Name`和`Color`字段中的所有信息。当调用`String`方法时,`Name`和`Color`打印为空,因为这些字段现在是空的。
379 |
380 | ## 总结
381 |
382 | 编写 Go 包与编写其他 Go 文件是一样的,但把它放在另一个目录中可以隔离代码,以便在其他地方重复使用。本教程介绍了如何在包中编写定义,演示了如何在另一个 Go 文件中使用这些定义,并解释了控制包是否可访问的选项。
--------------------------------------------------------------------------------
/content/zh/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 如何在 Ubuntu 18.04 上安装 Go 和设置本地编程环境
2 |
3 | ## Go 语言简介
4 |
5 | [Go](https://golang.org/) 是一门在 Google 备受挫折后而诞生的语言。开发者不得不频繁地在两种语言中选择,要么选择一门执行效率高但是编译时间长的语言,要么选择一种易于编程但在生产中运行效率低下的语言。 Go 被设计为同时提供所有这三个功能:快速编译、易于编程和生产中的高效执行。
6 |
7 | 虽然 Go 是一门通用的编程语言,可用于许多不同类型的编程项目。但它特别适合网络/分布式系统项目,赢得了“云语言”的美誉。Go 语言专注于通过一组强大的工具来帮助现代程序员完成更多的工作,通过使格式成为语言规范的一部分来消除对格式的争论,以及通过编译为单个二进制文件来简化部署。 Go 易于学习,关键字非常少,这使其成为不论是初学者还是经验丰富的开发人员的不二之选。
8 |
9 | 本教程将指导你通过命令行来安装 Go 和配置 Go 编程环境。本教程特别针对 Ubuntu 18.04 的安装过程,但是对于其他 Debian Linux 发行版也同样适用。
10 |
11 | ## 安装前提
12 |
13 | 你需要一台安装了 Ubuntu 18.04 的电脑或者虚拟机,并且有对该计算机的管理员访问权限和网络连接。 您可以通过 [Ubuntu 18.04 版本页面](http://releases.ubuntu.com/releases/18.04/) 下载此操作系统。
14 |
15 | ## 第一步 — 安装 Go
16 |
17 | 在这一步,你通过 [Go 官方下载页面](https://golang.org/dl/)下载最新版本来安装 Go。
18 |
19 | 为此,你需要找到最新二进制版本压缩包的 URL 。你还要注意旁边列出的 SHA256 哈希值,因为你将用它来[验证下载的文件](https://www.digitalocean.com/community/tutorials/how-to-verify-downloaded-files)。
20 |
21 | 你将通过命令行来完成安装和设置,这是一种与计算机交互的非图形化方式。也就是说,你输入的是文本,然后也是通过文本得到计算机的反馈,而不是点击按钮。
22 |
23 | 命令行,也就是我们熟知的 _shell_ 或者 _终端_ ,可以帮助你修改或自动化很多你每天执行在计算机上的任务,这是软件开发人员必备的工具。尽管有很多终端命令需要学习,但是这些命令可以让你做更强大的事情。有关命令行的更多信息,请查看 [Linux 终端简介](https://www.digitalocean.com/community/tutorials/an-introduction-to-the-linux-terminal) 教程。
24 |
25 | 在 Ubuntu 18.04 上,你可以通过点击屏幕左上角的 Ubuntu 图标并在搜索栏中输入`terminal`来找到终端程序。点击终端程序图标来打开终端。或者你可以在键盘上同时按住的“CTRL”、“ALT”和“T”键来自动打开终端程序。
26 |
27 | 
28 |
29 | 终端打开之后,你可以手动安装 Go 二进制。虽然可以通过包管理工具,比如 `apt-get`,但是通过手动安装可以帮助你理解一个有效的 Go 工作区系统里面任何必要的配置信息的修改。
30 |
31 | 下载 Go 之前,确保你在 home (`~`) 目录:
32 |
33 | ```shell
34 | cd ~
35 | ```
36 |
37 |
38 | 根据从官方 Go 下载页面复制的压缩包 URL,使用 `curl` 命令拉取下载:
39 |
40 | ```shell
41 | curl -LO https://dl.google.com/go/go1.12.1.linux-amd64.tar.gz
42 | ```
43 |
44 | 接下来,使用 `sha256sum` 命令来校验压缩包:
45 |
46 | ```shell
47 | sha256sum go1.12.1.linux-amd64.tar.gz
48 | ```
49 |
50 | 运行上面命令显示的哈希值应该和下载页面的哈希值一致,如果不一致的话,那么这个压缩包就不是一个有效文件,需要重新下载。
51 |
52 | ```shell
53 | Output
54 | 2a3fdabf665496a0db5f41ec6af7a9b15a49fbe71a85a50ca38b1f13a103aeec go1.12.1.linux-amd64.tar.gz
55 | ```
56 |
57 | 接下来,解压下载的文件并将其安装到系统所需位置。一般都是放在 `/usr/local` 目录下面:
58 |
59 | ```shell
60 | sudo tar -xvf go1.12.1.linux-amd64.tar.gz -C /usr/local
61 | ```
62 |
63 | 那么在 `/usr/local` 目录下面有一个 `go` 目录。
64 |
65 | **注意**:尽管 `/usr/local/go` 是官方推荐的位置,但有些用户可能更喜欢或需要不同的路径。
66 |
67 | 在这一步中,你在 Ubuntu 18.04 机器上下载并安装了 Go 。接下来你将配置 Go 的工作区。
68 |
69 | ## 第二步 — 创建你的 Go 工作区
70 |
71 | 安装完 Go 之后,你可以创建你的编码工作区。Go 语言的工作区在其根目录下包含两个目录:
72 |
73 | - `src`: 该目录包含 Go 的源文件。所谓源文件就是你用 Go 编程语言写的文件。源文件被 Go 编译器构建成可执行的二进制文件。
74 | - `bin`: 该目录包含了 Go 工具构建和安装的可执行文件。可执行文件就是运行在你系统上并执行任务的二进制文件。通常是你的源码或者是其他下载的 Go 源代码编译的程序。
75 |
76 | `src` 子目录可能包含多个版本控制仓库(例如 [Git](https://git-scm.com/), [Mercurial](https://www.mercurial-scm.org/) 和 [Bazaar](http://bazaar.canonical.com/))。这允许你在你的项目中规范导入代码。 规范导入就是引用完全限定包的导入,例如 `github.com/digitalocean/godo` 。
77 |
78 | 当你引入第三方库的时候,你可以看到类似 `github.com`, `golang.org` 或其他目录,如果你使用的是 `github.com` 之类的代码仓库,你还将把项目和源文件放在该目录下。 我们将在此步骤的后面部分探讨这个概念。
79 |
80 | 下面是典型的工作区目录结构:
81 |
82 | ```shell
83 | .
84 | ├── bin
85 | │ ├── buffalo # command executable
86 | │ ├── dlv # command executable
87 | │ └── packr # command executable
88 | └── src
89 | └── github.com
90 | └── digitalocean
91 | └── godo
92 | ├── .git # Git repository metadata
93 | ├── account.go # package source
94 | ├── account_test.go # test source
95 | ├── ...
96 | ├── timestamp.go
97 | ├── timestamp_test.go
98 | └── util
99 | ├── droplet.go
100 | └── droplet_test.go
101 | ```
102 |
103 | 从 1.8 开始,Go 工作区的默认目录是用户的 home 目录,并带有 `go` 子目录,或者是 `$HOME/go` 目录。 如果你使用的是早于 1.8 的 Go 版本,目前认为最佳做法是为你的工作区使用 `$HOME/go` 位置。
104 |
105 | 使用下面命令为你的 Go 工作区创建目录结构:
106 |
107 | ```shell
108 | mkdir -p $HOME/go/{bin,src}
109 | ```
110 |
111 | `-p` 选项是告诉 `mkdir` 在目录中创建所有的上级目录,尽管他们可能不存在。使用 `{bin,src}` 为 `mkdir` 创建一组参数,并告诉它创建 `bin` 目录和 `src` 目录。
112 |
113 | 以上命令将确保下面的目录结构各就各位:
114 |
115 | ```shell
116 | └── $HOME
117 | └── go
118 | ├── bin
119 | └── src
120 | ```
121 |
122 | 在 Go 1.8 之前,需要设置一个名为 `$GOPATH` 的本地环境变量。 `$GOPATH` 告诉编译器在哪里可以找到导入的第三方源代码,同样包括任何你写的本地源代码。 虽然不再明确要求它,但它仍然被认为是一种很好的做法,因为许多第三方工具仍然依赖于设置的这个变量。
123 |
124 | 你可以通过将全局变量添加到你的 `~/.profile` 中来设置你的 `$GOPATH`。 你可能想根据你的 shell 配置将其添加到 `.zshrc` 或 `.bashrc` 文件中。
125 |
126 | 首先,使用 `nano` 或你喜欢的文本编辑器打开 `~/.profile`:
127 |
128 | ```shell
129 | nano ~/.profile
130 | ```
131 |
132 | 通过下面的命令设置你的 `$GOPATH` :
133 |
134 | ```shell
135 | ~/.profile
136 | export GOPATH=$HOME/go
137 | ```
138 |
139 | 当 Go 编译和安装工具时,会将他们放在 `$GOPATH/bin` 目录。为方便起见,通常将工作区的 `/bin` 子目录添加到 `~/.profile` 中的 `PATH` 中:
140 |
141 | ```shell
142 | ~/.profile
143 | export PATH=$PATH:$GOPATH/bin
144 | ```
145 |
146 | 这将允许你在系统上的任何位置运行通过 Go 工具编译或下载的任何程序。
147 |
148 | 最后,你需要将 `go` 二进制文件添加到 `PATH` 中。 你可以通过在行尾添加 `/usr/local/go/bin` 来实现:
149 |
150 | ```shell
151 | ~/.profile
152 | export PATH=$PATH:$GOPATH/bin:/usr/local/go/bin
153 | ```
154 |
155 | 将 `/usr/local/go/bin` 添加到 `$PATH` 中,可以使所有 Go 工具都可以在系统上的任何位置使用。
156 |
157 | 为了更新你的 shell 配置,请使用下面的命令来加载全局变量:
158 |
159 | ```shell
160 | . ~/.profile
161 | ```
162 |
163 | 您可以通过使用 `echo` 命令并检查其输出,来验证你的 `$PATH` 是否已更新:
164 |
165 | ```shell
166 | echo $PATH
167 | ```
168 |
169 | 你将看到你的 `$GOPATH/bin` 显示在你的主目录中。如果你以 `root` 身份登录,你将在路径中看到 `/root/go/bin`。
170 |
171 | ```shell
172 | Output
173 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/root/go/bin:/usr/local/go/bin
174 | ```
175 |
176 | 你还会看到 `/usr/local/go/bin` 的 Go 工具的路径:
177 |
178 | ```shell
179 | Output
180 | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/root/go/bin:/usr/local/go/bin
181 | ```
182 |
183 | 通过检查 Go 的当前版本来验证安装:
184 |
185 | ```shell
186 | go version
187 | ```
188 |
189 | 我们应该得到类似下面的输出:
190 |
191 | ```shell
192 | Output
193 | go version go1.12.1 linux/amd64
194 | ```
195 |
196 | 现在你已经创建了工作区的根目录并设置了 `$GOPATH` 环境变量,你可以根据以下目录结构创建你未来的项目。 此示例假设你使用 `github.com` 作为仓库:
197 |
198 | ```shell
199 | $GOPATH/src/github.com/username/project
200 | ```
201 |
202 | 例如,如果你正在开发 [`https://github.com/digitalocean/godo`](https://github.com/digitalocean/godo) 项目,它将存储在以下目录中:
203 |
204 | ```shell
205 | $GOPATH/src/github.com/digitalocean/godo
206 | ```
207 |
208 | 该项目结构使项目可以通过 `go get` 工具使用。它也有助于以后的可读性。 你可以通过使用 `go get` 命令并获取 `godo` 库来验证这一点:
209 |
210 | ```shell
211 | go get github.com/digitalocean/godo
212 | ```
213 |
214 | 这将下载 `godo` 库的内容并在你的计算机上创建 `$GOPATH/src/github.com/digitalocean/godo` 目录。
215 | 你可以通过列出目录来检查下看看是否成功下载了 `godo`包:
216 |
217 | ```shell
218 | ll $GOPATH/src/github.com/digitalocean/godo
219 | ```
220 |
221 | 你应该看到类似下面这样的输出:
222 |
223 | ```shell
224 | >>>>>>> main
225 | Outputdrwxr-xr-x 4 root root 4096 Apr 5 00:43 ./
226 | drwxr-xr-x 3 root root 4096 Apr 5 00:43 ../
227 | drwxr-xr-x 8 root root 4096 Apr 5 00:43 .git/
228 | -rwxr-xr-x 1 root root 8 Apr 5 00:43 .gitignore*
229 | -rw-r--r-- 1 root root 61 Apr 5 00:43 .travis.yml
230 | -rw-r--r-- 1 root root 2808 Apr 5 00:43 CHANGELOG.md
231 | -rw-r--r-- 1 root root 1851 Apr 5 00:43 CONTRIBUTING.md
232 | .
233 | .
234 | .
235 | -rw-r--r-- 1 root root 4893 Apr 5 00:43 vpcs.go
236 | -rw-r--r-- 1 root root 4091 Apr 5 00:43 vpcs_test.go
237 | ```
238 |
239 | 在这一步中,你创建了一个 Go 工作区并且配置了必要的环境变量。下一步你将使用一些代码来测试下工作区。
240 |
241 | ## 第三步 — 创建一个简单的程序
242 |
243 | 现在你已经设置了工作区,来创建一个 “Hello, World!” 程序吧。这可以检验工作区配置是否正确,并且给你一个更加熟悉 Go 的机会。因为我们创建的是单个 Go 源文件,而不是实际项目,所以我们不需要在工作区中执行此操作。
244 |
245 | 在你的 home 目录,打开一个命令行文本编辑器,例如 `nano`,然后创建一个新文件:
246 |
247 | ```shell
248 | nano hello.go
249 | ```
250 |
251 | 在新文件里写下你的程序:
252 |
253 | ```go
254 | package main
255 |
256 | import "fmt"
257 |
258 | func main() {
259 | fmt.Println("Hello, World!")
260 | }
261 | ```
262 |
263 | 该代码使用了 `fmt` 包并且使用 `Hello, World!` 作为参数调用了 `Println` 函数。这将导致短语 `Hello, World!` 在程序运行时打印到终端上。
264 |
265 | 按 `CTRL` 和 `X` 键退出 `nano`。 当提示保存文件时,按 `Y`,然后按 `ENTER` 退出。
266 |
267 | 退出 `nano` 返回 shell 之后,运行程序:
268 |
269 | ```shell
270 | go run hello.go
271 | ```
272 |
273 | `hello.go` 程序会使终端产生以下输出:
274 |
275 | ```shell
276 | Output
277 | Hello, World!
278 | ```
279 |
280 | 在此步骤中,你使用了一个简单小程序来验证是否正确配置了 Go 工作区。
281 |
282 | ## 总结
283 |
284 | 恭喜!至此,你已经在 Ubuntu 机器上设置了 Go 编程工作区,可以开始写项目了!
--------------------------------------------------------------------------------
/content/zh/docs/36-How_To_Use_Struct_Tags_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中使用结构体标签
2 |
3 | ## 简介
4 |
5 | 结构,或称结构体,被用来将多个信息聚合在一个单元中。这些[信息集合]({{< relref "/docs/33-Defining_Structs_in_Go.md" >}})被用来描述更高层次的概念,例如由 `Street`、`City`、`State` 和 `PostalCode` 组成的 `Address`。当你从数据库或 API 等系统中读取这些信息时,你可以使用结构体标签来控制这些信息如何被分配到结构体的字段中。结构体标签是附加在结构体字段上的小块元数据,为与该结构体一起工作的其他 Go 代码提供指示。
6 |
7 | ## 结构体标签是怎么样的
8 |
9 | Go 结构体标签是出现在 Go 结构声明中类型后的注释,每个标签都由与一些相应的值相关的短字符串组成。
10 |
11 | 一个结构体的标签看起来像这样,标签的偏移量为 ` 字符:
12 |
13 | ```go
14 | type User struct {
15 | Name string `example:"name"`
16 | }
17 | ```
18 |
19 | 其他 Go 代码就能够检查这些结构并提取分配给它所要求的特定键的值。如果没有其他代码对其进行检查,结构体标签对你的代码运行没有任何影响。
20 |
21 | 试试这个例子,看看结构体标签是怎么样的,如果没有来自另一个包的代码,它们将没有任何作用。
22 |
23 | ```go
24 | package main
25 |
26 | import "fmt"
27 |
28 | type User struct {
29 | Name string `example:"name"`
30 | }
31 |
32 | func (u *User) String() string {
33 | return fmt.Sprintf("Hi! My name is %s", u.Name)
34 | }
35 |
36 | func main() {
37 | u := &User{
38 | Name: "Sammy",
39 | }
40 |
41 | fmt.Println(u)
42 | }
43 | ```
44 |
45 | 这将输出:
46 |
47 | ```bash
48 | Output
49 | Hi! My name is Sammy
50 | ```
51 |
52 | 这个例子定义了一个带有 `Name` 字段的 `User` 类型。`Name` 字段被赋了一个结构体标签 `example: "name"`。我们把这个特定的标签称为 “example 结构体标签”,因为它使用 “example”这个词作为它的键。`example` 结构体标签的 `Name` 字段值是 `"name"`。在 `User` 类型中,我们还定义了 `fmt.Stringer` 接口要求的 `String()` 方法。当我们将该类型传递给 `fmt.Println` 时,该方法将被自动调用,并使我们可以生成一个格式化的结构体版本。
53 |
54 | 在 `main` 方法中,我们创建了一个新的 `User` 类型实例,并将其传递给 `fmt.Println`。尽管这个结构体有一个结构体标签,但我们可以看到它对这个 Go 代码的操作没有影响。如果不存在结构标签,它的行为也会完全一样。
55 |
56 | 要使用结构标签来完成某些事,必须编写其他 Go 代码在运行时检查结构体。标准库中有一些包将结构体标签作为其操作的一部分,其中最受欢迎的是 `encoding/json` 包。
57 |
58 | ## 编码 JSON
59 |
60 | JavaScript 对象符号(JSON)是一种文本格式,用于编码根据不同字符串键组织的数据集合。它通常用于不同程序之间的数据通信,因为这种格式足够简单,以至于许多不同的语言都有库对其进行解码,下面是一个 JSON 的例子:
61 |
62 | ```json
63 | {
64 | "language": "Go",
65 | "mascot": "Gopher"
66 | }
67 | ```
68 |
69 | 这个 JSON 对象包含两个键,`language` 和 `mascot`。这些键后面是相关的值,`language` 键的值为 `Go`,`mascot` 则为 `Gopher`。
70 |
71 | 标准库中的 JSON 编码器利用结构体标签作为注解,向编码器表明你想在 JSON 输出中如何命名你的字段。这些 JSON 编码和解码机制可以在 `encoding/json` [包](https://godoc.org/encoding/json)中找到。
72 |
73 | 试试这个例子,看看没有结构体标签的 JSON 是如何编码的:
74 |
75 | ```go
76 | package main
77 |
78 | import (
79 | "encoding/json"
80 | "fmt"
81 | "log"
82 | "os"
83 | "time"
84 | )
85 |
86 | type User struct {
87 | Name string
88 | Password string
89 | PreferredFish []string
90 | CreatedAt time.Time
91 | }
92 |
93 | func main() {
94 | u := &User{
95 | Name: "Sammy the Shark",
96 | Password: "fisharegreat",
97 | CreatedAt: time.Now(),
98 | }
99 |
100 | out, err := json.MarshalIndent(u, "", " ")
101 | if err != nil {
102 | log.Println(err)
103 | os.Exit(1)
104 | }
105 |
106 | fmt.Println(string(out))
107 | }
108 | ```
109 |
110 | 这将打印以下输出:
111 |
112 | ```bash
113 | Output
114 | {
115 | "Name": "Sammy the Shark",
116 | "Password": "fisharegreat",
117 | "CreatedAt": "2019-09-23T15:50:01.203059-04:00"
118 | }
119 | ```
120 |
121 | 我们定义了一个描述用户的结构,其字段包括用户的姓名、密码和用户的创建时间。在 `main` 方法中,我们为所有字段提供了值,除了 `PreferredFish`(Sammy 喜欢所有的鱼),从而创建了这个用户的实例。然后我们把 `User` 实例传递给 `json.MarshalIndent` 方法。这样我们可以更容易地看到 JSON 的输出,而不需要使用外部格式化工具。这个调用可以用 `json.Marshal(u)` 代替,以接收没有任何额外空白的 JSON。`json.MarshalIndent` 的两个额外参数控制输出的前缀(我们用空字符串省略了),以及缩进使用的字符,这里是两个空格字符。任何由 `json.MarshalIndent` 产生的错误都会被记录下来,程序使用 `os.Exit(1)` 终止。最后,我们将从 `json.MarshalIndent` 返回的 `[]byte` 转换为 `string`,并将生成的字符串交给 `fmt.Println` 处理以便在终端打印。
122 |
123 | 该结构的字段完全按照我们的命名出现。这不是您所期望的典型的 JSON 风格,它使用了字段名的骆驼字母大小写。在接下来的例子中,你将改变字段的名称,使其遵循骆驼大写的风格。正如你在运行这个例子时看到的,这不会起作用,因为想要的字段名与 Go 导出字段名的规则相冲突。
124 |
125 | ```go
126 | package main
127 |
128 | import (
129 | "encoding/json"
130 | "fmt"
131 | "log"
132 | "os"
133 | "time"
134 | )
135 |
136 | type User struct {
137 | name string
138 | password string
139 | preferredFish []string
140 | createdAt time.Time
141 | }
142 |
143 | func main() {
144 | u := &User{
145 | name: "Sammy the Shark",
146 | password: "fisharegreat",
147 | createdAt: time.Now(),
148 | }
149 |
150 | out, err := json.MarshalIndent(u, "", " ")
151 | if err != nil {
152 | log.Println(err)
153 | os.Exit(1)
154 | }
155 |
156 | fmt.Println(string(out))
157 | }
158 | ```
159 |
160 | 这将呈现以下输出:
161 |
162 | ```bash
163 | Output
164 | {}
165 | ```
166 |
167 | 在这个版本中,我们把字段的名字改成了驼峰式。现在 `Name` 是 `name`,`Password` 是 `password`,最后 `CreatedAt` 是 `createdAt`。在 `main` 方法中,我们改变了结构的实例化,以使用这些新的名称。然后我们像以前一样将结构体传递给 `json.MarshalIndent` 函数。这次输出是一个空的 JSON 对象,`{}`。
168 |
169 | 骆驼对字段的正确命名要求第一个字符必须是小写的。虽然 JSON 并不关心你如何命名你的字段,但 Go 关心,因为它表示字段在包外的可见性。由于 `encoding/json` 包与我们使用的 `main` 包是互相独立的,我们必须将第一个字符大写,以使其对 `encoding/json` 可见。看来我们陷入了僵局,我们需要一些方法来向 JSON 编码器传达我们希望这个字段被命名成什么。
170 |
171 | ## 使用结构体标签来控制编码
172 |
173 | 你可以修改前面的例子,通过给每个字段注解一个结构体标签,使导出的字段用驼峰大写的字段名进行正确编码。`encoding/json` 识别的结构体标签有一个 `json` 的键和一个控制输出的值。通过将字段名的驼峰版本作为 `json` 键的值,编码器将使用该名称代替。这个例子修正了前两次的尝试:
174 |
175 | ```go
176 | package main
177 |
178 | import (
179 | "encoding/json"
180 | "fmt"
181 | "log"
182 | "os"
183 | "time"
184 | )
185 |
186 | type User struct {
187 | Name string `json:"name"`
188 | Password string `json:"password"`
189 | PreferredFish []string `json:"preferredFish"`
190 | CreatedAt time.Time `json:"createdAt"`
191 | }
192 |
193 | func main() {
194 | u := &User{
195 | Name: "Sammy the Shark",
196 | Password: "fisharegreat",
197 | CreatedAt: time.Now(),
198 | }
199 |
200 | out, err := json.MarshalIndent(u, "", " ")
201 | if err != nil {
202 | log.Println(err)
203 | os.Exit(1)
204 | }
205 |
206 | fmt.Println(string(out))
207 | }
208 | ```
209 |
210 | 这将输出:
211 |
212 | ```bash
213 | Output
214 | {
215 | "name": "Sammy the Shark",
216 | "password": "fisharegreat",
217 | "preferredFish": null,
218 | "createdAt": "2019-09-23T18:16:17.57739-04:00"
219 | }
220 | ```
221 |
222 | 我们把字段名改回来了,通过把名字的第一个字母大写来让其他包看到。然而,这次我们以 `json: "name"` 的形式添加了结构体标签,其中 `"name"` 是我们希望 `json.MarshalIndent` 将结构体打印成 JSON 时使用的名称。
223 |
224 | 我们现在已经成功地正确格式化了我们的 JSON。然而,请注意,一些我们没有设置值的字段也被打印了出来。如果你愿意,JSON 编码器也可以省略这些字段。
225 |
226 | ## 删除空的 JSON 字段
227 |
228 | 最常见的是,我们想省略输出 JSON 中未设置的字段。由于 Go 中的所有类型都有一个“零值”,即它们被设置成的一些默认值,`encoding/json` 包需要额外的信息,以便能够告诉某些字段在赋这个零值时应该被视为未设置。在任何 `json` 结构体标签的值部分,你可以在你的字段的所需名称后面加上 `,omitempty` 来告诉 JSON 编码器,当字段被设置为零值时,省略这个字段的输出。下面的例子修正了之前的例子,不再输出空字段:
229 |
230 | ```go
231 | package main
232 |
233 | import (
234 | "encoding/json"
235 | "fmt"
236 | "log"
237 | "os"
238 | "time"
239 | )
240 |
241 | type User struct {
242 | Name string `json:"name"`
243 | Password string `json:"password"`
244 | PreferredFish []string `json:"preferredFish,omitempty"`
245 | CreatedAt time.Time `json:"createdAt"`
246 | }
247 |
248 | func main() {
249 | u := &User{
250 | Name: "Sammy the Shark",
251 | Password: "fisharegreat",
252 | CreatedAt: time.Now(),
253 | }
254 |
255 | out, err := json.MarshalIndent(u, "", " ")
256 | if err != nil {
257 | log.Println(err)
258 | os.Exit(1)
259 | }
260 |
261 | fmt.Println(string(out))
262 | }
263 | ```
264 |
265 | 这个例子将输出:
266 |
267 | ```bash
268 | Output
269 | {
270 | "name": "Sammy the Shark",
271 | "password": "fisharegreat",
272 | "createdAt": "2019-09-23T18:21:53.863846-04:00"
273 | }
274 | ```
275 |
276 | 我们修改了前面的例子,使 `PreferredFish` 字段现在有结构体标签 `json:"preferredFish,omitempty"`。`,omitempty` 的存在使 JSON 编码器跳过该字段,因为我们决定不设置它。在我们以前的例子的输出中,它的值是 `null`。
277 |
278 | 这个输出看起来好多了,但我们仍然打印出了用户的密码。`encoding/json` 包提供了另一种方法,让我们完全忽略私有字段。
279 |
280 | ## 忽略私有字段
281 |
282 | 有些字段必须从结构体中导出,以便其他包可以正确地与该类型交互。然而,这些字段的性质可能是敏感的,所以在这些情况下,即使它被设置了值,我们仍希望 JSON 编码器能够完全忽略该字段。这可以用特殊值 `"-"` 作为 `json:` 结构体标签的值参数来实现。
283 |
284 | 这个例子修正了暴露用户密码的问题。
285 |
286 | ```go
287 | package main
288 |
289 | import (
290 | "encoding/json"
291 | "fmt"
292 | "log"
293 | "os"
294 | "time"
295 | )
296 |
297 | type User struct {
298 | Name string `json:"name"`
299 | Password string `json:"-"`
300 | CreatedAt time.Time `json:"createdAt"`
301 | }
302 |
303 | func main() {
304 | u := &User{
305 | Name: "Sammy the Shark",
306 | Password: "fisharegreat",
307 | CreatedAt: time.Now(),
308 | }
309 |
310 | out, err := json.MarshalIndent(u, "", " ")
311 | if err != nil {
312 | log.Println(err)
313 | os.Exit(1)
314 | }
315 |
316 | fmt.Println(string(out))
317 | }
318 | ```
319 |
320 | 当你运行这个例子时,你会看到这样的输出:
321 |
322 | ```bash
323 | Output
324 | {
325 | "name": "Sammy the Shark",
326 | "createdAt": "2019-09-23T16:08:21.124481-04:00"
327 | }
328 | ```
329 |
330 | 这个例子与之前的例子相比,唯一的变化是密码字段。现在使用了特殊的 `"-"` 作为其 `json:` 结构体标签的值。我们看到,在这个例子的输出中,`password` 字段不再存在了。
331 |
332 | `encoding/json` 包的这些特征,`,omitempty` 和 `"-"`,并不是标准。一个包决定对结构体标签的值做什么取决于它的实现。因为 `encoding/json` 包是标准库的一部分,其他包也以同样的方式实现这些功能,这是一个惯例。然而,很重要的一点是阅读任何使用结构体标签的第三方软件包的文档,以了解哪些是支持的,哪些是不支持的。
333 |
334 | ## 总结
335 |
336 | 结构体标签提供了一种强大的手段来拓展了使用你定义的结构体代码的功能。许多标准库和第三方包提供了通过使用结构体标签来定制其操作的方法。在你的代码中有效地使用它们,既能提供这种定制行为,又能为未来的开发者简要记录这些字段的使用方法。
--------------------------------------------------------------------------------
/content/zh/docs/28-How_To_Use_Variadic_Functions_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中使用可变参数函数
2 |
3 | ## 介绍
4 |
5 | _可变参数函数_ 是可以接受零个、一个或多个值作为单个参数的函数。虽然可变参数函数并不常见,但它们能够使您的代码更清晰、更具可读性。
6 |
7 | 可变参数函数其实很常见。最常见的是[`fmt`](https://golang.org/pkg/fmt)包中的 `Println`。
8 |
9 | ```go
10 | func Println(a ...interface{}) (n int, err error)
11 | ```
12 |
13 | 参数前面带有一组省略号 ( `...` )的[函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})被视为可变参数函数。省略号表示提供的参数可以是零个、一个或多个。对于`fmt.Println`包,它声明参数`a`是可变参数。
14 |
15 | 让我们创建一个使用 `fmt.Println` 函数并传入零个、一个或多个值的程序:
16 |
17 | ```go
18 | package main
19 |
20 | import "fmt"
21 |
22 | func main() {
23 | fmt.Println()
24 | fmt.Println("one")
25 | fmt.Println("one", "two")
26 | fmt.Println("one", "two", "three")
27 | }
28 | ```
29 |
30 | 第一次调用 `fmt.Println` 时,我们不传递任何参数。第二次调用时,我们只传入一个参数,值为 `one`。 然后我们传递 `one` 和 `two`,最后是 `one`,`two` 和 `three` 三个值。
31 |
32 | 让我们使用以下命令运行程序:
33 |
34 | ```shell
35 | $ go run print.go
36 | ```
37 |
38 | 我们将看到以下输出:
39 |
40 | ```shell
41 | Output
42 | one
43 | one two
44 | one two three
45 | ```
46 |
47 | 输出的第一行为空。这是因为我们在第一次调用 `fmt.Println` 时没有传递任何参数。第二次打印了 `one` 。然后打印 `one` 和 `two`,最后打印 `one`,`two` 和 `three`。
48 |
49 | 现在我们已经了解了如何调用可变参数函数,让我们看看如何定义自己的可变参数函数。
50 |
51 | ## 定义可变参数函数
52 |
53 | 我们可以通过在参数前面使用省略号 ( `...` ) 来定义可变参数函数。让我们创建一个程序,当人们的名字被发送到函数时会进行问候:
54 |
55 | ```go
56 | package main
57 |
58 | import "fmt"
59 |
60 | func main() {
61 | sayHello()
62 | sayHello("Sammy")
63 | sayHello("Sammy", "Jessica", "Drew", "Jamie")
64 | }
65 |
66 | func sayHello(names ...string) {
67 | for _, n := range names {
68 | fmt.Printf("Hello %s\n", n)
69 | }
70 | }
71 | ```
72 |
73 | 我们创建了一个 `sayHello` 函数,它只接受一个名为`names`,该参数是可变参数,因为我们在数据类型之前放置了一个省略号 (`...`): `...string`。这告诉 Go 这个函数可以接受零个、一个或多个参数。
74 |
75 | `sayHello` 函数将 `names` 参数作为 [`slice`](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go/#%E5%88%87%E7%89%87) 接收。由于数据类型是 [`string`](https://gocn.github.io/How-To-Code-in-Go/docs/07-Understanding_Data_Types_in_Go/#%E5%AD%97%E7%AC%A6%E4%B8%B2),因此 `names` 可以将参数视为字符串切片 ( `[]string` ) 。我们可以使用[`range`](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)运算符创建一个循环并遍历字符串切片。
76 |
77 | 如果我们运行程序,我们将得到以下输出:
78 |
79 | ```shell
80 | Output
81 | Hello Sammy
82 | Hello Sammy
83 | Hello Jessica
84 | Hello Drew
85 | Hello Jamie
86 | ```
87 |
88 | 请注意,我们第一次调用 `sayHello` 什么也没打印。这是因为可变参数是一个空的字符串切片。 由于我们会遍历切片,因此没有任何内容可以迭代,并且 `fmt.Printf` 永远不会被调用。
89 |
90 | 让我们修改程序以检测接收到值的情况:
91 |
92 | ```go
93 | package main
94 |
95 | import "fmt"
96 |
97 | func main() {
98 | sayHello()
99 | sayHello("Sammy")
100 | sayHello("Sammy", "Jessica", "Drew", "Jamie")
101 | }
102 |
103 | func sayHello(names ...string) {
104 | if len(names) == 0 {
105 | fmt.Println("nobody to greet")
106 | return
107 | }
108 | for _, n := range names {
109 | fmt.Printf("Hello %s\n", n)
110 | }
111 | }
112 | ```
113 |
114 | 现在,通过使用[`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),如果没有传递任何值,则 `names` 长度将为 `0`,我们将打印出`nobody to greet`:
115 |
116 | ```shell
117 | Output
118 | nobody to greet
119 | Hello Sammy
120 | Hello Sammy
121 | Hello Jessica
122 | Hello Drew
123 | Hello Jamie
124 | ```
125 |
126 | 使用可变参数可以使您的代码更具可读性。让我们创建一个函数,将单词通过指定的分隔符连接在一起。我们将首先创建没有可变参数函数的程序来展示它的读取方式:
127 |
128 | ```go
129 | package main
130 |
131 | import "fmt"
132 |
133 | func main() {
134 | var line string
135 |
136 | line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"})
137 | fmt.Println(line)
138 |
139 | line = join(",", []string{"Sammy", "Jessica"})
140 | fmt.Println(line)
141 |
142 | line = join(",", []string{"Sammy"})
143 | fmt.Println(line)
144 | }
145 |
146 | func join(del string, values []string) string {
147 | var line string
148 | for i, v := range values {
149 | line = line + v
150 | if i != len(values)-1 {
151 | line = line + del
152 | }
153 | }
154 | return line
155 | }
156 | ```
157 |
158 | 在这个程序中,我们将逗号 (`,`) 作为分隔符传递给 `join` 函数,再传递一个字符串切片。输出如下:
159 |
160 | ```shell
161 | Output
162 | Sammy,Jessica,Drew,Jamie
163 | Sammy,Jessica
164 | Sammy
165 | ```
166 |
167 | 因为该函数将字符串切片作为 `values` 参数,所以当我们调用 `join` 函数时,我们必须将所有单词包装在一个切片中。这会使代码难以阅读。
168 |
169 | 现在,让我们编写相同的函数,但我们将使用可变参数函数:
170 |
171 | ```go
172 | package main
173 |
174 | import "fmt"
175 |
176 | func main() {
177 | var line string
178 |
179 | line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
180 | fmt.Println(line)
181 |
182 | line = join(",", "Sammy", "Jessica")
183 | fmt.Println(line)
184 |
185 | line = join(",", "Sammy")
186 | fmt.Println(line)
187 | }
188 |
189 | func join(del string, values ...string) string {
190 | var line string
191 | for i, v := range values {
192 | line = line + v
193 | if i != len(values)-1 {
194 | line = line + del
195 | }
196 | }
197 | return line
198 | }
199 | ```
200 |
201 | 如果我们运行该程序,可以看到我们得到了与之前的程序相同的输出:
202 |
203 | ```shell
204 | Output
205 | Sammy,Jessica,Drew,Jamie
206 | Sammy,Jessica
207 | Sammy
208 | ```
209 |
210 | 虽然这两个版本的 `join` 函数执行完全相同的操作,但可变参数函数的版本在调用时更容易阅读。
211 |
212 | ## 可变参数顺序
213 |
214 | 一个函数中只能有一个可变参数,并且它必须是函数定义中的最后一个参数。将可变参数放在最后位置以外的任何顺序在函数定义会导致编译错误:
215 |
216 | ```go
217 | package main
218 |
219 | import "fmt"
220 |
221 | func main() {
222 | var line string
223 |
224 | line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
225 | fmt.Println(line)
226 |
227 | line = join(",", "Sammy", "Jessica")
228 | fmt.Println(line)
229 |
230 | line = join(",", "Sammy")
231 | fmt.Println(line)
232 | }
233 |
234 | func join(values ...string, del string) string {
235 | var line string
236 | for i, v := range values {
237 | line = line + v
238 | if i != len(values)-1 {
239 | line = line + del
240 | }
241 | }
242 | return line
243 | }
244 | ```
245 |
246 | 这次我们把 `values` 参数放在 `join` 函数的首位时,将导致以下编译错误:
247 |
248 | ```shell
249 | Output
250 | ./join_error.go:18:11: syntax error: cannot use ... with non-final parameter values
251 | ```
252 |
253 | 定义任何可变参数函数时,只有最后一个参数可以是可变参数。
254 |
255 | ## 分解参数
256 |
257 | 到目前为止,我们已经看到我们可以将零个、一个或多个值传递给可变参数函数。但是,有时我们有一个切片并且我们希望将它们发送到可变参数函数。
258 |
259 | 让我们用上一节中的 `join` 函数尝试一下,看看会发生什么:
260 |
261 | ```go
262 | package main
263 |
264 | import "fmt"
265 |
266 | func main() {
267 | var line string
268 |
269 | names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
270 |
271 | line = join(",", names)
272 | fmt.Println(line)
273 | }
274 |
275 | func join(del string, values ...string) string {
276 | var line string
277 | for i, v := range values {
278 | line = line + v
279 | if i != len(values)-1 {
280 | line = line + del
281 | }
282 | }
283 | return line
284 | }
285 | ```
286 |
287 | 如果我们运行这个程序,我们会收到一个编译错误:
288 |
289 | ```shell
290 | Output
291 | ./join-error.go:10:14: cannot use names (type []string) as type string in argument to join
292 | ```
293 |
294 | 即使可变参数函数将 `values ...string` 参数转换为字符串切片 `[]string`,我们也不能将字符串切片作为参数传递。这是因为编译器需要的是字符串的离散参数。
295 |
296 | 为了解决这个问题,我们可以通过在 _切片_ 后面加上一组省略号 (`...`) 来分解切片,将其转换为离散参数,然后传递给可变参数函数:
297 |
298 |
299 | ```go
300 | package main
301 |
302 | import "fmt"
303 |
304 | func main() {
305 | var line string
306 |
307 | names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
308 |
309 | line = join(",", names...)
310 | fmt.Println(line)
311 | }
312 |
313 | func join(del string, values ...string) string {
314 | var line string
315 | for i, v := range values {
316 | line = line + v
317 | if i != len(values)-1 {
318 | line = line + del
319 | }
320 | }
321 | return line
322 | }
323 | ```
324 |
325 | 这一次,当我们调用 `join` 函数时,我们通过附加省略号 (`...`)来分解 `names` 切片。
326 |
327 | 这样程序就会按照预期运行:
328 |
329 | ```shell
330 | Output
331 | Sammy,Jessica,Drew,Jamie
332 | ```
333 |
334 | 要注意,我们仍然可以传递零个、一个或多个参数,以及我们分解的切片。以下是我们迄今为止看到的所有变体的代码:
335 |
336 | ```go
337 | package main
338 |
339 | import "fmt"
340 |
341 | func main() {
342 | var line string
343 |
344 | line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...)
345 | fmt.Println(line)
346 |
347 | line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
348 | fmt.Println(line)
349 |
350 | line = join(",", "Sammy", "Jessica")
351 | fmt.Println(line)
352 |
353 | line = join(",", "Sammy")
354 | fmt.Println(line)
355 |
356 | }
357 |
358 | func join(del string, values ...string) string {
359 | var line string
360 | for i, v := range values {
361 | line = line + v
362 | if i != len(values)-1 {
363 | line = line + del
364 | }
365 | }
366 | return line
367 | }
368 | ```
369 |
370 | ```shell
371 | Output
372 | Sammy,Jessica,Drew,Jamie
373 | Sammy,Jessica,Drew,Jamie
374 | Sammy,Jessica
375 | Sammy
376 | ```
377 |
378 | 现在,我们知道了如何将零个、一个或多个参数以及我们分解的切片传递给可变参数函数。
379 |
380 | ## 结论
381 |
382 | 在本教程中,我们了解了可变参数函数如何使您的代码更简洁。虽然您并不总是需要使用它们,但您可能会发现它们很有用:
383 |
384 | - 如果你创建了一个临时切片,只是为了传递给函数。
385 | - 当输入参数的数量未知或调用时会发生变化。
386 | - 使您的代码更具可读性。
387 |
388 | 要了解有关创建和调用函数的更多信息,您可以阅读[如何在 Go 中定义和调用函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})。
389 |
--------------------------------------------------------------------------------
/content/zh/docs/23-How_To_Write_Conditional_Statements_in_Go.md:
--------------------------------------------------------------------------------
1 | # 如何在 Go 中编写条件语句
2 |
3 | ## 介绍
4 |
5 | 条件性语句是每一种编程语言的组成部分。通过条件语句,我们可以让代码有时运行,有时不运行,这取决于当时程序的条件。
6 |
7 | 当我们完全执行程序的每个语句时,我们并没有要求程序评估特定的条件。通过使用条件语句,程序可以确定某些条件是否被满足,然后被告知下一步该做什么。
8 |
9 | 让我们来看看一些使用条件语句的例子。
10 |
11 | - 如果学生的考试成绩超过65%,报告她的成绩通过;如果没有,报告她的成绩不合格。
12 | - 如果他的账户里有钱,就计算利息;如果没有,就收取罚款。
13 | - 如果他们买了10个或更多的橙子,计算5%的折扣;如果他们买的少,就不买。
14 |
15 | 通过评估条件,并根据是否满足这些条件来分配代码运行,我们就是在写条件代码。
16 |
17 | 本教程将带你了解在 Go 编程语言中编写条件语句。
18 |
19 | ## If 语句
20 |
21 | 我们将从 `if` 语句开始,它将评估一个语句是真的还是假的,并且只在该语句为真的情况下运行代码。
22 |
23 | 在一个纯文本编辑器中,打开一个文件,写入以下代码:
24 |
25 | ```go
26 | package main
27 |
28 | import "fmt"
29 |
30 | func main() {
31 | grade := 70
32 |
33 | if grade >= 65 {
34 | fmt.Println("Passing grade")
35 | }
36 | }
37 | ```
38 |
39 | 在这段代码中,我们有一个变量`grade`,并给它一个整数值`70`。然后我们使用`if`语句来评估变量`grade`是否大于或等于(`>=`)`65`。如果它确实满足这个条件,我们告诉程序打印出[字符串]({{< relref "/docs/08-An_Introduction_to_Working_with_Strings_in_Go.md" >}}) `Passing grade`。
40 |
41 | 将程序保存为`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`命令运行它。
42 |
43 | 在这种情况下,70分的成绩*符合大于或等于65分的条件,因此,一旦你运行该程序,你将收到以下输出:
44 |
45 | ```Output
46 | Passing grade
47 | ```
48 |
49 | 现在让我们改变这个程序的结果,把`grade`变量的值改为`60`:
50 |
51 | ```go
52 | package main
53 |
54 | import "fmt"
55 |
56 | func main() {
57 | grade := 60
58 |
59 | if grade >= 65 {
60 | fmt.Println("Passing grade")
61 | }
62 | }
63 | ```
64 |
65 | 当我们保存并运行*这个*代码时,我们不会收到任何输出,因为条件*没有得到满足,我们也没有告诉程序执行另一条语句。
66 |
67 | 再举一个例子,让我们计算一个银行账户余额是否低于0。让我们创建一个名为`account.go`的文件,并编写以下程序:
68 |
69 | ```go
70 | package main
71 |
72 | import "fmt"
73 |
74 | func main() {
75 | balance := -5
76 |
77 | if balance < 0 {
78 | fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.")
79 | }
80 | }
81 | ```
82 |
83 | 当我们用`go run account.go`运行该程序时,我们会收到以下输出:
84 |
85 | ```Output
86 | Balance is below 0, add funds now or you will be charged a penalty.
87 | ```
88 |
89 | 在程序中,我们将变量`balance`初始化为`5`,即小于0。由于`balance`符合`if`语句的条件(`balance<0`),一旦我们保存并运行代码,我们将收到字符串的输出。同样,如果我们把余额改为0或一个正数,我们将不会收到任何输出。
90 | ## Else 语句
91 |
92 | 我们很可能希望程序在 `if`语句评估为假时也能有所作为。在我们的成绩例子中,我们希望输出成绩是合格还是不合格。
93 |
94 | 要做到这一点,我们将在上面的成绩条件中添加一个 `else` 语句,其结构如下:
95 |
96 | ```go
97 | package main
98 |
99 | import "fmt"
100 |
101 | func main() {
102 | grade := 60
103 |
104 | if grade >= 65 {
105 | fmt.Println("Passing grade")
106 | } else {
107 | fmt.Println("Failing grade")
108 | }
109 | }
110 | ```
111 |
112 | 由于成绩变量的值是`60`,`if`语句评估为假,所以程序不会打印出`Passing grade`。接下来的 `else` 语句告诉程序无论如何都要做一些事情。
113 |
114 | 当我们保存并运行该程序时,我们将收到以下输出:
115 |
116 | ```Output
117 | Failing grade
118 | ```
119 |
120 | 如果我们重写程序,给成绩一个`65`或更高的值,我们将收到`Passing grade`的输出。
121 |
122 | 为了给银行账户的例子增加一个 `else` 语句,我们这样改写代码:
123 |
124 | ```go
125 | package main
126 |
127 | import "fmt"
128 |
129 | func main() {
130 | balance := 522
131 |
132 | if balance < 0 {
133 | fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.")
134 | } else {
135 | fmt.Println("Your balance is 0 or above.")
136 | }
137 | }
138 | ```
139 |
140 | ```Output
141 | Your balance is 0 or above.
142 | ```
143 |
144 | 在这里,我们把`balance`变量的值改为正数,这样`else`语句就会打印出来。为了让第一个`if`语句打印出来,我们可以把这个值改写成一个负数。
145 |
146 | 通过将`if`语句和`else`语句结合起来,你就构建了一个由两部分组成的条件语句,无论`if`条件是否满足,都会告诉计算机执行某些代码。
147 |
148 | ## Else if 语句
149 |
150 | 到目前为止,我们已经为条件语句提出了一个[布尔]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})选项,每个`if`语句的评估结果为真或假。在许多情况下,我们会希望一个程序能评估出两个以上的可能结果。为此,我们将使用**else if**语句,在 Go 中写成`else if`。`else if`或 else if 语句看起来和`if`语句一样,将评估另一个条件。
151 |
152 | 在银行账户程序中,我们可能希望在三种不同的情况下有三个离散的输出。
153 |
154 | - 余额低于0
155 | - 余额等于0
156 | - 余额高于0
157 |
158 | `else if`语句将被放在 `if` 语句和 `else` 语句之间,如下所示:
159 |
160 | ```go
161 | package main
162 |
163 | import "fmt"
164 |
165 | func main() {
166 | balance := 522
167 |
168 | if balance < 0 {
169 | fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.")
170 | } else if balance == 0 {
171 | fmt.Println("Balance is equal to 0, add funds soon.")
172 | } else {
173 | fmt.Println("Your balance is 0 or above.")
174 | }
175 | }
176 | ```
177 |
178 | 现在,一旦我们运行该程序,有三种可能的输出:
179 |
180 | - 如果变量`余额`等于`0`,我们将收到`else if`语句的输出(`余额等于0,尽快添加资金。)
181 | - 如果变量`balance`被设置为一个正数,我们将收到`else`语句的输出(`你的余额为0或以上`)。
182 | - 如果变量`balance`被设置为一个负数,输出将是`if`语句的字符串(`余额低于0,现在添加资金,否则将被收取罚款`)。
183 |
184 | 如果我们想有三个以上的可能性呢?我们可以通过在代码中写一个以上的`else if`语句来实现。
185 |
186 | 在`grade.go`程序中,让我们重写代码,以便有几个字母等级对应于数字等级的范围。
187 |
188 | - 90分或以上相当于A级
189 | - 80-89相当于B级
190 | - 70-79相当于C级
191 | - 65-69相当于D级
192 | - 64分或以下相当于F级
193 |
194 | 要运行这段代码,我们将需要一个`if`语句,三个`else if`语句,以及一个处理所有失败情况的`else`语句。
195 |
196 | 让我们重写前面的例子中的代码,让字符串打印出每个字母等级。我们可以保持我们的`else`语句不变。
197 |
198 | ```go
199 | package main
200 |
201 | import "fmt"
202 |
203 | func main() {
204 | grade := 60
205 |
206 | if grade >= 90 {
207 | fmt.Println("A grade")
208 | } else if grade >= 80 {
209 | fmt.Println("B grade")
210 | } else if grade >= 70 {
211 | fmt.Println("C grade")
212 | } else if grade >= 65 {
213 | fmt.Println("D grade")
214 | } else {
215 | fmt.Println("Failing grade")
216 | }
217 | }
218 | ```
219 |
220 | 由于`else if`语句将按顺序评估,我们可以保持我们的语句相当基本。这个程序正在完成以下步骤。
221 |
222 | 1. 如果成绩大于90,程序将打印 "A 级",如果成绩小于90,程序将继续下一个语句...。
223 | 2. 如果成绩大于或等于80,程序将打印 "B 级",如果成绩在79或以下,程序将继续下一个语句......
224 | 3. 如果成绩大于或等于70,程序将打印 "C 级",如果成绩是69或更少,程序将继续下一个语句......
225 | 4. 如果成绩大于或等于65,程序将打印 "D 级",如果成绩是64或更少,程序将继续下一个语句......
226 | 5. 程序将打印 "成绩不合格",因为上述所有的条件都没有满足。
227 |
228 | ## 嵌套的If语句
229 |
230 | 一旦你对 `if`, `else if`, 和 `else`语句感到满意,你就可以转到嵌套条件语句。我们可以使用嵌套的`if`语句来处理这样的情况:如果第一个条件执行为真,我们想检查第二个条件。为此,我们可以在另一个 if-else 语句中设置一个 if-else 语句。让我们来看看嵌套的`if`语句的语法。
231 |
232 | ```go
233 | if statement1 { // outer if statement
234 | fmt.Println("true")
235 |
236 | if nested_statement { // nested if statement
237 | fmt.Println("yes")
238 | } else { // nested else statement
239 | fmt.Println("no")
240 | }
241 |
242 | } else { // outer else statement
243 | fmt.Println("false")
244 | }
245 | ```
246 |
247 | 这段代码可以产生一些可能的输出。
248 |
249 | - 如果`statement1`评估为真,程序将评估`nested_statement`是否也评估为真。如果这两种情况都是真的,那么输出将是:
250 |
251 | > ```Output
252 | > true
253 | > yes
254 | > ```
255 |
256 | - 然而,如果`statement1`评估为真,但`nested_statement`评估为假,那么输出将是:
257 |
258 | > ```Output
259 | > true
260 | > no
261 | > ```
262 |
263 | - 而如果`statement1`评估为 false,嵌套的 if-else 语句将不会运行,所以`else`语句将单独运行,输出结果为:
264 |
265 | > ```Output
266 | > false
267 | > ```
268 |
269 | 我们也可以在代码中嵌套多个`if`语句:
270 |
271 | ```go
272 | if statement1 { // outer if
273 | fmt.Println("hello world")
274 |
275 | if nested_statement1 { // first nested if
276 | fmt.Println("yes")
277 |
278 | } else if nested_statement2 { // first nested else if
279 | fmt.Println("maybe")
280 |
281 | } else { // first nested else
282 | fmt.Println("no")
283 | }
284 |
285 | } else if statement2 { // outer else if
286 | fmt.Println("hello galaxy")
287 |
288 | if nested_statement3 { // second nested if
289 | fmt.Println("yes")
290 | } else if nested_statement4 { // second nested else if
291 | fmt.Println("maybe")
292 | } else { // second nested else
293 | fmt.Println("no")
294 | }
295 |
296 | } else { // outer else
297 | statement("hello universe")
298 | }
299 | ```
300 |
301 | 在这段代码中,除了 `else if` 语句外,每个 `if` 语句内都有一个嵌套的 `if` 语句。这将使每个条件内有更多的选项。
302 |
303 | 让我们用`grade.go`程序来看一个嵌套`if`语句的例子。可以首先检查一个成绩是否合格(大于或等于65%),然后评估数字成绩应该相当于哪个字母等级。如果成绩不合格,我们就不需要运行字母等级,而可以让程序报告该成绩不合格。修改后的代码和嵌套的 `if` 语句看起来是这样的:
304 |
305 | ```go
306 | package main
307 |
308 | import "fmt"
309 |
310 | func main() {
311 | grade := 92
312 | if grade >= 65 {
313 | fmt.Print("Passing grade of: ")
314 |
315 | if grade >= 90 {
316 | fmt.Println("A")
317 |
318 | } else if grade >= 80 {
319 | fmt.Println("B")
320 |
321 | } else if grade >= 70 {
322 | fmt.Println("C")
323 |
324 | } else if grade >= 65 {
325 | fmt.Println("D")
326 | }
327 |
328 | } else {
329 | fmt.Println("Failing grade")
330 | }
331 | }
332 | ```
333 |
334 | 如果我们在运行代码时将变量`grade`设置为整数值`92`,那么第一个条件就得到了满足,程序将打印出`Passing grade of:`。接下来,它将检查成绩是否大于或等于90,由于这个条件也被满足,它将打印出`A`。
335 |
336 | 如果我们在运行代码时将`grade`变量设置为`60`,那么第一个条件就没有得到满足,所以程序将跳过嵌套的`if`语句,向下移动到`else`语句,程序将打印出`Failing grade`。
337 |
338 | 当然,我们可以在此基础上增加更多的选项,并使用第二层嵌套的if语句。也许我们想对A+、A和A-的成绩分别进行评估。我们可以这样做,首先检查成绩是否合格,然后检查成绩是否在90分或以上,然后检查成绩是否在96分以上为A+:
339 |
340 | ```go
341 | ...
342 | if grade >= 65 {
343 | fmt.Print("Passing grade of: ")
344 |
345 | if grade >= 90 {
346 | if grade > 96 {
347 | fmt.Println("A+")
348 |
349 | } else if grade > 93 && grade <= 96 {
350 | fmt.Println("A")
351 |
352 | } else {
353 | fmt.Println("A-")
354 | }
355 | ...
356 | ```
357 |
358 | 在这段代码中,对于设置为96的 `grade` 变量,程序将运行以下程序。
359 |
360 | 1. 检查该等级是否大于或等于65(真)。
361 | 2. 打印出 `Passing grade of:`
362 | 3. 检查成绩是否大于或等于90(真)。
363 | 4. 检查成绩是否大于96(假)。
364 | 5. 检查等级是否大于93,同时小于或等于96(真)。
365 | 6. 打印 "A"。
366 | 7. 离开这些嵌套的条件语句,继续执行剩余的代码
367 |
368 | 因此,成绩为96的程序的输出看起来是这样的:
369 |
370 | ```Output
371 | Passing grade of: A
372 | ```
373 |
374 | 嵌套的`if`语句可以提供机会,在你的代码中添加几个特定级别的条件。
375 |
376 | ## 总结
377 |
378 | 通过使用像 `if` 语句这样的条件语句,你将对你的程序执行内容有更大的控制。条件性语句告诉程序要评估是否满足某个条件。如果满足条件,它将执行特定的代码,但如果不满足条件,程序将继续执行其他代码。
379 |
380 | 要继续练习条件语句,请尝试使用不同的[运算符]({{< relref "/docs/13-How_To_Do_Math_in_Go_with_Operators.md" >}})来获得对条件语句的更多熟悉。
--------------------------------------------------------------------------------
/content/zh/docs/29-Understanding_defer_in_Go.md:
--------------------------------------------------------------------------------
1 | # 了解 Go 中的 defer
2 |
3 | ## 简介
4 |
5 | Go 有许多其他编程语言中常见的控制流关键字,如 `if`、`switch`、`for` 等。有一个关键词在大多数其他编程语言中都没有,那就是 `defer` ,虽然它不太常见,但你很快就会发现它在你的程序中是多么有用。
6 |
7 | `defer` 语句的主要用途之一是清理资源,如打开的文件、网络连接和[数据库句柄](https://en.wikipedia.org/wiki/Handle_(computing))。当你的程序使用完这些资源后,关闭它们很重要,以避免耗尽程序的限制,并允许其他程序访问这些资源。`defer` 通过保持关闭文件/资源的调用与打开调用保持一致,使我们的代码更加简洁,不易出错。
8 |
9 | 在这篇文章中,我们将学习如何正确使用 `defer` 语句来清理资源,以及使用 `defer` 时常犯的几个错误。
10 |
11 | ## 什么是 `defer` 语句
12 |
13 | `defer` 语句将 `defer` 关键字后面的[函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})调用添加到一个栈中。当该语句所在的函数返回时,将执行堆栈中所有的函数调用。由于这些调用位于堆栈上,因此将按照后进先出的顺序进行调用。
14 |
15 | 让我们看看 `defer` 是如何工作的,打印出一些文本:
16 |
17 | main.go
18 |
19 | ```go
20 | package main
21 |
22 | import "fmt"
23 |
24 | func main() {
25 | defer fmt.Println("Bye")
26 | fmt.Println("Hi")
27 | }
28 | ```
29 |
30 |
31 | 在 `main` 函数中,我们有两条语句。第一条语句以 `defer` 关键字开始,后面是 `print` 语句,打印出 `Bye`。下一行打印出 `Hi`。
32 |
33 | 如果我们运行该程序,我们将看到以下输出:
34 |
35 | ```shell
36 | Hi
37 | Bye
38 | ```
39 |
40 | 请注意,`Hi` 被首先打印出来。这是因为以 `defer` 为前缀的语句直到该函数结束前,都不会被调用。
41 |
42 | 让我们再看看这个程序,这次我们将添加一些注释来帮助说明正在发生的事情:
43 |
44 | main.go
45 |
46 | ```go
47 | package main
48 |
49 | import "fmt"
50 |
51 | func main() {
52 | // defer statement is executed, and places
53 | // fmt.Println("Bye") on a list to be executed prior to the function returning
54 | defer fmt.Println("Bye")
55 |
56 | // The next line is executed immediately
57 | fmt.Println("Hi")
58 |
59 | // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
60 | }
61 | ```
62 |
63 |
64 | 理解 `defer` 的关键是,当 `defer` 语句被执行时,延迟函数的参数被立即评估。当 `defer` 执行时,它把后面的语句放在一个列表中,在函数返回之前被调用。
65 |
66 | 虽然这段代码说明了 `defer` 的运行顺序,但这并不是编写 Go 程序时的典型使用方式。我们更可能使用 `defer` 来清理资源,例如文件句柄。接下来让我们看看如何做到这一点。
67 |
68 | ## 使用 `defer` 来清理资源
69 |
70 | 使用 `defer` 来清理资源在 Go 中是非常常见的。让我们先看看一个将字符串写入文件的程序,但没有使用 `defer` 来处理资源清理的问题:
71 |
72 | main.go
73 |
74 | ```go
75 | package main
76 |
77 | import (
78 | "io"
79 | "log"
80 | "os"
81 | )
82 |
83 | func main() {
84 | if err := write("readme.txt", "This is a readme file"); err != nil {
85 | log.Fatal("failed to write file:", err)
86 | }
87 | }
88 |
89 | func write(fileName string, text string) error {
90 | file, err := os.Create(fileName)
91 | if err != nil {
92 | return err
93 | }
94 | _, err = io.WriteString(file, text)
95 | if err != nil {
96 | return err
97 | }
98 | file.Close()
99 | return nil
100 | }
101 | ```
102 |
103 |
104 | 在这个程序中,有一个叫做 `write` 的函数,它将首先尝试创建一个文件。如果它有错误,它将返回错误并退出函数。接下来,它试图将字符串 `This is a readme file` 写到指定文件中。如果它收到一个错误,它将返回错误并退出该函数。然后,该函数将尝试关闭该文件并将资源释放回系统。最后,该函数返回 `nil` 以表示该函数的执行没有错误。
105 |
106 | 虽然这段代码可以工作,但有一个细微的错误。如果对 `io.WriteString` 的调用失败,该函数将在没有关闭文件并将资源释放回系统的情况下返回。
107 |
108 | 我们可以通过添加另一个 `file.Close()` 语句来解决这个问题,在没有 `defer` 的语言中,你可能会这样解决:
109 |
110 | main.go
111 |
112 | ```go
113 | package main
114 |
115 | import (
116 | "io"
117 | "log"
118 | "os"
119 | )
120 |
121 | func main() {
122 | if err := write("readme.txt", "This is a readme file"); err != nil {
123 | log.Fatal("failed to write file:", err)
124 | }
125 | }
126 |
127 | func write(fileName string, text string) error {
128 | file, err := os.Create(fileName)
129 | if err != nil {
130 | return err
131 | }
132 | _, err = io.WriteString(file, text)
133 | if err != nil {
134 | file.Close()
135 | return err
136 | }
137 | file.Close()
138 | return nil
139 | }
140 | ```
141 |
142 | 现在,即使调用 `io.WriteString` 失败了,我们仍然会关闭该文件。虽然这是一个相对容易发现和修复的错误,但对于一个更复杂的函数来说,它可能会被遗漏。
143 |
144 | 我们可以使用 `defer` 语句来确保在执行过程中无论采取何种分支,我们都会调用 `Close()` ,而不是增加对 `file.Close()` 的第二次调用。
145 |
146 | 下面是使用 `defer` 关键字的版本:
147 |
148 | main.go
149 |
150 | ```go
151 | package main
152 |
153 | import (
154 | "io"
155 | "log"
156 | "os"
157 | )
158 |
159 | func main() {
160 | if err := write("readme.txt", "This is a readme file"); err != nil {
161 | log.Fatal("failed to write file:", err)
162 | }
163 | }
164 |
165 | func write(fileName string, text string) error {
166 | file, err := os.Create(fileName)
167 | if err != nil {
168 | return err
169 | }
170 | defer file.Close()
171 | _, err = io.WriteString(file, text)
172 | if err != nil {
173 | return err
174 | }
175 | return nil
176 | }
177 | ```
178 |
179 | 这一次我们添加了这行代码,`defer file.Close()`。这告诉编译器,它应该在退出函数 `write` 之前执行 `file.Close`。
180 |
181 | 现在我们已经确保,即使我们在未来添加更多的代码并创建另一个退出该函数的分支,我们也会一直清理并关闭该文件。
182 |
183 | 然而,我们通过添加 `defer` 引入了另一个错误。我们不再检查可能从 `Close` 方法返回的潜在错误。这是因为当我们使用 `defer` 时,没有办法将任何返回值传回给我们的函数。
184 |
185 | 在 Go 中,在不影响程序行为的情况下多次调用 `Close()` 被认为是一种安全和公认的做法。如果 `Close()` 要返回一个错误,它将在第一次被调用时返回。这使得我们可以在函数的成功执行路径中明确地调用它。
186 |
187 | 让我们看看我们如何既能 `defer` 对 `Close` 的调用,又能在遇到错误时报告错误。
188 |
189 | main.go
190 |
191 | ```go
192 | package main
193 |
194 | import (
195 | "io"
196 | "log"
197 | "os"
198 | )
199 |
200 | func main() {
201 | if err := write("readme.txt", "This is a readme file"); err != nil {
202 | log.Fatal("failed to write file:", err)
203 | }
204 | }
205 |
206 | func write(fileName string, text string) error {
207 | file, err := os.Create(fileName)
208 | if err != nil {
209 | return err
210 | }
211 | defer file.Close()
212 | _, err = io.WriteString(file, text)
213 | if err != nil {
214 | return err
215 | }
216 |
217 | return file.Close()
218 | }
219 | ```
220 |
221 | 这个程序中唯一的变化是最后一行,我们返回 `file.Close()`。如果对 `Close` 的调用导致错误,现在将按照预期返回给调用函数。请记住,我们的 `defer file.Close()` 语句也将在 `return `语句之后运行。这意味着 `file.Close()` 有可能被调用两次。虽然这并不理想,但这是可以接受的做法,因为它不应该对你的程序产生任何副作用。
222 |
223 | 然而,如果我们在函数的早期收到一个错误,例如当我们调用 `WriteString` 时,函数将返回该错误,并且也将尝试调用 `file.Close`,因为它被推迟了。尽管 `file.Close` 也可能(而且很可能)返回一个错误,但这不再是我们关心的事情,因为我们收到的错误更有可能告诉我们一开始就出了什么问题。
224 |
225 | 到目前为止,我们已经看到我们如何使用一个 `defer` 来确保我们正确地清理我们的资源。接下来我们将看到如何使用多个 `defer` 语句来清理多个资源。
226 |
227 | ## 多个 `defer` 语句
228 |
229 | 在一个函数中拥有多个 `defer` 语句是很正常的。让我们创建一个只有 `defer` 语句的程序,看看当我们引入多个 `defer` 时,会发生什么情况:
230 |
231 | main.go
232 |
233 | ```go
234 | package main
235 |
236 | import "fmt"
237 |
238 | func main() {
239 | defer fmt.Println("one")
240 | defer fmt.Println("two")
241 | defer fmt.Println("three")
242 | }
243 | ```
244 |
245 | 如果我们运行该程序,我们将收到以下输出结果:
246 |
247 | ```shell
248 | three
249 | two
250 | one
251 | ```
252 |
253 | 注意,顺序与我们调用 `defer` 语句的顺序相反。这是因为每个被调用的延迟语句都是堆叠在前一个语句之上的,然后在函数退出范围时反向调用(*后进先出*)。
254 |
255 | 在一个函数中,你可以根据需要有尽可能多的 `defer` 调用,但重要的是要记住它们都将以相反的顺序被调用。
256 |
257 | 现在我们了解了多个延迟的执行顺序,让我们看看如何使用多个延迟来清理多个资源。我们将创建一个程序,打开一个文件,向其写入内容,然后再次打开,将内容复制到另一个文件。
258 |
259 | main.go
260 |
261 | ```go
262 | package main
263 |
264 | import (
265 | "fmt"
266 | "io"
267 | "log"
268 | "os"
269 | )
270 |
271 | func main() {
272 | if err := write("sample.txt", "This file contains some sample text."); err != nil {
273 | log.Fatal("failed to create file")
274 | }
275 |
276 | if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
277 | log.Fatal("failed to copy file: %s")
278 | }
279 | }
280 |
281 | func write(fileName string, text string) error {
282 | file, err := os.Create(fileName)
283 | if err != nil {
284 | return err
285 | }
286 | defer file.Close()
287 | _, err = io.WriteString(file, text)
288 | if err != nil {
289 | return err
290 | }
291 |
292 | return file.Close()
293 | }
294 |
295 | func fileCopy(source string, destination string) error {
296 | src, err := os.Open(source)
297 | if err != nil {
298 | return err
299 | }
300 | defer src.Close()
301 |
302 | dst, err := os.Create(destination)
303 | if err != nil {
304 | return err
305 | }
306 | defer dst.Close()
307 |
308 | n, err := io.Copy(dst, src)
309 | if err != nil {
310 | return err
311 | }
312 | fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)
313 |
314 | if err := src.Close(); err != nil {
315 | return err
316 | }
317 |
318 | return dst.Close()
319 | }
320 | ```
321 |
322 |
323 | 我们添加了一个新的函数,叫做 `fileCopy`。在这个函数中,我们首先打开我们要复制的源文件。我们检查我们是否收到了一个打开文件的错误。如果是的话,我们 `return` 错误并退出该函数。否则,我们 `defer` 关闭我们刚刚打开的源文件。
324 |
325 | 接下来我们创建目标文件。再次,我们检查我们是否收到了创建文件的错误。如果是的话,我们 `return` 该错误并退出该函数。否则,我们也 `defer` 目标文件的 `Close()`。我们现在有两个 `defer` 函数,当函数退出其作用域时将被调用。
326 |
327 | 现在我们已经打开了两个文件,我们将`Copy()` 数据从源文件到目标文件。如果成功的话,我们将尝试关闭两个文件。如果我们在试图关闭任何一个文件时收到错误,我们将 `return` 错误并退出函数作用域。
328 |
329 | 注意,我们为每个文件明确地调用 `Close()`,尽管 `defer` 也将调用 `Close()`。这是为了确保如果关闭文件时出现错误,我们会报告这个错误。这也确保了如果因为任何原因函数提前退出,例如我们在两个文件之间复制失败,每个文件仍将尝试从延迟调用中正确关闭。
330 |
331 | ## 总结
332 |
333 | 在这篇文章中,我们了解了 `defer` 语句,以及如何使用它来确保我们在程序中正确清理系统资源。正确地清理系统资源将使你的程序使用更少的内存,表现更好。要了解更多关于 `defer` 的使用,请阅读处理恐慌的文章,或者探索我们整个[如何在 Go 中编码系列](https://gocn.github.io/How-To-Code-in-Go/)。
334 |
335 |
--------------------------------------------------------------------------------
/content/zh/docs/40-How_To_Use_the_Flag_Package_in_Go.md:
--------------------------------------------------------------------------------
1 | # 在 Go 里面如何使用 Flag 包
2 |
3 | ## 简介
4 |
5 | 命令行工具很少在没有额外配置的情况下开箱即用。好的默认值固然很重要,但有用的工具需要接受用户的配置。在大多数平台上,命令行工具通过接收标志来指定命令的执行。标志是以键值分隔的字符串,加在命令的名称后面。Go 让你通过使用标准库中的 flag 包来制作接受标志的命令行工具。
6 |
7 | 在本教程中,你将探索使用 flag 包来建立不同种类的命令行工具的各种方法。你将使用一个标志来控制程序输出,引入位置参数,在这里你将混合标志和其他数据,然后实现子命令。
8 |
9 | ## 用 Flag 来改变程序的行为
10 |
11 | 使用 flag 包包括三个步骤:首先,定义变量以捕获标志值,然后定义你的 Go 应用程序将使用的标志,最后解析执行时提供给应用程序的标志。`flag`包内的大多数函数都与定义标志和将它们与你定义的变量绑定有关。解析阶段由`Parse()`函数处理。
12 |
13 | 为了阐述这一点,你将创建一个程序,定义一个 [Boolean]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})标志,改变这个标志将会把信息打印到标准输出上。如果提供一个`-color`标志,程序会用蓝色来打印消息。如果没有这个标志,则打印消息不会有颜色。
14 |
15 | 创建一个叫`boolean.go`的文件:
16 |
17 | ```bash
18 | nano boolean.go
19 | ```
20 |
21 | 添加如下代码到文件里面来创建程序:
22 |
23 | ```go
24 | package main
25 |
26 | import (
27 | "flag"
28 | "fmt"
29 | )
30 |
31 | type Color string
32 |
33 | const (
34 | ColorBlack Color = "\u001b[30m"
35 | ColorRed = "\u001b[31m"
36 | ColorGreen = "\u001b[32m"
37 | ColorYellow = "\u001b[33m"
38 | ColorBlue = "\u001b[34m"
39 | ColorReset = "\u001b[0m"
40 | )
41 |
42 | func colorize(color Color, message string) {
43 | fmt.Println(string(color), message, string(ColorReset))
44 | }
45 |
46 | func main() {
47 | useColor := flag.Bool("color", false, "display colorized output")
48 | flag.Parse()
49 |
50 | if *useColor {
51 | colorize(ColorBlue, "Hello, DigitalOcean!")
52 | return
53 | }
54 | fmt.Println("Hello, DigitalOcean!")
55 | }
56 | ```
57 |
58 | 这个例子使用[ANSI 逃逸序列](https://en.wikipedia.org/wiki/ANSI_escape_code)来指示终端显示彩色输出。这些是专门的 character 序列,所以为它们定义一个新的类型是有意义的(L8)。在这个例子中,我们称该类型为`color`,并将该类型定义为`string`。然后我们定义了一个调色板,在后面的 `const` 块中使用。定义在`const`块之后的`colorize`函数接受`Color`常量其中之一和一个`string`,用于对信息进行着色。然后它指示终端改变颜色,首先打印所要求的颜色的转义序列,然后打印信息,最后要求终端通过打印特殊的颜色重置序列来重置其颜色。
59 |
60 | 在`main`中,我们使用`flag.Bool`函数来定义一个名为`color`的 Boolean 标志。这个函数的第二个参数,`false`,在没有提供这个标志的情况下,设置这个标志的默认值。与你可能有的期望相反,将其设置为`true`并不会颠倒行为,如提供一个标志会导致它变成 false。因此,这个参数的值在布尔标志下几乎总是`false`。
61 |
62 | 最后一个参数是一个可以作为使用信息打印出来的文档 string。从这个函数返回的值是一个指向`bool`的指针。下一行的`flag.Parse`函数使用这个指针,然后根据用户传入的标志,设置`bool`变量。 然后我们就可以通过取消引用这个指针来检查这个`bool`指针的值。更多关于指针变量的信息可以在[指针教程]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})找到。使用这个 Boolean,我们就可以在设置`-color`标志时调用`colorize`,而在没有这个标志时调用`fmt.Println`变量。
63 |
64 | 保存文件,并在未传入没有任何标志的情况下运行该程序:
65 |
66 | ```bash
67 | go run boolean.go
68 | ```
69 |
70 | 你将会看到如下输出:
71 |
72 | ```text
73 | Output
74 | Hello, DigitalOcean!
75 | ```
76 |
77 | 现在带上`-color`标志再跑一遍程序:
78 |
79 | ```bash
80 | go run boolean.go -color
81 | ```
82 |
83 | 输出文本会是一样的,只不过这时候颜色时蓝色的。
84 |
85 | 标志不是传递给命令的唯一参数。你也能发送文件名或其他数据。
86 |
87 | ## 使用位置参数
88 |
89 | 通常情况下,命令会接受一些参数,这些参数作为命令的重点对象。例如,打印文件第一行的`head`命令经常被以`head example.txt`调用。文件`example.txt`是调用`head`命令时的一个位置参数。
90 |
91 | `Parse()`函数将一直解析它所遇到的标志,直到它检测到一个非标志参数。`flag`包通过`Args()`和`Arg()`函数使这些参数可用。
92 |
93 | 为了阐述这一点,你将重新实现一个简化的`head`命令,它显示一个给定文件的前几行:
94 |
95 | 创建一个新的文件称为`head.go`,然后添加如下代码:
96 |
97 | ```go
98 | package main
99 |
100 | import (
101 | "bufio"
102 | "flag"
103 | "fmt"
104 | "io"
105 | "os"
106 | )
107 |
108 | func main() {
109 | var count int
110 | flag.IntVar(&count, "n", 5, "number of lines to read from the file")
111 | flag.Parse()
112 |
113 | var in io.Reader
114 | if filename := flag.Arg(0); filename != "" {
115 | f, err := os.Open(filename)
116 | if err != nil {
117 | fmt.Println("error opening file: err:", err)
118 | os.Exit(1)
119 | }
120 | defer f.Close()
121 |
122 | in = f
123 | } else {
124 | in = os.Stdin
125 | }
126 |
127 | buf := bufio.NewScanner(in)
128 |
129 | for i := 0; i < count; i++ {
130 | if !buf.Scan() {
131 | break
132 | }
133 | fmt.Println(buf.Text())
134 | }
135 |
136 | if err := buf.Err(); err != nil {
137 | fmt.Fprintln(os.Stderr, "error reading: err:", err)
138 | }
139 | }
140 | ```
141 |
142 | 首先,我们定义了一个`count`变量,用来保存程序应该从文件中读取的行数。然后,我们使用`flag.IntVar`定义`-n`标志,模拟原始`head`程序的行为。 这个函数允许我们将自己的[pointer]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})传递给一个变量,与没有`Var`后缀的标志函数相反。除了这个区别之外,`flag.IntVar`的其他参数与`flag.Int`对应的参数相同:标志名称、默认值和描述。 和前面的例子一样,我们随后调用`flag.Parse()`来处理用户的输入。
143 |
144 | 下一节读取文件。我们首先定义一个`io.Reader`变量,该变量将被设置为用户请求的文件,或传递给程序的标准输入。在`if`语句中,我们使用`flag.Arg`函数来访问所有标志之后的第一个位置参数。如果用户提供了文件名,这个位置参数会被设置。否则,它将为空 string(`""`)。当文件名提供时,我们使用`os.Open`函数来打开该文件,并将我们之前定义的`io.Reader`设置为该文件。否则,我们使用`os.stdin`来读取标准输入。
145 |
146 | 最后一节使用一个用`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`,表示行数少于用户要求的数量。
147 |
148 | 运行这个程序,用`head.go`作为文件参数,显示你刚才写的文件的内容:
149 |
150 | ```bash
151 | go run head.go -- head.go
152 | ```
153 |
154 | `--`分隔符是一个被`flag`包识别的特殊标志,它表示后面没有更多的 flag 参数。当你运行这个命令时,你会收到以下输出:
155 |
156 | ```text
157 | Output
158 | package main
159 |
160 | import (
161 | "bufio"
162 | "flag"
163 | ```
164 |
165 | 使用你定义的`-n`标志来调整输出的数量:
166 |
167 | ```bash
168 | go run head.go -n 1 head.go
169 | ```
170 |
171 | 这只输出包的声明:
172 |
173 | ```text
174 | Output
175 | package main
176 | ```
177 |
178 | 最后,当程序检测到没有提供位置参数时,它从标准输入中读取输入,就像`head`一样。试着运行这个命令:
179 |
180 | ```bash
181 | echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
182 | ```
183 |
184 | 你将会看到如下输出:
185 |
186 | ```text
187 | Output
188 | fish
189 | lobsters
190 | sharks
191 | ```
192 |
193 | 到目前为止,你所看到的`flag`函数的行为仅限于检查整个命令的调用。你并不总是想要这种行为,特别是当你在编写一个支持子命令的命令行工具时。
194 |
195 | ## 用 FlagSet 来实现子命令
196 |
197 | 现代的命令行应用程序经常实现 "子命令",将一套工具捆绑在一个命令之下。使用这种模式的最著名的工具是`git`。 当检查像`git init`这样的命令时,`git`是命令,`init`是 git 的子命令。子命令的一个显著特点是,每个子命令可以有自己的标志集合。
198 |
199 | Go 应用程序可以使用`flag.(*FlagSet)`类型支持具有自己的标志集的子命令。为了阐述这一点,创建一个程序,使用两个具有不同标志的子命令来实现一个命令。
200 |
201 | 创建一个名为`subcommand.go`的新文件,并在该文件中添加以下内容:
202 |
203 | ```go
204 | package main
205 |
206 | import (
207 | "errors"
208 | "flag"
209 | "fmt"
210 | "os"
211 | )
212 |
213 | func NewGreetCommand() *GreetCommand {
214 | gc := &GreetCommand{
215 | fs: flag.NewFlagSet("greet", flag.ContinueOnError),
216 | }
217 |
218 | gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")
219 |
220 | return gc
221 | }
222 |
223 | type GreetCommand struct {
224 | fs *flag.FlagSet
225 |
226 | name string
227 | }
228 |
229 | func (g *GreetCommand) Name() string {
230 | return g.fs.Name()
231 | }
232 |
233 | func (g *GreetCommand) Init(args []string) error {
234 | return g.fs.Parse(args)
235 | }
236 |
237 | func (g *GreetCommand) Run() error {
238 | fmt.Println("Hello", g.name, "!")
239 | return nil
240 | }
241 |
242 | type Runner interface {
243 | Init([]string) error
244 | Run() error
245 | Name() string
246 | }
247 |
248 | func root(args []string) error {
249 | if len(args) < 1 {
250 | return errors.New("You must pass a sub-command")
251 | }
252 |
253 | cmds := []Runner{
254 | NewGreetCommand(),
255 | }
256 |
257 | subcommand := os.Args[1]
258 |
259 | for _, cmd := range cmds {
260 | if cmd.Name() == subcommand {
261 | cmd.Init(os.Args[2:])
262 | return cmd.Run()
263 | }
264 | }
265 |
266 | return fmt.Errorf("Unknown subcommand: %s", subcommand)
267 | }
268 |
269 | func main() {
270 | if err := root(os.Args[1:]); err != nil {
271 | fmt.Println(err)
272 | os.Exit(1)
273 | }
274 | }
275 | ```
276 |
277 | 这个程序分为几个部分:`main`函数,`root`函数,以及实现子命令的各个函数。`main`函数处理从命令返回的错误。如果任何函数返回[错误]({{< relref "/docs/17-Handling_Errors_in_Go_DigitalOcean.md" >}}),`if`语句将捕捉到它,打印出错误,程序将以`1`的状态码退出,向操作系统的其他部分表明发生了错误。在`main`中,我们将程序被调用的所有参数传递给`root`。我们通过先将`os.Args`切片来删除第一个参数,也就是程序的名称(在前面的例子中是`./subcommand`)。
278 |
279 | `root`函数定义了`[]Runner`,所有的子命令都会在这里定义。`Runner`是一个子命令的 [interface]({{< relref "/docs/37-How_To_Use_Interfaces_in_Go.md" >}}) ,允许`root`使用`Name()`获取子命令的名称,并将其与变量`subcommand`内容进行比较。一旦在遍历`cmds`变量后找到了正确的子命令,我们就用其余的参数初始化子命令,并调用该命令的`Run()`方法。
280 |
281 | 我们只定义了一个子命令,尽管这个框架很容易让我们创建其他子命令。`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`方法。
282 |
283 | 如果你构建这个程序,然后运行它,就会更容易看到子命令。建立该程序:
284 |
285 | ```bash
286 | go build subcommand.go
287 | ```
288 |
289 | 现在运行该程序,没有参数:
290 |
291 | ```bash
292 | ./subcommand
293 | ```
294 |
295 | 你会看到如下输出:
296 |
297 | ```text
298 | Output
299 | You must pass a sub-command
300 | ```
301 |
302 | 现在用`greet`子命令运行该程序。
303 |
304 | ```bash
305 | ./subcommand greet
306 | ```
307 |
308 | 这会输出如下内容:
309 |
310 | ```text
311 | Output
312 | Hello World !
313 | ```
314 |
315 | 现在使用`-name`标志和`greet`来指定一个名字:
316 |
317 | ```bash
318 | ./subcommand greet -name Sammy
319 | ```
320 |
321 | 你会看到程序给出的这个输出:
322 |
323 | ```text
324 | Output
325 | Hello Sammy !
326 | ```
327 |
328 | 这个例子说明了在 Go 中如何构建大型命令行应用程序的一些原则。 `FlagSets`的设计是为了给开发者提供更多的控制权,使其能够通过 flag 解析逻辑,分析`flag`的位置和处理方式。
329 |
330 | ## 总结
331 |
332 | 标记使你的应用程序在更多情景下更有用,因为它们让你的用户控制程序的执行方式。给用户提供有用的默认值很重要,但你应该让他们有机会覆盖那些不适合他们情况的设置。你已经看到,`flag`包提供了灵活的选择,向你的用户展示配置选项。你可以选择一些简单的标志,或者建立一套可扩展的子命令。 无论是哪种情况,在过去长久历史沉淀的风格下,使用`flag`包都可以帮助你按照灵活的、可编写脚本的命令行工具。
333 |
334 |
--------------------------------------------------------------------------------
/content/zh/docs/17-Handling_Errors_in_Go_DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中处理错误
2 |
3 | 健壮的代码需要对用户的不正确输入、网络连接错误和磁盘错误等意外情况做出正确的反应。错误处理是识别程序处于异常状态并且采取措施去记录供后期调试诊断信息的过程。
4 |
5 | 相比于其他编程语言, 要求开发者使用专门的语法去处理错误, 在 Go 中将错误作为 `error`(Go 中的一个接口类型) 类型的值, 并且和其他类型的值一样作为函数返回值的一部分返回。要处理 Go 中的错误, 我们必须检查函数返回值中是否包含了错误信息, 并采取合适的措施去保护数据并告知用户或者操作人员发生错误。
6 |
7 | ## 创建错误
8 |
9 | 在处理错误之前,我们需要先创建一些错误。标准库提供了两个内置函数来创建错误:`errors.New` 和 `fmt.Errorf`。这两个函数都允许您指定一条自定义错误消息,这些信息可以向用户展示具体错误信息的一部分。
10 |
11 | `errors.New` 只提供了一个字符串类型的参数, 用户在使用的时候可以自定义一个错误发生时具体需要展示的错误消息.
12 |
13 | 尝试运行以下示例以查看由 `errors.New` 创建的错误并打印到标准输出:
14 |
15 | ```go
16 | package main
17 |
18 | import (
19 | "errors"
20 | "fmt"
21 | )
22 |
23 | func main() {
24 | // 使用 errors.New() 创建一个错误, 具体的错误消息是: barnacles
25 | err := errors.New("barnacles")
26 |
27 | // 将错误直接打印到标准错误输出
28 | fmt.Println("Sammy says:", err)
29 | }
30 |
31 | ```
32 |
33 | ```shell
34 | # 这里是控制台的输出
35 | # Output
36 | Sammy says: barnacles
37 | ```
38 |
39 | 我们使用标准库的 `errors.New` 函数创建了具体的消息是 `"barnacles"` 的错误。这里我们遵循了 [Go 程序设计风格指南](https://github.com/golang/go/wiki/CodeReviewComments#error-strings) 使用小写了表示错误消息。
40 |
41 | 最后,我们使用 `fmt.Println` 函数将我们的错误消息与`"Sammy says:"`相结合并且输出到控制台。
42 |
43 | `fmt.Errorf` 函数允许用户构建动态的错误消息。它的第一个参数是一个字符串,包含包含占位符值的错误消息,例如字符串的 `%s` 和整数的`%d`。`fmt.Errorf` 将这个格式化字符串后面的参数按顺序插入到这些占位符中:
44 |
45 | ```go
46 | package main
47 |
48 | import (
49 | "fmt"
50 | "time"
51 | )
52 |
53 | func main() {
54 | // 使用 fmt.Errorf() 来构建动态错误信息
55 | // 错误内容是 error occurred at: %v
56 | // 其中 %v 的具体内容由 time.Now() 的具体返回值决定
57 | err := fmt.Errorf("error occurred at: %v", time.Now())
58 |
59 | // 将具体的错误信息 结合 `An error happened:` 打印到控制台
60 | fmt.Println("An error happened:", err)
61 | }
62 |
63 | ```
64 |
65 | ```shell
66 | # 在控制台中输出错误信息
67 | # Output
68 | # 输出内容中的: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103 是由 `time.Now()` 动态生成的
69 | An error happened: error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103
70 | ```
71 |
72 | 我们使用 `fmt.Errorf` 函数来构建一个错误消息,该消息将包括当前时间。我们提供给 `fmt.Errorf` 的格式字符串包含 `%v` 格式指令,该指令告诉 `fmt.Errorf` 使用默认格式为格式化字符串后提供的第一个参数。这个参数由标准库的 `time.Now` 函数提供的当前时间。与较早的示例类似,我们将错误消息与简短前缀结合在一起,并使用 `fmt.Println` 函数将结果打印到标准输出。
73 |
74 | ## 错误处理
75 |
76 | 一般来说, 你不会看到像上面一样直接创建错误, 然后直接打印。实际上, 在出现问题时, 错误都是由从函数中创建并且返回这种情况更加普遍。调用者使用 `if` 语句判断返回的错误是否为 `nil`(error 非初始化的值) 来判断错误是否存在。
77 |
78 | 下面这个示例包含了一个总是返回错误的函数, 需要特别留意的时尽管这里的错误是由一个函数返回的, 当你在运行这个程序时, 它产生的输出总是与前面的示例相同。在其他位置声明错误不会改变错误的消息。
79 |
80 | ``` go
81 | package main
82 |
83 | import (
84 | "errors"
85 | "fmt"
86 | )
87 |
88 | // 定义一个名为: boom 的函数, 返回值总是 errors.New("barnacles")
89 | func boom() error {
90 | return errors.New("barnacles")
91 | }
92 |
93 | func main() {
94 | // 调用 boom() 函数, 并将返回值赋值给 err 变量
95 | err := boom()
96 |
97 | // 判断 err 是否等于 nil
98 | if err != nil {
99 | // 如果 err != nil 条件成立, 输出内容, 然后返回 main.main 函数
100 | fmt.Println("An error occurred:", err)
101 | return
102 | }
103 |
104 | // 如果 err == nil 成立,
105 | // 将会输出下面这一句
106 | fmt.Println("Anchors away!")
107 | }
108 |
109 | ```
110 |
111 | ```shell
112 | # Output
113 | An error occurred: barnacles
114 | ```
115 |
116 | 这里我们先定义了一个名为 `boom()` 的函数并且总是返回单个使用 `errors.New` 构造 `error` 的函数。然后, 我们通过 `err := boom()` 这行调用 `boom()` 并捕捉错误(赋值给 err 变量即为捕捉错误)。在赋值 error 之后, 我们使用 `if err != nil` 这个条件判断语句来进行判断错误是否存在。因为 `boom()` 函数总是返回有效的 `error` 所以这里的判断条件永远为 `true`。
117 |
118 | 但是情况并非总是如此(值的是 `boom()` 函数总是返回有效的 `error` 变量), 所以, 最好有逻辑去处理错误不存在和错误存在这两种情况。当错误存在时, 就像上面的示例中一样, 我们使用 `fmt.Println` 和前面的前缀打印错误。最后我们使用 `return` 语句来跳过 `fmt.Println("Anchors away!")` 语句的执行, 因为这个语句只有在 `err == nil` 时才会执行。
119 |
120 | > **注意:** 在 Go 中主要采用上一个示例中的 `if err != nil` 来进行错误处理。函数运行到哪里都有可能发生错误, 重要的是使用 `if` 语句来判断错误是否发生。这样, Go 代码通常就具有第一个缩进级别的 [快乐路径](https://en.wikipedia.org/wiki/Happy_path) 的逻辑, 并且所有的 "悲伤的路径" 在第二个缩进。
121 |
122 | `if` 语句有一个可选的赋值子句,可以用来帮助压缩函数调用和错误处理。
123 |
124 | 运行下一个程序,查看与前面示例相同的输出,但这一次使用复合 `if` 语句来减少一些重复的代码:
125 |
126 | ``` go
127 | package main
128 |
129 | import (
130 | "errors"
131 | "fmt"
132 | )
133 |
134 | func boom() error {
135 | return errors.New("barnacles")
136 | }
137 |
138 | func main() {
139 | // 将 err 变量的赋值和判断都压缩在一个语句块中执行
140 | if err := boom(); err != nil {
141 | fmt.Println("An error occurred:", err)
142 | return
143 | }
144 | fmt.Println("Anchors away!")
145 | }
146 | ```
147 |
148 | ```shell
149 | #Output
150 | An error occurred: barnacles
151 | ```
152 |
153 | 和之前的示例一样, 我们定义一个 `boom()` 总是返回错误的函数。我们将从 `boom()` 返回的错误赋值给 `err` 作为 `if` 语句的一部分。在 `if` 语句的第二部分语句中, `err` 变量变得可用。我们检查错误是否存在, 然后像以前一样使用一个简短的前缀字符串打印我们的错误。
154 |
155 | 在本节中,我们学习了如何处理只返回错误的函数。这些函数很常见,但是能够处理可能返回多个值的函数的错误也很重要。
156 |
157 | ## 同时返回错误和多个值
158 |
159 | 返回单个值的函数通常是影响某些状态更改的函数。 比如将行数据插入到数据库中。通常还会编写这样的函数: 如果成功则返回一个值, 如果失败则返回一个潜在的错误。Go 允许函数返回多个结果, 可以用来同时返回一个值和一个错误类型。
160 |
161 | 为了创建一个返回多个值的函数, 我们需要在函数签名的括号中列出返回值类型。例如, 一个 `capitalize` 函数返回值类型是 `string` 和 `error`, 那么我们可以这么声明 `func capitalize(name string)(string, error){}`。其中 `(string, error)` 这一块的语法是告诉 Go 的编译器, 函数会按照 `string` 和 `error` 这一顺序返回值。
162 |
163 | 运行下面的程序并且查看函数返回的 `string` 和 `error`:
164 |
165 | ``` go
166 | package main
167 |
168 | import (
169 | "errors"
170 | "fmt"
171 | "strings"
172 | )
173 |
174 | func capitalize(name string) (string, error) {
175 | if name == "" {
176 | return "", errors.New("no name provided")
177 | }
178 | return strings.ToTitle(name), nil
179 | }
180 |
181 | func main() {
182 | name, err := capitalize("sammy")
183 | if err != nil {
184 | fmt.Println("Could not capitalize:", err)
185 | return
186 | }
187 |
188 | fmt.Println("Capitalized name:", name)
189 | }
190 |
191 | ```
192 |
193 | ```shell
194 | # Output
195 | Capitalized name: SAMMY
196 | ```
197 |
198 | 我们定义了 `capitalize()` 函数, 这个函数需要传递一个字符串作为参数(完成将字符串的转为大写)并返回字符串和错误。在 `main()` 函数中, 我们调用 `capitalize()`, 然后在 `:=` 运算符的左边将函数的返回值赋值给 `name` 和 `err` 这两个变量。之后, 我们执行 `if err != nil` 检查错误, 如果存在错误, 使用 `fmt.Prtintln` 将错误信息打印到标准输出。如果没有错误, 输出 `Capitalized name: SAMMY`。
199 |
200 | 如果将 `err := capitalize("sammy")` 中的 `"sammy"` 更改为为空字符串 `("")`,你将收到 `Could not capitalize: no name provided` 这个错误。
201 |
202 | 当函数的调用者为 `name` 参数提供一个空字符串时, `capitalize` 函数将返回错误。当 `name` 参数不是空字符串时,`capledize()` 调用 `strings.ToTitle` 函数将 `name` 参数转为大写并返回为 `nil` 的错误值。
203 |
204 | 这个例子遵循一些微妙的规约,这些规约是 Go 代码的典型特征,但 GO 编译器并没有强制执行。当函数返回多个值(包括错误)时,规约我们将 `error` 类型作为最后一项。具有多个返回值的函数返回错误时,通常约定 GO 代码还将每个不是 `error` 类型的值设置为零值。比如字符串的零值空字符串,整数为 0,一个用于结构类型的空结构,以及用 `nil` 表示接口和指针类型的零值。我们在有关 [变量和常数的教程](https://gocn.github.io/How-To-Code-in-Go/docs/11-How_To_Use_Variables_and_Constants_in_Go/#%E9%9B%B6%E5%80%BC) 中更详细地介绍零值。
205 |
206 | ### 简化重复的代码
207 |
208 | 如果函数有多个返回值时,遵守这些约定可能会变得啰嗦。我们可以使用 [匿名函数](https://en.wikipedia.org/wiki/Anonymous_function) 来帮助减少重复的代码。匿名函数是分配变量的过程。与我们在较早的示例中定义的函数相反,它们仅在你声明它们的函数中可用 - 这使其非常适合用作可重复使用的 `helper` 逻辑代码片段。
209 |
210 | 以下程序是修改了最后一个示例,返回值增加了一个类型, 包括大写的名称的长度。由于它具有三个值可以返回的值,因此如果没有匿名函数来帮助我们,处理错误可能会变得麻烦:
211 |
212 | ``` go
213 | package main
214 |
215 | import (
216 | "errors"
217 | "fmt"
218 | "strings"
219 | )
220 |
221 | func capitalize(name string) (string, int, error) {
222 | handle := func(err error) (string, int, error) {
223 | return "", 0, err
224 | }
225 |
226 | if name == "" {
227 | return handle(errors.New("no name provided"))
228 | }
229 |
230 | return strings.ToTitle(name), len(name), nil
231 | }
232 |
233 | func main() {
234 | name, size, err := capitalize("sammy")
235 | if err != nil {
236 | fmt.Println("An error occurred:", err)
237 | }
238 |
239 | fmt.Printf("Capitalized name: %s, length: %d", name, size)
240 | }
241 |
242 | ```
243 |
244 | ```shell
245 | # Output
246 | Capitalized name: SAMMY, length: 5
247 | ```
248 |
249 | 在 `main()` 中,我们现在可以从 `capitalize()` 函数中获取转为大写的 `name`,`size` 和 `err` 这三个返回的参数。然后,我们检查是否通过检查错误变量不等于 `nil`。在尝试使用 `capitalize()` 返回的任何其他值之前,这一点很重要,因为匿名函数可以将它们设置为零值。由于我们提供了字符串 `"Sammy"`,因此没有发生错误,因此我们打印出转为大写之后的名称及其长度。
250 |
251 | 再次,你可以尝试将 `"Sammy"` 更改为空字符串 `("")` 以查看已打印的错误情况 (`An error occurred: no name provided`)。
252 |
253 | 在 `capitalize` 函数中,我们将 `handle` 变量定义为匿名函数。它需要传递要给错误类型的参数,并以与 `capitalize` 函数的返回值相同的顺序返回相同的值。`handle` 将这些值设置为零值,并将其作为最终返回值作为参数传递的错误转发。然后,使用 `err` 作为 `handle` 的参数,就可以返回在 `capitalize` 中遇到的任何错误。
254 |
255 | 请记住,`capitalize` 必须一直返回三个值,因为这就是我们定义函数的方式。有时我们不想处理函数可能返回的所有值。幸运的是,我们在赋值并如何使用这些值方面具有一定的灵活性。
256 |
257 | ### 处理多回报功能的错误
258 |
259 | 当函数返回许多值时,Go 要求我们将每个值分配给变量。在最后一个示例中,我们通过提供从 `capitalize` 函数返回的两个值的名称来做到这一点。这些名称应通过逗号分隔,并出现在 `:=` 操作符的左侧。从 `capitalize` 返回的第一个值将分配给 `name` 变量,第二个值(`error`)将分配给 `err` 这个变量。有时,我们只对错误值感兴趣。您可以丢弃使用特殊 `_` 变量名称返回功能的任何不需要值。
260 |
261 | 在以下程序中,我们修改了涉及大写功能的第一个示例,以通过传递空字符串 `("")` 来产生错误。尝试运行此程序,以查看我们如何通过使用 `_` 变量丢弃第一个返回的值来检查错误:
262 |
263 | ``` go
264 | package main
265 |
266 | import (
267 | "errors"
268 | "fmt"
269 | "strings"
270 | )
271 |
272 | func capitalize(name string) (string, error) {
273 | if name == "" {
274 | return "", errors.New("no name provided")
275 | }
276 | return strings.ToTitle(name), nil
277 | }
278 |
279 | func main() {
280 | _, err := capitalize("")
281 | if err != nil {
282 | fmt.Println("Could not capitalize:", err)
283 | return
284 | }
285 | fmt.Println("Success!")
286 | }
287 |
288 | ```
289 |
290 | ```shell
291 | # Output
292 | Could not capitalize: no name provided
293 | ```
294 |
295 | 这次在 `main()` 函数中,我们将 `capitalize` 的第一个返回值 (首先返回的字符串) 分配给下划线变量(`_`)。同时,我们分配了通过 `capitalize` 返回的 `err` 变量返回的错误。然后,我们通过 `if err != nil` 条件判断错误是否存在。由于我们已经对一个空字符串进行了硬编码,作为在行中大写的参数,`_, err := capitalize("")`,因此该条件始终将评估为 `true`。这会产生输出`"Could not capitalize: no name provided"`,该输出由 `fmt.Println` 函数在 `if` 语句的正文中打印出来。此后的返回将跳过 `fmt.Println("Success!")`。
296 |
297 | ## 结论
298 |
299 | 我们已经看到了许多使用标准库创建错误的方法,以及如何构建以惯用方式返回错误的函数。在本教程中,我们设法使用标准库的 `errors.New` 和 `fmt.Errorf` 函数成功地创建了各种错误。在将来的教程中,我们将研究如何创建自己的自定义错误类型,以向用户传达更丰富的信息。
300 |
--------------------------------------------------------------------------------
/content/zh/docs/19-Handling_Panics_in_Go _DigitalOcean.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中处理恐慌
2 |
3 | ## 介绍
4 |
5 | 程序遇到的错误分为两个广泛的类别:程序员已经预料到的错误和程序员没有预料到的错误。我们在前两篇关于 [错误处理]({{< relref "/docs/12-How_To_Convert_Data_Types_in_Go.md" >}}) 的文章中介绍过的 `error` 接口主要用于处理我们在编写 Go 程序时可能遇到的错误。`error` 接口甚至允许我们去确认在调用一个函数时发生罕见性错误的可能性,因此我们可以在这些情况下进行适当的响应。
6 |
7 | Panics 属于第二类错误,这些错误是程序员意料之外的。这些意料之外的错误导致一个 GO 程序自发终止并退出运行。常见的错误通常是造成 panic 的原因。在本教程中,我们将研究哪些常见操作可以引起 panic ,我们还将看到避免这些 panic 的方法。我们还将使用 [`defer`]({{< relref "/docs/29-Understanding_defer_in_Go.md" >}}) 语句与 `recover` 函数一起捕获 panic,以免 panic 有机会意外终止我们正在运行的 GO 程序。
8 |
9 | ## 了解 panics
10 |
11 | GO 中的某些操作会自动返回 panic 并停止程序的运行。常见的操作包括索引超出 [数组](https://gocn.github.io/How-To-Code-in-Go/docs/16-Understanding_Arrays_and_Slices_in_Go/#%E6%95%B0%E7%BB%84) 的容量,执行类型的断言,空指针上的调用方法,错误地使用互斥锁以及尝试使用已经关闭的 chanel 等等。这些情况中的大多数是由于编程时犯错而导致的,再加上编译器在编译程序时没有检测到这些错误。
12 |
13 | 由于 panic 包含了有助于解决问题的细节,所以开发者通常会使用 panic 来标记在开发过程中犯了一个错误。
14 |
15 | ## 由于越界引发的 panic
16 |
17 | 当你尝试访问超出切片长度或数组容量之外的索引时,GO 运行时会产生 panic。
18 |
19 | 下面的示例是尝试使用内置的 `len` 函数返回的切片的长度, 然后访问切片的最后一个元素时常见错误。尝试运行此代码以了解为什么这可能会引起 panic:
20 |
21 | ``` go
22 | package main
23 |
24 | import (
25 | "fmt"
26 | )
27 |
28 | func main() {
29 | names := []string{
30 | "lobster",
31 | "sea urchin",
32 | "sea cucumber",
33 | }
34 | fmt.Println("My favorite sea creature is:", names[len(names)])
35 | }
36 |
37 | ```
38 |
39 | 这将会有有以下输出:
40 |
41 | ``` shell
42 | # Output
43 | panic: runtime error: index out of range [3] with length 3
44 |
45 | goroutine 1 [running]:
46 | main.main()
47 | # 备注这一块信息可能会有不一样的输出
48 | /tmp/sandbox879828148/prog.go:13 +0x20
49 | ```
50 |
51 | panic 输出的名称提供了一个提示:`panic: runtime error: index out of range`。我们用三个海洋生物创建了一个切片。然后,我们尝试通过使用内置的 `len` 函数将切片的长度作为索引来获取切片的最后一个元素。请记住,切片和数组的第一个元素的下标都是 0; 因此,第一个元素的索引是 0,此切片中的最后一个元素在索引2。由于我们尝试在第三个索引,3 时,因此切片中没有元素要返回并且超出了切片的边界。运行时别无选择,只能终止和退出,因为我们要求它做一些不可能的事情。Go 在编译过程中也无法证明此代码将尝试执行此操作,因此编译器无法捕获到这种操作。
52 |
53 | > 还请注意,后续代码还没被执行。这是因为 panic 是一个完全阻止执行你的 GO 程序的事件。其中产生的消息中包含多个有助于诊断 panic 的原因。
54 |
55 | ## 剖析 panic
56 |
57 | panics 由指示 panic 的原因和一个 [堆栈跟踪](https://en.wikipedia.org/wiki/Stack_trace) 信息组成,这些可帮助你在代码中找到 panic 的位置。
58 |
59 | 任何 panic 的第一部分都是消息。它始终将以字符串 `panic:` 开始, 紧接着是引发 panic 的具体原因的字符串。在上一个练习中有一个 panic 的消息:
60 |
61 | ``` shell
62 | panic: runtime error: index out of range [3] with length 3
63 | ```
64 |
65 | 紧接着 `panic:` 的是 `runtime error:` 这告诉我们这个 panic 是由语言的运行时引起的。这个 panic 告诉我们, 我们尝试使用下标 `[3]`已经超出了切片的长度 `3` 了。
66 |
67 | 消息后面的是堆栈跟踪。堆栈跟踪形成一个映射,我们可以根据映射信息,以准确地定位生成 panic 时正在执行的代码所在的行,和代码的调用链关系。
68 |
69 | ``` shell
70 | goroutine 1 [running]:
71 | main.main()
72 | /tmp/sandbox879828148/prog.go:13 +0x20
73 | ```
74 |
75 | 上一个示例的堆栈跟踪表示,我们的程序从 `/tmp/sandbox879828148/prog.go` 文件的第 13 行中生成了 panic。这些信息还告诉我们 panic 在 `main` 包中的 `main()` 函数产生。
76 |
77 | 堆栈跟踪分为单独的块 - 对于你程序中的每个 [goroutine](https://tour.golang.org/concurrency/1)一个块。每个 GO 程序的执行都是通过一个或多个 goroutines 来完成的,它们可以独立并同时执行 GO 代码的一部分。每个块从标头 `goroutine x [state]:` (其中 x: 表示 goroutine 的 id, [state] 表示 goroutine 当前的状态)开头。标头给出了 goroutine 的 ID 号,以及发生 panic 时所处的状态。标头后,堆栈跟踪显示了发生 panic 时程序执行的函数,以及执行函数所在的文件名和行号。
78 |
79 | 上一个示例中的 panic 是通过对切片的越界访问而产生的。当使用空指针去调用方法时,也可以生成 panic。
80 |
81 | ## Nil Receivers
82 |
83 | ## nil 指针调用方法
84 |
85 | GO 编程语言在运行时具有指向计算机内存中存在的某种类型的特定实例的指针。指针可以是 `nil` 值, 这表明他们没有指向任何东西。当我们尝试在零指针上调用方法时,GO 运行时会产生 panic。同样,当调用方法时,是接口类型的变量也会产生 panic。要查看这些情况下产生的 panic,请尝试以下示例:
86 |
87 | ``` go
88 | package main
89 |
90 | import (
91 | "fmt"
92 | )
93 |
94 | type Shark struct {
95 | Name string
96 | }
97 |
98 | func (s *Shark) SayHello() {
99 | fmt.Println("Hi! My name is", s.Name)
100 | }
101 |
102 | func main() {
103 | s := &Shark{"Sammy"}
104 | s = nil
105 | s.SayHello()
106 | }
107 |
108 | ```
109 |
110 | The panics produced will look like this:
111 |
112 | 由此产生的 panic 将是这样的:
113 |
114 | ``` shell
115 | # Output
116 | panic: runtime error: invalid memory address or nil pointer dereference
117 | [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba]
118 |
119 | goroutine 1 [running]:
120 | main.(*Shark).SayHello(...)
121 | /tmp/sandbox160713813/prog.go:12
122 | main.main()
123 | /tmp/sandbox160713813/prog.go:18 +0x1a
124 | ```
125 |
126 | 在此示例中,我们定义了一个称为 `Shark` 的结构体。`Shark` 在其指针接收器上定义了一个叫做 `Sayhello` 的方法,这个方法将在被调用时在标准输出中打印出问候信息。在我们的 `main` 函数主体中,我们创建了 `Shark` 结构体的新实例,并使用 `&` 操作符取变量的指针并将指针分配给 `S` 变量。然后,我们使用语句 `s = nil` 将 `s` 变量重新赋值为 `nil`。最后,我们尝试在变量 `s` 上调用 `SayHello` 方法。我们没有收到 `Sammy` 的友好消息,而是收到 panic,因为我们试图访问无效的内存地址。因为 `s` 变量为 `nil`,所以当调用 `SayHello` 函数时,它试图访问 `*Shark` 类型上的 `Name` 字段。因为这是一个指针接收者,并且在这种情况下的接收者是 `nil` 的,所以无法解引用零值指针而引起的 panic。
127 |
128 | 虽然我们在本例中显式地将 `s` 设置为`nil`,但实际上,这种情况却不明显。当你看到有关解引用 `nil` 指针而引发的 panic 时,请确保你已正确分配了你可能创建的任何指针变量。
129 |
130 | > 备注, 通过使用指针作为接收者时, 使用零值取调用时没有不会发生 panic 的, 真正发生 panic 的时, 解引用 `nil` 指针。
131 |
132 | ``` go
133 | // 这种定义时, 使用零值的 `* Shark` 对象去调用 SayHello 方法是没有问题的
134 | func (s *Shark) SayHello() {
135 | if s == nil {
136 | return
137 | }
138 | fmt.Println("Hi! My name is", s.Name)
139 | }
140 | ```
141 |
142 | 解引用 `nil` 指针和越界访问产生的 panic 是两种在运行时产生的 panic 常见的场景。也可以使用内置函数手动产生 panic。
143 |
144 | ## 使用内置的 `panic` 函数
145 |
146 | 我们还可以使用内置的 `panic` 函数来产生自己的 panic。它使用单个字符串作为参数,这是 panic 产生的信息。一般这条消息比重写 error 代码中的消息简单得多。此外,我们可以在我们自己的软件包中使用它向开发者指出,他们在使用包装代码时可能犯了一个错误。但是,最佳实践就是尝试在我们提供的软件包中将 `error` 值返回给开发者。
147 |
148 | 运行此代码以查看从 `main` 函数调用 `foo` 函数产生的 panic:
149 |
150 | ``` go
151 | package main
152 |
153 | func main() {
154 | foo()
155 | }
156 |
157 | func foo() {
158 | panic("oh no!")
159 | }
160 | ```
161 |
162 | 产生的 panic 输出看起来像:
163 |
164 | ``` shell
165 | # Output
166 | panic: oh no!
167 |
168 | goroutine 1 [running]:
169 | main.foo(...)
170 | /tmp/sandbox494710869/prog.go:8
171 | main.main()
172 | /tmp/sandbox494710869/prog.go:4 +0x40
173 | ```
174 |
175 | 在这里,我们定义了一个 `foo` 函数,里面会使用 `"oh no!"` 这个字符串调用 `panic` 这个内置函数。`foo` 函数由我们的 `main` 函数调用。请注意输出如何输出 `panic: oh no!` 和堆栈跟踪, 在堆栈跟踪中展示一个 goroutine 和两行堆栈跟踪: 一行是 `main()` 函数,另一行是 `foo()` 函数。
176 |
177 | 我们已经看到,panic 产生时似乎终止了我们的程序的运行。当需要正确关闭的开放资源时, 这可能会产生一些问题。GO 提供了一种机制,即使在 panic 的情况下,也可以始终执行一些代码。
178 |
179 | ## derfer 函数
180 |
181 | 你的程序即使在运行时处理 panic 也必须能够正确清理的资源。GO 允许使用 defer 来调用延迟执行函数,直到调用它的函数完成时才会执行。延迟函数即使在出现 panic 的情况下也会运行,并被用作一种安全机制,用来防范 panic 的混乱本质。通过调用普通一样调用函数, 使用关键字 `defer` 作为调用整个函数调用语句的前缀,比如像调用 `defer sayHello()` 一样。运行此示例以查看即使产生 panic 时也可以打印消息:
182 |
183 | ``` go
184 | package main
185 |
186 | import "fmt"
187 |
188 | func main() {
189 | defer func() {
190 | fmt.Println("hello from the deferred function!")
191 | }()
192 |
193 | panic("oh no!")
194 |
195 | }
196 |
197 | ```
198 |
199 | 此示例产生的输出看起来像:
200 |
201 | ``` shell
202 | # Output
203 | hello from the deferred function!
204 | panic: oh no!
205 |
206 | goroutine 1 [running]:
207 | main.main()
208 | /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55
209 | ```
210 |
211 | 在此示例的 `main()` 函数中,我们首先使用 `defer` 调用到打印消息 `"hello from the deferred function!"` 的匿名函数。然后,`main` 函数立即使用 `panic` 函数产生 panic。在此程序的输出中,我们首先看到执行递延函数并打印其消息。在此之后是,我们在 `main` 中产生 panic 消息。
212 |
213 | 延迟函数提供了防范 panic 的保护。在递延函数中,GO 提供另一个内置函数来阻止 panic 终止 GO 程序的机会。
214 |
215 | ## 处理 panic
216 |
217 | go 内置的 `recover` 函数提供了一个恢复 panic 的机制。这个函数通过拦截函数的调用栈并且阻止程序的意外退出。它具有严格的使用规则,但是在编写应用代码时非常有用。
218 |
219 | 因为 `recover` 是内置包的一部分, 所以我们可以在不导包的情况下使用这个函数:
220 |
221 | ``` go
222 | package main
223 |
224 | import (
225 | "fmt"
226 | "log"
227 | )
228 |
229 | func main() {
230 | divideByZero()
231 | fmt.Println("we survived dividing by zero!")
232 |
233 | }
234 |
235 | func divideByZero() {
236 | defer func() {
237 | if err := recover(); err != nil {
238 | log.Println("panic occurred:", err)
239 | }
240 | }()
241 | fmt.Println(divide(1, 0))
242 | }
243 |
244 | func divide(a, b int) int {
245 | return a / b
246 | }
247 |
248 | ```
249 |
250 | 此示例将输出:
251 |
252 | ``` shell
253 | # Output
254 | 2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
255 | we survived dividing by zero!
256 | ```
257 |
258 | 在此示例中,我们在 `main` 函数调用了我们定义的 `DivideByZero` 函数。在 `DivideByZero` 中,我们使用 `defer` 关键字调用匿名函数。这个匿名函数负责处理在 `divideByZero` 中出现的任何 panic。在匿名函数中, 我们调用内置的 `recover` 函数并且将错误信息赋值给 `err`, 如果 `DivideByZero`感处于 panic 状态,那么 `err` 将会被设置值,否则为 `nil`。通过将 `err` 与 `nil` 进行比较,我们可以检测到是否发生了 panic,在这种情况下,我们处理 `panic` 就像处理其他错误一样, 使用 `log.Println` 函数记录了 panic。
259 |
260 | 在延迟执行匿名函数之后, 我们调用了另外一个我们定义的另一个函数, 并且尝试使用 `fmt.Println` 打印这个函数的返回值。所提供的参数将导致除法执行除数为零的操作,这将引起 panic。
261 |
262 | 在此示例的输出中,我们首先从匿名函数中恢复 panic 的日志消息,接下来是 `we survived dividing by zero!` 的消息。我们真的做到了这一点,这要归功于内置的 `recover` 函数, 它成功阻止有可能终止 GO 程序运行的灾难性 panic。
263 |
264 | 从 `recover()` 函数中返回的 `err` 值正是调用 `panic` 的值。因此,在没有发生 panic 时,确保 `err` 值仅为 `nil` 至关重要。
265 |
266 | ## 使用 `recover` 检测 panic
267 |
268 | `recover` 函数依赖于错误的值来确定是否发生了 panic。因为 `panic` 函数的参数是空接口,所以它可以是任何类型。任何接口类型 (包括空接口) 的零值为 `nil`。必须注意避免使用 `nil`作为 `panic` 的参数,如本示例所证明的:
269 |
270 | ``` go
271 | package main
272 |
273 | import (
274 | "fmt"
275 | "log"
276 | )
277 |
278 | func main() {
279 | divideByZero()
280 | fmt.Println("we survived dividing by zero!")
281 |
282 | }
283 |
284 | func divideByZero() {
285 | defer func() {
286 | if err := recover(); err != nil {
287 | log.Println("panic occurred:", err)
288 | }
289 | }()
290 | fmt.Println(divide(1, 0))
291 | }
292 |
293 | func divide(a, b int) int {
294 | if b == 0 {
295 | panic(nil)
296 | }
297 | return a / b
298 | }
299 |
300 | ```
301 |
302 | 这将输出:
303 |
304 | ``` shell
305 | # Output
306 | we survived dividing by zero!
307 | ```
308 |
309 | 此示例与以前的示例相同,该示例涉及 `recover` 并进行一些小的修改。已更改了 `divide` 函数判断 `b` 是否为 `0`。如果是, 它将使用带有 `nil` 作为参数调用 `panic` 函数来产生 panic。这次的输出不包括 `defer` 调用匿名函数的日志消息,即使通过 `Divide` 创建了 panic,也会出现 panic。这种沉默行为是为什么确保调用 `panic` 的参数不是 `nil` 很重要的原因。
310 |
311 | ## Conclusion
312 |
313 | ## 总结
314 |
315 | 我们已经看到了多种可以在 GO 中造成 panic 的方法,以及如何使用恢复的内置的 `recover` 来恢复它们。虽然您不一定会自己使用 `panic`,但适当的 panic 的恢复机制是使 Go 代码达到生产级别应用程序的重要步骤。
316 |
--------------------------------------------------------------------------------
/content/zh/docs/34-Defining_Methods_in_Go.md:
--------------------------------------------------------------------------------
1 | # 在 Go 中定义方法
2 |
3 | ## 简介
4 |
5 | [函数]({{< relref "/docs/27-How_To_Define_and_Call_Functions_in_Go.md" >}})允许你将逻辑组织成可重复的程序,每次运行时可以使用不同的参数。在定义函数的过程中,你常常会发现,可能会有多个函数每次对同一块数据进行操作。Go 可以识别这种模式,并允许您定义特殊的函数,称为方法,其目的是对某些特定类型(称为接收器)的实例进行操作。将方法添加到类型中,不仅可以传达数据是什么,还可以传达如何使用这些数据。
6 |
7 | ## 定义一个方法
8 |
9 | 定义一个方法的语法与定义一个函数的语法很相似。唯一的区别是在 `func` 关键字后面增加了一个额外的参数,用于指定方法的接收器。接收器是你希望定义的方法的类型声明。下面的例子为一个结构体类型定义了一个方法。
10 |
11 | ```go
12 | package main
13 |
14 | import "fmt"
15 |
16 | type Creature struct {
17 | Name string
18 | Greeting string
19 | }
20 |
21 | func (c Creature) Greet() {
22 | fmt.Printf("%s says %s", c.Name, c.Greeting)
23 | }
24 |
25 | func main() {
26 | sammy := Creature{
27 | Name: "Sammy",
28 | Greeting: "Hello!",
29 | }
30 | Creature.Greet(sammy)
31 | }
32 | ```
33 |
34 | 如果你运行这段代码,输出将是:
35 |
36 | ```bash
37 | Output
38 | Sammy says Hello!
39 | ```
40 |
41 | 我们创建了一个名为 `Creature` 的结构,包含字符串类型的 `Name` 和 `Greeting` 字段。这个 `Creature` 结构体有一个定义的方法,即 `Greet`。在接收器声明中,我们将 `Creature` 的实例分配给变量 `c`,以便我们在 `fmt.Printf` 中打印问候信息时可以引用 `Creature` 字段。
42 |
43 | 在其他编程语言中,方法调用的接收器通常用一个关键字来表示(例如:`this` 或 `self`)。Go 认为接收器和其他变量一样,是一个变量,所以你可以自由地命名。社区对这个参数的首选风格是接收器类型小写版本的第一个字符。在这个例子中,我们使用了 `c`,因为接收器的类型是 `Creature`。
44 |
45 | 在 `main` 方法中,我们创建了一个 `Creature` 实例,并为其 `Name` 和 `Greeting` 字段进行赋值。我们在这里调用了 `Greet` 方法,用 `.` 连接类型名和方法名,并提供 `Creature` 实例作为第一个参数。
46 |
47 | Go 提供了另一种更简洁的方式来调用结构体实例的方法,如本例所示:
48 |
49 | ```go
50 | package main
51 |
52 | import "fmt"
53 |
54 | type Creature struct {
55 | Name string
56 | Greeting string
57 | }
58 |
59 | func (c Creature) Greet() {
60 | fmt.Printf("%s says %s", c.Name, c.Greeting)
61 | }
62 |
63 | func main() {
64 | sammy := Creature{
65 | Name: "Sammy",
66 | Greeting: "Hello!",
67 | }
68 | sammy.Greet()
69 | }
70 | ```
71 |
72 | 如果你运行这个,输出将与前面的例子相同:
73 |
74 | ```bash
75 | Output
76 | Sammy says Hello!
77 | ```
78 |
79 | 这个例子与前一个例子相同,但这次我们使用点号来调用 `Greet` 方法,使用存储在 `sammy` 变量中的 `Creature` 作为接收器,这是对第一个例子中的方法调用的简化。标准库和 Go 社区更喜欢这种风格,以至于你很少看到前面所示的方法调用风格。
80 |
81 | 下一个例子展示了使用点号比较普遍的一个原因:
82 |
83 | ```go
84 | package main
85 |
86 | import "fmt"
87 |
88 | type Creature struct {
89 | Name string
90 | Greeting string
91 | }
92 |
93 | func (c Creature) Greet() Creature {
94 | fmt.Printf("%s says %s!\n", c.Name, c.Greeting)
95 | return c
96 | }
97 |
98 | func (c Creature) SayGoodbye(name string) {
99 | fmt.Println("Farewell", name, "!")
100 | }
101 |
102 | func main() {
103 | sammy := Creature{
104 | Name: "Sammy",
105 | Greeting: "Hello!",
106 | }
107 | sammy.Greet().SayGoodbye("gophers")
108 |
109 | Creature.SayGoodbye(Creature.Greet(sammy), "gophers")
110 | }
111 | ```
112 |
113 | 如果你运行这段代码,输出看起来像这样:
114 |
115 | ```go
116 | Output
117 | Sammy says Hello!!
118 | Farewell gophers !
119 | Sammy says Hello!!
120 | Farewell gophers !
121 | ```
122 |
123 | 我们修改了前面的例子,引入了另一个名为 `SayGoodbye` 的方法,并将 `Greet` 改为返回一个 `Creature`,这样我们就可以对该实例调用更多的方法。在 `main` 方法中,我们首先使用点号调用 `sammy` 变量上的 `Greet` 和 `SayGoodbye` 方法,然后使用函数式调用方式。
124 |
125 | 两种风格输出的结果相同,但使用点号的例子更易读。点号调用链路还会告诉我们方法被调用的顺序,而函数式则颠倒了这个顺序。在 `SayGoodbye` 的调用中增加了一个参数,进一步模糊了方法调用的顺序。点号调用的清晰性是 Go 中调用方法的首选风格,无论是在标准库中还是在整个 Go 生态的第三方包中都是如此。
126 |
127 | 相对于定义对某些值进行操作的方法,为类型定义方法对 Go 编程语言还有其他特殊意义,方法是接口背后的核心概念。
128 |
129 | ## 接口
130 |
131 | 当你在 Go 中为任何类型定义方法时,该方法会被添加到该类型的方法集中。方法集是与该类型相关联的方法的集合,并被 Go 编译器用来确定某种类型是否可以分配给具有接口类型的变量。接口类型是一种方法的规范,被编译器用来保证一个类型会实现这些方法。任何具有与接口定义中相同名称、相同参数与相同返回值的方法类型都被称为实现了该接口,并允许被分配给具有该接口类型的变量。下面是标准库中 `fmt.Stringer` 接口的定义。
132 |
133 | ```go
134 | type Stringer interface {
135 | String() string
136 | }
137 | ```
138 |
139 | 一个类型要实现 `fmt.Stringer` 接口,需要提供一个返回 `string` 的 `String()` 方法。实现了这个接口,当你把你的类型实例传递给 `fmt` 包中定义的函数时,你的类型就可以完全按照你的意愿被打印出来(有时称为 “pretty-printed”)。下面的例子定义了一个实现了这个接口的类型:
140 |
141 | ```go
142 | package main
143 |
144 | import (
145 | "fmt"
146 | "strings"
147 | )
148 |
149 | type Ocean struct {
150 | Creatures []string
151 | }
152 |
153 | func (o Ocean) String() string {
154 | return strings.Join(o.Creatures, ", ")
155 | }
156 |
157 | func log(header string, s fmt.Stringer) {
158 | fmt.Println(header, ":", s)
159 | }
160 |
161 | func main() {
162 | o := Ocean{
163 | Creatures: []string{
164 | "sea urchin",
165 | "lobster",
166 | "shark",
167 | },
168 | }
169 | log("ocean contains", o)
170 | }
171 | ```
172 |
173 | 当你运行该代码时,你会看到这样的输出:
174 |
175 | ```bash
176 | Output
177 | ocean contains : sea urchin, lobster, shark
178 | ```
179 |
180 | 这个例子定义了一个名为 `Ocean` 的新结构体类型。`Ocean` 实现了 `fmt.Stringer` 接口,因为 `Ocean` 定义了一个名为 `String` 的方法,该方法不需要参数,返回一个 `string`。在 `main` 方法中,我们定义了一个新的 `Ocean`,并把它传递给一个 `log` 函数,该函数首先接收一个 `string` 来打印,然后是任何实现 `fmt.Stringer` 的参数。Go 编译器允许我们在这里传递 `o`,因为 `Ocean` 实现了 `fmt.Stringer` 所要求的所有方法。在 `log` 中,我们使用 `fmt.Println` ,当它遇到 `fmt.Stringer` 作为其参数之一时,会调用 `Ocean` 的 `String` 方法。
181 |
182 | 如果 `Ocean` 没有实现 `String()` 方法,Go 会产生一个编译错误,因为 `log` 方法要求一个 `fmt.Stringer` 作为其参数。这个错误看起来像这样:
183 |
184 | ```bash
185 | Output
186 | src/e4/main.go:24:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
187 | Ocean does not implement fmt.Stringer (missing String method)
188 | ```
189 |
190 | Go 还将确保提供的 `String()` 方法与 `fmt.Stringer` 接口所要求的方法完全一致。如果不匹配,就会产生一个类似这样的错误:
191 |
192 | ```bash
193 | Output
194 | src/e4/main.go:26:6: cannot use o (type Ocean) as type fmt.Stringer in argument to log:
195 | Ocean does not implement fmt.Stringer (wrong type for String method)
196 | have String()
197 | want String() string
198 | ```
199 |
200 | 在到目前为止的例子中,我们已经在值接收器上定义了方法。也就是说,如果我们使用方法的功能调用,第一个参数(指的是方法所定义的类型)将是一个该类型的值,而不是一个[指针]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})。因此,我们对方法实例所做的任何修改都会在方法执行完毕后被丢弃,因为收到的值是数据的副本。此外,我们也可以在一个类型的指针接收器上定义方法。
201 |
202 | ## 指针接收器
203 |
204 | 在指针接收器上定义方法的语法与在值接收器上定义方法的语法几乎相同。不同的是在接收器声明中用星号(`*`)作为类型名称的前缀。下面的例子在指针接收器上定义了一个类型的方法:
205 |
206 | ```go
207 | package main
208 |
209 | import "fmt"
210 |
211 | type Boat struct {
212 | Name string
213 |
214 | occupants []string
215 | }
216 |
217 | func (b *Boat) AddOccupant(name string) *Boat {
218 | b.occupants = append(b.occupants, name)
219 | return b
220 | }
221 |
222 | func (b Boat) Manifest() {
223 | fmt.Println("The", b.Name, "has the following occupants:")
224 | for _, n := range b.occupants {
225 | fmt.Println("\t", n)
226 | }
227 | }
228 |
229 | func main() {
230 | b := &Boat{
231 | Name: "S.S. DigitalOcean",
232 | }
233 |
234 | b.AddOccupant("Sammy the Shark")
235 | b.AddOccupant("Larry the Lobster")
236 |
237 | b.Manifest()
238 | }
239 | ```
240 |
241 | 当你运行这个例子时,你会看到以下输出:
242 |
243 | ```bash
244 | Output
245 | The S.S. DigitalOcean has the following occupants:
246 | Sammy the Shark
247 | Larry the Lobster
248 | ```
249 |
250 | 这个例子定义了一个包含 `Name` 和 `occupants` 的 `Boat` 类型。我们想规定其他包中的代码只用 `AddOccupant` 方法来添加乘员,所以我们通过小写字段名的第一个字母使 `occupants` 字段不被导出。我们还想确保调用 `AddOccupant` 会导致 `Boat` 实例被修改,这就是为什么我们通过指针接收器定义 `AddOccupant`。指针作为一个类型的特定实例的引用,而不是该类型的副本。`AddOccupant` 将使用 `Boat` 类型的指针调用,可以保证任何修改都是持久的。
251 |
252 | 在 `main` 方法中,我们定义了一个新的变量 `b`,它将持有一个指向 `Boat`(`*Boat`)的指针。我们在这个实例上调用了两次 `AddOccupant` 方法来增加两名乘客。`Manifest` 方法是在`Boat` 值上定义的,因为在其定义中,接收器被指定为(`b Boat`)。在 `main` 方法中,我们仍然能够调用 `Manifest`,因为 Go 能够自动解引用指针以获得 `Boat` 值。`b.Manifest()`在这里等同于 `(*b).Manifest()`。
253 |
254 | 当试图为接口类型的变量赋值时,一个方法是定义在一个指针接收器上还是定义在一个值接收器上有重要的影响。
255 |
256 | ## 指针接收器和接口
257 |
258 | 当你为一个接口类型的变量赋值时,Go 编译器会检查被赋值类型的方法集,以确保它实现了所有接口方法。指针接收器和值接收器的方法集是不同的,因为接收指针的方法可以修改其接收器,而接收值的方法则不能。
259 |
260 | 下面的例子演示了定义两个方法:一个在一个类型的指针接收器上,一个在它的值接收器上。然而,只有指针接收器能够满足本例中也定义的接口:
261 |
262 | ```go
263 | package main
264 |
265 | import "fmt"
266 |
267 | type Submersible interface {
268 | Dive()
269 | }
270 |
271 | type Shark struct {
272 | Name string
273 |
274 | isUnderwater bool
275 | }
276 |
277 | func (s Shark) String() string {
278 | if s.isUnderwater {
279 | return fmt.Sprintf("%s is underwater", s.Name)
280 | }
281 | return fmt.Sprintf("%s is on the surface", s.Name)
282 | }
283 |
284 | func (s *Shark) Dive() {
285 | s.isUnderwater = true
286 | }
287 |
288 | func submerge(s Submersible) {
289 | s.Dive()
290 | }
291 |
292 | func main() {
293 | s := &Shark{
294 | Name: "Sammy",
295 | }
296 |
297 | fmt.Println(s)
298 |
299 | submerge(s)
300 |
301 | fmt.Println(s)
302 | }
303 | ```
304 |
305 | 当你运行该代码时,你会看到这样的输出:
306 |
307 | ```bash
308 | Output
309 | Sammy is on the surface
310 | Sammy is underwater
311 | ```
312 |
313 | 这个例子定义了一个叫做 `Submersible` 的接口,它要求类型实现一个 `Dive()` 方法。然后我们定义了一个包含 `Name` 字段 和 `isUnderwater` 方法的 `Shark` 类型来跟踪 `Shark` 的状态。我们在 `Shark` 的指针接收器上定义了一个 `Dive()` 方法,将 `isUnderwater` 修改为 `true`。我们还定义了值接收器的 `String()` 方法,这样它就可以使用 `fmt.Println` 干净利落地打印出 `Shark` 的状态,方法使用我们之前看过的 `fmt.Println` 所接收的 `fmt.Stringer` 接口。我们还使用了一个函数 `submerge`,它接受一个 `Submersible` 参数。
314 |
315 | 使用 `Submersible` 接口而不是 `*Shark` 允许 `submerge` 方法只依赖于一个类型所提供的行为。这使得 `submerge` 方法更容易重用,因为你不必为 `Submarine`、`Whale` 或任何其他我们还没有想到的未来水生居民编写新的 `submerge` 方法。只要它们定义了一个 `Dive()` 方法,就可以和 `submerge` 方法一起使用。
316 |
317 | 在 `main` 方法中,我们定义了一个变量 `s`,它是一个指向 `Shark` 的指针,并立即用 `fmt.Println` 打印了 `s`。这展示了输出的第一部分,`Sammy is on the surface`。我们把 `s` 传给`submerge`,然后再次调用 `fmt.Println`,以 `s` 为参数,看到输出的第二部分,`Sammy is underwater`。
318 |
319 | 如果我们把 `s` 改成 `Shark`而不是 `*Shark`,Go 编译器会产生错误:
320 |
321 | ```bash
322 | Output
323 | cannot use s (type Shark) as type Submersible in argument to submerge:
324 | Shark does not implement Submersible (Dive method has pointer receiver)
325 | ```
326 |
327 | Go 编译器很好心地告诉我们,`Shark` 确实有一个 `Dive` 方法,它只在指针接收器上定义。当你在自己的代码中看到这条信息时,解决方法是在分配值类型的变量前使用 `&` 操作符,传递一个指向接口类型的指针。
328 |
329 | ## 总结
330 |
331 | 在 Go 中声明方法与定义接收不同类型变量的函数本质上没有区别。同样,[使用指针]({{< relref "/docs/32-Understanding_Pointers_in_Go.md" >}})规则也适用。Go 为这种极其常见的函数定义提供了一些便利,并将这些方法收集到可以通过接口类型进行要求的方法集中。有效地使用方法可以让你在代码中使用接口来提高可测试性,并为你的代码的未来读者留下更好的结构。
332 |
333 | 如果你想了解更多关于 Go 编程语言的一般信息,请查看我们的 [How To Code in Go 系列](https://gocn.github.io/How-To-Code-in-Go/)。
--------------------------------------------------------------------------------
/content/zh/docs/31-Customizing_Go_Binaries_with_Build_Tags.md:
--------------------------------------------------------------------------------
1 | # 用构建标签定制 Go 二进制文件
2 |
3 | ## 简介
4 |
5 | 在 Go 中,*构建标签* 或 *构建约束* 是添加到一段代码中的标识符,它决定了该文件在 `build` 过程中何时应被包含在一个包中。这允许你从同一源代码中构建不同版本的 Go 应用程序,并以快速和有组织的方式在它们之间进行切换。许多开发者使用构建标签来改善构建跨平台兼容的应用程序的工作流程,例如需要修改代码以考虑不同操作系统之间的差异的程序。构建标签还可用于[集成测试](https://en.wikipedia.org/wiki/Integration_testing),允许你在集成代码和带有[Mock 服务](https://en.wikipedia.org/wiki/Mock_object)的代码之间快速切换,并用于应用程序内不同级别的功能集。
6 |
7 | 让我们以不同的客户功能集的问题为例。在编写一些应用程序时,你可能想控制在二进制文件中包括哪些功能,例如一个提供**免费**、**专业**和**企业**级别的应用程序。当客户在这些应用程序中增加他们的订阅级别时,更多的功能将被解锁并可用。为了解决这个问题,你可以维护独立的项目,并试图通过使用 `import` 语句来保持它们的同步性。虽然这种方法可行,但随着时间的推移,它将变得乏味和容易出错。另一种方法是使用构建标签。
8 |
9 | 在本文中,您将使用 Go 中的构建标签来生成不同的可执行二进制文件,这些文件提供了一个示例应用程序的免费、专业和企业功能集。每一个都有不同的功能集,其中免费版本是默认的。
10 |
11 | ## 先决条件
12 |
13 | 要遵循本文的例子,你将需要:
14 |
15 | * 按照 [如何安装 Go 和设置本地编程环境]({{< relref "/docs/01-How_To_Install_Go_and_Set_Up_a_Local Programming_Environment_on_Ubuntu_18.04_DigitalOcean.md" >}})设置的 Go 工作区。
16 |
17 | ## 构建免费版本
18 |
19 | 让我们从构建应用程序的免费版本开始,因为当运行 `go build` 而没有任何构建标签时,它将是默认的。稍后,我们将使用构建标签来有选择地将其他部分添加到我们的程序中。
20 |
21 | 在 `src` 目录下,用你的应用程序的名字创建一个文件夹。本教程将使用`app`:
22 |
23 | ```shell
24 | mkdir app
25 | ```
26 |
27 | 移动到这个文件夹中:
28 |
29 | ```shell
30 | cd app
31 | ```
32 |
33 | 接下来,在你选择的文本编辑器中建立一个新的文本文件,名为 `main.go`:
34 |
35 | ```go
36 | nano main.go
37 | ```
38 |
39 | 现在,我们将定义该应用程序的免费版本。在以下内容中加入`main.go`:
40 |
41 | main.go
42 |
43 | ```go
44 | package main
45 |
46 | import "fmt"
47 |
48 | var features = []string{
49 | "Free Feature #1",
50 | "Free Feature #2",
51 | }
52 |
53 | func main() {
54 | for _, f := range features {
55 | fmt.Println(">", f)
56 | }
57 | }
58 | ```
59 | 在这个文件中,我们创建了一个程序,声明了一个名为 `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` 切片,并将所有可用的功能打印到屏幕上。
60 |
61 | 保存并退出该文件。现在这个文件已经保存了,在文章的其余部分,我们将不再需要编辑它。相反,我们将使用构建标签来改变我们将从中构建的二进制文件的功能特性。
62 |
63 | 构建和运行程序:
64 |
65 | ```shell
66 | go build
67 | ./app
68 | ```
69 |
70 | 你将收到以下输出:
71 |
72 | ```shell
73 | > Free Feature #1
74 | > Free Feature #2
75 | ```
76 | 该程序已经打印出我们的两个免费功能,完成了我们应用程序的免费版本。
77 | 到目前为止,你创建了一个具有非常基本功能集的应用程序。接下来,你将建立一种方法,在构建时向应用程序添加更多的功能。
78 |
79 | ## 用 `go build` 添加专业功能
80 |
81 | 到目前为止,我们避免了对 `main.go` 的修改,模拟了一个常见的生产环境,在这个环境中,需要在不改变和可能破坏主代码的情况下增加代码。由于我们不能编辑 `main.go` 文件,我们需要使用另一种机制,使用构建标签将更多的功能注入到 `features` 切片中。
82 |
83 | 让我们创建一个名为 `pro.go` 的新文件,该文件将使用 `init()` 函数将更多的特特性追加到 `features` 切片中:
84 |
85 | ```shell
86 | nano pro.go
87 | ```
88 |
89 | 一旦编辑器打开了该文件,添加以下几行:
90 |
91 | pro.go
92 |
93 | ```go
94 | package main
95 |
96 | func init() {
97 | features = append(features,
98 | "Pro Feature #1",
99 | "Pro Feature #2",
100 | )
101 | }
102 | ```
103 |
104 | 在这段代码中,我们使用 `init()` 在我们应用程序的 `main()` 函数前运行代码,然后使用 `append()` 将专业功能添加到 `features`切片中。保存并退出该文件。
105 |
106 | 编译并运行应用程序,使用 `go build`:
107 |
108 | ```shell
109 | go build
110 | ```
111 |
112 | 由于现在我们的当前目录中有两个文件( `pro.go` 和 `main.go` ),`go build`将从这两个文件创建一个二进制文件。执行这个二进制文件:
113 |
114 | ```shell
115 | ./app
116 | ```
117 |
118 | 这将为你提供以下功能组合:
119 |
120 | ```shell
121 | > Free Feature #1
122 | > Free Feature #2
123 | > Pro Feature #1
124 | > Pro Feature #2
125 | ```
126 | 该应用程序现在同时包括专业版和免费版的功能。然而,这并不可取:由于版本之间没有区别,免费版现在包括了本应只有专业版才有的功能。为了解决这个问题,你可以加入更多的代码来管理应用程序的不同层级,或者你可以使用构建标签来告诉 Go 工具链哪些`.go`文件需要构建,哪些需要忽略。让我们在下一步中添加构建标签。
127 | ## 添加构建标签
128 |
129 | 你现在可以使用构建标签来区分你的应用程序的专业版本和免费版本。
130 |
131 | 让我们先来看看构建标签是什么样子的:
132 |
133 | ```go
134 | // +build tag_name
135 | ```
136 |
137 | 将这行代码作为软件包的第一行,并将 `tag_name` 替换为你的构建标签的名称,你将把这个软件包标记为可以选择性地包含在最终二进制文件中的代码。让我们来看看这个操作,在 `pro.go` 文件中添加一个构建标签,告诉 `go build` 命令忽略它,除非指定标签。在你的文本编辑器中打开该文件:
138 |
139 | ```shell
140 | nano pro.go
141 | ```
142 |
143 | 然后添加以下突出显示的一行:
144 |
145 | pro.go
146 |
147 | ```go
148 | // +build pro
149 |
150 | package main
151 |
152 | func init() {
153 | features = append(features,
154 | "Pro Feature #1",
155 | "Pro Feature #2",
156 | )
157 | }
158 | ```
159 | 在 `pro.go` 文件的顶部,我们添加了 `// +build pro`,后面是一个空白的换行。这个尾部的换行是必须的,否则 Go 会将其解释为一个注释。构建标签的声明也必须在 `.go` 文件的最顶端。任何东西,甚至是评论,都不能在构建标签之上。
160 | `+build` 声明告诉 `go build` 命令,这不是一个注释,而是一个构建标签。第二部分是 `pro` 标签。通过在 `pro.go` 文件的顶部添加这个标签,`go build` 命令现在将只包括有 `pro` 标签的 `pro.go` 文件。
161 |
162 | 编译并再次运行应用程序:
163 |
164 | ```shell
165 | go build
166 | ./app
167 | ```
168 |
169 | 你将收到以下输出:
170 |
171 | ```shell
172 | > Free Feature #1
173 | > Free Feature #2
174 | ```
175 | 由于 `pro.go` 文件需要有 `pro` 标签,所以该文件被忽略,应用程序在没有标签的情况下进行编译。
176 | 当运行 `go build` 命令时,我们可以使用 `-tags` 标志,通过添加标签本身作为参数,有条件地将代码纳入编译后的源代码中。让我们对 `pro` 标签这样做:
177 |
178 | ```go
179 | go build -tags pro
180 | ```
181 |
182 | 这将输出以下内容:
183 |
184 | ```shell
185 | > Free Feature #1
186 | > Free Feature #2
187 | > Pro Feature #1
188 | > Pro Feature #2
189 | ```
190 | 现在,我们只有在使用 `pro` 构建标签构建应用程序时才能获得额外的功能。
191 | 如果只有两个版本,这很好,但当你加入更多标签时,事情就变得复杂了。为了在下一步添加我们应用程序的企业版,我们将使用多个构建标签,用布尔逻辑连接起来。
192 |
193 | ## 构建标签布尔逻辑
194 |
195 | 当一个 Go 包中有多个构建标签时,这些标签会使用[布尔逻辑]({{< relref "/docs/14-Understanding_Boolean_Logic_in_Go.md" >}})相互作用。为了证明这一点,我们将同时使用 `pro` 标签和 `enterprise` 标签来添加我们应用程序的企业级。
196 |
197 | 为了建立一个企业级二进制文件,我们将需要包括默认功能、专业级功能和一套新的企业级功能。首先,打开一个编辑器并创建一个新文件,`enterprise.go`,它将添加新的企业级功能:
198 |
199 | ```shell
200 | nano enterprise.go
201 | ```
202 |
203 | `enterprise.go` 的内容将看起来与 `pro.go`几乎相同,但将包含新的功能。在该文件中添加以下几行:
204 |
205 | enterprise.go
206 |
207 | ```go
208 | package main
209 |
210 | func init() {
211 | features = append(features,
212 | "Enterprise Feature #1",
213 | "Enterprise Feature #2",
214 | )
215 | }
216 | ```
217 |
218 | 保存并退出该文件。
219 |
220 | 目前 `enterprise.go` 文件没有任何构建标签,正如你在添加 `pro.go` 时了解到的,这意味着这些功能将在执行 `go build` 时被添加到免费版本中。对于 `pro.go`,你在文件的顶部添加了 `// +build pro` 和一个换行符,以告诉 `go build` 只有在使用 `tags pro` 时才应包含它。在这种情况下,你只需要一个构建标记就能达到目的。然而,当添加新的企业版功能时,你首先必须同时拥有专业版功能。
221 |
222 | 让我们先在 `enterprise.go` 中添加对 `pro` 构建标签的支持。用你的文本编辑器打开该文件:
223 |
224 | ```shell
225 | nano enterprise.go
226 | ```
227 |
228 | 接下来,在 `package main` 声明前添加构建标签,并确保在构建标签后包含一个换行符:
229 |
230 | enterprise.go
231 |
232 | ```go
233 | // +build pro
234 |
235 | package main
236 |
237 | func init() {
238 | features = append(features,
239 | "Enterprise Feature #1",
240 | "Enterprise Feature #2",
241 | )
242 | }
243 | ```
244 | 保存并退出该文件。
245 | 编译并运行没有任何标签的应用程序:
246 |
247 | ```shell
248 | go build
249 | ./app
250 | ```
251 |
252 | 你将收到以下输出:
253 |
254 | ```shell
255 | > Free Feature #1
256 | > Free Feature #2
257 | ```
258 | 企业版的功能在免费版中不再显示。现在让我们添加 `pro` 构建标签,再次构建并运行该应用程序:
259 | ```shell
260 | go build -tags pro
261 | ./app
262 | ```
263 |
264 | 你将收到以下输出:
265 |
266 | ```shell
267 | > Free Feature #1
268 | > Free Feature #2
269 | > Enterprise Feature #1
270 | > Enterprise Feature #2
271 | > Pro Feature #1
272 | > Pro Feature #2
273 | ```
274 | 这仍然不完全是我们需要的。现在,当我们试图构建专业版时,企业版的功能出现了。为了解决这个问题,我们需要使用另一个构建标签。但与 `pro` 标签不同的是,我们现在需要确保 `pro` 和 `enterprise` 功能都是可用的。
275 | Go 构建系统通过允许在构建标签系统中使用一些基本的布尔逻辑来说明这种情况。
276 |
277 | 让我们再次打开 `enterprise.go`:
278 |
279 | ```shell
280 | nano enterprise.go
281 | ```
282 |
283 | 在`pro`标签的同一行添加另一个构建标签,`enterprise`:
284 |
285 | enterprise.go
286 |
287 | ```go
288 | // +build pro enterprise
289 |
290 | package main
291 |
292 | func init() {
293 | features = append(features,
294 | "Enterprise Feature #1",
295 | "Enterprise Feature #2",
296 | )
297 | }
298 | ```
299 | 保存并关闭该文件。
300 | 现在让我们用新的 `enterprise` 构建标签来编译和运行该应用程序。
301 |
302 | ```shell
303 | go build -tags enterprise
304 | ./app
305 | ```
306 |
307 | 这将得到以下信息:
308 |
309 | ```shell
310 | > Free Feature #1
311 | > Free Feature #2
312 | > Enterprise Feature #1
313 | > Enterprise Feature #2
314 | ```
315 | 现在我们已经失去了专业功能。这是因为当我们在 `.go` 文件中的同一行放置多个构建标签时,`go build` 会将它们解释为使用 `OR` 逻辑。 加上 `// +build pro enterprise`一行,如果存在 `pro` 构建标签**或** `enterprise` 构建标签,`enterprise.go` 文件将被构建。 我们需要正确设置构建标签,要求**两个同时**,并使用 `AND` 逻辑来代替。
316 | 如果我们不把这两个标签放在同一行,而是把它们放在不同的行上,那么 `go build` 将使用 `AND` 逻辑来解释这些标签。
317 |
318 | 再次打开 `enterprise.go`,让我们把构建标签分成多行。
319 |
320 | enterprise.go
321 |
322 | ```go
323 | // +build pro
324 | // +build enterprise
325 |
326 | package main
327 |
328 | func init() {
329 | features = append(features,
330 | "Enterprise Feature #1",
331 | "Enterprise Feature #2",
332 | )
333 | }
334 | ```
335 | 现在用新的 `enterprise` 构建标签编译和运行应用程序:
336 | ```shell
337 | go build -tags enterprise
338 | ./app
339 | ```
340 |
341 | 你将收到以下输出:
342 |
343 | ```shell
344 | > Free Feature #1
345 | > Free Feature #2
346 | ```
347 | 仍然没有达到目的。因为 `AND` 语句需要两个元素都被认为是 `true`,我们需要同时使用 `pro` 和 `enterprise` 构建标签。
348 | 让我们再试一次:
349 |
350 | ```shell
351 | go build -tags "enterprise pro"
352 | ./app
353 | ```
354 |
355 | 你将收到以下输出:
356 |
357 | ```shell
358 | > Free Feature #1
359 | > Free Feature #2
360 | > Enterprise Feature #1
361 | > Enterprise Feature #2
362 | > Pro Feature #1
363 | > Pro Feature #2
364 | ```
365 | 现在,我们的应用程序可以从同一个源树中以多种方式构建,并相应地解锁应用程序的功能。
366 | 在这个例子中,我们使用了一个新的 `// +build` 标签来表示 `AND` 逻辑,但也有其他方法可以用构建标签表示布尔逻辑。下表是构建标签的其他语法格式的一些例子,以及它们的布尔等价物:
367 |
368 | |构建标签语法|构建标签示例|布尔声明|
369 | |:----|:----|:----|
370 | |空格分隔的元素|// +build pro enterprise|pro OR enterprise|
371 | |逗号分隔的元素|// +build pro,enterprise|pro AND enterprise|
372 | |感叹号元素|// +build !pro|NOT pro|
373 |
374 | ## 总结
375 |
376 | 在本教程中,你使用构建标签来控制你的哪些代码被编译到二进制中。首先,你声明了构建标签并使用它们与 `go build`,然后你用布尔逻辑组合了多个标签。然后,你建立了一个程序,代表了免费版、专业版和企业版的不同功能集,显示了构建标签对项目的强大控制能力。
377 |
378 | 如果你想了解更多关于构建标签的信息,请看一下 [Golang 的相关文档](https://golang.org/pkg/go/build/#hdr-Build_Constraints),或者继续探索我们的[如何在 Go 中编码系列](https://gocn.github.io/How-To-Code-in-Go/)。
379 |
380 |
381 |
382 |
--------------------------------------------------------------------------------
/content/zh/docs/15-Understanding_Maps_in_Go.md:
--------------------------------------------------------------------------------
1 | # 理解 Go 中的 Map
2 |
3 | 大多数现代编程语言都有_字典_或_哈希_类型的概念。这些类型通常用于以成对的方式存储数据,其中的**key**映射到**value**。
4 |
5 | 在 Go 中,_map_ 数据类型就是大多数程序员认为的字典类型。它将键映射到值,形成键值对,是 Go 中存储数据的一种有效方式。一个 map 的构造是通过使用关键字 `map`,然后是方括号中的键数据类型 `[ ]`,接着是值数据类型。然后将键值对放在大括号的两侧 { } 中。
6 |
7 | ```go
8 | map[key]value{}
9 | ```
10 |
11 | 通常在 Go 中使用 map 来保存相关数据,例如 ID 中包含的信息。一个有数据的 map 看起来像这样。
12 |
13 | ```go
14 | map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
15 | ```
16 |
17 | 除了大括号外,整个 map 中还有冒号连接键值对。冒号左边的字是键。键值可以是 Go 中可比较的类型,如 `strings`、`ints` 等。
18 |
19 | 示例 map 中的键是:
20 |
21 | * `"name"`
22 | * `"animal"`
23 | * `"color"`
24 | * `"location"`
25 |
26 | 冒号右边的字是值,值可以是任何数据类型。示例 map 中的值是:
27 |
28 | * `"Sammy"`
29 | * `"shark"`
30 | * `"blue"`
31 | * `"ocean"`
32 |
33 | 像其他数据类型一样,你可以将 map 存储在一个变量内,并将其打印出来:
34 |
35 | ```go
36 | sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
37 | fmt.Println(sammy)
38 |
39 | ```
40 |
41 | 输出为:
42 |
43 | ```go
44 | Output
45 | map[animal:shark color:blue location:ocean name:Sammy]
46 | ```
47 |
48 | 键值对的顺序可能发生了变化。在 Go 中,map 数据类型是无序的。无论顺序如何,键值对将保持不变,你能够根据键值关系来访问数据。
49 |
50 | ## 获取 map 元素
51 |
52 | 你可以通过引用相关的键来获取一个 map 的值。由于 map 提供了存储数据的键值对,它们可以成为你的 Go 程序中重要而有用的东西。
53 |
54 | 如果你想获取 Sammy 的用户名,你可以通过调用 `sammy["name"]` 来实现;持有你的 map 和相关键的变量。我们把它打印出来:
55 |
56 | ```go
57 | fmt.Println(sammy["name"])
58 | ```
59 |
60 | 输出的值如下:
61 |
62 | ```go
63 | Output
64 | Sammy
65 | ```
66 |
67 | map 的行为就像一个数据库;而不是像分片那样调用一个整数来获得一个特定的索引值,你把一个值分配给一个键,然后调用这个键来获得它的相关值。
68 |
69 | 通过调用键 `name`,你会得到该键的值,也就是 `Sammy`。
70 |
71 | 类似地,你可以用同样的格式调用 `sammy` 映射中的其余值:
72 |
73 | ```go
74 | fmt.Println(sammy["animal"])
75 | // returns shark
76 |
77 | fmt.Println(sammy["color"])
78 | // returns blue
79 |
80 | fmt.Println(sammy["location"])
81 | // returns ocean
82 | ```
83 |
84 | 通过利用 map 数据类型中的键值对,你可以引用键来查询值。
85 |
86 | ## 键和值
87 |
88 | 与某些编程语言不同,Go 没有任何方便的函数来列出 map 的键或值。比如 Python 可以用 `.keys()` 方法来查看所有的键。然而,它允许通过使用 `range` 操作符来进行迭代查看:
89 |
90 | ```go
91 | for key, value := range sammy {
92 | fmt.Printf("%q is the key for the value %q\n", key, value)
93 | }
94 | ```
95 |
96 | 当在 Go 中对一个 map 进行 range 遍历时,它将返回两个值。第一个值是键,第二个值是值。Go 将以正确的数据类型创建这些变量。在这个例子中,map 的键是一个 `string`,所以 `key` 也将是一个字符串。`value` 也是一个字符串:
97 |
98 | ```go
99 | Output
100 | animal" is the key for the value "shark"
101 | "color" is the key for the value "blue"
102 | "location" is the key for the value "ocean"
103 | "name" is the key for the value "Sammy"
104 | ```
105 |
106 | 要获得一个只有键的列表,你可以再次使用 range 操作符。你可以只声明一个变量,只访问键:
107 |
108 |
109 | ```go
110 | keys := []string{}
111 |
112 | for key := range sammy {
113 | keys = append(keys, key)
114 | }
115 | fmt.Printf("%q", keys)
116 | ```
117 |
118 | 程序一开始就声明了一个切片来存储你的键。
119 |
120 | 只输出 map 的所有键:
121 |
122 | ```go
123 | Output
124 | ["color" "location" "name" "animal"]
125 | ```
126 |
127 | 同样,这些键没有被排序。如果你想对它们进行排序,你可以使用[`sort`](https://golang.org/pkg/sort)包中的 `sort.Strings` 函数:
128 |
129 |
130 | ```go
131 | sort.Strings(keys)
132 | ```
133 |
134 | 使用这个函数,你会收到以下输出:
135 |
136 | ```go
137 | Output
138 | ["animal" "color" "location" "name"]
139 | ```
140 |
141 | 你可以使用同样的模式来检索一个 map 中的值。在下一个例子中,你预先分配了切片以避免分配,从而使程序更有效率:
142 |
143 |
144 | ```go
145 | sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
146 |
147 | items := make([]string, len(sammy))
148 |
149 | var i int
150 |
151 | for _, v := range sammy {
152 | items[i] = v
153 | i++
154 | }
155 | fmt.Printf("%q", items)
156 | ```
157 |
158 | 首先,你声明一个切片来存储键;因为你知道需要多少个元素,你可以通过定义切片的大小来避免潜在的内存分配。然后你声明索引变量。由于你不想要这个键而使用 `_` 操作符,当开始循环时,忽略这个键的值。输出将如下:
159 |
160 |
161 | ```go
162 | Output
163 | ["ocean" "Sammy" "shark" "blue"]
164 | ```
165 |
166 | 要查看一个 map 中的元素数量,可以使用内置的`len`函数:
167 |
168 | ```go
169 | sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
170 | fmt.Println(len(sammy))
171 | ```
172 |
173 | 输出展示 map 中的元素数量:
174 |
175 | ```go
176 | Output
177 | 4
178 | ```
179 |
180 | 尽管 Go 没有提供获取键和值的便利函数,但在需要时只需要几行代码就可以检索到键和值。
181 |
182 | ## 检查存在性
183 |
184 | 当请求的键不存在时,Go 中的 map 将为 map 的值类型返回零值。正因为如此,你需要用另一种方法来区分存储零值和不存在的键。
185 |
186 | 我们来查询 map 中的一个不存在的键,并看看返回的值:
187 |
188 | ```go
189 | counts := map[string]int{}
190 | fmt.Println(counts["sammy"])
191 | ```
192 |
193 | 你会看到一下输出:
194 |
195 | ```go
196 | Output
197 | 0
198 | ```
199 |
200 | 即使键 `sammy` 不在 map 中,Go 仍然返回 `0` 。这是因为值的数据类型是 `int`,由于 Go 中所有的变量都有零值,所以它返回的是 `0` 的零值。
201 |
202 | 在很多情况下,这是不可取的,会导致你的程序出现错误。在查找 map 中的值时,Go 可以返回两个值。这第二个值是一个 `bool` 类型,如果找到了键,则为 `true` ,如果没有找到键,则为 `false`。在 Go 中,惯用变量名为 `ok`。尽管你可以把捕捉第二个参数的变量命名为任何名字,但在 Go 中,`ok` 是一种惯用方式:
203 |
204 | ```go
205 | count, ok := counts["sammy"]
206 | ```
207 |
208 | 如果键 `sammy` 存在于 `counts` map 中,那么 `ok` 将是 `true`。否则,`ok` 将是 `false`。
209 |
210 | 你可以使用 `ok` 变量来决定在你的程序中做什么:
211 |
212 | ```go
213 | if ok {
214 | fmt.Printf("Sammy has a count of %d\n", count)
215 | } else {
216 | fmt.Println("Sammy was not found")
217 | }
218 | ```
219 |
220 | 输出结果如下:
221 |
222 | ```go
223 | Output
224 | Sammy was not found
225 | ```
226 |
227 | 在 Go 中,你可以将变量声明和条件检查与 if/else 相结合。这使得你可以使用一个单一的语句来进行这种检查:
228 |
229 | ```go
230 | if count, ok := counts["sammy"]; ok {
231 | fmt.Printf("Sammy has a count of %d\n", count)
232 | } else {
233 | fmt.Println("Sammy was not found")
234 | }
235 | ```
236 |
237 | 在 Go 中从 map 中查询一个值时,检查其是否存在是很好的做法,以避免程序中出现错误。
238 |
239 | ## 修改 map
240 |
241 | map 是一个可变的数据结构,所以你可以修改它们。让我们在本节中看看添加和删除 map 的元素。
242 |
243 | ### 增加和修改 map 的元素
244 |
245 | 在不使用方法或函数的情况下,你可以向 map 添加键值对。你可以使用 map 的变量名,然后是方括号中的键值 `[ ]`,并使用 `=` 操作符来设置一个新值:
246 |
247 | ```go
248 | map[key] = value
249 | ```
250 |
251 | 你可以通过在一个名为 `usernames` 的 map 上添加一个键值对来看到这个行为:
252 |
253 | ```go
254 | usernames := map[string]string{"Sammy": "sammy-shark", "Jamie": "mantisshrimp54"}
255 |
256 | usernames["Drew"] = "squidly"
257 | fmt.Println(usernames)
258 | ```
259 |
260 | 输出将展示 map 中新的 `Drew:squidly` 键值对:
261 |
262 | ```go
263 | Output
264 | map[Drew:squidly Jamie:mantisshrimp54 Sammy:sammy-shark]
265 | ```
266 |
267 | 因为 map 是无序返回的,这一对键值可以出现在 map 输出的任何地方。如果你程序后面使用 `usernames` 的 map,它将包括新加的这个键值对。
268 |
269 | 你也可以使用这个语法来修改一个键的值。在这种情况下,你引用一个现有的键,并传递一个不同的值给它。
270 |
271 | 考虑一个名为 `followers` 的 map,它记录某个网络上用户的粉丝。用户 `drew` 今天的粉丝增加了,所以你需要更新 `drew` 键的整数值。可以使用 `Println()` 函数来检查 map 是否被修改:
272 |
273 | ```go
274 | followers := map[string]int{"drew": 305, "mary": 428, "cindy": 918}
275 | followers["drew"] = 342
276 | fmt.Println(followers)
277 | ```
278 |
279 | 以下输出了 `drew` 更新后的值:
280 |
281 | ```go
282 | Output
283 | map[cindy:918 drew:342 mary:428]
284 | ```
285 |
286 | 你可以用这种方法将键值对添加到用户输入的 map 中。让我们写一个快速程序 `usernames.go`,它在命令行上运行,允许用户输入,以增加更多的名字和相关的用户名:
287 |
288 | usernames.go
289 |
290 | ```go
291 | package main
292 |
293 | import (
294 | "fmt"
295 | "strings"
296 | )
297 |
298 | func main() {
299 | usernames := map[string]string{"Sammy": "sammy-shark", "Jamie": "mantisshrimp54"}
300 |
301 | for {
302 | fmt.Println("Enter a name:")
303 |
304 | var name string
305 | _, err := fmt.Scanln(&name)
306 |
307 | if err != nil {
308 | panic(err)
309 | }
310 |
311 | name = strings.TrimSpace(name)
312 |
313 | if u, ok := usernames[name]; ok {
314 | fmt.Printf("%q is the username of %q\n", u, name)
315 | continue
316 | }
317 |
318 | fmt.Printf("I don't have %v's username, what is it?\n", name)
319 |
320 | var username string
321 | _, err = fmt.Scanln(&username)
322 |
323 | if err != nil {
324 | panic(err)
325 | }
326 |
327 | username = strings.TrimSpace(username)
328 |
329 | usernames[name] = username
330 |
331 | fmt.Println("Data updated.")
332 | }
333 | }
334 | ```
335 |
336 | 在 `usernames.go` 中,你首先定义 map。然后设置了一个循环来迭代名字。你要求用户输入一个名字,并声明一个变量来存储它。接下来检查是否有错误;如果有,程序将以 _panic_ 退出。因为 `Scanln` 捕获了整个输入,包括回车,你需要从输入中删除空格;你可以用 `strings.TrimSpace` 函数来做这个。
337 |
338 | `if` 代码块检查名字是否存在于 map 中并打印反馈。如果名字是存在的,则继续回到循环的顶部。如果名字不在 map 中,它会向用户提供反馈,然后会要求提供一个新的用户名。程序再次检查,看是否有错误。如果没有错误,它就去掉回车键,将用户名的值分配给名字键,然后打印出数据被更新的反馈。
339 |
340 | 让我们在命令行上运行该程序:
341 |
342 | ```go
343 | go run usernames.go
344 | ```
345 |
346 | 你将看到如下输出:
347 |
348 | ```go
349 | Output
350 | Enter a name:
351 | Sammy
352 | "sammy-shark" is the username of "Sammy"
353 | Enter a name:
354 | Jesse
355 | I don't have Jesse's username, what is it?
356 | JOctopus
357 | Data updated.
358 | Enter a name:
359 | ```
360 |
361 | 完成测试后,可以通过 `CTRL + C` 键退出程序。
362 |
363 | 这展示了你如何以交互方式修改 map。对于这个特殊的程序,只要你用 `CTRL + C` 退出程序,你就会丢失所有的数据,除非你实现了处理读写文件的方法。
364 |
365 | 总结一下,你可以用 `map[key] = value` 语法向 map 添加元素或修改值。
366 |
367 | ### 删除 map 的元素
368 |
369 | 正如你可以在 map 数据类型中添加键值对和改变数值一样,你也可以在 map 中删除元素。
370 |
371 | 要从一个 map 中删除一个键值对,你可以使用内置函数 `delete()`。第一个参数是你想要删除的 map。第二个参数是你要删除的键:
372 |
373 | ```go
374 | delete(map, key)
375 | ```
376 |
377 | 我们定义一个名为 `permission` 的 map:
378 |
379 | ```go
380 | permissions := map[int]string{1: "read", 2: "write", 4: "delete", 8: "create", 16:"modify"}
381 | ```
382 |
383 | 你不再需要 `modify` 变量,所以你要把它从 map 上删除。然后,你要打印出 map,以确认它被删除。
384 |
385 | ```go
386 | permissions := map[int]string{1: "read", 2: "write", 4: "delete", 8: "create", 16: "modify"}
387 | delete(permissions, 16)
388 | fmt.Println(permissions)
389 | ```
390 |
391 | 输出如下:
392 |
393 | ```go
394 | Output
395 | map[1:read 2:write 4:delete 8:create]
396 | ```
397 |
398 | 这一行 `delete(permissions, 16)` 从 `permissions` map 中移除键值对 `16: "modify"`。
399 |
400 | 如果你想清除一个 map 的所有值,你可以通过把它设置为一个相同类型的空 map 来实现。这将创建一个新的空 map 来使旧的 map 被垃圾收集器清除。
401 |
402 | 让我们删除 `permissions` map 中的所有元素:
403 |
404 | ```go
405 | permissions = map[int]string{}
406 | fmt.Println(permissions)
407 | ```
408 |
409 | 输出展示 map 为空:
410 |
411 | ```go
412 | Output
413 | map[]
414 | ```
415 |
416 | 因为 map 是可变的数据类型,它们可以被添加、修改、删除和清除元素。
417 |
418 | ## 总结
419 |
420 | 本教程探讨了 Go 中的 map 数据结构。map 是由键值对组成的,提供了一种不依赖索引的数据存储方式。这使得我们可以根据其含义和与其他数据类型的关系来检索数值。
--------------------------------------------------------------------------------