Java程序员快速入门 go 语言
这篇文章帮助Java程序员快速入门Go语言。
本文将以一个有代表性的例子为开始,以此让Java程序员对Go语言有个初步认识,随后将详细的描述Go语言的各个构成模块,最后以一个例子来讲解Go语言与Java语言的不同点。
先来认识堆栈(例子)
为了满足大家的好奇心,我们将开始于一个麻雀虽小但五脏内全的例子,这个例子将是Stack.java的Go语言版本。
//实现堆栈的包集合 package collection //生成空堆栈 type Stack struct { data []interface{} } //压栈顶 func (s *Stack) Push(x interface{}) { s.data = append(s.data, x) } //弹栈顶,并删除栈顶 //如果是空栈将会引发运行时错误 func (s *Stack) Pop() interface{} { i := len(s.data) - 1 res := s.data[i] s.data[i] = nil // to avoid memory leak s.data = s.data[:i] return res } //返回栈大小 func (s *Stack) Size() int { return len(s.data) }
- 用纯文本在声明语句上方添加注释。
- 申明的名字写在类型的后面。
- 结构体和Java中类相对应,但结构体的成员只能是变量,不能是方法。
- interface{}类型对应Java中的Object。它不仅能代表引用类型,同时也能实现Go语言的所有类型。
- (s *Stack)表示申明一个方法,s对应于Java中方法隐式传的this参数。
- 操作符:=表示申明和初始化一个变量,其类型由初始化语句决定。
下面是是使用collection.Stackabstract数据类型的简单例子。
package collection_test import ( collection "." "fmt" ) func Example() { var s collection.Stack s.Push("world") s.Push("hello, ") for s.Size() > 0 { fmt.Print(s.Pop()) } fmt.Println() // Output: hello, world }将测试包package colloection_test和collectionpackage放在同一目录。第一个import声明“.”表示将要用当前路径中的包并同时命名为collection,“fmt”是标准库的包名,没有重新命名,所以就直接用包名fmt即可。
概念上的不同点
- Go没有类的构造器。Go提供结构体和接口,类的继承以及动态方法查询来进行实例化,而非通过实例方法。接口同样用于Java中的泛型。
- Go中不仅对象和数组可以有指向数据的指针,其它所有类型都有。对于任何类型T,都有一个指针类型*T与之相对应,其表明指向类型为T的值。
- Go允许在任何类型上创建方法;方法的接收者,就是Java中this,可以是个值或是指针。
- 数组在Go中是一些值。当数组作为函数参数传递时,函数将收到数组值的拷贝,不是指针。然而实际中,经常用slices作为参数;因为切片中有数组的引用。
- Go中提供字串类型;字串就像是由比特序列构成的slices,但字串是不可变的。
- 在Go中哈希表叫maps。
- Go提过channels类型来在多线程,goroutines之间通信来实现并发。
- 一般类型(maps,slices,channels)传参是引用,不是值。如果传参map,参数不是对map的值拷贝;如果函数里改变了map的值,函数调用者的map也将改变。这个和Java中map类似。
- Go中访问权限有两种,和Java中public 和包private类似。如果声明的名称首字母大写就是public,否则就是包private。
- Go用error类型取代Java中的异常,诸如访问文件到结尾、运行时刻panic,数组越界等等。
- Go不支持隐式类型转换,必须是显式的。
- Go不支持覆盖。函数名和方法名在同一作用域必须不同。
- Go用nil表示空指针,而Java用null。
语法
声明
声明的语法同Java比较是逆向的。你写上名字然后再跟着写类型。从左至右的类型声明也许读起来容易些。
Go | 约相等的Java写法 |
---|
var v1 int | int v1 = 0; |
var v2 *int | Integer v2 = null; |
var v3 string | String v3 = ""; |
var v4 [10]int | int[] v4 = new int[10]; // v4 is a value in Go. |
var v5 []int | int[] v5 = null; |
var v6 *struct { a int } | C v6 = null; // Given: class C { int a; } |
var v7 map[string]int | HashMap |
var v8 func(a int) int | F v8 = null; // interface F { int f(int a); } |
声明一般是采用一个关键字后面跟着被定义对象的名称,这种形式。关键字是const、type、var或者func其中之一。你也可以使用一个关键字后面跟着放在括号之中的一系列声明,这种形式。
var ( n int x float64 )当声明函数时,你必须为每个参数提供一个名称,或者不为任何参数提供名称;你不能省略一些参数的名称并提供其它参数的名称。你可以用相同的类型来组织几个名字,例如:
func f(i, j, k int, s, t string)一个变量可以在声明它时初始化值。进行此操作时,可以指定变量的类型,但这并非必需的。当不指定类型时,则默认为初始化表达式的类型。
var v9 = *v2
如果一个变量没有明确地初始化,则必须指定其类型。在这种情况下,它将隐式地初始化为其类型的零值(0,nil 等)。在Go语言中没有其它某些语言中的未初始化的变量。
短声明
在函数内,可以用 := 进行短声明,比如:
v10 := v1它等效于:
var v10 = v1
函数类型
在Go语言中,函数是一等公民。Go的函数类型表示有相同的参数和结果类型的所有函数的集合。
type binOp func(int, int) int var op binOp add := func(i, j int) int { return i + j } op = add n = op(100, 200) // n = 100 + 200
多重赋值
Go 允许多重赋值。右边的表达式先被求值然后赋给左边的操作数。
i, j = j, i // 交换 i 和 j 的值(不用象传统语言中需要第三个临时变量).
函数可以有多个返回值,用参数括号后面的一个括号中的列表表示。返回值通过赋予一个变量列表来存储,如:
func f() (i int, pj *int) { ... } v1, v2 = f()
空标识符
空标识符用下划线字符表示,它提供了一种方法来忽略由多值表达式返回的某个值,如:
v1, _ = f() // 忽略f()返回的第二个值.
分号和格式
不需要担心分号和格式,你可以用 gofmt程序创建一个标准的Go样式。虽然这个样式最初看起来或许有点古怪,但它同任何其它样式一样的好,而且熟悉以后会感觉越来越舒服。
在实践中Go代码使用分号不多。严格说,所有的Go语句都是由分号终止。不过,Go在非空白行的末尾隐式插入一个分号,除非该行明显未结束。这带来的影响是,在某些情况下Go不允许换行符。例如,你不能这么写
func g() { // INVALID; "{" should be on previous line. }
g()后面会插入一个分号,使它成为一个函数声明,而不是函数定义。类似的,你也不能写
if n == 0 { } else { // INVALID; "else {" should be on previous line. }
}后面else前面会插入一个分号,导致语法错误。
条件语句
Go 不使用括号来包裹 if 语句中的条件,和 for 语句中的表达式, 以及 switch 语句中的值。但另一方面,它必须用花括号来包裹 if 或 for 的执行语句体。
if a < b { f() } if (a < b) { f() } // 不需要括号. if (a < b) f() // 非法(执行体未用花括号括起来) for i = 0; i < 10; i++ {} for (i = 0; i < 10; i++) {} // 非法(表达式不需要括起来)此外,if 和 switch 接受一个可选的初始化语句,通常用它建立一个局部变量,如:
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
For 语句
Go 不具有 while 语句,也没有 do-while 语句。可以赋以for语句一个单一条件,使其与while语句等效。完全省略条件会制造一个无限循环。
for语句可能包含一个范围条件,对strings, arrays, slices, maps, 或 channels进行迭代。除了这么写
for i := 0; i < len(a); i++ { ... }
要循环遍历a的元素,我们也可以这么写
for i, v := range 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)语句来使得它们可以向下传递。
switch n { case 0: // empty case body case 1: f() // f is not called when n == 0. }
但是一个case能够有多个值:
switch n { case 0, 1: f() // f is called if n == 0 || n == 1. }
case后面的值可以是支持相等比较(equality comparison)操作符的任何类型,比如string或者pointer。缺省的switch表达式等效于true表达式:
switch { case n < 0: f1() case n == 0: f2() default: f3() }
++ 和 -- 语句
++ 和 --只能被用作语句中的后缀操作符,而不能用在表达式中。例如,你不能再这样写了: n = i++。
defer 语句
defer语句用来调用一个函数,但将其执行延迟到上一个附近的函数返回之后的时刻。被延迟的函数的执行与附近函数返回采取的路径无关。然而,当defer语句执行时,被延迟函数的参数已经被计算并保存以供之后使用。
f, err := os.Open("filename") defer f.Close() // 当上个函数返回后,f 会被关闭.
常量
在 Go 中常量可以是无类型的。这适用于数值常量,只使用无类型常量的表达式,以及没有给出类型和初始化表达式是无类型的常量声明。当一个无类型的常量被用于一个需要类型化的值的环境中,它的值会转换成类型化的。所以即使 Go 没有隐式的类型转换,这也能允许常量被相对自由地使用。
var a uint f(a + 1) // 无类型的数字常量 1 成为 uint 类型. f(a + 1e3) // 1e3 也作为 uint 类型.语言在无类型的数值常数的大小上不强加任何限制。 只有当一个常数被用在需要一个有类型的值时才会根据该类型对大小有所限制,如:
const huge = 1 << 100 var n int = huge >> 98
如果在一个类型声明中类型关键字缺失,而相关的表达式计算出来是一个非类型(untyped)的数字常量,这个常量就会被分别转换成rune、int、float64、或者complex128类型,取决于这个值是否是一个字符(character)、整形(integer)、浮点型(float-point)还是复杂型(complex)的常量。
c := 'å' // rune (alias for int32) n := 1 + 2 // int x := 2.7 // float64 z := 1 + 2i // complex128
Go没有枚举(enumerate)类型。取而代之,你可以在一个单独的常量(const)声明中使用特殊的名字iota来获得一系列增长的值。当一个初始化表达简化成一个常量时,它就会重用前面的表达式。
const ( red = iota // red == 0 blue // blue == 1 green // green == 2 )
结构
结构对应于 Java 中的类,但结构的成员不能是方法,只能是变量。结构指针类似 Java 中的引用变量。与 Java 的类不同,结构也可以被定义为直接值。对于结构和结构指针都可以使用“.”来访问结构中的成员,如:
type MyStruct struct { s string n int64 } var x MyStruct // x 被初始化为 MyStruct{"", 0}. var px *MyStruct // 指针 px 初始化为 nil. px = new(MyStruct) // px 指向新结构体 MyStruct{"", 0}. x.s = "Foo" px.s = "Bar"
在 Go 中,方法可以与任意命名类型有关,不只与结构; 参考方法和接口的讨论.
指针
如果你有一个整数或一个结构或一个数组,赋值会复制对象的内容。Go 使用指针来实现 Java 的引用变量的效果。对于任意类型T,有一个相应的指针类型*T,表示指向类型T的值。
要为一个指针变量分配存储空间,要使用内置函数 new,它接受一个类型并返回一个指向已分配存储空间的指针。分配的空间将根据类型进行零初始化。 例如,new(int)为一个新int分配存储空间,初始值为0,并返回它的地址,类型为*int。
在Java代码 T p = new T()中,T是一个带有两个int类型实体变量a和b的类,对应于:
type T struct { a, b int } var p *T = new(T)
或者更地道的:
p := new(T)
声明语句 var v T 声明了一个装着类型T的值的变量,在Java中没有相匹配的语句。值也可以使用一种复合语法来创建或者初始化,例如:
v := T{1, 2}
等效于:
var v T v.a = 1 v.b = 2
对于类型T的一个操作数x,寻址操作符 &x 提供x的地址,它是类型*T的一个值。例如:
p := &T{1, 2} // p has type *T
对于指针类型的一个操作数x,指针指向(pointer indirection)通过x用*x表示所指向的值。指针指向是很少被用到的;而Go像Java一样,能够自动获取到一个变量的地址:
p := new(T) p.a = 1 // equivalent to (*p).a = 1
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。其数值的容量是原数值的长度。
var s[]int var a[10]int s=a[:]//s=a[0:len(a)]的简写
如果创建了一个数[100]byte(100bytes的数组,也许用作缓存区),并且想将它传递给一个函数,那么可以将函数的参数设置为[]byte类型,这样就会传递一个slice。slice也可以通过make函数来创建(如下有描述)。
和Java中的ArrayList用法一样,slice也内建append函数。
s0:=[]int{1,2} s1:=append(s0,3)//添加一个单元素 s2:=append(s1,4,5)//添加多元素 s3:=append(s2,s0...)//添加sliceslice也能用于string,它将返回子字符串。
初始化
Map和channel的值必须用内建的函数make来申请值。例如,用
make(map[string]int)将得到一个类型为map[string]int类型的值。于用new不同,make将返回对象值而不是地址。这样就于map和channel为引用类型就保持一致了。
对于map,make函数提供一个隐含可选的第二个参数。对于channel,也有第二个可选参数,它是用来设置channel缓冲区的大小;默认是0(没有缓冲区)。
make函数也能为slice来申请值。那样将申请一个隐藏在slice里数组,而返回的是指向它的slice引用。此时用make需要一个slice元素个数的参数。第二个可选参数是slice的容量。
m:=make([]int,10,20)//于new([20]int)[:10]相同
方法和接口
方法
方法像是一个普通的函数定义,除非它有一个接收器(receiver)。接收器类似于Java实体方法中的this引用。
type MyType struct { i int } func (p *MyType) Get() int { return p.i } var pm = new(MyType) var n = pm.Get()
这里定义了一个同MyType联系起来的Get方法。名为p的接收器在函数体之中。
方法被定义在有命名的类型中。如果转换成不同类型的值,新的值将会拥有新类型的方法,而不是原有类型的那些方法。
你也许会在一个内建的类型中定义方法,通过声明一个继承自它的新命名的类型。新的类型同内建的类型是不同的。
type MyInt int func (p MyInt) Get() int { return int(p) // The conversion is required. } func f(i int) {} var v MyInt v = v * v // The operators of the underlying type still apply. f(int(v)) // int(v) has no defined methods. f(v) // INVALID
接口
Go的接口同Java的接口类似,但任何提供了用一个Go接口命名的方法的类型,都可以被看做是对那个接口的实现。不需要额外的声明了。
给定下面这个接口:
type MyInterface interface { Get() int Set(i int) }由于MyType已经有了一个Get方法,我们能够让Mytype满足这个接口的要求,通过添加:
func (p *MyType) Set(i int) { p.i = i }现在任何使用MyInterface作为参数的函数,将可以接受 MyType类型的变量:
func GetAndSet(x MyInterface) {} func f1() { var p MyType GetAndSet(&p) }在Java中,为MyType类型定义Set和Get,就会让MyType自动实现了MyInterface。一个类型可以满足多个接口。这是一种鸭式类型(duck typing)的形式。
当我看到一只鸟儿走起路来像鸭子,游起来也像鸭子,呱呱的叫起来也像鸭子,我就会把这只鸟儿称作鸭子.James Whitcomb Riley 匿名域
匿名域和Java中的子类类似.
type MySubType struct { MyType j int } func (pMySubType) Get() int { p.j++ return p.MyType.Get() }</pre>MySubType是作为MyType的子类。
func f2() { var p MySubType GetAndSet(&p) }Set方法继承于MyType,在闭合类型中的匿名域的方法可以提升为闭合类型的方法。也就是说,MyType做为MySubType的匿名域,MyType中的方法MySubType都能用。Get方法是覆盖方法,Set方法是继承来的。
匿名域和也不是和Java中的子类完全一样。当调用匿名域的方法,方法的接收者是匿名域而不是闭合类型。也就是说,匿名域的方法不会动态分发。如果想实现Java中的动态方法looup,就等用interface。
func f3() { var v MyInterface v = new(MyType) v.Get() // Call the Get method for *MyType. v = new(MySubType) v.Get() // Call the Get method for *MySubType. }
类型断言
用类型断言可以将变量从一个接口类型转变为不同的接口类型。这是在运行时动态实现的。与Java不同,不需要对两个接口之间的关系作任何声明,如:
type Printer interface { Print() } func f4(x MyInterface) { x.(Printer).Print() // 类型断言为 Printer }
转换到Printer完全是动态的。 只要x的动态类型(存储在x中的值的实际类型)定义了一个Print方法,它就会工作。
泛型Go 没有泛型类型,但通过结合匿名字段和类型断言可以实现类似于Java的参数化的类型,如:
type StringStack struct { Stack } func (s *StringStack) Push(n string) { s.Stack.Push(n) } func (s *StringStack) Pop() string { return s.Stack.Pop().(string) }
StringStack限定Hello stack例子中的泛型Stack,所以它只操作字符串元素——就像Java中的Stack
错误
Java经常使用异常,Go则有两种机制。大多数程序只有真正不能回收的情况下返回错误,比如超出范围的索引,产生一个运行时异常。
Go的多值返回使得返回一个详细的错误消息和正常的返回值十分容易。按照惯例,这些消息有类型错误,一个简单的内置接口。
type error interface { Error() string }举个例子,如果打开文件失败,os.Open函数返回一个非空错误值。
func Open(name string) (file *File, err error)下面的代码使用os.Open来打开一个文件。如果遇到错误就会调用log.Fatal来打印错误消息并终止。
f, err := os.Open("filename.ext") if err != nil { log.Fatal(err) } // do something with the open *File f
错误接口只需要一个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协程像协程一样并存在操作系统的多个线程中。你不必去拘泥于这些细节。
go list.Sort() // Run list.Sort in parallel; don’t wait for it.Go 拥有函数字面量,可以表现为闭包函数,与go 声明一起使用的话,功能将更为强大.
// 发布后打印文本到标准输出,直到给定的时间过期。 func Publish(text string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(text) }() // 注意括号,我们必须调用该函数. }
变量text和delay在外部函数和闭包(函数字面量)之间共享,只存在于在它们可访问期间。
Channels
channel 通过传递特定元素类型的值,提供了一套两个 goroutines 同步执行及交流的机制。 <- 操作符制定 channel 发送或接收的方向。如果没有明确方向,则通道为双向的。
chan Sushi // can be used to send and receive values of type Sushi chan<- float64 // can only be used to send float64s <-chan int // can only be used to receive ints
Channel 为引用类型,使用 make 分配。
ic := make(chan int) // unbuffered channel of ints wc := make(chan *Work, 10) // buffered channel of pointers to Work
要向通道传递值,以二进制操作符的方式使用 <- 。要接收数据,则以一元运算符的方式使用它。
ic <- 3 // Send 3 on the channel. work := <-wc // Receive a pointer to Work from the channel.
如若 channel 无缓冲区,则发送者堵塞,直到接收者接受到传值。如果 channel 有缓冲区,发送者堵塞,直到接收者开始读取缓冲区;如果缓冲区满了,则需要等到某些接收者开始检索值。接收者堵塞,直到有数据可接收。
close 函数将记录通道不能再被用来发送数据。当调用 close 函数后,在所有先前发送的值都被接收以后,接收操作将不会堵塞,同时返回 0 值。一个多值的接收操作将能够获取到 channel 是否被关闭的指示。
ch := make(chan string) go func() { ch <- "Hello!" close(ch) }() fmt.Println(<-ch) // Print "Hello!". fmt.Println(<-ch) // Print the zero value "" without blocking. fmt.Println(<-ch) // Once again print "". v, ok := <-ch // v is "", ok is false.
在下面的例子,我们将使 Publish 函数返回一个通道,它将被用来在文本发表完成后广播消息
// Publish prints text to stdout after the given time has expired. // It closes the wait channel when the text has been published. func Publish(text string, delay time.Duration) (wait <-chan struct{}) { ch := make(chan struct{}) go func() { time.Sleep(delay) fmt.Println(text) close(ch) }() return ch }
下面就是 Publish 函数的大概用法
wait := Publish("important news", 2 * time.Minute) // Do some more work. <-wait // blocks until the text has been published
Select语句
select语句是Go统一工具箱中的最终工具。它选择哪些通信将被处理。如果任何的通信都能处理,那么就会随机选其一,与之对应的语句就会执行。另外,如果没有默认的case,语句就会阻塞直到其中一个通信完成为止。
这里有一个toy的例子,展示了select语句如何用来实现一个随机数发生器。
rand := make(chan int) for { // Send random sequence of bits to rand. select { case rand <- 0: // note: no statement case rand <- 1: } }更加现实的是,这里有一个select语句可以用来设置时间限制一个接受操作。
select { case news := <-AFP: fmt.Println(news) case <-time.After(time.Minute): fmt.Println("Time out: no news in one minute.") }
time.After函数式标准库的一部分;它等待一段特定时间后发送当前时间到返回的频道。
并发(示例)
最后我们通过一个小而全的例子展示如何将若干块拼凑在一起。它是一个服务器通过一个频道(channel)接受Work请求(Work request)的草案代码。每一个请求都使用一个单独的渠道进行处理。Work将自身构造包含进一个用来返回结果的频道中。
package server import "log" // New creates a new server that accepts Work requests // through the req channel. func New() (req chan<- *Work) { wc := make(chan *Work) go serve(wc) return wc } type Work struct { Op func(int, int) int A, B int Reply chan int // Server sends result on this channel. } func serve(wc <-chan *Work) { for w := range wc { go safelyDo(w) } } func safelyDo(w *Work) { // Regain control of panicking goroutine to avoid // killing the other executing goroutines. defer func() { if err := recover(); err != nil { log.Println("work failed:", err) } }() do(w) } func do(w *Work) { w.Reply <- w.Op(w.A, w.B) }而下面这是你如何使用它:
package server_test import ( server "." "fmt" "time" ) func main() { s := server.New() divideByZero := &server.Work{ Op: func(a, b int) int { return a / b }, A: 100, B: 0, Reply: make(chan int), } s <- divideByZero select { case res := <-divideByZero.Reply: fmt.Println(res) case <-time.After(time.Second): fmt.Println("No result in one second.") } // Output: No result in one second. }并发是一个庞大的话题,而Go的方法和Java的方法时相当不同的。有两篇文章涉及到了这些基础:
并发编程基础(Fundamentals of concurrent programming )使用Go编写的小例子来介绍并发。
通过交流共享内存(Share Memory by Communicating )使用更大幅度的例子进行代码走读(codewalk)。