Skip to content

Commit

Permalink
第六章修改 (unknwon#843)
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 f5dae8f commit c42d8a3
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 99 deletions.
2 changes: 1 addition & 1 deletion eBook/06.0.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 6.0 函数function
# 6.0 函数 (function)

函数是 Go 里面的基本代码块:Go 函数的功能非常强大,以至于被认为拥有函数式编程语言的多种特性。在这一章,我们将对 [第 4.2.2 节](04.2.md) 所简要描述的函数进行详细的讲解。

Expand Down
30 changes: 15 additions & 15 deletions eBook/06.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可

编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。

(事实上,好的程序是非常注意DRY原则的,即不要重复你自己Don't Repeat Yourself,意思是执行特定任务的代码只能在程序里面出现一次。)
(事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己 (Don't Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。)

当函数执行到代码块最后一行(`}` 之前)或者 `return` 语句的时候会退出,其中 `return` 语句可以带有零个或多个参数;这些参数将作为返回值(参考 [第 6.2 节](06.2.md))供调用者使用。简单的 `return ` 语句也可以用来结束 for 死循环,或者结束一个协程goroutine
当函数执行到代码块最后一行(`}` 之前)或者 `return` 语句的时候会退出,其中 `return` 语句可以带有零个或多个参数;这些参数将作为返回值(参考 [第 6.2 节](06.2.md))供调用者使用。简单的 `return` 语句也可以用来结束 `for` 死循环,或者结束一个协程 (goroutine)

Go 里面有三种类型的函数:

- 普通的带有名字的函数
- 匿名函数或者lambda函数(参考 [第 6.8 节](06.8.md)
- 方法(Methods,参考 [第 10.6 节](10.6.md)

除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
除了 `main()``init()` 函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。

作为提醒,提前介绍一个语法:

Expand All @@ -27,21 +27,21 @@ func g()
{
}
```

它必须是这样的:

```go
func g() {
}
```

函数被调用的基本格式如下:

```go
pack1.Function(arg1, arg2, …, argn)
```

`Function``pack1` 包里面的一个函数,括号里的是被调用函数的实参argument:这些值被传递给被调用函数的*形参*(parameter,参考 [第 6.2 节](06.2.md))。函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数calling function。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。
`Function``pack1` 包里面的一个函数,括号里的是被调用函数的实参 (argument):这些值被传递给被调用函数的*形参*(parameter,参考[第 6.2 节](06.2.md))。函数被调用的时候,这些实参将被复制(简单而言)然后传递给被调用函数。函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数 (calling function)。函数能多次调用其他函数,这些被调用函数按顺序(简单而言)执行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。

一个简单的函数调用其他函数的例子:

Expand All @@ -60,21 +60,21 @@ func greeting() {
println("In greeting: Hi!!!!!")
}
```

代码输出:

In main before calling greeting
In greeting: Hi!!!!!
In main after calling greeting

函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:

假设 f1 需要 3 个参数 `f1(a, b, c int)`,同时 f2 返回 3 个参数 `f2(a, b int) (int, int, int)`,就可以这样调用 f1`f1(f2(a, b))`
假设 `f1` 需要 3 个参数 `f1(a, b, c int)`,同时 `f2` 返回 3 个参数 `f2(a, b int) (int, int, int)`,就可以这样调用 `f1``f1(f2(a, b))`

函数重载function overloading指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
函数重载 (function overloading) 指的是可以编写多个同名函数,只要它们拥有不同的形参/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:

funcName redeclared in this book, previous declaration at lineno

Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名(参考 [第 11.12.5 节](11.12.md))。

如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
Expand All @@ -91,16 +91,16 @@ type binOp func(int, int) int

在这里,不需要函数体 `{}`
函数是一等值first-class value:它们可以赋值给变量,就像 `add := binOp` 一样。
函数是一等值 (first-class value):它们可以赋值给变量,就像 `add := binOp` 一样。
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值functions value之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数(参考 [第 6.8 节](06.8.md))来破除这个限制。
函数值 (functions value) 之间可以相互比较:如果它们引用的是相同的函数或者都是 `nil` 的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数(参考 [第 6.8 节](06.8.md))来破除这个限制。
目前 Go 没有泛型generic的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口interface,特别是空接口与类型选择(type switch,参考 [第 11.12 节](11.12.md))与/或者通过使用反射(reflection,参考 [第 6.8 节](06.8.md))来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
目前 Go 没有泛型 (generic) 的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口 (interface),特别是空接口与类型选择(type switch,参考 [第 11.12 节](11.12.md))与/或者通过使用反射(reflection,参考 [第 6.8 节](06.8.md))来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强。
## 链接
- [目录](directory.md)
- 上一节:[函数function](06.0.md)
- 上一节:[函数 (function)](06.0.md)
- 下一节:[函数参数与返回值](06.2.md)
4 changes: 2 additions & 2 deletions eBook/06.10.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ where()
where()
```

您也可以设置 `log` 包中的 flag 参数来实现:
您也可以设置 `log` 包中的 `flag` 参数来实现:

```go
log.SetFlags(log.Llongfile)
log.Print("")
```

或使用一个更加简短版本的 `where` 函数:
或使用一个更加简短版本的 `where()` 函数:

```go
var where = log.Print
Expand Down
2 changes: 1 addition & 1 deletion eBook/06.11.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 6.11 计算函数执行时间

有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()``Sub` 函数:
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 `time` 包中的 `Now()``Sub()` 函数:

```go
start := time.Now()
Expand Down
6 changes: 3 additions & 3 deletions eBook/06.12.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# 6.12 通过内存缓存来提升性能

当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。最明显的例子就是生成斐波那契数列的程序(详见第 6.6 和 6.11 节):
当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。最明显的例子就是生成斐波那契数列的程序(详见第 [6.6](06.6.md)[6.11](06.11.md) 节):

要计算数列中第 n 个数字,需要先得到之前两个数的值,但很明显绝大多数情况下前两个数的值都是已经计算过的。即每个更后面的数都是基于之前计算结果的重复计算,正如示例 6.11 [fibonnaci.go](examples/chapter_6/fibonacci.go) 所展示的那样。

而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置(详见第 7 章),然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。
而我们要做就是将第 n 个数的值存在数组中索引为 n 的位置(详见[ 7 章](07.0.md)),然后在数组中查找是否已经计算过,如果没有找到,则再进行计算。

程序 Listing 6.17 - [fibonacci_memoization.go](examples/chapter_6/fibonacci_memoization.go) 就是依照这个原则实现的,下面是计算到第 40 位数字的性能对比:

- 普通写法:4.730270 秒
- 内存缓存:0.001000 秒

内存缓存的优势显而易见,而且您还可以将它应用到其它类型的计算中,例如使用 map(详见第 7 章)而不是数组或切片(Listing 6.21 - [fibonacci_memoization.go](examples/chapter_6/fibonacci_memoization.go)):
内存缓存的优势显而易见,而且您还可以将它应用到其它类型的计算中,例如使用 `map`(详见[ 7 章](07.0.md))而不是数组或切片(Listing 6.21 - [fibonacci_memoization.go](examples/chapter_6/fibonacci_memoization.go)):

```go
package main
Expand Down
46 changes: 23 additions & 23 deletions eBook/06.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行(参考 [第 5.2 节](05.2.md))提供了方便。

我们通过 `return` 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 `return ``panic`(参考 [第 13 章](13.0.md))结尾。
我们通过 `return` 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 `return``panic`(参考 [第 13 章](13.0.md))结尾。

在函数块里面,`return` 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支code-path都要有 `return` 语句。
在函数块里面,`return` 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支 (code-path) 都要有 `return` 语句。

问题 6.1:下面的函数将不会被编译,为什么呢?大家可以试着纠正过来。

Expand All @@ -22,19 +22,19 @@ func (st *Stack) Pop() int {

函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:`func f(int, int, float64)`

没有参数的函数通常被称为 **niladic** 函数niladic function,就像 `main.main()`
没有参数的函数通常被称为 **niladic** 函数 (niladic function),就像 `main.main()`

## 6.2.1 按值传递call by value 按引用传递call by reference
## 6.2.1 按值传递 (call by value) 按引用传递 (call by reference)

Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 `Function(arg1)`

如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 `Function(&arg1)`,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(**译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。**
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加 `&` 符号,比如 `&variable`)传递给函数,这就是按引用传递,比如 `Function(&arg1)`,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(**译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。**

几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。

在函数调用时,像切片slice、字典map、接口interface、通道channel这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
在函数调用时,像切片 (slice)、字典 (map)、接口 (interface)、通道 (channel) 这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。

有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用(side-effect),就像输出文本到终端,发送一个邮件或者是记录一个错误等。
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用 (side-effect),就像输出文本到终端,发送一个邮件或者是记录一个错误等。

但是绝大部分的函数还是带有返回值的。

Expand All @@ -59,11 +59,11 @@ func MultiPly3Nums(a int, b int, c int) int {
return a * b * c
}
```

输出显示:

Multiply 2 * 5 * 6 = 60

如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。

问题 6.2:
Expand All @@ -77,14 +77,14 @@ func MultiPly3Nums(a int, b int, c int) int {
(B) func DoSomething(a A) {
b = &a
}
## 6.2.2 命名的返回值(named return variables)

如下,multiple_return.go 里的函数带有一个 `int` 参数,返回两个 `int` 值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。
## 6.2.2 命名的返回值 (named return variables)

如下 multiple_return.go 里的函数带有一个 `int` 参数,返回两个 `int` 值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。

`getX2AndX3``getX2AndX3_2` 两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 `()` 把它们括起来,比如 `(int, int)`

命名返回值作为结果形参result parameters被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 `return` 语句。需要注意的是,即使只有一个命名返回值,也需要使用 `()` 括起来(参考 [第 6.6 节](06.6.md)fibonacci.go 函数)。
命名返回值作为结果形参 (result parameters) 被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 `return` 语句。需要注意的是,即使只有一个命名返回值,也需要使用 `()` 括起来(参考[第 6.6 节](06.6.md)[fibonacci.go](.\examples\chapter_6\fibonacci.go) 函数)。

示例 6.3 [multiple_return.go](examples/chapter_6/multiple_return.go)

Expand Down Expand Up @@ -118,7 +118,7 @@ func getX2AndX3_2(input int) (x2 int, x3 int) {
return
}
```

输出结果:

num = 10, 2x num = 20, 3x num = 30
Expand All @@ -142,9 +142,9 @@ func getX2AndX3_2(input int) (x2 int, x3 int) {

练习 6.2 [error_returnval.go](exercises/chapter_6/error_returnval.go)

编写一个名字为 MySqrt 的函数,计算一个 float64 类型浮点数的平方根,如果参数是一个负数的话将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。
编写一个名字为 `MySqrt()` 的函数,计算一个 `float64` 类型浮点数的平方根,如果参数是一个负数的话将返回一个错误。编写两个版本,一个是非命名返回值,一个是命名返回值。

## 6.2.3 空白符blank identifier
## 6.2.3 空白符 (blank identifier)

空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。

Expand All @@ -168,12 +168,12 @@ func ThreeValues() (int, int, float32) {
return 5, 6, 7.5
}
```

输出结果:

The int: 5, the float: 7.500000
另外一个示例,函数接收两个参数,比较它们的大小,然后按小-大的顺序返回这两个数,示例代码为minmax.go。

另外一个示例,函数接收两个参数,比较它们的大小,然后按小-大的顺序返回这两个数,示例代码为 minmax.go。

示例 6.5 [minmax.go](examples/chapter_6/minmax.go)

Expand All @@ -199,12 +199,12 @@ func MinMax(a int, b int) (min int, max int) {
return
}
```

输出结果:

Minimum is: 65, Maximum is 78
## 6.2.4 改变外部变量outside variable

## 6.2.4 改变外部变量 (outside variable)

传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 `return` 返回。如下的例子,`reply` 是一个指向 `int` 变量的指针,通过这个指针,我们在函数内修改了这个 `int` 变量的数值。

Expand All @@ -229,7 +229,7 @@ func main() {
fmt.Println("Multiply:", *reply) // Multiply: 50
}
```

这仅仅是个指导性的例子,当需要在函数内改变一个占用内存比较大的变量时,性能优势就更加明显了。然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。

## 链接
Expand Down
Loading

0 comments on commit c42d8a3

Please sign in to comment.