不要通过共享内存来通信,而应该通过通信来共享内存

通信与共享内存

如何理解“不要通过共享内存来通信,而应该通过通信来共享内存”?
http://www.imooc.com/wenda/detail/505010

使用共享内存的话在多线程的场景下为了处理竞态,需要加锁,使用起来比较麻烦。另外使用过多的锁,容易使得程序的代码逻辑坚涩难懂,并且容易使程序死锁,死锁了以后排查问题相当困难,特别是很多锁同时存在的时候。

共享内存会涉及到多个线程同时访问修改数据的情况,那得保证数据的安全性,可见性,那就会加锁,加锁会让并行变为串行,cpu也忙于线程抢锁。不如换一种方式,把数据复制一份,每个线程有自己的,只要一个线程干完一件事其他线程不用去抢锁了,这就是一种通信方式,把共享的以通知方式交给线程,实现并发。

其实如果从分布式的角度来理解,就会比较明了了。
打比方,ab两个进程共同对同一个消息队列进行操作,那么,如果使用共享内存的话,是不是这两个进程就必须局限在同一个物理机上,那么通信的意义就大大缩小了。
如果在设计的时候,对于消息队列,只提供读写接口,而对于内部的实现你完全不用去在意,看起来消息队列就像是共享内存一样了。然而你的消息队列可以利用socket进行通信。
所以,上述这句话,不要用共享内存实现通信是指不要让程序一开始就局限在单机上,而是利用通信,也就是封装内部实现,提供接口的方式来进行相应的操作。

并行方法总结:
https://zhuanlan.zhihu.com/p/46678895
1、按并行分类

(1)阻塞(非并行):

非并行方式下,子进程串行执行(完成一个,然后开始下一个),实际不是并行。包括:

multiprocessing.Pool(),apply方法
pathos.multiprocessing.ProcessPool(),pipe方法
pathos.pp.ParallelPool(),pipe方法
pathos.pp.ParallelPool(),map方法
pathos.pp.ParallelPool(),imap方法

(2)批次并行:

批次并行指一批子进程并行执行,且直到该批次所有子进程完成后,才开始下一批次。包括:

multiprocessing.Process() #只能一批一批地添加进程,同一批次内并行

(3)异步:

异步执行指的是一批子进程并行执行,且子进程完成一个,就新开始一个,而不必等待同一批其他进程完成。包括:

multiprocessing.Pool(),apply_async方法
multiprocessing.Pool(),map方法
multiprocessing.Pool(),map_async方法
multiprocessing.Pool(),imap方法
multiprocessing.Pool(),imap_unordered方法
multiprocessing.Pool(),starmap方法
multiprocessing.Pool(),starmap_async方法
concurrent.futures.ProcessPoolExecutor(),submit方法
concurrent.futures.ProcessPoolExecutor(),map方法
pathos.multiprocessing.ProcessPool(),map方法
pathos.multiprocessing.ProcessPool(),imap方法
pathos.multiprocessing.ProcessPool(),uimap方法
pathos.multiprocessing.ProcessPool(),amap方法
pathos.multiprocessing.ProcessPool(),apipe方法
pp.Server(),submit方法
pathos.pp.ParallelPool(),apipe方法
pathos.pp.ParallelPool(),amap方法
pathos.pp.ParallelPool(),uimap方法

 

不要通过共享内存来通信,而通过通信来共享内存。

 
转载

https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.0.md

 

14 协程(goroutine)与通道(channel)

作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算,参见第 15 章)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。

不要通过共享内存来通信,而通过通信来共享内存。

通信强制协作。

链接

 

 

14.1 并发、并行和协程

14.1.1 什么是协程

一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有'正式'的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求(如 Web 服务器),或增加性能和吞吐量(例如,通过对不同的数据集并行执行代码)。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行。

并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。

公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作 竞态)。

不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。

解决之道在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。在 Go 的标准库 sync 中有一些工具用来在低级别的代码中实现加锁;我们在第 9.3 节中讨论过这个问题。不过过去的软件开发经验告诉我们这会带来更高的复杂度,更容易使代码出错以及更低的性能,所以这个经典的方法明显不再适合现代多核/多处理器编程:thread-per-connection模型不够有效。

Go 更倾向于其他的方式,在诸多比较合适的范式中,有个被称作 Communicating Sequential Processes(顺序通信处理)(CSP, C. Hoare 发明的)还有一个叫做 message passing-model(消息传递)(已经运用在了其他语言中,比如 Erlang)。

在 Go 中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用 sync 包来实现(参见第 9.3 节),不过我们很不鼓励这样做:Go 使用 channels 来同步协程(可以参见第 14.2 节等章节)

当系统调用(比如等待 I/O)阻塞协程时,其他协程会继续在其他线程上工作。协程的设计隐藏了许多线程创建和管理方面的复杂工作。

协程是轻量的,比线程更轻。它们痕迹非常不明显(使用少量的内存和资源):使用 4K 的栈内存就可以在堆中创建它们。因为创建非常廉价,必要的时候可以轻松创建并运行大量的协程(在同一个地址空间中 100,000 个连续的协程)。并且它们对栈进行了分割,从而动态的增加(或缩减)内存的使用;栈的管理是自动的,但不是由垃圾回收器管理的,而是在协程退出后自动释放。

协程可以运行在多个操作系统线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程。

存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。Go 的协程和通道理所当然的支持确定性的并发方式(例如通道具有一个 sender 和一个 receiver)。我们会在第 14.7 节中使用一个常见的算法问题(工人问题)来对比两种处理方式。

协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中并且分配了独立的栈,比如:go sum(bigArray),在后台计算总和。

协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。

任何 Go 程序都必须有的 main() 函数也可以看做是一个协程,尽管它并没有通过 go 来启动。协程可以在程序初始化的过程中运行(在 init() 函数中)。

在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用 runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched() 可以使计算均匀分布,使通信不至于迟迟得不到响应。

14.1.2 并发和并行的差异

Go 的并发原语提供了良好的并发设计基础:表达程序结构以便表示独立地执行的动作;所以Go的重点不在于并行的首要位置:并发程序可能是并行的,也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是,一个设计良好的并发程序在并行方面的表现也非常出色。

在当前的运行时(2012 年一月)实现中,Go 默认没有并行指令,只有一个独立的核心或处理器被专门用于 Go 程序,不论它启动了多少个协程;所以这些协程是并发运行的,但他们不是并行运行的:同一时间只有一个协程会处在运行状态。

这个情况在以后可能会发生改变,不过届时,为了使你的程序可以使用多个核心运行,这时协程就真正的是并行运行了,你必须使用 GOMAXPROCS 变量。

这会告诉运行时有多少个协程同时执行。

并且只有 gc 编译器真正实现了协程,适当的把协程映射到操作系统线程。使用 gccgo 编译器,会为每一个协程创建操作系统线程。

14.1.3 使用 GOMAXPROCS

在 gc 编译器下(6g 或者 8g)你必须设置 GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 gccgo 编译器 GOMAXPROCS 有效的与运行中的协程数量相等。假设 n 是机器上处理器或者核心的数量。如果你设置环境变量 GOMAXPROCS>=n,或者执行 runtime.GOMAXPROCS(n),接下来协程会被分割(分散)到 n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。

所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS!

还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 GOMAXPROCS 到 9 会带来性能提升。在一台 32 核的机器上,设置 GOMAXPROCS=8 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 GOMAXPROCS 只会带来轻微的性能下降;设置 GOMAXPROCS=100,使用 top 命令和 H 选项查看到只有 7 个活动的线程。

增加 GOMAXPROCS 的数值对程序进行并发计算是有好处的;

请看 goroutine_select2.go

总结:GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于1个的机器上,会尽可能有等同于核心数的线程在并行运行。

14.1.4 如何用命令行指定使用的核心数量

使用 flags 包,如下:

var numCores = flag.Int("n", 2, "number of CPU cores to use")

in main()
flag.Parse()
runtime.GOMAXPROCS(*numCores)

协程可以通过调用runtime.Goexit()来停止,尽管这样做几乎没有必要。

示例 14.1-goroutine1.go 介绍了概念:

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("In main()")
	go longWait()
	go shortWait()
	fmt.Println("About to sleep in main()")
	// sleep works with a Duration in nanoseconds (ns) !
	time.Sleep(10 * 1e9)
	fmt.Println("At the end of main()")
}

func longWait() {
	fmt.Println("Beginning longWait()")
	time.Sleep(5 * 1e9) // sleep for 5 seconds
	fmt.Println("End of longWait()")
}

func shortWait() {
	fmt.Println("Beginning shortWait()")
	time.Sleep(2 * 1e9) // sleep for 2 seconds
	fmt.Println("End of shortWait()")
}

输出:

In main()
About to sleep in main()
Beginning longWait()
Beginning shortWait()
End of shortWait()
End of longWait()
At the end of main() // after 10s

main()longWait() 和 shortWait() 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了 time 包中的 Sleep 函数。Sleep() 可以按照指定的时间来暂停函数或协程的执行,这里使用了纳秒(ns,符号 1e9 表示 1 乘 10 的 9 次方,e=指数)。

他们按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,以并行的方式。我们让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让 main() 函数停止 4 秒),main() 会提前结束,longWait() 则无法完成。如果我们不在 main() 中等待,协程会随着程序的结束而消亡。

当 main() 函数返回的时候,程序退出:它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。

另外,协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。

为了对比使用一个线程,连续调用的情况,移除 go 关键字,重新运行程序。

现在输出:

In main()
Beginning longWait()
End of longWait()
Beginning shortWait()
End of shortWait()
About to sleep in main()
At the end of main() // after 17 s

协程更有用的一个例子应该是在一个非常长的数组中查找一个元素。

将数组分割为若干个不重复的切片,然后给每一个切片启动一个协程进行查找计算。这样许多并行的协程可以用来进行查找任务,整体的查找时间会缩短(除以协程的数量)。

14.1.5 Go 协程(goroutines)和协程(coroutines)

(译者注:标题中的“Go协程(goroutines)” 即是 14 章讲的协程指的是 Go 语言中的协程。而“协程(coroutines)”指的是其他语言中的协程概念,仅在本节出现。)

在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go协程有些相似,不过有两点不同:

  • Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
  • Go 协程通过通道来通信;协程通过让出和恢复操作来通信

Go 协程比协程更强大,也很容易从协程的逻辑复用到 Go 协程。

 

Sleep() 可以按照指定的时间来暂停函数或协程的执行】 

【协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。】

 

 

不要通过共享内存来通信,而应该通过通信来共享内存

这是一句风靡golang社区的经典语,对于刚接触并发编程的人,该如何理解这句话?

其实如果从分布式的角度来理解,就会比较明了了。

打比方,ab两个进程共同对同一个消息队列进行操作,那么,如果使用共享内存的话,是不是这两个进程就必须局限在同一个物理机上,那么通信的意义就大大缩小了。

如果在设计的时候,对于消息队列,只提供读写接口,而对于内部的实现你完全不用去在意,看起来消息队列就像是共享内存一样了。然而你的消息队列可以利用socket进行通信。

所以,上述这句话,不要用共享内存实现通信是指不要让程序一开始就局限在单机上,而是利用通信,也就是封装内部实现,提供接口的方式来进行相应的操作

通过通信共享内存

Andrew Gerrand
2010年7月13日

传统的线程模型(例如,在编写Java,C ++和Python程序时通常使用)要求程序员使用共享内存在线程之间进行通信。通常,共享数据结构受锁保护,线程将争夺这些锁以访问数据。在某些情况下,通过使用线程安全的数据结构(例如Python的Queue)可以使此操作变得更容易。

Go的并发原语-goroutine和channel-提供了一种优雅而独特的方式来构造并发软件。(这些概念有一个有趣的历史,始于CAR Hoare的通信顺序过程。)Go并未明确使用锁来调解对共享数据的访问,而是鼓励使用通道在goroutine之间传递对数据的引用。这种方法可确保在给定时间只有一个goroutine可以访问数据。该概念在有效的Go语言 (任何Go程序员都必须阅读)中进行了总结

不要通过共享内存进行通信;而是通过通信共享内存。

考虑一个轮询URL列表的程序。在传统的线程环境中,人们可能会像这样构造其数据:

type Resource struct {
    url        string
    polling    bool
    lastPolled int64
}

type Resources struct {
    data []*Resource
    lock *sync.Mutex
}

然后,一个Poller函数(其中的许多函数将在单独的线程中运行)看起来像这样:

func Poller(res *Resources) {
    for {
        // get the least recently-polled Resource
        // and mark it as being polled
        res.lock.Lock()
        var r *Resource
        for _, v := range res.data {
            if v.polling {
                continue
            }
            if r == nil || v.lastPolled < r.lastPolled {
                r = v
            }
        }
        if r != nil {
            r.polling = true
        }
        res.lock.Unlock()
        if r == nil {
            continue
        }

        // poll the URL

        // update the Resource's polling and lastPolled
        res.lock.Lock()
        r.polling = false
        r.lastPolled = time.Nanoseconds()
        res.lock.Unlock()
    }
}

此功能大约需要一页纸,并且需要更多详细信息才能完成。它甚至不包括URL轮询逻辑(它本身只有几行),也不会优雅地处理资源池的耗尽。

让我们看一下使用Go惯用语实现的相同功能。在此示例中,轮询器是一项功能,该函数从输入通道接收要轮询的资源,并在完成后将其发送到输出通道。

type Resource string

func Poller(in, out chan *Resource) {
    for r := range in {
        // poll the URL

        // send the processed Resource to out
        out <- r
    }
}

上一个示例中的微妙逻辑显然不存在,并且我们的Resource数据结构不再包含簿记数据。实际上,剩下的就是重要的部分。这应该使您对这些简单语言功能的功能有所了解。

以上代码段有很多遗漏之处。有关使用这些思想的完整,惯用的Go程序的演练,请参阅Codewalk通过通信共享内存

 

使用共享内存的话在多线程的场景下为了处理竞态,需要加锁,使用起来比较麻烦。另外使用过多的锁,容易使得程序的代码逻辑坚涩难懂,并且容易使程序死锁,死锁了以后排查问题相当困难,特别是很多锁同时存在的时候。

