标签 博客 下的文章

Go语言标准库概览

本文翻译自Dr.Dobb's的"A Brief Tour of the Go Standard Library"一文。

Go语言五周系列教程的最后一部分中,我们将带领大家一起来浏览一下Go语言丰富的标准库。

Go标准库包含了大量包,提供了丰富广泛的功能特性。这里提供了概览仅仅是有选择性的且非常简单。本文发表后,标准库的内容还可能继续增加,因此 建议大家最好是通过在线查阅库API或使用godoc(包含在Go发布包中)来获取最新信息以及全面了解每个包所具备的功能。

exp包(试验性的)是那些未来可能被加入标准库的包起步的地方,因此除非你想参加这些包的开发(通过测试、讨论、提交补丁),否则不应该使用其 下面的包。exp包通常只存在于从Google Go源码树上签出的源码包中,但一般不会包含在预构建好的包中。其他包可以放心使用,虽然在写下本文的这一刻,很多包依旧不够完整。

Archive(归档)和Compression(压缩)包

Go支持读写tarball和.zip文件。与此相关的包为archive/tar和archive/zip;以及用于压缩tarball的 compress/gzip和compress/bzip2。

Go同样也支持其他压缩格式;例如用于TIFF图像和PDF文件的Lempel-Ziv-Welch (compress/lzw)格式。

Bytes(字节)和String(字符串)相关包

bytes和strings包中有很多相同的函数,但前者操作的是[]byte类型的值,而后者操作的是string类型的值。strings包 提供了所有最有用的功能函数,诸如查找子字符串、替换子字符串、拆分字符串、剔除字符串以及大小写变换等。strconv包提供了数字和布尔类型 与string类型相互转换的功能。

fmt包提供了大量有用的print和scan函数,它们在本系列教程的第一和第二部分已有相关介绍。

unicode包提供一些用于确定字符属性的函数,诸如判断一个字符是否是可打印的,或是否是一个数字。unicode/utf8与 unicode/utf16这两个包提供了rune(即,Unicode码点/字符)的编码和解码功能。

text/template和html/template包可以被用于创建模板,这些模板可基于填入的数据生成文本形式的输出(例如HTML)。 这里是一个小且简单的有关text/template包使用的例子。

type GiniIndex struct {
    Country string
    Index float64
}
gini := []GiniIndex{{"Japan", 54.7}, {"China", 55.0}, {"U.S.A.", 80.1}}
giniTable := template.New("giniTable")
giniTable.Parse(
'<TABLE>' +
    '{{range .}}' +
    '{{printf "<TR><TD>%s</TD><TD>%.1f%%</TD></TR>"' +
    '.Country .Index}}'+
    '{{end}}' +
    '</TABLE>')
err := giniTable.Execute(os.Stdout, gini)

输出:

<TABLE>
<TR><TD>Japan</TD><TD>54.7%</TD></TR>
<TR><TD>China</TD><TD>55.0%</TD></TR>
<TR><TD>U.S.A.</TD><TD>80.1%</TD></TR>
</TABLE>

template.New()函数用给定的名字创建了一个新的template.Template。模板名字用于识别模板,尤其是嵌入在其他模板 中时。template.Template.Parse()函数用于解析一个模板(通常从一个.html文件中),解析后模板即可用。 template.Template.Execute()函数执行这个模板,将结果输出到给定的io.Writer,并且从其第二个参数那里读取 用于生成模板的数据。在这个例子中,我将结果输出到os.Stdout,并将GiniIndex类型的gini切片作为数据传入(我将输出拆分为 多行以便让结果更加清晰)。

在模板内部,行为(action)包含在双大括号中({{和}})。{{range}} … {{end}}可用于迭代访问一个切片中的每个元素。这里我将切片中的每个GiniIndex设置为点(.);即是当前的元素。我们可以通过在名字访问导 出字段,当然名字前面需要用.来指明当前元素。{{printf}}的行为与fmt.Printf()函数类似,但用空格替换括号以及用于分隔参 数的逗号。

text/template和html/template包自身支持一种复杂的模板语言,包括许多action,迭代和条件分支,支持变量和方法 调用,以及其它一些。除此之外,html/templage包还对代码注入免疫。

集合包

切片是Go语言提供了最高效的集合类型,但有些时候使用一个更为特定的集合类型更有用或有必要。在多数情况下,内置的map类型已经足够了,但Go标准库还是提供了container包,其中包含了各种不同的集合包。

container/heap包提供了操作heap(堆)的函数,这里heap必须是一个自定义类型的值,该类型必须满足定义在heap包中 heap.Interface。一个heap(严格地说是一个min-heap)按特定顺序维护其中的值 – 即第一个元素总是heap中最小的(对于max-heap,应该是最大的)- 这就是熟知的heap属性。heap.Interface中嵌入了sort.Interface以及Push()和Pop方法。

