golang入门(5)

Go 并发

叫做 goroutine 是因为已有的短语——线程、协程、进程等等——传递了不准确的含义。goroutine 有简单的模型:它是与其他 goroutine

并行执行的,有着相同地址空间的函数。它是轻量的,仅比分配栈空间多一点点消耗。而初始时栈是很小的,所以它们也是廉价的,并且随着需要在堆空间上分配(和释放)。

goroutine 是一个普通的函数,只是需要使用保留字 go 作为开头。
ready(“Tea”, 2) //普通函数调用
go ready(“Tea”, 2) //ready() 作为 goroutine 运行
让一个函数作为两个 goroutine 执行,goroutine 等待一段时间,然后打印一些内容到屏幕。在第 14 和 15 行,启动了 goroutine。 main
函数等待足够的长的时间,这样每个 goroutine 会打印各自的文本到屏幕。现在是在第 17 行等待 5 秒钟,但实际上没有任何办法知道,当所有 goroutine 都已经退出应当等待多久。
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 秒后
//过5秒之后程序退出
如果不等待 goroutine 的执行(例如,移除第 17 行),程序立刻终止,而任何正在执行的 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 用于字符串,以及 channelcf 使用了空接口来满足各种类型。向 channel 发送或接收数据,是通过类似的操作符完成的: <-. 具体作用则依赖于操作符的位置:
ci <- 1 //发送整数 1 到 channel ci
<-ci //从 channel ci 接收整数
i := <-ci //从 channel ci 接收整数,并保存到 i 中
看一个实例:
var c chan int
func ready(w string, sec int) {
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, “is ready!”)
c <- 1
}
func main() {
c = make(chan int)
go ready(“Tea”, 2)
go ready(“Coffee”, 1)
fmt.Println(“I’m waiting, but not too long”)
<-c
<-c
}
1.定义 c 作为 int 型的 channel。就是说:这个 channel 传输整数。注意这个变量是全局的,这样 goroutine 可以访问它;
2.发送整数 1 到 channel c
3.初始化 c;
4.用保留字 go 开始一个 goroutine;
5.等待,直到从 channel 上接收一个值。注意,收到的值被丢弃了;
6.两个 goroutines,接收两个值。

这里仍然有一些丑陋的东西;不得不从 channel 中读取两次(第 14 和 15 行)。在这个例子中没问题,但是如果不知道有启动了多少个 goroutine 怎么办呢?这里有另一个 Go 内建的保留字:select。通过 select(和其他东西)可以监听channel 上输入的数据。

在这个程序中使用 select,并不会让它变得更短,因为运行的 goroutine 太少了。移除第 14 和 15 行,并用下面的内容替换它们:
L: for {
select {
case <-c:
i++
if i > 1 {
break L
}
}
}
现在将会一直等待下去。只有当从 channel c 上收到多个响应时才会退出循环L。使其并行运行
虽然 goroutine 是并发执行的,但是它们并不是并行运行的。如果不告诉 Go 额外的东西,同一时刻只会有一个 goroutine 执行。利用 runtime.GOMAXPROCS(n) 可以设置 goroutine 并行执行的数量。来自文档:
GOMAXPROCS 设置了同时运行的 CPU 的最大数量,并返回之前的设置。如果 n < 1,不会改变当前设置。 当调度得到改进后,这将被移
除。

更多关于 channel
当在 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 中读取一些元素,腾出空间。一句话说,在 Go 中下面的为 true:
ch := make(chan type, value)
value == 0 ! 无缓冲
value > 0 ! 缓冲 value 的元素
关闭 channel:
当 channel 被关闭后,读取端需要知道这个事情。
x, ok = <-ch
当 ok 被赋值为 true 意味着 channel 尚未被关闭,同时 可以读取数据。否则 ok 被赋值为 false。在这个情况下表示 channel 被关闭。

