Skip to content

Commit

Permalink
4-5 章 (unknwon#844)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Chen <[email protected]>
  • Loading branch information
loftea and unknwon authored May 9, 2022
1 parent 8202608 commit f5dae8f
Show file tree
Hide file tree
Showing 16 changed files with 272 additions and 266 deletions.
4 changes: 2 additions & 2 deletions eBook/04.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Go 的源文件以 `.go` 为后缀名存储在计算机中,这些文件名均由小写字母组成,如 `scanner.go` 。如果文件名由多个部分组成,则使用下划线 `_` 对它们进行分隔,如 `scanner_test.go` 。文件名不包含空格或其他特殊字符。

一个源文件可以包含任意多行的代码,Go 本身没有对源文件的大小进行限制。

你会发现在 Go 代码中的几乎所有东西都有一个名称或标识符。另外,Go 语言也是区分大小写的,这与 C 家族中的其它语言相同。有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 `_`)开头,然后紧跟着 0 个或多个字符或 Unicode 数字,如:X56、group1、_x23、i、өԑ12。

以下是无效的标识符:
Expand Down Expand Up @@ -58,7 +58,7 @@ Go 的源文件以 `.go` 为后缀名存储在计算机中,这些文件名均

之所以刻意地将 Go 代码中的关键字保持的这么少,是为了简化在编译过程第一步中的代码解析。和其它语言一样,关键字不能够作标识符使用。

除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数(第 6.5 节),它们的作用都将在接下来的章节中进行进一步地讲解。
除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数([第 6.5 节](06.5.md)),它们的作用都将在接下来的章节中进行进一步地讲解。

<table class="table table-bordered table-striped table-condensed">
<tr>
Expand Down
70 changes: 35 additions & 35 deletions eBook/04.2.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions eBook/04.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 i
- 正确的做法:`const c1 = 2/3`
- 错误的做法:`const c2 = getNumber()` // 引发构建错误: `getNumber() used as value`

**因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。**
**因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:`len()`**

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出:

Expand Down Expand Up @@ -64,7 +64,7 @@ const (
)
```

现在,数字 0、12 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case 结构 (第 5.3 节).
现在,数字 `0``1``2` 分别代表未知性别、女性和男性。这些枚举值可以用于测试某个变量或常量的实际值,比如使用 switch/case 结构[第 5.3 节](.\05.3.md))。

在这个例子中,`iota` 可以被用作枚举值:

Expand Down Expand Up @@ -116,9 +116,9 @@ const (
)
```