我们可以很容易地创建一个满足heap.Interface的自定义heap类型。下面是一个正在使用的heap的例子:

ints := &IntHeap{5, 1, 6, 7, 9, 8, 2, 4}
heap.Init(ints) // Heapify
ints.Push(9) // IntHeap.Push() doesn't preserve the heap property
ints.Push(7)
ints.Push(3)
heap.Init(ints) // Must reheapify after heap-breaking changes
for ints.Len() > 0 {
    fmt.Printf("%v ", heap.Pop(ints))
}
fmt.Println() // prints: 1 2 3 4 5 6 7 7 8 9 9

下面是完整的自定义heap实现。

type IntHeap []int

func (ints *IntHeap) Less(i, j int) bool {
    return (*ints)[i] < (*ints)[j]
}

func (ints *IntHeap) Swap(i, j int) {
    (*ints)[i], (*ints)[j] = (*ints)[j], (*ints)[i]
}

func (ints *IntHeap) Len() int {
    return len(*ints)
}

func (ints *IntHeap) Pop() interface{} {
    x := (*ints)[ints.Len()-1]
    *ints = (*ints)[:ints.Len()-1]
    return x
}

func (ints *IntHeap) Push(x interface{}) {
    *ints = append(*ints, x.(int))
}

对于多数情况这个实现都足以应付了。我们可以将IntHeap类型换为type IntHeap struct { ints []int},这样代码更漂亮,我们可以在方法内部使用ints.ints,而不再是*ints了。

container/list包提供了双向链表。元素以interface{}类型的值加入链表。从链表中获取的元素的类型为list.Element,其原始值可通过list.Element.Value访问到。

items := list.New()
for _, x := range strings.Split("ABCDEFGH", "") {
    items.PushFront(x)
}
items.PushBack(9)
for element := items.Front(); element != nil;
    element = element.Next() {
    switch value := element.Value.(type) {
        case string:
            fmt.Printf("%s ", value)
        case int:
            fmt.Printf("%d ", value)
    }
}
fmt.Println() // prints: H G F E D B A 9