go语言的channel保证同一个时间只有一个goroutine能够访问里面的数据,为开发者提供了一种优雅简单的工具,所以go原生的做法就是使用channle来通信,而不是使用共享内存来通信。

共享内存会涉及到多个线程同时访问修改数据的情况,那得保证数据的安全性,可见性,那就会加锁,加锁会让并行变为串行,cpu也忙于线程抢锁。不如换一种方式,把数据复制一份,每个线程有自己的,只要一个线程干完一件事其他线程不用去抢锁了,这就是一种通信方式,把共享的以通知方式交给线程,实现并发

贝尔实验室和CSP线程

介绍

该页面是并发编程历史的一部分,重点介绍了Hoare的顺序顺序通信(CSP​​)语言[1] [1a]的特定血统 这种风格的并发编程很有趣,原因不是效率而是清晰。即,仅将并发编程视为提高性能的一种手段是一个普遍的错误, 例如,以重叠磁盘I / O请求,通过将结果预取到预期的查询来减少延迟,或利用多个处理器。这些优势很重要,但与本讨论无关。毕竟,它们可以用其他样式来实现,例如异步事件驱动的编程。相反,我们对并发编程很感兴趣,因为它提供了自然的抽象,可以使某些程序更简单。

这不是什么

大多数计算机科学专业的本科生都被迫阅读安德鲁·比雷尔(Andrew Birrell)的 “线程编程简介”。 SRC线程模型是当前可用的大多数线程包使用的模型。所有这些的问题在于它们太低级了。与Hoare提供的通信原语不同,SRC样式线程模块中的原语必须与其他技术(通常是共享内存)组合才能有效使用。通常,程序员倾向于不构建自己的高级结构,而由于需要注意这些低级细节而感到沮丧。

目前,将Birrell的教程排除在外。这是一个不同的线程模型。如果您将其作为其他线程模型来处理,则可能会更容易理解。

顺序流程沟通

到1978年,在对多处理器进行编程的情况下,有许多提议的方法用于通信和同步。共享内存是最常见的通信机制,而信号量,关键区域和监视器是同步机制之一。CAR Hoare使用一种语言原语解决了这两个问题:同步通信。在Hoare的CSP语言中,进程通过从命名的未缓冲通道发送或接收值进行通信。由于通道是无缓冲的,因此发送操作将阻塞,直到将值传输到接收器为止,从而提供了一种同步机制。

Hoare的例子之一是重新格式化80列卡片以在125列打印机上进行打印。在他的解决方案中,一个进程一次读取一张卡,将分解后的内容逐个字符发送到第二个进程。第二个过程将组合125个字符的组,然后将这些组发送到行式打印机。这听起来很琐碎,但是在没有缓冲的I / O库的情况下,单进程解决方案中涉及的必要簿记工作繁重。实际上,缓冲的I / O库实际上只是这两种导出单字符通信接口的过程的封装。

举另一个例子,霍亚尔(Hoare)归功于道格·麦克罗伊(Doug McIlroy),考虑所有质数的生成少于一千。可以通过执行以下伪代码的一系列流程来模拟Eratosthenes的筛网:

 

p =从左邻居得到一个数字
打印p
循环:
    n =从左邻居得到一个数字
    如果(p不除n)
        发送n到正确的邻居

生成过程可以将数字2、3、4,...,1000馈入管道的左端:该行中的第一个过程消除了2的倍数,第二个过程消除了3的倍数,第三个过程消除了5的倍数,依此类推:

到目前为止,示例的线性管线性质不能正确代表CSP的一般性质,但是即使仅限于线性管线,该模型也非常强大。Unix操作系统众所周知的过滤器和管道方法的成功已经有力地证明了这种能力。 [2] 实际上,管道早于Hoare的论文。在1964年10月11日的贝尔实验室内部备忘录中,道格·麦克罗伊(Doug McIlroy)玩弄了将成为Unix管道的想法:“我们应该有一些耦合程序的方法,例如花园软管,当有必要对数据进行按摩时,将其拧入另一个部分。另一种方式。这也是IO的方式。” [3]

Hoare的通信过程比典型的Unix Shell管道更通用,因为它们可以以任意模式连接。实际上,Hoare给出了一个3x3的处理矩阵示例,该矩阵有点像素筛,可用于将矢量乘以3x3的正方形矩阵。

当然,Unix管道机制不需要线性布局。只有shell语法可以。McIlroy报告说,早期使用通用语法对外壳的语法进行了玩弄,但对语法的接受程度不足以实现它(个人交流,2011年)。后来的外壳确实支持非线性管道的某些受限形式。Rochkind的2dsh支持dags;汤姆·达夫(Tom Duff)的rc支持树木。

Hoare的语言新颖新颖,很有影响力,但缺乏一些关键方面。主要缺陷是用于通信的未缓冲通道不是一流的对象:它们不能存储在变量中,不能作为参数传递给函数或跨通道发送。结果,在编写程序时必须固定通信结构。因此,我们必须编写一个程序来打印前1000个素数,而不是前n个素数,并将向量乘以3x3矩阵而不是n x n矩阵。

潘与普罗梅拉

1980年,距Hoare的论文发表仅短短两年,Gerard Holzmann和Rob Pike创建了一个协议分析器,称为pan,它使用CSP方言作为输入。Pan的CSP方言具有串联,选择和循环的功能,但是没有变量。即便如此,霍尔兹曼报告说:“潘在1980年11月21日的Bell Labs数据交换控制协议中发现了它的第一个错误。” [14]该方言很可能是Bell Labs的第一种CSP语言,它无疑为Pike提供了使用和实现类似CSP的语言的经验,这是他的第一种语言。

Holzmann的协议分析仪已发展为Spin模型检查器及其Promela语言,它具有与Newsqueak(qv)相同的一流频道。

新闻快讯

朝着不同的方向发展,Luca Cardelli和Rob Pike将CSP中的思想发展成Squeak微型语言 [4], 用于生成用户界面代码。(此Squeak与Squeak Smalltalk实现不同。)Pike随后将Squeak扩展为成熟的编程语言Newsqueak [5] [6] ,该语言源自 Plan 9的Alef [7] [8],Inferno的Limbo [9]和Google的去 [13]相较于Squeak,Newsqueak的主要语义优势在于Newsqueak将通讯渠道视为一流的对象:与CSP和Squeak不同,渠道 可以 被存储在变量中,作为参数传递给函数,并通过通道发送。这继而实现了通信结构的程序化构造,从而允许创建比手工设计合理的结构更复杂的结构。尤其是,道格·麦克罗伊(Doug McIlroy)演示了如何利用Newsqueak的通讯工具编写精美的程序来处理符号幂级数 [10]传统语言中的类似尝试往往会使簿记工作陷入困境。同样,罗伯·派克(Rob Pike)演示了如何利用通信工具来突破常见的基于事件的编程模型,并编写了并发窗口系统 [11]

阿列夫

Alef [7] [8] 是由Phil Winterbottom设计的一种语言,用于将Newsqueak的思想应用于成熟的系统编程语言。Alef有两种我们称为进程的类型:proc和线程。该程序被组织为一个或多个proc,它们是可以抢先调度的共享内存操作系统进程。每个proc包含一个或多个任务,这些任务是协同调度的协程。请注意,每个任务都分配给一个特定的proc:它们不会在proc之间迁移。

proc的主要用途是提供可以独立于主要任务而阻止I / O的上下文。(Plan 9有没有选择调用,甚至在Unix上,如果你想与非网络I / O重叠计算你需要多个特效。)Acme的论文 [12] 有特效和线程的一个很好的简短的讨论,因为这样做 的关于Plan 9窗口系统的讲义,也将在下面提及。

凌波

Inferno操作系统是计划9衍生产品,用于机顶盒。它的编程语言Limbo [9]受Alef的影响很大。它消除了proc和任务之间的区别,实际上仅包含proc,尽管它们的权重比大多数人认为的流程轻。所有并行性都是抢先的。有趣的是,尽管如此,该语言仍未提供对锁定的真正支持。取而代之的是,通道通信通常提供足够的同步,并鼓励程序员安排任何数据始终拥有明确的所有者。不需要明确的锁定。

线程线程

回到计划9的世界,随着计划9移植到越来越多的体系结构上,Alef编译器变得难以维护。Libthread最初是为了将Alef程序移植到C而创建的,因此可以淘汰Alef编译器。Alef的proc和任务在libthread中称为proc和线程。手册 是最权威的参考。

go

Rob Pike和Ken Thompson转到了Google,并将CSP置于Go语言并发支持的中心。

入门

为了对模型有所了解,特别是进程和线程如何交互,值得阅读《 Alef用户指南》 [8]此演示文稿的前三十张幻灯片 很好地介绍了如何在C中表示Alef构造。

CSP模型功能的最好例子是上文提到的McIlroy和Pike的论文 [10] [11]

Rob Pike的主页包含有关并发编程的课程的讲义: 简介,并介绍了上述两篇论文的幻灯片: 斜视 和 窗口系统这三个中的最后一个提供了一个很好的示例,说明了Plan 9程序通常如何使用proc和任务。

罗伯·派克(Rob Pike)在Google进行了一次 技术演讲 ,提供了很好的介绍(57分钟的视频)。

Rob Pike在2010年与Ru​​ss Cox进行的Google I / O演讲中的一半 展示了如何使用通道和Go的并发性来实现负载平衡工作管理系统。

FROM: https://swtch.com/~rsc/thread/

沟通顺序流程(CSP)

通信顺序过程(CSP)是一种用于描述交互模式的语言。它得到了优雅的数学理论,一组证明工具和大量文献的支持。通信顺序过程》一书由Prentice Hall International(已发行版权)于1985年首次出版;这是对语言以及数学理论的出色入门。

介绍

Go是一种新语言。尽管它借鉴了现有语言的思想,但它具有非同寻常的特性,使有效的Go程序的特性与亲戚编写的程序不同。将C ++或Java程序直接转换为Go不太可能产生令人满意的结果-Java程序是用Java而不是Go编写的。另一方面,从Go角度考虑问题可能会产生一个成功但完全不同的程序。换句话说,要编写好语言,重要的是要了解其属性和习惯用法。了解Go编程中已建立的约定(例如命名,格式设置,程序构造等)也很重要,这样您编写的程序将易于其他Go程序员理解。

本文档提供了编写清晰,惯用的Go代码的技巧。它增强了语言规范,“ Go旅”和“如何编写Go代码”,您应该首先阅读所有这些内容。

例子

围棋包源 的目的是不仅作为核心库,而且为如何使用语言的例子。此外,许多软件包都包含可以运行的,独立的可执行示例,您可以直接从golang.org网站上运行该示例 ,例如 示例(如有必要,请单击“示例”一词以将其打开)。如果您对如何解决问题或如何实现解决方案有疑问,则库中的文档,代码和示例可以提供答案,想法和背景。

格式化

格式化问题是最有争议但后果最不严重的问题。人们可以适应不同的格式样式,但是如果他们不必这样做会更好,如果每个人都遵循相同的样式,那么花在该主题上的时间就会更少。问题在于如何在没有冗长的说明性风格指南的情况下处理这种乌托邦。

使用Go,我们可以采用一种不寻常的方法,让机器处理大多数格式化问题。gofmt程序(也可作为go fmt,以软件包级别而不是源文件级别运行)读取Go程序,并以缩进和垂直对齐的标准样式发出源代码,并保留注释,并在必要时重新格式化注释。如果您想知道如何处理一些新的布局情况,请运行gofmt如果答案似乎不正确,请重新排列程序(或提交有关的错误gofmt),请不要解决它。

例如,无需花时间对结构字段进行注释排列。 Gofmt将为您做到这一点。给出声明

类型T struct {
    名称字符串//对象的名称
    value int //其值
}

gofmt 将列对齐:

类型T struct {
    名称字符串//对象的名称
    value int //其值
}

标准软件包中的所有Go代码都已使用格式化gofmt

保留一些格式详细信息。非常简短:

缩进
我们使用制表符进行缩进,gofmt并在默认情况下发出它们。仅在必要时使用空格。
线长
Go没有行长限制。不必担心打孔卡溢出。如果线条感觉太长,则将其包裹起来并用额外的制表符缩进。
括弧
转到需要的不仅仅是C和Java括号少:控制结构(if, forswitch)没有在他们的语法括号。同样,运算符优先级层次更短更清晰,因此
x << 8 + y << 16
表示间距意味着什么,与其他语言不同。

评论

Go提供了C样式的/* */块注释和C ++样式的//行注释。行注释是常态;块注释主要显示为程序包注释,但在表达式中很有用,或者用于禁用大量代码。

该程序和Web服务器godoc处理Go源文件以提取有关软件包内容的文档。顶级声明之前出现的注释(中间没有换行符)与声明一起被提取,以用作该项目的解释性文本。这些注释的性质和样式决定了文档godoc生成的质量

每个包都应在package子句之前有一个package注释,一个块注释。对于多文件包,包注释仅需要出现在一个文件中,任何一个都可以。包装评论应介绍包装,并提供与包装整体有关的信息。它会首先出现在godoc页面上,并应设置随后的详细文档。

/ *
软件包regexp为正则表达式实现了一个简单的库。

接受的正则表达式的语法为:

    正则表达式:
        串联{'|' 串联}
    级联:
        {封闭}
    关闭:
        术语['*'| '+'| '?' ]
    术语:
        '^'
        '$'
        '。'
        字符
        '['['^']字符范围']'
        '('regexp')'
* /
软件包正则表达式

如果软件包很简单,则软件包注释可以简短。

//包路径实现实用程序例程
//处理斜杠分隔的文件名路径。

注释不需要额外的格式,例如星号横幅。生成的输出甚至可能不会以固定宽度的字体显示,因此不必依赖对齐的间距godoc,例如gofmt,就可以了。注释是未经解释的纯文本,因此HTML和其他注释(如_this_逐字复制)不应该使用。godoc所做的一项调整是以固定宽度的字体显示缩进的文本,适用于程序片段。对于包注释 fmt使用此效果良好。

根据上下文的不同,godoc甚至可能不会重新格式化注释,因此请确保它们看起来直截了当:使用正确的拼写,标点和句子结构,折叠长行等。

