首页 > 代码库 > Go 语言基础(七) 之 并发和网络

Go 语言基础(七) 之 并发和网络

1、goroutine
在这章中将展示 Go 使用 channel 和 goroutine 开发并行程序的能力。
goroutine 是 Go 并发能力的核心要素。但是,goroutine 到底 是什么?
叫做 goroutine 是因为已有的短语——线程、协程、进程等等——传 递了不准确的含义。
goroutine 有简单的模型:它是与其他 goroutine 并行执行的,
有着相同地址空间的函数。。它是轻量的,仅比分配 栈空间多一点点 。
而初始时栈是很小的,所以它们也是廉价的,并且随着需要在堆空间上分配(和释放)。
 
goroutine 是一个普通的函数,只是需要使用保留字 go 作为开头。
ready("Tea", 2) ← 普通函数调用
go ready("Tea", 2) ← ready() 作为 goroutine 运行
 
Go routine 实践
func ready(w string, sec int) {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready!")
}
func main() {
go ready("Tea", 2)
go ready("Coffee", 1)
fmt.Println("I‘m waiting")
time.Sleep(5 * time.Second)
}
输出:
I‘m waiting ← 立刻
Coffee is ready! ← 1 秒后
Tea is ready! ← 2 秒后
 
2、用 channel
如果不等待 goroutine 的执行(例如,移除 I’m waiting 行),
程序立刻终止,而任何正在执行的 goroutine 都会停止。
为了修复这个,需要一些能够同 goroutine 通讯的机制。
这一机制通过 channels 的形式使用。
channel 可以与 Unix sehll 中的双向管道做类比:可以通过它发送或者接收值。
这些值只能是特定的类型:channel 类型。定义一个 channel 时,
也需要定义发送到 channel 的值的类型。
注意,必须使用 make 创建 channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
创建 channel ci 用于发送和接收整数,创建 channel cs 用于字符串,
以及 channel cf 使用了空接口来满足各种类型。
向 channel 发送或接收数据,是通过类似的 操作符完成的:<-.
具体作用则依赖于操作符的位置:
ci <- 1 ←发送整数1 到channelci
<-ci ← 从 channel ci 接收整数
i := <-ci ← 从 channel ci 接收整数,并保存到 i 中
 
将这些放到实例中去:
 
// 定义 c 作为 int 型的 channel。就是说:这个 channel 传输整数。
// 注意这个变量是全局的,这样 goroutine 可以访问它;
var c chan int
func ready(w string, sec int) {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready!")
c <- 1 // 发送整数1 到 channel c;
}
func main() {
c = make(chan int) // 初始化c
go ready("Tea",2) // 用保留字go 开始一个 goroutine
go ready("Coffee", 1)
fmt.Println("I‘m waiting, but not too long")
<-c // 等待,直到从 channel 上接收一个值。注意,收到的值被丢弃了;
<-c // 两个 goroutines,接收两个值。
}
 
如果不知道有启动了多少个 goroutine 怎么办呢?
这里有另一个 Go 内建的保留字:select。
通过 select(和其他东西)可以监听 channel 上输入的数据。
 
在这个程序中使用 select,并不会让它变得更短,因为运行的 goroutine 太少 了。
移除两行 <-c,并用下面的内容替换它们:
使用 select
L: for {
select {
case <-c:
i++
if i > 1 {
break L
}
}
}
现在将会一直等待下去。
只有当从 channel c 上收到多个响应时才会退出循环 L。
 
3、并行运行
虽然 goroutine 是并发执行的,但是它们并不是并行运行的。
如果不告诉 Go 额 外的东西,同一时刻只会有一个 goroutine 执行。
利用 runtime.GOMAXPROCS(n) 可以设置 goroutine 并行执行的数量。
 
GOMAXPROCS 设置了同时运行的 CPU 的最大数量,并返回之前的设 置。
如果 n < 1,不会改变当前设置。 当调度得到改进后,这将被移 除。
 
当在 Go 中用 ch := make(chan bool) 创建 chennel 时,bool 型的 无缓冲 channel 会被创建。
这对于程序来说意味着什么呢?首先,如果读取(value := <-ch)它 将会被阻塞,直到有数据接收。
其次,任何发送(ch<-5)将会被阻塞,直到数 据被读出。
无缓冲 channel 是在多个 goroutine 之间同步很棒的工具。
不过 Go 也允许指定 channel 的缓冲大小,很简单,就是 channel 可以存储多少 元素。
ch := make(chan bool, 4),创建了可以存储 4 个元素的 bool 型 channel。
在这个 channel 中,前 4 个元素可以无阻塞的写入。
当写入第 5 元素时,代码 将会阻塞,直到其他 goroutine 从 channel 中读取一些元素,腾出空间。
 
 
4、关闭 channel
当 channel 被关闭后,读取端需要知道这个事情。下面的代码演示了如何检查
channel 是否被关系。
x, ok = <-ch
当 ok 被赋值为 true 意味着 channel 尚未被关闭,同时 可以读取数据。
否则 ok 被 赋值为 false。在这个情况下表示 channel 被关闭。
 
 
十七、通讯
了解文件、目录、网络通 讯和运行其他程序。
Go 的 I/O 核心是接口 io.Reader 和 io.Writer。
在 Go 中,从文件读取(或写入)是非常容易的,即使用os包
 