在这里例子中,我们将8个单字母字符串推入一个新链表的前端,将一个整型数推入尾端。接下来,我们迭代访问链表中的元素并打印元素的值。我们不是真的需要 type switch,因为我们可以使用fmt.Printf(%v ", element.Value)打印元素值,但如果我们要做的不仅仅是打印的话,如果列表中包含的元素类型不同,我们将需要type switch。当然,如果所有的元素都具有同一类型,我们可以使用type assertion,例如对string类型元素,我们使用element.Value.(string)。

除了上述提到的方法之外,list.List类型还提供了其他许多方法,包括Back(),Init()(用于清理链 表),InsertAfter(),InsertBefore(),Len(),MoveToBack(),MoveToFront(),PushBackList() (用于将一个链表推入另外一个链表的尾端),以及Remove()。

标准库还提供了container/ring包,这个包实现了一个环形链表。

然而所有的集合类型都将数据存储在内存中,Go还提供了database/sql包,该包提供了一个通用的SQL数据库接口。当与真实数据库交互时,特定的数据库驱动包必须被单独安装。这些包,以及其他许多集合包都放在了Go Bashboard上。

文件,操作系统以及相关包

标准库提供了许多支持文件和目录操作以及与操作系统交互的包。在许多情况下,这些包提供了操作系统无关的抽象使得创建跨平台Go应用更为简单。

os(操作系统)包提供了与操作系统交互相关的函数,诸如改变当前工作目录,修改文件模式和所有权,获取和设置环境变量,创建和删除文件和目录等。

此外,该包还提供了创建和打开文件(os.Create()和os.Open())、获取文件属性(例如,通过os.FileInfo类型),以及在之前系列文章中我们所见过的函数。

一旦文件被打开,尤其是对于那些文本文件,通过一个buffer来访问该文件是非常常见的情况(将读取的行存入字符串而不是byte切片)。我们需要的这 个功能由bufio包提供。除了用bufio.Reader和bufio.Writer进行读写字符串外,我们还可以读(不读)rune,读(不读)单字 节,读多字节以及写rune和写单字节以及多字节。

io(input/output)包提供了大量的函数用于与io.Reader和io.Writer一起工作(这两个接口都可以被os.File类型值满 足)。例如,我们曾用过io.Copy()函数将数据从一个reader拷贝到一个writer中。这个包还包含了用于创建同步的内存管道(pipe)的函数。

io/iotuil包提供了一些非常易用的函数。其中,这个包提供的ioutil.ReadAll()函数用于读取一个io.Reader的所有数据,并 将数据放入一个[]byte中返回;ioutil.ReadFile()函数所做的事情类似,只是参数由一个io.Reader换成了一个字符串(文件 名);ioutil.TempFile()函数返回一个临时文件(一个os.File);ioutil.WriteFile()函数向一个给定名字的文件 中写入由[]byte承载的数据。

path包提供的函数用于操作Unix样式路径,例如Linux和Mac OS X路径,用于处理URL路径,git“引用”,FTP文件等。path/filepath包提供提供了与path相同的函数- 许多其他的 – 函数被设计用于提供平台中立的路径处理。这个包还提供了filepath.Walk()函数用于递归地对给定路径下的所有文件和目录进行迭代访问。

runtime包包含了许多函数和类型用于访问Go的运行时系统。这里面的大多数都是高级功能,在日常创建标准Go程序时不应该使用到这些功能。但是,一 些包中的常量可能十分有用 – 例如,字符串runtime.GOOS(其值例如,"darwin," "freebsd," "linux," 或 "windows")和字符串runtime.GOARCH(其值例如386," "amd64,"或 "arm")。runtime.GOROOT()函数返回GOROOT环境变量的值(或者如果该环境变量没有设置,返回Go构建根目 录),runtime.Version()返回Go版本(以一个字符串形式)。runtime.GOMAXPROCS()和 runtime.NumCPU()函数保证Go使用机器的所有处理器,在Go的文档中有详尽解释。

文件格式相关包

Go提供出色的文件处理功能,既可用于文本文件(使用7-bit ASCII编码或UTF-8和UTF-16 Unicode编码),也可用于二进制文件。Go提供了专门的包,用于处理JSON和XML文件以及它自己专有的快速、简洁以及方便的Go二进制格式。此 外,Go提供了csv包用于读取CSV(逗号分隔的值)文件。这个包将这些文件视为记录(每行算作一个记录),么个记录由多个(逗号分隔的)字段组成。这 个包用途非常广泛,例如,可以用它修改分隔符(从逗号改为tab或其他字符),以及其他诸如如何读写记录和字段的方面。

encoding包包含许多子包,其中的encoding/binary包我们曾用于读写二进制数据。其他包提供了针对各种格式的编解码功能 – 例如,encoding/base64包可以用于编码和解码我们日常常用的URL。

图像相关包

Go的image包提供了一些高层次的函数和类型,用于创建和持有图像数据。它还提供了一些包,可用于不同种类标准图像文件格式的编解码,例如image/jpeg和image/png。

image/draw包提供了一些基本的绘图函数。第三方的freetype包加入了更多绘图函数。freetype自身可以使用任意指定TrueType字体绘制文本,freetype/raster包可以绘制线条以及立方和二次曲线。

数学包

math/big包提供了无限大(实际受限于内存)整型数(big.Int)以及有理数(big.Rat)。math包提供了所有标准数学函数(基于float64)以及一些标准常量。math/cmplx包提供一些用于复数计算的标准函数(基于complex128)。

其他杂项包

除了这些可以被粗略分组的包外,标准库还包含了许多相对独立的包。

crypto包提供了使用MD5, SHA-1, SHA-224, SHA-256, SHA-384以及SHA-512算法的Hash(每个算法由一个包提供,例如crypto/sha512)。此外,crypto还提供了用于加密和解密 的子包,这些包使用了不同算法,诸如AES、DES等等。每个包都对应相应的名字(例如,crypto/aes和crypto/des)。

exec包用于运行外部程序。我们也可以使用os.StartProcess来完成这件事,但exec.Cmd类型用起来更加简单。

flag包提供了一个命令行解析器。它接受X11风格的命令行选项(例如,-width,非GNU风格的-w以及–width)。这个包产生一个非常基 本的usage消息并且没有提供除值类型之外的任何校验(因此,这个包可以用于指定一个int型选项,而不是用于检查接受哪些值)。还有一些候选包可以在 Go Bashboard中找到。

log包提供了一些函数,用于记录日志信息(默认输出到os.Stdout)、结束程序或抛出异常(panick)并携带一条日志信息。log包输出目标 可以使用log.SetOutput()函数变更为任何io.Writer。日志信息以一个时间戳加后续消息的格式输出;时间戳的分割符可以在调用第一个 log函数之前通过log.SetFlags(0)设置。通过log.New()函数我们还可以创建自定义的logger。

math/rand包提供许多有用的伪随机数生成函数,包括返回一个随机整型数的rand.Int()以及rand.Intn(n),后者返回[0,n]范围内的一个随机整数。crypto/rand包中有一个函数,可用于产生加密的强伪随机数字。regexp包提供快速且强大的正则式引擎,并支持RE2引擎的语法。

sort包提供了许多方便易用的函数,用于对ints、float64以及string类型的切片进行排序,并且提供基于有序切片的高效(二分查找)的查找。它还提供了用于自定义数据的通用sort.Sort()和sort.Search函数。

time包提供了用于测量时间、解析和格式化日期,日期/时间以及时间值的函数。time.After()函数可用于在特定纳秒后,向通道 (channel)发送当前时间。time.Tick()和time.NewTicker()函数可用于提供一个通道,它会返回在特定时间间隔后将 'tick'发送到该通道上。time.Time结构具有一些方法,可提供当前时间,将data/time格式化为一个字符串以及解析data /time。

网络包

Go标准库中有许多包用于支持网络以及相关方面的编程。net包提供的函数和类型可用于使用Unix域以及网络socket通信、TCP/IP和UDP编程。

这个包还提供了用于域名解析的函数。net/http包充分利用了net包,并提供了解析HTTP请求和应答的功能,并提供了一个基本的HTTP客户端。net/http包也包含一个易于扩展的HTTP server。net/url包提供了URL解析和查询转义。

标准库中还包含其他一些其他高层次的网络包。一个是net/rpc(远程过程调用)包,它允许一个服务端提供导出可被客户端调用的方法的对象。另外一个是net/smtp(简单邮件传输协议)包,可用于发送email。

Reflect包

reflect包提供了运行时反射(或称为自省);即,在运行时访问和与任意类型的值交互的能力。

这个包还提供了一些有用的工具函数,诸如reflect.DeepEqual()用于比较任意两个值 – 例如,切片,我们无法用==和!=操作符对其进行比较。

Go中的每个值都有两个属性:它的实际值与类型。reflect.TypeOf()函数可以告诉我们任意值的类型。

x := 8.6
y := float32(2.5)
fmt.Printf("var x %v = %v\n", reflect.TypeOf(x), x)
fmt.Printf("var y %v = %v\n", reflect.TypeOf(y), y)

输出

var x float64 = 8.6
var y float32 = 2.5

这里我们使用reflection输出两个浮点变量和它们的类型,类似Go的变量声明。

当将reflect.ValueOf函数用于一个值时,该函数返回一个reflect.Value,它持有值但它本身却不是那个值。如果我们想访问那个被持有的值,我们必须使用reflect.Value的一个方法。

word := "Chameleon"
value := reflect.ValueOf(word)
text := value.String()
fmt.Println(text)

输出:

Chameleon

reflect.Value类型拥有很多可以提取底层类型的方法,包括 reflect.Value.Bool(), reflect.Value.Complex(), reflect.Value.Float(), reflect.Value.Int(),以及reflect.Value.String()。

reflect包也可以与集合类型一起使用,比如切片和map,也可以与struct一起使用;它甚至可以访问结构体tag的文本(这种能力被用到了JSON和XML的编码和解码中)。

type Contact struct {
    Name string "check:len(3,40)"
    Id int "check:range(1,999999)"
}
person := Contact{"Bjork", 0xDEEDED}
personType := reflect.TypeOf(person)
if nameField, ok := personType.FieldByName("Name"); ok {
    fmt.Printf("%q %q %q\n", nameField.Type, nameField.Name, nameField.Tag)
}

输出:

"string" "Name" "check:len(3,40)"

reflect.Value持有的真实值如果是"可设置的",那么它可以被改变。是否具备可设置能力可以通过reflect.Value.CanSet()来获知,该函数返回一个布尔值。

presidents := []string{"Obama", "Bushy", "Clinton"}
sliceValue := reflect.ValueOf(presidents)
value = sliceValue.Index(1)
value.SetString("Bush")
fmt.Println(presidents)

输出:
[Obama Bush Clinton]

虽然Go的字符串是不可改变的,但给定[]string中的任意一个元素都可以被另外一个字符串所替代,这就是我们在这里所做的。(顺利成章地,在这个特定的例子中,最容易的修改方法应该是presidents[1] = "Bush",而且完全没有用到自省特性)。

你无法改变不可改变的值本身,但如果我们得到原值的地址,我们可以将原不可改变的值替换为另一个新值。

count := 1
if value = reflect.ValueOf(count); value.CanSet() {
    value.SetInt(2) // 将抛出异常,我们不能设置int
}
fmt.Print(count, " ")
value = reflect.ValueOf(&count)
// 不能在值上调用SetInt(),因为值是一个*int,而不是一个int
pointee := value.Elem()
pointee.SetInt(3) // OK. 我们可以通过值指针替换
fmt.Println(count)

输出:

1 3

这小段代码的输出表明如果条件表达式求值结果为false,其分支语句将不会被执行。虽然我们无法重新设置那些不可改变的值,诸如ints、 float64或字符串,但我们可以使用reflect.Value.Elem()方法来获取一个reflectValue,通过它我们可以重新设置该地 址上的值,这就是我们在这段代码结尾处所做的。

我们还可以使用反射来调用任意函数和方法。这里是一个例子,例子用两次调用了自定义函数TitleCase,一次是用传统的方式,一次则是用反射。

caption := "greg egan's dark integers"
title := TitleCase(caption)
fmt.Println(title)
titleFuncValue := reflect.ValueOf(TitleCase)
values := titleFuncValue.Call(
[]reflect.Value{reflect.ValueOf(caption)})
title = values[0].String()
fmt.Println(title)

输出:

Greg Egan's Dark Integers
Greg Egan's Dark Integers

reflect.Value.Call()方法接受以及返回一个类型[]reflect.Value的切片。在这个例子中,我们传入一个单一值(作为一个长度为1的切片),并获取到一个单一的结果值。

我们可以用同样的方法调用方法 – 事实上,我们甚至可以查询一个方法是否存在,并且在它确实存在的情况下再调用它。

a := list.New() // a.Len() == 0
b := list.New()
b.PushFront(1) // b.Len() == 1
c := stack.Stack{}
c.Push(0.5)
c.Push(1.5) // c.Len() == 2
d := map[string]int{"A": 1, "B": 2, "C": 3} // len(d) == 3
e := "Four" // len(e) == 4
f := []int{5, 0, 4, 1, 3} // len(f) == 5
fmt.Println(Len(a), Len(b), Len(c), Len(d), Len(e), Len(f))

输出:

0 1 2 3 4 5

这里我们创建了两个链表(使用container/list包),我们给其中一个加入一个元素。我们还创建了一个stack,并向其中加入两个元素。我们 接下来创建了一个map,一个字符串以及一个int类型切片,它们长度各不相同。我们使用Len()函数获取了它们的长度。

func Len(x interface{}) int {
    value := reflect.ValueOf(x)
    switch reflect.TypeOf(x).Kind() {
        case reflect.Array, reflect.Chan, reflect.Map,
                reflect.Slice, reflect.String:
            return value.Len()
        default:
            if method := value.MethodByName("Len"); method.IsValid() {
                values := method.Call(nil)
                return int(values[0].Int())
            }
        }
    panic(fmt.Sprintf("'%v' does not have a length", x))
}

这个函数返回传入值的长度或当值类型不支持长度概念时引发异常。

我们开始获得reflect.Value类型值,因为我们后续需要这个值。接下来我们根据reflect.Kind做switch判断。如果value的 kind是某支持内建len()函数的内建类型的话,我们可以在该值上直接调用reflect.Value.Len()函数。否则,我们要么得到一个不支 持长度概念的类型,要么是一个拥有Len()方法的类型。我们使用reflect.Value.MethodByName()方法来获取这个方法-或者获 取一个无效的reflect.Value。如果这个方法有效,我们就调用它。

这个例子用没有任何参数传入,因为传统Len()方法不接收任何参数。当我们使用reflect.Value.MethodByName()方法获取一个 方法时,返回的reflect.Value既持有方法,又持有这个value。因此当我们调用reflect.Value.Call()时,这个 value将传入并作为接收者。

reflect.Value.Int()方法返回一个int64类型值;我们这里已将其转换成一个普通的int以匹配通用的Len()函数的返回值类型。

如果一个传入的值不支持内建的len()函数并且没有Len()方法,通用的Len()将引发异常。我们本可以采用其他方式处理这个错误情况 – 例如,返回-1一表明"不支持长度",或返回一个整型值和一个错误码。

Go的reflect包十分灵活,允许我们在运行时根据程序的动态状态做一些事情。但是,这里引用Rob Pike的观点,反射是“一个强大的工具,需谨慎并尽量避免使用,除非非常必要。(Rob Pick撰写了一篇非常有趣和实用的有关Go反射的博客文章)。

结论

这篇文章给Go语言五周系列教程做了一个收尾。此时此刻,你应该对这门语言,其工具以及它的标准库有了一个很好的感性认识了。- 甚至是如何在Google App Engine上编写Go程序以运行一个Web应用。我希望你能认可这一点:Go是一门非常有趣的语言,它提供了一种编写可移植的、本地代码的愉快的方式。

Go程序设计语言(三)

本文译自Rob PikeGo语言PPT教程 – "The Go Programming Language Part3(updated June 2011)"。由于该教程的最新更新时间早于Go 1版本发布,因此该PPT中的一些内容与Go 1语言规范略有差异,到时我会在相应的地方做上注解。

第三部分大纲

  • 并发与通信
    • Goroutines
    • 通道(Channel)
    • 并发相关话题

并发与通信:Goroutines

Goroutines

术语:

对于"并发运行的事物"已经有了好多术语 – 进程、线程、协程(coroutine)、POSIX线程、NPTL线程、轻量级进程…,但这些事物都或多或少有不同。并且Go中的并发与哪种都不甚相同。

因此我们介绍一个新术语:goroutine。

定义

一个Goroutine是一个与其他goroutines运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。

它与线程、协程、进程等不同。它是一个goroutine。

注意:Concurrency与Parallelism是不同的概念。如果你不了解它们的不同,查查相关资料吧。

关于并发的问题有许多。我们后续会提及。现在就假设它能按其对外所宣称的那样正常工作吧。

启动一个Goroutine

调用一个函数或方法,然后说go:

func IsReady(what string, minutes int64) {
    time.Sleep(minutes * 60*1e9) // Unit is nanosecs.
    fmt.Println(what, "is ready")
}
go IsReady("tea", 6)
go IsReady("coffee", 2)
fmt.Println("I'm waiting…")

打印:

I'm waiting… (立即)
coffee is ready (2分钟后)
tea is ready (6分钟后)

一些简单的事实

goroutine的使用代价很低。

当从最外层函数返回,或执行到结尾处时,goroutine退出。

goroutines可以并行地在不同CPU上执行,共享内存。

你无需担心栈大小。

在gccgo中,至少目前goroutines就是pthreads。在6g中,goroutines采用基于线程的多路复用技术,因此它们的代价更低廉。

无论是上面哪个实现,栈都很小(几KB),可以根据需要增长。因此goroutines使用很少的内存。你可以创建很多goroutines,它们还可以动态拥有很大的栈。

程序员无需考虑栈大小相关话题。在Go中,这种考虑甚至不应该出现。

调度

Goroutine多路复用系统线程。当一个goroutine执行了一个阻塞的系统调用时,其他goroutine不会不阻塞。

计划后续实现CPU绑定的goroutines,不过目前用6g如果你想要用户层级别的并行,你必须设置环境变量GOMAXPROCS或调用runtime.GOMAXPROCS(n)。

GOMAXPROCS告诉运行时调度器有多少个用户空间goroutine即将同时执行,理想情况下在不同的CPU核上。

*gccgo总是为每个goroutine单独分配一个线程执行。

并发与通信:Channels

Go中的Channel

除非两个goroutine可以通信,否则它们无法协作。

Go中有一个名为channel的类型,提供通信和同步能力。

Go中还提供一些特殊的基于channel的控制结构,使得编写并发程序更加容易。

Channel类型

该类型最简单形式:
    chan elementType

通过这个类型的值,你可以发送和接收elementType类型的元素。

Channel是引用类型,这意味着如果你将一个chan变量赋值给另外一个,则这两个变量访问的是相同的channel。同样,这也意味着可以用make分配一个channel:

    var c = make(chan int)

通信操作符:<-

箭头指示数据流向。

作为一个二元操作符,<-将值从右侧发送到左侧的channel中:

c := make(chan int)
c <- 1 // 向c发送1

作为前缀一元操作符,<- 从一个channel中接收数据:

v = <-c // 从c中接收数据,赋值给v
<-c // 接收数据,丢弃
i := <-c // 接收值,用于初始化i

语义

默认情况下,通信是同步的。(我们后续将讨论异步通信)。这意味着:

1) A在一个channel上的发送操作会阻塞,直到该channel上有一个接收者就绪。
2) 在一个channel上到的接收操作会阻塞,直到该channel上有一个发送者就绪。

