forked from mikespook/Learning-Go-zh-cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgo-functions.tex
324 lines (278 loc) · 13.3 KB
/
go-functions.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
\epi{``阳光的轻抚或沉溺在新兴的编程语言中总是会让我兴奋。无需太多文字;许多事情就已经完成了。旧的程序阅读起来就像是同表达良好的研究工作者或受到良好训练的机器同事沟通一样,而不是与编译器争论。谁愿意让其成熟到发出这样的声音呢?''}{\textsc{RICHARD P. GABRIEL}}
\noindent{}函数是构建Go程序的基础部件;所遇有趣的事情都是在它其中发生的。函数的定义看起来像这样:
\input{fig/function.tex}
\showremarks
这里有两个例子,左边的函数没有返回值,右边的只是简单的将输入返回。
\begin{lstlisting}
func subroutine(in int) { return }
\end{lstlisting}
\begin{lstlisting}
func identity(in int) int { return in }
\end{lstlisting}
你可以用任意顺序定义函数。编译器在运行之前会扫描所有的文件,因此函数原型是 Go 密不可分的部分。
Go 不允许嵌套函数,不过仍然可以利用匿名函数来实现。参阅本章第 \pageref{sec:functions as values}
页 ``\titleref{sec:functions as values}'' 的内容。
递归函数的行为跟在其他语言中是一样的:
\begin{lstlisting}[caption=递归函数]
func rec(i int) {
if i == 10 {
return
}
rec(i+1)
fmt.Printf("%d ", i)
}
\end{lstlisting}
这会打印:\texttt{9 8 7 6 5 4 3 2 1 0}.
%%\newpage %% TODO don't want a newpage here actually
\section{作用域}
在Go中,定义在函数外的变量是\first{全局}{scope!local}的,而对于函数来说,那些定义在函数内部的变量是\first{局部}{scope!local}的。
如果发生重名:一个局部变量与一个全局变量有相同的名字,那么在函数执行的时候,局部变量将覆盖全局变量。
\begin{minipage}{.5\textwidth}
\input{fig/scope1.tex}
\hfill
\vfill
\end{minipage}
\hfill
\begin{minipage}{.5\textwidth}
\input{fig/scope2.tex}
\hfill
\vfill
\end{minipage}
在列表 \ref{src:scope1} 中我们定义了函数 \func{q()} 的局部变量 \var{a}。
这个局部变量 \var{a} 仅可以在 \func{q()} 中访问。
这也就是为什么代码会输出:\texttt{656}。
在列表 \ref{src:scope2} 中,没有定义新的变量,只有一个全局变量 \var{a}。
向 \var{a} 进行赋值,在全局都可以访问。这段代码将会输出:\texttt{655}。
在接下来的例子中,我们将在函数 \func{f()} 中调用 \func{g()}:
\lstinputlisting[caption=当函数调用函数时的作用域]{src/scope3.go}
输出内容将是:\texttt{565}。\emph{局部}变量\emph{仅在}执行定义它的函数内有效。
%%Finally, one can create a \first{"function literal"}{function literal} in which you essentially
%%define a function inside another
%%function, i.e. a \first{nested function}{nested function}.
%%The following figure should clarify why it prints: \texttt{565757}.
%%\input{fig/scope3.tex}
\section{多值返回}
\label{sec:multiple return}
Go 一个非常特别的特性(对于编译语言而言)是函数和方法可以返回多个值(Python
和 Perl 同样也可以)。这可以用于改进一大堆在 C 程序中糟糕的惯例用法:
结果内返回错误(例如遇到 EOF 则返回 -1)或修改输入参数。
在~Go 中,\lstinline{Write} 返回一个总数和一个错误:
“是的,你写入了一些字节,但是由于设备异常,并不是全部都写入了。”。
\package{os} 包中的 \lstinline{*File.Write} 是这样声明的:
\begin{lstlisting}
func (file *File) Write(b []byte) (n int, err error)
\end{lstlisting}
如同文档所述,它返回写入的字节数,并且当 \lstinline{n ! = len (b)} 时,返回非
\lstinline{nil} 的 \var{error}。这是 Go 中常见的方式。
由于缺乏作为原生类型的元组,所以多返回值可能是最佳的选择。
你可以精确的返回希望的值,而无须在某个域内用特定的值标识错误。
\section{命名返回值}
\label{sec:named result parameters}
Go 函数的返回值或者结果参数可以指定一个名字,并且可以像普通的变量那样使用,
这有点类似输入参数。如果它们命名,函数开始时就会用其类型的零值对其进行初始化。
如果函数在不加参数的情况下执行了~\key{return} 语句,结果参数会返回。
用这个特性,允许(再一次的)用较少的代码做更多的事\footnote{这是 Go 的一个格言:``用更\emph{少}的代码做更\emph{多}的事''}。
名字不是强制的,但是它们可以使得代码更加健壮和清晰:
\emph{这就是文档}。例如命名 \type{int} 类型的返回值为 \lstinline{nextPos},就能说明哪值个代表什么。
\begin{lstlisting}
func nextInt(b []byte, pos int) (value, nextPos int) { /* ... */ }
\end{lstlisting}
由于命名结果会被初始化并关联于无参数的 \key{return},
因此它们可以非常清晰明了。这段 \lstinline{io.ReadFull} 的代码对此运用得很好:
\begin{lstlisting}
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:len(buf)]
}
return
}
\end{lstlisting}
\section{延迟代码}
\label{sec:deferred code}
假设有一个函数,打开文件并且对其进行若干读写。在这样的函数中,经常有提前返
回的地方。如果你这样做,就需要关闭正在工作的文件描述符。这经常导致产生下面
的代码:
\begin{lstlisting}[caption=没有 defer]
func ReadWrite() bool {
file.Open("file")
// 作一些事情
if failureX {
file.Close() |\coderemark{这里 Close()}|
return false
}
if failureY {
file.Close() |\coderemark{还有这里……}|
return false
}
file.Close() |\coderemark{……和这里}|
return true
}
\end{lstlisting}
在这里有许多重复的代码。为了解决这个问题,Go 有了
\first{\key{defer}}{keyword!defer} 语句。在 \key{defer}
后指定的函数会在函数退出\emph{前}调用。
上面的代码可以被改写为下面这样。将 \func{Close} 对应的放置于
\func{Open} 后,能够使函数更加可读、健壮。
\begin{lstlisting}[caption=使用 defer]
func ReadWrite() bool {
file.Open("file")
defer file.Close() |\coderemark{\func{file.Close()} 被添加到了延迟列表}|
// Do your thing
if failureX {
return false |\coderemark{\func{Close()} 现在自动调用}|
}
if failureY {
return false |\coderemark{这里也是}|
}
return true |\coderemark{还有这里}|
}
\end{lstlisting}
可以将多个函数放入 “延迟列表”\index{deferred list}中,这个例子来自\cite{effective_go}:
\begin{lstlisting}
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
\end{lstlisting}
延迟的函数是按照后进先出(LIFO)的顺序执行,因此上面的代码打印:\lstinline{4 3 2 1 0}。
利用 \func{defer} 甚至可以修改返回值,
假设正在使用命名结果参数和函数文法\index{function!literal}
\footnote{函数文法也叫做闭包\index{closure}。},例如:
\begin{lstlisting}[caption=函数文法]
defer func() {
/* ... */
}() |\coderemark{这里的 () 是必须的}|
\end{lstlisting}
或者这个例子,更加容易了解为什么,以及在哪里需要括号:
\begin{lstlisting}[caption=带参数的函数文法]
defer func(x int) {
/* ... */
}(5) |\coderemark{为输入参数 \var{x} 赋值 5}|
\end{lstlisting}
在这个(匿名)函数中,可以访问任何命名返回参数:
\begin{lstlisting}[caption=在 defer 中访问返回值]
func f() (ret int) {|\coderemark{\var{ret} 被初始化为零}|
defer func() {
ret++|\coderemark{将 \var{ret} 加一}|
}()
return 0|\coderemark{返回的是 1 而\emph{不是} 0!}|
}
\end{lstlisting}
\section{变参}
接受不定数量的参数的函数叫做变参函数。为了使其接受变参需要进行如下定义:
\begin{lstlisting}
func myfunc(arg ...int) {}
\end{lstlisting}
\lstinline{arg ... int} 告诉 Go 这个函数接受不定数量的参数。注意,这些参数的类型全部是 \type{int}。
在函数体中,变量 \var{arg} 是一个 int 类型的 slice:
\begin{lstlisting}
for _, n := range arg { |\longremark{我们不关心 range 返回的序号,因此在这里使用下划线。}|
fmt.Printf("And the number is: %d\n", n)
}
\end{lstlisting}
\showremarks
% TODO 似乎并没有这个规则,存疑。
% If you don't specify the type of the variadic argument it defaults to the
% empty interface \var{interface\{\}} (see chapter
% \ref{chap:interfaces}).
假设有另一个变参函数叫做 \func{myfunc2},下面的例子演示了如何向其传递变参:
\begin{lstlisting}
func myfunc(arg ...int) {
myfunc2(arg...) |\coderemark{按照原样传递}|
myfunc2(arg[:2]...)|\coderemark{对其进行切片}|
}
\end{lstlisting}
\section{函数作为值}
\label{sec:functions as values}
\index{function!as values}
\index{function!literals}
就像其他在 Go 中的其他东西一样,函数也\emph{是}值。它们可以像下面这样赋值给变
量:
\lstinputlisting[label=src:anonfunc,caption=匿名函数,linerange={3,}]{src/anon-func.go}
如果使用 \lstinline{fmt.Printf("%T\n", a)} 打印 \var{a} 的类型,\func{func()}。
函数作为值也会被用在其他地方,例如 map。这里将整数转换为函数:
\begin{lstlisting}[caption=使用 map 的函数作为值]
var xs = map[int]func() int{
1: func() int { return 10 },
2: func() int { return 20 },
3: func() int { return 30 }, |\coderemark{必须有逗号}|
/* ... */
}
\end{lstlisting}
也可以编写一个接受函数作为参数的函数,例如用于操作 int 类型的 slice 的
\func{Map} 函数。这是一个留给读者的练习,在第 \pageref{ex:map function}
页,练习 Q\ref{ex:map function}。
\section{回调}
\label{sec:callbacks}
由于函数也是值,所以可以很容易的传递到其他函数里,然后可以作为回调。首先定
义一个函数,对整数做一些 “事情”:
\begin{lstlisting}
func printit(x int) {|\coderemark{什么都不返回的函数}|
fmt.Printf("%v\n", x)|\coderemark{仅打印出来}|
}
\end{lstlisting}
这个函数的标识是 \lstinline{func printit(int)},或者没有函数名的:
\mbox{\lstinline{func(int)}}。创建新的函数使用这个作为回调,
需要用到这个标识:
\begin{lstlisting}
func callback(y int, f func(int)) {|\coderemark{\func{f} 保存函数}|
f(y) |\coderemark{调用回调 \func{f} 传递参数 \var{y}}|
}
\end{lstlisting}
\section{恐慌(Panic)和恢复(Recover)}
\label{sec:panic}
Go 没有像 Java 那样的异常机制,例如你无法像在 Java 中那样抛出一个异常。作为替
代,它使用了恐慌和恢复(panic-and-recover)机制。一定要记得,这应当作为最后的
手段被使用,你的代码中应当没有,或者很少的能令人恐慌的东西。这是个强大的工具,
务必明智的使用它。那么,应该如何使用它呢?
下面的描述来自于 \cite{go_blog_panic}:
\begin{description}
\item[Panic]{
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。
当函数 \func{F} 调用 \key{panic},函数 \func{F} 的执行被中断,
并且 \func{F} 中的延迟函数会正常执行,然后 \func{F} 返回到调用它的地方。
在调用的地方,\func{F} 的行为就像调用了 \key{panic}。这一过程继续向上,
直到当前的 goroutine 返回,这时程序崩溃。
恐慌可以直接调用 \key{panic} 产生。也可以由 运行时错误 产生,
例如访问越界的数组。}
\item[Recover]{
是一个内建的函数,可以让进入令人恐慌的流程中的 goroutine 恢复过来。
recover \emph{仅}在延迟函数中有效。在正常的执行过程中,调用
\func{recover} 会返回 \type{nil} 并且没有其他任何效果。如果当前的
goroutine 陷入恐慌,调用 \func{recover} 可以捕获到 \func{panic}
的输入值,并且恢复正常的执行。}
\end{description}
这个函数检查作为其参数的函数在执行时是否会产生 panic\footnote{复制于
Eleanor McHugh 的演讲稿。}:
\begin{lstlisting}
func throwsPanic(f func()) (b bool) { |\longremark{定义一个新函数 \func{throwsPanic}
接受一个函数作为参数(参看 “\titleref{sec:functions as values}”)。
函数 \func{f} 产生 panic,就返回 true,否则返回 false;}|
defer func() { |\longremark{定义了一个利用 \func{recover} 的 \func{defer} 函数。
如果当前的 goroutine 产生了 panic,这个 defer 函数能够发现。
当 \func{recover()} 返回非 \var{nil} 值,设置 \var{b} 为 true;}|
if x := recover(); x != nil {
b = true
}
}()
f() |\longremark{调用作为参数接收的函数;}|
return |\longremark{返回 \var{b} 的值。由于 \var{b} 是命名返回值(第 \pageref{sec:named result parameters} 页),因此无须指定 \var{b}。}|
}
\end{lstlisting}
\showremarks
\section{练习}
\input{ex-functions/ex-average.tex}
\input{ex-functions/ex-order.tex}
\input{ex-functions/ex-scope.tex}
\input{ex-functions/ex-stack.tex}
\input{ex-functions/ex-vararg.tex}
\input{ex-functions/ex-fib.tex}
\input{ex-functions/ex-map.tex}
\input{ex-functions/ex-minmax.tex}
\input{ex-functions/ex-bubblesort.tex}
\input{ex-functions/ex-funcfunc.tex}
\cleardoublepage
\section{答案}
\shipoutAnswer