forked from mikespook/Learning-Go-zh-cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
go-beyond.tex
373 lines (330 loc) · 15.4 KB
/
go-beyond.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
\epi{``Go 有指针,但是没有指针运算。你不能用指针变量遍历字符串的各个字节。''}{\textit{Go For C++
Programmers}\\{\textsc{GO AUTHORS}}}
\noindent{}
Go 有指针。然而却没有指针运算,因此它们更象是引用而不是你所知道的来自于 C 的指针。
指针非常有用。
在 Go 中调用函数的时候,得记得变量是\emph{值传递}的。
因此,为了修改一个传递\emph{入}函数的值的效率和可能性,有了指针。
通过类型作为前缀来定义一个指针 '\key{*}':
\lstinline{var p *int}。现在 \var{p} 是一个指向整数值的指针。
所有新定义的变量都被赋值为其类型的零值,而指针也一样。
一个新定义的或者没有任何指向的指针,有值 \first{nil}{nil}。
在其他语言中,这经常被叫做空(NULL)指针,在 Go 中就是 \var{nil}。
让指针指向某些内容,可以使用\first{取址操作符}{operator!address-of}
(\func{\&}),像这样:
\begin{lstlisting}[caption=指针的使用,label=src:pointers]
var p *int
fmt.Printf("%v", p) |\coderemark{打印 \var{nil}}|
var i int |\coderemark{定义一个整形变量 \var{i}}|
p = &i |\coderemark{使得 \var{p} 指向 \var{i}}|
fmt.Printf("%v", p) |\coderemark{打印出来的内容类似 \var{0x7ff96b81c000a}}|
\end{lstlisting}
从指针获取值是通过在指针变量前置'\type{*}'实现的:
\begin{lstlisting}[caption=获取指针指向的值,label=src:deref]
p = &i |\coderemark{获取 \var{i} 的地址}|
*p = 8 |\coderemark{修改 \var{i} 的值}|
fmt.Printf("%v\n", *p) |\coderemark{打印 8}|
fmt.Printf("%v\n", i) |\coderemark{同上}|
\end{lstlisting}
\label{main:pointer arithmetic}
前面已经说了,没有指针运算,所以如果这样写:
\lstinline{*p++},它表示 \lstinline{(*p)++}:首先获取指针指向的值,然后对这个值加一。
\index{operator!increment}
\footnote{参看练习 \ref{ex:pointer arithmetic}。}
\section{内存分配}
Go 同样也垃圾收集,也就是说无须担心内存分配和回收。
Go 有两个内存分配原语,\key{new} 和 \key{make}。它们应用于不同的类型,做不同的工作,
可能有些迷惑人,但是规则很简单。
下面的章节展示了在 Go 中如何处理内存分配,并且希望能够让
\first{\key{new}}{built-in!new} 和 \first{\key{make}}{built-in!make} 之间的区别更加清晰。
\subsection{用 new 分配内存}
\label{sec:allocation with new}
内建函数 \key{new} 本质上说跟其他语言中的同名函数功能一样:
\func{new(T)} 分配了零值填充的 \type{T} 类型的内存空间,并且返回其地址,
一个 \type{*T} 类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型 \type{T}
的零值。记住这点非常重要。
这意味着使用者可以用 \key{new} 创建一个数据结构的实例并且可以直接工作。
如 \func{bytes.Buffer} 的文档所述``Buffer 的零值是一个准备好了的空缓冲。''
类似的,\func{sync.Mutex} 也没有明确的构造函数或 Init 方法。取而代之,
\func{sync.Mutex} 的零值被定义为非锁定的互斥量。
零值是非常有用的。例如这样的类型定义,\pageref{sec:defining your own}
页的 "\titleref{sec:defining your own}" 内容。
\begin{lstlisting}
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
\end{lstlisting}
\type{SyncedBuffer} 的值在分配内存或定义之后立刻就可以使用。在这个片段中,
\var{p} 和 \var{v} 都可以在没有任何更进一步处理的情况下工作。
\begin{lstlisting}
p := new(SyncedBuffer) |\coderemark{Type *SyncedBuffer,已经可以使用}|
var v SyncedBuffer |\coderemark{Type SyncedBuffer,同上}|
\end{lstlisting}
\subsection{用 make 分配内存}
\label{sec:allocation with make}
回到内存分配。内建函数 \func{make(T, args)} 与 \func{new(T)} 有着不同的功能。
它\emph{只能}创建 slice,map 和 channel,并且返回一个有初始值(非零)的 \type{T} 类型,
而不是 \type{*T}。本质来讲,
导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个 slice,是一个包含指向数据(内部 array)的指针,长度和容量的三项描述符;
在这些项目被初始化之前,slice 为 \type{nil}。对于 slice,map 和 channel,
\key{make} 初始化了内部的数据结构,填充适当的值。
例如,
\lstinline{make([]int, 10, 100)}
分配了 100 个整数的数组,然后用长度 10 和容量 100 创建了 slice
结构指向数组的前 10 个元素。
区别是,\lstinline{new([]int)} 返回指向新分配的内存的指针,
而零值填充的 slice 结构是指向 \type{nil} 的 slice 值。
这个例子展示了 \key{new} 和 \key{make} 的不同。
\begin{lstlisting}
var p *[]int = new([]int) |\coderemark{分配~slice 结构内存;很少使用}|
var v []int = make([]int, 100) |\coderemark{\var{v} 指向一个新分配的有~100 个整数的数组}|
var p *[]int = new([]int) |\coderemark{不必要的复杂例子}|
*p = make([]int, 100, 100)
v := make([]int, 100) |\coderemark{更常见}|
\end{lstlisting}
务必记得 \key{make} 仅适用于 map,slice 和 channel,并且返回的不是指针。
应当用 \key{new} 获得特定的指针。
\begin{lbar}[new 分配;make 初始化]
上面的两段可以简单总结为:
\begin{itemize}
\item \lstinline{new(T)} 返回 \var{*T} 指向一个零值 \var{T}
\item \lstinline{make(T)} 返回初始化后的 \var{T}
\end{itemize}
当然 \lstinline{make} 仅适用于 slice,map 和 channel。
\end{lbar}
\subsection{构造函数与复合声明}
\label{sec:constructors and composite literals}
有时零值不能满足需求,必须要有一个用于初始化的构造函数,例如这个来自
\package{os} 包的例子。
\begin{lstlisting}
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
\end{lstlisting}
有许多冗长的内容。可以使用\first{复合声明}{literal!composite}使其更加简洁,每次只用一个表达式创建一个新的实例。
\begin{lstlisting}
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0} |\coderemark{Create a new \type{File}}|
return &f|\coderemark{返回~\var{f} 的地址}|
}
\end{lstlisting}
返回本地变量的地址没有问题;在函数返回后,相关的存储区域仍然存在。
事实上,从复合声明获取分配的实例的地址更好,因此可以最终将两行缩短到一行。
\footnote{从复合声明中获取地址,意味着告诉编译器在堆中分配空间,而不是栈中。}
\begin{lstlisting}
return &File{fd, name, nil, 0}
\end{lstlisting}
The items (called of a composite
+literal are laid out in order and must all be
所有的项目(称作 \first{字段}{fields})都必须按顺序全部写上。
然而,通过对元素用字段:值成对的标识,
初始化内容可以按任意顺序出现,并且可以省略初始化为零值的字段。因此可以这样
\begin{lstlisting}
return &File{fd: fd, name: name}
\end{lstlisting}
在特定的情况下,如果复合声明不包含任何字段,它创建特定类型的零值。
表达式 \lstinline{new(File)} 和 \lstinline|&File{}| 是等价的。
复合声明同样可以用于创建 array,slice 和 map,通过指定适当的索引和 map 键来标识字段。
在这个例子中,无论是 \var{Enone},\var{Eio} 还是 \var{Einval} 初始化都能很好的工作,只要确保它们不同就好了。
\begin{lstlisting}
ar := [...]string{Enone: "no error", Einval: "invalid argument"}
sl := []string{Enone: "no error", Einval: "invalid argument"}
ma := map[int]string {Enone: "no error", Einval: "invalid argument"}
\end{lstlisting}
\section{定义自己的类型}
\label{sec:defining your own}
自然,Go 允许定义新的类型,通过关键字
\first{\key{type}}{keyword!type} 实现:
\begin{lstlisting}
type foo int
\end{lstlisting}
创建了一个新的类型 \lstinline{foo} 作用跟 \lstinline{int} 一样。
创建更加复杂的类型需要用到 \first{\key{struct}}{keyword!struct} 关键字。
这有个在一个数据结构中记录某人的姓名(\type{string})和年龄(\type{int}),
并且使其成为一个新的类型的例子:
\lstinputlisting[label=src:struct,caption=结构体]{src/struct.go}
\needspace{5\baselineskip}
通常,\lstinline{fmt.Printf("%v\n", a)} 的输出是
\begin{display}
&\{Pete 42\}
\end{display}
这很棒!Go 知道如何打印结构。如果仅想打印某一个,或者某几个结构中的字段,
需要使用 \verb|.<field name>|。例如,仅仅打印名字:
\begin{lstlisting}
fmt.Printf("%s", a.name) |\coderemark{\%s 格式化字符串}|
\end{lstlisting}
%% add text if a is a pointer
\subsection{结构字段}
之前已经提到结构中的项目被称为\index{字段}{field}。
没有字段的结构:\lstinline|struct {}|
或者有四个\footnote{是的,四(4)个。}字段的:
\begin{lstlisting}
struct {
x, y int
A *[]int
F func()
}
\end{lstlisting}
如果省略字段的名字,可以创建\first{匿名字段}{field!anonymous},例如:
\begin{lstlisting}
struct {
T1 |\coderemark{字段名字是 \var{T1}}|
*T2 |\coderemark{字段名字是 \var{T2}}|
P.T3 |\coderemark{字段名字是 \var{T3}}|
x, y int |\coderemark{字段名字是 \var{x} 和 \var{y}}|
}
\end{lstlisting}
注意首字母大写的字段可以被导出,也就是说,在其他包中可以进行读写。
字段名以小写字母开头是当前包的私有的。包的函数定义是类似的,参阅第 \ref{chap:packages} 章了解更多细节。
\subsection{方法}
\label{sec:methods}
可以对新定义的类型创建函数以便操作,可以通过两种途径:
\begin{enumerate}
\item 创建一个函数接受这个类型的参数。
\begin{lstlisting}
func doSomething(n1 *NameAge, n2 int) { /* */ }
\end{lstlisting}
(你可能已经猜到了)这是 \first{\emph{函数调用}}{function!call}。
\item 创建一个工作在这个类型上的函数(参阅在 \ref{src:function definition}
中定义的\emph{接收方}):
\begin{lstlisting}
func (n1 *NameAge) doSomething(n2 int) { /* */ }
\end{lstlisting}
这是\first{\emph{方法调用}}{method call},可以类似这样使用:
\begin{lstlisting}
var n *NameAge
n.doSomething(2)
\end{lstlisting}
\end{enumerate}
使用函数还是方法完全是由程序员个人决定,但是若需要满足接口(参看下一章)就必须使用方法。
如果没有这样的需求,那就完全由习惯来决定是使用函数还是方法。
但是下面的内容一定要留意,引用自 \cite{go_spec}:
\begin{quote}
如果 \type{x} 可获取地址,并且 \lstinline{&x} 的方法中包含了 \func{m},
\lstinline{x.m()} 是 \mbox{\lstinline{(&x).m()}} 更短的写法。
\end{quote}
根据上面所述,这意味着下面的情况\emph{不是}错误:
\begin{lstlisting}
var n NameAge |\coderemark{不是指针}|
n.doSomething(2)
\end{lstlisting}
这里 Go 会查找 \type{NameAge} 类型的变量 \var{n} 的方法列表,
没有找到就会\emph{再}查找 \type{*NameAge} 类型的方法列表,并且将其转化为
\lstinline{(&n).doSomething(2)}。
下面的类型定义中有一些微小但是很重要的不同之处。同时可以参阅 \cite[section~``Type Declarations'']{go_spec}。
假设有:
\begin{lstlisting}
// Mutex 数据类型有两个方法,Lock 和 Unlock。
type Mutex struct { /* Mutex 字段 */ }
func (m *Mutex) Lock() { /* Lock 实现 */ }
func (m *Mutex) Unlock() { /* Unlock 实现 */ }
\end{lstlisting}
现在用两种不同的风格创建了两个数据类型。
\begin{itemize}
\item{\lstinline|type NewMutex Mutex|};
\item{\lstinline|type PrintableMutex struct{Mutex}|}.
\end{itemize}
Now \var{NewMutex} is equal to \var{Mutex}, but
it \emph{does not} have \emph{any} of the methods of \var{Mutex}. In other words
its method set is empty.
But \var{PrintableMutex} \emph{has} \first{\emph{inherited}}{methods!inherited} the
method set from \var{Mutex}. The Go term for this is \first{\emph{embedding}}{structures!embed}.
In the words of \cite{go_spec}:
\begin{quote}
\var{*PrintableMutex} 的方法集合包含了
\func{Lock} 和 \func{Unlock} 方法,被绑定到其匿名字段 \var{Mutex}。
\end{quote}
\section{转换}
\label{sec:conversions}
有时需要将一个类型转换为另一个类型。
在 Go 中可以做到,不过有一些规则。首先,将一个值转换为另一个是由操作符(看起来像函数:\func{byte()})完成的,
并且不是所有的转换都是允许的。
\begin{table}[H]
\begin{center}
\caption[Valid conversions]{合法的转换,
\lstinline{float64} 同 \lstinline{float32} 类似。注意,
为了适配表格的显示,float32 被简写为~flt32。}
\label{tab:convesion}
\input{tab/conversion.tex}
\end{center}
\end{table}
\begin{itemize}
\item{
从 \lstinline{string} 到字节或者 ruin 的 slice。
\begin{lstlisting}
mystring := "hello this is string"
\end{lstlisting}
\begin{lstlisting}
byteslice := []byte(mystring)
\end{lstlisting}
转换到 \type{byte} slice,每个 \type{byte} 保存字符串对应字节的整数值。
注意 Go 的字符串是 UTF-8 编码的,一些字符可能是1、2、3 或者 4 个字节结尾。
\begin{lstlisting}
runeslice := []rune(mystring)
\end{lstlisting}
转换到 \type{rune} slice,每个 \type{rune} 保存 Unicode 编码的指针。
字符串中的每个字符对应一个整数。
}
\item{
从字节或者整形的 slice 到 \lstinline{string}。
\begin{lstlisting}
b := []byte{'h','e','l','l','o'} // Composite
// literal
s := string(b)
i := []rune{257,1024,65}
r := string(i)
\end{lstlisting}
}
\end{itemize}
对于数值,定义了下面的转换:
\begin{itemize}
\item{将整数转换到指定的(bit)长度:
\lstinline{uint8(int)};}
\item{从浮点数到整数:
\lstinline{int(float32)}。这会截断浮点数的小数部分;}
\item{其他的类似:\lstinline{float32(int)}。}
\end{itemize}
\subsection{用户定义类型的转换}
如何在自定义类型之间进行转换?
这里创建了两个类型 \type{Foo} 和 \type{Bar},而
\lstinline{Bar} 是 \type{Foo} 的一个别名:
\begin{lstlisting}
type foo struct { int } |\coderemark{匿名字段}|
type bar foo |\coderemark{bar 是 foo 的别名}|
\end{lstlisting}
然后:
\begin{lstlisting}
var b bar = bar{1} |\coderemark{声明 \var{b} 为 \type{bar} 类型}|
var f foo = b |\coderemark{赋值 \var{b} 到 \var{f}}|
\end{lstlisting}
最后一行会引起错误:
\noindent\error{cannot use b (type bar) as type foo in assignment(不能使用 b(类型 bar)作为类型 foo 赋值)}
\noindent{}这可以通过转换来修复:
\begin{lstlisting}
var f foo = foo(b)
\end{lstlisting}
注意转换那些字段不一致的结构是相当困难的。同时注意,转换
\lstinline{b} 到 \type{int} 同样会出错;整数与有整数字段的结构并不一样。
\section{练习}
\input{ex-beyond/ex-pointer-arith.tex}
\input{ex-beyond/ex-map.tex}
\input{ex-beyond/ex-pointers.tex}
\input{ex-beyond/ex-doubly-linked-list.tex}
\input{ex-beyond/ex-cat.tex}
\input{ex-beyond/ex-pointers-method.tex}
\cleardoublepage
\section{答案}
\shipoutAnswer