因此通信是同步的一种形式:两个通过channel交换数据的goroutine在通信的时刻同步。

让我们泵一些数据吧

func pump(ch chan int) {
    for i := 0; ; i++ { ch <- i }
}
ch1 := make(chan int)
go pump(ch1) // pump挂起; 我们运行
fmt.Println(<-ch1) // 打印 0

现在我们启动一个循环接收者:

func suck(ch chan int) {
    for { fmt.Println(<-ch) }
}
go suck(ch1) // 大量数字出现

你仍可以溜进去,抓取一个值:

fmt.Println(<-ch1) // 输出:3141159

返回channel的函数

在前面的例子中,pump像一个生成器,喷涌出值。但在分配channel等方面做了很多工作。让我们将其打包到一个返回channel的函数中:

func pump() chan int {
    ch := make(chan int)
    go func() {
        for i := 0; ; i++ { ch <- i }
    }()
    return ch
}
stream := pump()
fmt.Println(<-stream)// 打印 0

"返回channel的函数"是Go中的一个重要的惯用法。

到处都是返回channel的函数

我这里不再重复那些你可以从其他地方找到的知名例子。这里有些可以了解一下:

1) prime sieve: 在语言规范以及教程中。

2) Doug McIlroy的Power系列论文:http://plan9.bell-labs.com/who/rsc/thread/squint.pdf