在包中,顶级声明之前的任何注释都将用作该声明doc注释程序中的每个导出(大写)名称都应带有文档注释。

Doc注释最好作为完整的句子使用,它可以进行各种各样的自动演示。第一句应该是单句摘要,以声明的名称开头。

//编译会解析一个正则表达式,如果成功,则返回
//可用于与文本匹配的Regexp。
func Compile(str字符串)(* Regexp,错误){

如果每个文档注释都以其描述的项目名称开头,则可以使用go工具doc 子命令,并通过运行输出想象一下,您忘记了名称“ Compile”,但在寻找正则表达式的解析函数,因此您运行了该命令, grep

$ go doc -all regexp | grep -i解析

如果包中的所有文档注释均以“此功能...”开头,则grep 不会帮助您记住该名称。但是,由于该软件包使用名称开头每个文档注释,因此您会看到类似这样的内容,该名称会回忆起您要查找的单词。

$ go doc -all regexp | grep -i解析
    编译将解析正则表达式,如果成功,则返回一个Regexp
    MustCompile类似于Compile,但如果无法解析该表达式,则会发生恐慌。
    解析。它简化了全局变量保存的安全初始化
$

Go的声明语法允许对声明进行分组。单个文档注释可以引入一组相关的常量或变量。由于整个声明都已提出,因此这样的评论常常是敷衍了事。

//未能解析表达式返回的错误代码。
var(
    ErrInternal = errors.New(“ regexp:内部错误”)
    ErrUnmatchedLpar = errors.New(“ regexp:不匹配的'('”)
    ErrUnmatchedRpar = errors.New(“ regexp:不匹配')'”)
    ...

分组还可以指示项目之间的关系,例如一组变量受互斥锁保护的事实。

var(
    countLock sync.Mutex
    inputCount uint32
    outputCount uint32
    errorCount uint32

名字

名称在Go语言中与其他语言一样重要。它们甚至具有语义效果:包外名称的可见性取决于其首字符是否为大写。因此,值得花一些时间讨论Go程序中的命名约定。

包装名称

导入软件包时,软件包名称将成为内容的访问器。

导入“字节”

导入包可以讨论bytes.Buffer如果每个使用该软件包的人都可以使用相同的名称来引用其内容,这将很有帮助,这意味着该软件包的名称应该很好:简短,简洁,令人回味。按照惯例,软件包使用小写的单字名称。不需要下划线或首字母大写。为了简便起见,Err是错误的,因为每个使用您的软件包的人都会输入该名称。而且不用担心先验碰撞包名称仅是导入的默认名称。它不必在所有源代码中都是唯一的,并且在发生冲突的极少数情况下,导入包可以选择其他名称以在本地使用。在任何情况下,混淆都是很少的,因为导入中的文件名决定了所使用的软件包。

另一个约定是,程序包名称是其源目录的基本名称。src/encoding/base64 导入的包"encoding/base64"名称为,但名称为base64notencoding_base64和not encodingBase64

包的导入者将使用该名称来引用其内容,因此,包中的导出名称可以使用该事实来避免卡顿。(不要使用这种import .表示法,它可以简化必须在所测试的程序包之外运行的测试,但应避免这样做。)例如,bufio程序包中的缓冲读取器类型称为Reader,而不是BufReader,因为用户将其视为bufio.Reader,这是一个简洁明了的名称。此外,由于导入的实体始终使用其包名称来寻址,因此bufio.Reader 不会与冲突io.Reader同样,通常会调用来创建新实例的函数(ring.Ring这是Go中构造函数的定义)NewRing,但是由于 Ring是程序包导出的唯一类型,由于调用了程序包ring,因此将其称为just New,程序包的客户端将其视为ring.New使用包结构可以帮助您选择好名字。

另一个简短的例子是once.Do; once.Do(setup)阅读很好,写作不会改善once.DoOrWaitUntilDone(setup)长名不会自动使事情更具可读性。有用的文档注释通常比加长名称更有价值。

吸气剂

Go不会自动为getter和setter提供支持。自己提供getter和setter并没有错,这样做通常是适当的,但是Get使用getter的名字既不是惯用的,也没有必要如果您有一个名为owner(小写,未导出)的字段 ,则应调用getter方法Owner(大写,已导出),而不是GetOwner使用大写名称进行导出提供了挂钩,以将字段与方法区分开。如果需要,可以使用setter函数SetOwner这两个名字在实践中都读得很好:

所有者:= obj.Owner()
如果所有者!=用户{
    obj.SetOwner(用户)
}

接口名称

按照惯例,一个方法接口由该方法name加上后缀-er或类似的修改命名构建的试剂名:Reader, WriterFormatter, CloseNotifier等。

有许多这样的名称,兑现它们和它们捕获的函数名称很有用。 ReadWriteCloseFlush, String等有规范签名和意义。为避免混淆,除非您的方法具有相同的签名和含义,否则请不要给它们使用任何名称。相反,如果您的类型实现的方法的含义与众所周知的类型的方法的含义相同,请为其赋予相同的名称和签名;调用您的string-converter方法Stringnot ToString

混合帽

最后,Go中的约定是使用MixedCaps 或mixedCaps而不使用下划线来编写多字名称。

分号

与C一样,Go的形式语法使用分号来终止语句,但是与C中不同,这些分号不会出现在源代码中。相反,词法分析器使用一条简单规则在扫描时自动插入分号,因此输入文本几乎没有分号。

规则是这样的。如果换行符之前的最后一个标记是标识符(包括诸如int和的float64),基本文字(例如数字或字符串常量)或标记之一

中断继续失败返回++-)}

词法分析器总是在标记后插入分号。可以概括为:“如果换行符位于可以结束语句的标记之后,请插入分号”。

也可以在紧接大括号前省略分号,因此可以使用如下语句:

    go func(){for {dst <-<-src}}()

不需要分号。惯用的Go程序仅在诸如for循环子句之类的地方使用分号 ,以分隔初始化程序,条件和延续元素。如果您以这种方式编写代码,则在一行上分隔多个语句也是必需的。

的分号插入规则的一个后果是,你不能把一个控制结构(中左括号ifforswitch,或select)在下一行。如果这样做,将在分号之前插入一个分号,这可能会导致不想要的效果。这样写

如果我<f(){
    G()
}

不像这样

如果我<f()//错误!
{//错误!
    G()
}

控制结构

Go的控制结构与C的控制结构相关,但在重要方面有所不同。没有do没有while循环,只有略微的概括 for; switch更灵活; ifswitch接受可选的初始化语句,如for; breakcontinue声明接受可选的标签,以确定哪些中断或继续; 并且有新的控制结构,包括类型开关和多路通信多路复用器select语法也略有不同:没有括号,并且主体必须始终用大括号分隔。

如果

在Go中,一个简单的if样子是这样的:

如果x> 0 {
    返回y
}

强制括号鼓励if在多行上编写简单的语句。无论如何都是这样做的好风格,尤其是当主体包含诸如areturn的控制语句时 break

由于ifswitch接受初始化语句,通常会看到用来设置局部变量的语句。

如果err:= file.Chmod(0664); err!= nil {
    log.Print(错误)
    返回错误
}

在去图书馆,你会发现,当一个if语句不流入下一条语句,也就是说,身体两端breakcontinue, goto,或return-the不必要的 else省略。

f,err:= os.Open(名称)
如果err!= nil {
    返回错误
}
codeUsing(f)

这是一种常见情况的示例,在这种情况下,代码必须防止出现一系列错误情况。如果成功的控制流贯穿页面,代码将很好地读取,从而消除了出现的错误情况。由于错误情况倾向于以return 语句结尾,因此生成的代码不需要else语句。

f,err:= os.Open(名称)
如果err!= nil {
    返回错误
}
d,err:= f.Stat()
如果err!= nil {
    f。关闭()
    返回错误
}
codeUsing(f,d)

重新声明和重新分配

旁白:上一节中的最后一个示例演示了:=简短声明表单如何工作的详细信息 调用的声明os.Open为:

f,err:= os.Open(名称)

该语句声明了两个变量ferr几行后,对f.Statread 的调用

d,err:= f.Stat()

看起来好像在声明derr但是请注意,err这两个语句中都会出现。这种重复是合法的:err由第一个语句声明,但仅在第二个语句中重新分配这意味着对的调用将f.Stat使用err上面声明的现有 变量,并为其赋予一个新值。

:=声明中,v即使已经声明了变量,也可能会出现一个变量,条件是:

  • 此声明与的现有声明在同一范围内v (如果v已经在外部范围中声明,则该声明将创建一个新变量§),
  • 初始化中的对应值可分配给v
  • 声明创建了至少一个其他变量。

这种不寻常的特性是纯粹的实用主义,err例如,很容易在长if-else链中使用单个您会看到它经常使用。

§值得注意的是,在Go中,函数参数和返回值的范围与函数主体相同,即使它们按词法出现在包围主体的括号之外。

对于

Gofor循环类似于C,但不相同。它统一了for ,while没有do-while共有三种形式,其中只有一种具有分号。

//像C一样
初始化 健康)状况; 发表{}

//像C一样
对于条件{}

//像C的for(;;)
为{}

简短的声明使在循环中轻松声明索引变量变得容易。

和:= 0
对于我:= 0; 我<10; 我++ {
    总和==我
}

如果要遍历数组,切片,字符串或映射,或者从通道读取,则range子句可以管理该循环。

对于键,值:= range oldMap {
    newMap [key] =值
}

如果只需要范围内的第一项(键或索引),请放下第二项:

对于键:=范围m {
    如果key.expired(){
        删除(m,键)
    }
}

如果只需要范围(值)中的第二项,请使用空白标识符(下划线)来丢弃第一项:

和:= 0
对于_,值:=范围数组{
    总和+ =值
}

后面的部分所述,空白标识符有许多用途

对于字符串,range它可以为您做更多的工作,通过解析UTF-8来分解单个Unicode代码点。错误的编码会占用一个字节并产生替换符文U + FFFD。(名称(具有关联的内置类型)rune是单个Unicode代码点的Go术语。有关 详细信息,请参见语言规范。)

对于pos,字符:= range“日本\ x80语”“ // // \ x80是非法的UTF-8编码
    fmt.Printf(“字符%#U从字节位置%d \ n开始”,char,pos)
}

版画

字符U + 65E5'日'从字节位置0开始
字符U + 672C'本'从字节位置3开始
字符U + FFFD' '从字节位置6开始
字符U + 8A9E'语'从字节位置7开始

最后,Go没有逗号运算符,++并且-- 语句不是表达式。因此,如果您要在中运行多个变量,for 则应使用并行赋值(尽管这排除了++--)。

//反转a
对于i,j:= 0,len(a)-1; 我<j; i,j = i + 1,j-1 {
    a [i],a [j] = a [j],a [i]
}

开关

Goswitch比C更通用。表达式不必是常量,甚至不必是整数,大小写从上到下进行评估,直到找到匹配项为止;如果switch没有表达式,则将其打开 true它因此可能和习惯,写的 ifelseif-else 链作为switch

func unhex(c字节)字节{
    切换{
    情况'0'<= c && c <='9':
        返回c-'0'
    情况'a'<= c && c <='f':
        返回c-'a'+ 10
    情况'A'<= c && c <='F':
        返回c-'A'+ 10
    }
    返回0
}

不会自动掉线,但案件可以用逗号分隔的列表显示。

func shouldEscape(c byte)bool {
    开关c {
    大小写'','?','&','=','#','+','%':
        返回真
    }
    返回假
}

尽管它们在Go中不像其他一些类似C的语言那样普遍,break但是可以使用语句来switch尽早终止但是,有时有时需要跳出周围的循环而不是开关,而在Go中,可以通过在循环上放置标签并“断开”该标签来实现。此示例显示了两种用法。

循环:
	对于n:= 0; n <len(src); n + =大小{
		切换{
		大小写src [n] <sizeOne:
			如果validateOnly {
				打破
			}
			大小= 1
			更新(src [n])

		大小写src [n] <sizeTwo:
			如果n + 1> = len(src){
				err = errShortInput
				断环
			}
			如果validateOnly {
				打破
			}
			大小= 2
			更新(src [n] + src [n + 1] << shift)
		}
	}

当然,该continue语句还接受可选标签,但仅适用于循环。

要结束本节,这是一个使用两个switch语句的字节片比较例程 

//比较返回比较两个字节片的整数,
//按照字典顺序。
//如果a == b,结果将为0;如果a <b,结果将为-1;如果a> b,结果将为+1
func Compare(a,b [] byte)int {
    对于我:= 0; i <len(a)&& i <len(b); 我++ {
        切换{
        情况a [i]> b [i]:
            返回1
        情况a [i] <b [i]:
            返回-1
        }
    }
    切换{
    情况len(a)> len(b):
        返回1
    情况len(a)<len(b):
        返回-1
    }
    返回0
}

类型开关

开关也可以用来发现接口变量的动态类型。这种类型切换使用type括号内带有关键字的类型声明的语法如果开关在表达式中声明了变量,则该变量在每个子句中将具有相应的类型。在这种情况下重用名称也是惯用的,实际上是在每种情况下声明一个具有相同名称但类型不同的新变量。

var t接口{}
t = functionOfSomeType()
开关t:= t。(type){
默认:
    fmt.Printf(“意外类型%T \ n”,t)//%T打印t具有的任何类型
案例布尔:
    fmt.Printf(“ boolean%t \ n”,t)// t的类型为bool
case int:
    fmt.Printf(“ integer%d \ n”,t)// t的类型为int
案例*布尔:
    fmt.Printf(“指向布尔值%t \ n的指针”,* t)// t的类型为* bool
案例* int:
    fmt.Printf(“整数%d \ n的指针,* t)// t的类型为* int
}

职能

多个返回值

Go的不寻常功能之一是函数和方法可以返回多个值。这种形式可以用来改善C程序中的一些笨拙的习惯用法:带内错误返回,例如-1forEOF 和修改由地址传递的参数。

在C语言中,写入错误由负数表示,错误代码被隐藏在易失性位置中。在Go中,Write 可以返回一个计数一个错误:“是的,您写了一些字节,但不是全部,因为您填满了设备”。Write软件包中文件方法签名os为:

func(文件*文件)写(b [] byte)(n int,err错误)

而作为文件说,它返回写入的字节数和一个非空error的时候n != len(b)这是一种常见的样式。有关更多示例,请参见错误处理部分。

类似的方法避免了将指针传递给返回值以模拟参考参数的需要。这是一个简单的函数,可从字节片中的某个位置获取一个数字,然后返回该数字和下一个位置。

func nextInt(b [] byte,i int)(int,int){
    为; i <len(b)&&!isDigit(b [i]); 我++ {
    }
    x:= 0
    为; i <len(b)&& isDigit(b [i]); 我++ {
        x = x * 10 + int(b [i])-'0'
    }
    返回x,我
}

您可以使用它来扫描输入切片中的数字,b如下所示:

    对于我:= 0; 我<len(b); {
        x,i = nextInt(b,i)
        fmt.Println(x)
    }

命名结果参数

可以给Go函数的返回或结果“参数”指定名称,并将其用作常规变量,就像传入的参数一样。命名后,函数开始时会将它们初始化为零值。如果函数执行return不带参数语句,则将结果参数的当前值用作返回值。

名称不是强制性的,但它们可以使代码更短,更清晰:它们是文档。如果我们命名,nextInt则返回的结果显而易见int 。

func nextInt(b [] byte,pos int)(value,nextPos int){

由于命名结果已初始化并绑定到未经修饰的返回,因此它们既可以简化又可以澄清。这是io.ReadFull很好使用它们的版本

func ReadFull(r Reader,buf [] byte)(n int,err error){
    对于len(buf)> 0 && err == nil {
        var nr int
        nr,err = r.Read(buf)
        n + = nr
        buf = buf [nr:]
    }
    返回
}

延期

Go的defer语句将函数调用( 延迟函数)计划为在执行defer返回的函数之前立即运行这是处理异常情况的一种不寻常但有效的方法,例如无论函数返回哪个路径都必须释放资源。典型的例子是解锁互斥锁或关闭文件。

// Contents以字符串形式返回文件的内容。
func Contents(文件名字符串)(字符串,错误){
    f,err:= os.Open(文件名)
    如果err!= nil {
        返回“”,错误
    }
    defer f.Close()// f.Close将在完成后运行。

    var结果[] byte
    buf:= make([] byte,100)
    为{
        n,错误:= f.Read(buf [0:])
        result = append(result,buf [0:n] ...)//稍后将讨论append。
        如果err!= nil {
            如果err == io.EOF {
                打破
            }
            return“”,err //如果我们返回这里f将被关闭。
        }
    }
    return string(result),nil //如果我们返回这里f将被关闭。
}

推迟调用诸如之类的函数Close有两个优点。首先,它确保您永远不会忘记关闭文件,如果以后编辑函数以添加新的返回路径,则很容易犯此错误。其次,这意味着关闭位于打开附近,这比将其放置在函数的末尾要清晰得多。

延迟函数的参数(如果函数是方法,则包括接收方)在延迟 执行时(而不是在调用执行)进行评估除了避免担心变量在函数执行时会更改值之外,这还意味着单个延迟的调用站点可以延迟多个函数的执行。这是一个愚蠢的例子。

对于我:= 0; 我<5; 我++ {
    延迟fmt.Printf(“%d”,i)
}

延迟的功能按LIFO顺序执行,因此该代码将 4 3 2 1 0在函数返回时被打印。一个更合理的示例是通过程序跟踪函数执行的简单方法。我们可以编写一些简单的跟踪例程,如下所示:

func trace(s string){fmt.Println(“ entering:”,s)}
func untrace(s字符串){fmt.Println(“ leaving:”,s)}

//像这样使用它们:
func a(){
    trace(“ a”)
    延迟取消跟踪(“ a”)
    // 做点什么....
}

通过利用以下事实,我们可以做得更好:在defer执行时评估对延迟函数的参数跟踪例程可以将参数设置为取消跟踪例程。这个例子:

func trace(s string)string {
    fmt.Println(“ entering:”,s)
    返回s
}

func un(s string){
    fmt.Println(“ leaving:”,s)
}

func a(){
    延迟un(trace(“ a”))
    fmt.Println(“ in a”)
}

func b(){
    延迟un(trace(“ b”))
    fmt.Println(“ in b”)
    一种()
}

func main(){
    b()
}

版画

输入:b
在b
输入:a
在一个
离开:
离开:b

对于习惯于使用其他语言进行块级资源管理的程序员来说,这defer似乎很奇怪,但是它最有趣,功能最强大的应用正是基于它不是基于块而是基于函数的事实。在上的部分中 panicrecover我们将看到其可能性的另一个示例。

数据

分配与 new

Go有两个分配原语,内置函数 newmake它们执行不同的操作,并应用于不同的类型,这可能会造成混淆,但是规则很简单。让我们new谈谈它是一个分配内存的内置函数,但与其他语言中的同名函数不同,它不会初始化内存,只会将其清零也就是说, new(T)为类型为new的项目分配零存储 T并返回其地址,类型为value *T在Go术语中,它返回一个指针,该指针指向新分配的type零值 T

由于返回的内存new为零,因此在设计数据结构时安排使用每种类型的零值而无需进一步初始化将很有帮助。这意味着数据结构的用户可以使用它来创建一个new并且可以正常工作。例如,的文档bytes.Buffer指出“零值Buffer是准备使用的空缓冲区”。同样,sync.Mutex没有显式的构造函数或Init方法。而是将a的零值sync.Mutex 定义为未锁定的互斥锁。

零值即有用属性会暂时起作用。考虑此类型声明。

输入SyncedBuffer struct {
    锁定同步
    缓冲区字节
}

type的值SyncedBuffer也可以在分配或声明后立即使用。在下一个代码段中,两者pv都可以正常工作,而无需进一步安排。

p:= new(SyncedBuffer)//输入* SyncedBuffer
var v SyncedBuffer //类型SyncedBuffer

构造函数和复合文字

有时零值不够好,因此需要初始化构造函数,如本例中从package派生的那样os

func NewFile(fd int,name string)*文件{
    如果fd <0 {
        返回零
    }
    f:= new(文件)
    f.fd = fd
    f.name =名称
    f.dirinfo = nil
    f.nepipe = 0
    返回f
}

那里有很多样板。我们可以使用复合文字来简化它,它是一个表达式,每次对其求值时都会创建一个新实例。

func NewFile(fd int,name string)*文件{
    如果fd <0 {
        返回零
    }
    f:= File {fd,name,nil,0}
    返回&f
}

注意,与C语言不同,完全可以返回局部变量的地址。函数返回后,与变量关联的存储将保留。实际上,采用复合文字的地址会在每次对其求值时分配一个新实例,因此我们可以将后两行结合在一起。

    返回&File {fd,name,nil,0}

复合文字的字段按顺序排列,并且必须全部存在。但是,通过将元素显式标记为字段: 对,初始化器可以按任何顺序出现,而缺失的则保留为各自的零值。因此我们可以说

    返回&File {fd:fd,name:name}

作为一种限制情况,如果复合文字完全不包含任何字段,则它将为该类型创建一个零值。表达式new(File)&File{}是等效的。

也可以为数组,切片和映射创建复合文字,其中字段标签为索引或映射键。在这些例子中,初始化工作无论的值的Enone, EioEinval,只要它们是不同的。

一个:= [...]字符串{Enone:“没有错误”,Eio:“ Eio”,Einval:“无效参数”}}
s:= [] string {Enone:“无错误”,Eio:“ Eio”,Einval:“无效参数”}
m:= map [int] string {Enone:“没有错误”,Eio:“ Eio”,Einval:“无效参数”}}

分配与 make