**译者注:关于 iota 的使用涉及到非常复杂多样的情况,这里作者解释的并不清晰,因为很难对 iota 的用法进行直观的文字描述。如希望进一步了解,请观看视频教程 [《Go编程基础》](https://github.com/Unknwon/go-fundamental-programming) [第四课:常量与运算符](https://github.com/Unknwon/go-fundamental-programming/blob/master/lectures/lecture4.md)**
**译者注:关于 `iota` 的使用涉及到非常复杂多样的情况,这里作者解释的并不清晰,因为很难对 `iota` 的用法进行直观的文字描述。如希望进一步了解,请观看视频教程 [《Go编程基础》](https://github.com/Unknwon/go-fundamental-programming) [第四课:常量与运算符](https://github.com/Unknwon/go-fundamental-programming/blob/master/lectures/lecture4.md)**

`iota` 也可以用在表达式中,如:`iota + 50`。在每遇到一个新的常量块或单个常量声明时, `iota` 都会重置为 0( **简单地讲,每遇到一次 const 关键字,iota 就重置为 0** )。
`iota` 也可以用在表达式中,如:`iota + 50`。在每遇到一个新的常量块或单个常量声明时, `iota` 都会重置为 0( **简单地讲,每遇到一次 const 关键字,`iota` 就重置为 0** )。

当然,常量之所以为常量就是恒定不变的量,因此我们无法在程序运行过程中修改它的值;如果你在代码中试图修改常量的值则会引发编译错误。

Expand Down
61 changes: 31 additions & 30 deletions eBook/04.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

需要注意的是,Go 和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。Go 为什么要选择这么做呢?

首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如:`int* a, b;`。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写(你可以在 [Go 语言的声明语法](http://blog.golang.org/2010/07/gos-declaration-syntax.html) 页面找到有关于这个话题的更多讨论)。
首先,它是为了避免像 C 语言中那样含糊不清的声明形式,例如:`int* a, b;`。在这个例子中,只有 `a` 是指针而 `b` 不是。如果你想要这两个变量都是指针,则需要将它们分开书写(你可以在 [Go 语言的声明语法](http://blog.golang.org/2010/07/gos-declaration-syntax.html) 页面找到有关于这个话题的更多讨论)。

而在 Go 中,则可以很轻松地将它们都声明为指针类型:

Expand Down Expand Up @@ -36,15 +36,15 @@ var (

这种因式分解关键字的写法一般用于声明全局变量。

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 falsestring 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
当一个变量被声明之后,系统自动赋予它该类型的零值:`int``0``float32(64)``0.0`,bool 为 `false``string` 为空字符串,指针为 `nil`。记住,所有的内存在 Go 中都是经过初始化的。

变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:`numShips``startDate`

但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写(第 4.2 节:可见性规则)。

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。在第 5 章,我们将会学习到像 if 和 for 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。[5 章](05.0.md),我们将会学习到像 `if``for` 这些控制结构,而在这些结构中声明的变量的作用域只在相应的代码块内。一般情况下,局部变量的作用域可以通过代码块(用大括号括起来的部分)判断。

尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。

Expand All @@ -57,7 +57,7 @@ a = 15
b = false
```

一般情况下,当变量a和变量b之间类型相同时,才能进行如`a = b`的赋值。
一般情况下,当变量a和变量b之间类型相同时,才能进行如 `a = b` 的赋值。

声明与赋值(初始化)语句也可以组合起来。

Expand Down Expand Up @@ -113,7 +113,7 @@ var (
a := 1
```

下面这个例子展示了如何通过`runtime`包在运行时获取所在的操作系统类型,以及如何通过 `os` 包中的函数 `os.Getenv()` 来获取环境变量中的值,并保存到 string 类型的局部变量 path 中。
下面这个例子展示了如何通过 `runtime` 包在运行时获取所在的操作系统类型,以及如何通过 `os` 包中的函数 `os.Getenv()` 来获取环境变量中的值,并保存到 `string` 类型的局部变量 `path` 中。

示例 4.5 [goos.go](examples/chapter_4/goos.go)

Expand All @@ -136,44 +136,45 @@ func main() {

如果你在 Windows 下运行这段代码,则会输出 `The operating system is: windows` 以及相应的环境变量的值;如果你在 Linux 下运行这段代码,则会输出 `The operating system is: linux` 以及相应的的环境变量的值。

这里用到了 `Printf` 的格式化输出的功能(第 4.4.3 节)。
这里用到了 `Printf` 的格式化输出的功能([第 4.4.3 节](.\04.4.md))。

## 4.4.2 值类型和引用类型

程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。
程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为“字”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。

所有像 intfloatbool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:
所有像 `int``float``bool``string` 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:

![](images/4.4.2_fig4.1.jpg?raw=true)
<img src="images/4.4.2_fig4.1.jpg?raw=true" style="zoom:67%;" />

另外,像数组(第 7 章)和结构(第 10 章)这些复合类型也是值类型。
另外,像数组([第 7 章](.\07.0.md))和结构([第 10 章](.\10.0md))这些复合类型也是值类型。

当使用等号 `=` 将一个变量的值赋值给另一个变量时,如:`j = i`,实际上是在内存中将 i 的值进行了拷贝:
当使用等号 `=` 将一个变量的值赋值给另一个变量时,如:`j = i`,实际上是在内存中将 `i` 的值进行了拷贝:

![](images/4.4.2_fig4.2.jpg?raw=true)
<img src="images/4.4.2_fig4.2.jpg?raw=true" style="zoom: 67%;" />

你可以通过 &i 来获取变量 i 的内存地址(第 4.9 节),例如:0xf840000040(每次的地址都可能不一样)。值类型的变量的值存储在栈中。
你可以通过 `&i` 来获取变量 `i` 的内存地址([第 4.9 节](.\04.9.md)),例如:`0xf840000040`(每次的地址都可能不一样)。值类型的变量的值存储在栈中。

内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。

更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。

一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
一个引用类型的变量 `r1` 存储的是 `r1` 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。

![](images/4.4.2_fig4.3.jpg?raw=true)
<img src="images/4.4.2_fig4.3.jpg?raw=true" style="zoom:67%;" />

这个内存地址被称之为指针(你可以从上图中很清晰地看到,第 4.9 节将会详细说明),这个指针实际上也被存在另外的某一个字中。
这个内存地址被称之为指针(你可以从上图中很清晰地看到,[第 4.9 ](.\04.9.md) 将会详细说明),这个指针实际上也被存在另外的某一个字中。

同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。

当使用赋值语句 `r2 = r1` 时,只有引用(地址)被复制。

如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
如果 `r1` 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,`r2` 也会受到影响。

在 Go 语言中,指针(第 4.9 节)属于引用类型,其它的引用类型还包括 slices(第 7 章),maps(第 8 章)和 channel(第 13 章)。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
在 Go 语言中,指针([第 4.9 节](.\04.9.md))属于引用类型,其它的引用类型还包括 slices([第 7 章](07.0.md)),maps([第 8 章](08.0.md))和 channel([第 13 章](13.0.md))。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。

## 4.4.3 打印
函数 `Printf` 可以在 fmt 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:

函数 `Printf` 可以在 `fmt` 包外部使用,这是因为它以大写字母 P 开头,该函数主要用于打印输出到控制台。通常使用的格式化字符串作为第一个参数:

```go
func Printf(format string, list of variables to be printed)
Expand All @@ -197,17 +198,17 @@ fmt.Print("Hello:", 23)

我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,而这个时候再在 Example 4.4.1 的最后一个声明语句写上 `var` 关键字就显得有些多余了,因此我们可以将它们简写为 `a := 50` 或 `b := false`。

ab 的类型(int 和 bool)将由编译器自动推断。
`a``b` 的类型(`int``bool`)将由编译器自动推断。

这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 `:=` 可以高效地创建一个新的变量,称之为初始化声明。

**注意事项**

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:`a := 20` 就是不被允许的,编译器会提示错误 `no new variables on left side of :=`,但是 `a = 20` 是可以的,因为这是给相同的变量赋予一个新的值。

如果你在定义变量 a 之前使用它,则会得到编译错误 `undefined: a`。
如果你在定义变量 `a` 之前使用它,则会得到编译错误 `undefined: a`。

如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 `a`

```go
func main() {
Expand All @@ -218,7 +219,7 @@ func main() {

尝试编译这段代码将得到错误 `a declared and not used`

此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用 `fmt.Println("hello, world", a)` 会移除错误。
此外,单纯地给 `a` 赋值也是不够的,这个值必须被使用,所以使用 `fmt.Println("hello, world", a)` 会移除错误。

但是全局变量是允许声明但不使用。

Expand All @@ -238,13 +239,13 @@ var a, b, c int
a, b, c = 5, 7, "abc"
```

上面这行假设了变量 a,bc 都已经被声明,否则的话应该这样使用:
上面这行假设了变量 `a``b``c` 都已经被声明,否则的话应该这样使用:

```go
a, b, c := 5, 7, "abc"
```

右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 `5`b 的值是 `7`c 的值是 `"abc"`
右边的这些值以相同的顺序赋值给左边的变量,所以 `a` 的值是 `5``b` 的值是 `7``c` 的值是 `"abc"`

这被称为 **并行****同时** 赋值。

Expand All @@ -260,9 +261,9 @@ a, b, c := 5, 7, "abc"

## 4.4.5 init 函数

变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。
变量除了可以在全局声明中初始化,也可以在 `init()` 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 `main()` 函数高。

每个源文件可以包含多个 init 函数,同一个源文件中的 init函数会按照从上到下的顺序执行,如果一个包有多个源文件包含 init 函数的话,则官方鼓励但不保证以文件名的顺序调用。初始化总是以单线程并且按照包的依赖关系顺序执行。
每个源文件可以包含多个 `init()` 函数,同一个源文件中的 `init()` 函数会按照从上到下的顺序执行,如果一个包有多个源文件包含 `init()` 函数的话,则官方鼓励但不保证以文件名的顺序调用。初始化总是以单线程并且按照包的依赖关系顺序执行。

一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。

Expand All @@ -280,9 +281,9 @@ func init() {
}
```

在它的 init 函数中计算变量 Pi 的初始值。
在它的 `init()` 函数中计算变量 `Pi` 的初始值。

示例 4.7 [user_init.go](examples/chapter_4/user_init.go) 中导入了包 trans(需要init.go目录为./trans/init.go)并且使用到了变量 Pi
示例 4.7 [user_init.go](examples/chapter_4/user_init.go) 中导入了包 `trans`(需要 `init.go` 目录为 `./trans/init.go` )并且使用到了变量 `Pi`

```go
package main
Expand All @@ -299,7 +300,7 @@ func main() {
}
```

init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 `backend()`
`init()` 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 `backend()`

```go
func init() {
Expand Down
Loading

0 comments on commit f5dae8f

Please sign in to comment.