8第1章初识GO语言我们在实现Bird类型时完全没有任何IF1y的信息。我们可以在另外一个地方定义这个IF1y接口:type Irly interface(Fly()1这两者自前看起来完全没有关系,现在看看我们如何使用它们:func main()(var flyIFlynew(Bird)fly.Fly()0可以看出,虽然Bird类型实现的时候,没有声明与接口IF1y的关系,但接口和类型可以直接转换,甚至接口的定义都不用在类型定义之前,这种比较松散的对应关系可以大幅降低因为接口调整而导致的大量代码调整工作。1.2.7并发编程Go语言引人了goroutine概念,它使得并发编程变得非常简单。通过使用goroutine而不是裸用操作系统的并发机制,以及使用消息传递来共享内存而不是使用共享内存来通信,Go语言让并发编程变得更加轻盈和安全。通过在函数调用前使用关键字go,我们即可让该函数以goroutine方式执行。goroutine是一种比线程更加轻盈、更省资源的协程。Go语言通过系统的线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。Go语言实现了CSP(通信顺序进程,CommunicatingSequentialProcess)模型来作为goroutine间的推荐通信方式。在CSP模型中,一个并发系统由若干并行运行的顺序进程组成,每个进程不能对其他进程的变量赋值。进程之间只能通过一对通信原语实现协作。Go语言用channel(通道)这个概念来轻巧地实现了CSP模型。channel的使用方式比较接近Unix系统中的管道(pipe)概念,可以方便地进行跨goroutine的通信。另外,由于一个进程内创建的所有goroutine运行在同一个内存地址空间中,因此如果不同的goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写锁。Go语言标准库中的sync包提供了完备的读写锁功能。下面我们用一个简单的例子来演示goroutine和channel的使用方式。这是一个并行计算的例子,由两个goroutine进行并行的累加计算,待这两个计算过程都完成后打印计算结果,具体如代码清单1-1所示。图灵社区会员soooldier(soooldier@live.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) 专享 尊重版权
1.2语言特性9代码清单1-1paracalc.gopackage mainimport"fmt"func sum(values[] int,resultchan chanint)(sum:= 0for -, value := range values sum += valueresultChan<-sum//将计算结果发送到channel中Tfunc main()fvalues:=[]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:j,resultchan)suml,sum2:=<-resultchan,<-resultChan//接收结果fmt.Println("Result:,suml,sum2,sumi+sum2)11.2.83反射反射(reflection)是在Java语言出现后迅速流行起来的一种概念。通过反射,你可以获取对象类型的详细信息,并可动态操作对象。反射是把双刃剑,功能强大但代码可读性并不理想。若非必要,我们并不推荐使用反射。Go语言的反射实现了反射的大部分功能,但没有像Java语言那样内置类型工厂,故而无法做到像Java那样通过类型字符串创建对象实例。在Java中,你可以读取配置并根据类型名称创建对应的类型,这是一种常见的编程手法,但在Go语言中这并不被推荐。反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal&Unmarshal)。例如,Go语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量依赖于反射功能来实现。这里先举一个小例子,可以利用反射功能列出某个类型中所有成员变量的值,如代码清单1-2所示。代码清单1-2reflect.gopackage mainimport("fmt""reflect')type Bird struct(图灵社区会员soooldier(soooldier@live.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语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量 依赖于反射功能来实现。 这里先举一个小例子,可以利用反射功能列出某个类型中所有成员变量的值,如代码清单1-2 所示。 代码清单1-2 reflect.go package main import ( "fmt" "reflect" ) type Bird struct { 图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权
第1章初识Go语言10Name stringLifeExpectance int0func(b*Bird)Fly()(fmt.Println("I am flying..."))func main()(sparrow:=&Bird("sparrow",3)s:=reflect.Valueof(sparrow).Elem()typeofT :=s.Type()for i :=O;i <s.NumField; i++(f := s.Field(i)fmt.Printf("gd:&s&s=vin",i,typeofT.Field(i).Name,f.Type()f.Interface())10该程序的输出结果为:O: Name string = Sparrowl:LifeExpectanceint -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-3cprint.gopackage main/*#include<stdio.h>*/import"c"import "unsafe"func main() (C.cstring("Hello,world")cstrc.puts(cstr)C.free(unsafe.Pointer(cstr))1我们将在第9章中详细介绍Cgo的用法。图灵社区会员soooldier(soooldier@live.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 <stdio.h> */ 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) 专享 尊重版权
1.3第一个Go程序111.3第一个Go程序自Kernighan和Ritchie合著的《C程序设计语言》(TheCProgrammingLanguage)出版以来,几乎所有的编程书都以一个Helloworld小例子作为开始。我们也不免俗(或者说尊重传统),下面我们从一个简单Go语言版本的Helloworld来初窥Go这门新语言的模样,如代码清单1-4所示。代码清单1-4hello.gopackage mainimport"fmt"1/我们需要使用fmt包中的Println()函数func main()(fmt.Println("Hello,world.你好,世界!")11.3.1代码解读每个Go源代码文件的开头都是一个package声明,表示该Go代码所属的包。包是Go语言里最基本的分发单位,也是工程管理中依赖关系的体现。要生成Go可执行程序,必须建立一个名字为main的包,并且在该包中包含一个叫main()的函数(该函数是Go可执行程序的执行起点)。Go语言的main()函数不能带参数,也不能定义返回值。命令行传人的参数在os.Args变量中保存。如果需要支持命令行开关,可使用f1ag包。在本书后面我们将解释如何使用f1ag包来做命令行参数规范的定义,以及获取和解析命令行参数。在包声明之后,是一系列的import语句,用于导入该程序所依赖的包。由于本示例程序用到了Print1n()函数,所以需要导人该函数所属的fmt包。有一点需要注意,不得包含在源代码文件中没有用到的包,否则Go编译器会报编译错误。这与下面提到的强制左花括号(的放置位置以及之后会提到的函数名的大小写规则,均体现了G语言在语言层面解决软件工程问题的设计哲学。所有Go函数(包括在对象编程中会提到的类型成员函数)以关键字func开头。一个常规的函数定义包含以下部分:func西数名(参数列表)(返回值列表)(//函数体7对应的一个实例如下:func Compute(valuel int, value2 float64)(result float64,err error)(//函数体JGo支持多个返回值。以上的示例函数compute()返回了两个值,一个叫result,另一个是err。并不是所有返回值都必须赋值。在函数返回时没有被明确赋值的返回值都会被设置为默认值,比如result会被设为o.0,err会被设为nil。图灵社区会员soooldier(soooldier@live.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) 专享 尊重版权
12第1章初识Go语言Go程序的代码注释与C++保持一致,即同时支持以下两种用法:/+块注释*///行注释相信熟悉C和C++的读者也发现了另外一点,即在这段Go示例代码里没有出现分号。Go程序并不要求开发者在每个语句后面加上分号表示语句结束,这是与C和C++的一个明显不同之处。有些读者可能会自然地把左花括号(另起一行放置,这样做的结果是Go编译器报告编译错误,这点需要特别注意:syntax error:unexpected semicolon or newline before(1.3.2编译环境准备前面我们给大家大概介绍了第一个Go程序的基本结构,接下来我们来准备编译这段小程序的环境。在Go1发布之前,开发者要想使用Go,只能自行下载代码并进行编译,而现在可以直接下载对应的安装包进行安装,安装包的下载地址为http://code.google.com/p/go/downloads/list。在*nix环境中,Go默认会被安装到/usr/local/go目录中。安装包在安装完成后会自动添加执行文件目录到系统路径中。安装完成后,请重新启动命令行程序,然后运行以下命令以验证Go是否已经正确安装:$ go versiongo version gol如果该命令能够正常运行并输出相应的信息,说明Go编译环境已经正确安装完毕。如果提示找不到go命令,可以通过手动添加/usr/local/go/bin到PATH环境变量来解决。1.3.3编译程序假设之前介绍的Hello,world代码被保存为了hello.go,并位于~/goyard目录下,那么可以用以下命令行编译并直接运行该程序:$ cd~/goyard$gorunhello.go#直接运行Hello,world,你好,世界!使用这个命令,会将编译、链接和运行3个步骤合并为一步,运行完后在当前目录下也看不到任何中间文件和最终的可执行文件。如果要只生成编译结果而不自动运行,我们也可以使用GC命令行工具的build命令:图灵社区会员soooldier(soooldier@live.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.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命令: 图灵社区会员 soooldier(soooldier@live.com) 专享 尊重版权