这个程序的一个Go版本在测试套件中:http://golang.org/test/chan/powser1.go

Range和Channel

for循环的range子句接收channel作为一个操作数,在这种情况下,for循环迭代处理从channel接收到的值。我们来重写pump函数;这里是suck的重写,让它也启动一个goroutine:

func suck(ch chan int) {
    go func() {
        for v := range ch { fmt.Println(v) }
    }()
}
suck(pump()) // 现在不再阻塞

关闭一个Channel

range是如何知道何时channel上的数据传输结束了呢?发送者调用一个内置函数close:

    close(ch)

接收者使用"comma ok"测试发送者是否关闭了channel:

    val, ok:= <- ch

当结果为(value, true),说明依然有数据;一旦channel关闭,数据流干,结果将会是(zero, false)。

在一个Channel上使用Range

在一个channel上使用range,诸如:

for value := range <-ch {
    use(value)
}

等价于:

for {
    value, ok := <-ch
    if !ok {
        break
    }
    use(value)
}

Close

关键点:

只有发送者可以调用close。
只有接收者可以询问是否channel被关闭了。
只有在获取值的同时询问(避免竞争)

只有在有必要通知接收者不会再有数据的时候才调用close。

大多数情况下,不需要用close;它与关闭一个文件没有可比性。

