当前位置: 首页 > 新闻 > 信息荟萃
编号:5770
Go语言编程版高清.pdf
http://www.100md.com 2020年11月19日
第1页
第5页
第12页
第29页
第41页
第158页

    参见附件(9160KB,245页)。

     Go语言编程版

    这本书从整体的写作风格来说,会以介绍 Go 语言特性为主,示例则尽量采用作者平常的实践,而不是一个没有太大实际意义的语法示范样例,小编今天给大家准备了Go语言编程版,有需要的就快来吧

    内容简介

    这本书从整体的写作风格来说,会以介绍 Go 语言特性为主,示例则尽量采用作者平常的实践,而不是一个没有太大实际意义的语法示范样例。

    本书作者背景极强,许式伟为原金山WPS首席架构师、曾是盛大创新院研究员,目前是国内Go语言实践圈子公认的Go语言专家。参与本书写作的几位作者都是实际用Go语言开发的项目的开发人员,有较强的实战经验。

    本书以介绍Go语言特性为主,示例则尽量采用作者开发团队平常的实践,内容涉及内存管理(堆和栈)、错误处理、OOP、并发编程等关键话题。 这本书面向的读者是所有打算用Go语言的开发者,主要包括目前使用C、C++、Java、C#的开发人员,甚至一些Python、PHP开发人员也可能转为 Go 程序员。

    相关内容部分预览

    编辑推荐

    《Go语言编程》首先概览了Go语言的诞生和发展历程,从面向过程编程特性入手介绍Go语言的基础用法,让有一定C语言基础的读者可以非常迅速地入门并开始上手用Go语言来解决实际问题,之后介绍了Go语言简洁却又无比强大的面向对象编程特性和并发编程能力,至此读者已经可以理解为什么Go语言是为互联网时代而生的语言。

    从实用性角度出发,本书还介绍了Go语言标准库和配套工具的用法,包括安全编程、网络编程、工程管理工具等。

    对于希望对Go语言有更深入了解的读者,我们也特别组织了一系列进阶话题,包括语言交互性、链接符号、goroutine机理和接口机制等。

    内容简介

    在C语言和Unix操作系统发布40年后 ......

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)内 容 提 要

    本书首先引领读者快速浏览Go语言的全貌,迅速消除读者对这门语言的陌生感,然后循序渐进地介绍

    了 Go 语言的面向过程和面向对象的编程语法,其中穿插了一些与其他主流语言的比较以让读者理解 Go 语

    言的设计动机,接着探讨了Go语言最为重要的并行编程方法,之后介绍了网络编程、工程管理、安全编程、开发工具等非语法相关但非常重要的内容,最后为一系列关于Go语言的文章,可以帮助读者更深入了解这

    门全新的语言。

    本书适合所有层次的开发者阅读。

    图灵原创

    Go语言编程

    定价:49.00元

    读者服务热线:(010)51095186转604 印装质量热线:(010)67129223

    反盗版热线:(010)67171154

    编著 许式伟 吕桂华 等

    责任编辑 王军花

    人民邮电出版社出版发行 北京市崇文区夕照寺街14号

    邮编 100061 电子邮件 315@ptpress.com.cn

    网址 http:www.ptpress.com.cn

    北京 印刷

    开本:800×1000 116

    印张:15.25

    字数:361千字 2012年 8 月第 1 版

    印数:1 — 4 000册 2012年 8 月北京第 1 次印刷

    ISBN 978-7-115-29036-6

    ◆

    ◆

    ◆

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)前言:为什么我们需要一门新语言 1

    1

    2

    3

    4

    5

    9

    6

    7

    8

    前言:为什么我们需要一门新语言

    编程语言已经非常多,偏性能敏感的编译型语言有 C、C++、Java、C、Delphi和Objective-C

    等,偏快速业务开发的动态解析型语言有PHP、Python、Perl、Ruby、JavaScript和Lua等,面向特

    定领域的语言有Erlang、R和MATLAB等,那么我们为什么需要 Go这样一门新语言呢?

    在2000年前的单机时代,C语言是编程之王。随着机器性能的提升、软件规模与复杂度的提

    高,Java逐步取代了C的位置。尽管看起来Java已经深获人心,但Java编程的体验并未尽如人意。

    历年来的编程语言排行榜(如图0-1所示)显示,Java语言的市场份额在逐步下跌,并趋近于C语

    言的水平,显示了这门语言后劲不足。

    图0-1 编程语言排行榜①

    Go语言官方自称,之所以开发Go 语言,是因为“近10年来开发程序之难让我们有点沮丧”。

    这一定位暗示了Go语言希望取代C和Java的地位,成为最流行的通用开发语言。

    Go希望成为互联网时代的C语言。多数系统级语言(包括Java和C)的根本编程哲学来源于

    ——————————

    ① 数据来源:http:www.tiobe.comindex.phpcontentpaperinfotpciindex.html。

    Tiobe编程语言排行榜

    时间

    所占的百分比(%)

    Java语言

    C语言

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2 前言:为什么我们需要一门新语言

    C++,将C++的面向对象进一步发扬光大。但是Go语言的设计者却有不同的看法,他们认为C++ 真

    的没啥好学的,值得学习的是C语言。C语言经久不衰的根源是它足够简单。因此,Go语言也要

    足够简单!

    那么,互联网时代的C语言需要考虑哪些关键问题呢?

    首先,并行与分布式支持。多核化和集群化是互联网时代的典型特征。作为一个互联网时代

    的C语言,必须要让这门语言操作多核计算机与计算机集群如同操作单机一样容易。

    其次,软件工程支持。工程规模不断扩大是产业发展的必然趋势。单机时代语言可以只关心

    问题本身的解决,而互联网时代的C语言还需要考虑软件品质保障和团队协作相关的话题。

    最后,编程哲学的重塑。计算机软件经历了数十年的发展,形成了面向对象等多种学术流派。

    什么才是最佳的编程实践?作为互联网时代的C语言,需要回答这个问题。

    接下来我们来聊聊Go语言在这些话题上是如何应对的。

    并发与分布式

    多核化和集群化是互联网时代的典型特征,那语言需要哪些特性来应对这些特征呢?

    第一个话题是并发执行的“执行体”。执行体是个抽象的概念,在操作系统层面有多个概念

    与之对应,比如操作系统自己掌管的进程(process)、进程内的线程(thread)以及进程内的协程

    (coroutine,也叫轻量级线程)。多数语言在语法层面并不直接支持协程,而通过库的方式支持的

    协程的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的协程中调

    用一个同步IO操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行协程,从而无法真

    正达到协程本身期望达到的目标。

    Go语言在语言级别支持协程,叫goroutine。Go语言标准库提供的所有系统调用(syscall)操

    作,当然也包括所有同步IO操作,都会出让CPU给其他goroutine,这让事情变得非常简单。我们

    对比一下Java和Go,近距离观摩下两者对“执行体”的支持。

    为了简化,我们在样例中使用的是Java标准库中的线程,而不是协程,具体代码如下:

    public class MyThread implements Runnable {

    String arg;

    public MyThread(String a) {

    arg = a;

    }

    public void run {

    ...

    }

    public static void main(String[] args) {

    new Thread(new MyThread(test)).start;

    ...

    }

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)前言:为什么我们需要一门新语言 3

    1

    2

    3

    4

    5

    9

    6

    7

    8

    }

    相同功能的代码,在Go语言中是这样的:

    func run(arg string) {

    ...

    }

    func main {

    go run(test)...

    }

    对比非常鲜明。我相信你已经明白为什么Go语言会叫Go语言了:Go语言献给这个时代最好

    的礼物,就是加了go这个关键字。当然也有人会说,叫Go语言是因为它是Google出的。好吧,这也是个不错的闲聊主题。

    第二个话题是“执行体间的通信”。执行体间的通信包含几个方式:

    执行体之间的互斥与同步

    执行体之间的消息传递

    先说“执行体之间的互斥与同步”。当执行体之间存在共享资源(一般是共享内存)时,为

    保证内存访问逻辑的确定性,需要对访问该共享资源的相关执行体进行互斥。当多个执行体之间

    的逻辑存在时序上的依赖时,也往往需要在执行体之间进行同步。互斥与同步是执行体间最基础

    的交互方式。

    多数语言在库层面提供了线程间的互斥与同步支持,那么协程之间的互斥与同步呢?呃,不

    好意思,没有。事实上多数语言标准库中连协程都是看不到的。

    再说“执行体之间的消息传递”。在并发编程模型的选择上,有两个流派,一个是共享内存

    模型,一个是消息传递模型。多数传统语言选择了前者,少数语言选择后者,其中选择“消息传

    递模型”的最典型代表是Erlang语言。业界有专门的术语叫“Erlang风格的并发模型”,其主体思

    想是两点:一是“轻量级的进程(Erlang中‘进程’这个术语就是我们上面说的‘执行体’)” ,二

    是“消息乃进程间通信的唯一方式”。当执行体之间需要相互传递消息时,通常需要基于一个消

    息队列(message queue)或者进程邮箱(process mail box)这样的设施进行通信。

    Go语言推荐采用“Erlang风格的并发模型”的编程范式,尽管传统的“共享内存模型”仍然

    被保留,允许适度地使用。在Go语言中内置了消息队列的支持,只不过它叫通道(channel) 。两

    个goroutine之间可以通过通道来进行交互。

    软件工程

    单机时代的语言可以只关心问题本身的解决,但是随着工程规模的不断扩大,软件复杂度的

    不断增加,软件工程也成为语言设计层面要考虑的重要课题。多数软件需要一个团队共同去完

    成,在团队协作的过程中,人们需要建立统一的交互语言来降低沟通的成本。规范化体现在多

    个层面,如:

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)4 前言:为什么我们需要一门新语言

    代码风格规范

    错误处理规范

    包管理

    契约规范(接口)

    单元测试规范

    功能开发的流程规范

    Go语言很可能是第一个将代码风格强制统一的语言,例如Go语言要求public的变量必须以

    大写字母开头,private变量则以小写字母开头,这种做法不仅免除了public、private关键

    字,更重要的是统一了命名风格。

    另外,Go语言对{ }应该怎么写进行了强制,比如以下风格是正确的:

    if expression {...

    }

    但下面这个写法就是错误的:

    if expression

    {...

    }

    而C和Java语言中则对花括号的位置没有任何要求。哪种更有利,这个见仁见智。但很显然

    的是,所有的Go代码的花括号位置肯定是非常统一的。

    最有意思的其实还是 Go 语言首创的错误处理规范:

    f, err := os.Open(filename)

    if err != nil {

    log.Println(Open file failed:, err)

    return

    }

    defer f.Close... 操作已经打开的f文件

    这里有两个关键点。其一是defer关键字。defer语句的含义是不管程序是否出现异常,均

    在函数退出时自动执行相关代码。在上面的例子中,正是因为有了defer,才使得无论后续是否

    会出现异常,都可以确保文件被正确关闭。其二是Go语言的函数允许返回多个值。大多数函数

    的最后一个返回值会为error类型,以在错误情况下返回详细信息。error类型只是一个系统内

    置的interface,如下:

    type error interface {

    Error string

    }

    有了error类型,程序出现错误的逻辑看起来就相当统一。

    在Java中,你可能这样写代码来保证资源正确释放:

    Connection conn = ...;

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)前言:为什么我们需要一门新语言 5

    1

    2

    3

    4

    5

    9

    6

    7

    8

    try {

    Statement stmt = ...;

    try {

    ResultSet rset = ...;

    try {... 正常代码

    }

    finally {

    rset.close;

    }

    }

    finally {

    stmt.close;

    }

    }

    finally {

    conn.close;

    }

    完成同样的功能,相应的Go代码只需要写成这样:

    conn := ...

    defer conn.Close

    stmt := ...

    defer stmt.Close

    rset := ...

    defer rset.Close... 正常代码

    对比两段代码,Go语言处理错误的优势显而易见。当然,其实Go语言带给我们的惊喜还有

    很多,后续有机会我们可以就某个更具体的话题详细展开来谈一谈。

    编程哲学

    计算机软件经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程、函数式编程、面向消息编程等,这些思想究竟孰优孰劣,众说纷纭。

    C语言是纯过程式的,这和它产生的历史背景有关。Java语言则是激进的面向对象主义推崇

    者,典型表现是它不能容忍体系里存在孤立的函数。而Go语言没有去否认任何一方,而是用批

    判吸收的眼光,将所有编程思想做了一次梳理,融合众家之长,但时刻警惕特性复杂化,极力维

    持语言特性的简洁,力求小而精。

    从编程范式的角度来说,Go语言是变革派,而不是改良派。

    对于C++、Java和C等语言为代表的面向对象(OO)思想体系,Go语言总体来说持保守态

    度,有限吸收。

    首先,Go语言反对函数和操作符重载(overload) ,而C++、Java和C都允许出现同名函数或

    操作符,只要它们的参数列表不同。虽然重载解决了一小部分面向对象编程(OOP)的问题,但

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)6 前言:为什么我们需要一门新语言

    同样给这些语言带来了极大的负担。而Go语言有着完全不同的设计哲学,既然函数重载带来了

    负担,并且这个特性并不对解决任何问题有显著的价值,那么Go就不提供它。

    其次,Go语言支持类、类成员方法、类的组合,但反对继承,反对虚函数(virtual function)

    和虚函数重载。确切地说,Go也提供了继承,只不过是采用了组合的文法来提供:

    type Foo struct {

    Base...

    }

    func (foo Foo) Bar {...

    }

    再次,Go语言也放弃了构造函数(constructor)和析构函数(destructor) 。由于Go语言中没

    有虚函数,也就没有vptr,支持构造函数和析构函数就没有太大的价值。本着“如果一个特性

    并不对解决任何问题有显著的价值,那么Go就不提供它”的原则,构造函数和析构函数就这样

    被Go语言的作者们干掉了。

    在放弃了大量的OOP特性后,Go语言送上了一份非常棒的礼物:接口(interface) 。你可能

    会说,除了C这么原始的语言外,还有什么语言没有接口呢?是的,多数语言都提供接口,但它

    们的接口都不同于Go语言的接口。

    Go语言中的接口与其他语言最大的一点区别是它的非侵入性。在C++、Java和C中,为了实

    现一个接口,你需要从该接口继承,具体代码如下:

    class Foo implements IFoo { Java文法...

    }

    class Foo : public IFoo { C++文法...

    }

    IFoo foo = new Foo;

    在Go语言中,实现类的时候无需从接口派生,具体代码如下:

    type Foo struct { Go 文法...

    }

    var foo IFoo = new(Foo)

    只要Foo实现了接口IFoo要求的所有方法,就实现了该接口,可以进行赋值。

    Go语言的非侵入式接口,看似只是做了很小的文法调整,实则影响深远。

    其一,Go语言的标准库再也不需要绘制类库的继承树图。你只需要知道这个类实现了哪些

    方法,每个方法是啥含义就足够了。

    其二,不用再纠结接口需要拆得多细才合理,比如我们实现了File类,它有下面这些方法:

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)前言:为什么我们需要一门新语言 7

    1

    2

    3

    4

    5

    9

    6

    7

    8

    Read(buf []byte) (n int, err error)

    Write(buf []byte) (n int, err error)

    Seek(off int64, whence int) (pos int64, err error)

    Close error

    那么,到底是应该定义一个IFile接口,还是应该定义一系列的IReader、IWriter、ISeeker和ICloser接口,然后让File从它们派生好呢?事实上,脱离了实际的用户场景,讨

    论这两个设计哪个更好并无意义。问题在于,实现File类的时候,我怎么知道外部会如何用它

    呢?

    其三,不用为了实现一个接口而专门导入一个包,而目的仅仅是引用其中的某个接口的定义。

    在Go语言中,只要两个接口拥有相同的方法列表,那么它们就是等同的,可以相互赋值,如对

    于以下两个接口,第一个接口:

    package one

    type ReadWriter interface {

    Read(buf [] byte) (n int, err error)

    Write(buf [] byte) (n int, err error)

    }

    第二个接口:

    package two

    type IStream interface {

    Write(buf [] byte) (n int, err error)

    Read(buf [] byte) (n int, err error)

    }

    这里我们定义了两个接口,一个叫one.ReadWriter,一个叫two.IStream,两者都定义

    了Read和Write方法,只是定义的次序相反。one.ReadWriter先定义了Read再定义

    Write,而two.IStream反之。

    在Go语言中,这两个接口实际上并无区别,因为:

    任何实现了one.ReadWriter接口的类,均实现了two.IStream;

    任何one.ReadWriter接口对象可赋值给two.IStream,反之亦然;

    在任何地方使用one.ReadWriter接口,与使用two.IStream并无差异。

    所以在Go语言中,为了引用另一个包中的接口而导入这个包的做法是不被推荐的。因为多

    引用一个外部的包,就意味着更多的耦合。

    除了OOP外,近年出现了一些小众的编程哲学,Go语言对这些思想亦有所吸收。例如,Go

    语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了以Erlang语言为

    代表的面向消息编程思想,支持goroutine和通道,并推荐使用消息而不是共享内存来进行并发编

    程。总体来说,Go语言是一个非常现代化的语言,精小但非常强大。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)8 前言:为什么我们需要一门新语言

    小结

    在十余年的技术生涯中,我接触过、使用过、喜爱过不同的编程语言,但总体而言,Go语

    言的出现是最让我兴奋的事情。我个人对未来10年编程语言排行榜的趋势判断如下:

    Java语言的份额继续下滑,并最终被C和Go语言超越;

    C语言将长居编程榜第二的位置,并有望在Go取代Java前重获语言榜第一的宝座;

    Go语言最终会取代Java,居于编程榜之首。

    由七牛云存储团队编著的这本书将尽可能展现出Go语言的迷人魅力。希望本书能够让更多

    人理解这门语言,热爱这门语言,让这门优秀的语言能够落到实处,把程序员从以往繁杂的语言

    细节中解放出来,集中精力开发更加优秀的系统软件。

    许式伟

    2012年3月7日

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)目 录 1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    18

    17

    目 录

    第 1章 初识 Go语言........................................

    1

    1.1 语言简史......................................................

    1

    1.2 语言特性......................................................

    2

    1.2.1 自动垃圾回收...................................

    3

    1.2.2 更丰富的内置类型...........................

    4

    1.2.3 函数多返回值...................................

    5

    1.2.4 错误处理..........................................

    6

    1.2.5 匿名函数和闭包...............................

    6

    1.2.6 类型和接口......................................

    7

    1.2.7 并发编程..........................................

    8

    1.2.8 反射..................................................

    9

    1.2.9 语言交互性....................................

    10

    1.3 第一个Go程序..........................................

    11

    1.3.1 代码解读........................................

    11

    1.3.2 编译环境准备.................................

    12

    1.3.3 编译程序........................................

    12

    1.4 开发工具选择............................................

    13

    1.5 工程管理....................................................

    13

    1.6 问题追踪和调试.........................................

    18

    1.6.1 打印日志........................................

    18

    1.6.2 GDB调试.......................................

    18

    1.7 如何寻求帮助............................................

    18

    1.7.1 邮件列表........................................

    19

    1.7.2 网站资源........................................

    19

    1.8 小结............................................................

    19

    第 2章 顺序编程..............................................

    20

    2.1 变量............................................................

    20

    2.1.1 变量声明........................................

    20

    2.1.2 变量初始化....................................

    21

    2.1.3 变量赋值........................................

    21

    2.1.4 匿名变量........................................

    22

    2.2 常量............................................................

    22

    2.2.1 字面常量........................................

    22

    2.2.2 常量定义........................................

    23

    2.2.3 预定义常量....................................

    23

    2.2.4 枚举................................................

    24

    2.3 类型............................................................

    24

    2.3.1 布尔类型........................................

    25

    2.3.2 整型................................................

    25

    2.3.3 浮点型............................................

    27

    2.3.4 复数类型........................................

    28

    2.3.5 字符串............................................

    28

    2.3.6 字符类型........................................

    30

    2.3.7 数组................................................

    31

    2.3.8 数组切片........................................

    32

    2.3.9 map .................................................

    36

    2.4 流程控制....................................................

    38

    2.4.1 条件语句........................................

    38

    2.4.2 选择语句........................................

    39

    2.4.3 循环语句........................................

    40

    2.4.4 跳转语句........................................

    41

    2.5 函数............................................................

    41

    2.5.1 函数定义........................................

    42

    2.5.2 函数调用........................................

    42

    2.5.3 不定参数........................................

    43

    2.5.4 多返回值........................................

    45

    2.5.5 匿名函数与闭包.............................

    45

    2.6 错误处理....................................................

    47

    2.6.1 error 接口....................................

    47

    2.6.2 defer.............................................

    48

    2.6.3 panic和recover ...............

    49

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2 目 录

    2.7 完整示例....................................................

    50

    2.7.1 程序结构........................................

    51

    2.7.2 主程序............................................

    51

    2.7.3 算法实现........................................

    54

    2.7.4 主程序............................................

    57

    2.7.5 构建与执行....................................

    59

    2.8 小结............................................................

    61

    第 3章 面向对象编程.....................................

    62

    3.1 类型系统....................................................

    62

    3.1.1 为类型添加方法.............................

    63

    3.1.2 值语义和引用语义.........................

    66

    3.1.3 结构体............................................

    67

    3.2 初始化........................................................

    68

    3.3 匿名组合....................................................

    68

    3.4 可见性........................................................

    71

    3.5 接口............................................................

    71

    3.5.1 其他语言的接口.............................

    71

    3.5.2 非侵入式接口................................

    73

    3.5.3 接口赋值........................................

    74

    3.5.4 接口查询........................................

    76

    3.5.5 类型查询........................................

    78

    3.5.6 接口组合........................................

    78

    3.5.7 Any 类型........................................

    79

    3.6 完整示例....................................................

    79

    3.6.1 音乐库............................................

    80

    3.6.2 音乐播放........................................

    82

    3.6.3 主程序............................................

    84

    3.6.4 构建运行........................................

    86

    3.6.5 遗留问题........................................

    86

    3.7 小结............................................................

    87

    第 4章 并发编程..............................................

    88

    4.1 并发基础....................................................

    88

    4.2 协程............................................................

    90

    4.3 goroutine .....................................................

    90

    4.4 并发通信....................................................

    91

    4.5 channel........................................................

    94

    4.5.1 基本语法........................................

    95

    4.5.2 select...........................................

    95

    4.5.3 缓冲机制........................................

    96

    4.5.4 超时机制........................................

    97

    4.5.5 channel的传递...............................

    98

    4.5.6 单向channel...................................

    98

    4.5.7 关闭channel...................................

    99

    4.6 多核并行化..............................................

    100

    4.7 出让时间片..............................................

    101

    4.8 同步..........................................................

    101

    4.8.1 同步锁..........................................

    101

    4.8.2 全局唯一性操作...........................

    102

    4.9 完整示例..................................................

    103

    4.9.1 简单IPC框架..............................

    105

    4.9.2 中央服务器..................................

    108

    4.9.3 主程序..........................................

    113

    4.9.4 运行程序......................................

    116

    4.10 小结........................................................

    117

    第 5章 网络编程............................................

    118

    5.1 Socket编程...............................................

    118

    5.1.1 Dial函数.................................

    118

    5.1.2 ICMP示例程序............................

    119

    5.1.3 TCP示例程序..............................

    121

    5.1.4 更丰富的网络通信.......................

    122

    5.2 HTTP编程................................................

    124

    5.2.1 HTTP客户端................................

    124

    5.2.2 HTTP服务端................................

    130

    5.3 RPC编程..................................................

    132

    5.3.1 Go语言中的RPC支持与

    处理..............................................

    132

    5.3.2 Gob简介.......................................

    134

    5.3.3 设计优雅的RPC接口.................

    134

    5.4 JSON处理................................................

    135

    5.4.1 编码为JSON格式.......................

    136

    5.4.2 解码JSON数据...........................

    137

    5.4.3 解码未知结构的JSON数据.......

    138

    5.4.4 JSON的流式读写........................

    140

    5.5 网站开发..................................................

    140

    5.5.1 最简单的网站程序.......................

    141

    5.5.2 nethttp 包简介.......................

    141

    5.5.3 开发一个简单的相册网站...........

    142

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)目 录 3

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    18

    17

    5.6 小结..........................................................

    157

    第 6章 安全编程............................................

    158

    6.1 数据加密..................................................

    158

    6.2 数字签名..................................................

    158

    6.3 数字证书..................................................

    159

    6.4 PKI体系...................................................

    159

    6.5 Go语言的哈希函数.................................

    159

    6.6 加密通信..................................................

    160

    6.6.1 加密通信流程...............................

    161

    6.6.2 支持HTTPS的 Web服务器.......

    162

    6.6.3 支持HTTPS的文件服务器.........

    165

    6.6.4 基于SSLTLS的ECHO程序.....

    166

    6.7 小结..........................................................

    169

    第 7章 工程管理............................................

    170

    7.1 Go命令行工具.........................................

    170

    7.2 代码风格..................................................

    172

    7.2.1 强制性编码规范...........................

    172

    7.2.2 非强制性编码风格建议...............

    173

    7.3 远程import 支持...................................

    175

    7.4 工程组织..................................................

    175

    7.4.1 GOPATH.........................................

    176

    7.4.2 目录结构......................................

    176

    7.5 文档管理..................................................

    177

    7.6 工程构建..................................................

    180

    7.7 跨平台开发..............................................

    180

    7.7.1 交叉编译......................................

    181

    7.7.2 Android支持................................

    182

    7.8 单元测试..................................................

    183

    7.9 打包分发..................................................

    184

    7.10 小结........................................................

    184

    第 8章 开发工具............................................

    186

    8.1 选择开发工具..........................................

    186

    8.2 gedit..........................................................

    187

    8.2.1 语法高亮......................................

    187

    8.2.2 编译环境......................................

    187

    8.3 Vim...........................................................

    188

    8.4 Eclipse ......................................................

    189

    8.5 Notepad++ ................................................

    192

    8.5.1 语法高亮......................................

    192

    8.5.2 编译环境......................................

    192

    8.6 LiteIDE .....................................................

    193

    8.7 小结..........................................................

    195

    第 9章 进阶话题............................................

    196

    9.1 反射..........................................................

    196

    9.1.1 基本概念......................................

    196

    9.1.2 基本用法......................................

    197

    9.1.3 对结构的反射操作.......................

    199

    9.2 语言交互性..............................................

    199

    9.2.1 类型映射......................................

    200

    9.2.2 字符串映射..................................

    201

    9.2.3 C程序...........................................

    201

    9.2.4 函数调用......................................

    202

    9.2.5 编译Cgo.......................................

    203

    9.3 链接符号..................................................

    203

    9.4 goroutine机理..........................................

    204

    9.4.1 协程..............................................

    204

    9.4.2 协程的C语言实现......................

    205

    9.4.3 协程库概述..................................

    205

    9.4.4 任务..............................................

    208

    9.4.5 任务调度......................................

    210

    9.4.6 上下文切换..................................

    211

    9.4.7 通信机制......................................

    215

    9.5 接口机理..................................................

    216

    9.5.1 类型赋值给接口...........................

    217

    9.5.2 接口查询......................................

    223

    9.5.3 接口赋值......................................

    224

    附录 A...................................................................

    225

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.1 语言简史 1

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    初识Go语言

    本章将简要介绍Go语言的发展历史和关键的语言特性,并引领读者对Go语言的主要特性进

    行一次快速全面的浏览,让读者对Go语言的总体情况有一个清晰的印象,并能够快速上手,用

    Go语言编写和运行自己的第一个小程序。

    1.1 语言简史

    提起Go语言的出身,我们就必须将我们饱含敬意的眼光投向持续推出惊世骇俗成果的贝尔

    实验室。贝尔实验室已经走出了多位诺贝尔奖获得者,一些对于现在科技至关重要的研究成果,比如晶体管、通信技术、数码相机的感光元件CCD和光电池等都源自贝尔实验室。该实验室在科

    技界的地位可想而之,是一个毫无争议的科研圣地。

    这里我们重点介绍一下贝尔实验室中一个叫计算科学研究中心的部门对于操作系统和编程

    语言的贡献。回溯至1969年(估计大部分读者那时候都还没出世) ,肯·汤普逊(Ken Thompson)

    和丹尼斯·里奇(Dennis Ritchie)在贝尔实验室的计算科学研究中心里开发出了Unix这个大名鼎

    鼎的操作系统,还因为开发Unix而衍生出了一门同样赫赫有名的编程语言——C语言。对于很大

    一部分人而言, Unix就是操作系统的鼻祖, C语言也是计算机课程中最广泛使用的编程语言。 Unix

    和C语言在过去的几十年以来已经造就了无数的成功商业故事,比如曾在90年代如日中天的太阳

    微系统(Sun MicroSystems),现在正如日中天的苹果的Mac OS X操作系统其实也可以认为是Unix

    的一个变种(FreeBSD)。

    虽然已经取得了如此巨大的成就,贝尔实验室的这几个人并没有因此而沉浸在光环中止步不

    前,他们从20世纪80年代又开始了一个名为Plan 9的操作系统研究项目,目的就是解决Unix中的

    一些问题,发展出一个Unix的后续替代系统。在之后的几十年中,该研究项目又演变出了另一个

    叫Inferno的项目分支,以及一个名为Limbo的编程语言。

    Limbo是用于开发运行在小型计算机上的分布式应用的编程语言,它支持模块化编程,编译

    期和运行时的强类型检查,进程内基于具有类型的通信通道,原子性垃圾收集和简单的抽象数据

    类型。它被设计为:即便是在没有硬件内存保护的小型设备上,也能安全运行。

    Limbo语言被认为是Go语言的前身,不仅仅因为是同一批人设计的语言,而是Go语言确实从

    Limbo语言中继承了众多优秀的特性。

    贝尔实验室后来经历了多次的动荡,包括肯·汤普逊在内的Plan 9项目原班人马加入了

    第1章

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2 第 1章 初识 Go语言

    Google。在Google,他们创造了Go语言。早在2007年9月,Go语言还是这帮大牛的20%自由时间

    的实验项目。幸运的是,到了2008年5月,Google发现了Go语言的巨大潜力,从而开始全力支持

    这个项目,让这批人可以全身心投入Go语言的设计和开发工作中。Go语言的第一个版本在2009

    年11月正式对外发布,并在此后的两年内快速迭代,发展迅猛。第一个正式版本的Go语言于2012

    年3月28日正式发布,让Go语言迎来了第一个引人瞩目的里程碑。

    基于Google对开源的一贯拥抱态度, Go语言也自然而然地选择了开源方式发布, 并使用BSD

    授权协议。任何人可以查看Go语言的所有源代码,并可以为Go语言发展而奉献自己的力量。

    Google作为Go语言的主推者,并没有简简单单地把语言推给开源社区了事,它不仅组建了一

    个独立的小组全职开发Go语言,还在自家的服务中逐步增加对Go语言的支持,比如对于Google

    有战略意义的云计算平台GAE(Google AppEngine)很早就开始支持Go语言了。按目前的发展态

    势,在Google内部,Go语言有逐渐取代Java和Python主流地位的趋势。在Google的更多产品中,我们将看到Go语言的踪影,比如Google最核心的搜索和广告业务。

    在本书的序中,我们已经清晰诠释了为什么在语言泛滥的时代Google还要设计和推出一门新

    的编程语言。按照已经发布的Go语言的特性,我们有足够的理由相信Google推出此门新编程语言

    绝不仅仅是简单的跑马圈地运动,而是为了解决切实的问题。

    下面我们再来看看Go语言的主要作者。

    肯·汤普逊(Ken Thompson,http:en.wikipedia.orgwikiKen_Thompson):设计了B语言

    和C语言,创建了Unix和Plan 9操作系统,1983年图灵奖得主,Go语言的共同作者。

    罗布·派克(Rob Pike,http:en.wikipedia.orgwikiRob_Pike):Unix小组的成员,参与Plan

    9和Inferno操作系统,参与 Limbo和Go语言的研发,《Unix编程环境》作者之一。

    罗伯特·格里泽默(Robert Griesemer) :曾协助制作Java的HotSpot编译器和Chrome浏览

    器的JavaScript引擎V8。

    拉斯· 考克斯(Russ Cox, http:swtch.com~rsc):参与Plan 9操作系统的开发, Google Code

    Search项目负责人。

    伊安·泰勒(Ian Lance Taylor):GCC社区的活跃人物,gold连接器和GCC过程间优化LTO

    的主要设计者,Zembu公司的创始人。

    布拉德·菲茨帕特里克(Brad Fitzpatrick,http:en.wikipedia.orgwikiBrad_Fitzpatrick):

    LiveJournal的创始人,著名开源项目memcached的作者。

    虽然我们这里只列出了一部分,大家已经可以看出这个语言开发团队空前强大,这让我们在

    为Go语言的优秀特性而兴奋之外,还非常看好这门语言的发展前景。

    1.2 语言特性

    Go语言作为一门全新的静态类型开发语言,与当前的开发语言相比具备众多令人兴奋不已

    的新特性。本书从第2章开始,我们将对Go语言的各个方面进行详细解析,让读者能够尽量轻松

    地掌握这门简洁、有趣却又超级强大的新语言。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.2 语言特性 3

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    这里先给读者罗列一下Go语言最主要的特性:

    自动垃圾回收

    更丰富的内置类型

    函数多返回值

    错误处理

    匿名函数和闭包

    类型和接口

    并发编程

    反射

    语言交互性

    1.2.1 自动垃圾回收

    我们可以先看下不支持垃圾回收的语言的资源管理方式,以下为一小段C语言代码:

    void foo

    {

    char p = new char[128];... 对p指向的内存块进行赋值

    func1(p); 使用内存指针

    delete[] p;

    }

    各种非预期的原因,比如由于开发者的疏忽导致最后的delete语句没有被调用,都会引发

    经典而恼人的内存泄露问题。假如该函数被调用得非常频繁,那么我们观察该进程执行时,会发

    现该进程所占用的内存会一直疯长,直至占用所有系统内存并导致程序崩溃,而如果泄露的是系

    统资源的话,那么后果还会更加严重,最终很有可能导致系统崩溃。

    手动管理内存的另外一个问题就是由于指针的到处传递而无法确定何时可以释放该指针所

    指向的内存块。假如代码中某个位置释放了内存,而另一些地方还在使用指向这块内存的指针,那么这些指针就变成了所谓的“野指针”(wild pointer)或者“悬空指针”(dangling pointer) ,对

    这些指针进行的任何读写操作都会导致不可预料的后果。

    由于其杰出的效率,C和C++语言在非常长的时间内都作为服务端系统的主要开发语言,比

    如Apache、Nginx和MySQL等著名的服务器端软件就是用C和C++开发的。然而,内存和资源管

    理一直是一个让人非常抓狂的难题。服务器的崩溃十有八九就是因为不正确的内存和资源管理导

    致,更讨厌的是这种内存和资源管理问题即使被发现了,也很难定位到具体的错误地点,导致无

    数程序员通宵达旦地调试程序。

    这个问题在多年里被不同人用不同的方式来试图解决,并诞生了一些非常著名的内存检查工

    具,比如Rational Purify、Compuware BoundsChecker和英特尔的Parallel Inspector等。从设计方法的

    角度也衍生了类似于内存引用计数之类的方法(通常被称为“智能指针” ) ,后续在Windows平台

    上标准化的COM出现的一个重要原因就是为了解决内存管理的难题。但是事实证明,这些工具和

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)4 第 1章 初识 Go语言

    方法虽然能够在一定程度上辅助开发者,但并没法让开发者避免通宵调试这样又苦又累的工作。

    到目前为止,内存泄露的最佳解决方案是在语言级别引入自动垃圾回收算法(Garbage

    Collection,简称GC)。所谓垃圾回收,即所有的内存分配动作都会被在运行时记录,同时任何对

    该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现

    有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存。当然,因为需要尽量最小

    化垃圾回收的性能损耗,以及降低对正常程序执行过程的影响,现实中的垃圾回收算法要比这个

    复杂得多,比如为对象增加年龄属性等,但基本原理都是如此。

    自动垃圾回收在CC++社区一直作为一柄双刃剑看待,虽然到C++0x(后命名为C++11)正

    式发布时,这个呼声颇高的特性总算是被加入了,但按C++之父的说法,由于C++本身过于强大,导致在C++中支持垃圾收集变成了一个困难的工作。假如C++支持垃圾收集,以下的代码片段在

    运行时就会是一个严峻的考验:

    int p = new int;

    p += 10; 对指针进行了偏移,因此那块内存不再被引用

    …… 这里可能会发生针对这块int内存的垃圾收集 ……

    p -= 10; 咦,居然又偏移到原来的位置

    p = 10; 如果有垃圾收集,这里就无法保证可以正常运行了

    微软的C++CLI算是用一种偏门的方式让C++程序员们有机会品尝一下垃圾回收功能的鲜美

    味道。在CC++之后出现的新语言,比如Java和C等,基本上都已经自带自动垃圾回收功能。

    Go语言作为一门新生的开发语言,当然不能忽略内存管理这个问题。又因为Go语言没有C++

    这么“强大”的指针计算功能,因此可以很自然地包含垃圾回收功能。因为垃圾回收功能的支持,开发者无需担心所指向的对象失效的问题,因此Go语言中不需要delete关键字,也不需要free

    方法来明确释放内存。例如,对于以上的这个C语言例子,如果使用Go语言实现,我们就完全不

    用考虑何时需要释放之前分配的内存的问题,系统会自动帮我们判断,并在合适的时候(比如CPU

    相对空闲的时候)进行自动垃圾收集工作。

    1.2.2 更丰富的内置类型

    除了几乎所有语言都支持的简单内置类型(比如整型和浮点型等)外,Go语言也内置了一

    些比较新的语言中内置的高级类型,比如C和Java中的数组和字符串。除此之外,Go语言还内置

    了一个对于其他静态类型语言通常用库方式支持的字典类型(map)。Go语言设计者对为什么内

    置map这个问题的回答也颇为简单:既然绝大多数开发者都需要用到这个类型,为什么还非要每

    个人都写一行import语句来包含一个库?这也是一个典型的实战派观点,与很多其他语言的学

    院派气息迥然不同。

    另外有一个新增的数据类型:数组切片(Slice) 。我们可以认为数组切片是一种可动态增

    长的数组。这几种数据结构基本上覆盖了绝大部分的应用场景。数组切片的功能与C++标准库中

    的vector非常类似。Go语言在语言层面对数组切片的支持,相比C++开发者有效地消除了反复

    写以下几行代码的工作量:

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.2 语言特性 5

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    include

    include

    include

    using namespace std;

    因为是语言内置特性,开发者根本不用费事去添加依赖的包,既可以少一些输入工作量,也

    可以让代码看起来尽量简洁。

    1.2.3 函数多返回值

    目前的主流语言中除Python外基本都不支持函数的多返回值功能,不是没有这类需求,可能

    是语言设计者没有想好该如何提供这个功能,或者认为这个功能会影响语言的美感。

    比如我们如果要定义一个函数用于返回个人名字信息,而名字信息因为包含多个部分——姓

    氏、名字、中间名和别名,在不支持多返回值的语言中我们有以下两种做法:要么专门定义一个

    结构体用于返回,比如:

    struct name

    {

    char first_name[20];

    char middle_name[20];

    char last_name[20];

    char nick_name[48];

    };

    函数原型

    extern name get_name;

    函数调用

    name n = get_name;

    或者以传出参数的方式返回多个结果:

    函数原型

    extern void get_name(

    outchar first_name,outchar middle_name,outchar last_name,outchar nick_name);

    先分配内存

    char first_name[20];

    char middle_name[20];

    char last_name[20];

    char nick_name[48];

    函数调用

    get_name(first_name, middle_name, last_name, nick_name);

    Go语言革命性地在静态开发语言阵营中率先提供了多返回值功能。这个特性让开发者可以

    从原来用各种比较别扭的方式返回多个值的痛苦中解脱出来,既不用再区分参数列表中哪几个用

    于输入,哪几个用于输出,也不用再只为了返回多个值而专门定义一个数据结构。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)6 第 1章 初识 Go语言

    在Go语言中,上述的例子可以修改为以下的样子:

    func getName(firstName, middleName, lastName, nickName string){

    return May, M, Chen, Babe

    }

    因为返回值都已经有名字,因此各个返回值也可以用如下方式来在不同的位置进行赋值,从

    而提供了极大的灵活性:

    func getName(firstName, middleName, lastName, nickName string){

    firstName = May

    middleName = M

    lastName = Chen

    nickName = Babe

    return

    }

    并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。而函数的调

    用相比CC++语言要简化很多:

    fn, mn, ln, nn := getName

    如果开发者只对该函数其中的某几个返回值感兴趣的话,也可以直接用下划线作为占位符来

    忽略其他不关心的返回值。下面的调用表示调用者只希望接收lastName的值,这样可以避免声

    明完全没用的变量:

    _, _, lastName, _ := getName

    我们会在第2章中详细讲解多重返回值的用法。

    1.2.4 错误处理

    Go语言引入了3个关键字用于标准的错误处理流程,这3个关键字分别为defer、panic和

    recover。本书的“序”已经用示例展示了defer关键字的强大之处,在第2章中我们还会详细

    描述Go语言错误处理机制的独特之处。整体上而言与C++和Java等语言中的异常捕获机制相比,Go语言的错误处理机制可以大量减少代码量,让开发者也无需仅仅为了程序安全性而添加大量

    一层套一层的try-catch语句。这对于代码的阅读者和维护者来说也是一件很好的事情,因为可

    以避免在层层的代码嵌套中定位业务代码。2.6节将介绍Go语言中的错误处理机制。

    1.2.5 匿名函数和闭包

    在Go语言中,所有的函数也是值类型,可以作为参数传递。Go语言支持常规的匿名函数和

    闭包,比如下列代码就定义了一个名为f的匿名函数,开发者可以随意对该匿名函数变量进行传

    递和调用:

    f := func(x, y int) int {

    return x + y

    }

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.2 语言特性 7

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    1.2.6 类型和接口

    Go语言的类型定义非常接近于C语言中的结构(struct),甚至直接沿用了struct关键字。相

    比而言,Go语言并没有直接沿袭C++和Java的传统去设计一个超级复杂的类型系统,不支持继承

    和重载,而只是支持了最基本的类型组合功能。

    巧妙的是,虽然看起来支持的功能过于简洁,细用起来你却会发现,C++和Java使用那些复

    杂的类型系统实现的功能在Go语言中并不会出现无法表现的情况,这反而让人反思其他语言中

    引入这些复杂概念的必要性。我们在第3章中将详细描述Go语言的类型系统。

    Go语言也不是简单的对面向对象开发语言做减法,它还引入了一个无比强大的“非侵入式”

    接口的概念,让开发者从以往对C++和Java开发中的接口管理问题中解脱出来。在C++中,我们

    通常会这样来确定接口和类型的关系:

    抽象接口

    interface IFly

    {

    virtual void Fly=0;

    };

    实现类

    class Bird : public IFly

    {

    public:

    Bird

    {}

    virtual ~Bird

    {}

    public:

    void Fly

    {

    以鸟的方式飞行

    }

    };

    void main

    {

    IFly pFly = new Bird;

    pFly->Fly;

    delete pFly;

    }

    显然,在实现一个接口之前必须先定义该接口,并且将类型和接口紧密绑定,即接口的修改

    会影响到所有实现了该接口的类型,而Go语言的接口体系则避免了这类问题:

    type Bird struct {...

    }

    func (b Bird) Fly {

    以鸟的方式飞行

    }

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)8 第 1章 初识 Go语言

    我们在实现Bird类型时完全没有任何IFly的信息。我们可以在另外一个地方定义这个IFly

    接口:

    type IFly interface {

    Fly

    }

    这两者目前看起来完全没有关系,现在看看我们如何使用它们:

    func main {

    var fly IFly = new(Bird)

    fly.Fly

    }

    可以看出,虽然Bird类型实现的时候,没有声明与接口IFly的关系,但接口和类型可以直

    接转换,甚至接口的定义都不用在类型定义之前,这种比较松散的对应关系可以大幅降低因为接

    口调整而导致的大量代码调整工作。

    1.2.7 并发编程

    Go语言引入了goroutine概念,它使得并发编程变得非常简单。通过使用goroutine而不是裸用

    操作系统的并发机制,以及使用消息传递来共享内存而不是使用共享内存来通信,Go语言让并

    发编程变得更加轻盈和安全。

    通过在函数调用前使用关键字go,我们即可让该函数以goroutine方式执行。goroutine是一种

    比线程更加轻盈、更省资源的协程。Go语言通过系统的线程来多路派遣这些函数的执行,使得

    每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自

    动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销

    非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可

    以很轻松地编写高并发程序,达到我们想要的目的。

    Go语言实现了CSP(通信顺序进程,Communicating Sequential Process)模型来作为goroutine

    间的推荐通信方式。在CSP模型中,一个并发系统由若干并行运行的顺序进程组成,每个进程不

    能对其他进程的变量赋值。进程之间只能通过一对通信原语实现协作。Go语言用channel(通道)

    这个概念来轻巧地实现了CSP模型。channel的使用方式比较接近Unix系统中的管道(pipe)概念,可以方便地进行跨goroutine的通信。

    另外,由于一个进程内创建的所有goroutine运行在同一个内存地址空间中,因此如果不同的

    goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写锁。Go语言标准库中的

    sync包提供了完备的读写锁功能。

    下面我们用一个简单的例子来演示goroutine和channel的使用方式。这是一个并行计算的例

    子,由两个goroutine进行并行的累加计算,待这两个计算过程都完成后打印计算结果,具体如代

    码清单1-1所示。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.2 语言特性 9

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    代码清单1-1 paracalc.go

    package main

    import fmt

    func sum(values [] int, resultChan chan int) {

    sum := 0

    for _, value := range values {

    sum += value

    }

    resultChan <- sum 将计算结果发送到channel中

    }

    func main {

    values := [] int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    resultChan := make(chan int, 2)

    go sum(values[:len(values)2], resultChan)

    go sum(values[len(values)2:], resultChan)

    sum1, sum2 := <-resultChan, <-resultChan 接收结果

    fmt.Println(Result:, sum1, sum2, sum1 + sum2)

    }

    1.2.8 反射

    反射(reflection)是在Java语言出现后迅速流行起来的一种概念。通过反射,你可以获取对

    象类型的详细信息,并可动态操作对象。反射是把双刃剑,功能强大但代码可读性并不理想。若

    非必要,我们并不推荐使用反射。

    Go语言的反射实现了反射的大部分功能,但没有像Java语言那样内置类型工厂,故而无法做

    到像Java那样通过类型字符串创建对象实例。在Java中,你可以读取配置并根据类型名称创建对

    应的类型,这是一种常见的编程手法,但在Go语言中这并不被推荐。

    反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal Unmarshal)。

    例如,Go语言标准库的encodingjson、encodingxml、encodinggob、encodingbinary等包就大量

    依赖于反射功能来实现。

    这里先举一个小例子,可以利用反射功能列出某个类型中所有成员变量的值,如代码清单1-2

    所示。

    代码清单1-2 reflect.go

    package main

    import (

    fmt

    reflect)

    type Bird struct {

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)10 第 1章 初识 Go语言

    Name string

    LifeExpectance int

    }

    func (b Bird) Fly {

    fmt.Println(I am flying...)

    }

    func main {

    sparrow := Bird{Sparrow, 3}

    s := reflect.ValueOf(sparrow).Elem

    typeOfT := s.Type

    for i := 0; i < s.NumField; i++ {

    f := s.Field(i)

    fmt.Printf(%d: %s %s = %v\n, i, typeOfT.Field(i).Name, f.Type,f.Interface)

    }

    }

    该程序的输出结果为:

    0: Name string = Sparrow

    1: LifeExpectance int = 3

    我们会在第9章中简要介绍反射的基本使用方法和注意事项。

    1.2.9 语言交互性

    由于Go语言与C语言之间的天生联系,Go语言的设计者们自然不会忽略如何重用现有C模块

    的这个问题,这个功能直接被命名为Cgo。Cgo既是语言特性,同时也是一个工具的名称。

    在Go代码中,可以按Cgo的特定语法混合编写C语言代码,然后Cgo工具可以将这些混合的C

    代码提取并生成对于C功能的调用包装代码。开发者基本上可以完全忽略这个Go语言和C语言的

    边界是如何跨越的。

    与Java中的JNI不同,Cgo的用法非常简单,比如代码清单1-3就可以实现在Go中调用C语言标

    准库的puts函数。

    代码清单1-3 cprint.go

    package main

    include

    import C

    import unsafe

    func main {

    cstr := C.CString(Hello, world)

    C.puts(cstr)

    C.free(unsafe.Pointer(cstr))

    }

    我们将在第9章中详细介绍Cgo的用法。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.3 第一个Go程序 11

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    1.3 第一个 Go程序

    自Kernighan和Ritchie合著的《C程序设计语言》(The C Programming Language)出版以来,几乎所有的编程书都以一个Hello world小例子作为开始。我们也不免俗(或者说尊重传统) ,下

    面我们从一个简单Go语言版本的Hello world来初窥Go这门新语言的模样,如代码清单1-4所示。

    代码清单1-4 hello.go

    package main

    import fmt 我们需要使用fmt包中的Println函数

    func main {

    fmt.Println(Hello, world. 你好,世界!)

    }

    1.3.1 代码解读

    每个Go源代码文件的开头都是一个package声明,表示该Go代码所属的包。包是Go语言里

    最基本的分发单位,也是工程管理中依赖关系的体现。要生成Go可执行程序,必须建立一个名

    字为main的包,并且在该包中包含一个叫main的函数(该函数是Go可执行程序的执行起点)。

    Go语言的main函数不能带参数,也不能定义返回值。命令行传入的参数在os.Args变量

    中保存。如果需要支持命令行开关,可使用flag包。在本书后面我们将解释如何使用flag包来

    做命令行参数规范的定义,以及获取和解析命令行参数。

    在包声明之后,是一系列的import语句,用于导入该程序所依赖的包。由于本示例程序用

    到了Println函数,所以需要导入该函数所属的fmt包。

    有一点需要注意,不得包含在源代码文件中没有用到的包,否则Go编译器会报编译错误。

    这与下面提到的强制左花括号{的放置位置以及之后会提到的函数名的大小写规则,均体现了Go

    语言在语言层面解决软件工程问题的设计哲学。

    所有Go函数(包括在对象编程中会提到的类型成员函数)以关键字func开头。一个常规的

    函数定义包含以下部分:

    func 函数名(参数列表)(返回值列表) {

    函数体

    }

    对应的一个实例如下:

    func Compute(value1 int, value2 float64)(result float64, err error) {

    函数体

    }

    Go支持多个返回值。以上的示例函数Compute返回了两个值,一个叫result,另一个是

    err。并不是所有返回值都必须赋值。在函数返回时没有被明确赋值的返回值都会被设置为默认

    值,比如result会被设为0.0,err会被设为nil。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)12 第 1章 初识 Go语言

    Go程序的代码注释与C++保持一致,即同时支持以下两种用法:

    块注释

    行注释

    相信熟悉C和C++的读者也发现了另外一点,即在这段Go示例代码里没有出现分号。Go

    程序并不要求开发者在每个语句后面加上分号表示语句结束,这是与C和C++的一个明显不同

    之处。

    有些读者可能会自然地把左花括号{另起一行放置,这样做的结果是Go编译器报告编译错

    误,这点需要特别注意:

    syntax error: unexpected semicolon or newline before {

    1.3.2 编译环境准备

    前面我们给大家大概介绍了第一个Go程序的基本结构,接下来我们来准备编译这段小程序

    的环境。

    在Go 1发布之前,开发者要想使用Go,只能自行下载代码并进行编译,而现在可以直接下

    载对应的安装包进行安装,安装包的下载地址为http:code.google.compgodownloadslist。

    在nix环境中,Go默认会被安装到usrlocalgo目录中。安装包在安装完成后会自动添加执行

    文件目录到系统路径中。

    安装完成后,请重新启动命令行程序,然后运行以下命令以验证Go是否已经正确安装:

    go version

    go version go1

    如果该命令能够正常运行并输出相应的信息,说明Go编译环境已经正确安装完毕。如果提

    示找不到go命令,可以通过手动添加usrlocalgobin到PATH环境变量来解决。

    1.3.3 编译程序

    假设之前介绍的Hello, world代码被保存为了hello.go,并位于~goyard目录下,那么可以用以

    下命令行编译并直接运行该程序:

    cd ~goyard

    go run hello.go 直接运行

    Hello, world. 你好,世界!

    使用这个命令,会将编译、链接和运行3个步骤合并为一步,运行完后在当前目录下也看不

    到任何中间文件和最终的可执行文件。如果要只生成编译结果而不自动运行,我们也可以使用 Go

    命令行工具的build命令:

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.5 工程管理 13

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    cd ~goyard

    go build hello.go

    .hello

    Hello, world. 你好,世界!

    可以看出,Go命令行工具是一个非常强大的源代码管理工具。我们将在第4章中详细讲解Go

    命令行工具所包含的更多更强大的功能。

    从根本上说,Go命令行工具只是一个源代码管理工具,或者说是一个前端。真正的Go编译

    器和链接器被Go命令行工具隐藏在后面,我们可以直接使用它们:

    6g helloworld.go

    6l helloworld.6

    .6.out

    Hello, world. 你好,世界!

    6g和6l是64位版本的Go编译器和链接器,对应的32位版本工具为8g和8l。Go还有另外一个

    GCC版本的编译器,名为 gccgo,但不在本书的讨论范围内。

    1.4 开发工具选择

    Google并没有随着Go 1的发布推出官方的Go集成开发环境(IDE) ,因此开发者需要自行考

    虑和选择合适的开发工具。目前比较流行的开发工具如下:

    文本编辑工具gedit(Linux)Notepad++(Windows)Fraise(Mac OS X);

    安装了GoClipse插件的Eclipse,集成性做得很好;

    VimEmacs,万能开发工具;

    LiteIDE,一款专为Go语言开发的集成开发环境。

    由于Go代码的轻巧和模块化特征,其实一般的文本编辑工具就可以胜任Go开发工作。本书

    的所有代码均使用Linux上的gedit工具完成。

    Go社区提供了各种文本编辑器的语法高亮设置方法,这在本书最后一章也有所介绍。

    1.5 工程管理

    在实际的开发工作中,直接调用编译器进行编译和链接的场景是少而又少,因为在工程中不

    会简单到只有一个源代码文件,且源文件之间会有相互的依赖关系。如果这样一个文件一个文件

    逐步编译,那不亚于一场灾难。Go语言的设计者作为行业老将,自然不会忽略这一点。早期Go

    语言使用makefile作为临时方案,到了Go 1发布时引入了强大无比的Go命令行工具。

    Go命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推

    导工程结构和构建顺序。针对只有一个源文件的情况讨论工程管理看起来会比较多余,因为这可

    以直接用go run和go build搞定。下面我们将用一个更接近现实的虚拟项目来展示Go语言的

    基本工程管理方法。

    假设有这样一个场景:我们需要开发一个基于命令行的计算器程序。下面为此程序的基本

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)14 第 1章 初识 Go语言

    用法:

    calc help

    USAGE: calc command [arguments] ...

    The commands are:

    sqrt Square root of a non-negative value.

    add Addition of two values.

    calc sqrt 4 开根号

    2

    calc add 1 2 加法

    3

    我们假设这个工程被分割为两个部分:

    可执行程序,名为calc,内部只包含一个calc.go文件;

    算法库,名为simplemath,每个command对应于一个同名的go文件,比如add.go。

    则一个正常的工程目录组织应该如下所示:

    

    ├─

    ├─

    ├─calc.go

    ├─

    ├─add.go

    ├─add_test.go

    ├─sqrt.go

    ├─sqrt_test.go

    ├─

    ├─#包将被安装到此处

    在上面的结构里,带尖括号的名字表示其为目录。xxx_test.go表示的是一个对于xxx.go的单元

    测试,这也是Go工程里的命名规则。

    为了让读者能够动手实践,这里我们会列出所有的源代码并以注释的方式解释关键内容,如

    代码清单1-5至代码清单1-9所示。需要注意的是,本示例主要用于示范工程管理,并不保证代码

    达到产品级质量。

    代码清单1-5 calc.go

    calc.go

    package main

    import os 用于获得命令行参数os.Args

    import fmt

    import simplemath

    import strconv

    var Usage = func {

    fmt.Println(USAGE: calc command [arguments] ...)

    fmt.Println(\nThe commands are:\n\tadd\tAddition of two values.\n\tsqrt\tSquare

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.5 工程管理 15

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    root of a non-negative value.)

    }

    func main {

    args := os.Args

    if args == nil || len(args) < 2 {

    Usage

    return

    }

    switch args[0] {

    case add:

    if len(args) != 3 {

    fmt.Println(USAGE: calc add )

    return

    }

    v1, err1 := strconv.Atoi(args[1])

    v2, err2 := strconv.Atoi(args[2])

    if err1 != nil || err2 != nil {

    fmt.Println(USAGE: calc add )

    return

    }

    ret := simplemath.Add(v1, v2)

    fmt.Println(Result: , ret)

    case sqrt:

    if len(args) != 2 {

    fmt.Println(USAGE: calc sqrt )

    return

    }

    v, err := strconv.Atoi(args[1])

    if err != nil {

    fmt.Println(USAGE: calc sqrt )

    return

    }

    ret := simplemath.Sqrt(v)

    fmt.Println(Result: , ret)

    default:

    Usage

    }

    }

    代码清单1-6 add.go

    add.go

    package simplemath

    func Add(a int, b int) int {

    return a + b

    }

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)16 第 1章 初识 Go语言

    代码清单1-7 add_test.go

    add_test.go

    package simplemath

    import testing

    func TestAdd1(t testing.T) {

    r := Add(1, 2)

    if r != 3 {

    t.Errorf(Add(1, 2) failed. Got %d, expected 3., r)

    }

    }

    代码清单1-8 sqrt.go

    sqrt.go

    package simplemath

    import math

    func Sqrt(i int) int {

    v := math.Sqrt(float64(i))

    return int(v)

    }

    代码清单1-9 sqrt_test.go

    sqrt_test.go

    package simplemath

    import testing

    func TestSqrt1(t testing.T) {

    v := Sqrt(16)

    if v != 4 {

    t.Errorf(Sqrt(16) failed. Got %v, expected 4., v)

    }

    }

    为了能够构建这个工程,需要先把这个工程的根目录加入到环境变量GOPATH中。 假设calcproj

    目录位于~goyard下,则应编辑~.bashrc文件,并添加下面这行代码:

    export GOPATH=~goyardcalcproj

    然后执行以下命令应用该设置:

    source ~.bashrc

    GOPATH和PATH环境变量一样,也可以接受多个路径,并且路径和路径之间用冒号分割。

    设置完GOPATH后,现在我们开始构建工程。假设我们希望把生成的可执行文件放到

    calcprojbin目录中,需要执行的一系列指令如下:

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.5 工程管理 17

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    cd ~goyardcalcproj

    mkdir bin

    cd bin

    go build calc

    顺利的话,将在该目录下发现生成的一个叫做calc的可执行文件,执行该文件以查看帮助信

    息并进行算术运算:

    .calc

    USAGE: calc command [arguments] ...

    The commands are:

    addAddition of two values.

    sqrtSquare root of a non-negative value.

    .calc add 2 3

    Result: 5

    .calc sqrt 9

    Result: 3

    从上面的构建过程中可以看到,真正的构建命令就一句:

    go build calc

    这就是为什么说Go命令行工具是非常强大的。我们不需要写makefile,因为这个工具会替我

    们分析,知道目标代码的编译结果应该是一个包还是一个可执行文件,并分析import语句以了

    解包的依赖关系,从而在编译calc.go之前先把依赖的simplemath编译打包好。Go命令行程序制

    定的目录结构规则让代码管理变得非常简单。

    另外,我们在写simplemath包时,为每一个关键的函数编写了对应的单元测试代码,分别

    位于add_test.go和sqrt_test.go中。那么我们到底怎么运行这些单元测试呢?这也非常简单。因为

    已经设置了GOPATH,所以可以在任意目录下执行以下命令:

    go test simplemath

    ok simplemath0.014s

    可以看到,运行结果列出了测试的内容、测试结果和测试时间。如果我故意把add_test.go的

    代码改成这样的错误场景:

    func TestAdd1(t testing.T) {

    r := Add(1, 2)

    if r != 2 { 这里本该是3,故意改成2测试错误场景

    t.Errorf(Add(1, 2) failed. Got %d, expected 3., r)

    }

    }

    然后我们再次执行单元测试,将得到如下的结果:

    go test simplemath--- FAIL: TestAdd1 (0.00 seconds)

    add_test.go:8: Add(1, 2) failed. Got 3, expected 3.

    FAIL

    FAILsimplemath0.013s

    打印的错误信息非常简洁,却已经足够让开发者快速定位到问题代码所在的文件和行数,从

    而在最短的时间内确认是单元测试的问题还是程序的问题。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)18 第 1章 初识 Go语言

    1.6 问题追踪和调试

    Go语言所提供的是尽量简单的语法和尽量完善的库,以尽可能降低问题的发生概率。当然,问题还是会发生的,这时需要用到问题追踪和调试技能。这里我们简单介绍下两个最常规的问题

    跟踪方法:打印日志和使用GDB进行逐步调试。

    1.6.1 打印日志

    Go语言包中包含一个fmt包,其中提供了大量易用的打印函数,我们会接触到的主要是

    Printf和Println。这两个函数可以满足我们的基本调试需求,比如临时打印某个变量。

    这两个函数的参数非常类似于C语言运行库中的Printf,有C语言开发经验的同学会很容易上

    手。下面是几个使用Printf和Println的例子:

    fval := 110.48

    ival := 200

    sval := This is a string.

    fmt.Println(The value of fval is, fval)

    fmt.Printf(fval=%f, ival=%d, sval=%s\n, fval, ival, sval)

    fmt.Printf(fval=%v, ival=%v, sval=%v\n, fval, ival, sval)

    输出结果为:

    The value of fval is 100.48

    fval=100.48, ival=200, sval=This is a string.

    fval=100.48, ival=200, sval=This is a string.

    fmt包的这一系列格式化打印函数使用起来非常方便,但在正式开始用Go开发服务器系统

    时,我们就不能只依赖fmt包了,而是需要设计严格的日志规范。Go语言的log包提供了基础的

    日志功能。如果有需要,你也可以引入自己的log模块。

    1.6.2 GDB调试

    不用设置什么编译选项, Go语言编译的二进制程序直接支持GDB调试,比如之前用go build

    calc编译出来的可执行文件calc,就可以直接用以下命令以调试模式运行:

    gdb calc

    因为GDB的标准用法与Go没有特别关联,这里就不详细展开了,有兴趣的读者可以自行查

    看对应的文档。需要注意的是,Go编译器生成的调试信息格式为DWARFv3,只要版本高于7.1

    的GDB应该都支持它。

    1.7 如何寻求帮助

    Go语言已经发展了两年时间,凭借着语言本身的优越品质和Google的强大号召力,在推出正

    式版本之前就已经拥有了广大的爱好者和社区,本节就介绍一些不错的Go语言社区。在遇到问

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)1.8 小结 19

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2

    题时,请随时访问这些社区,并勇敢地提问,相信你能得到满意的解决方法。

    1.7.1 邮件列表

    邮件列表是Go语言最活跃的社区之一,而且与其他语言社区不同的是,在这里你可以很频

    繁地看到好多Go语言的核心开发成员(比如Ross Cox)亲自回答问题,其权威程度和对学习Go

    语言的价值显而易见。

    Go邮件组的地址为http:groups.google.comgroupgolang-nuts 。该邮件列表对所有人公开,你

    可以在这个页面上直接加入。该邮件列表的沟通语言为英语。根据我们的经验,在该邮件列表上

    提出的问题通常在24小时内可以得到解决。

    Go的中文邮件组为http:groups.google.comgroupgolang-china。如果你更习惯中文讨论环境,可以参与。另外,尽管http:groups.google.comgroupecug不是以Go语言为专题,但有关Go语言

    的服务端开发,也是它最重要的话题之一。

    1.7.2 网站资源

    Go语言的官方网站为 http:golang.org,这个网站只随着Go的主要版本发布而更新,因此并

    不反映Go的最新进展。如果读者希望跟进Go语言的最新进展,可以到http:code.google.compgo

    直接下载最新代码。这里持续对Go资料进行了整理:http:github.comwonderfowonderfogowiki。

    1.8 小结

    本章我们简要介绍了Go语言的起源和背景,并结合若干代码示例简要介绍了我们认为最值

    得关注的关键特性,之后按老规矩以Hello, world这个例子作为起点帮助读者快速熟悉这门新语

    言,消除对Go语言的陌生感,并搭建好自己的Go开发环境。

    通过这一章的学习,我们相信读者对于Go语言的简单易学特性已经有了比较直接的了解。

    在后续的章节中,各位读者可以利用在本章中搭建的开发环境和学习的工程管理知识,快速动手

    尝试各种Go语言令人兴奋的语言功能。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)20 第 2章 顺序编程

    顺 序 编 程

    从本章开始,我们将为你逐步展开Go语言的各种美妙特性,而本章主要介绍Go语言的顺序

    编程特性。在阅读完本章后,相信你会理解为什么Go语言会被称为“更好的C语言” 。

    在本章中我们会自然涉及一些C语言的知识。如果你之前没有学过C语言,也没关系,对于

    Go语言的整体理解并不会有太大的影响,但如果之前学过C语言,那么你将会更具体地理解Go

    语言相比C语言的众多革新之处。

    2.1 变量

    变量是几乎所有编程语言中最基本的组成元素。从根本上说,变量相当于是对一块数据存储

    空间的命名,程序可以通过定义一个变量来申请一块数据存储空间,之后可以通过引用变量名来

    使用这块存储空间。

    Go语言中的变量使用方式与C语言接近,但具备更大的灵活性。

    2.1.1 变量声明

    Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了

    关键字var,而类型信息放在变量名之后,示例如下:

    var v1 int

    var v2 string

    var v3 [10]int 数组

    var v4 []int 数组切片

    var v5 struct {

    f int

    }

    var v6 int 指针

    var v7 map[string]int map,key为string类型,value为int类型

    var v8 func(a int) int

    变量声明语句不需要使用分号作为结束符。与C语言相比,Go语言摒弃了语句必须以分号作

    为语句结束标记的习惯。

    var关键字的另一种用法是可以将若干个需要声明的变量放置在一起,免得程序员需要重复

    写var关键字,如下所示:

    第2章

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.1 变量 21

    1

    2

    3

    4

    5

    9

    6

    7

    8

    var (

    v1 int

    v2 string)

    2.1.2 变量初始化

    对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素,如下

    所示:

    var v1 int = 10 正确的使用方式1

    var v2 = 10 正确的使用方式2,编译器可以自动推导出v2的类型

    v3 := 10 正确的使用方式3,编译器可以自动推导出v3的类型

    以上三种用法的效果是完全一样的。与第一种用法相比,第三种用法需要输入的字符数大大

    减少,是懒程序员和聪明程序员的最佳选择。这里Go语言也引入了另一个C和C++中没有的符号

    (冒号和等号的组合:=),用于明确表达同时进行变量声明和初始化的工作。

    指定类型已不再是必需的,Go编译器可以从初始化表达式的右值推导出该变量应该声明为

    哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语

    言(静态类型语言)。

    当然,出现在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误,比如下面这个

    写法:

    var i int

    i := 2

    会导致类似如下的编译错误:

    no new variables on left side of :=

    2.1.3 变量赋值

    在Go语法中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值

    过程:

    var v10 int

    v10 = 123

    Go语言的变量赋值与多数语言一致,但Go语言中提供了CC++程序员期盼多年的多重赋值功

    能,比如下面这个交换i和j变量的语句:

    i, j = j, i

    在不支持多重赋值的语言中,交互两个变量的内容需要引入一个中间变量:

    t = i; i = j; j = t;

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)22 第 2章 顺序编程

    多重赋值的特性在Go语言库的实现中也被使用得相当充分,在介绍函数的多重返回值时,将对其进行更加深入的介绍。总而言之,多重赋值功能让Go语言与CC++语言相比可以非常明显

    地减少代码行数。

    2.1.4 匿名变量

    我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个

    值,却因为该函数返回多个值而不得不定义一堆没用的变量。在Go中这种情况可以通过结合使

    用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。

    假设GetName函数的定义如下,它返回3个值,分别为firstName、lastName和

    nickName:

    func GetName (firstName, lastName, nickName string) {

    return May, Chan, Chibi Maruko

    }

    若只想获得nickName,则函数调用语句可以用如下方式编写:

    _, _, nickName := GetName

    这种用法可以让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅

    降低沟通的复杂度和代码维护的难度。

    2.2 常量

    在Go语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型(包括整型、浮点型和复数类型)、布尔类型、字符串类型等。

    2.2.1 字面常量

    所谓字面常量(literal),是指程序中硬编码的常量,如:

    -12

    3.14159265358979323846 浮点类型的常量

    3.2+12i 复数类型的常量

    true 布尔类型的常量

    foo 字符串常量

    在其他语言中,常量通常有特定的类型,比如?12在C语言中会认为是一个int类型的常量。

    如果要指定一个值为?12的long类型常量,需要写成?12l,这有点违反人们的直观感觉。Go语言

    的字面常量更接近我们自然语言中的常量概念,它是无类型的。只要这个常量在相应类型的值域

    范围内,就可以作为该类型的常量,比如上面的常量?12,它可以赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.2 常量 23

    1

    2

    3

    4

    5

    9

    6

    7

    8

    2.2.2 常量定义

    通过const关键字,你可以给字面常量指定一个友好的名字:

    const Pi float64 = 3.14159265358979323846

    const zero = 0.0 无类型浮点常量

    const (

    size int64 = 1024

    eof = -1 无类型整型常量)

    const u, v float32 = 0, 3 u = 0.0, v = 3.0,常量的多重赋值

    const a, b, c = 3, 4, foo

    a = 3, b = 4, c = foo, 无类型整型和字符串常量

    Go的常量定义可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它

    与字面常量一样,是无类型常量。

    常量定义的右值也可以是一个在编译期运算的常量表达式,比如

    const mask = 1 << 3

    由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达

    式,比如试图以如下方式定义常量就会导致编译错误:

    const Home = os.GetEnv(HOME)

    原因很简单,os.GetEnv只有在运行期才能知道返回结果,在编译期并不能确定,所以

    无法作为常量定义的右值。

    2.2.3 预定义常量

    Go语言预定义了这些常量:true、false和iota。

    iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被

    重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。

    从以下的例子可以基本理解iota的用法:

    const ( iota被重设为0

    c0 = iota c0 == 0

    c1 = iota c1 == 1

    c2 = iota c2 == 2)

    const (

    a = 1 << iota a == 1 (iota在每个const开头被重设为0)

    b = 1 << iota b == 2

    c = 1 << iota c == 4)

    const (

    u = iota 42 u == 0

    v float64 = iota 42 v == 42.0

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)24 第 2章 顺序编程

    w = iota 42 w == 84)

    const x = iota x == 0 (因为iota又被重设为0了)

    const y = iota y == 0 (同上)

    如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上

    面的前两个const语句可简写为:

    const ( iota被重设为0

    c0 = iota c0 == 0

    c1 c1 == 1

    c2 c2 == 2)

    const (

    a = 1 <
    b b == 2

    c c == 4)

    2.2.4 枚举

    枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们

    看到可以用在const后跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义

    枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。

    下面是一个常规的枚举表示法,其中定义了一系列整型常量:

    const (

    Sunday = iota

    Monday

    Tuesday

    Wednesday

    Thursday

    Friday

    Saturday

    numberOfDays 这个常量没有导出)

    同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。

    以上例子中numberOfDays为包内私有,其他符号则可被其他包访问。

    2.3 类型

    Go语言内置以下这些基础类型:

    布尔类型:bool。

    整型:int8、byte、int16、int、uint、uintptr等。

    浮点类型:float32、float64。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.3 类型 25

    1

    2

    3

    4

    5

    9

    6

    7

    8

    复数类型:complex64、complex128。

    字符串:string。

    字符类型:rune。

    错误类型:error。

    此外,Go语言也支持以下这些复合类型:

    指针(pointer)

    数组(array)

    切片(slice)

    字典(map)

    通道(chan)

    结构体(struct)

    接口(interface)

    关于错误类型,我们会在“错误处理”一节中介绍;关于通道,我们会在4.5节中进一步介

    绍;关于结构体和接口,我们则在第3章中进行详细的阐述。

    在这些基础类型之上Go还封装了下面这几种类型:int、uint和uintptr等。这些类型的

    特点在于使用方便,但使用者不能对这些类型的长度做任何假设。对于常规的开发来说,用int

    和uint就可以了,没必要用int8之类明确指定长度的类型,以免导致移植困难。

    2.3.1 布尔类型

    Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和

    false示例代码如下:

    var v1 bool

    v1 = true

    v2 := (1 == 2) v2也会被推导为bool类型

    布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误

    的用法,会导致编译错误:

    var b bool

    b = 1 编译错误

    b = bool(1) 编译错误

    以下的用法才是正确的:

    var b bool

    b = (1!=0) 编译正确

    fmt.Println(Result:, b) 打印结果为Result: true

    2.3.2 整型

    整型是所有编程语言里最基础的数据类型。Go语言支持表2-1所示的这些整型类型。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)26 第 2章 顺序编程

    表 2-1

    类 型 长度(字节) 值 范 围

    int8 1 ?128 ~ 127

    uint8(即byte) 1 0 ~ 255

    int16 2 ?32 768 ~ 32 767

    uint16 2 0 ~ 65 535

    int32 4 ?2 147 483 648 ~ 2 147 483 647

    uint32 4 0 ~ 4 294 967 295

    int64 8 ?9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807

    uint64 8 0 ~ 18 446 744 073 709 551 615

    int 平台相关 平台相关

    uint 平台相关 平台相关

    uintptr 同指针 在32位平台下为4字节,64位平台下为8字节

    1. 类型表示

    需要注意的是,int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你自动

    做类型转换,比如以下的例子会有编译错误:

    var value2 int32

    value1 := 64 value1将会被自动推导为int类型

    value2 = value1 编译错误

    编译错误类似于:

    cannot use value1 (type int) as type int32 in assignment。

    使用强制类型转换可以解决这个编译错误:

    value2 = int32(value1) 编译通过

    当然,开发者在做强制类型转换时,需要注意数据长度被截短而发生的数据精度损失(比如

    将浮点数强制转为整数)和值溢出(值超过转换的目标类型的值范围时)问题。

    2. 数值运算

    Go语言支持下面的常规整数运算:+、?、、和%。加减乘除就不详细解释了,需要说下的

    是,% 和在C语言中一样是求余运算,比如:

    5 % 3 结果为:2

    3. 比较运算

    Go语言支持以下的几种比较运算符:>、<、==、>=、<=和!=。这一点与大多数其他语言相

    同,与C语言完全一致。

    下面为条件判断语句的例子:

    i, j := 1, 2

    if i == j {

    fmt.Println(i and j are equal.)

    }

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.3 类型 27

    1

    2

    3

    4

    5

    9

    6

    7

    8

    两个不同类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但

    各种类型的整型变量都可以直接与字面常量(literal)进行比较,比如:

    var i int32

    var j int64

    i, j = 1, 2

    if i == j { 编译错误

    fmt.Println(i and j are equal.)

    }

    if i == 1 || j == 2 { 编译通过

    fmt.Println(i and j are equal.)

    }

    4. 位运算

    Go语言支持表2-2所示的位运算符。

    表 2-2

    运 算 含 义 样 例

    x << y 左移 124 << 2 结果为496

    x >> y 右移 124 >> 2 结果为31

    x ^ y 异或 124 ^ 2 结果为126

    x y 与 124 2 结果为0

    x | y 或 124 | 2 结果为126

    ^x 取反 ^2 结果为?3

    Go语言的大多数位运算符与C语言都比较类似,除了取反在C语言中是~x,而在Go语言中

    是^x。

    2.3.3 浮点型

    浮点型用于表示包含小数点的数据,比如1.234就是一个浮点型数据。Go语言中的浮点类型

    采用IEEE-754标准的表达方式。

    1. 浮点数表示

    Go语言定义了两个类型float32和float64,其中float32等价于C语言的float类型,float64等价于C语言的double类型。

    在Go语言里,定义一个浮点数变量的代码如下:

    var fvalue1 float32

    fvalue1 = 12

    fvalue2 := 12.0 如果不加小数点,fvalue2会被推导为整型而不是浮点型

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)28 第 2章 顺序编程

    对于以上例子中类型被自动推导的fvalue2,需要注意的是其类型将被自动设为float64,而不管赋给它的数字是否是用32位长度表示的。因此,对于以上的例子,下面的赋值将导致编译

    错误:

    fvalue1 = fvalue2

    而必须使用这样的强制类型转换:

    fvalue1 = float32(fvalue2)

    2. 浮点数比较

    因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等

    是不可行的,这可能会导致不稳定的结果。

    下面是一种推荐的替代方案:

    import math

    p为用户自定义的比较精度,比如0.00001

    func IsEqual(f1, f2, p float64) bool {

    return math.Fdim(f1, f2) < p

    }

    2.3.4 复数类型

    复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示

    虚部(imag)。如果了解了数学上的复数是怎么回事,那么Go语言的复数就非常容易理解了。

    1. 复数表示

    复数表示的示例如下:

    var value1 complex64 由2个float32构成的复数类型

    value1 = 3.2 + 12i

    value2 := 3.2 + 12i value2是complex128类型

    value3 := complex(3.2, 12) value3结果同 value2

    2. 实部与虚部

    对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实

    部,也就是x,通过imag(z)获得该复数的虚部,也就是y。

    更多关于复数的函数,请查阅mathcmplx标准库的文档。

    2.3.5 字符串

    在Go语言中,字符串也是一种基本类型。相比之下, CC++语言中并不存在原生的字符串

    类型,通常使用字符数组来表示,并以字符指针来传递。

    Go语言中字符串的声明和初始化非常简单,举例如下:

    var str string 声明一个字符串变量

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.3 类型 29

    1

    2

    3

    4

    5

    9

    6

    7

    8

    str = Hello world 字符串赋值

    ch := str[0] 取字符串的第一个字符

    fmt.Printf(The length of \%s\ is %d \n, str, len(str))

    fmt.Printf(The first character of \%s\ is %c.\n, str, ch)

    输出结果为:

    The length of Hello world is 11

    The first character of Hello world is H.

    字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始

    化后被修改,比如以下的例子:

    str := Hello world 字符串也支持声明时进行初始化的做法

    str[0] = 'X' 编译错误

    编译器会报类似如下的错误:

    cannot assign to str[0]

    在这个例子中我们使用了一个Go语言内置的函数len来取字符串的长度。这个函数非常有

    用,我们在实际开发过程中处理字符串、数组和切片时将会经常用到。

    本节中我们还顺便示范了Printf函数的用法。有C语言基础的读者会发现,Printf函

    数的用法与C语言运行库中的printf函数如出一辙。读者在以后学习更多的Go语言特性时,可以配合使用Println和Printf来打印各种自己感兴趣的信息,从而让学习过程更加直

    观、有趣。

    Go编译器支持UTF-8的源代码文件格式。这意味着源代码中的字符串可以包含非ANSI的字

    符,比如“Hello world. 你好,世界!”可以出现在Go代码中。但需要注意的是,如果你的Go代

    码需要包含非ANSI字符,保存源文件时请注意编码格式必须选择UTF-8。特别是在Windows下一

    般编辑器都默认存为本地编码,比如中国地区可能是GBK编码而不是UTF-8,如果没注意这点在

    编译和运行时就会出现一些意料之外的情况。

    字符串的编码转换是处理文本文档(比如TXT、XML、HTML等)非常常见的需求,不过可

    惜的是Go语言仅支持UTF-8和Unicode编码。对于其他编码,Go语言标准库并没有内置的编码转

    换支持。不过,所幸的是我们可以很容易基于iconv库用Cgo包装一个。这里有一个开源项目:

    https:github.comxushiweigo-iconv。

    1. 字符串操作

    平时常用的字符串操作如表2-3所示。

    表 2-3

    运 算 含 义 样 例

    x + y 字符串连接 Hello + 123 结果为Hello123

    len(s) 字符串长度 len(Hello) 结果为5

    s[i] 取字符 Hello [1] 结果为'e'

    更多的字符串操作,请参考标准库strings包。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)30 第 2章 顺序编程

    2. 字符串遍历

    Go语言支持两种方式遍历字符串。一种是以字节数组的方式遍历:

    str := Hello,世界

    n := len(str)

    for i := 0; i < n; i++ {

    ch := str[i] 依据下标取字符串中的字符,类型为byte

    fmt.Println(i, ch)

    }

    这个例子的输出结果为:

    0 72

    1 101

    2 108

    3 108

    4 111

    5 44

    6 32

    7 228

    8 184

    9 150

    10 231

    11 149

    12 140

    可以看出,这个字符串长度为13。尽管从直观上来说,这个字符串应该只有9个字符。这是

    因为每个中文字符在UTF-8中占3个字节,而不是1个字节。

    另一种是以Unicode字符遍历:

    str := Hello,世界

    for i, ch := range str {

    fmt.Println(i, ch)ch的类型为rune

    }

    输出结果为:

    0 72

    1 101

    2 108

    3 108

    4 111

    5 44

    6 32

    7 19990

    10 30028

    以Unicode字符方式遍历时,每个字符的类型是rune(早期的Go语言用int类型表示Unicode

    字符),而不是byte。

    2.3.6 字符类型

    在Go语言中支持两个字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.3 类型 31

    1

    2

    3

    4

    5

    9

    6

    7

    8

    的单个字节的值;另一个是rune,代表单个Unicode字符。

    关于rune相关的操作,可查阅Go标准库的unicode包。另外unicodeutf8包也提供了

    UTF8和Unicode之间的转换。

    出于简化语言的考虑,Go语言的多数API都假设字符串为UTF-8编码。尽管Unicode字符在标

    准库中有支持,但实际上较少使用。

    2.3.7 数组

    数组是Go语言编程中最常用的数据结构之一。顾名思义,数组就是指一系列同一类型数据

    的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数

    组的长度。

    以下为一些常规的数组声明方法:

    [32]byte 长度为32的数组,每个元素为一个字节

    [2N] struct { x, y int32 } 复杂类型数组

    [1000]float64 指针数组

    [3][5]int 二维数组

    [2][2][2]float64 等同于[2]([2]([2]float64))

    从以上类型也可以看出,数组可以是多维的,比如[3][5]int就表达了一个3行5列的二维整

    型数组,总共可以存放15个整型元素。

    在Go语言中,数组长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量

    表达式(常量表达式是指在编译期即可计算结果的表达式)。数组的长度是该数组类型的一个内

    置常量,可以用Go语言的内置函数len来获取。下面是一个获取数组arr元素个数的写法:

    arrLength := len(arr)

    1. 元素访问

    可以使用数组下标来访问数组中的元素。与C语言相同,数组下标从0开始,len(array)-1

    则表示最后一个元素的下标。下面的示例遍历整型数组并逐个打印元素内容:

    for i := 0; i < len(array); i++ {

    fmt.Println(Element, i, of array is, array[i])

    }

    Go语言还提供了一个关键字range,用于便捷地遍历容器中的元素。当然,数组也是range

    的支持范围。上面的遍历过程可以简化为如下的写法:

    for i, v := range array {

    fmt.Println(Array element[, i, ]=, v)

    }

    在上面的例子里可以看到,range具有两个返回值,第一个返回值是元素的数组下标,第二

    个返回值是元素的值。

    2. 值类型

    需要特别注意的是,在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)32 第 2章 顺序编程

    和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该

    参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所

    传入数组的一个副本。

    下面用例子来说明这一特点:

    package main

    import fmt

    func modify(array [10]int) {

    array[0] = 10 试图修改数组的第一个元素

    fmt.Println(In modify, array values:, array)

    }

    func main {

    array := [5]int{1,2,3,4,5} 定义并初始化一个数组

    modify(array) 传递给一个函数,并试图在函数体内修改这个数组内容

    fmt.Println(In main, array values:, array)

    }

    该程序的执行结果为:

    In modify, array values: [10 2 3 4 5]

    In main, array values: [1 2 3 4 5]

    从执行结果可以看出, 函数modify内操作的那个数组跟main中传入的数组是两个不同的实

    例。那么,如何才能在函数内操作外部的数据结构呢?我们将在2.3.6节中详细介绍如何用数组切

    片功能来达成这个目标。

    2.3.8 数组切片

    在前一节里我们已经提过数组的特点:数组的长度在定义之后无法再次修改;数组是值类型,每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。

    不用失望,Go语言提供了数组切片(slice)这个非常酷的功能来弥补数组的不足。

    初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是

    个指针。数组切片的数据结构可以抽象为以下3个变量:

    一个指向原生数组的指针;

    数组切片中的元素个数;

    数组切片已分配的存储空间。

    从底层实现的角度来看,数组切片实际上仍然使用数组来管理元素,因此它们之间的关系让

    C++程序员们很容易联想起STL中std::vector和数组的关系。基于数组,数组切片添加了一系

    列管理功能,可以随时动态扩充存放空间,并且可以被随意传递而不会导致所管理的元素被重复

    复制。

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)2.3 类型 33

    1

    2

    3

    4

    5

    9

    6

    7

    8

    1. 创建数组切片

    创建数组切片的方法主要有两种——基于数组和直接创建,下面我们来简要介绍一下这两种

    方法。

    基于数组

    数组切片可以基于一个已存在的数组创建。数组切片可以只使用数组的一部分元素或者整个

    数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。代码清单2-1演示了如何基

    于一个数组的前5个元素创建一个数组切片。

    代码清单2-1 slice.go

    package main

    import fmt

    func main {

    先定义一个数组

    var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    基于数组创建一个数组切片

    var mySlice []int = myArray[:5]

    fmt.Println(Elements of myArray: )

    for _, v := range myArray {

    fmt.Print(v, )

    }

    fmt.Println(\nElements of mySlice: )

    for _, v := range mySlice {

    fmt.Print(v, )

    }

    fmt.Println

    }

    运行结果为:

    Elements of myArray:

    1 2 3 4 5 6 7 8 9 10

    Elements of mySlice:

    1 2 3 4 5

    读者应该已经注意到, Go语言支持用myArray[first:last]这样的方式来基于数组生成一

    个数组切片,而且这个用法还很灵活,比如下面几种都是合法的。

    基于myArray的所有元素创建数组切片:

    mySlice = myArray[:]

    基于myArray的前5个元素创建数组切片:

    mySlice = myArray[:5]

    图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权

    请访问稀酷客(www.ckook.com)34 第 2章 顺序编程

    基于从第5个元素开始的所有元素创建数组切片:

    mySlice = myArray[5:]

    直接创建

    并非一定要事先准备一个数组才能创建数组切片。Go语言提供的内置函数make可以用于

    灵活地创建数组切片。下面的例子示范了直接创建数组切片的各种方法。

    创建一个初始元素个数为5的数组切片,元素初始值为0:

    mySlice1 := make([]int, 5)

    创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:

    mySlice2 := make([]int, 5, 10)

    直接创建并初始化包含5个元素的数组切片:

    mySlice3 := []int{1, 2, 3, 4, 5}

    当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。

    2. 元素遍历

    操作数组元素的所有 ......

您现在查看是摘要介绍页, 详见PDF附件(9160KB,245页)