通讯
在这章中将介绍 Go 中与外部通讯的通讯模块。将会了解文件、目录、网络通讯和运行其他程序。Go 的 I/O 核心是接口 io.Reader 和 io.Writer。
在 Go 中,从文件读取(或写入)是非常容易的。程序只需要使用 os 包就可以从文件 /etc/passwd 中读取数据。
package main
import “os”
func main() {
buf := make([]byte, 1024)
f, _ := os.Open(“/etc/passwd”)
defer f.Close()
for {
n, _ := f.Read(buf)
if n == 0 { break }
os.Stdout.Write(buf[:n])
}
}
1.打开文件,os.Open 返回一个实现了 io.Reader 和 io.Writer 的 *os.File;
2.确保关闭了 f
3.一次读取 1024 字节
4.到达文件末尾
5.将内容写入 os.Stdout

如果想要使用缓冲 IO,则有 bufio 包:
package main
import ( “os”; “bufio”)
func main() {
buf := make([]byte, 1024)
f, _ := os.Open(“/etc/passwd”)
defer f.Close()
r := bufio.NewReader(f)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
for {
n, _ := r.Read(buf)
if n == 0 { break }
w.Write(buf[0:n])
}
}

1.打开文件;
2.转换 f 为有缓冲的 Reader。NewReader 需要一个 io.Reader,因此或许你认为这会出错。但其实不会。任何有 Read() 函数就实现了这个接口。同时,从列表8.1可以看到,*os.File 已经这样做了;
3.从 Reader 读取,而向 Writer 写入,然后向屏幕输出文件。

io.Reader
在前面已经提到 io.Reader 接口对于 Go 语言来说非常重要。许多(如果不是全部的话)函数需要通过 io.Reader 读取一些数据作为输入。为了满足这个接口,只需要实现一个方法: Read(p []byte) (n int, err error)。写入则是(你可能已经猜到了)实现了 Write 方法的 io.Writer。如果你让自己的程序或者包中的类型实现了 io.Reader 或者 io.Writer 接口,那么整个 Go 标准库都可以使用这个类型!

前面的程序将整个文件读出,但是通常情况下会希望一行一行的读取。下面的片段展示了如何实现:

f, _ := os.Open(“/etc/passwd”)
defer f.Close()
r := bufio.NewReader(f) //使其成为一个 bufio,以便访问 ReadString 方法
s, ok := r.ReadString(‘\n’) //从输入中读取一行
// … | s 保存了字符串,通过 string 包就可以解析它|
这两个例子的相似之处展示了 Go 拥有的 “脚本” 化特性,例如,用 Go 编写程序感觉上类似使用动态语言(Python、Ruby、Perl 或者 PHP)。

命令行参数
来自命令行的参数在程序中通过字符串 slice os.Args 获取,导入包 os 即可。 flag包有着精巧的接口,同样提供了解析标识的方法。这个例子是一个 DNS 查询工具:
dnssec := flag.Bool(“dnssec”, false, “Request DNSSEC records”)
port := flag.String(“port”, “53”, “Set the query port”)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, “Usage: %s [OPTIONS] [name …]\n”, os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
1.定义 bool 标识,-dnssec。变量必须是指针,否则 package 无法设置其值;

  1. 类似的,port 选项;
  2. 简单的重定义 Usage 函数,有点罗嗦;
  3. 指定的每个标识,PrintDefaults 将输出帮助信息;
  4. 解析标识,并填充变量。

当参数被解析之后,就可以使用它们:
if *dnssec { 定义传入参数 dnssec
//做点事情
}

执行命令
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

网络
所有网络相关的类型和函数可以在 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”) //方括号是强制的
一个简单的 http Get 作为例子:
import ( “io/ioutil”; “net/http”; “fmt” )
func main() {
r, err := http.Get(“http://www.google.com/robots.txt")
if err != nil { fmt.Printf(“%s\n”, err.String()); return }
b, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err == nil { fmt.Printf(“%s”, string(b)) }
}
1.需要的导入;
2.使用 http 的 Get 获取 html;
3.错误处理;
4.将整个内容读入 b;
5.如果一切 OK 的话,打印内容。

附件:http://t.cn/z8ymHY3