不管怎样,channel是可以垃圾回收的。

Channel的方向性

一个channel变量的最简单形式是一个非缓冲(同步的)值,该值可以用于进行发送和接收。

一个channel类型可以被指定为只发或只收:

var recvOnly <-chan int
var sendOnly chan<- int

Channel的方向性(2)

所有Channel创建时都是双向的,但我们可以将它们赋值给带方向性的channel变量。从类型安全性角度考虑,对于函数内的实例非常有用:

func sink(ch <-chan int) {
    for { <-ch }
}
func source(ch chan<- int) {
    for { ch <- 1 }
}
c := make(chan int)//双向的
go source(c)
go sink(c)

同步的Channel

同步的Channel是非缓冲的。发送动作不会完成,直到一个接收者接收这个值。

c := make(chan int)
go func() {
    time.Sleep(60*1e9)
    x := <-c
    fmt.Println("received", x)
}()

fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)

输出:

sending 10 (立即发生)
sent 10 (60秒后,这两行出现)
received 10

异步的Channel

通过告知make缓冲中元素的数量,我们可以创建一个带缓冲的、异步的channel。

c := make(chan int, 50)
go func() {
    time.Sleep(60*1e9)
    x := <-c
    fmt.Println("received", x)
}()
fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)

输出:

sending 10 (立刻发生)
sent 10(现在)
received 10 (60秒后)

