forked from mikespook/Learning-Go-zh-cn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgo-communication.tex
163 lines (136 loc) · 7.09 KB
/
go-communication.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
\epi{``好的沟通就像是一杯刺激的浓咖啡,然后就难以入睡。''}{\textsc{ANNE MORROW LINDBERGH}}
\noindent{}在这章中将介绍 Go 中与外部通讯的通讯模块。将会了解文件、目录、网络通讯和运行其他程序。Go 的 I/O 核心是接口 \type{io.Reader} 和 \type{io.Writer}。
在 Go 中,从文件读取(或写入)是非常容易的。程序只需要使用
\package{os} 包就可以从文件 \file{/etc/passwd} 中读取数据。
\lstinputlisting[caption=从文件读取(无缓冲),label=src:read]{src/file.go}
接下来展示了如何做到这点:
\showremarks
如果想要使用\first{缓冲}{buffered} IO,则有
\package{bufio}\index{package!bufio} 包:
\lstinputlisting[caption=从文件读取(缓冲),label=src:bufread]{src/buffile.go}
\showremarks
\section{io.Reader}
在前面已经提到 \first{io.Reader}{io.Reader} 接口对于 Go 语言来说非常重要。许多(如果不是全部的话)函数需要通过
\type{io.Reader}\index{package!io} 读取一些数据作为输入。
为了满足这个接口,只需要实现一个方法:
\func{Read(p []byte) (n int, err error)}。
写入则是(你可能已经猜到了)实现了 \func{Write} 方法的 \type{io.Writer}。
如果你让自己的程序或者包中的类型实现了 \type{io.Reader} 或者
\type{io.Writer} 接口,那么\emph{整个 Go 标准库都可以使用}这个类型!
\section{一些例子}
前面的程序将整个文件读出,但是通常情况下会希望一行一行的读取。下面的片段展示了如何实现:
\begin{lstlisting}
f, _ := os.Open("/etc/passwd"); defer f.Close()
r := bufio.NewReader(f) |\coderemark{使其成为一个 bufio,以便访问 ReadString 方法}|
s, ok := r.ReadString('\n') {|\coderemark{从输入中读取一行}|
// ... |\coderemark{\var{s} 保存了字符串,通过 \package{string} 包就可以解析它}|
\end{lstlisting}
更加通用(但是也更加复杂)的方法是~\func{ReadLine},参阅包
~\package{bufio} 的文档了解更多内容。
在~shell 脚本中通常遇到的场景是需要检查某个目录是否存在,
如果不存在,就创建一个。
\begin{minipage}{.5\textwidth}
\begin{lstlisting}[language=sh,caption={用~shell 创建一个目录}]
if [ ! -e name ]; then
mkdir name
else
# error
fi
\end{lstlisting}
\end{minipage}
\hspace{1em}
\begin{minipage}{.5\textwidth}
\begin{lstlisting}[caption={用~Go 创建一个目录}]
if f, e := os.Stat("name"); e != nil {
os.Mkdir("name", 0755)
} else {
// error
}
\end{lstlisting}
\end{minipage}
这两个例子的相似之处展示了 Go 拥有的``脚本''化特性,例如,用 Go 编写程序感觉上类似使用动态语言(Python、Ruby、Perl 或者 PHP)。
\section{命令行参数}
\label{sec:option parsing}
来自命令行的参数在程序中通过字符串 slice \var{os.Args} 获取,导入包 \package{os} 即可。
\package{flag} 包有着精巧的接口,同样提供了解析标识的方法。这个例子是一个 DNS 查询工具:
\begin{lstlisting}
dnssec := flag.Bool("dnssec", false, "Request DNSSEC records") |\longremark{定义 \texttt{bool} 标识,%%
\texttt{-dnssec}。变量必须是指针,否则 package 无法设置其值;}|
port := flag.String("port", "53", "Set the query port") |\longremark{类似的,\texttt{port} 选项;}|
flag.Usage = func() { |\longremark{简单的重定义 \func{Usage} 函数,有点罗嗦;}|
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] [name ...]\n", os.Args[0])
flag.PrintDefaults() |\longremark{指定的每个标识,\func{PrintDefaults} 将输出帮助信息;}|
}
flag.Parse() |\longremark{解析标识,并填充变量。}|
\end{lstlisting}
\showremarks
当参数被解析之后,就可以使用它们:
\begin{lstlisting}
if *dnssec { |\coderemark{定义传入参数 \var{dnssec}}|
// 做点啥
}
\end{lstlisting}
\section{执行命令}
\package{os/exec}\index{package!os/exec} 包有函数可以执行外部命令,这也是在 Go 中主要的执行命令的方法。
通过定义一个有着数个方法的 \var{*exec.Cmd} 结构来使用。
执行 \verb|ls -l|:
\begin{lstlisting}
import "os/exec"
cmd := exec.Command("/bin/ls", "-l")
err := cmd.Run()
\end{lstlisting}
上面的例子运行了``ls -l'',但是没有对其返回的数据进行任何处理,
通过如下方法从命令行的标准输出中获得信息:
\begin{lstlisting}
import "exec"
cmd := exec.Command("/bin/ls", "-l")
buf, err := cmd.Output() |\coderemark{\var{buf} 是一个 \type{[]byte}}|
\end{lstlisting}
\section{网络}
所有网络相关的类型和函数可以在 \package{net} 包中找到。这其中最重要的函数是 \func{Dial}\index{networking!Dial}。
当 \func{Dial} 到远程系统,这个函数返回 \var{Conn} 接口类型,可以用于发送或接收信息。
函数 \func{Dial} 简洁的抽象了网络层和传输层。因此 IPv4 或者 IPv6,TCP 或者 UDP 可以共用一个接口。
通过 TCP 连接到远程系统(端口 80),然后是 UDP,最后是 TCP 通过 IPv6,大致是这样
\footnote{在这个例子中,可以认为 192.0.32.10 和 2620:0:2d0:200::10 是 \url{www.example.org}。}:
\begin{lstlisting}
conn, e := Dial("tcp", "192.0.32.10:80")
conn, e := Dial("udp", "192.0.32.10:80")
conn, e := Dial("tcp", "[2620:0:2d0:200::10]:80") |\coderemark{方括号是强制的}|
\end{lstlisting}
如果没有错误(由 \var{e} 返回),就可以使用 \var{conn} 从套接字中读写。
在包 \package{net} 中的原始定义是:
\begin{quote}
// \func{Read} reads data from the connection.\\
\lstinline{Read(b []byte) (n int, err error)}
\end{quote}
这使得 \var{conn} 成为了 \type{io.Reader}。
\begin{quote}
// \func{Write} writes data to the connection.\\
\lstinline{Write(b []byte) (n int, err error)}
\end{quote}
这同样使得 \var{conn} 成为了 \type{io.Writer},事实上 \var{conn} 是 \type{io.ReadWriter}。\footnote{变量 \var{conn} 同样实现了 \func{close} 方法,这使其成为一个 \type{io.ReadWriteCloser}。}
但是这些都是隐含的低层\footnote{练习 Q\ref{ex:echo} 是关于使用这些的。},通常总是应该使用更高层次的包。
例如 \package{http} 包。一个简单的 http Get 作为例子:
\begin{lstlisting}
package main
import ( "io/ioutil"; "net/http"; "fmt" ) |\longremark{需要的导入;}|
func main() {
r, err := http.Get("http://www.google.com/robots.txt") |\longremark{使用 http 的 \func{Get} 获取 html;}|
if err != nil { fmt.Printf("%s\n", err.String()); return } |\longremark{错误处理;}|
b, err := ioutil.ReadAll(r.Body) |\longremark{将整个内容读入 \var{b};}|
r.Body.Close()
if err == nil { fmt.Printf("%s", string(b)) } |\longremark{如果一切 OK 的话,打印内容。}|
}
\end{lstlisting}
\showremarks
\section{练习}
\input{ex-communication/ex-processes.tex}
\input{ex-communication/ex-wordcount.tex}
\input{ex-communication/ex-uniq.tex}
\input{ex-communication/ex-quine.tex}
\input{ex-communication/ex-echo.tex}
\input{ex-communication/ex-numbercruncher.tex}
\input{ex-communication/ex-finger.tex}
\cleardoublepage
\section{答案}
\shipoutAnswer