目录
本章将简要介绍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项目原班人马加入了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.org/wiki/Ken_Thompson):设计了B语言 和C语言,创建了Unix和Plan 9操作系统,1983年图灵奖得主,Go语言的共同作者。
- 罗布·派克(Rob Pike,http://en.wikipedia.org/wiki/Rob_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.org/wiki/Brad_Fitzpatrick): LiveJournal的创始人,著名开源项目memcached的作者
1.2 语言特性
Go语言作为一门全新的静态类型开发语言,与当前的开发语言相比具备众多令人兴奋不已 的新特性。本书从第2章开始,我们将对Go语言的各个方面进行详细解析,让读者能够尽量轻松 地掌握这门简洁、有趣却又超级强大的新语言。
下面是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出现的一个重要原因就是为了解决内存管理的难题。但是事实证明,这些工具和方法虽然能够在一定程度上辅助开发者,但并没法让开发者避免通宵调试这样又苦又累的工作。
到目前为止,内存泄露的最佳解决方案是在语言级别引入自动垃圾回收算法(Garbage Collection,简称GC)。所谓垃圾回收,即所有的内存分配动作都会被在运行时记录,同时任何对 该内存的使用也都会被记录,然后垃圾回收器会对所有已经分配的内存进行跟踪监测,一旦发现 有些内存已经不再被任何人使用,就阶段性地回收这些没人用的内存。当然,因为需要尽量最小 化垃圾回收的性能损耗,以及降低对正常程序执行过程的影响,现实中的垃圾回收算法要比这个 复杂得多,比如为对象增加年龄属性等,但基本原理都是如此。
自动垃圾回收在C/C++社区一直作为一柄双刃剑看待,虽然到C++0x(后命名为C++11)正 式发布时,这个呼声颇高的特性总算是被加入了,但按C++之父的说法,由于C++本身过于强大, 导致在C++中支持垃圾收集变成了一个困难的工作。假如C++支持垃圾收集,以下的代码片段在 运行时就会是一个严峻的考验:
int* p = new int;
p += 10; // 对指针进行了偏移,因此那块内存不再被引用
// …… 这里可能会发生针对这块int内存的垃圾收集 ……
p -= 10; // 咦,居然又偏移到原来的位置
*p = 10; // 如果有垃圾收集,这里就无法保证可以正常运行了
微软的C++/CLI算是用一种偏门的方式让C++程序员们有机会品尝一下垃圾回收功能的鲜美 味道。在C/C++之后出现的新语言,比如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++开发者有效地消除了反复 写以下几行代码的工作量:
#include <vector>
#include <map>
#include <algorithm>
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(
/*out*/char* first_name,
/*out*/char* middle_name,
/*out*/char* last_name,
/*out*/char* 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语言革命性地在静态开发语言阵营中率先提供了多返回值功能。这个特性让开发者可以 从原来用各种比较别扭的方式返回多个值的痛苦中解脱出来,既不用再区分参数列表中哪几个用 于输入,哪几个用于输出,也不用再只为了返回多个值而专门定义一个数据结构。
在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
}
并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。而函数的调 用相比C/C++语言要简化很多:
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
}
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() {
// 以鸟的方式飞行
}
我们在实现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进行并行的累加计算,待这两个计算过程都完成后打印计算结果,
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语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量 依赖于反射功能来实现。
例子:
package main
import (
"fmt"
"reflect"
)
type Bird struct {
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
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函数。
package main
/*
#include <stdio.h>
*/
import "C"
import "unsafe"
func main() {
cstr := C.CString("Hello, world")
C.puts(cstr)
C.free(unsafe.Pointer(cstr))
}
1.3 第一个 Go 程序
自Kernighan和Ritchie合著的《C程序设计语言》(The C Programming Language)出版以来, 几乎所有的编程书都以一个Hello world小例子作为开始。我们也不免俗(或者说尊重传统),下 面我们从一个简单Go语言版本的Hello world来初窥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。
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.com/p/go/downloads/list。
在*nix环境中,Go默认会被安装到/usr/local/go目录中。安装包在安装完成后会自动添加执行 文件目录到系统路径中
安装完成后,请重新启动命令行程序,然后运行以下命令以验证Go是否已经正确安装:
$ go version
go version go1
如果该命令能够正常运行并输出相应的信息,说明Go编译环境已经正确安装完毕。如果提 示找不到go命令,可以通过手动添加/usr/local/go/bin到PATH环境变量来解决。
1.3.3 编译程序
假设之前介绍的Hello, world代码被保存为了hello.go,并位于~/goyard目录下,那么可以用以 下命令行编译并直接运行该程序:
$ cd ~/goyard
$ go run hello.go # 直接运行
Hello, world. 你好,世界!
使用这个命令,会将编译、链接和运行3个步骤合并为一步,运行完后在当前目录下也看不 到任何中间文件和最终的可执行文件。如果要只生成编译结果而不自动运行,我们也可以使用 Go 命令行工具的build命令:
$ 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,集成性做得很好;
- Vim/Emacs,万能开发工具;
- LiteIDE,一款专为Go语言开发的集成开发环境。
由于Go代码的轻巧和模块化特征,其实一般的文本编辑工具就可以胜任Go开发工作。本书 的所有代码均使用Linux上的gedit工具完成。
Go社区提供了各种文本编辑器的语法高亮设置方法,这在本书最后一章也有所介绍
1.5 工程管理
在实际的开发工作中,直接调用编译器进行编译和链接的场景是少而又少,因为在工程中不 会简单到只有一个源代码文件,且源文件之间会有相互的依赖关系。如果这样一个文件一个文件 逐步编译,那不亚于一场灾难。Go语言的设计者作为行业老将,自然不会忽略这一点。早期Go 语言使用makefile作为临时方案,到了Go 1发布时引入了强大无比的Go命令行工具。
Go命令行工具的革命性之处在于彻底消除了工程文件的概念,完全用目录结构和包名来推 导工程结构和构建顺序。针对只有一个源文件的情况讨论工程管理看起来会比较多余,因为这可 以直接用go run和go build搞定。下面我们将用一个更接近现实的虚拟项目来展示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
我们假设这个工程被分割为两个部分:
1.可执行程序,名为calc,内部只包含一个calc.go文件;
2. 算法库,名为simplemath,每个command对应于一个同名的go文件,比如add.go。
则一个正常的工程目录组织应该如下所示:
<calcproj>
├─<src>
├─<calc>
├─calc.go
├─<simplemath>
├─add.go
├─add_test.go
├─sqrt.go
├─sqrt_test.go
├─<bin>
├─<pkg>#包将被安装到此处
在上面的结构里,带尖括号的名字表示其为目录。xxx_test.go表示的是一个对于xxx.go的单元 测试,这也是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
}
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 <integer1><integer2>")
return
}
v1, err1 := strconv.Atoi(args[1])
v2, err2 := strconv.Atoi(args[2])
if err1 != nil || err2 != nil {
fmt.Println("USAGE: calc add <integer1><integer2>")
return
}
ret := simplemath.Add(v1, v2)
fmt.Println("Result: ", ret)
case "sqrt":
if len(args) != 2 {
fmt.Println("USAGE: calc sqrt <integer>")
return
}
v, err := strconv.Atoi(args[1])
if err != nil {
fmt.Println("USAGE: calc sqrt <integer>")
return
}
ret := simplemath.Sqrt(v)
fmt.Println("Result: ", ret)
default:
Usage()
}
}
// add.go
package simplemath
func Add(a int, b int) int {
return a + b
}
// 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)
}
}
// sqrt.go
package simplemath
import "math"
func Sqrt(i int) int {
v := math.Sqrt(float64(i))
return int(v)
}
// 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=~/goyard/calcproj
然后执行以下命令应用该设置:
$ source ~/.bashrc
GOPATH和PATH环境变量一样,也可以接受多个路径,并且路径和路径之间用冒号分割。
设置完GOPATH后,现在我们开始构建工程。假设我们希望把生成的可执行文件放到 calcproj/bin目录中,需要执行的一系列指令如下:
$ cd ~/goyard/calcproj
$ 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
打印的错误信息非常简洁,却已经足够让开发者快速定位到问题代码所在的文件和行数,从 而在最短的时间内确认是单元测试的问题还是程序的问题。
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语言社区。在遇到问题时,请随时访问这些社区,并勇敢地提问,相信你能得到满意的解决方法。
1.7.1 邮件列表
邮件列表是Go语言最活跃的社区之一,而且与其他语言社区不同的是,在这里你可以很频 繁地看到好多Go语言的核心开发成员(比如Ross Cox)亲自回答问题,其权威程度和对学习Go 语言的价值显而易见。
Go邮件组的地址为http://groups.google.com/group/golang-nuts 。该邮件列表对所有人公开,你 可以在这个页面上直接加入。该邮件列表的沟通语言为英语。根据我们的经验,在该邮件列表上 提出的问题通常在24小时内可以得到解决。
Go的中文邮件组为http://groups.google.com/group/golang-china。如果你更习惯中文讨论环境, 可以参与。另外,尽管http://groups.google.com/group/ecug不是以Go语言为专题,但有关Go语言 的服务端开发,也是它最重要的话题之一。
1.8 小结
本章我们简要介绍了Go语言的起源和背景,并结合若干代码示例简要介绍了我们认为最值 得关注的关键特性,之后按老规矩以Hello, world这个例子作为起点帮助读者快速熟悉这门新语 言,消除对Go语言的陌生感,并搭建好自己的Go开发环境。 通过这一章的学习,我们相信读者对于Go语言的简单易学特性已经有了比较直接的了解。 在后续的章节中,各位读者可以利用在本章中搭建的开发环境和学习的工程管理知识,快速动手 尝试各种Go语言令人兴奋的语言功能。