缓冲不是类型的一部分

注意缓冲的大小甚至其自身都不是channel类型的一部分,只是值的一部分。因此下面的代码虽危险,但合法:

buf = make(chan int, 1)
unbuf = make(chan int)
buf = unbuf
unbuf = buf

缓冲是一个值的属性,而不是类型的。

Select

select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是send要么是receive。

ci, cs := make(chan int), make(chan string)
select {
    case v := <-ci:
        fmt.Printf("received %d from ci\n", v)
    case v := <-cs:
        fmt.Printf("received %s from cs\n", v)
}

Select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

Select语义

快速一览:

- 每个case都必须是一个通信(可能是:=)
- 所有channel表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行;其他被忽略。
- 如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
- 否则:
    – 如果有default子句,则执行该语句。
    – 如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

随机bit生成器

幼稚但很有说明性的例子:

c := make(chan int)
go func() {
    for {
        fmt.Println(<-c)
    }
}()

for {
    select {
        case c <- 0: //没有语句,没有fallthrough
        case c <- 1:
    }
}

测试可通信性

一个通信是否可以进行,而不阻塞?一个带default字句的select可以告诉我们:

select {
    case v := <-ch:
        fmt.Println("received", v)
    default:
        fmt.Println("ch not ready for receive")
}

如果没有其他case可以运行,那default子句将被执行,因此这对于非阻塞接收是一个惯用法;非阻塞发送显然也可以这么做。

超时

一个通信可以在一个给定的时间内成功完成么?time包包含了after函数:

func After(ns int64) <-chan int64

在指定时间段之后,它向返回的channel中传递一个值(当前时间)。

在select中使用它以实现超时:

