Go 语言是什么?
Google 最近发布新型的编程语言, Go 。它被设计为将现代编程语言的先进 性带入到目前仍由 C 语言占统治地位的系统层面。然而,这一语言仍在试验阶段并在不断演变。
Go 语言的设计者计划设计一门简单、高效、安全和 并发的语言。这门语言简单到甚至不需要有一个符号表来进行词法分析。它可以快速地编译;整个工程的编译时间在秒以下的情况是常事。它具备垃圾回收功能,因 此从内存的角度是安全的。它进行静态类型检查,并且不允许强制类型转换,因而对于类型而言是安全的。同时语言还内建了强大的并发实现机制。
阅读 Go
Go 的语法传承了与 C 一样的风格。程序由函数组成,而函数体是一系列的语句序列。一段代码块用花括号括起来。这门语言保留有限的关键字。表达式使用 同样的中缀运算符。语法上并无 太多出奇之处。
Go 语言的作者在设计这一语言时坚持一个单一的指导原则:简单明了至上。一些新的语法构件提供了简明地表达一些约定俗成的概 念的方式,相较之下用 C 表达显得冗长。而其他方面则是针对几十年的使用所呈现出来的一些不合理的语言选择作出了改进。
变量声明
变量是如下声明的:
var sum int // <span face="Courier New"> 简单声明 </span> var total int = 42 //<span face="Courier New"> 声明并初始化 </span>
最值得注意的是,这些声明里的类型跟在变量名的后面。乍一看有点怪,但这更清晰明了。比如,以下面这个 C 片段来说:
int* a, b;
它并明了,但这里实际的意思是 a 是一个指针,但 b 不是。如果要将两者都声明为指针,必须要重复星号。然后在 Go 语言里,通过如下方式可以将两者都 声明为指针:
var a, b *int
如果一个变量初始化了,编译器通常能推断它的类型,所以程序员不必显式的敲出来:
var label = "name"
然而,在这种情况下 var 几乎显得是多余了。因此,Go 的作者引入了一个新的运算符来 声明和初始化一个新的变量:
name := "Samuel"
条件语句
Go 语言当中的条件句与 C 当中所熟知的 if-else 构造一样,但条件不需要被打包在括号内。这样可以减少阅读代码时的视觉上的混乱。
括号并不是唯一被移去的视觉干扰。在条件之间可以包括一个简单的语句,所以如下的代码:
result := someFunc(); if result > 0 { /* Do something */ } else { /* Handle error */ }
可以被精简成:
if result := someFunc(); result > 0 { /* Do something */ } else { /* Handle error */ }
然而,在后面这个例子当中,result 只在条件块内部有效——而前者 中,它在整个包含它的上下文中都是可存取的。
分支语句
分支语句同样是似曾相识,但也有增强。像条件语句一样,它允许一个简单的语句位于分支的表达式之前。然而,他们相对于在 C 语言中的分支而言走得更远。
首先,为了让分支跳转更简明,作了两个修改。情况可以是逗号分隔的列表,而 fall-throuth 也不再是默认的行为。
因此,如下的 C 代码:
int result; switch (byte) { case 'a': case 'b': { result = 1 break } default: result = 0 }
在 Go 里就变成了这样:
var result int switch byte { case 'a', 'b': result = 1 default: result = 0 }
第二点,Go 的分支跳转可以匹配比整数和字符更多的内容,任何有效的表达式都可以作为跳转语句值。只要它与分支条件的类型是一样的。
因此如下的 C 代码:
int result = calculate(); if (result < 0) { /* negative */ } else if (result > 0) { /* positive */ } else { /* zero */ }
在 Go 里可以这样表达:
switch result := calculate(); true { case result < 0: /* negative */ case result > 0: /* positive */ default: /* zero */ }
这些都是公共的约定俗成,比如如果分支值省略了,就是默认为真,所以上面的代码可以这样写:
switch result := calculate(); { case result < 0: /* negative */ case result > 0: /* positive */ default: /* zero */ }
循环
Go 只有一个关键字用于引入循环。但它提供了除 do-while 外 C 语言当中所有可用的循环方式。
条件
for a > b { /* ... */ }
初始,条件和步进
for i := 0; i < 10; i++ { /* ... */ }
范围
range 语句右边的表达式必须是 array , slice , string 或者 map , 或是指向 array 的指针,也可以是 channel 。
for i := range "hello" { /* ... */ }
无限循环
for { /* ever */ }
函数
声明函数的语法与 C 不同。就像变量声明一样,类型是在它们所描述的术语之后声明的。在 C 语言中:
int add(int a, b) { return a + b }
在 Go 里面是这样描述的:
func add(a, b int) int { return a + b }
多返回值
在 C 语言当中常见的做法是保留一个返回值来表示错误 (比如,read() 返回 0),或 者保留返回值来通知状态,并将传递存储结果的内存地址的指针。这容易产生了不安全的编程实践,因此在像 Go 语言这样有良好管理的语言中是不可行的。
认识到这一问题的影响已超出了函数结果与错误通讯的简单需求的范畴,Go 的作者们在语言中内建了函数返回多个值的能力。
作为例子,这个函数将返回整数除法的两个部分:
func divide(a, b int) (int, int) { quotient := a / b remainder := a % b return quotient, remainder }
有了多个返回值,有良好的代码文档会更好——而 Go 允许你给返回值命名,就像参数一样。你可以对这些返回的变量赋值,就像其它的变量一样。所以我们可以重写 divide:
func divide(a, b int) (quotient, remainder int) { quotient = a / b remainder = a % b return }
多返回值的出现促进了"comma-ok"的模式。有可能失败的函数可以返回第二个布尔结果来表示成功。作为替代,也可以返回一个错误对象,因此像下面这样的代码也就不见怪了:
if result, ok := moreMagic(); ok { /* Do something with result */ }
匿名函数
有了垃圾收集器意味着为许多不同的特性敞开了大门——其中就包括匿名函数。Go 为声明匿名函数提供了简单的语法。像许多动态语言一样,这些函数在它们被定义的范围内创建了词法闭包。
考虑如下的程序:
func makeAdder(x int) (func(int) int) { return func(y int) int { return x + y } } func main() { add5 := makeAdder(5) add36 := makeAdder(36) fmt.Println("The answer:", add5(add36(1))) //=> The answer: 42 }
基本类型
像 C 语言一样,Go 提供了一系列的基本类型,常见的布尔,整数和浮点数类型都具备。它有一个 Unicode 的字符串类型和数组类型。同时该语言还引入了两 种新的类型: slice 和 map 。
数组和切片
Go 语言当中的数组不是像 C 语言那样动态的。它们的大小是类型的一部分,在编译时就决定了。数组的索引还是使用的熟悉的 C 语法 (如 a[i]),并且与 C 一样,索引是由 0 开始的。编译器提供了内建的功能在编译时求得一个数组的长度 (如 len(a))。如果试图超过数组界限写入,会产生一个运行时错误。
Go 还提供了切片(slices),作为数组的变形。一个切片 (slice) 表示一个数组内的连续分段,支持程序员指定底层存储的明确部分。构建一个切片 的语法与访问一个数组元素类似:
/* Construct a slice on ary that starts at s and is len elements long */ s1 := ary[s:len] /* Omit the length to create a slice to the end of ary */ s2 := ary[s:] /* Slices behave just like arrays */ s[0] == ary[s] //=> true // Changing the value in a slice changes it in the array ary[s] = 1 s[0] = 42 ary[s] == 42 //=> true
该切片所引用的数组分段可以通过将新的切片赋值给同一变量来更改:
/* Move the start of the slice forward by one, but do not move the end */ s2 = s2[1:] /* Slices can only move forward */ s2 = s2[-1:] // this is a compile error
切片的长度可以更改,只要不超出切片的容量。切片 s 的容量是数组从 s[0] 到数组尾端的大小,并由内建的 cap() 函数返回。一个切片的长度永远不能超出它的容量。
这里有一个展示长度和容量交互的例子:
a := [...]int{1,2,3,4,5} // The ... means "whatever length the initializer has" len(a) //=> 5 /* Slice from the middle */ s := a[2:4] //=> [3 4] len(s), cap(s) //=> 2, 3 /* Grow the slice */ s = s[0:3] //=> [3 4 5] len(s), cap(s) //=> 3, 3 /* Cannot grow it past its capacity */ s = s[0:4] // this is a compile error
通常,一个切片就是一个程序所需要的全部了,在这种情况下,程序员根本用不着一个数组,Go 有两种方式直接创建切片而不用引用底层存储:
/* literal */ s1 := []int{1,2,3,4,5} /* empty (all zero values) */ s2 := make([]int, 10) // cap(s2) == len(s2) == 10
Map 类型
几乎每个现在流行的动态语言都有的数据类型,但在 C 中不具备的,就是 dictionary。Go 提供了一个基本的 dictionary 类型叫做 map。下 面的例子展示了如何创建和使用 Go map:
m := make(map[string] int) // A mapping of strings to ints /* Store some values */ m["foo"] = 42 m["bar"] = 30 /* Read, and exit program with a runtime error if key is not present. */ x := m["foo"] /* Read, with comma-ok check; ok will be false if key was not present. */ x, ok := m["bar"] /* Check for presence of key, _ means "I don't care about this value." */ _, ok := m["baz"] // ok == false /* Assign zero as a valid value */ m["foo"] = 0; _, ok := m["foo"] // ok == true /* Delete a key */ m["bar"] = 0, false _, ok := m["bar"] // ok == false
面向对象
Go 语言支持类似于 C 语言中使用的面向对象风格。数据被组织成 structs,然后定义操作这些 structs 的函数。类似于 Python,Go 语言提供 了定义函数并调用它们的方式,因此语法并不会笨拙。
Struct 类型
定义一个新的 struct 类型很简单:
type Point struct { x, y float64 }
现在这一类型的值可以通过内建的函数 new 来分配,这将返回一个指针,指向一块内存单元,其所占内存槽初始化为零。
var p *Point = new(Point) p.x = 3 p.y = 4
这显得很冗长,而 Go 语言的一个目标是尽可能的简明扼要。所以提供了一个同时分配和初始化 struct 的语法:
var p1 Point = Point{3,4} // Value var p2 *Point = &Point{3,4} // Pointer
方法
一旦声明了类型,就可以将该类型显式的作为第一个参数来声明函数:
func (self Point) Length() float { return math.Sqrt(self.x*self.x + self.y*self.y); }
这些函数之后可作为 struct 的方法而被调用:
p := Point{3,4} d := p.Length() //=> 5
方法实际上既可以声明为值也可以声明为指针类型。Go 将会适当的处理引用或解引用对象,所以既可以对类型 T,也可以对类型 *T 声明方式,并合理地使用它们。
让我们为 Point 扩展一个变换器:
/* Note the receiver is *Point */ func (self *Point) Scale(factor float64) { self.x = self.x * factor self.y = self.y * factor }
然后我们可以像这样调用:
p.Scale(2); d = p.Length() //=> 10
很重要的一点是理解传递给 MoveToXY 的 self 和其它的参数一样,并且是值传递,而不是引用传递。如果它被声明为 Point,那么在方法内修改的 struct 就不再跟调用方的一样——值在它们传递给方法的时候被 拷贝,并在调用结束后被丢弃。
接口
像 Ruby 这样的动态语言所强调面向对象编程的风格认为对象的行为比哪种对象是动态类型( duck typing )更为重要。Go 所 带来的一个最强大的特性之一就是提供了可以在编程时运用动态类型的思想而把行为定义的合法性检查的工作推到编译时。这一行为的名字被称作接口。
定义一个接口很简单:
type Writer interface { Write(p []byte) (n int, err os.Error) }
这里定义了一个接口和一个写字节缓冲的方法。任何实现了这一方法的对象也实现了这一接口。不需要像 Java 一样进行声明,编译器能推断出来。这既给予了动态类型的表达能力又保留了静态类型检查的安全。
Go 当中接口的运作方式支持开发者在编写程序的时候发现程序的类型。如果几个对象间存在公共行为,而开发者想要抽象这种行为,那么它就可以创建一个接口并使用它。
考虑如下的代码:
// Somewhere in some code: type Widget struct {} func (Widget) Frob() { /* do something */ } // Somewhere else in the code: type Sprocket struct {} func (Sprocket) Frob() { /* do something else */ } /* New code, and we want to take both Widgets and Sprockets and Frob them */ type Frobber interface { Frob() } func frobtastic(f Frobber) { f.Frob() }
需要特别指出的很重要的一点就是所有的对象都实现了这个空接口:
interface {}
继承
Go 语言不支持继承,至少与大多数语言的继承不一样。并不存在类型的层次结构。相较于继承,Go 鼓励使用组合和委派,并为此提供了相应的语法甜点使其更容易接受。
有了这样的定义:
type Engine interface { Start() Stop() } type Car struct { Engine }
于是我可以像下面这样编写:
func GoToWorkIn(c Car) { /* get in car */ c.Start(); /* drive to work */ c.Stop(); /* get out of car */ }
当我声明 Car 这个 struct 的时候,我定义了一个匿名成员。这是一 个只能被其类型识别的成员。匿名成员与其它的成员一样,并有着和类型一样的名字。因此我还可以写成 c.Engine.Start()。 如果 Car 并没有其自身方法可以满足调用的话, 编译器自动的会将在 Car 上的调用委派给它的 Engine 上面的方法。
由匿名成员提供的分离方法的规则是保守的。如果为一个类型定义了一个方法,就使用它。如果不是,就使用为匿名成员定义的方法。如果有两个匿名成员都提供一 个方法,编译器将会报错,但只在该方法被调用的情况下。
这种组合是通过委派来实现的,而不是继承。一旦匿名成员的方法被调用,控制流整个都被委派给了该方法。所以你无法做到和下面的例子一样来模拟类型层次:
type Base struct {} func (Base) Magic() { fmt.Print("base magic") } func (self Base) MoreMagic() { self.Magic() self.Magic() } type Foo struct { Base } func (Foo) Magic() { fmt.Print("foo magic") }
当你创建一个 Foo 对象时,它将会影响 Base 的两个方法。然而,当你调用 MoreMagic 时, 你将得不到期望的结果:
f := new(Foo) f.Magic() //=> foo magic f.MoreMagic() //=> base magic base magic
并发
Go 的作者选择了消息传递模型来作为推荐的并发编程方法。该语言同样支持共享内存,然后作者自有道理:
不要通过共享内存来通信,相反,通过通信来共享内存。
该语言提供了两个基本的构件来支持这一范型: goroutines 和 channels 。
Go 例程
Goroutine 是轻量级的并行程序执行路径,与线程,coroutine 或者进程类似。然而,它们彼此相当不同,因此 Go 作者决定给它一个新的名字并 放弃其它术语可能隐含的意义。
创建一个 goroutine 来运行名为 DoThis 的函数十分简单:
go DoThis() // but do not wait for it to complete
匿名的函数可以这样使用:
go func() { for { /* do something forever */ } }() // Note that the function must be invoked
这些 goroutine 将会通过 Go 运行时而映射到适当的操作系统原语(比如,POSIX 线程)。
通道类型
有了 goroutine,代码的并行执行就容易了。然而,它们之间仍然需要通讯机制。Channel 提供一个 FIFO 通信队列刚好能达到这一目的。
以下是使用 channel 的语法:
/* Creating a channel uses make(), not new - it was also used for map creation */ ch := make(chan int) /* Sending a value blocks until the value is read */ ch <- 4 /* Reading a value blocks until a value is available */ i := <-ch
举例来说,如果我们想要进行长时间运行的数值计算,我们可以这样做:
ch := make(chan int) go func() { result := 0 for i := 0; i < 100000000; i++ { result = result + i } ch <- result }() /* Do something for a while */ sum := <-ch // This will block if the calculation is not done yet fmt.Println("The sum is:", sum)
channel 的阻塞行为并非永远是最佳的。该语言提供了两种对其进行定制的方式:
- 程序员可以指定缓冲大小——想缓冲的 channel 发送消息不会阻塞,除非缓冲已满,同样从缓冲的 channel 读取也不会阻塞,除非缓冲是空的。
- 该语言同时还提供了不会被阻塞的发送和接收的能力,而操作成功是仍然要报告。
/* Create a channel with buffer size 5 */ ch := make(chan int, 5) /* Send without blocking, ok will be true if value was buffered */ ok := ch <- 42 /* Read without blocking, ok will be true if a value was read */ val, ok := <-ch
包
Go 提供了一种简单的机制来组织代码:包。每个文件开头都会声明它属于哪一个包,每个文件也可以引入它所用到的包。任何首字母大写的名字是由包导出的,并可以被其它的包所使用。
以下是一个完整的源文件:
package geometry import "math" /* Point is capitalized, so it is visible outside the package. */ type Point struct { /* the fields are not capitalized, so they are not visible outside of the package */ x, y float64 } /* These functions are visible outside of the package */ func (self Point) Length() float64 { /* This uses a function in the math package */ return math.Sqrt(self.x*self.x + self.y*self.y) } func (self *Point) Scale(factor float64) { self.setX(self.x * factor) self.setY(self.y * factor) } /* These functions are not visible outside of the package, but can be used inside the package */ func (self *Point) setX(x float64) { self.x = x } func (self *Point) setY(y float64) { self.y = y }
缺失
Go 语言的作者试图将代码的清晰明确作为设计该语言作出所有决定的指导思想。第二个目标是生产一个编译速度很快的语言。有了这两个标准作为方向,来 自其它语言的许多特性就不那么适合了。许多程序员会发现他们最爱的语言特性在 Go 当中不存在,确实,有很多人也许会觉得 Go 语言由于缺乏其它语言所共有的 一些特性,还不太可用。
这当中两个缺失的特性就是异常和泛型,两者在其它语言当中都是非常有用的。而它们目前都不是 Go 的一分子。但因为该 语言仍处于试验阶段,它们有可能最终会加入到语言里。然而,如果将 Go 与其它语言作比较的话,我们应当记住 Go 是打算在系统编程层面作为 C 语言的替代。明 白这一点的话,那么缺失的这许多特性倒也不是很大的问题了。
最后,因为这一语言才刚刚发布,因此它没有什么类库或工具可以用,也没有 Go 语 言的集成编程环境。Go 语言标准库有些有用的代码,但这与更为成熟的语言比 起来仍还是很少的。
查看英文原文: Google Go: A Primer 。
感谢马国耀对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论