这篇文章帮助Java程序员快速入门Go语言。 转载至 开源中国社区。 http://www.oschina.net 本文将以一个有代表性的例子为开始,以此让Java程序员对Go语言有个初步认识,随后将详细的描述Go语言的各个构成模块,最后以一个例子来讲解Go语言与Java语言的不同点。 |
先来认识堆栈(例子)为了满足大家的好奇心,我们将开始于一个麻雀虽小但五脏内全的例子,这个例子将是Stack.java的Go语言版本。
|
|
概念上的不同点
|
语法 |
当声明函数时,你必须为每个参数提供一个名称,或者不为任何参数提供名称;你不能省略一些参数的名称并提供其它参数的名称。你可以用相同的类型来组织几个名字,例如:
如果一个变量没有明确地初始化,则必须指定其类型。在这种情况下,它将隐式地初始化为其类型的零值(0,nil 等)。在Go语言中没有其它某些语言中的未初始化的变量。 |
短声明在函数内,可以用 := 进行短声明,比如:
函数类型在Go语言中,函数是一等公民。Go的函数类型表示有相同的参数和结果类型的所有函数的集合。
|
多重赋值Go 允许多重赋值。右边的表达式先被求值然后赋给左边的操作数。
函数可以有多个返回值,用参数括号后面的一个括号中的列表表示。返回值通过赋予一个变量列表来存储,如:
空标识符空标识符用下划线字符表示,它提供了一种方法来忽略由多值表达式返回的某个值,如:
|
分号和格式不需要担心分号和格式,你可以用 gofmt程序创建一个标准的Go样式。虽然这个样式最初看起来或许有点古怪,但它同任何其它样式一样的好,而且熟悉以后会感觉越来越舒服。 在实践中Go代码使用分号不多。严格说,所有的Go语句都是由分号终止。不过,Go在非空白行的末尾隐式插入一个分号,除非该行明显未结束。这带来的影响是,在某些情况下Go不允许换行符。例如,你不能这么写
g()后面会插入一个分号,使它成为一个函数声明,而不是函数定义。类似的,你也不能写
}后面else前面会插入一个分号,导致语法错误。 |
条件语句Go 不使用括号来包裹 if 语句中的条件,和 for 语句中的表达式, 以及 switch 语句中的值。但另一方面,它必须用花括号来包裹 if 或 for 的执行语句体。
|
For 语句Go 不具有 while 语句,也没有 do-while 语句。可以赋以for语句一个单一条件,使其与while语句等效。完全省略条件会制造一个无限循环。 for语句可能包含一个范围条件,对strings, arrays, slices, maps, 或 channels进行迭代。除了这么写
要循环遍历a的元素,我们也可以这么写
这会将i作为索引,给v赋以一个array, slice, 或 string中的连续元素。对于字符串,i是对单一字节的索引,v是一个符文类型的Unicode代码点(符文是int32的一个别名)。对maps迭代会产生键值对,而对channels则只会产生一个迭代值。 |
Break和Continue 像Java一样,Go允许使用break和continue来指定标签(label),但是这个标签所引用的必须是一个for、switch或者select语句。 Switch语句在一个switch语句中,case标签默认不会往下传递(fall through,也就是在没有break的情况下也不会执行后续case的程序),但是你能够通过使用一个向下传递(fallthrough)语句来使得它们可以向下传递。
但是一个case能够有多个值:
case后面的值可以是支持相等比较(equality comparison)操作符的任何类型,比如string或者pointer。缺省的switch表达式等效于true表达式:
|
++ 和 -- 语句++ 和 --只能被用作语句中的后缀操作符,而不能用在表达式中。例如,你不能再这样写了: n = i++。 defer 语句defer语句用来调用一个函数,但将其执行延迟到上一个附近的函数返回之后的时刻。被延迟的函数的执行与附近函数返回采取的路径无关。然而,当defer语句执行时,被延迟函数的参数已经被计算并保存以供之后使用。
|
常量在 Go 中常量可以是无类型的。这适用于数值常量,只使用无类型常量的表达式,以及没有给出类型和初始化表达式是无类型的常量声明。当一个无类型的常量被用于一个需要类型化的值的环境中,它的值会转换成类型化的。所以即使 Go 没有隐式的类型转换,这也能允许常量被相对自由地使用。
|
如果在一个类型声明中类型关键字缺失,而相关的表达式计算出来是一个非类型(untyped)的数字常量,这个常量就会被分别转换成rune、int、float64、或者complex128类型,取决于这个值是否是一个字符(character)、整形(integer)、浮点型(float-point)还是复杂型(complex)的常量。
Go没有枚举(enumerate)类型。取而代之,你可以在一个单独的常量(const)声明中使用特殊的名字iota来获得一系列增长的值。当一个初始化表达简化成一个常量时,它就会重用前面的表达式。
|
结构结构对应于 Java 中的类,但结构的成员不能是方法,只能是变量。结构指针类似 Java 中的引用变量。与 Java 的类不同,结构也可以被定义为直接值。对于结构和结构指针都可以使用“.”来访问结构中的成员,如:
在 Go 中,方法可以与任意命名类型有关,不只与结构; 参考方法和接口的讨论. |
在Java代码 T p = new T()中,T是一个带有两个int类型实体变量a和b的类,对应于:
或者更地道的:
声明语句 var v T 声明了一个装着类型T的值的变量,在Java中没有相匹配的语句。值也可以使用一种复合语法来创建或者初始化,例如:
等效于:
对于类型T的一个操作数x,寻址操作符 &x 提供x的地址,它是类型*T的一个值。例如:
对于指针类型的一个操作数x,指针指向(pointer indirection)通过x用*x表示所指向的值。指针指向是很少被用到的;而Go像Java一样,能够自动获取到一个变量的地址:
|
Slices 一个slice是一个含有3个域的结构体:一个指向数组的指针,一个长度,一个容量大小。Slices可以用[]来访问数组元素。内置的len函数返回slice的长度,cap函数返回容量大小。 创建一个新的slice,可以用给定数组或slice a,通过a[i:j]的方式创建。新创建的slice是对a的引用,并且内容用是从a内容的索引的i到索引j。它的长度是j-i。如果i缺省,其slice开始于0,j缺省表示len(a)。新的slice是原来a的引用,如果改变了新slice里元素的值,a也会改变。新slice的容量是a的容量减去i。其数值的容量是原数值的长度。
如果创建了一个数[100]byte(100bytes的数组,也许用作缓存区),并且想将它传递给一个函数,那么可以将函数的参数设置为[]byte类型,这样就会传递一个slice。slice也可以通过make函数来创建(如下有描述)。 和Java中的ArrayList用法一样,slice也内建append函数。
|
初始化Map和channel的值必须用内建的函数make来申请值。例如,用
对于map,make函数提供一个隐含可选的第二个参数。对于channel,也有第二个可选参数,它是用来设置channel缓冲区的大小;默认是0(没有缓冲区)。 make函数也能为slice来申请值。那样将申请一个隐藏在slice里数组,而返回的是指向它的slice引用。此时用make需要一个slice元素个数的参数。第二个可选参数是slice的容量。
|
方法和接口 方法方法像是一个普通的函数定义,除非它有一个接收器(receiver)。接收器类似于Java实体方法中的this引用。
这里定义了一个同MyType联系起来的Get方法。名为p的接收器在函数体之中。 方法被定义在有命名的类型中。如果转换成不同类型的值,新的值将会拥有新类型的方法,而不是原有类型的那些方法。 你也许会在一个内建的类型中定义方法,通过声明一个继承自它的新命名的类型。新的类型同内建的类型是不同的。
|
接口 Go的接口同Java的接口类似,但任何提供了用一个Go接口命名的方法的类型,都可以被看做是对那个接口的实现。不需要额外的声明了。 给定下面这个接口:
当我看到一只鸟儿走起路来像鸭子,游起来也像鸭子,呱呱的叫起来也像鸭子,我就会把这只鸟儿称作鸭子.James Whitcomb Riley |
匿名域
匿名域和Java中的子类类似.
匿名域和也不是和Java中的子类完全一样。当调用匿名域的方法,方法的接收者是匿名域而不是闭合类型。也就是说,匿名域的方法不会动态分发。如果想实现Java中的动态方法looup,就等用interface。
|
|||||||||||||||||||||||||||||||||||||||||||||||
类型断言用类型断言可以将变量从一个接口类型转变为不同的接口类型。这是在运行时动态实现的。与Java不同,不需要对两个接口之间的关系作任何声明,如:
转换到Printer完全是动态的。 只要x的动态类型(存储在x中的值的实际类型)定义了一个Print方法,它就会工作。 |
泛型
Go 没有泛型类型,但通过结合匿名字段和类型断言可以实现类似于Java的参数化的类型,如:
StringStack限定Hello stack例子中的泛型Stack,所以它只操作字符串元素——就像Java中的Stack<String> 。注意,Sizemethod继承于Stack。 |
错误Java经常使用异常,Go则有两种机制。大多数程序只有真正不能回收的情况下返回错误,比如超出范围的索引,产生一个运行时异常。 Go的多值返回使得返回一个详细的错误消息和正常的返回值十分容易。按照惯例,这些消息有类型错误,一个简单的内置接口。
错误接口只需要一个Error方法,但是特定的错误实现往往会有附加的方法,允许调用者检查详细的错误。 |
Panic和recover一个panic是一个运行时刻错误,并释放Go程序的堆栈,同时运行defer程序,最终程序停止。Panic和Java中的异常相似,但它仅仅表明是运行时错误,比如空指针或是数组越界。Go用内建错误类型来描述如访问文件结尾等上述错误信息。内建函数recover可用在panic并内恢复Go程序运行,同时recover能停止循环返回参数传递给panic。因为在defer函数里只能运行循环代码,同时revcover只能运行defer函数中。如果Go程序没有panic,recover将返回nil。 |
Go协程和信道 Go协程Go中的线程,使用go声明,执行一个goroutine.并且在不同的,新创建的goroutine中运行该函数.在一个程序中所有的Go协程,共用相同的地址空间. Go协程是轻量级的,消耗成本只比分配的栈空间多一点, 栈开始时较小并通过堆存储的分配和释放来实现其增长。内部的Go协程像协程一样并存在操作系统的多个线程中。你不必去拘泥于这些细节。
变量text和delay在外部函数和闭包(函数字面量)之间共享,只存在于在它们可访问期间。 |
|
Channelschannel 通过传递特定元素类型的值,提供了一套两个 goroutines 同步执行及交流的机制。 <- 操作符制定 channel 发送或接收的方向。如果没有明确方向,则通道为双向的。
Channel 为引用类型,使用 make 分配。
要向通道传递值,以二进制操作符的方式使用 <- 。要接收数据,则以一元运算符的方式使用它。
如若 channel 无缓冲区,则发送者堵塞,直到接收者接受到传值。如果 channel 有缓冲区,发送者堵塞,直到接收者开始读取缓冲区;如果缓冲区满了,则需要等到某些接收者开始检索值。接收者堵塞,直到有数据可接收。 close 函数将记录通道不能再被用来发送数据。当调用 close 函数后,在所有先前发送的值都被接收以后,接收操作将不会堵塞,同时返回 0 值。一个多值的接收操作将能够获取到 channel 是否被关闭的指示。
在下面的例子,我们将使 Publish 函数返回一个通道,它将被用来在文本发表完成后广播消息
下面就是 Publish 函数的大概用法
|
Select语句select语句是Go统一工具箱中的最终工具。它选择哪些通信将被处理。如果任何的通信都能处理,那么就会随机选其一,与之对应的语句就会执行。另外,如果没有默认的case,语句就会阻塞直到其中一个通信完成为止。 这里有一个toy的例子,展示了select语句如何用来实现一个随机数发生器。
time.After函数式标准库的一部分;它等待一段特定时间后发送当前时间到返回的频道。 |
并发(示例) 最后我们通过一个小而全的例子展示如何将若干块拼凑在一起。它是一个服务器通过一个频道(channel)接受Work请求(Work request)的草案代码。每一个请求都使用一个单独的渠道进行处理。Work将自身构造包含进一个用来返回结果的频道中。
并发编程基础(Fundamentals of concurrent programming )使用Go编写的小例子来介绍并发。 通过交流共享内存(Share Memory by Communicating )使用更大幅度的例子进行代码走读(codewalk)。 |