select {
case v := <-ch:
    fmt.Println("received", v)
case <-time.After(30*1e9):
    fmt.Println("timed out after 30 seconds")
}

多路复用(multiplexing)

channel是原生值,这意味着他们也能通过channel发送。这个属性使得编写一个服务类多路复用器变得十分容易,因为客户端在提交请求时可一并提供用于回复应答的channel。

chanOfChans := make(chan chan int)

或者更典型的如:

type Reply struct { … }
type Request struct {
    arg1, arg2 someType
    replyc chan *Reply
}

多路复用服务器

type request struct {
    a, b int
    replyc chan int
}

type binOp func(a, b int) int
func run(op binOp, req *request) {
    req.replyc <- op(req.a, req.b)
}

func server(op binOp, service <-chan *request) {
    for {
        req := <-service // 请求到达这里
        go run(op, req) // 不等op
    }
}

启动服务器

使用"返回channel的函数"惯用法来为一个新服务器创建一个channel:

func startServer(op binOp) chan<- *request {
    service := make(chan *request)
    go server(op, req)
    return service
}

adderChan := startServer(
    func(a, b int) int { return a + b }
)

客户端

在教程中有个例子更为详尽,但这里是一个变体:

func (r *request) String() string {
    return fmt.Sprintf("%d+%d=%d",
    r.a, r.b, <-r.replyc)
}
req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}

请求已经就绪,发送它们:

adderChan <- req1
adderChan <- req2

可以以任何顺序获得结果;r.replyc多路分解:

fmt.Println(req2, req1)

停掉

在多路复用的例子中,服务将永远运行下去。要将其干净地停掉,可通过一个channel发送信号。下面这个server具有相同的功能,但多了一个quit channel:

func server(op binOp, service <-chan *request,
            quit <-chan bool) {
    for {
        select {
            case req := <-service:
                go run(op, req) // don't wait for it
            case <-quit:
                return
        }
    }
}

启动服务器

其余代码都相似,只是多了个channel:

func startServer(op binOp) (service chan<- *request,
        quit chan<- bool) {
    service = make(chan *request)
    quit = make(chan bool)
    go server(op, service, quit)
    return service, quit
}

adderChan, quitChan := startServer(
    func(a, b int) int { return a + b }
)

停掉:客户端

只有当准备停掉服务端的时候,客户端才会受到影响:

req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}
adderChan <- req1
adderChan <- req2
fmt.Println(req2, req1)

所有都完成后,向服务器发送信号,让其退出:

quitChan <- true

package main
import ("flag"; "fmt")
var nGoroutine = flag.Int("n", 100000, "how many")
func f(left, right chan int) { left <- 1 + <-right }
func main() {
    flag.Parse()
    leftmost := make(chan int)
    var left, right chan int = nil, leftmost

    for i := 0; i < *nGoroutine; i++ {
        left, right = right, make(chan int)
        go f(left, right)
    }

    right <- 0 // bang!

    x := <-leftmost // 等待完成
    fmt.Println(x)    // 100000
}

例子:Channel作为缓存

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func server() {
    for {
        b := <-serverChan // 等待做work
        process(b) // 在缓存中处理请求
        select {
            case freeList <- b: // 如果有空间,重用缓存
            default:             // 否则,丢弃它
        }
    }
}

func client() {
    for {
        var b *Buffer
        select {
            case b = <-freeList:            // 如果就绪,抓取一个
            default: b = new(Buffer) // 否则,分配一个
        }
        load(b)// 读取下一个请求放入b中
        serverChan <- b // 将请求发给server.
    }
}

并发

并发相关话题

许多并发方面,当然,Go一直在尽力做好它们。诸如Channel发送和接收是原子的。select语句也是缜密定义和实现的等。

但goroutine在共享内存中运行,通信网络可能死锁,多线程调试器糟糕透顶等等。

接下来做什么?

Go给予你原生的

不要用你在使用C或C++或甚至是Java时的方式去编程。

channel给予你同步和通信的能力,并且使得它们很强大,但也可以很容易知道你是否可以很好的使用它们。

规则是:

    不要通过共享内存通信,相反,通过通信共享内存。

特有的通信行为保证了同步!

模型

例如,使用一个channel发送数据到一个专职服务goroutine。如果同一时刻只有一个goroutine拥有指向数据的指针,那谈不上什么并发。

这是我们极力推荐的服务端编程模型,至少是对旧的"每个客户端一个线程"的泛化。它自从20世纪80年代就开始使用了,它工作的很好。

 

内存模型

那关于同步和共享内存的令人生厌的细节在:

http://golang.org/doc/go_mem.html

但如果你遵循我们的方法,你很少需要理解那些内容。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats