Go语言实战(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-06 02:39:30

点击下载

作者:(美)威廉·肯尼迪(William Kennedy)布赖恩·克特森(Brian Ketelsen)

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Go语言实战

Go语言实战试读:

前言

那是2013年10月,我刚刚花几个月的时间写完GoingGo.net博客,就接到了Brian Ketelsen和Erik St. Martin的电话。他们正在写这本书,问我是否有兴趣参与进来。我立刻抓住机会,参与到写作中。当时,作为一个Go语言的新手,这是我进一步了解这门语言的好机会。毕竟,与Brian和Erik一起工作、一起分享获得的知识,比我从构建博客中学到的要多得多。

完成前4章后,我们在Manning早期访问项目(MEAP)中发布了这本书。很快,我们收到了来自语言团队成员的邮件。这位成员对很多细节提供了评审意见,还附加了大量有用的知识、意见、鼓励和支持。根据这些评审意见,我们决定从头开始重写第2章,并对第4章进行了全面修订。据我们所知,对整章进行重写的情况并不少见。通过这段重写的经历,我们学会要依靠社区的帮助来完成写作,因为我们希望能立刻得到社区的支持。

自那以后,这本书就成了社区努力的成果。我们投入了大量的时间研究每一章,开发样例代码,并和社区一起评审、讨论并编辑书中的材料和代码。我们尽了最大的努力来保证本书在技术上没有错误,让代码符合通用习惯,并且使用社区认为应该有的方式来教Go语言。同时,我们也融入了自己的思考、自己的实践和自己的指导方式。

我们希望本书能帮你学习Go语言,不仅是当下,就是多年以后,你也能从本书中找到有用的东西。Brian、Erik和我总会在线上帮助那些希望得到我们帮助的人。如果你购买了本书,谢谢你,来和我们打个招呼吧。William Kennedy致谢

我们花了18个月的时间来写本书。但是,离开下面这些人的支持,我们不可能完成这本书:我们的家人、朋友、同学、同事以及导师,整个Go社区,还有我们的出版商Manning。

当你开始撰写类似的书时,你需要一位编辑。编辑不仅要分享喜悦与成就,而且要不惜一切代价,帮你渡过难关。Jennifer Stout,你才华横溢,善于指导,是很棒的朋友。感谢你这段时间的付出,尤其是在我们最需要你的时候。感谢你让这本书变成现实。还要感谢为本书的开发和出版作出贡献的Manning的其他人。

每个人都不可能知晓一切,所以需要社区里的人付出时间和学识。感谢Go社区以及所有参与本书不同阶段书稿评审并提供反馈的人。特别感谢Adam McKay、Alex Basile、Alex Jacinto、Alex Vidal、Anjan Bacchu、Benoît Benedetti、Bill Katz、Brian Hetro、Colin Kennedy、Doug Sparling,、Jeffrey Lim、Jesse Evans、Kevin Jackson、Mark Fisher、Matt Zulak、Paulo Pires、Peter Krey、Philipp K. Janert、Sam Zaydel以及Thomas O’Rourke。还要感谢Jimmy Frasché,他在出版前对本书书稿做了快速、准确的技术审校。

这里还需要特别感谢一些人。

Kim Shrier,从最开始就在提供评审意见,并花时间来指导我们。我们从你那里学到了很多,非常感谢。因为你,本书在技术上达到了更好的境界。

Bill Hathaway在写书的最后一年,深入参与,并校正了每一章。你的想法和意见非常宝贵。我们必须给予Bill“第9章合著者”的头衔。没有Bill的参与、天赋以及努力,就没有这一章的存在。

我们还要特别感谢Cory Jacobson、Jeffery Lim、Chetan Conikee和Nan Xiao为本书持续提供了评审意见和指导,感谢Gabriel Aszalos、Fatih Arslan、Kevin Gillette和Jason Waldrip帮助评审样例代码,还要特别感谢Steve Francia帮我们作序,认可我们的工作。

最后,我们真诚地感谢我们的家人和朋友。为本书付出的时间和代价,总会影响到你所爱的人。William Kennedy

我首先要感谢Lisa,我美丽的妻子,以及我的5个孩子:Brianna、Melissa、Amanda、Jarrod和Thomas。Lisa,我知道你和孩子们有太多的日夜和周末,缺少丈夫和父亲的陪伴。感谢你让我这段时间全力投入本书的工作:我爱你们,爱你们每一个人。

我也要感谢我生意上的伙伴Ed Gonzalez、创意经理Erick Zelaya,以及整个Ardan工作室的团队。Ed,感谢你从一开始就支持我。没有你,我就无法完成本书。你不仅是生意伙伴,还是朋友和兄长:谢谢你。Erick,感谢你为我、为公司做的一切。我不确定没有你,我们还能不能做到这一切。Brian Ketelsen

首先要感谢我的家人在我写书的这4年间付出的耐心。Christine、Nathan、Lauren和Evelyn,感谢你们在游泳时放过在旁边椅子上写作的我,感谢你们相信这本书一定会出版。Erik St. Martin

我要感谢我的未婚妻Abby以及我的3个孩子Halie、Wyatt和Allie。感谢你们对我花大量时间写书和组织会议如此耐心和理解。我非常爱你们,有你们我非常幸运。

还要感谢Bill Kennedy为本书付出的巨大努力,以及当我们需要他的帮助的时候,他总是立刻想办法组织GopherCon来满足我们的要求。还要感谢整个社区出力评审并给出一些鼓励的话。关于本书

Go是一门开源的编程语言,目的在于降低构建简单、可靠、高效软件的门槛。尽管这门语言借鉴了很多其他语言的思想,但是凭借自身统一和自然的表达,Go程序在本质上完全不同于用其他语言编写的程序。Go平衡了底层系统语言的能力,以及在现代语言中所见到的高级特性。你可以依靠Go语言来构建一个非常快捷、高性能且有足够控制力的编程环境。使用Go语言,可以写得更少,做得更多。谁应该读这本书

本书是写给已经有一定其他语言编程经验,并且想学习Go语言的中级开发者的。我们写这本书的目的是,为读者提供一个专注、全面且符合语言习惯的视角。我们同时关注语言的规范和实现,涉及的内容包括语法、类型系统,并发、通道、测试以及其他一些主题。我们相信,对于刚开始学Go语言的人,以及想要深入了解这门语言内部实现的人来说,本书都是极佳的选择。章节速览

本书由9章组成,每章内容简要描述如下。● 第1章快速介绍这门语言是什么,为什么要创造这门语言,以及

这门语言要解决什么问题。这一章还会简要介绍一些Go语言的

核心概念,如并发。● 第2章引导你完成一个完整的Go程序,并教你Go作为一门编程

语言必须提供的特性。● 第3章介绍打包的概念,以及搭建Go工作空间和开发环境的最佳

实践。这一章还会展示如何使用Go语言的工具链,包括获取和

构建代码。● 第4章展示Go语言内置的类型,即数组、切片和映射。还会解释

这些数据结构背后的实现和机制。● 第5章详细介绍Go语言的类型系统,从结构体类型到具名类型,

再到接口和类型嵌套。这一章还会展示如何综合利用这些数据结

构,用简单的方法来设计结构并编写复杂的软件。● 第6章深入展示Go调度器、并发和通道是如何工作的。这一章还

将介绍这个方面背后的机制。● 第7章基于第6章的内容,展示一些实际开发中用到的并发模

式。你会学到为了控制任务如何实现一个goroutine池,以及如何

利用池来复用资源。● 第8章对标准库进行探索,深入介绍3个包,即log、json和io。这

一章专门介绍这3个包之间的某些复杂关系。● 第9章以如何利用测试和基准测试框架来结束全书。读者会学到

如何写单元测试、表组测试以及基准测试,如何在文档中增加示

例,以及如何把这些示例当作测试使用。关于代码

本书中的所有代码都使用等宽字体表示,以便和周围的文字区分开。在很多代码清单中,代码被注释是为了说明关键概念,并且有时在正文中会用数字编号来给出对应代码的其他信息。

本书的源代码既可以在Manning网站(www.manning.com/books/①go-in-action)上下载,也可以在GitHub(https://github.com/goinaction/code)上找到这些源代码。读者在线

购买本书后,可以在线访问由Manning出版社提供的私有论坛。在这个论坛上可以对本书做评论,咨询技术问题,并得到作者或其他读者的帮助。通过浏览器访问www.manning.com/books/go-in-action可以访问并订阅这个论坛。这个网页还提供了注册后如何访问论坛,论坛提供什么样的帮助,以及论坛的行为准则等信息。

Manning向读者承诺提供一个读者之间以及读者和作者之间交流的场所。Manning并不承诺作者一定会参与,作者参与论坛的行为完全出于作者自愿(没有报酬)。我们建议你向作者提一些有挑战性的问题,否则可能提不起作者的兴趣。

只要本书未绝版,作者在线论坛以及早期讨论的存档就可以在出版商的网站上获取到。关于作者

William Kennedy(@goinggodotnet)是Ardan工作室的管理合伙人。这家工作室位于佛罗里达州迈阿密,是一家专注移动、Web和系统开发的公司。他也是博客GoingGo.net的作者,迈阿密Go聚会的组织者。从在培训公司Ardan Labs开始,他就专注于Go语言教学。无论是在当地,还是在线上,经常可以在大会或者工作坊中看到他的身影。他总是找时间来帮助那些想获取Go语言知识、撰写博客和编码技能提升到更高水平的公司或个人。

Brian Ketelsen(@bketelsen)是XOR Data Exchange的CIO和联合创始人。Brian也是每年Go语言大会(GohperCon)的合办者,同时也是Gopher Academy的创立者。作为专注于社区的组织,Gopher Academy一直在促进Go语言的发展和对Go语言开发者的培训。Brian从2010年就开始使用Go语言。

Erik St. Martin(@erikstmartin)是XOR Data Exchange的软件开发总监。他所在的公司专注于大数据分析,最早在得克萨斯州奥斯汀,后来搬到了佛罗里达州坦帕湾。Erik长时间为开源软件及其社区做贡献。他是每年GopherCon的组织者,也是坦帕湾Go聚会的组织者。他非常热爱Go语言及Go语言社区,积极寻求促进社区成长的新方法。

①本书源代码也可以从www.epubit.com.cn本书网页免费下载。关于封面插图

本书封面插图的标题为“来自东印度的人”。这幅图选自伦敦的Thomas Jefferys的《A Collection of the Dresses of Different Nations, Ancient and Modern》(4卷),出版于1757年到1772年之间。书籍首页说明了这幅画的制作工艺是铜版雕刻,手工上色,外层用阿拉伯胶做保护。Thomas Jefferys(1719—1771)被称作“地理界的乔治三世国王”。作为制图者,他在当时英国地图商中处于领先地位。他为政府和其他官员雕刻和印刷地图,同时也制作大量的商业地图和地图册,尤其是北美地图。他作为地图制作者的经历,点燃了他收集各地风俗服饰的兴趣,最终成就了这部衣着集。

对遥远大陆的着迷以及对旅行的乐趣,是18世纪晚期才兴起的现象。这类收集品也风行一时,向实地旅行家和空想旅行家们介绍各地的风俗。Jefferys的画集如此多样,生动地向我们描述了200年前世界上不同民族的独立特征。从那之后,衣着的特征发生了改变,那个时代不同地区和国家的多样性,也逐渐消失。现在,很难再通过本地居民的服饰来区分他们所在的大陆。也许,从乐观的方面看,也许,从乐观的角度来看,我们用文化的多样性换取了更加多样化的个人生活——当然也是更加多样化和快节奏的科技生活。

在很难将一本计算机书与另一本区分开的时代,Manning创造性地将两个世纪以前不同地区的多样性,附着在计算机行业的图书封面上,借以来赞美计算机行业的创造力和进取精神也为Jeffreys的画带来了新的生命。第1章关于Go语言的介绍

本章主要内容● 用Go语言解决现代计算难题● 使用Go语言工具

计算机一直在演化,但是编程语言并没有以同样的速度演化。现在的手机,内置的CPU核数可能都多于我们使用的第一台电脑。高性能服务器拥有64核、128核,甚至更多核。但是我们依旧在使用为单核设计的技术在编程。

编程的技术同样在演化。大部分程序不再由单个开发者来完成,而是由处于不同时区、不同时间段工作的一组人来完成。大项目被分解为小项目,指派给不同的程序员,程序员开发完成后,再以可以在各个应用程序中交叉使用的库或者包的形式,提交给整个团队。

如今的程序员和公司比以往更加信任开源软件的力量。Go语言是一种让代码分享更容易的编程语言。Go 语言自带一些工具,让使用别人写的包更容易,并且 Go 语言也让分享自己写的包更容易。

在本章中读者会看到Go语言区别于其他编程语言的地方。Go语言对传统的面向对象开发进行了重新思考,并且提供了更高效的复用代码的手段。Go语言还让用户能更高效地利用昂贵服务器上的所有核心,而且它编译大型项目的速度也很快。

在阅读本章时,读者会对影响Go语言形态的很多决定有一些认识,从它的并发模型到快如闪电的编译器。我们在前言中提到过,这里再强调一次:这本书是写给已经有一定其他编程语言经验、想学习Go语言的中级开发者的。本书会提供一个专注、全面且符合习惯的视角。我们同时专注语言的规范和实现,涉及的内容包括语法、Go语言的类型系统、并发、通道、测试以及其他一些非常广泛的主题。我们相信,对刚开始要学习Go语言和想要深入了解语言内部实现的人来说,本书都是最佳选择。

本书示例中的源代码可以在https://github.com/goinaction/code下载。

我们希望读者能认识到,Go语言附带的工具可以让开发人员的生活变得更简单。最后,读者会意识到为什么那么多开发人员用Go语言来构建自己的新项目。1.1 用Go解决现代编程难题

Go语言开发团队花了很长时间来解决当今软件开发人员面对的问题。开发人员在为项目选择语言时,不得不在快速开发和性能之间做出选择。C和C++这类语言提供了很快的执行速度,而Ruby和Python这类语言则擅长快速开发。Go语言在这两者间架起了桥梁,不仅提供了高性能的语言,同时也让开发更快速。

在探索Go语言的过程中,读者会看到精心设计的特性以及简洁的语法。作为一门语言,Go不仅定义了能做什么,还定义了不能做什么。Go语言的语法简洁到只有几个关键字,便于记忆。Go语言的编译器速度非常快,有时甚至会让人感觉不到在编译。所以,Go开发者能显著减少等待项目构建的时间。因为Go语言内置并发机制,所以不用被迫使用特定的线程库,就能让软件扩展,使用更多的资源。Go语言的类型系统简单且高效,不需要为面向对象开发付出额外的心智,让开发者能专注于代码复用。Go语言还自带垃圾回收器,不需要用户自己管理内存。让我们快速浏览一下这些关键特性。1.1.1 开发速度

编译一个大型的C或者C++项目所花费的时间甚至比去喝杯咖啡的时间还长。图1-1是XKCD中的一幅漫画,描述了在办公室里开小差的经典借口。图1-1 努力工作?(来自XKCD)

Go语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。编译Go程序时,编译器只会关注那些直接被引用的库,而不是像Java、C和C++那样,要遍历依赖链中所有依赖的库。因此,很多Go程序可以在1秒内编译完。在现代硬件上,编译整个Go语言的源码树只需要20秒。

因为没有从编译代码到执行代码的中间过程,用动态语言编写应用程序可以快速看到输出。代价是,动态语言不提供静态语言提供的类型安全特性,不得不经常用大量的测试套件来避免在运行的时候出现类型错误这类bug。

想象一下,使用类似JavaScript这种动态语言开发一个大型应用程序,有一个函数期望接收一个叫作ID的字段。这个参数应该是整数,是字符串,还是一个UUID?要想知道答案,只能去看源代码。可以尝试使用一个数字或者字符串来执行这个函数,看看会发生什么。在Go语言里,完全不用为这件事情操心,因为编译器就能帮用户捕获这种类型错误。1.1.2 并发

作为程序员,要开发出能充分利用硬件资源的应用程序是一件很难的事情。现代计算机都拥有多个核,但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源。这些语言需要写大量的线程同步代码来利用多个核,很容易导致错误。

Go语言对并发的支持是这门语言最重要的特性之一。goroutine很像线程,但是它占用的内存远少于线程,使用它需要的代码更少。通道(channel)是一种内置的数据结构,可以让用户在不同的goroutine之间同步发送具有类型的消息。这让编程模型更倾向于在goroutine之间发送消息,而不是让多个goroutine争夺同一个数据的使用权。让我们看看这些特性的细节。1.goroutine

goroutine是可以与其他goroutine并行执行的函数,同时也会与主程序(程序的入口)并行执行。在其他编程语言中,你需要用线程来完成同样的事情,而在Go语言中会使用同一个线程来执行多个goroutine。例如,用户在写一个Web服务器,希望同时处理不同的Web请求,如果使用C或者Java,不得不写大量的额外代码来使用线程。在Go语言中,net/http库直接使用了内置的goroutine。每个接收到的请求都自动在其自己的goroutine里处理。goroutine使用的内存比线程更少,Go语言运行时会自动在配置的一组逻辑处理器上调度执行goroutine。每个逻辑处理器绑定到一个操作系统线程上(见图1-2)。这让用户的应用程序执行效率更高,而开发工作量显著减少。图1-2 在单一系统线程上执行多个goroutine

如果想在执行一段代码的同时,并行去做另外一些事情,goroutine是很好的选择。下面是一个简单的例子:func log(msg string) { ...这里是一些记录日志的代码}// 代码里有些地方检测到了错误go log("发生了可怕的事情")

关键字go是唯一需要去编写的代码,调度log函数作为独立的goroutine去运行,以便与其他goroutine并行执行。这意味着应用程序的其余部分会与记录日志并行执行,通常这种并行能让最终用户觉得性能更好。就像之前说的,goroutine占用的资源更少,所以常常能启动成千上万个goroutine。我们会在第6章更加深入地探讨goroutine和并发。2.通道

通道是一种数据结构,可以让goroutine之间进行安全的数据通信。通道可以帮用户避免其他语言里常见的共享内存访问的问题。

并发的最难的部分就是要确保其他并发运行的进程、线程或goroutine不会意外修改用户的数据。当不同的线程在没有同步保护的情况下修改同一个数据时,总会发生灾难。在其他语言中,如果使用全局变量或者共享内存,必须使用复杂的锁规则来防止对同一个变量的不同步修改。

为了解决这个问题,通道提供了一种新模式,从而保证并发修改时的数据安全。通道这一模式保证同一时刻只会有一个goroutine修改数据。通道用于在几个运行的goroutine之间发送数据。在图1-3中可以看到数据是如何流动的示例。想象一个应用程序,有多个进程需要顺序读取或者修改某个数据,使用goroutine和通道,可以为这个过程建立安全的模型。图1-3 使用通道在goroutine之间安全地发送数据

图1-3中有3个goroutine,还有2个不带缓存的通道。第一个goroutine通过通道把数据传给已经在等待的第二个goroutine。在两个goroutine间传输数据是同步的,一旦传输完成,两个goroutine都会知道数据已经完成传输。当第二个goroutine利用这个数据完成其任务后,将这个数据传给第三个正在等待的goroutine。这次传输依旧是同步的,两个goroutine都会确认数据传输完成。这种在goroutine之间安全传输数据的方法不需要任何锁或者同步机制。

需要强调的是,通道并不提供跨goroutine的数据访问保护机制。如果通过通道传输数据的一份副本,那么每个goroutine都持有一份副本,各自对自己的副本做修改是安全的。当传输的是指向数据的指针时,如果读和写是由不同的goroutine完成的,每个goroutine依旧需要额外的同步动作。1.1.3 Go语言的类型系统

Go语言提供了灵活的、无继承的类型系统,无需降低运行性能就能最大程度上复用代码。这个类型系统依然支持面向对象开发,但避免了传统面向对象的问题。如果你曾经在复杂的Java和C++程序上花数周时间考虑如何抽象类和接口,你就能意识到Go语言的类型系统有多么简单。Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型,就能复用所有的功能。其他语言也能使用组合,但是不得不和继承绑在一起使用,结果使整个用法非常复杂,很难使用。在Go语言中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型。

另外,Go语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。在Go语言中,不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口。Go标准库里的很多接口都非常简单,只开放几个函数。从实践上讲,尤其对那些使用类似Java的面向对象语言的人来说,需要一些时间才能习惯这个特性。1.类型简单

Go语言不仅有类似int和string这样的内置类型,还支持用户定义的类型。在Go语言中,用户定义的类型通常包含一组带类型的字段,用于存储数据。Go语言的用户定义的类型看起来和C语言的结构很像,用起来也很相似。不过Go语言的类型可以声明操作该类型数据的方法。传统语言使用继承来扩展结构——Client继承自User,User继承自Entity,Go语言与此不同,Go开发者构建更小的类型——Customer和Admin,然后把这些小类型组合成更大的类型。图1-4展示了继承和组合之间的不同。图1-4 继承和组合的对比2.Go接口对一组行为建模

接口用于描述类型的行为。如果一个类型的实例实现了一个接口,意味着这个实例可以执行一组特定的行为。你甚至不需要去声明这个实例实现某个接口,只需要实现这组行为就好。其他的语言把这个特性叫作鸭子类型——如果它叫起来像鸭子,那它就可能是只鸭子。Go语言的接口也是这么做的。在Go语言中,如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明。

在类似Java这种严格的面向对象语言中,所有的设计都围绕接口展开。在编码前,用户经常不得不思考一个庞大的继承链。下面是一个Java接口的例子:interface User { public void login(); public void logout();}

在Java中要实现这个接口,要求用户的类必须满足User接口里的所有约束,并且显式声明这个类实现了这个接口。而Go语言的接口一般只会描述一个单一的动作。在Go语言中,最常使用的接口之一是io.Reader。这个接口提供了一个简单的方法,用来声明一个类型有数据可以读取。标准库内的其他函数都能理解这个接口。这个接口的定义如下:type Reader interface { Read(p []byte) (n int, err error)}

为了实现io.Reader这个接口,你只需要实现一个Read方法,这个方法接受一个byte切片,返回一个整数和可能出现的错误。

这和传统的面向对象编程语言的接口系统有本质的区别。Go语言的接口更小,只倾向于定义一个单一的动作。实际使用中,这更有利于使用组合来复用代码。用户几乎可以给所有包含数据的类型实现io.Reader接口,然后把这个类型的实例传给任意一个知道如何读取io.Reader的Go函数。

Go语言的整个网络库都使用了io.Reader接口,这样可以将程序的功能和不同网络的实现分离。这样的接口用起来有趣、优雅且自由。文件、缓冲区、套接字以及其他的数据源都实现了io.Reader接口。使用同一个接口,可以高效地操作数据,而不用考虑到底数据来自哪里。1.1.4 内存管理

不当的内存管理会导致程序崩溃或者内存泄漏,甚至让整个操作系统崩溃。Go语言拥有现代化的垃圾回收机制,能帮你解决这个难题。在其他系统语言(如C或者C++)中,使用内存前要先分配这段内存,而且使用完毕后要将其释放掉。哪怕只做错了一件事,都可能导致程序崩溃或者内存泄漏。可惜,追踪内存是否还被使用本身就是十分艰难的事情,而要想支持多线程和高并发,更是让这件事难上加难。虽然Go语言的垃圾回收会有一些额外的开销,但是编程时,能显著降低开发难度。Go语言把无趣的内存管理交给专业的编译器去做,而让程序员专注于更有趣的事情。1.2 你好,Go

感受一门语言最简单的方法就是实践。让我们看看用Go语言如何编写经典的Hello World!应用程序:package main   ●――――Go程序都组织成包。import "fmt"   ●――――import语句用于导入外部代码。标准库中的fmt包用于格式化并输出数据。func main() {  ●――――像C语言一样,main函数是程序执行的入口。 fmt.Println("Hello world!")}

运行这个示例程序后会在屏幕上输出我们熟悉的一句话。但是怎么运行呢?无须在机器上安装Go语言,在浏览器中就可以使用几乎所有Go语言的功能。介绍Go Playground

Go Playground允许在浏览器里编辑并运行Go语言代码。在浏览器中打开http://play.golang.org。浏览器里展示的代码是可编辑的(见图1-5)。点击Run,看看会发生什么。图1-5 Go Playground

可以把输出的问候文字改成别的语言。试着改动fmt.Println()里面的文字,然后再次点击Run。

分享Go代码 Go开发者使用Playground分享他们的想法,测试理论,或者调试代码。你也可以这么做。每次使用Playground创建一个新程序之后,可以点击Share得到一个用于分享的网址。任何人都能打开这个链接。试试http://play.golang.org/p/EWIXicJdmz。

要给想要学习写东西或者寻求帮助的同事或者朋友演示某个想法时,Go Playground是非常好的方式。在Go语言的IRC频道、Slack群组、邮件列表和Go开发者发送的无数邮件里,用户都能看到创建、修改和分享Go Playground上的程序。1.3 小结● Go语言是现代的、快速的,带有一个强大的标准库。● Go语言内置对并发的支持。● Go语言使用接口作为代码复用的基础模块。第2章快速开始一个Go程序

本章主要内容● 学习如何写一个复杂的Go程序● 声明类型、变量、函数和方法● 启动并同步操作goroutine● 使用接口写通用的代码● 处理程序逻辑和错误

为了能更高效地使用语言进行编码,Go语言有自己的哲学和编程习惯。Go语言的设计者们从编程效率出发设计了这门语言,但又不会丢掉访问底层程序结构的能力。设计者们通过一组最少的关键字、内置的方法和语法,最终平衡了这两方面。Go语言也提供了完善的标准库。标准库提供了构建实际的基于Web和基于网络的程序所需的所有核心库。

让我们通过一个完整的Go语言程序,来看看Go语言是如何实现这些功能的。这个程序实现的功能很常见,能在很多现在开发的Go程序里发现类似的功能。这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,进行网络调用,解码XML和JSON成为结构化类型数据,并且利用Go语言的并发机制保证这些操作的速度。

读者可以下载本章的代码,用自己喜欢的编辑器阅读。代码存放在这个代码库:https://github.com/goinaction/code/tree/master/chapter2/sample

没必要第一次就读懂本章的所有内容,可以多读两遍。在学习时,虽然很多现代语言的概念可以对应到Go语言中,Go语言还是有一些独特的特性和风格。如果放下已经熟悉的编程语言,用一种全新的眼光来审视Go语言,你会更容易理解并接受Go语言的特性,发现Go语言的优雅。2.1 程序架构

在深入代码之前,让我们看一下程序的架构(如图 2-1 所示),看看如何在所有不同的数据源中搜索数据。图2-1 程序架构流程图

这个程序分成多个不同步骤,在多个不同的goroutine里运行。我们会根据流程展示代码,从主goroutine开始,一直到执行搜索的goroutine和跟踪结果的goroutine,最后回到主goroutine。首先来看一下整个项目的结构,如代码清单2-1所示。

代码清单2-1 应用程序的项目结构cd $GOPATH/src/github.com/goinaction/code/chapter2- sample - data data.json -- 包含一组数据源 - matchers rss.go -- 搜索rss源的匹配器 - search default.go -- 搜索数据用的默认匹配器 feed.go -- 用于读取json数据文件 match.go -- 用于支持不同匹配器的接口 search.go -- 执行搜索的主控制逻辑 main.go -- 程序的入口

这个应用的代码使用了4个文件夹,按字母顺序列出。文件夹data中有一个JSON文档,其内容是程序要拉取和处理的数据源。文件夹matchers中包含程序里用于支持搜索不同数据源的代码。目前程序只完成了支持处理RSS类型的数据源的匹配器。文件夹search中包含使用不同匹配器进行搜索的业务逻辑。最后,父级文件夹sample中有个main.go文件,这是整个程序的入口。

现在了解了如何组织程序的代码,可以继续探索并了解程序是如何工作的。让我们从程序的入口开始。2.2 main包

程序的主入口可以在main.go文件里找到,如代码清单2-2所示。虽然这个文件只有21行代码,依然有几点需要注意。

代码清单2-2 main.go01 package main0203 import (04 "log"05 "os"0607 _ "github.com/goinaction/code/chapter2/sample/matchers"08 "github.com/goinaction/code/chapter2/sample/search"09 )1011 // init在main之前调用12 func init() {13 // 将日志输出到标准输出14 log.SetOutput(os.Stdout)15 }1617 // main 是整个程序的入口18 func main() {19 // 使用特定的项做搜索20 search.Run("president")21 }

每个可执行的Go程序都有两个明显的特征。一个特征是第18行声明的名为main的函数。构建程序在构建可执行文件时,需要找到这个已经声明的main函数,把它作为程序的入口。第二个特征是程序的第01行的包名main,如代码清单2-3所示。

代码清单2-3 main.go:第01行01 package main

可以看到,main函数保存在名为main的包里。如果main函数不在main包里,构建工具就不会生成可执行的文件。

Go语言的每个代码文件都属于一个包,main.go也不例外。包这个特性对于Go语言来说很重要,我们会在第3章中接触到更多细节。现在,只要简单了解以下内容:一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符。这个特性可以把不同包中定义的同名标识符区别开。

现在,把注意力转到main.go的第03行到第09行,如代码清单2-4所示,这里声明了所有的导入项。

代码清单2-4 main.go:第03行到第09行03 import (04 "log"05 "os"0607 _ "github.com/goinaction/code/chapter2/sample/matchers"08 "github.com/goinaction/code/chapter2/sample/search"09 )

顾名思义,关键字import就是导入一段代码,让用户可以访问其中的标识符,如类型、函数、常量和接口。在这个例子中,由于第08行的导入,main.go里的代码就可以引用search包里的Run函数。程序的第04行和第05行导入标准库里的log和os包。

所有处于同一个文件夹里的代码文件,必须使用同一个包名。按照惯例,包和文件夹同名。就像之前说的,一个包定义一组编译后的代码,每段代码都描述包的一部分。如果回头去看看代码清单2-1,可以看看第08行的导入是如何指定那个项目里名叫search的文件夹的。

读者可能注意到第07行导入matchers包的时候,导入的路径前面有一个下划线,如代码清单2-5所示。

代码清单2-5 main.go:第07行07 _ "github.com/goinaction/code/chapter2/sample/matchers"

这个技术是为了让Go语言对包做初始化操作,但是并不使用包里的标识符。为了让程序的可读性更强,Go 编译器不允许声明导入某个包却不使用。下划线让编译器接受这类导入,并且调用对应包内的所有代码文件里定义的init函数。对这个程序来说,这样做的目的是调用matchers包中的rss.go代码文件里的init函数,注册RSS匹配器,以便后用。我们后面会展示具体的工作方式。

代码文件main.go里也有一个init函数,在第12行到第15行中声明,如代码清单2-6所示。

代码清单2-6 main.go:第11行到第15行11 // init在main之前调用12 func init() {13 // 将日志输出到标准输出14 log.SetOutput(os.Stdout)15 }

程序中每个代码文件里的init函数都会在main函数执行前调用。这个init函数将标准库里日志类的输出,从默认的标准错误(stderr),设置为标准输出(stdout)设备。在第7章,我们会进一步讨论log包和标准库里其他重要的包。

最后,让我们看看main函数第20行那条语句的作用,如代码清单2-7所示。

代码清单2-7 main.go:第19行到第20行19 // 使用特定的项做搜索20 search.Run("president")

可以看到,这一行调用了search包里的Run函数。这个函数包含程序的核心业务逻辑,需要传入一个字符串作为搜索项。一旦Run函数退出,程序就会终止。

现在,让我们看看search包里的代码。2.3 search包

这个程序使用的框架和业务逻辑都在search包里。这个包由4个不同的代码文件组成,每个文件对应一个独立的职责。我们会逐步分析这个程序的逻辑,到时再说明各个代码文件的作用。

由于整个程序都围绕匹配器来运作,我们先简单介绍一下什么是匹配器。这个程序里的匹配器,是指包含特定信息、用于处理某类数据源的实例。在这个示例程序中有两个匹配器。框架本身实现了一个无法获取任何信息的默认匹配器,而在matchers包里实现了RSS匹配器。RSS匹配器知道如何获取、读入并查找RSS数据源。随后我们会扩展这个程序,加入能读取JSON文档或CSV文件的匹配器。我们后面会再讨论如何实现匹配器。2.3.1 search.go

代码清单2-8中展示的是search.go代码文件的前9行代码。之前提到的Run函数就在这个文件里。

代码清单2-8 search/search.go:第01行到第09行01 package search0203 import (04 "log"05 "sync"06 )0708 // 注册用于搜索的匹配器的映射09 var matchers = make(map[string]Matcher)

可以看到,每个代码文件都以package关键字开头,随后跟着包的名字。文件夹search下的每个代码文件都使用search作为包名。第03行到第06行代码导入标准库的log和sync包。

与第三方包不同,从标准库中导入代码时,只需要给出要导入的包名。编译器查找包的时候,总是会到GOROOT和GOPATH环境变量(如代码清单2-9所示)引用的位置去查找。

代码清单2-9 GOROOT和GOPATH环境变量GOROOT="/Users/me/go"GOPATH="/Users/me/spaces/go/projects"

log包提供打印日志信息到标准输出(stdout)、标准错误(stderr)或者自定义设备的功能。sync包提供同步goroutine的功能。这个示例程序需要用到同步功能。第09行是全书第一次声明一个变量,如代码清单2-10所示。

代码清单2-10 search/search.go:第08行到第09行08 // 注册用于搜索的匹配器的映射09 var matchers = make(map[string]Matcher)

这个变量没有定义在任何函数作用域内,所以会被当成包级变量。这个变量使用关键字var声明,而且声明为Matcher类型的映射(map),这个映射以string类型值作为键,Matcher类型值作为映射后的值。Matcher类型在代码文件matcher.go中声明,后面再讲这个类型的用途。这个变量声明还有一个地方要强调一下:变量名matchers是以小写字母开头的。

在Go语言里,标识符要么从包里公开,要么不从包里公开。当代码导入了一个包时,程序可以直接访问这个包中任意一个公开的标识符。这些标识符以大写字母开头。以小写字母开头的标识符是不公开的,不能被其他包中的代码直接访问。但是,其他包可以间接访问不公开的标识符。例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,哪怕调用者不是在这个包里声明的,都可以访问这个值。

这行变量声明还使用赋值运算符和特殊的内置函数make初始化了变量,如代码清单2-11所示。

代码清单2-11 构建一个映射make(map[string]Matcher)

map是Go语言里的一个引用类型,需要使用make来构造。如果不先构造map并将构造后的值赋值给变量,会在试图使用这个map变量时收到出错信息。这是因为map变量默认的零值是nil。在第4章我们会进一步了解关于映射的细节。

在Go语言中,所有变量都被初始化为其零值。对于数值类型,零值是0;对于字符串类型,零值是空字符串;对于布尔类型,零值是false;对于指针,零值是nil。对于引用类型来说,所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量,会返回nil作为其值。

现在,让我们看看之前在main函数中调用的Run函数的内容,如代码清单2-12所示。

代码清单2-12 search/search.go:第11行到第57行11 // Run执行搜索逻辑12 func Run(searchTerm string) {13 // 获取需要搜索的数据源列表14 feeds, err := RetrieveFeeds()15 if err != nil {16 log.Fatal(err)17 }1819 // 创建一个无缓冲的通道,接收匹配后的结果20 results := make(chan *Result)2122 // 构造一个waitGroup,以便处理所有的数据源23 var waitGroup sync.WaitGroup2425 // 设置需要等待处理26 // 每个数据源的goroutine的数量27 waitGroup.Add(len(feeds))2829 // 为每个数据源启动一个goroutine来查找结果30 for _, feed := range feeds {31 // 获取一个匹配器用于查找32 matcher, exists := matchers[feed.Type]33 if !exists {34 matcher = matchers["default"]35 }3637 // 启动一个goroutine来执行搜索38 go func(matcher Matcher, feed *Feed) {39 Match(matcher, feed, searchTerm, results)40 waitGroup.Done()41 }(matcher, feed)42 }4344 // 启动一个goroutine来监控是否所有的工作都做完了45 go func() {46 // 等候所有任务完成47 waitGroup.Wait()4849 // 用关闭通道的方式,通知Display函数50 // 可以退出程序了51 close(results)52 }()5354 // 启动函数,显示返回的结果,并且55 // 在最后一个结果显示完后返回56 Display(results)57 }

Run函数包括了这个程序最主要的控制逻辑。这段代码很好地展示了如何组织Go程序的代码,以便正确地并发启动和同步goroutine。先来一步一步考察整个逻辑,再考察每步实现代码的细节。

先来看看Run函数是怎么定义的,如代码清单2-13所示。

代码清单2-13 search/search.go:第11行到第12行11 // Run 执行搜索逻辑12 func Run(searchTerm string) {

Go语言使用关键字func声明函数,关键字后面紧跟着函数名、参数以及返回值。对于Run这个函数来说,只有一个参数,是string类型的,名叫searchTerm。这个参数是Run函数要搜索的搜索项,如果回头看看main函数(如代码清单2-14所示),可以看到如何传递这个搜索项。

代码清单2-14 main.go:第17行到第21行17 // main 是整个程序的入口18 func main() {19 // 使用特定的项做搜索20 search.Run("president")21 }

Run函数做的第一件事情就是获取数据源feeds列表。这些数据源从互联网上抓取数据,之后对数据使用特定的搜索项进行匹配,如代码清单2-15所示。

代码清单2-15 search/search.go:第13行到第17行13 // 获取需要搜索的数据源列表14 feeds, err := RetrieveFeeds()15 if err != nil {16 log.Fatal(err)17 }

这里有几个值得注意的重要概念。第14行调用了search包的RetrieveFeeds函数。这个函数返回两个值。第一个返回值是一组Feed类型的切片。切片是一种实现了一个动态数组的引用类型。在Go语言里可以用切片来操作一组数据。第4章会进一步深入了解有关切片的细节。

第二个返回值是一个错误值。在第15行,检查返回的值是不是真的是一个错误。如果真的发生错误了,就会调用log包里的Fatal函数。Fatal函数接受这个错误的值,并将这个错误在终端窗口里输出,随后终止程序。

不仅仅是Go语言,很多语言都允许一个函数返回多个值。一般会像RetrieveFeeds函数这样声明一个函数返回一个值和一个错误①值。如果发生了错误,永远不要使用该函数返回的另一个值。这时必须忽略另一个值,否则程序会产生更多的错误,甚至崩溃。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载