回到分配。内置函数make(T, args)的用途不同于new(T)它仅创建切片,地图和通道,并返回类型(not 初始化 (未归零)值区别的原因是,这三种类型在幕后表示了对在使用之前必须初始化的数据结构的引用。例如,切片是一个三项描述符,其中包含指向数据(在数组内部),长度和容量的指针,在初始化这些项目之前,切片为对于切片,地图和通道, 初始化内部数据结构并准备要使用的值。例如, T*Tnilmake

make([] int,10,100)

分配一个100个int的数组,然后创建一个长度为10且容量为100的切片结构,指向该数组的前10个元素。(制作切片时,可以省略容量;有关更多信息,请参见切片部分。)相反,new([]int)返回指向新分配的归零切片结构的指针,即指向nil切片值的指针

这些示例说明了new和 之间的区别make

var p * [] int = new([] int)//分配切片结构;* p ==零; 很少有用
var v [] int = make([] int,100)//切片v现在引用一个包含100个整数的新数组

//不必要的复杂:
var p * [] int = new([] int)
* p = make([] int,100,100)

//惯用语:
v:= make([] int,100)

请记住,这make仅适用于地图,切片和通道,不返回指针。要获得显式指针new,请为变量明确分配或采用变量地址。

数组

数组在计划内存的详细布局时很有用,有时可以帮助避免分配,但是数组主要是切片的构建块,切片是下一节的主题。为奠定该主题的基础,以下是有关数组的几句话。

在Go和C中,数组的工作方式之间存在主要差异。在Go中,

  • 数组是值。将一个数组分配给另一个数组将复制所有元素。
  • 特别是,如果将数组传递给函数,它将接收该数组副本,而不是指向它的指针。
  • 数组的大小是其类型的一部分。类型[10]int 和[20]int是不同的。

value属性既有用又昂贵。如果您想要类C的行为和效率,则可以将指针传递给数组。

func Sum(a * [3] float64)(sum float64){
    对于_,v:=范围* a {
        和+ = v
    }
    返回
}

数组:= [... float64 {7.0,8.5,9.1}
x:= Sum(&array)//注意显式地址运算符

但是,即使这种样式也不是惯用的Go。请改用切片。

切片

切片包装数组可为数据序列提供更通用,更强大和更方便的接口。除了具有明确维数的项(例如转换矩阵)外,Go中的大多数数组编程都是使用切片而不是简单数组完成的。

切片包含对基础数组的引用,如果将一个切片分配给另一个切片,则两个切片均引用同一数组。如果函数采用slice参数,则对slice的元素所做的更改将对调用者可见,这类似于将指针传递给基础数组。Read 因此函数可以接受一个切片参数,而不是一个指针和一个计数; 切片内的长度设置了要读取多少数据的上限。这是包中类型 Read方法的签名 : Fileos

func(f * File)读取(buf [] byte)(n int,错误)

该方法返回读取的字节数和错误值(如果有)。读入所述第一32个字节的较大的缓冲区的 buf切片(这里用作动词)的缓冲液中。

    n,错误:= f.Read(buf [0:32])

这种切片是普通且有效的。实际上,暂时不考虑效率,以下代码段还将读取缓冲区的前32个字节。

    var n int
    var err错误
    对于我:= 0; 我<32; 我++ {
        nbytes,e:= f.Read(buf [i:i + 1])//读取一个字节。
        n + =字节
        如果nbytes == 0 || e!= nil {
            误差= e
            打破
        }
    }

切片的长度可以更改,只要它仍然适合基础数组的限制即可;只需将其分配给自身的一部分即可。切片容量(可通过内置函数访问cap)报告切片可能采用的最大长度。这是将数据追加到切片的功能。如果数据超出容量,则会重新分配片。返回结果切片。该函数使用lencap应用于nil切片时合法 的事实 ,并返回0。

func Append(切片,数据[] byte)[] byte {
    l:= len(切片)
    如果l + len(data)> cap(slice){//重新分配
        //为未来的增长分配两倍的需求。
        newSlice:= make([] byte,(l + len(data))* 2)
        //复制函数是预先声明的,适用于任何切片类型。
        复制(newSlice,切片)
        slice = newSlice
    }
    slice = slice [0:l + len(data)]
    复制(切片[l:],数据)
    返回切片
}

之后必须返回分片,因为尽管Append 可以修改的元素slice,但分片本身(保存指针,长度和容量的运行时数据结构)是按值传递的。

附加到切片的想法非常有用,它被append内置函数捕获 但是,要了解该功能的设计,我们需要更多信息,因此我们将在稍后返回。

二维切片

Go的数组和切片是一维的。要创建等效于2D数组或切片的数组,必须定义一个数组数组或切片切片,如下所示:

type Transform [3] [3] float64 //一个3x3数组,实际上是一个数组数组。
type LinesOfText [] [] byte //字节片的一部分。

由于切片的长度是可变的,因此有可能使每个内部切片的长度不同。这可能是常见的情况,例如在我们的LinesOfText 示例中:每行都有独立的长度。

文字:= LinesOfText {
	[] byte(“现在是时间”),
	[] byte(“为了所有好的地鼠”),
	[] byte(“为聚会带来一些乐趣。”),
}

有时有必要分配2D切片,例如,在处理像素的扫描线时可能会出现这种情况。有两种方法可以实现此目的。一种是独立分配每个分片;另一种是分配单个数组,并将单个切片指向该数组。使用哪种取决于您的应用程序。如果切片可能增大或缩小,则应独立分配它们,以避免覆盖下一行;如果不是,则使用单一分配构造对象可能会更有效。作为参考,以下是这两种方法的示意图。首先,一次一行:

//分配顶级切片。
picture:= make([] [] uint8,YSize)// y的每单位一行。
//循环遍历行,为每行分配切片。
对于我:=范围图片{
	图片[i] = make([[uint8,XSize)
}

现在作为一种分配,分成几行:

//分配顶级切片,与之前相同。
picture:= make([] [] uint8,YSize)// y的每单位一行。
//分配一个大切片以容纳所有像素。
pixel:= make([] uint8,XSize * YSize)//即使图片是[] [] uint8,类型也为[] uint8。
//循环遍历各行,从其余像素切片的前面切成每行。
对于我:=范围图片{
	图片[i],像素=像素[:XSize],像素[XSize:]
}

地图

映射是一种方便且功能强大的内置数据结构,该结构将一种类型的值()与另一种类型的值(元素)相关联。键可以是定义了相等运算符的任何类型,例如整数,浮点数和复数,字符串,指针,接口(只要动态类型支持相等),结构和数组。切片不能用作映射键,因为未在其上定义相等性。像切片一样,映射保留对基础数据结构的引用。如果将地图传递给更改地图内容的函数,则更改将在调用方中可见。

可以使用带有冒号分隔的键/值对的常规复合文字语法来构建地图,因此在初始化过程中轻松构建它们。

var timeZone = map [string] int {
    “ UTC”:0 * 60 * 60,
    “ EST”:-5 * 60 * 60,
    “ CST”:-6 * 60 * 60,
    “ MST”:-7 * 60 * 60,
    “ PST”:-8 * 60 * 60,
}

语法上分配和获取映射值的方式类似于对数组和切片执行相同的操作,只是索引不必为整数。

偏移量:= timeZone [“ EST”]

尝试使用映射中不存在的键来获取映射值时,将为映射中的条目类型返回零值。例如,如果映射包含整数,则查找不存在的键将返回0集合可以实现为具有值类型的映射bool将映射项设置true为将值放入集合中,然后通过简单的索引对其进行测试。

参加过:= map [string] bool {
    “安”:是的,
    “乔”:是的,
    ...
}

如果有人参加[人] {//如果人不在地图中则为假
    fmt.Println(人员,“正在开会”)
}

有时您需要从零值中区分出丢失的条目。是否有条目"UTC" 或为0,因为它根本不在地图中?您可以采用多种分配形式进行区分。

var seconds int
var ok bool
秒,确定= timeZone [tz]

由于明显的原因,这被称为“逗号可以”的成语。在此示例中,如果tz存在,seconds 将进行适当设置并ok为true;否则为true。如果不是, seconds则将其设置为零,并且ok为false。这是一个将其与良好的错误报告结合在一起的函数:

func offset(tz string)int {
    如果秒,确定:= timeZone [tz]; 好 {
        返回秒
    }
    log.Println(“未知时区:”,tz)
    返回0
}

要在地图中测试是否存在而不必担心实际值,可以使用空白标识符_)代替该值的常规变量。

_,目前:= timeZone [tz]

要删除地图条目,请使用delete 内置函数,该函数的参数是地图和要删除的键。即使地图上已经没有钥匙,也可以这样做。

delete(timeZone,“ PDT”)//现在是标准时间

列印

Go中的格式化打印使用类似于Cprintf 家族的样式,但功能更丰富,更通用。该函数住在fmt 包装和有大写的名字:fmt.Printffmt.Fprintf, fmt.Sprintf等。字符串函数(Sprintf等)返回字符串,而不是填充提供的缓冲区。

您不需要提供格式字符串。对于每一个Printf, FprintfSprintf有另一种双功能,如PrintPrintln这些函数不采用格式字符串,而是为每个参数生成默认格式。这些Println版本还会在参数之间插入一个空格,并在输出中添加一个换行符,而Print仅当双方的操作数都不是字符串时,这些版本才会添加空格。在此示例中,每行产生相同的输出。

fmt.Printf(“ Hello%d \ n”,23)
fmt.Fprint(os.Stdout,“ Hello”,23,“ \ n”)
fmt.Println(“ H​​ello”,23)
fmt.Println(fmt.Sprint(“ Hello”,23))

格式化的打印功能fmt.Fprint 和朋友将实现该io.Writer接口的任何对象作为第一个参数变量os.Stdout 和os.Stderr熟悉的实例。

在这里,事情开始与C%d 背道而驰。相反,打印例程使用参数的类型来决定这些属性。

var x uint64 = 1 << 64-1
fmt.Printf(“%d%x;%d%x \ n”,x,x,int64(x),int64(x))

版画

18446744073709551615 ffffffffffffffff; -1 -1

如果只需要默认转换(例如,十进制表示整数),则可以使用catchall格式%v(表示“ value”);结果是什么Print,并Println会产生。此外,该格式可以打印任何值,甚至可以打印数组,切片,结构和映射。这是上一节中定义的时区图的打印语句。

fmt.Printf(“%v \ n”,timeZone)//或只是fmt.Println(timeZone)

给出输出:

地图[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

对于地图,Printf和朋友按字母顺序对输出进行字典排序。

打印结构时,修改后的格式会%+v用其名称注释结构的字段,对于任何值,备用格式都会%#v以完整的Go语法打印该值。

类型T struct {
    一个整数
    b float64
    C字串
}
t:=&T {7,-2.35,“ abc \ tdef”}
fmt.Printf(“%v \ n”,t)
fmt.Printf(“%+ v \ n”,t)
fmt.Printf(“%#v \ n”,t)
fmt.Printf(“%#v \ n”,timeZone)

版画

&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T {a:7,b:-2.35,c:“ abc \ tdef”}
map [string] int {“ CST”:-21600,“ EST”:-18000,“ MST”:-25200,“ PST”:-28800,“ UTC”:0}

(请注意,“&”号。)将引号字符串格式%q应用于类型为string也可以使用[]byte%#q如果可能,替代格式将使用反引号代替。(该%q格式也适用于整数和符文,产生单引号符文常量。)此外,该方法%x适用于字符串,字节数组和字节片以及整数,生成长十六进制字符串,且格式为空格(% x),在字节之间放置空格。

另一种方便的格式是%T,它打印类型

fmt.Printf(“%T \ n”,timeZone)

版画

map [string] int

如果要控制自定义类型的默认格式,只需定义一种String() string在类型上带有签名的方法即可对于我们的简单类型T,可能看起来像这样。

func(t * T)String()字符串{
    返回fmt.Sprintf(“%d /%g /%q”,ta,tb,tc)
}
fmt.Printf(“%v \ n”,t)

以以下格式打印

7 / -2.35 /“ abc \ tdef”

(如果您需要打印类型T以及指向的指针T,则for的接收器String必须为值类型;此示例使用了指针,因为这对于结构类型更有效且更习惯。有关指针与值接收器的关系请参见以下部分更多信息。)

我们的String方法之所以能够调用Sprintf是因为打印例程是完全可重入的,并且可以通过这种方式包装。但是,有一个重要的细节需要了解这种方法:不要String通过 无限期地调用 Sprintf您的String方法的方式来构造方法。如果该Sprintf 调用尝试将接收方直接打印为字符串,则可能会发生这种情况,这又将再次调用该方法。如本例所示,这是一个常见且容易犯的错误。

输入MyString字符串

func(m MyString)String()字符串{
    return fmt.Sprintf(“ MyString =%s”,m)//错误:将永远重复。
}

它也很容易修复:将参数转换为基本字符串类型,该类型没有方法。

输入MyString字符串
func(m MyString)String()字符串{
    return fmt.Sprintf(“ MyString =%s”,string(m))// OK:注释转换。
}

初始化部分,我们将看到另一种避免这种递归的技术。

另一种打印技术是将打印例程的参数直接传递给另一个此类例程。的签名Printf使用类型...interface{} 作为其最终参数,以指定可以在格式之后显示任意数量的参数(任意类型)。

func Printf(格式字符串,v ... interface {})(n int,错误错误){

在函数中Printf,其v作用类似于类型的变量, []interface{}但如果将其传递给另一个可变参数函数,则其作用类似于常规参数列表。这是log.Println我们上面使用的功能的实现它直接将其参数传递给 fmt.Sprintln实际格式。

// Println以fmt.Println的方式打印到标准记录器。
func Println(v ... interface {}){
    std.Output(2,fmt.Sprintln(v ...))//输出带有参数(int,string)
}

我们在嵌套调用中写了...之后vSprintln以告诉编译器将其v视为参数列表。否则,它将v作为单个切片参数传递 

打印比我们这里讨论的还要多。有关详细信息,请参见godoc软件包文档fmt

顺便说一句,...参数可以是特定类型,例如...int 对于选择最小整数列表的min函数而言:

func Min(a ... int)int {
    min:= int(^ uint(0)>> 1)//最大int
    对于_,i:=范围{
        如果我<分钟{
            最小=我
        }
    }
    返回最小值
}

附加

现在,我们缺少了解释append内置功能设计所需的内容的签名与上面append 的自定义Append函数不同示意图如下:

func append(slice [] T,elements ... T)[] T

其中T是任何给定类型的占位符。实际上,您无法在Go中编写T 由调用者确定类型的函数append就是内置的原因:它需要编译器的支持。

什么append是将元素附加到切片的末尾并返回结果。需要返回结果,因为与我们手写的一样Append,底层数组可能会更改。这个简单的例子

x:= [] int {1,2,3}
x =追加(x,4,5,6)
fmt.Println(x)

版画[1 2 3 4 5 6]所以append工作有点像Printf,收集任意数量的参数。

但是,如果我们想做我们想做的事Append并将一个切片附加到一个切片上怎么办?简便:...就像在上述呼叫中一样,可以在呼叫站点上使用Output此代码段产生与上面相同的输出。

x:= [] int {1,2,3}
y:= [] int {4,5,6}
x =追加(x,y ...)
fmt.Println(x)

没有它...,它就不会编译,因为类型是错误的。y不是type int

初始化

尽管从表面上看,它与C或C ++中的初始化没有太大区别,但Go中的初始化功能更强大。可以在初始化期间构建复杂的结构,并且正确处理了初始化对象之间(甚至不同包之间)的排序问题。

常数

Go中的常量就是常量。即使在函数中定义为局部变量时,也可以在编译时创建它们,并且只能是数字,字符(符文),字符串或布尔值。由于编译时的限制,定义它们的表达式必须是可由编译器评估的常量表达式。例如, 1<<3是一个常量表达式,而 math.Sin(math.Pi/4)不是因为函数调用math.Sin需要在运行时发生。

在Go中,使用枚举器创建枚举常量iota 。由于iota可以作为表达式的一部分,并且表达式可以隐式重复,因此可以轻松地构建复杂的值集。

类型ByteSize float64

const(
    _ = iota //通过分配给空白标识符来忽略第一个值
    KB字节大小= 1 <<(10 * iota)
    兆字节
    国标
    结核病
    PB
    EB
    ZB
    B

将方法附加String到任何用户定义的类型的能力使得任意值都可以自动格式化自身以进行打印。尽管您会看到它最常用于结构,但该技术对于标量类型(例如的浮点类型)也很有用ByteSize

func(b ByteSize)String()字符串{
    切换{
    情况b> = YB:
        返回fmt.Sprintf(“%。2fYB”,b / YB)
    情况b> = ZB:
        返回fmt.Sprintf(“%。2fZB”,b / ZB)
    情况b> = EB:
        返回fmt.Sprintf(“%。2fEB”,b / EB)
    情况b> = PB:
        返回fmt.Sprintf(“%。2fPB”,b / PB)
    情况b> = TB:
        返回fmt.Sprintf(“%。2fTB”,b / TB)
    情况b> = GB:
        返回fmt.Sprintf(“%。2fGB”,b / GB)
    情况b> = MB:
        返回fmt.Sprintf(“%。2fMB”,b / MB)
    情况b> = KB:
        返回fmt.Sprintf(“%。2fKB”,b / KB)
    }
    返回fmt.Sprintf(“%。2fB”,b)
}

表达式YB打印为1.00YB,而ByteSize(1e13)打印为9.09TB

这里的使用Sprintf 来实现ByteSizeString方法是不是因为转换的安全(避免重复下去),而是因为它要求Sprintf%f,这不是一个字符串格式:Sprintf只调用String时,它要一个字符串的方法,以及%f 想要一个浮点点值。

变数

变量可以像常量一样被初始化,但是初始化器可以是在运行时计算的通用表达式。

var(
    home = os.Getenv(“ HOME”)
    用户= os.Getenv(“ USER”)
    gopath = os.Getenv(“ GOPATH”)

初始化功能

最后,每个源文件都可以定义自己的niladicinit函数以设置所需的任何状态。(实际上,每个文件都可以具有多个 init功能。)最后意味着final ::init在包中的所有变量声明评估了它们的初始值设定项之后,即被调用,并且仅在所有导入的包被初始化之后才对它们进行赋值。

除了不能表示为声明的初始化外,init函数的常见用法是在实际执行开始之前验证或修复程序状态的正确性。

func init(){
    如果用户==“” {
        log.Fatal(“未设置$ USER”)
    }
    如果home ==“” {
        home =“ / home /” +用户
    }
    如果gopath ==“” {
        gopath =主页+“ / go”
    }
    // gopath可能会在命令行上被--gopath标志覆盖。
    flag.StringVar(&gopath,“ gopath”,gopath,“覆盖默认的GOPATH”)
}

方法

指针与值

如我们所见ByteSize,可以为任何命名类型(指针或接口除外)定义方法;接收者不必是结构。

在上面的切片讨论中,我们编写了一个Append 函数。我们可以将其定义为切片方法。为此,我们首先声明一个可以绑定该方法的命名类型,然后使该方法的接收者成为该类型的值。

类型ByteSlice [] byte

func(slice ByteSlice)追加(数据[] byte)[] byte {
    //正文与上面定义的Append函数完全相同。
}

这仍然需要该方法返回更新的切片。我们可以通过重新定义方法采取消除笨拙 指针ByteSlice它的接收器,因此该方法可以覆盖调用者的切片。

func(p * ByteSlice)追加(数据[] byte){
    切片:= * p
    //上面的正文,没有返回值。
    * p =切片
}

实际上,我们可以做得更好。如果我们修改函数,使其看起来像是标准Write方法,就像这样,

func(p * ByteSlice)写(数据[] byte)(n int,错误,错误){
    切片:= * p
    //同样如上。
    * p =切片
    返回len(data),nil
}

然后该类型*ByteSlice满足标准接口 io.Writer,这很方便。例如,我们可以打印成一张。

    var b ByteSlice
    fmt.Fprintf(&b,“这个小时有%d天\ n”,7)

我们传递a的地址是ByteSlice 因为仅*ByteSlice满足io.Writer关于指针与接收器的值的规则是,可以在指针和值上调用值方法,但是只能在指针上调用指针方法。

之所以出现此规则,是因为指针方法可以修改接收者。在值上调用它们将导致该方法接收该值的副本,因此任何修改都将被丢弃。因此,该语言不允许出现此错误。但是,有一个方便的例外。当该值是可寻址的时,该语言将通过自动插入地址运算符来处理在值上调用指针方法的常见情况。在我们的示例中,变量b是可寻址的,因此我们可以Write使用just调用其方法b.Write编译器会将其重写(&b).Write为我们。

顺便说一句,在Write字节片上使用的想法对于实现至关重要bytes.Buffer

接口及其他类型

介面

Go中的接口提供了一种指定对象行为的方法:如果可以做到这一点,则可以在此处使用它 我们已经看过几个简单的例子。定制打印机可以用一种String方法来实现,Fprintf可以用一种Write方法生成任何东西的输出只有一个或两个方法的接口在Go代码中很常见,并且通常使用从该方法派生的名称(例如io.Writer 实现的名称)命名Write

一个类型可以实现多个接口。例如,一个集合可以通过在包中的例程进行排序sort,如果它实现了 sort.Interface,其中包含Len(), Less(i, j int) bool以及Swap(i, j int),它也可以有一个自定义的格式。在这个人为的例子中,Sequence两者都满足。

类型Sequence [] int

// sort.Interface所需的方法。
func(s Sequence)Len()int {
    返回镜头
}
func(s Sequence)Less(i,j int)bool {
    返回s [i] <s [j]
}
func(s Sequence)Swap(i,j int){
    s [i],s [j] = s [j],s [i]
}

//复制返回序列的副本。
func(s序列)Copy()序列{
    复制:= make(Sequence,0,len(s))
    返回append(copy,s ...)
}

//打印方法-在打印之前对元素进行排序。
func(s序列)String()字符串{
    s = s.Copy()//进行复制;不要覆盖参数。
    排序
    str:=“ [”
    对于i,elem:= range s { //循环为O(N²); 将在下一个示例中解决。
        如果我> 0 {
            str + =“”
        }
        str + = fmt.Sprint(elem)
    }
    返回str +“]”
}

转换次数

String方法Sequence是重新创建Sprint已经对切片进行的工作(它的复杂度为O(N²),这很差。)如果在调用之前将转换Sequence为普通 格式,我们可以分担努力(并加快速度)。 []intSprint

func(s序列)String()字符串{
    s = s.Copy()
    排序
    返回fmt.Sprint([] int(s))
}

此方法是用于SprintfString方法安全调用的转换技术的另一个示例 因为如果忽略类型名称,这两个类型(Sequence[]int)是相同的,因此在它们之间进行转换是合法的。转换不会创建新值,而只是暂时地充当现有值具有新类型的行为。(还有其他合法的转换,例如从整数到浮点的转换,它们确实创建了一个新值。)

在Go程序中,习惯用法是转换表达式的类型以访问不同的方法集。例如,我们可以使用现有类型sort.IntSlice将整个示例简化为:

类型Sequence [] int

//打印方法-在打印之前对元素进行排序
func(s序列)String()字符串{
    s = s.Copy()
    sort.IntSlice(s).Sort()
    返回fmt.Sprint([] int(s))
}

现在,而不是Sequence实现多个接口(排序和打印),我们使用一个数据项的转换为多种类型的能力(Sequencesort.IntSlice 和[]int),每个做这项工作的某些部分。在实践中,这种情况不常见,但可以有效。

接口转换和类型断言

类型开关是转换的一种形式:它们采用一个接口,并且对于开关中的每种情况,在某种意义上都将其转换为该情况的类型。这是下面的代码如何fmt.Printf使用类型开关将值转换为字符串的简化版本如果已经是字符串,则我们希望接口保留实际的字符串值,而如果它具有 String方法,则需要调用该方法的结果。

类型Stringer接口{
    String()字符串
}

var value interface {} //调用者提供的值。
切换str:=值。(类型){
大小写字符串:
    返回str
Case Stringer:
    返回str.String()
}

第一种情况找到了具体的价值。第二个将接口转换为另一个接口。这样混合类型就很好了。

如果我们只关心一种类型该怎么办?如果我们知道该值包含a,string 而我们只想提取它?单例类型开关可以,但类型断言也可以类型断言采用接口值并从中提取指定的显式类型的值。该语法是从打开类型开关的子句中借用的,但具有显式类型而不是type关键字:

值。(typeName)

结果是具有静态类型的新值typeName该类型必须是接口保留的具体类型,或者是可以将值转换为的第二种接口类型。为了提取我们知道值中的字符串,我们可以这样写:

str:=值。(字符串)

但是,如果事实证明该值不包含字符串,则程序将因运行时错误而崩溃。为了防止这种情况,请使用“逗号,好”惯用法来安全地测试该值是否为字符串:

str,好的:=值。((字符串)
如果可以,{
    fmt.Printf(“字符串值为:%q \ n”,str)
}其他{
    fmt.Printf(“值不是字符串\ n”)
}

如果类型断言失败,str该类型断言将仍然存在并且为字符串类型,但是它将具有零值(一个空字符串)。

为了说明该功能,这里有一个if-else 语句,它等效于打开此部分的类型开关。

如果是str,好的:= value。(string); 好 {
    返回str
}否则为str,确定:= value。(Stringer); 好 {
    返回str.String()
}

概论

如果类型仅存在于实现接口,并且永远不会有超出该接口的导出方法,则无需导出类型本身。仅导出接口即可清楚地知道该值除了接口中描述的内容外没有其他有趣的行为。它还避免了需要在通用方法的每个实例上重复文档。

在这种情况下,构造函数应返回接口值而不是实现类型。作为一个例子,在散列库既crc32.NewIEEEadler32.New 返回接口类型hash.Hash32在Go程序中将CRC-32算法替换为Adler-32只需更改构造函数调用即可;其余代码不受算法更改的影响。

一种相似的方法允许将各个crypto包中的流密码算法与它们链接在一起的分组密码分开。Block数据crypto/cipher包中接口指定了分组密码的行为,该密码提供了单个数据块的加密。然后,类似于该bufio包,实现该接口的密码包可用于构造该Stream接口表示的流密码,而无需了解块加密的细节。

该 crypto/cipher接口是这样的:

类型Block接口{
    BlockSize()int
    加密(dst,src [] byte)
    解密(dst,src [] byte)
}

类型Stream接口{
    XORKeyStream(dst,src [] byte)
}

这是计数器模式(CTR)流的定义,它将块密码转换为流密码。注意,分组密码的详细信息已被抽象化:

// NewCTR返回一个Stream,该Stream使用中的给定Block进行加密/解密
//计数器模式。iv的长度必须与块的块大小相同。
func NewCTR(block Block,iv [] byte)流

NewCTR不仅适用于一种特定的加密算法和数据源,而且适用于Block接口的任何实现以及任何 Stream因为它们返回接口值,所以用其他加密模式替换CTR加密是本地化的更改。构造函数调用必须进行编辑,但是由于周围的代码必须仅将结果视为a Stream,因此不会注意到差异。

接口和方法

由于几乎所有内容都可以附加方法,因此几乎所有内容都可以满足接口。包中有一个说明性示例http ,它定义了Handler接口。任何实现的对象Handler都可以处理HTTP请求。

类型Handler接口{
    ServeHTTP(ResponseWriter,* Request)
}

ResponseWriter本身是一个接口,提供对将响应返回给客户端所需的方法的访问。这些方法包括标准Write方法,因此 http.ResponseWriter可以在可以使用an的任何地方使用an io.Writer 。 Request是一个包含来自客户端的请求的已解析表示的结构。

为简便起见,让我们忽略POST,并假设HTTP请求始终是GET。简化不会影响处理程序的设置方式。这是处理程序的简单实现,用于计算访问页面的次数。

//简单的计数器服务器。
类型Counter struct {
    n int
}

func(ctr * Counter)ServeHTTP(w http.ResponseWriter,req * http.Request){
    ctr.n ++
    fmt.Fprintf(w,“ counter =%d \ n”,ctr.n)
}

(注意我们的主题,注意如何Fprintf打印到 http.ResponseWriter。)在真实服务器中,访问ctr.n需要防止并发访问。请参阅syncatomic软件包以获取建议。

供参考,这里是如何将这样的服务器附加到URL树上的节点。

导入“ net / http”
...
ctr:= new(Counter)
http.Handle(“ / counter”,ctr)

但是为什么要Counter构造一个结构?整数就足够了。(接收者必须是一个指针,这样增量才能对调用者可见。)

//更简单的计数器服务器。
类型Counter int

func(ctr * Counter)ServeHTTP(w http.ResponseWriter,req * http.Request){
    * ctr ++
    fmt.Fprintf(w,“ counter =%d \ n”,* ctr)
}

如果您的程序有一些内部状态需要通知已访问页面怎么办?将频道绑定到网页。

//每次访问都会发送通知的渠道。
//(可能希望通道被缓冲。)
类型Chan chan * http.Request

func(ch Chan)ServeHTTP(w http.ResponseWriter,req * http.Request){
    ch <-要求
    fmt.Fprint(w,“已发送通知”)
}

最后,假设我们要介绍/args调用服务器二进制文件时使用的参数。编写函数以打印参数很容易。

func ArgServer(){
    fmt.Println(os.Args)
}

我们如何将其变成HTTP服务器?我们可以使用ArgServer 某种类型的方法来忽略其值,但是有一种更简洁的方法。由于我们可以为除指针和接口之外的任何类型定义方法,因此我们可以为函数编写方法。http软件包包含以下代码:

// HandlerFunc类型是一个适配器,允许使用
//作为HTTP处理程序的普通函数。如果f是一个函数
//具有适当的签名,HandlerFunc(f)是一个
//调用f的处理程序对象。
键入HandlerFunc func(ResponseWriter,* Request)

// ServeHTTP调用f(w,req)。
func(f HandlerFunc)ServeHTTP(w ResponseWriter,req * Request){
    f(w,req)
}

HandlerFunc是具有方法的类型ServeHTTP,因此该类型的值可以处理HTTP请求。看一下该方法的实现:接收者是一个函数f,并且该方法调用f这看起来可能很奇怪,但是与接收方是通道和在该通道上发送方法没有什么不同。

为了ArgServer成为HTTP服务器,我们首先将其修改为具有正确的签名。

//参数服务器。
func ArgServer(w http.ResponseWriter,req * http.Request){
    fmt.Fprintln(w,os.Args)
}

ArgServer现在有相同的签名HandlerFunc,因此它可以被转换成该类型来访问它的方法,就像我们转换SequenceIntSlice 访问IntSlice.Sort设置它的代码很简洁:

http.Handle(“ / args”,http.HandlerFunc(ArgServer))

当有人访问该页面时/args,安装在该页面上的处理程序具有值ArgServer 和类型HandlerFuncHTTP服务器将以接收者的身份调用该ServeHTTP 类型的方法ArgServer,而后者将依次调用 ArgServer(通过f(w, req) 内部调用HandlerFunc.ServeHTTP)。然后将显示参数。

在本节中,我们通过结构,整数,通道和函数构造了HTTP服务器,这都是因为接口只是方法集,可以(几乎)定义任何类型。

空白标识符

for range循环 和map的上下文中,我们已经多次提到了空白标识符 可以使用任何类型的任何值来分配或声明空白标识符,并且可以无害地丢弃该值。这有点像写入Unix/dev/null文件:它表示只写值,用作需要变量但实际值无关的占位符。它的用途超出了我们已经看到的范围。

多重分配中的空白标识符

for range循环中使用空白标识符是一般情况的一种特殊情况:多重分配。

如果赋值在左侧需要多个值,但是程序不会使用其中一个值,则赋值左侧的空白标识符可以避免创建虚拟变量的需要,并明确说明:该值将被丢弃。例如,当调用返回一个值和一个错误但仅错误重要的函数时,请使用空白标识符丢弃不相关的值。

如果_,err:= os.Stat(path); os.IsNotExist(err){
	fmt.Printf(“%s不存在\ n”,路径)
}

有时,您会看到丢弃该错误值以忽略该错误的代码。这是可怕的做法。始终检查错误返回;提供它们是有原因的。

//不好!如果路径不存在,此代码将崩溃。
fi,_:= os.Stat(path)
如果fi.IsDir(){
    fmt.Printf(“%s是目录\ n”,路径)
}

未使用的导入和变量

导入包或声明变量而不使用它是错误的。未使用的导入会使程序and肿,并且编译缓慢,而已初始化但未使用的变量至少会浪费计算量,并且可能表明有一个较大的错误。但是,当程序正在积极开发中时,经常会出现未使用的导入和变量,并且为了继续进行编译而删除它们,而稍后又需要它们,可能会很烦人。空白标识符提供了一种解决方法。

这个写得一半的程序有两个未使用的导入(fmtio)和一个未使用的变量(fd),因此它不会编译,但是很高兴看到到目前为止的代码是否正确。

包主

导入(
    “ fmt”
    “ io”
    “日志”
    “ os”

func main(){
    fd,err:= os.Open(“ test.go”)
    如果err!= nil {
        log.Fatal(错误)
    }
    // TODO:使用fd。
}

要使对未使用进口商品的投诉保持沉默,请使用空白标识符来引用进口包裹中的符号。同样,将未使用的变量分配给fd 空白标识符将使未使用的变量错误消失。该版本的程序可以编译。

包主

导入(
    “ fmt”
    “ io”
    “日志”
    “ os”

var _ = fmt.Printf //用于调试;完成后删除。
var _ io.Reader     //用于调试;完成后删除。

func main(){
    fd,err:= os.Open(“ test.go”)
    如果err!= nil {
        log.Fatal(错误)
    }
    // TODO:使用fd。
    _ = fd
}

按照惯例,使进口错误无声的全局声明应在导入之后立即进行并加以注释,以使它们易于查找,并提醒以后进行清理。

导入有副作用

像一个未使用的进口fmtio在前面的例子应该最终被用于去除或空白分配识别码作为工作正在进行中。但是有时仅出于副作用导入软件包是有用的,而无需任何显式使用。例如, 程序包在其init功能期间net/http/pprof注册提供调试信息的HTTP处理程序。它具有导出的API,但是大多数客户端仅需要处理程序注册即可通过网页访问数据。要仅出于副作用导入软件包,请将软件包重命名为空白标识符:

导入_“ net / http / pprof”

这种导入形式清楚地表明该软件包是出于其副作用而被导入的,因为该软件包没有其他可能的用途:在此文件中,它没有名称。(如果这样做,并且我们没有使用该名称,则编译器将拒绝该程序。)

接口检查

正如我们在上面关于接口的讨论中所看到的,类型不需要显式声明它实现了接口。相反,类型仅通过实现接口的方法来实现接口。实际上,大多数接口转换都是静态的,因此在编译时进行检查。例如,除非实现接口,否则*os.File将an传递给期望的函数io.Reader将不会编译 。 *os.Fileio.Reader

但是,某些接口检查确实在运行时发生。encoding/json 包中有一个实例,它定义了一个Marshaler 接口。当JSON编码器收到实现该接口的值时,编码器将调用该值的编组方法将其转换为JSON,而不是执行标准转换。编码器在运行时使用以下类型断言检查此属性

m,好的:= val。(json.Marshaler)

如果只需要问一个类型是否实现了一个接口而不实际使用该接口本身(也许作为错误检查的一部分),则可以使用空白标识符忽略类型声明的值:

如果_,好的:= val。(json.Marshaler); 好 {
    fmt.Printf(“%T类型的值%v实现json.Marshaler \ n”,val,val)
}

这种情况出现的一个地方是,有必要在实现该类型的包中保证它实际上满足接口的情况。如果某个类型(例如) json.RawMessage需要自定义JSON表示形式,则应实现 json.Marshaler,但是没有静态转换会导致编译器自动对此进行验证。如果类型意外地不满足该接口,则JSON编码器仍将起作用,但将不使用自定义实现。为了确保实现正确,可以在包中使用使用空白标识符的全局声明:

var _ json.Marshaler =(* RawMessage)(无)

在这个声明中,涉及的转换分配 *RawMessageMarshaler 需要*RawMessage工具Marshaler,并且该属性将在编译时进行检查。如果json.Marshaler接口发生更改,则此软件包将不再编译,我们将注意到需要对其进行更新。

在此结构中出现空白标识符表示该声明仅存在于类型检查中,而不用于创建变量。但是,请不要对满足接口的每种类型执行此操作。按照惯例,只有在代码中不存在静态转换的情况下才使用此类声明,这种情况很少见。

嵌入

Go没有提供典型的类型驱动的子类化概念,但是它确实具有通过类型嵌入结构或接口中来“借用”实现的各个部分的能力

接口嵌入非常简单。我们之前提到过io.Readerandio.Writer接口;这是他们的定义。

类型Reader接口{
    读取(p [] byte)(n个整数,错误错误)
}

类型Writer接口{
    写(p [] byte)(n int,错误)
}

io软件包还出口,指定可以实现几个这样的方法的对象几个其他接口。例如,有io.ReadWriter一个同时包含Read的接口Write我们可以io.ReadWriter通过显式列出这两种方法来指定,但是将两个接口嵌入以形成新的接口更容易,也更具有启发性,如下所示:

// ReadWriter是结合Reader和Writer接口的接口。
键入ReadWriter接口{
    读者
    作家
}

这只是说,是什么样子:一个ReadWriter可以做一Reader一个什么Writer 呢; 它是嵌入式接口的并集。只有接口可以嵌入接口中。

相同的基本思想适用于结构,但意义更深远。所述bufio封装具有两个结构类型, bufio.Reader并且bufio.Writer,其中每个过程器具从包的类似接口的 io并且bufio还实现了一个缓冲的读取器/写入器,该操作通过使用嵌入将读取器和写入器组合为一个结构来完成:它列出了结构中的类型,但未提供字段名称。

// ReadWriter存储指向Reader和Writer的指针。
//实现io.ReadWriter。
输入ReadWriter struct {
    *读者// * bufio.Reader
    * Writer // * bufio.Writer
}

嵌入的元素是指向结构的指针,并且在使用它们之前,必须先初始化它们以指向有效的结构。ReadWriter结构可以写成

输入ReadWriter struct {
    读者*读者
    作家*作家
}

但是为了提升字段的方法并满足io接口要求,我们还需要提供转发方法,如下所示:

func(rw * ReadWriter)Read(p [] byte)(n int,err error){
    返回rw.reader.Read(p)
}

通过直接嵌入结构,可以避免这种簿记。嵌入式类型的方法来一起免费,这意味着bufio.ReadWriter 不仅具有方法bufio.Readerbufio.Writer,同时也满足了所有三个接口: io.Reader, io.Writer,和 io.ReadWriter

嵌入与子类化有一个重要的区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当它们被调用时,该方法的接收者是内部类型,而不是外部类型。在我们的示例中,调用Reada方法时bufio.ReadWriter,其效果与上面写出的转发方法完全相同;接收者是的reader领域,而ReadWriter不是 ReadWriter本身。

嵌入也可以很方便。此示例显示了一个嵌入的字段以及一个常规的命名字段。

输入Job struct {
    命令字符串
    *日志记录器
}

Job类型现在有PrintPrintfPrintln 和其他方法*log.LoggerLogger 当然,我们可以给一个字段名,但是没有必要这样做。现在,一旦初始化,我们就可以登录到Job

job.Println(“正在开始...”)

Logger是有规律场Job结构,所以我们可以用通常的方法进行初始化的构造函数中进行Job,这样,

func NewJob(命令字符串,logger * log.Logger)* Job {
    返回&Job {command,logger}
}

或使用复合文字,

job:=&Job {command,log.New(os.Stderr,“ Job:”,log.Ldate)}

如果我们需要直接引用一个嵌入式字段,则该字段的类型名称(忽略包限定符)将用作字段名称,就像在structRead方法中一样ReadWriter在这里,如果我们需要访问 *log.Logger一个的Job变量job,我们会写job.Logger,如果我们想要改进的方法,这将是有益的Logger

func(job * Job)Printf(format string,args ... interface {}){
    job.Logger.Printf(“%q:%s”,job.Command,fmt.Sprintf(format,args ...))
}

嵌入类型引入了名称冲突的问题,但是解决它们的规则很简单。首先,字段或方法X将其他任何项目隐藏X在该类型的更深层嵌套的部分中。如果log.Logger包含称为的字段或方法Command,则的Command字段Job将占主导地位。

其次,如果相同的名称出现在相同的嵌套级别,则通常是错误的。log.Logger如果Job结构包含另一个称为的字段或方法,则嵌入将是错误的Logger但是,如果从未在类型定义之外的程序中提及重复名称,则可以。这种资格保护提供了一些保护,以防止外部嵌入的类型发生更改。如果添加的字段与另一个子类型中的另一个字段发生冲突(如果这两个字段都不曾使用过),则没有问题。

并发

通过交流分享

并发编程是一个很大的话题,这里仅留有一些特定于Go的亮点。

实现对共享变量的正确访问所需的微妙之处使得在许多环境中进行并行编程变得很困难。Go鼓励采用一种不同的方法,在这种方法中,共享值在通道之间传递,并且实际上,决不由单独的执行线程主动共享。在任何给定时间,只有一个goroutine可以访问该值。根据设计,不会发生数据争用。为了鼓励这种思维方式,我们将其简化为一个口号:

不要通过共享内存进行通信;而是通过通信共享内存。

这种方法可能太过分了。例如,最好通过在整数变量周围放置一个互斥锁来最好地完成引用计数。但是作为一种高级方法,使用通道来控制访问权限使编写清晰,正确的程序变得更加容易。

考虑该模型的一种方法是考虑一个CPU上运行的典型单线程程序。它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个人交流;如果通信是同步器,则仍然不需要其他同步。例如,Unix管道非常适合此模型。尽管Go的并发方法起源于Hoare的通信顺序过程(CSP),但它也可以被视为Unix管道的类型安全的泛化。

Goroutines

之所以称为goroutine,是因为现有的术语(线程,协程,进程等)传达了不准确的含义。goroutine有一个简单的模型:它是在相同地址空间中与其他goroutine同时执行的函数。它是轻量级的,仅比分配堆栈空间花费更多。而且堆栈从小开始,因此价格便宜,并且可以通过根据需要分配(和释放)堆存储来增长。

Goroutine被多路复用到多个OS线程上,因此,如果一个应阻塞,例如在等待I / O时,其他将继续运行。他们的设计隐藏了线程创建和管理的许多复杂性。

给函数或方法调用加上前缀,以go 在新的goroutine中运行该调用。调用完成后,goroutine会静默退出。(效果类似于&在后台运行命令的Unix Shell 表示法。)

go list.Sort()//同时运行list.Sort; 不要等待。

函数文字在goroutine调用中可以派上用场。

func Announce(消息字符串,延迟时间。Duration){
    go func(){
        时间。睡眠(延迟)
        fmt.Println(消息)
    }()//注意括号-必须调用该函数。
}

在Go中,函数文字是闭包:实现可确保函数所引用的变量只要处于活动状态就可以保留。

这些示例不太实用,因为这些函数无法发出完成信号的方式。为此,我们需要渠道。

频道

与映射一样,通道也分配有make,并且结果值用作对基础数据结构的引用。如果提供了可选的整数参数,则它将设置通道的缓冲区大小。对于无缓冲或同步通道,默认值为零。

ci:= make(chan int)//无缓冲的整数通道
cj:= make(chan int,0)//无缓冲的整数通道
cs:= make(chan * os.File,100)//指向文件的指针的缓冲通道

无缓冲通道将通信(值的交换)与同步相结合,从而确保两个计算(goroutines)处于已知状态。

使用频道有很多不错的习惯用法。这是一个让我们开始的地方。在上一节中,我们在后台启动了排序。通道可以允许启动goroutine等待排序完成。

c:= make(chan int)//分配频道
//在goroutine中开始排序;完成后,在通道上发出信号。
go func(){
    list.Sort()
    c <-1 //发送信号;价值无所谓。
}()
doSomethingForAWhile()
<-c //等待排序完成;丢弃发送的值。

接收器始终阻塞,直到有数据要接收为止。如果通道未缓冲,则发送方将阻塞,直到接收方收到该值为止。如果通道具有缓冲区,则发送方仅阻塞该值,直到将值复制到缓冲区为止;否则,发送方才阻塞。如果缓冲区已满,则意味着要等到某些接收者检索到一个值。

可以像信号灯一样使用缓冲的通道,例如以限制吞吐量。在此示例中,传入的请求被传递到handle,后者将值发送到通道,处理该请求,然后从通道接收一个值,以为下一个使用者准备“信号量”。通道缓冲区的容量将同时呼叫的数量限制为process

var sem = make(chan int,MaxOutstanding)

func handle(r * Request){
    sem <-1 //等待活动队列耗尽。
    process(r)//可能需要很长时间。
    <-sem //完成;启用下一个请求运行。
}

func Serve(queue chan * Request){
    为{
        req:= <-队列
        go handle(req)//不要等待句柄完成。
    }
}

一旦MaxOutstanding处理程序执行process完毕,任何其他处理程序都将阻止尝试发送到已填充的通道缓冲区,直到现有处理程序之一完成并从缓冲区接收消息为止。

但是,这种设计有一个问题:Serve 即使每个请求MaxOutstanding 都可以随时运行,它也会为每个传入请求创建一个新的goroutine 如此一来,如果请求太快,程序可能会消耗无限的资源。我们可以通过更改Serve以控制goroutine的创建来解决该缺陷这是一个显而易见的解决方案,但是请注意,它有一个错误,我们将在随后修复:

func Serve(queue chan * Request){
    对于req:=范围队列{
        sem <-1
        go func(){
            process(req)//越野车; 请参阅下面的说明。
            <-sem
        }()
    }
}

错误是在Gofor循环中,循环变量在每次迭代中都会重复使用,因此该req 变量在所有goroutine中共享。那不是我们想要的。我们需要确保req每个goroutine都是唯一的。这是一种实现方法,将go的值req作为参数传递给goroutine中的闭包:

func Serve(queue chan * Request){
    对于req:=范围队列{
        sem <-1
        go func(req * Request){
            处理(要求)
            <-sem
        }(要求)
    }
}

将此版本与先前版本进行比较,以了解在声明和运行闭包的方式上的差异。另一个解决方案是仅创建一个具有相同名称的新变量,如下例所示:

func Serve(queue chan * Request){
    对于req:=范围队列{
        req:= req //为goroutine创建req的新实例。
        sem <-1
        go func(){
            处理(要求)
            <-sem
        }()
    }
}

写起来似乎很奇怪

要求:=要求

但这在Go中是合法且惯用的。您将获得具有相同名称的变量的新版本,有意在本地对循环变量进行阴影处理,但每个goroutine均具有唯一性。

回到编写服务器的一般问题,另一种很好管理资源的方法是启动handle所有从请求通道读取的固定数量的goroutine。goroutine的数量将同时调用的数量限制为processServe函数还接受一个将告知其退出的通道;启动goroutines后,它将阻止从该通道接收。

func handle(queue chan * Request){
    对于r:=范围队列{
        处理(r)
    }
}

func Serve(clientRequests chan *请求,退出chan bool){
    //启动处理程序
    对于我:= 0; 我<MaxOutstanding; 我++ {
        去处理(clientRequests)
    }
    <-quit //等待被告知退出。
}

渠道渠道

Go的最重要属性之一是通道是一流的值,可以像其他通道一样进行分配和传递。此属性的常见用法是实现安全的并行多路分解。

在上一节的示例中,它handle是请求的理想处理程序,但我们没有定义其处理的类型。如果该类型包括用于回复的渠道,则每个客户端可以提供自己的答案路径。这是type的示意图定义Request

输入Request struct {
    args [] int
    f func([] int)int
    结果陈灿诠释
}

客户端提供一个函数及其参数,以及在请求对象内部接收答案的通道。

func sum(a [] int)(s int){
    对于_,v:=范围a {
        s + = v
    }
    返回
}

request:=&Request {[] int {3,4,5},sum,make(chan int)}
// 发送请求
clientRequests <-请求
//等待响应。
fmt.Printf(“ answer:%d \ n”,<-request.resultChan)

在服务器端,处理程序功能是唯一更改的东西。

func handle(queue chan * Request){
    对于req:=范围队列{
        req.resultChan <-req.f(req.args)
    }
}

要使它变得现实,显然还有很多工作要做,但是此代码是一个用于速率受限,并行,无阻塞RPC系统的框架,并且看不到互斥量。

并行化

这些想法的另一个应用是使多个CPU内核之间的计算并行化。如果可以将计算分解为可以独立执行的单独部分,则可以并行化计算,并在每个部分完成时发出信号。

假设我们要对项向量执行昂贵的操作,并且在此理想示例中,对每个项的操作值是独立的。

类型Vector [] float64

//将运算应用于v [i],v [i + 1] ...直至v [n-1]。
func(v Vector)DoSome(i,n int,u Vector,c chan int){
    为; 我<n; 我++ {
        v [i] + = u.Op(v [i])
    }
    c <-1 //表示此片段已完成
}

我们以循环方式独立启动各个部分,每个CPU一个。他们可以按任何顺序完成,但这无关紧要。在启动所有goroutine之后,我们通过排空通道来计数完成信号。

const numCPU = 4 // CPU核心数

func(v Vector)DoAll(u Vector){
    c:= make(chan int,numCPU)//缓冲是可选的,但是明智的。
    对于我:= 0; 我<numCPU; 我++ {
        去v.DoSome(i * len(v)/ numCPU,(i + 1)* len(v)/ numCPU,u,c)
    }
    //清空频道。
    对于我:= 0; 我<numCPU; 我++ {
        <-c //等待一项任务完成
    }
    // 全做完了。
}

可以为运行时询问合适的值,而不是为numCPU创建一个常数。该函数runtime.NumCPU 返回机器中硬件CPU内核的数量,因此我们可以编写

var numCPU = runtime.NumCPU()

还有一个功能 runtime.GOMAXPROCS,可以报告(或设置)Go程序可以同时运行的用户指定的内核数。它的默认值为,runtime.NumCPU但可以通过设置类似名称的shell环境变量或使用正数调用该函数来覆盖用零调用它只是查询值。因此,如果我们想满足用户的资源请求,我们应该写

var numCPU = runtime.GOMAXPROCS(0)

确保不要混淆并发的思想(将程序构造为独立执行的组件)和并行性,并发执行并行计算以提高多个CPU的效率。尽管Go的并发特性可以使一些问题易于并行计算,但Go是一种并发语言,而不是并行语言,并且并非所有并行化问题都适合Go的模型。有关区别的讨论,请参见此博客文章中引用的演讲 

缓冲区泄漏

并发编程工具甚至可以使非并发思想更容易表达。这是从RPC包中抽象出来的示例。客户端goroutine循环从某个来源(可能是网络)接收数据。为了避免分配和释放缓冲区,它会保留一个空闲列表,并使用一个缓冲的通道来表示它。如果通道为空,则会分配一个新的缓冲区。消息缓冲区准备就绪后,它将通过发送到服务器 serverChan

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

func client(){
    为{
        var b *缓冲区
        //获取一个缓冲区(如果有);如果没有分配。
        选择 {
        情况b = <-freeList:
            // 拿到一个; 无事可做。
        默认:
            //没有一个免费的,因此分配一个新的。
            b = new(缓冲区)
        }
        load(b)//从网上读取下一条消息。
        serverChan <-b //发送到服务器。
    }
}

服务器循环从客户端接收每个消息,对其进行处理,然后将缓冲区返回到空闲列表。

func server(){
    为{
        b:= <-serverChan //等待工作。
        处理(b)
        //如果有空间,请重新使用缓冲区。
        选择 {
        case freeList <-b:
            //空闲列表中的缓冲区;无事可做。
        默认:
            //免费列表已满,只需继续。
        }
    }
}

客户端尝试从中检索缓冲区freeList如果没有可用的,它将分配一个新的。除非列表已满,否则服务器的“发送至”freeList将放b回到空闲列表中,在这种情况下,缓冲区将被放置在地板上以由垃圾收集器回收。default语句中的子句在select 没有其他情况下准备就绪时执行,这意味着selects永不阻塞。)此实现仅依靠几行内容就建立了一个无泄漏存储桶列表,并依赖于缓冲通道和垃圾收集器进行簿记。

失误

库例程必须经常向调用者返回某种错误指示。如前所述,Go的多值返回可以轻松地在正常返回值旁边返回详细的错误描述。使用此功能提供详细的错误信息是一种很好的方式。例如,正如我们将看到的那样,os.Open不仅nil在失败时返回一个指针,还会返回一个描述错误原因的错误值。

按照惯例,错误的类型为error,是一个简单的内置接口。

类型错误界面{
    Error()字符串
}

图书馆作者可以自由地用一个更丰富的模型来实现此接口,从而不仅可以看到错误,而且可以提供一些上下文。如前所述,除了通常的*os.File 返回值之外,os.Open还返回错误值。如果文件成功打开,则错误将为nil,但出现问题时它将包含 os.PathError

// PathError记录错误,并且操作和
//导致它的文件路径。
输入PathError struct {
    操作字符串//“打开”,“取消链接”等
    路径字符串//关联的文件。
    错误错误//由系统调用返回。
}

func(e * PathError)Error()字符串{
    返回e.Op +“” + e.Path +“:” + e.Err.Error()
}

PathError的会Error产生一个像这样的字符串:

打开/ etc / passwx:没有这样的文件或目录

这种错误,包括有问题的文件名,操作以及所触发的操作系统错误,即使在导致错误的调用远未打印的情况下也有用。它比普通的“没有这样的文件或目录”提供更多信息。

在可行的情况下,错误字符串应标识其来源,例如通过使用前缀来命名产生错误的操作或程序包。例如,在package中 image,由于未知格式导致的解码错误的字符串表示形式是“ image:unknown format”。

关心精确错误详细信息的调用者可以使用类型切换或类型断言来查找特定错误并提取详细信息。为此,PathErrors 可能包括检查内部Err 字段是否存在可恢复的故障。

尝试:= 0; 尝试<2; 试试++ {
    文件,err = os.Create(文件名)
    如果err == nil {
        返回
    }
    如果e,好的:=错误(* os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()//恢复一些空间。
        继续
    }
    返回
}

if这里 的第二条语句是另一种类型断言如果失败,ok则为false,e 为nil如果成功, 则为oktrue,表示错误的类型为*os.PathError,然后为e,我们可以检查该错误的更多信息。

恐慌

向调用者报告错误的通常方法是返回an error作为额外的返回值。规范 Read方法是一个众所周知的实例。它返回一个字节数和一个error但是,如果错误无法恢复怎么办?有时程序根本无法继续。

为此,有一个内置函数panic 实际上会创建一个运行时错误,该错误将使程序停止运行(但请参阅下一节)。该函数采用一个任意类型的参数(通常是字符串),以便在程序死亡时打印出来。这也是一种指示发生了不可能的事情的方法,例如退出无限循环。

//使用牛顿方法的多维数据集根的玩具实现。
func CubeRoot(x float64)float64 {
    z:= x / 3 //任意初始值
    对于我:= 0; 我<1e6; 我++ {
        上一个:= z
        z-=(z * z * zx)/(3 * z * z)
        如果veryClose(z,prevz){
            返回z
        }
    }
    //一百万次迭代尚未收敛;出了点问题。
    恐慌(fmt.Sprintf(“ CubeRoot(%g)未收敛”,x))
}

这只是一个示例,但实际的库函数应避免使用panic如果问题可以掩盖或解决,最好还是让事情继续运行而不是取消整个程序。一个可能的反例是在初始化期间:如果该库确实无法进行设置,那么恐慌是可以理解的。

var user = os.Getenv(“ USER”)

func init(){
    如果用户==“” {
        panic(“ $ USER没有价值”)
    }
}

恢复

panic被调用时(包括对运行时错误的隐式调用,例如,对切片进行索引超出范围或类型声明失败),它将立即停止当前函数的执行并开始展开goroutine的堆栈,并在此过程中运行所有延迟函数。如果解散到达goroutine栈的顶部,程序将终止。但是,可以使用内置函数recover来重新获得对goroutine的控制并恢复正常执行。

调用将recover停止展开并返回传递给的参数panic因为展开时运行的唯一代码是在延迟函数内部,recover 所以仅在延迟函数内部有用。

一种应用recover是关闭服务器内部失败的goroutine,而不会杀死其他正在执行的goroutine。

func服务器(workChan <-chan *工作){
    工作:=范围workChan {
        安全去做(工作)
    }
}

func safeDo(工作*工作){
    延迟func(){
        如果err:= recovery(); err!= nil {
            log.Println(“工作失败:”,错误)
        }
    }()
    做工作)
}

在此示例中,如果出现do(work)紧急情况,将记录结果,并且goroutine将干净地退出而不会打扰其他程序。延迟的关闭过程中无需执行任何其他操作;调用recover完全处理条件。

因为recover总是返回,nil除非直接从延迟函数调用,因此延迟代码可以调用本身使用的库例程,panicrecover不会失败。例如,in中的deferred函数safelyDo可能在调用之前先调用日志记录函数recover,并且该日志记录代码将不受恐慌状态的影响。

有了我们的恢复模式,该do 函数(及其调用的任何函数)都可以通过调用彻底清除任何不良情况panic我们可以使用该想法来简化复杂软件中的错误处理。让我们看一下regexp软件包的理想版本,该版本通过调用panic本地错误类型来报告解析错误这是Errorerror方法和Compile函数的定义

// Error是解析错误的类型;它满足错误界面。
类型错误字符串
func(e Error)Error()字符串{
    返回字符串(e)
}

//错误是* Regexp的一种方法,通过以下方法报告解析错误
//出现错误时惊慌失措。
func(regexp * Regexp)错误(错误字符串){
    恐慌(错误(错误))
}

//编译返回正则表达式的解析表示形式。
func Compile(str字符串)(regexp * Regexp,错误错误){
    regexp = new(正则表达式)
    //如果存在解析错误,doParse将会恐慌。
    延迟func(){
        如果e:= recovery(); e!= nil {
            regexp = nil //清除返回值。
            err = e。(Error)//如果不是解析错误,将重新出现紧急情况。
        }
    }()
    返回regexp.doParse(str),nil
}

如果出现doParse紧急情况,恢复块会将返回值设置为—nil延迟函数可以修改命名的返回值。然后err,它将通过断言其具有本地类型来检查问题是否为解析错误Error如果不是这样,则类型声明将失败,从而导致运行时错误,该错误将继续展开堆栈,就像没有任何中断一样。此检查意味着,如果发生意外情况(例如索引超出范围),即使我们正在使用panicrecover处理解析错误,代码也将失败

有了错误处理,该error方法(因为它是绑定到类型的方法,所以它很好,甚至很自然,因为它具有与内置error类型相同的名称),可以很容易地报告解析错误,而不必担心展开解析堆栈用手:

如果pos == 0 {
    re.error(“'*'在表达式开始时是非法的”)
}

尽管此模式很有用,但应仅在包内使用。 Parse将内部panic调用转化为 error价值;它不会panics 向其客户公开这是遵循的好规则。

顺便说一句,如果发生实际错误,此重新恐慌习惯用法会更改恐慌值。但是,原始故障和新故障都会在崩溃报告中显示,因此问题的根本原因仍然可见。因此,这种简单的重新恐慌方法通常就足够了-毕竟是崩溃。但是,如果您只想显示原始值,则可以编写更多代码来过滤意外问题并使用原始错误重新恐慌。留给读者练习。

Web服务器

让我们完成一个完整的Go程序,一个Web服务器。这实际上是一种Web重新服务器。Google提供了一项服务,chart.apis.google.com 可以将数据自动格式化为图表和图形。但是,很难以交互方式使用它,因为您需要将数据作为查询放入URL。这里的程序为一种数据形式提供了一个更好的接口:给定一小段文本,它会在图表服务器上调用以产生QR码,即编码文本的盒子矩阵。该图像可以用手机的摄像头捕获,并解释为例如URL,从而省去了在手机的小键盘上键入URL的麻烦。

这是完整的程序。解释如下。

包主

导入(
    “旗”
    “ html /模板”
    “日志”
    “ net / http”

var addr = flag.String(“ addr”,“:1718”,“ http服务地址”)// Q = 17,R = 18

var templ = template.Must(template.New(“ qr”)。Parse(templateStr))

func main(){
    flag.Parse()
    http.Handle(“ /”,http.HandlerFunc(QR))
    错误:= http.ListenAndServe(* addr,nil)
    如果err!= nil {
        log.Fatal(“ ListenAndServe:”,err)
    }
}

func QR(w http.ResponseWriter,req * http.Request){
    templ.Execute(w,req.FormValue(“ s”))
}

const templateStr =`
<html>
<头>
<title> QR链接生成器</ title>
</ head>
<身体>
{{if。}}
<img src =“ http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl= {{。}}” />
<br>
{{。}}
<br>
<br>
{{结束}}
<form action =“ /” name = f method =“ GET”>
    <input maxLength = 1024 size = 70 name = s value =“” title =“文本到QR编码”>
    <input type =提交值=“ Show QR” name = qr>
</ form>
</ body>
</ html>
`

最多的部分main应该易于遵循。一个标志为我们的服务器设置默认的HTTP端口。模板变量templ是有趣的地方。它构建了一个HTML模板,该模板将由服务器执行以显示页面。稍后了解更多。

main函数解析标志,并使用我们上面讨论的机制将函数绑定QR到服务器的根路径。然后http.ListenAndServe被称为启动服务器;服务器运行时会阻塞。

QR只会接收包含表单数据的请求,并以名为的表单值对数据执行模板s

模板包html/template功能强大;该程序仅涉及其功能。本质上,它通过替换从传递给的数据项派生的元素templ.Execute(在本例中为表单值)来即时重写HTML文本在模板文本(templateStr)中,用双括号分隔的段表示模板动作。仅当当前数据项的值(点)为非空时,from{{if .}} 才{{end}}执行.即,当字符串为空时,该模板部分被抑制。

这两个摘要{{.}}表示要在网页上显示提供给模板的数据(查询字符串)。HTML模板包会自动提供适当的转义符,因此可以安全地显示文本。

模板字符串的其余部分只是页面加载时显示的HTML。如果解释太快,请参阅 模板包文档以进行更全面的讨论。

在那里,您可以找到:一个有用的Web服务器,其中包含几行代码以及一些数据驱动的HTML文本。Go的功能强大到足以在几行中完成很多事情。

 

posted @ 2020-12-15 16:41  CharyGao  阅读(135)  评论(0)    收藏  举报