1、从文件读取(无缓冲)
package main
import "os"
func main() {
buf := make([]byte, 1024)
f,_ := os.Open("/etc/passwd") // 打开文件,os.Open 返回一个实现了 io.Reader 和 io.Writer 的 *os.File;
defer f.Close() // 确保关闭了f;
for {
n,_ := f.Read(buf) // 一次读取1024 字节
if n==0 {break} // 到达文件末尾
os.Stdout.Write(buf[:n]) // 将内容写入os.Stdout
}
}
 
 
2、从文件读取(缓冲,用bufio包)
package main
import ( "os"; "bufio")
func main() {
buf := make([]byte, 1024)
f,_ := os.Open("/etc/passwd") // 打开文件
defer f.Close()
// 转换 f 为有缓冲的 Reader。NewReader 需要一个 io.Reader,
// 因此或许你认为这会出错。但其实不会。任何有 Read() 函数就实现了这个接口。
// 同时, 从上例可以看到,*os.File 已经这样做了;
r := bufio.NewReader(f)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
for {
n, _ := r.Read(buf) // 从 Reader 读取,而向 Writer 写入,然后向屏幕输出文件。
if n == 0 { break }
w.Write(buf[0:n])
}
}
 
3、io.Reader
io.Reader 接口对于 Go 语言来说非常重要。
许多(如果不是全 部的话)函数需要通过 io.Reader 读取一些数据作为输入
 
4、一行一行读取文件
f,_ := os.Open("/etc/passwd");
defer f.Close()
r := bufio.NewReader(f) ← 使其成为一个 bufio,以便访问 ReadString 方法
s, ok := r.ReadString(‘\n‘) { ← 从输入中读取一行
// ... | ← s 保存了字符串,通过 string 包就可以解析它|
 
 
5、命令行参数
一个 DNS 查询工 具:
// 定义 bool 标识,-dnssec。变量必须是指针,否则 package 无法设置其值
dnssec := flag.Bool("dnssec",false,"RequestDNSSECrecords")
port := flag.String("port", "53", "Set the query port") // 类似的,port 选项;
flag.Usage = func() { // 简单的重定义 Usage 函数
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] [name ...]\n", os.Args[0])
flag.PrintDefaults() // 指定的每个标识,PrintDefaults 将输出帮助信息;
}
flag.Parse() // 解析标识,并填充变量。
当参数被解析之后,就可以使用它们:
if *dnssec { ← 定义传入参数 dnssec
// 做点
}
 
6、执行命令
os/exec 包有函数可以执行外部命令,这也是在 Go 中主要的执行命令的方法。
通过定义一个有着数个方法的 *exec.Cmd 结构来使用。
执行 ls -l:
import "os/exec"
cmd := exec.Command("/bin/ls", "-l")
err := cmd.Run()
上面的例子运行了 “ls -l”,但是没有对其返回的数据进行任何处理
 
通过如下 方法从命令行的标准输出中获得信息:
import "exec"
cmd := exec.Command("/bin/ls", "-l")
buf, err := cmd.Output() ← buf 是一个 []byte
 
 
7、网络
所有网络相关的类型和函数可以在 net 包中找到。这其中最重要的函数是 Dial。
当 Dial 到远程系统,这个函数返回 Conn 接口类型,可以用于发送或接收信息。
函数 Dial 简洁的抽象了网络层和传输层。
因此 IPv4 或者 IPv6,TCP 或者 UDP 可 以共用一个接口。
 
通过 TCP 连接到远程系统(端口 80),然后是 UDP,最后是 TCP 通过 IPv6,
大致 是这样:
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") ← 方括号是强制的
如果没有错误(由 e 返回),就可以使用 conn 从套接字中读写。
在包 net 中的原 始定义是:
// Read reads data from the connection.
Read(b []byte)(n int, err error)
这使得 conn 成为了 io.Reader。
// Write writes data to the connection.
Write(b []byte)(n int, err error)
这同样使得 conn 成为了 io.Writer,
事实上 conn 是 io.ReadWriter。
但是这些都是隐含的低层
 
通常总是应该使用更高层次的包:http
一个简单的 http Get 例子
package main
import("io/ioutil";"net/http";"fmt") // 需要的导入
func main() {
r,err := http.Get("http://www.google.com/robots.txt") // 使用http.Get获取html
if err != nil { fmt.Printf("%s\n",err.String()); return } // 错误处理
b, err := ioutil.ReadAll(r.Body) // 将整个内容读入b
r.Body.Close()
if err == nil { fmt.Printf("%s",string(b)) } // 如果一切ok,打印内容
}

Go 语言基础(七) 之 并发和网络